1use std::sync::{Arc, Mutex as StdMutex};2use std::time::{Duration, Instant};34use iroh::Endpoint;5use serde::Serialize;6use sigil_protocol::{7 Capability, INPUT_ALPN_V1, InputEvent, InvitationGrants, PointerPosition,8 RELATIVE_POINTER_DELTA_MAX, RELATIVE_POINTER_DELTA_MIN, read_input_ack, write_input_event,9};10use tauri::ipc::Channel;1112use crate::commands::state::AppState;13use crate::media::network_diagnostics::{NetworkSessionDiagnostics, lock_network_diagnostics};14use crate::media::transport::{NegotiatedV1Stream, negotiate_v1};15use crate::platform_capabilities::relative_pointer_capture_enabled;1617pub(crate) const CLIENT_INPUT_QUEUE_CAPACITY: usize = 256;181920pub(crate) struct InputAvailability {21 pub(crate) relative_pointer: bool,22 pub(crate) pointer_position_feedback: bool,23 pub(crate) absolute_pointer: bool,24 pub(crate) keyboard: bool,25 pub(crate) text: bool,26 pub(crate) gamepad: bool,27 pub(crate) input_ack: bool,28 pub(crate) control: bool,29}3031impl InputAvailability {32 fn from_capabilities(capabilities: &[Capability]) -> Self {33 let relative_pointer = capabilities.contains(&Capability::RelativePointer);34 let pointer_position_feedback = capabilities.contains(&Capability::PointerPositionFeedback);35 let absolute_pointer = capabilities.contains(&Capability::AbsolutePointer);36 let keyboard = capabilities.contains(&Capability::Keyboard);37 let text = capabilities.contains(&Capability::Text);38 let gamepad = capabilities.contains(&Capability::Gamepad);39 let input_ack = capabilities.contains(&Capability::InputAck);40 Self {41 relative_pointer,42 pointer_position_feedback,43 absolute_pointer,44 keyboard,45 text,46 gamepad,47 input_ack,48 control: relative_pointer || absolute_pointer || keyboard || text || gamepad,49 }50 }51}52535455pub enum PointerFeedbackPayload {56 Position {57 sequence: u64,58 position: Option<PointerPosition>,59 60 pointer_visible: Option<bool>,61 },62 Terminal {63 reason: PointerFeedbackTerminalReason,64 },65}66676869pub enum PointerFeedbackTerminalReason {70 Eof,71 Malformed,72}737475struct RelativePointerAccumulator {76 dx: i64,77 dy: i64,78}7980impl RelativePointerAccumulator {81 fn push(&mut self, dx: i32, dy: i32) {82 self.dx = self.dx.saturating_add(i64::from(dx));83 self.dy = self.dy.saturating_add(i64::from(dy));84 }8586 fn take(&mut self) -> Option<InputEvent> {87 if self.dx == 0 && self.dy == 0 {88 return None;89 }90 let dx = self.dx.clamp(91 i64::from(RELATIVE_POINTER_DELTA_MIN),92 i64::from(RELATIVE_POINTER_DELTA_MAX),93 ) as i32;94 let dy = self.dy.clamp(95 i64::from(RELATIVE_POINTER_DELTA_MIN),96 i64::from(RELATIVE_POINTER_DELTA_MAX),97 ) as i32;98 let event = InputEvent::MouseMoveRelative { dx, dy };99 self.dx -= i64::from(dx);100 self.dy -= i64::from(dy);101 Some(event)102 }103104 fn is_pending(&self) -> bool {105 self.dx != 0 || self.dy != 0106 }107}108109fn stage_relative_input(110 pending: &mut RelativePointerAccumulator,111 event: InputEvent,112) -> Option<InputEvent> {113 match event {114 InputEvent::MouseMoveRelative { dx, dy } => {115 pending.push(dx, dy);116 None117 }118 event => Some(event),119 }120}121122async fn open_negotiated_input_stream(123 endpoint: &Endpoint,124 address: &iroh::EndpointAddr,125 nonce: [u8; 16],126 capabilities: Vec<Capability>,127) -> Result<128 (129 iroh::endpoint::Connection,130 iroh::endpoint::SendStream,131 iroh::endpoint::RecvStream,132 NegotiatedV1Stream,133 ),134 String,135> {136 let connection = endpoint137 .connect(address.clone(), INPUT_ALPN_V1)138 .await139 .map_err(|error| format!("Failed to connect input stream: {error}"))?;140 let (mut send, mut recv) = connection141 .open_bi()142 .await143 .map_err(|error| format!("Failed to open input stream: {error}"))?;144 let negotiation = negotiate_v1(145 &mut send,146 &mut recv,147 nonce,148 capabilities,149 None,150 "input",151 None,152 )153 .await?;154 Ok((connection, send, recv, negotiation))155}156157fn input_capability_offers_for(158 grants: InvitationGrants,159 relative_pointer_capture: bool,160) -> Vec<Vec<Capability>> {161 // Keep the relative compatibility ladder ordered from newest to oldest.162 // When local relative capture is unavailable, deliberately collapse it to163 // only the inherited absolute-pointer offer.164 let mut base = Vec::with_capacity(if relative_pointer_capture { 4 } else { 1 });165 if relative_pointer_capture {166 base.extend([167 vec![168 Capability::RelativePointer,169 Capability::PointerPositionFeedback,170 Capability::PointerVisibilityFeedback,171 Capability::AbsolutePointer,172 Capability::Keyboard,173 Capability::Text,174 Capability::Gamepad,175 ],176 vec![177 Capability::RelativePointer,178 Capability::PointerPositionFeedback,179 Capability::AbsolutePointer,180 Capability::Keyboard,181 Capability::Text,182 Capability::Gamepad,183 ],184 vec![185 Capability::RelativePointer,186 Capability::AbsolutePointer,187 Capability::Keyboard,188 Capability::Text,189 Capability::Gamepad,190 ],191 ]);192 }193 base.push(vec![194 Capability::AbsolutePointer,195 Capability::Keyboard,196 Capability::Text,197 Capability::Gamepad,198 ]);199 let has_input_grant = grants.contains(InvitationGrants::POINTER_KEYBOARD)200 || grants.contains(InvitationGrants::GAMEPAD);201 let mut offers = Vec::with_capacity(base.len() * 2);202 if has_input_grant {203 for mut offer in base.clone() {204 offer.push(Capability::InputAck);205 offers.push(offer);206 }207 }208 offers.extend(base);209 for offer in &mut offers {210 offer.retain(|capability| match capability {211 Capability::Gamepad => grants.contains(InvitationGrants::GAMEPAD),212 Capability::AbsolutePointer213 | Capability::RelativePointer214 | Capability::Keyboard215 | Capability::Text216 | Capability::PointerPositionFeedback217 | Capability::PointerVisibilityFeedback => {218 grants.contains(InvitationGrants::POINTER_KEYBOARD)219 }220 Capability::InputAck => has_input_grant,221 _ => true,222 });223 }224 offers.dedup();225 offers226}227228fn input_capability_offers(grants: InvitationGrants) -> Vec<Vec<Capability>> {229 input_capability_offers_for(grants, relative_pointer_capture_enabled())230}231232fn mask_unavailable_relative_pointer_capabilities(233 mut capabilities: Vec<Capability>,234 relative_pointer_capture: bool,235) -> Vec<Capability> {236 if !relative_pointer_capture {237 capabilities.retain(|capability| {238 !matches!(239 capability,240 Capability::RelativePointer241 | Capability::PointerPositionFeedback242 | Capability::PointerVisibilityFeedback243 )244 });245 }246 capabilities247}248249fn input_event_allowed(capabilities: &[Capability], event: &InputEvent) -> bool {250 match event {251 InputEvent::Probe => capabilities.contains(&Capability::InputAck),252 InputEvent::MouseMove { .. } => capabilities.contains(&Capability::AbsolutePointer),253 InputEvent::MouseMoveRelative { .. } => capabilities.contains(&Capability::RelativePointer),254 InputEvent::MousePositionSync { .. } => capabilities.contains(&Capability::RelativePointer),255 InputEvent::MouseClick { .. }256 | InputEvent::MouseDown { .. }257 | InputEvent::MouseUp { .. }258 | InputEvent::MouseScroll { .. } => {259 capabilities.contains(&Capability::RelativePointer)260 || capabilities.contains(&Capability::AbsolutePointer)261 }262 InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {263 capabilities.contains(&Capability::Keyboard)264 }265 InputEvent::Text { .. } => capabilities.contains(&Capability::Text),266 InputEvent::Gamepad { .. } => capabilities.contains(&Capability::Gamepad),267 }268}269270async fn write_client_input_event(271 stream: &mut iroh::endpoint::SendStream,272 event: &InputEvent,273 diagnostics: Option<&Arc<StdMutex<NetworkSessionDiagnostics>>>,274) -> Result<(), String> {275 if let Some(diagnostics) = diagnostics {276 lock_network_diagnostics(diagnostics).begin_input_send(Instant::now());277 }278 write_input_event(stream, event)279 .await280 .map_err(|error| error.to_string())281}282283fn observe_input_ack_if_negotiated(284 diagnostics: &StdMutex<NetworkSessionDiagnostics>,285 negotiated: bool,286 sequence: u64,287 now: Instant,288) -> Result<(), String> {289 if !negotiated {290 return Ok(());291 }292 lock_network_diagnostics(diagnostics).observe_input_ack(sequence, now)293}294295pub(crate) struct InputSession {296 pub(crate) connection: iroh::endpoint::Connection,297 pub(crate) send: iroh::endpoint::SendStream,298 pub(crate) recv: iroh::endpoint::RecvStream,299 pub(crate) capabilities: Vec<Capability>,300 pub(crate) availability: InputAvailability,301}302303pub(crate) async fn open_input_session(304 endpoint: &Endpoint,305 address: &iroh::EndpointAddr,306 nonce: [u8; 16],307 media_session_id: u64,308 grants: InvitationGrants,309) -> Result<InputSession, String> {310 let mut errors = Vec::new();311 let mut accepted = None;312 // Older hosts reject unknown capability enum values. Try all four313 // pointer feature levels with ACK first, then repeat the exact legacy314 // offers without ACK so lack of diagnostics never forces absolute315 // pointer input prematurely.316 for capabilities in input_capability_offers(grants) {317 match open_negotiated_input_stream(endpoint, address, nonce, capabilities).await {318 Ok(result) => {319 accepted = Some(result);320 break;321 }322 Err(error) => errors.push(error),323 }324 }325 let (connection, send, recv, input_negotiation) = accepted326 .ok_or_else(|| format!("All input capability offers failed: {}", errors.join("; ")))?;327 if input_negotiation.session_id != media_session_id {328 return Err("Host returned mismatched media and input sessions".to_string());329 }330 let capabilities = input_negotiation.capabilities;331 // A malformed or future host must not be able to re-enable a local input332 // path that this Portal build deliberately withheld from its offers.333 let capabilities = mask_unavailable_relative_pointer_capabilities(334 capabilities,335 relative_pointer_capture_enabled(),336 );337 let availability = InputAvailability::from_capabilities(&capabilities);338 Ok(InputSession {339 connection,340 send,341 recv,342 capabilities,343 availability,344 })345}346347pub(crate) async fn run_input_feedback(348 mut input_feedback: iroh::endpoint::RecvStream,349 feedback_diagnostics: Arc<StdMutex<NetworkSessionDiagnostics>>,350 mut pointer_feedback_enabled: bool,351 input_ack_enabled: bool,352 pointer_channel: Channel<PointerFeedbackPayload>,353) {354 let terminal_reason = loop {355 let response = match read_input_ack(&mut input_feedback).await {356 Ok(Some(response)) => response,357 Ok(None) => {358 lock_network_diagnostics(&feedback_diagnostics).mark_input_feedback_closed();359 break PointerFeedbackTerminalReason::Eof;360 }361 Err(error) => {362 lock_network_diagnostics(&feedback_diagnostics).mark_input_feedback_malformed();363 eprintln!("[client] invalid input feedback: {error}");364 break PointerFeedbackTerminalReason::Malformed;365 }366 };367 if let Err(error) = observe_input_ack_if_negotiated(368 &feedback_diagnostics,369 input_ack_enabled,370 response.sequence,371 Instant::now(),372 ) {373 eprintln!("[client] invalid input acknowledgement: {error}");374 break PointerFeedbackTerminalReason::Malformed;375 }376 if pointer_feedback_enabled377 && pointer_channel378 .send(PointerFeedbackPayload::Position {379 sequence: response.sequence,380 position: response.pointer_position,381 pointer_visible: response.pointer_visible,382 })383 .is_err()384 {385 // Losing the webview's pointer channel must not stop ACK386 // draining and apply backpressure to host input.387 pointer_feedback_enabled = false;388 if !input_ack_enabled {389 return;390 }391 }392 };393 // The session-owned channel emits at most one terminal message.394 // JavaScript rejects deliveries from superseded channel closures.395 let _ = pointer_channel.send(PointerFeedbackPayload::Terminal {396 reason: terminal_reason,397 });398}399400pub(crate) async fn run_input_forwarder(401 mut input_stream: iroh::endpoint::SendStream,402 rx: tokio::sync::mpsc::Receiver<InputEvent>,403 input_capabilities: Vec<Capability>,404 input_send_diagnostics: Arc<StdMutex<NetworkSessionDiagnostics>>,405) {406 let mut rx = rx;407 const MOUSE_INTERVAL: Duration = Duration::from_millis(16);408 let started = Instant::now();409 let mut last_absolute_mouse_time = started.checked_sub(MOUSE_INTERVAL).unwrap_or(started);410 let mut last_relative_mouse_time = started.checked_sub(MOUSE_INTERVAL).unwrap_or(started);411 let mut pending_relative = RelativePointerAccumulator::default();412 let mut input_open = true;413414 while input_open {415 let event = if pending_relative.is_pending() {416 let wait = MOUSE_INTERVAL.saturating_sub(last_relative_mouse_time.elapsed());417 if wait.is_zero() {418 let Some(event) = pending_relative.take() else {419 continue;420 };421 if let Err(error) = write_client_input_event(422 &mut input_stream,423 &event,424 Some(&input_send_diagnostics),425 )426 .await427 {428 eprintln!("[client] input stream write failed: {error}; disconnecting");429 break;430 }431 last_relative_mouse_time = Instant::now();432 continue;433 }434 tokio::select! {435 event = rx.recv() => event,436 () = tokio::time::sleep(wait) => {437 let Some(event) = pending_relative.take() else {438 continue;439 };440 if let Err(error) = write_client_input_event(441 &mut input_stream,442 &event,443 Some(&input_send_diagnostics),444 ).await {445 eprintln!("[client] input stream write failed: {error}; disconnecting");446 break;447 }448 last_relative_mouse_time = Instant::now();449 continue;450 }451 }452 } else {453 rx.recv().await454 };455 let Some(event) = event else {456 input_open = false;457 continue;458 };459 // The host's accepted capability set is an authorization boundary.460 // Drop unavailable event classes silently so event contents never461 // reach logs or the wire even if a compromised webview invokes the462 // command directly.463 if !input_event_allowed(&input_capabilities, &event) {464 continue;465 }466 let Some(event) = stage_relative_input(&mut pending_relative, event) else {467 continue;468 };469 let mut flushed_relative_barrier = false;470 while let Some(relative_barrier) = pending_relative.take() {471 if let Err(error) = write_client_input_event(472 &mut input_stream,473 &relative_barrier,474 Some(&input_send_diagnostics),475 )476 .await477 {478 eprintln!("[client] input stream write failed: {error}; disconnecting");479 input_open = false;480 break;481 }482 flushed_relative_barrier = true;483 }484 if !input_open {485 break;486 }487 if flushed_relative_barrier {488 last_relative_mouse_time = Instant::now();489 }490 if matches!(event, InputEvent::MouseMove { .. }) {491 let now = Instant::now();492 if now.duration_since(last_absolute_mouse_time) < MOUSE_INTERVAL {493 continue;494 }495 last_absolute_mouse_time = now;496 }497 if let Err(error) =498 write_client_input_event(&mut input_stream, &event, Some(&input_send_diagnostics)).await499 {500 eprintln!("[client] input stream write failed: {error}; disconnecting");501 break;502 }503 }504 while let Some(event) = pending_relative.take() {505 if let Err(error) =506 write_client_input_event(&mut input_stream, &event, Some(&input_send_diagnostics)).await507 {508 eprintln!("[client] final relative input write failed: {error}");509 break;510 }511 }512 let _ = input_stream.finish();513}514515pub(crate) async fn send_input(state: &AppState, event: InputEvent) -> Result<bool, String> {516 let tx = state517 .input_send518 .lock()519 .await520 .clone()521 .ok_or_else(|| "Not connected to host".to_string())?;522 match tx.try_send(event) {523 Ok(()) => Ok(true),524 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => Ok(false),525 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {526 Err("Input channel closed".to_string())527 }528 }529}530531532mod tests {533 use super::*;534535 536 fn pointer_feedback_channel_has_explicit_bounded_terminal_envelopes() {537 let position = serde_json::to_value(PointerFeedbackPayload::Position {538 sequence: 7,539 position: Some(PointerPosition { x: 1280, y: 800 }),540 pointer_visible: Some(false),541 })542 .unwrap();543 assert_eq!(position["type"], "position");544 assert_eq!(position["sequence"], 7);545 assert_eq!(position["position"]["x"], 1280);546 assert_eq!(position["pointer_visible"], false);547548 let legacy = serde_json::to_value(PointerFeedbackPayload::Position {549 sequence: 8,550 position: Some(PointerPosition { x: 640, y: 400 }),551 pointer_visible: None,552 })553 .unwrap();554 assert!(legacy.get("pointer_visible").is_none());555556 let eof = serde_json::to_value(PointerFeedbackPayload::Terminal {557 reason: PointerFeedbackTerminalReason::Eof,558 })559 .unwrap();560 assert_eq!(561 eof,562 serde_json::json!({ "type": "terminal", "reason": "eof" })563 );564565 let malformed = serde_json::to_value(PointerFeedbackPayload::Terminal {566 reason: PointerFeedbackTerminalReason::Malformed,567 })568 .unwrap();569 assert_eq!(570 malformed,571 serde_json::json!({ "type": "terminal", "reason": "malformed" })572 );573 }574575 576 fn input_events_require_their_negotiated_capability() {577 let pointer = [Capability::AbsolutePointer];578 assert!(input_event_allowed(579 &pointer,580 &InputEvent::MouseMove { x: 1, y: 2 }581 ));582 assert!(input_event_allowed(583 &pointer,584 &InputEvent::MouseClick { b: 1 }585 ));586 assert!(input_event_allowed(587 &pointer,588 &InputEvent::MouseDown { b: 1 }589 ));590 assert!(input_event_allowed(&pointer, &InputEvent::MouseUp { b: 1 }));591 assert!(input_event_allowed(592 &pointer,593 &InputEvent::MouseScroll { dx: 0, dy: 1 }594 ));595 assert!(!input_event_allowed(596 &pointer,597 &InputEvent::MouseMoveRelative { dx: 1, dy: -2 }598 ));599 assert!(!input_event_allowed(600 &pointer,601 &InputEvent::MousePositionSync { x: 640, y: 400 }602 ));603 assert!(!input_event_allowed(604 &pointer,605 &InputEvent::KeyDown { k: "A".into() }606 ));607 assert!(!input_event_allowed(608 &pointer,609 &InputEvent::Text { s: "a".into() }610 ));611612 let relative_pointer = [Capability::RelativePointer];613 assert!(input_event_allowed(614 &relative_pointer,615 &InputEvent::MouseMoveRelative { dx: 1, dy: -2 }616 ));617 assert!(input_event_allowed(618 &relative_pointer,619 &InputEvent::MousePositionSync { x: 640, y: 400 }620 ));621 assert!(input_event_allowed(622 &relative_pointer,623 &InputEvent::MouseDown { b: 1 }624 ));625 assert!(input_event_allowed(626 &relative_pointer,627 &InputEvent::MouseUp { b: 1 }628 ));629 assert!(input_event_allowed(630 &relative_pointer,631 &InputEvent::MouseScroll { dx: 0, dy: 1 }632 ));633 assert!(!input_event_allowed(634 &relative_pointer,635 &InputEvent::MouseMove { x: 1, y: 2 }636 ));637638 let keyboard = [Capability::Keyboard];639 assert!(input_event_allowed(640 &keyboard,641 &InputEvent::KeyDown { k: "A".into() }642 ));643 assert!(input_event_allowed(644 &keyboard,645 &InputEvent::KeyUp { k: "A".into() }646 ));647 assert!(input_event_allowed(648 &keyboard,649 &InputEvent::KeyClick { k: "A".into() }650 ));651 assert!(!input_event_allowed(652 &keyboard,653 &InputEvent::Text { s: "a".into() }654 ));655656 let text = [Capability::Text];657 assert!(input_event_allowed(658 &text,659 &InputEvent::Text { s: "a".into() }660 ));661 assert!(!input_event_allowed(662 &text,663 &InputEvent::KeyDown { k: "A".into() }664 ));665666 let gamepad = [Capability::Gamepad];667 assert!(input_event_allowed(668 &gamepad,669 &InputEvent::Gamepad {670 state: sigil_protocol::GamepadState::default(),671 }672 ));673 assert!(!input_event_allowed(674 &keyboard,675 &InputEvent::Gamepad {676 state: sigil_protocol::GamepadState::default(),677 }678 ));679 }680681 682 fn empty_input_capabilities_are_view_only() {683 let capabilities = [];684 assert!(!input_event_allowed(685 &capabilities,686 &InputEvent::MouseMove { x: 1, y: 2 }687 ));688 assert!(!input_event_allowed(689 &capabilities,690 &InputEvent::KeyDown { k: "A".into() }691 ));692 assert!(!input_event_allowed(693 &capabilities,694 &InputEvent::Text { s: "a".into() }695 ));696 assert_eq!(697 InputAvailability::from_capabilities(&capabilities),698 InputAvailability {699 relative_pointer: false,700 pointer_position_feedback: false,701 absolute_pointer: false,702 keyboard: false,703 text: false,704 gamepad: false,705 input_ack: false,706 control: false,707 }708 );709 }710711 712 fn input_availability_reports_each_accepted_capability_exactly() {713 assert_eq!(714 InputAvailability::from_capabilities(&[Capability::AbsolutePointer, Capability::Text]),715 InputAvailability {716 relative_pointer: false,717 pointer_position_feedback: false,718 absolute_pointer: true,719 keyboard: false,720 text: true,721 gamepad: false,722 input_ack: false,723 control: true,724 }725 );726 assert_eq!(727 InputAvailability::from_capabilities(&[Capability::Keyboard]),728 InputAvailability {729 relative_pointer: false,730 pointer_position_feedback: false,731 absolute_pointer: false,732 keyboard: true,733 text: false,734 gamepad: false,735 input_ack: false,736 control: true,737 }738 );739 assert_eq!(740 InputAvailability::from_capabilities(&[Capability::Gamepad]),741 InputAvailability {742 relative_pointer: false,743 pointer_position_feedback: false,744 absolute_pointer: false,745 keyboard: false,746 text: false,747 gamepad: true,748 input_ack: false,749 control: true,750 }751 );752 assert!(753 InputAvailability::from_capabilities(&[Capability::Gamepad, Capability::InputAck])754 .input_ack755 );756 assert_eq!(757 InputAvailability::from_capabilities(&[758 Capability::RelativePointer,759 Capability::PointerPositionFeedback,760 ]),761 InputAvailability {762 relative_pointer: true,763 pointer_position_feedback: true,764 absolute_pointer: false,765 keyboard: false,766 text: false,767 gamepad: false,768 input_ack: false,769 control: true,770 }771 );772 }773774 775 fn pointer_feedback_without_input_ack_does_not_fail_ack_validation() {776 let diagnostics = StdMutex::new(NetworkSessionDiagnostics::new(Instant::now(), false));777 assert!(observe_input_ack_if_negotiated(&diagnostics, false, 0, Instant::now()).is_ok());778 }779780 781 fn input_capability_fallbacks_remove_only_one_protocol_extension_at_a_time() {782 let offers = input_capability_offers_for(InvitationGrants::ALL, true);783 assert_eq!(offers.len(), 8);784 let visibility = &offers[0];785 let position = &offers[1];786 let relative = &offers[2];787 let inherited = &offers[3];788789 assert!(790 offers[..4]791 .iter()792 .all(|offer| offer.contains(&Capability::InputAck))793 );794 assert!(795 offers[4..]796 .iter()797 .all(|offer| !offer.contains(&Capability::InputAck))798 );799 assert!(visibility.contains(&Capability::PointerVisibilityFeedback));800 assert!(visibility.contains(&Capability::PointerPositionFeedback));801 assert!(!position.contains(&Capability::PointerVisibilityFeedback));802 assert!(position.contains(&Capability::PointerPositionFeedback));803 assert!(!relative.contains(&Capability::PointerVisibilityFeedback));804 assert!(!relative.contains(&Capability::PointerPositionFeedback));805 assert!(relative.contains(&Capability::RelativePointer));806 assert!(!inherited.contains(&Capability::RelativePointer));807 assert_eq!(808 inherited.as_slice(),809 &[810 Capability::AbsolutePointer,811 Capability::Keyboard,812 Capability::Text,813 Capability::Gamepad,814 Capability::InputAck,815 ]816 );817 }818819 820 fn disabled_relative_capture_preserves_only_non_relative_input_offers() {821 let offers = input_capability_offers_for(InvitationGrants::ALL, false);822 assert_eq!(823 offers,824 [825 vec![826 Capability::AbsolutePointer,827 Capability::Keyboard,828 Capability::Text,829 Capability::Gamepad,830 Capability::InputAck,831 ],832 vec![833 Capability::AbsolutePointer,834 Capability::Keyboard,835 Capability::Text,836 Capability::Gamepad,837 ],838 ]839 );840 assert!(offers.iter().flatten().all(|capability| !matches!(841 capability,842 Capability::RelativePointer843 | Capability::PointerPositionFeedback844 | Capability::PointerVisibilityFeedback845 )));846 }847848 849 fn compiled_pointer_policy_controls_the_actual_negotiation_offers() {850 let offers = input_capability_offers(InvitationGrants::ALL);851 let offers_relative_pointer = offers852 .iter()853 .flatten()854 .any(|capability| *capability == Capability::RelativePointer);855 assert_eq!(offers_relative_pointer, relative_pointer_capture_enabled());856 assert!(857 offers858 .iter()859 .all(|offer| offer.contains(&Capability::AbsolutePointer))860 );861 }862863 864 fn negotiated_capability_mask_fails_closed_without_losing_other_input() {865 let accepted = vec![866 Capability::RelativePointer,867 Capability::PointerPositionFeedback,868 Capability::PointerVisibilityFeedback,869 Capability::AbsolutePointer,870 Capability::Keyboard,871 Capability::Text,872 Capability::Gamepad,873 Capability::InputAck,874 ];875 let masked = mask_unavailable_relative_pointer_capabilities(accepted.clone(), false);876 assert_eq!(877 masked,878 [879 Capability::AbsolutePointer,880 Capability::Keyboard,881 Capability::Text,882 Capability::Gamepad,883 Capability::InputAck,884 ]885 );886 assert_eq!(887 mask_unavailable_relative_pointer_capabilities(accepted.clone(), true),888 accepted889 );890 assert_eq!(891 InputAvailability::from_capabilities(&masked),892 InputAvailability {893 relative_pointer: false,894 pointer_position_feedback: false,895 absolute_pointer: true,896 keyboard: true,897 text: true,898 gamepad: true,899 input_ack: true,900 control: true,901 }902 );903 }904905 906 fn local_invitation_grants_bound_input_offers_before_the_host_intersection() {907 let view_only = input_capability_offers(InvitationGrants::VIEW);908 assert!(view_only.iter().all(Vec::is_empty));909910 let pointer = input_capability_offers(911 InvitationGrants::VIEW.union(InvitationGrants::POINTER_KEYBOARD),912 );913 assert!(pointer[0].contains(&Capability::Keyboard));914 assert!(pointer[0].contains(&Capability::InputAck));915 assert!(!pointer[0].contains(&Capability::Gamepad));916917 let gamepad =918 input_capability_offers(InvitationGrants::VIEW.union(InvitationGrants::GAMEPAD));919 assert_eq!(gamepad[0], vec![Capability::Gamepad, Capability::InputAck]);920 assert_eq!(gamepad[1], vec![Capability::Gamepad]);921 }922923 924 fn relative_pointer_accumulator_coalesces_chunks_and_resets() {925 let mut accumulator = RelativePointerAccumulator::default();926 accumulator.push(10, -20);927 accumulator.push(5, 8);928 assert_eq!(929 accumulator.take(),930 Some(InputEvent::MouseMoveRelative { dx: 15, dy: -12 })931 );932 assert_eq!(accumulator.take(), None);933934 accumulator.push(RELATIVE_POINTER_DELTA_MAX, RELATIVE_POINTER_DELTA_MIN);935 accumulator.push(1_000, -1_000);936 assert_eq!(937 accumulator.take(),938 Some(InputEvent::MouseMoveRelative {939 dx: RELATIVE_POINTER_DELTA_MAX,940 dy: RELATIVE_POINTER_DELTA_MIN,941 })942 );943 assert_eq!(944 accumulator.take(),945 Some(InputEvent::MouseMoveRelative {946 dx: 1_000,947 dy: -1_000,948 })949 );950 assert_eq!(accumulator.take(), None);951 }952953 954 fn relative_motion_chunks_are_staged_immediately_before_a_following_button() {955 let mut accumulator = RelativePointerAccumulator::default();956 assert_eq!(957 stage_relative_input(958 &mut accumulator,959 InputEvent::MouseMoveRelative {960 dx: RELATIVE_POINTER_DELTA_MAX,961 dy: RELATIVE_POINTER_DELTA_MIN,962 }963 ),964 None965 );966 assert_eq!(967 stage_relative_input(968 &mut accumulator,969 InputEvent::MouseMoveRelative { dx: 7, dy: -1 }970 ),971 None972 );973 assert_eq!(974 stage_relative_input(&mut accumulator, InputEvent::MouseDown { b: 1 }),975 Some(InputEvent::MouseDown { b: 1 })976 );977 assert_eq!(978 accumulator.take(),979 Some(InputEvent::MouseMoveRelative {980 dx: RELATIVE_POINTER_DELTA_MAX,981 dy: RELATIVE_POINTER_DELTA_MIN,982 })983 );984 assert_eq!(985 accumulator.take(),986 Some(InputEvent::MouseMoveRelative { dx: 7, dy: -1 })987 );988 assert_eq!(accumulator.take(), None);989 }990}