diff --git a/compositor/src/input.rs b/compositor/src/input.rs index dff97e1..8017467 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -29,6 +29,8 @@ const BTN_RIGHT: u32 = 0x111; #[cfg(test)] pub(crate) mod binding_mode_tests; #[cfg(test)] +mod launcher_touch_tests; +#[cfg(test)] mod layer_focus_tests; pub(crate) struct LockedPointerHint { diff --git a/compositor/src/input/launcher_touch_tests.rs b/compositor/src/input/launcher_touch_tests.rs new file mode 100644 index 0000000..d7a5bf7 --- /dev/null +++ b/compositor/src/input/launcher_touch_tests.rs @@ -0,0 +1,206 @@ +//! Exercise dismissal through the real input path, including contact lifetimes. +use super::*; +use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; +use smithay::backend::input::{ + TouchCancelEvent, TouchDownEvent, TouchFrameEvent, TouchMotionEvent, TouchSlot, TouchUpEvent, + UnusedEvent, +}; +use smithay::backend::winit::WinitVirtualDevice; +use std::io::Read; +use std::os::unix::net::UnixStream; +use std::sync::Arc; + +#[derive(Debug)] +struct TouchInput; + +impl InputBackend for TouchInput { + type Device = WinitVirtualDevice; + type KeyboardKeyEvent = UnusedEvent; + type PointerButtonEvent = UnusedEvent; + type PointerAxisEvent = UnusedEvent; + type PointerMotionEvent = UnusedEvent; + type PointerMotionAbsoluteEvent = UnusedEvent; + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + type TouchDownEvent = Contact; + type TouchMotionEvent = Contact; + type TouchUpEvent = Contact; + type TouchCancelEvent = Contact; + type TouchFrameEvent = Contact; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + type SwitchToggleEvent = UnusedEvent; + type SpecialEvent = UnusedEvent; +} + +#[derive(Clone, Debug)] +struct Contact(u32, Point); + +impl Event for Contact { + fn time(&self) -> smithay::backend::input::InputTime { + smithay::backend::input::InputTime::from_millis(1) + } + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} +impl TouchEvent for Contact { + fn slot(&self) -> TouchSlot { + Some(self.0).into() + } +} +impl AbsolutePositionEvent for Contact { + fn x(&self) -> f64 { + self.1.x + } + fn y(&self) -> f64 { + self.1.y + } + fn x_transformed(&self, _: i32) -> f64 { + self.1.x + } + fn y_transformed(&self, _: i32) -> f64 { + self.1.y + } +} +impl TouchDownEvent for Contact {} +impl TouchMotionEvent for Contact {} +impl TouchUpEvent for Contact {} +impl TouchCancelEvent for Contact {} +impl TouchFrameEvent for Contact {} + +#[test] +fn outside_launcher_touches_are_consumed_until_up_or_cancel() { + if !crate::startup_tests::private_runtime( + "input::launcher_touch_tests::outside_launcher_touches_are_consumed_until_up_or_cancel", + ) { + return; + } + let (mut state, mut events, output) = super::layer_focus_tests::fixture(); + state.window_rules = crate::config::Config::parse_source( + "window-rule app-id=dev.ferese.TouchTarget floating=#true\n", + ) + .unwrap() + .window_rules() + .unwrap(); + let (window, _wire) = crate::state::test_window_with_app_id( + &mut state, + &mut events, + 0xff112233, + "dev.ferese.TouchTarget", + ); + let id = state.windows.ids()[&window]; + let bounds = state.windows.geometry(&id).unwrap().logical; + let app = window.toplevel().unwrap().wl_surface().clone(); + let (launcher, _launcher_wire) = super::layer_focus_tests::layer_with_namespace( + &mut state, + &mut events, + &output, + 1, + 3, + "ferese-launcher", + ); + let keyboard = state.seat.get_keyboard().unwrap(); + let touch = state.seat.get_touch().unwrap(); + assert_eq!( + keyboard.current_focus(), + Some(launcher.wl_surface().clone()) + ); + let outside = Contact( + 0, + (bounds.x + bounds.width / 2., bounds.y + bounds.height / 2.).into(), + ); + assert_eq!(state.surface_under(outside.1).unwrap().0, app); + let (server, mut shell_wire) = UnixStream::pair().unwrap(); + let client = state + .display_handle + .insert_client(server, Arc::new(crate::state::ClientState::default())) + .unwrap(); + let shell = client + .create_resource::(&state.display_handle, 10, ()) + .unwrap(); + state.shell_resources.push(shell.downgrade()); + state.process_input_event::(InputEvent::TouchDown { + event: outside.clone(), + }); + assert!( + !touch.is_grabbed(), + "dismissing must not begin an app touch sequence" + ); + assert_eq!( + keyboard.current_focus(), + Some(launcher.wl_surface().clone()) + ); + // The launcher can close before the rest of this gesture arrives. + keyboard.set_focus(&mut state, Some(app.clone()), SERIAL_COUNTER.next_serial()); + let second = Contact(1, outside.1); + state.process_input_event::(InputEvent::TouchDown { + event: second.clone(), + }); + for event in [outside.clone(), second.clone()] { + state.process_input_event::(InputEvent::TouchMotion { + event: event.clone(), + }); + state.process_input_event::(InputEvent::TouchUp { event }); + } + assert!(!touch.is_grabbed()); + assert!(state.consumed_touch_slots.is_empty()); + // A fresh tap must work after the swallowed contacts have ended. + state.process_input_event::(InputEvent::TouchDown { + event: outside.clone(), + }); + assert_eq!(touch.grab_start_data().unwrap().focus.unwrap().0, app); + state.process_input_event::(InputEvent::TouchUp { + event: outside.clone(), + }); + keyboard.set_focus( + &mut state, + Some(launcher.wl_surface().clone()), + SERIAL_COUNTER.next_serial(), + ); + state.process_input_event::(InputEvent::TouchDown { + event: outside.clone(), + }); + state.process_input_event::(InputEvent::TouchCancel { + event: outside.clone(), + }); + assert!(state.consumed_touch_slots.is_empty()); + assert!(!touch.is_grabbed()); + // An inside tap still reaches the launcher itself after cancellation. + let inside = Contact(0, (20., 20.).into()); + state.process_input_event::(InputEvent::TouchDown { + event: inside.clone(), + }); + assert_eq!( + touch.grab_start_data().unwrap().focus.unwrap().0, + *launcher.wl_surface() + ); + state.process_input_event::(InputEvent::TouchUp { event: inside }); + state.display_handle.flush_clients().unwrap(); + shell_wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + let _ = shell_wire.read_to_end(&mut bytes); + let opcode = FereseShellV1::interface() + .events + .iter() + .position(|event| event.name == "toggle_launcher") + .unwrap() as u16; + let (mut offset, mut dismissals) = (0, 0); + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == shell.id().protocol_id() && header as u16 == opcode { + dismissals += 1; + } + offset += (header >> 16) as usize; + } + assert_eq!(dismissals, 2, "each outside gesture dismisses exactly once"); +} diff --git a/compositor/src/input/layer_focus_tests.rs b/compositor/src/input/layer_focus_tests.rs index c7c27c9..b10899f 100644 --- a/compositor/src/input/layer_focus_tests.rs +++ b/compositor/src/input/layer_focus_tests.rs @@ -28,6 +28,17 @@ fn layer_with_policy( output: &Output, interactivity: u32, level: u32, +) -> (LayerSurface, UnixStream) { + layer_with_namespace(state, events, output, interactivity, level, "bar") +} + +pub(super) fn layer_with_namespace( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + output: &Output, + interactivity: u32, + level: u32, + namespace: &str, ) -> (LayerSurface, UnixStream) { let initial_focus = state.seat.get_keyboard().unwrap().current_focus(); let (server, mut wire) = UnixStream::pair().unwrap(); @@ -57,13 +68,15 @@ fn layer_with_policy( ) .unwrap(); request(&mut wire, compositor.id().protocol_id(), 0, &[2], None); - request( - &mut wire, - layers.id().protocol_id(), - 0, - &[3, 2, 0, level, 4, u32::from_ne_bytes(*b"bar\0")], - None, + let mut name = namespace.as_bytes().to_vec(); + name.push(0); + let mut args = vec![3, 2, 0, level, name.len() as u32]; + name.resize(name.len().next_multiple_of(4), 0); + args.extend( + name.chunks_exact(4) + .map(|word| u32::from_ne_bytes(word.try_into().unwrap())), ); + request(&mut wire, layers.id().protocol_id(), 0, &args, None); request(&mut wire, 3, 0, &[64, 48], None); request(&mut wire, 3, 1, &[1 | 4], None); // Top left. request(&mut wire, 3, 4, &[interactivity], None); @@ -274,7 +287,7 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() { ); } -fn fixture() -> (Ferese, EventLoop<'static, Ferese>, Output) { +pub(super) fn fixture() -> (Ferese, EventLoop<'static, Ferese>, Output) { fixture_with_animations(false) } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index b204429..cec2580 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -3,6 +3,7 @@ mod capture_privacy; #[cfg(test)] pub(crate) use capture_privacy::tests::{ dispatch as dispatch_test_client, request as test_client_request, window as test_window, + window_with_app_id as test_window_with_app_id, }; mod floating; mod focus;