test(input): cover launcher touch dismissal and contact lifetimes

Verify outside contacts and additional fingers cannot reach the underlying window, and that up/cancel restore normal input. Covers the existing fix for #43.
This commit is contained in:
Samuel Onoja committed 2026-10-24 19:54:40 +01:00
1 parent df36d1c9b0
commit a8ef3967a1
4 files changed
+229 -7

No files matched your search

+2
View File
@@ -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 {
@@ -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<f64, Logical>);
impl Event<TouchInput> for Contact {
fn time(&self) -> smithay::backend::input::InputTime {
smithay::backend::input::InputTime::from_millis(1)
}
fn device(&self) -> WinitVirtualDevice {
WinitVirtualDevice
}
}
impl TouchEvent<TouchInput> for Contact {
fn slot(&self) -> TouchSlot {
Some(self.0).into()
}
}
impl AbsolutePositionEvent<TouchInput> 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<TouchInput> for Contact {}
impl TouchMotionEvent<TouchInput> for Contact {}
impl TouchUpEvent<TouchInput> for Contact {}
impl TouchCancelEvent<TouchInput> for Contact {}
impl TouchFrameEvent<TouchInput> 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::<FereseShellV1, (), Ferese>(&state.display_handle, 10, ())
.unwrap();
state.shell_resources.push(shell.downgrade());
state.process_input_event::<TouchInput>(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::<TouchInput>(InputEvent::TouchDown {
event: second.clone(),
});
for event in [outside.clone(), second.clone()] {
state.process_input_event::<TouchInput>(InputEvent::TouchMotion {
event: event.clone(),
});
state.process_input_event::<TouchInput>(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::<TouchInput>(InputEvent::TouchDown {
event: outside.clone(),
});
assert_eq!(touch.grab_start_data().unwrap().focus.unwrap().0, app);
state.process_input_event::<TouchInput>(InputEvent::TouchUp {
event: outside.clone(),
});
keyboard.set_focus(
&mut state,
Some(launcher.wl_surface().clone()),
SERIAL_COUNTER.next_serial(),
);
state.process_input_event::<TouchInput>(InputEvent::TouchDown {
event: outside.clone(),
});
state.process_input_event::<TouchInput>(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::<TouchInput>(InputEvent::TouchDown {
event: inside.clone(),
});
assert_eq!(
touch.grab_start_data().unwrap().focus.unwrap().0,
*launcher.wl_surface()
);
state.process_input_event::<TouchInput>(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");
}
+20 -7
View File
@@ -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)
}
+1
View File
@@ -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;