diff --git a/src/backend/wayland/handlers/mod.rs b/src/backend/wayland/handlers/mod.rs index d85d690e..a488e99b 100644 --- a/src/backend/wayland/handlers/mod.rs +++ b/src/backend/wayland/handlers/mod.rs @@ -3,7 +3,7 @@ use smithay_client_toolkit::{ delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer, delegate_pointer_constraints, delegate_registry, delegate_relative_pointer, delegate_seat, - delegate_shm, delegate_xdg_shell, delegate_xdg_window, + delegate_shm, delegate_touch, delegate_xdg_shell, delegate_xdg_window, }; use super::state::WaylandState; @@ -15,6 +15,7 @@ delegate_layer!(WaylandState); delegate_seat!(WaylandState); delegate_keyboard!(WaylandState); delegate_pointer!(WaylandState); +delegate_touch!(WaylandState); delegate_pointer_constraints!(WaylandState); delegate_relative_pointer!(WaylandState); delegate_registry!(WaylandState); @@ -36,4 +37,5 @@ mod seat; mod shm; #[cfg(tablet)] mod tablet; +mod touch; mod xdg; diff --git a/src/backend/wayland/handlers/seat.rs b/src/backend/wayland/handlers/seat.rs index 50c6224f..405d9587 100644 --- a/src/backend/wayland/handlers/seat.rs +++ b/src/backend/wayland/handlers/seat.rs @@ -53,6 +53,19 @@ impl SeatHandler for WaylandState { } } + if capability == Capability::Touch { + info!("Touch capability available"); + match self.seat_state.get_touch(qh, &seat) { + Ok(touch) => { + debug!("Touch initialized"); + self.touch = Some(touch); + } + Err(err) => { + warn!("Touch initialization failed: {}", err); + } + } + } + #[cfg(tablet)] if let Some(manager) = &self.tablet_manager && self.tablet_seats.is_empty() @@ -79,6 +92,11 @@ impl SeatHandler for WaylandState { self.current_pointer_shape = None; self.cursor_hidden = false; } + if capability == Capability::Touch { + info!("Touch capability removed"); + self.touch = None; + self.cancel_active_touch_sequence(); + } } fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: wl_seat::WlSeat) { diff --git a/src/backend/wayland/handlers/touch.rs b/src/backend/wayland/handlers/touch.rs new file mode 100644 index 00000000..29bf1ca9 --- /dev/null +++ b/src/backend/wayland/handlers/touch.rs @@ -0,0 +1,419 @@ +use log::debug; +use smithay_client_toolkit::seat::touch::TouchHandler; +use wayland_client::{ + Connection, QueueHandle, + protocol::{wl_surface, wl_touch}, +}; + +use crate::backend::wayland::state::{ + TouchTarget, WaylandState, debug_toolbar_drag_logging_enabled, +}; +use crate::backend::wayland::toolbar_intent::intent_to_event; +use crate::input::MouseButton; + +impl TouchHandler for WaylandState { + fn down( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _touch: &wl_touch::WlTouch, + serial: u32, + _time: u32, + surface: wl_surface::WlSurface, + id: i32, + position: (f64, f64), + ) { + if !self.active_touch.begin(id, position) { + debug!("Ignoring secondary touch down id={}", id); + return; + } + + self.set_last_activation_serial(Some(serial)); + self.active_touch_surface = Some(surface.clone()); + let target = self.handle_touch_down(conn, qh, &surface, position); + self.active_touch.set_target(target); + } + + fn up( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _touch: &wl_touch::WlTouch, + _serial: u32, + _time: u32, + id: i32, + ) { + if !self.active_touch.is_active_id(id) { + debug!("Ignoring inactive touch up id={}", id); + return; + } + + let surface = self.active_touch_surface.clone(); + let position = self.active_touch.last_position(); + let target = self.active_touch.target(); + if let (Some(surface), Some(position)) = (surface.as_ref(), position) { + self.handle_touch_up(surface, position, target); + } + self.clear_active_touch(); + } + + fn motion( + &mut self, + conn: &Connection, + _qh: &QueueHandle, + _touch: &wl_touch::WlTouch, + _time: u32, + id: i32, + position: (f64, f64), + ) { + if !self.active_touch.update_position(id, position) { + return; + } + + let Some(surface) = self.active_touch_surface.clone() else { + return; + }; + self.handle_touch_motion(conn, &surface, position, self.active_touch.target()); + } + + fn shape( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _touch: &wl_touch::WlTouch, + _id: i32, + _major: f64, + _minor: f64, + ) { + } + + fn orientation( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _touch: &wl_touch::WlTouch, + _id: i32, + _orientation: f64, + ) { + } + + fn cancel(&mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &wl_touch::WlTouch) { + self.cancel_active_touch_sequence(); + } +} + +impl WaylandState { + pub(in crate::backend::wayland) fn cancel_active_touch_sequence(&mut self) { + if !self.active_touch.is_active() { + self.active_touch_surface = None; + return; + } + + let target = self.active_touch.target(); + self.set_pending_toast_press(false); + self.set_suppress_next_release(false); + + if !matches!( + target, + TouchTarget::Overlay | TouchTarget::Toolbar | TouchTarget::InlineToolbar + ) { + self.clear_active_touch(); + return; + } + + if let Some(surface) = self.active_touch_surface.take() + && target == TouchTarget::Toolbar + { + self.toolbar.pointer_leave(&surface); + self.toolbar.mark_dirty(); + self.set_pointer_over_toolbar(false); + } + if target == TouchTarget::InlineToolbar { + self.inline_toolbar_leave(); + } + self.set_toolbar_dragging(false); + self.end_toolbar_move_drag(); + if self.board_panning_active() { + self.stop_board_pan(); + } + self.input_state.cancel_active_interaction(); + self.input_state.needs_redraw = true; + self.clear_active_touch(); + } + + fn clear_active_touch(&mut self) { + self.active_touch.clear(); + self.active_touch_surface = None; + } + + fn classify_touch_surface(&self, surface: &wl_surface::WlSurface) -> TouchTarget { + if self.toolbar.is_toolbar_surface(surface) { + TouchTarget::Toolbar + } else if self + .surface + .wl_surface() + .is_some_and(|overlay| overlay == surface) + { + TouchTarget::Overlay + } else { + TouchTarget::Other + } + } + + fn touch_screen_position( + &self, + surface: &wl_surface::WlSurface, + position: (f64, f64), + target: TouchTarget, + ) -> Option<(f64, f64)> { + match target { + TouchTarget::Overlay | TouchTarget::InlineToolbar => Some(position), + TouchTarget::Toolbar => self.toolbar_surface_screen_coords(surface, position), + TouchTarget::None | TouchTarget::Other => None, + } + } + + fn handle_touch_down( + &mut self, + conn: &Connection, + qh: &QueueHandle, + surface: &wl_surface::WlSurface, + position: (f64, f64), + ) -> TouchTarget { + let target = self.classify_touch_surface(surface); + let Some(screen_position) = self.touch_screen_position(surface, position, target) else { + return TouchTarget::Other; + }; + let screen_x = screen_position.0.round() as i32; + let screen_y = screen_position.1.round() as i32; + self.set_current_mouse(screen_x, screen_y); + + if self.input_state.tour_active { + return TouchTarget::Other; + } + + if self.input_state.command_palette_open { + let screen_width = self.surface.width(); + let screen_height = self.surface.height(); + if self.input_state.handle_command_palette_click( + screen_x, + screen_y, + screen_width, + screen_height, + ) { + self.set_suppress_next_release(true); + } + return TouchTarget::Other; + } + + let inline_active = self.inline_toolbars_active() && self.toolbar.is_visible(); + if target == TouchTarget::Overlay + && inline_active + && self.inline_toolbar_press(screen_position, Some(conn), Some(qh)) + { + return TouchTarget::InlineToolbar; + } + + if target == TouchTarget::Toolbar { + self.set_pointer_over_toolbar(true); + if let Some((intent, drag)) = self.toolbar.pointer_press(surface, position) { + let toolbar_event = intent_to_event(intent, self.toolbar.last_snapshot()); + self.set_toolbar_dragging(drag); + self.handle_toolbar_event(toolbar_event, Some(conn), Some(qh)); + self.toolbar.mark_dirty(); + self.input_state.needs_redraw = true; + self.refresh_keyboard_interactivity(); + } + return TouchTarget::Toolbar; + } + + self.set_pointer_over_toolbar(false); + if target != TouchTarget::Overlay { + return target; + } + + self.set_pending_toast_press(false); + if self.input_state.toast_contains(screen_x, screen_y) { + self.set_pending_toast_press(true); + return target; + } + + if self.board_pan_key_held() && self.can_start_board_pan() { + self.start_board_pan(screen_position.0, screen_position.1); + self.input_state.needs_redraw = true; + return target; + } + + let (wx, wy) = self.zoomed_world_coords(screen_position.0, screen_position.1); + self.input_state + .on_mouse_press_with_canvas(MouseButton::Left, screen_x, screen_y, wx, wy); + self.input_state.needs_redraw = true; + target + } + + fn handle_touch_motion( + &mut self, + conn: &Connection, + surface: &wl_surface::WlSurface, + position: (f64, f64), + target: TouchTarget, + ) { + let Some(screen_position) = self.touch_screen_position(surface, position, target) else { + return; + }; + let screen_x = screen_position.0.round() as i32; + let screen_y = screen_position.1.round() as i32; + self.set_current_mouse(screen_x, screen_y); + + if self.is_move_dragging() + && let Some(kind) = self.active_move_drag_kind() + { + if target == TouchTarget::Toolbar { + self.handle_toolbar_move(kind, position); + } else { + self.handle_toolbar_move_screen(kind, screen_position); + } + self.toolbar.mark_dirty(); + self.input_state.needs_redraw = true; + return; + } + + if target == TouchTarget::InlineToolbar { + let _ = self.inline_toolbar_motion(screen_position); + return; + } + + if target == TouchTarget::Toolbar { + self.set_pointer_over_toolbar(true); + let (wx, wy) = self.zoomed_world_coords(screen_position.0, screen_position.1); + self.input_state + .update_pointer_positions(screen_x, screen_y, wx, wy); + let evt = self.toolbar.pointer_motion(surface, position); + if self.toolbar_dragging() { + let intent = evt.or_else(|| self.move_drag_intent(position.0, position.1)); + if let Some(intent) = intent { + let evt = intent_to_event(intent, self.toolbar.last_snapshot()); + self.handle_toolbar_event(evt, Some(conn), None); + } + } else { + self.toolbar.mark_dirty(); + } + self.input_state.needs_redraw = true; + self.refresh_keyboard_interactivity(); + return; + } + + if target != TouchTarget::Overlay { + return; + } + + if self.board_panning_active() { + let (dx, dy) = self.update_board_pan_position(screen_position.0, screen_position.1); + let _ = self.pan_board_by_screen_delta(dx, dy); + let (wx, wy) = self.zoomed_world_coords(screen_position.0, screen_position.1); + self.input_state + .update_pointer_positions(screen_x, screen_y, wx, wy); + return; + } + + if self.input_state.command_palette_open || self.input_state.tour_active { + return; + } + + let (wx, wy) = self.zoomed_world_coords(screen_position.0, screen_position.1); + self.input_state + .update_pointer_positions(screen_x, screen_y, wx, wy); + self.input_state + .on_mouse_motion_with_canvas(screen_x, screen_y, wx, wy); + } + + fn handle_touch_up( + &mut self, + surface: &wl_surface::WlSurface, + position: (f64, f64), + target: TouchTarget, + ) { + if self.take_suppress_next_release() { + self.set_pending_toast_press(false); + return; + } + + if self.input_state.command_palette_open || self.input_state.tour_active { + self.set_pending_toast_press(false); + self.cancel_active_touch_sequence(); + return; + } + + if target == TouchTarget::Toolbar { + if debug_toolbar_drag_logging_enabled() { + debug!( + "touch release: target={:?}, drag_active={}, toolbar_dragging={}", + target, + self.is_move_dragging(), + self.toolbar_dragging() + ); + } + self.toolbar.pointer_leave(surface); + self.set_pointer_over_toolbar(false); + self.set_toolbar_dragging(false); + self.end_toolbar_move_drag(); + self.toolbar.mark_dirty(); + self.input_state.needs_redraw = true; + return; + } + + let Some(screen_position) = self.touch_screen_position(surface, position, target) else { + return; + }; + let screen_x = screen_position.0.round() as i32; + let screen_y = screen_position.1.round() as i32; + + if self.take_pending_toast_press() { + let (hit, action) = self.input_state.check_toast_click(screen_x, screen_y); + if hit && let Some(action) = action { + self.dispatch_input_action(action); + } + return; + } + + if debug_toolbar_drag_logging_enabled() { + debug!( + "touch release: target={:?}, drag_active={}, toolbar_dragging={}", + target, + self.is_move_dragging(), + self.toolbar_dragging() + ); + } + + if target == TouchTarget::InlineToolbar { + let _ = self.inline_toolbar_release(screen_position); + return; + } + + if self.is_move_dragging() { + self.set_toolbar_dragging(false); + self.end_toolbar_move_drag(); + return; + } + + if target != TouchTarget::Overlay { + return; + } + + if self.board_panning_active() { + self.stop_board_pan(); + self.input_state.needs_redraw = true; + return; + } + + let (wx, wy) = self.zoomed_world_coords(screen_position.0, screen_position.1); + self.input_state.on_mouse_release_with_canvas( + MouseButton::Left, + screen_x, + screen_y, + wx, + wy, + ); + self.input_state.needs_redraw = true; + } +} diff --git a/src/backend/wayland/state.rs b/src/backend/wayland/state.rs index b9487cf5..df5e4a09 100644 --- a/src/backend/wayland/state.rs +++ b/src/backend/wayland/state.rs @@ -24,7 +24,7 @@ use smithay_client_toolkit::{ use std::time::{Duration, Instant}; use wayland_client::{ Proxy, QueueHandle, - protocol::{wl_output, wl_pointer, wl_seat, wl_shm, wl_surface}, + protocol::{wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_touch}, }; #[cfg(tablet)] use wayland_protocols::wp::tablet::zv2::client::{ @@ -187,6 +187,10 @@ pub(super) struct WaylandState { // Pointer cursor pub(super) themed_pointer: Option>, + #[allow(dead_code)] // Retains the WlTouch protocol object while the seat advertises touch. + pub(super) touch: Option, + pub(super) active_touch: TouchState, + pub(super) active_touch_surface: Option, pub(super) locked_pointer: Option, pub(super) current_pointer_shape: Option, pub(super) relative_pointer: Option, @@ -261,6 +265,71 @@ pub(super) struct PendingStylusFrame { pub(super) button_presses: Vec, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(super) enum TouchTarget { + #[default] + None, + Overlay, + Toolbar, + InlineToolbar, + Other, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct TouchState { + active_id: Option, + target: TouchTarget, + last_position: Option<(f64, f64)>, +} + +impl TouchState { + pub(super) fn begin(&mut self, id: i32, position: (f64, f64)) -> bool { + if self.active_id.is_some() { + return false; + } + self.active_id = Some(id); + self.target = TouchTarget::None; + self.last_position = Some(position); + true + } + + pub(super) fn update_position(&mut self, id: i32, position: (f64, f64)) -> bool { + if self.active_id != Some(id) { + return false; + } + self.last_position = Some(position); + true + } + + pub(super) fn is_active_id(&self, id: i32) -> bool { + self.active_id == Some(id) + } + + pub(super) fn is_active(&self) -> bool { + self.active_id.is_some() + } + + pub(super) fn set_target(&mut self, target: TouchTarget) { + if self.active_id.is_some() { + self.target = target; + } + } + + pub(super) fn target(&self) -> TouchTarget { + self.target + } + + pub(super) fn last_position(&self) -> Option<(f64, f64)> { + self.last_position + } + + pub(super) fn clear(&mut self) { + self.active_id = None; + self.target = TouchTarget::None; + self.last_position = None; + } +} + #[cfg(tablet)] impl PendingStylusFrame { pub(super) fn is_empty(&self) -> bool { diff --git a/src/backend/wayland/state/core/init.rs b/src/backend/wayland/state/core/init.rs index ab761173..1cb0b1b7 100644 --- a/src/backend/wayland/state/core/init.rs +++ b/src/backend/wayland/state/core/init.rs @@ -113,6 +113,9 @@ impl WaylandState { zoom: ZoomState::new(zoom_manager), exit_after_capture_mode, themed_pointer: None, + touch: None, + active_touch: TouchState::default(), + active_touch_surface: None, current_pointer_shape: None, locked_pointer: None, relative_pointer: None, diff --git a/src/backend/wayland/state/tests.rs b/src/backend/wayland/state/tests.rs index 3eb3cfe2..11fe2266 100644 --- a/src/backend/wayland/state/tests.rs +++ b/src/backend/wayland/state/tests.rs @@ -94,6 +94,39 @@ fn parse_boolish_env_handles_case_and_on() { assert!(!helpers::parse_boolish_env("0")); } +#[test] +fn touch_state_accepts_only_one_active_contact() { + let mut state = TouchState::default(); + + assert!(state.begin(7, (10.0, 20.0))); + state.set_target(TouchTarget::Overlay); + assert!(!state.begin(8, (30.0, 40.0))); + + assert!(state.is_active()); + assert!(state.is_active_id(7)); + assert!(!state.is_active_id(8)); + assert_eq!(state.target(), TouchTarget::Overlay); + assert_eq!(state.last_position(), Some((10.0, 20.0))); +} + +#[test] +fn touch_state_updates_and_clears_only_active_contact() { + let mut state = TouchState::default(); + + assert!(state.begin(7, (10.0, 20.0))); + assert!(!state.update_position(8, (30.0, 40.0))); + assert_eq!(state.last_position(), Some((10.0, 20.0))); + + assert!(state.update_position(7, (30.0, 40.0))); + assert_eq!(state.last_position(), Some((30.0, 40.0))); + + state.clear(); + assert!(!state.is_active()); + assert!(!state.is_active_id(7)); + assert_eq!(state.target(), TouchTarget::None); + assert_eq!(state.last_position(), None); +} + #[test] fn damage_summary_truncates_after_five_regions() { let mut regions = Vec::new();