diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index f5f8efd69..59a4813c6 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -62,6 +62,8 @@ pub struct Input { pub trackpoint: Trackpoint, #[knuffel(child, default)] pub tablet: Tablet, + #[knuffel(child, default)] + pub touch: Touch, #[knuffel(child)] pub disable_power_key_handling: bool, } @@ -201,6 +203,12 @@ pub struct Tablet { pub map_to_output: Option, } +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Touch { + #[knuffel(child, unwrap(argument))] + pub map_to_output: Option, +} + #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Output { #[knuffel(child)] @@ -1092,6 +1100,10 @@ mod tests { map-to-output "eDP-1" } + touch { + map-to-output "eDP-1" + } + disable-power-key-handling } @@ -1223,6 +1235,9 @@ mod tests { tablet: Tablet { map_to_output: Some("eDP-1".to_owned()), }, + touch: Touch { + map_to_output: Some("eDP-1".to_owned()), + }, disable_power_key_handling: true, }, outputs: vec![Output { diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 36a9ca1df..35a04ca71 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -54,6 +54,13 @@ input { map-to-output "eDP-1" } + touch { + // Set the name of the output (see below) which touch input will map to. + // If this is unset or the output doesn't exist, touch input maps to one of the + // existing outputs. + map-to-output "eDP-1" + } + // By default, niri will take over the power button to make it sleep // instead of power off. // Uncomment this if you would like to configure the power button elsewhere diff --git a/src/input.rs b/src/input.rs index 3d470eba2..41ac7888c 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,7 +8,7 @@ use smithay::backend::input::{ GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, - TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, + TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, }; use smithay::backend::libinput::LibinputInputBackend; use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState}; @@ -17,6 +17,7 @@ use smithay::input::pointer::{ GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, RelativeMotionEvent, }; +use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}; use smithay::utils::{Logical, Point, SERIAL_COUNTER}; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint}; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; @@ -101,11 +102,11 @@ impl State { GesturePinchEnd { event } => self.on_gesture_pinch_end::(event), GestureHoldBegin { event } => self.on_gesture_hold_begin::(event), GestureHoldEnd { event } => self.on_gesture_hold_end::(event), - TouchDown { .. } => (), - TouchMotion { .. } => (), - TouchUp { .. } => (), - TouchCancel { .. } => (), - TouchFrame { .. } => (), + TouchDown { event } => self.on_touch_down::(event), + TouchMotion { event } => self.on_touch_motion::(event), + TouchUp { event } => self.on_touch_up::(event), + TouchCancel { event } => self.on_touch_cancel::(event), + TouchFrame { event } => self.on_touch_frame::(event), SwitchToggle { .. } => (), Special(_) => (), } @@ -154,9 +155,14 @@ impl State { } } + if device.has_capability(input::DeviceCapability::Touch) { + self.niri.touch.insert(device.clone()); + } + apply_libinput_settings(&self.niri.config.borrow().input, device); } InputEvent::DeviceRemoved { device } => { + self.niri.touch.remove(device); self.niri.tablets.remove(device); self.niri.devices.remove(device); } @@ -171,6 +177,9 @@ impl State { let desc = TabletDescriptor::from(&device); tablet_seat.add_tablet::(&self.niri.display_handle, &desc); } + if device.has_capability(DeviceCapability::Touch) && self.niri.seat.get_touch().is_none() { + self.niri.seat.add_touch(); + } } fn on_device_removed(&mut self, device: impl Device) { @@ -185,6 +194,9 @@ impl State { tablet_seat.clear_tools(); } } + if device.has_capability(DeviceCapability::Touch) && self.niri.touch.is_empty() { + self.niri.seat.remove_touch(); + } } /// Computes the cursor position for the tablet event. @@ -283,6 +295,10 @@ impl State { return; } + if let Some(touch) = self.niri.seat.get_touch() { + touch.cancel(self); + } + match action { Action::Quit(skip_confirmation) => { if !skip_confirmation { @@ -1354,6 +1370,116 @@ impl State { }, ); } + + /// Computes the cursor position for the touch event. + /// + /// This function handles the touch output mapping, as well as coordinate transform + fn compute_touch_location>( + &self, + evt: &E, + ) -> Option> { + let output = self.niri.output_for_touch()?; + let output_geo = self.niri.global_space.output_geometry(output).unwrap(); + let transform = output.current_transform(); + let size = transform.invert().transform_size(output_geo.size); + Some( + transform.transform_point_in(evt.position_transformed(size), &size.to_f64()) + + output_geo.loc.to_f64(), + ) + } + + fn on_touch_down(&mut self, evt: I::TouchDownEvent) { + let Some(handle) = self.niri.seat.get_touch() else { + return; + }; + let Some(touch_location) = self.compute_touch_location(&evt) else { + return; + }; + + if !handle.is_grabbed() { + let output_under_touch = self + .niri + .global_space + .output_under(touch_location) + .next() + .cloned(); + if let Some(window) = self.niri.window_under(touch_location) { + let window = window.clone(); + self.niri.layout.activate_window(&window); + + // FIXME: granular. + self.niri.queue_redraw_all(); + } else if let Some(output) = output_under_touch { + self.niri.layout.activate_output(&output); + + // FIXME: granular. + self.niri.queue_redraw_all(); + }; + }; + + let serial = SERIAL_COUNTER.next_serial(); + let under = self + .niri + .surface_under_and_global_space(touch_location) + .map(|under| under.surface); + handle.down( + self, + under, + &DownEvent { + slot: evt.slot(), + location: touch_location, + serial, + time: evt.time_msec(), + }, + ); + } + fn on_touch_up(&mut self, evt: I::TouchUpEvent) { + let Some(handle) = self.niri.seat.get_touch() else { + return; + }; + let serial = SERIAL_COUNTER.next_serial(); + handle.up( + self, + &UpEvent { + slot: evt.slot(), + serial, + time: evt.time_msec(), + }, + ) + } + fn on_touch_motion(&mut self, evt: I::TouchMotionEvent) { + let Some(handle) = self.niri.seat.get_touch() else { + return; + }; + let Some(touch_location) = self.compute_touch_location(&evt) else { + return; + }; + let under = self + .niri + .surface_under_and_global_space(touch_location) + .map(|under| under.surface); + handle.motion( + self, + under, + &TouchMotionEvent { + slot: evt.slot(), + location: touch_location, + time: evt.time_msec(), + }, + ); + } + fn on_touch_frame(&mut self, _evt: I::TouchFrameEvent) { + let Some(handle) = self.niri.seat.get_touch() else { + return; + }; + handle.frame(self); + } + fn on_touch_cancel(&mut self, _evt: I::TouchCancelEvent) { + let Some(handle) = self.niri.seat.get_touch() else { + return; + }; + handle.cancel(self); + } } /// Check whether the key should be intercepted and mark intercepted diff --git a/src/niri.rs b/src/niri.rs index 5b2491d93..418ef6d0e 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -144,6 +144,7 @@ pub struct Niri { pub devices: HashSet, pub tablets: HashMap, + pub touch: HashSet, // Smithay state. pub compositor_state: CompositorState, @@ -702,6 +703,10 @@ impl State { self.niri.reposition_outputs(None); self.backend.on_output_config_changed(&mut self.niri); + + if let Some(touch) = self.niri.seat.get_touch() { + touch.cancel(self); + } } // Can't really update xdg-decoration settings since we have to hide the globals for CSD @@ -1002,6 +1007,7 @@ impl Niri { devices: HashSet::new(), tablets: HashMap::new(), + touch: HashSet::new(), compositor_state, xdg_shell_state, @@ -1602,6 +1608,14 @@ impl Niri { .or_else(|| self.global_space.outputs().next()) } + pub fn output_for_touch(&self) -> Option<&Output> { + let config = self.config.borrow(); + let map_to_output = config.input.touch.map_to_output.as_ref(); + map_to_output + .and_then(|name| self.output_by_name.get(name)) + .or_else(|| self.global_space.outputs().next()) + } + pub fn output_for_root(&self, root: &WlSurface) -> Option<&Output> { // Check the main layout. let win_out = self.layout.find_window_and_output(root);