Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(touch): make touchscreens work
The seat never offered WL_SEAT_CAPABILITY_TOUCH, so no client bound
wl_touch and every touch_notify call reached nobody. It is now offered
while a touch device is attached, and each finger is routed once at
touch-down (cursor::TouchRoute):
- a client that bound wl_touch gets real touch events, focus as a click
gives, and the press-time menu dismissals; motion stays surface-local
through the frame frozen at down, so a finger sliding off the window
keeps reporting to it (the old code re-hit-tested every motion and sent
the coordinates of whatever surface happened to be underneath);
- everything else -- every cce-ui app, the bar, the lock screen, the
desktop, and in overview/adjust mode every press -- becomes a held left
button on the real pointer path, so overview drags, handles and the
rubber band work by finger with no touch path of their own;
- further fingers while one drives the pointer are ignored.
Cancel is per point now (wlroots 0.20 carries the id), replacing a C
helper that iterated a list its own calls were freeing. The cursor image
hides on touch and returns on real pointer motion. `ccectl touch
down|motion|up|cancel|tap` drives it in a headless shadow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 49 ++++
src/cce_ctl.rs | 3 +
src/server/cursor.rs | 571 ++++++++++++++++++++++-----------------
src/server/drag_icon.rs | 2 +-
src/server/seat.rs | 38 ++-
src/server/window_manager.rs | 73 +++++
src/server/wlroots_log_wrapper.c | 9 +-
wrapper.h | 2 +-
8 files changed, 499 insertions(+), 248 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index c494b21e..4e2e3d5a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1106,6 +1106,55 @@ headless seat has no keyboard and Chromium crashes in
measurement and window-content readbacks ran every frame to feed a bar-side
scrim.
+### Touchscreens
+
+Until 2026-10-04 the seat never offered the touch capability, so no client
+ever bound `wl_touch` and every `wlr_seat_touch_notify_*` call went
+nowhere: a touchscreen did nothing. The seat now offers it while a touch
+device is attached (`Seat::touch_devices`, counted in `attach_device` /
+`detach_device`), and **each finger is routed once, at touch-down**
+(`cursor::TouchRoute`, decided by `Cursor::touch_route_at`):
+
+- **`Client`** — the surface under it belongs to a client that bound
+ `wl_touch` (`wlr_surface_accepts_touch`: GTK, Qt, Chromium, Xwayland,
+ foot) and the compositor is not in window-adjust mode. It gets real touch
+ events, focus as a click would give, and the press-time menu dismissals
+ (`press_dismissals`, shared with `handle_button`). Motion is mapped through
+ the surface frame frozen at down (origin plus the scene buffer's scale,
+ the pointer implicit grab's `grab_origin`/`grab_scale`), so a finger that
+ slides off the window keeps reporting surface-local positions to it.
+- **`Pointer`** — everything else: a left button held where the finger is,
+ run through the real `handle_button` and motion path. That covers every
+ cce-ui app (the toolkit has no `wl_touch` handling), the status bar, the
+ lock screen, the desktop, and all the compositor's own presses — overview
+ (where every finger takes this route, even over a touch client), the
+ adjust handles, window body drags and the rubber band — none of which
+ needed a touch path of its own. One finger at a time, since the pointer is
+ singular.
+- **`Ignored`** — a second finger while one drives the pointer, or any
+ finger while a real button or seat op holds it.
+
+A cancel on a `Pointer` finger releases the button (a stuck button is worse
+than a stray click), and on a `Client` finger sends `wl_touch.cancel`
+(`river_wlr_seat_touch_cancel_point`, which voids that client's whole
+sequence, the protocol's unit). Unplugging the last touchscreen cancels any
+fingers still down. The cursor image goes away on touch-down
+(`Cursor::hidden_by_touch`; `set_xcursor` and `handle_request_set_cursor`
+both honour it) and real pointer motion brings it back
+(`unhide_after_touch`, which clears pointer focus so the client under it
+re-enters and sets its cursor again).
+
+**Not yet done:** touch gestures (edge swipes, a two-finger pan or pinch of
+the desk) and finger scrolling in cce-ui apps, where a drag is a held button
+and so selects rather than scrolls.
+
+Drive it in a shadow with `ccectl touch down <id> <x> <y>`, `motion <id> <x>
+<y>`, `up <id>`, `cancel <id>` and `tap <x> <y>` (layout pixels). The first
+use sets `Seat::touch_injected`, which offers the capability as a
+touchscreen would, so `weston-simple-touch` under `WAYLAND_DEBUG=1` shows
+the `Client` route's `wl_touch` traffic and a cce-ui app shows the
+`Pointer` route.
+
### Portal global shortcuts
A native Wayland app cannot grab a key; it asks xdg-desktop-portal's
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index 5d7da17b..afa0cce4 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -108,6 +108,9 @@ fn usage(name: &str, to_stderr: bool) {
print(" pointer-click [button] (left|right|middle|back|forward or evdev code)");
print(" pointer-press [button] (held until pointer-release — drives drags)");
print(" pointer-release [button]");
+ print(" touch down <id> <x> <y> | motion <id> <x> <y> | up <id> | cancel <id> | tap <x> <y>");
+ print(" (a touchscreen finger, layout pixels; a client that");
+ print(" bound wl_touch gets touch, anything else the pointer)");
print(" migrate-input (local: move config.kdl keybindings to input.kdl)");
print(" keypress <keycode> (evdev code; press+release to the focused client)");
print(" key-down <keycode> (modifier codes — ctrl 29/97, shift 42/54,");
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 0d034fdf..4a958f43 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -5,6 +5,37 @@ use crate::scene_node_data::SceneNodeDataVal;
use crate::drag_icon::DragIcon;
use std::collections::{HashMap, HashSet};
+/// One finger on a touchscreen (`Cursor::touch_points`).
+#[derive(Clone, Copy, Debug)]
+pub struct TouchPoint {
+ pub lx: f64,
+ pub ly: f64,
+ pub route: TouchRoute,
+}
+
+/// Where a touch point's events go, fixed at touch-down for its lifetime —
+/// a finger that slides onto another window keeps talking to the one it
+/// went down on, exactly like a held button's implicit grab.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum TouchRoute {
+ /// A client that bound `wl_touch` gets real touch events. Positions are
+ /// surface-local through the frame frozen at down: `origin` is the
+ /// surface's layout origin and `scale` its surface-per-layout-pixel
+ /// ratio, the same mapping the pointer's implicit grab uses
+ /// (`grab_origin` / `grab_scale`).
+ Client { origin: (f64, f64), scale: f64 },
+ /// This finger drives the pointer: a left button held at its position.
+ /// Everything that is not a touch-capable client goes this way — cce-ui
+ /// apps, the status bar, the lock screen, the desktop, and every press
+ /// the compositor itself handles (overview, the adjust-mode handles,
+ /// drag-selection) — so all of those work by touch without a touch path
+ /// of their own. One finger at a time: the pointer is singular.
+ Pointer,
+ /// Swallowed until it lifts: a second finger while another drives the
+ /// pointer, or a touch while a real button is held.
+ Ignored,
+}
+
pub struct Cursor {
pub seat: *mut Seat,
pub wlr_cursor: *mut ffi::wlr_cursor,
@@ -58,7 +89,15 @@ pub struct Cursor {
pub tablet_tool_tip_listener: ffi::wl_listener,
pub tablet_tool_button_listener: ffi::wl_listener,
- pub touch_points: HashMap<i32, (f64, f64)>,
+ /// Every finger on a touchscreen, by touch id: where it is (layout
+ /// coordinates; the drag icon of a touch drag follows it) and where its
+ /// events go, decided once at touch-down (`TouchRoute`).
+ pub touch_points: HashMap<i32, TouchPoint>,
+ /// The cursor image is off because the last input was a touchscreen.
+ /// A finger has no pointer to show, and the emulated pointer warping to
+ /// every tap would otherwise leave an arrow wherever the hand last was.
+ /// Real pointer motion brings it back (`unhide_after_touch`).
+ pub hidden_by_touch: bool,
pub pressed: HashMap<u32, Option<*mut crate::pointer_binding::PointerBinding>>,
/// Buttons whose PRESS was forwarded to the focused client. The paired
/// release must reach the client no matter what the compositor is doing
@@ -179,6 +218,7 @@ impl Default for Cursor {
tablet_tool_button_listener: unsafe { std::mem::zeroed() },
touch_points: HashMap::new(),
+ hidden_by_touch: false,
pressed: HashMap::new(),
notified_pressed: HashSet::new(),
grab_origin: (0.0, 0.0),
@@ -591,6 +631,9 @@ impl Cursor {
}
pub unsafe fn set_xcursor(&mut self, name: *const std::os::raw::c_char) {
+ if self.hidden_by_touch {
+ return;
+ }
ffi::wlr_cursor_set_xcursor(
self.wlr_cursor,
self.xcursor_manager,
@@ -1011,6 +1054,33 @@ impl Cursor {
self.clear_focus();
}
+ /// Take the cursor image off for a touch (see `hidden_by_touch`).
+ unsafe fn hide_for_touch(&mut self) {
+ if self.hidden_by_touch {
+ return;
+ }
+ self.hidden_by_touch = true;
+ self.stop_xcursor_animation();
+ ffi::wlr_cursor_unset_image(self.wlr_cursor);
+ }
+
+ /// Bring the cursor image back once a real pointer moves. Called before
+ /// the motion's passthrough, which re-enters the surface under the
+ /// pointer: the clear here is what makes that enter fresh, so the
+ /// client sets its cursor again — `handle_request_set_cursor` dropped
+ /// whatever it asked for while the image was off. Not while a client
+ /// holds an implicit grab: clearing pointer focus would end it.
+ pub unsafe fn unhide_after_touch(&mut self) {
+ if !self.hidden_by_touch {
+ return;
+ }
+ self.hidden_by_touch = false;
+ if self.notified_pressed.is_empty() {
+ ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
+ }
+ self.set_xcursor(b"default\0".as_ptr() as *const _);
+ }
+
pub unsafe fn clear_focus(&mut self) {
ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
self.set_xcursor(b"default\0".as_ptr() as *const _);
@@ -1030,6 +1100,13 @@ impl Cursor {
/// layout pixels — `wlr_cursor_warp_absolute` is 0..1-normalized, which is the
/// bug the old `pointer-move-to` had.
pub unsafe fn inject_motion_to(&mut self, x: f64, y: f64) {
+ self.unhide_after_touch();
+ self.warp_to(x, y);
+ }
+
+ /// `inject_motion_to` without bringing a touch-hidden cursor back: the
+ /// emulated pointer of a touch (`TouchRoute::Pointer`) moves this way.
+ unsafe fn warp_to(&mut self, x: f64, y: f64) {
(*self.seat).handle_activity();
ffi::wlr_cursor_warp(self.wlr_cursor, std::ptr::null_mut(), x, y);
self.update_hovered();
@@ -1304,6 +1381,8 @@ unsafe extern "C" fn handle_motion(listener: *mut ffi::wl_listener, data: *mut s
cursor.end_hscroll_shift("motion");
}
+ cursor.unhide_after_touch();
+
let mut dx = (*event).delta_x;
let mut dy = (*event).delta_y;
@@ -1358,6 +1437,7 @@ unsafe extern "C" fn handle_motion_absolute(listener: *mut ffi::wl_listener, dat
let cursor = &mut *crate::container_of!(listener, Cursor, motion_absolute_listener);
let event = data as *mut ffi::wlr_pointer_motion_absolute_event;
(*cursor.seat).handle_activity();
+ cursor.unhide_after_touch();
let wlr_device = if (*event).pointer.is_null() {
std::ptr::null_mut()
@@ -1408,6 +1488,74 @@ unsafe fn layer_takes_click_focus(layer_surface: *mut crate::layer_shell::LayerS
!= ffi::zwlr_layer_surface_v1_keyboard_interactivity_ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE
}
+/// What every press does before anyone handles it: close the menus it
+/// lands outside of. Shared by a pointer press (`handle_button`) and a
+/// touch that goes to a client as touch (`handle_touch_down`); an
+/// emulated touch arrives as a pointer press and runs it there.
+unsafe fn press_dismissals(server: *mut crate::server::Server, lx: f64, ly: f64) {
+ // Click-away-close for in-surface status menus: if any status
+ // segment is expanded (menu open) and this press did not land on it,
+ // push a one-shot dismiss over the status socket. The line carries
+ // the pressed segment's app_id so a press ON an expanded segment
+ // exempts that segment (it handles its own clicks) while still
+ // dismissing any other open menu.
+ {
+ let mut target_status: *mut crate::window::Window = std::ptr::null_mut();
+ if let Some(result) = (*server).scene.at(lx, ly) {
+ if let SceneNodeDataVal::Window(window) = result.data {
+ if (*window).is_status_bar() {
+ target_status = window;
+ }
+ }
+ }
+ let any_other_expanded = (*server).wm.any_expanded_status_segment(target_status);
+ if any_other_expanded {
+ let except = if target_status.is_null() {
+ "-".to_string()
+ } else {
+ (*target_status).get_app_id_string().unwrap_or_else(|| "-".to_string())
+ };
+ if let Some(ref sender) = (*server).wm.status_sender {
+ sender.send_menu_dismiss(&except);
+ }
+ }
+ }
+
+ // Click-away for X11 popups. Xwayland only sees the pointer over
+ // its own surfaces, so a popup menu an X11 app opened (Wine's, most
+ // of all — a tray icon's menu opened through cce-xembed-tray) never
+ // hears a press on a Wayland window and stays open; only a click on
+ // one of the app's own X windows used to close it. So a press that
+ // lands on no X11 surface while some override-redirect window is
+ // showing is reported on the status socket's `clickaway` topic, and
+ // the bridge closes the popup it opened. The tray bridge's own
+ // containers have no scene tree, so they never count as showing.
+ {
+ let or_showing = (*server)
+ .wm
+ .override_redirects
+ .iter()
+ .any(|&or| !or.is_null() && !(*or).surface_tree.is_null());
+ if or_showing {
+ let on_x11 = match (*server).scene.at(lx, ly) {
+ Some(result) => match result.data {
+ SceneNodeDataVal::Window(window) => {
+ matches!((*window).impl_type, crate::window::WindowImpl::Xwayland(_))
+ }
+ SceneNodeDataVal::OverrideRedirect(_) => true,
+ _ => false,
+ },
+ None => false,
+ };
+ if !on_x11 {
+ if let Some(ref sender) = (*server).wm.status_sender {
+ sender.send_click_away();
+ }
+ }
+ }
+ }
+}
+
unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
let cursor = &mut *crate::container_of!(listener, Cursor, button_listener);
let event = data as *mut ffi::wlr_pointer_button_event;
@@ -1476,67 +1624,7 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
return;
}
- // Click-away-close for in-surface status menus: if any status
- // segment is expanded (menu open) and this press did not land on it,
- // push a one-shot dismiss over the status socket. The line carries
- // the pressed segment's app_id so a press ON an expanded segment
- // exempts that segment (it handles its own clicks) while still
- // dismissing any other open menu.
- {
- let mut target_status: *mut crate::window::Window = std::ptr::null_mut();
- if let Some(result) = (*server).scene.at(lx, ly) {
- if let SceneNodeDataVal::Window(window) = result.data {
- if (*window).is_status_bar() {
- target_status = window;
- }
- }
- }
- let any_other_expanded = (*server).wm.any_expanded_status_segment(target_status);
- if any_other_expanded {
- let except = if target_status.is_null() {
- "-".to_string()
- } else {
- (*target_status).get_app_id_string().unwrap_or_else(|| "-".to_string())
- };
- if let Some(ref sender) = (*server).wm.status_sender {
- sender.send_menu_dismiss(&except);
- }
- }
- }
-
- // Click-away for X11 popups. Xwayland only sees the pointer over
- // its own surfaces, so a popup menu an X11 app opened (Wine's, most
- // of all — a tray icon's menu opened through cce-xembed-tray) never
- // hears a press on a Wayland window and stays open; only a click on
- // one of the app's own X windows used to close it. So a press that
- // lands on no X11 surface while some override-redirect window is
- // showing is reported on the status socket's `clickaway` topic, and
- // the bridge closes the popup it opened. The tray bridge's own
- // containers have no scene tree, so they never count as showing.
- {
- let or_showing = (*server)
- .wm
- .override_redirects
- .iter()
- .any(|&or| !or.is_null() && !(*or).surface_tree.is_null());
- if or_showing {
- let on_x11 = match (*server).scene.at(lx, ly) {
- Some(result) => match result.data {
- SceneNodeDataVal::Window(window) => {
- matches!((*window).impl_type, crate::window::WindowImpl::Xwayland(_))
- }
- SceneNodeDataVal::OverrideRedirect(_) => true,
- _ => false,
- },
- None => false,
- };
- if !on_x11 {
- if let Some(ref sender) = (*server).wm.status_sender {
- sender.send_click_away();
- }
- }
- }
- }
+ press_dismissals(server, lx, ly);
// Status-bar segments are dragged either in adjust-position mode or
// directly with super+left-drag (0x40 = WLR_MODIFIER_LOGO).
@@ -3497,8 +3585,11 @@ unsafe extern "C" fn handle_tablet_tool_axis(listener: *mut ffi::wl_listener, da
}
unsafe extern "C" fn handle_tablet_tool_proximity(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
- let _cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_proximity_listener);
+ let cursor = &mut *crate::container_of!(listener, Cursor, tablet_tool_proximity_listener);
let event = data as *mut ffi::wlr_tablet_tool_proximity_event;
+ // A stylus sets its own image (`TabletTool::proximity`); the touch
+ // hide must not then block the compositor's next one.
+ cursor.hidden_by_touch = false;
let wlr_tablet = (*event).tablet;
let wlr_device = &mut (*wlr_tablet).base as *mut ffi::wlr_input_device;
@@ -3565,219 +3656,215 @@ unsafe extern "C" fn handle_tablet_tool_button(listener: *mut ffi::wl_listener,
}
}
-unsafe extern "C" fn handle_touch_down(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
- let cursor = &mut *crate::container_of!(listener, Cursor, touch_down_listener);
- let event = data as *mut ffi::wlr_touch_down_event;
+// ── Touchscreen ──────────────────────────────────────────────────────────────
+// A finger is routed once, at touch-down (`TouchRoute`): a surface whose
+// client bound `wl_touch` gets real touch events; anything else gets the
+// pointer, a left button held where the finger is. The `Cursor` methods take
+// layout coordinates, so the device handlers below and `ccectl touch` (a
+// headless shadow has no touchscreen) share every line of the routing.
- let seat = &mut *cursor.seat;
- seat.handle_activity();
-
- let mut lx = 0.0;
- let mut ly = 0.0;
- let wlr_device = &mut (*(*event).touch).base as *mut ffi::wlr_input_device;
- ffi::wlr_cursor_absolute_to_layout_coords(
- cursor.wlr_cursor,
- wlr_device,
- (*event).x,
- (*event).y,
- &mut lx,
- &mut ly,
- );
-
- cursor.touch_points.insert((*event).touch_id, (lx, ly));
-
- let server = seat.server;
- if let Some(result) = (*server).scene.at(lx, ly) {
- match result.data {
- SceneNodeDataVal::LayerSurface(layer_surface) => {
- if layer_takes_click_focus(layer_surface) {
- seat.focus(Focus::LayerSurface(result.surface));
- }
- }
- _ => {}
- }
-
- let mut is_app_surface = false;
- let mut is_overlay_window = false;
- match result.data {
- SceneNodeDataVal::Window(window) => {
- if !(*window).is_status_bar() && !(*window).is_wallpaper() {
- is_app_surface = true;
- // Popup counts as chrome like Overlay: the cce-cloud
- // launcher must keep receiving clicks in overview.
- if (*window).tiling_mode == crate::tiling::TilingMode::Overlay
- || (*window).tiling_mode == crate::tiling::TilingMode::Popup
- {
- is_overlay_window = true;
- }
- }
- }
- SceneNodeDataVal::LayerSurface(layer_surface) => {
- if !layer_surface.is_null() {
- is_app_surface = true;
- if is_cloud_layer(layer_surface) {
- is_overlay_window = true;
+impl Cursor {
+ /// Decide where a new finger at `lx, ly` goes. In window-adjust mode
+ /// (overview, or Super held) every press is the compositor's to spend
+ /// — the handles, a window body's drag, the rubber band — and those live
+ /// on the pointer path, so the finger becomes the pointer even over a
+ /// touch-capable window.
+ unsafe fn touch_route_at(&self, lx: f64, ly: f64) -> TouchRoute {
+ let seat = &*self.seat;
+ let server = seat.server;
+ if let Some(result) = (*server).scene.at(lx, ly) {
+ // Locked, only the lock surface may hear a finger, as only it
+ // may hear the pointer (`passthrough`).
+ let locked = (*server).lock_manager.state != crate::lock_manager::LockState::Unlocked;
+ let lock_ok = !locked || matches!(result.data, SceneNodeDataVal::LockSurface(_));
+ if lock_ok
+ && !result.surface.is_null()
+ && !(*server).wm.window_adjust_active()
+ && ffi::wlr_surface_accepts_touch(result.surface, seat.wlr_seat)
+ {
+ // The implicit grab's frame (see the button path): the
+ // surface's scene buffer may be drawn scaled, so map through
+ // its destination size rather than assume 1:1.
+ let mut scale = 1.0;
+ if !result.node.is_null() {
+ let dest_w = ffi::river_scene_buffer_get_dest_width(result.node as *mut ffi::wlr_scene_buffer);
+ let surf_w = ffi::river_wlr_surface_get_width(result.surface);
+ if dest_w > 0 && surf_w > 0 {
+ scale = surf_w as f64 / dest_w as f64;
}
}
+ let origin = (lx - result.sx / scale, ly - result.sy / scale);
+ return TouchRoute::Client { origin, scale };
}
- SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
- is_app_surface = true;
- }
- _ => {}
}
- let should_block_touch = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview && is_app_surface && !is_overlay_window;
-
- if !result.surface.is_null() && !should_block_touch {
- ffi::wlr_seat_touch_notify_down(
- seat.wlr_seat,
- result.surface,
- (*event).time_msec,
- (*event).touch_id,
- result.sx,
- result.sy,
- );
+ // The pointer is singular: a second finger cannot also drive it, and
+ // a real button held (or a seat op it started) owns it already.
+ let pointer_busy = self.touch_points.values().any(|p| p.route == TouchRoute::Pointer)
+ || !self.pressed.is_empty()
+ || seat.op.is_some();
+ if pointer_busy {
+ TouchRoute::Ignored
+ } else {
+ TouchRoute::Pointer
}
}
-}
-
-unsafe extern "C" fn handle_touch_motion(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
- let cursor = &mut *crate::container_of!(listener, Cursor, touch_motion_listener);
- let event = data as *mut ffi::wlr_touch_motion_event;
- let seat = &mut *cursor.seat;
- seat.handle_activity();
-
- if cursor.touch_points.contains_key(&(*event).touch_id) {
- let wlr_device = &mut (*(*event).touch).base as *mut ffi::wlr_input_device;
- let mut lx: f64 = 0.0;
- let mut ly: f64 = 0.0;
- ffi::wlr_cursor_absolute_to_layout_coords(
- cursor.wlr_cursor,
- wlr_device,
- (*event).x,
- (*event).y,
- &mut lx,
- &mut ly,
- );
-
- cursor.touch_points.insert((*event).touch_id, (lx, ly));
-
- cursor.update_drag_icons();
-
- let server = seat.server;
- if let Some(result) = (*server).scene.at(lx, ly) {
- let mut is_app_surface = false;
- let mut is_overlay_window = false;
- match result.data {
- SceneNodeDataVal::Window(window) => {
- if !(*window).is_status_bar() && !(*window).is_wallpaper() {
- is_app_surface = true;
- if (*window).tiling_mode == crate::tiling::TilingMode::Overlay {
- is_overlay_window = true;
+ pub unsafe fn touch_down(&mut self, id: i32, lx: f64, ly: f64, time_msec: u32) {
+ (*self.seat).handle_activity();
+ let server = (*self.seat).server;
+ // A touch is deliberate input, like a press (see `Seat::focus`).
+ (*server).wm.startup_input_seen = true;
+ self.hide_for_touch();
+ // A device that reuses a live id without lifting it first has lost
+ // the up; finish the old point so its button or client is released.
+ if self.touch_points.contains_key(&id) {
+ log::warn!("touch: down for live touch id {id}; lifting the old point first");
+ self.touch_up(id, time_msec);
+ }
+
+ let route = self.touch_route_at(lx, ly);
+ self.touch_points.insert(id, TouchPoint { lx, ly, route });
+ log::debug!("touch: down id={id} at ({lx:.0}, {ly:.0}) -> {route:?}");
+
+ match route {
+ TouchRoute::Client { origin, scale } => {
+ press_dismissals(server, lx, ly);
+ let Some(result) = (*server).scene.at(lx, ly) else { return };
+ // Focus as a click would (`handle_button`).
+ let seat = &mut *self.seat;
+ match result.data {
+ SceneNodeDataVal::Window(window) => {
+ if !(*window).is_status_bar() && !(*window).is_wallpaper() {
+ seat.focus(Focus::Window(window));
}
}
- }
- SceneNodeDataVal::LayerSurface(layer_surface) => {
- if !layer_surface.is_null() {
- is_app_surface = true;
- let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
- if !wlr_layer_surface.is_null() && !(*wlr_layer_surface).namespace.is_null() {
- let ns = std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace).to_string_lossy();
- if ns.starts_with("cce-cloud") {
- is_overlay_window = true;
- }
+ SceneNodeDataVal::LayerSurface(layer_surface) => {
+ if layer_takes_click_focus(layer_surface) {
+ seat.focus(Focus::LayerSurface(result.surface));
}
}
+ _ => {}
}
- SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
- is_app_surface = true;
- }
- _ => {}
+ ffi::wlr_seat_touch_notify_down(
+ seat.wlr_seat,
+ result.surface,
+ time_msec,
+ id,
+ (lx - origin.0) * scale,
+ (ly - origin.1) * scale,
+ );
+ }
+ TouchRoute::Pointer => {
+ self.warp_to(lx, ly);
+ self.inject_button(BTN_LEFT, true);
}
- let should_block_touch = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview && is_app_surface && !is_overlay_window;
+ TouchRoute::Ignored => {}
+ }
+ }
- if !should_block_touch {
+ pub unsafe fn touch_motion(&mut self, id: i32, lx: f64, ly: f64, time_msec: u32) {
+ (*self.seat).handle_activity();
+ let Some(point) = self.touch_points.get_mut(&id) else { return };
+ point.lx = lx;
+ point.ly = ly;
+ let route = point.route;
+ match route {
+ TouchRoute::Client { origin, scale } => {
ffi::wlr_seat_touch_notify_motion(
- seat.wlr_seat,
- (*event).time_msec,
- (*event).touch_id,
- result.sx,
- result.sy,
+ (*self.seat).wlr_seat,
+ time_msec,
+ id,
+ (lx - origin.0) * scale,
+ (ly - origin.1) * scale,
);
+ // A touch drag's icon follows the finger.
+ self.update_drag_icons();
}
+ TouchRoute::Pointer => self.warp_to(lx, ly),
+ TouchRoute::Ignored => {}
}
}
-}
-unsafe extern "C" fn handle_touch_up(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
- let cursor = &mut *crate::container_of!(listener, Cursor, touch_up_listener);
- let event = data as *mut ffi::wlr_touch_up_event;
-
- let seat = &mut *cursor.seat;
- seat.handle_activity();
-
- if let Some((lx, ly)) = cursor.touch_points.remove(&(*event).touch_id) {
- let server = seat.server;
- let mut is_app_surface = false;
- let mut is_overlay_window = false;
- if let Some(result) = (*server).scene.at(lx, ly) {
- match result.data {
- SceneNodeDataVal::Window(window) => {
- if !(*window).is_status_bar() && !(*window).is_wallpaper() {
- is_app_surface = true;
- if (*window).tiling_mode == crate::tiling::TilingMode::Overlay {
- is_overlay_window = true;
- }
- }
- }
- SceneNodeDataVal::LayerSurface(layer_surface) => {
- if !layer_surface.is_null() {
- is_app_surface = true;
- let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
- if !wlr_layer_surface.is_null() && !(*wlr_layer_surface).namespace.is_null() {
- let ns = std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace).to_string_lossy();
- if ns.starts_with("cce-cloud") {
- is_overlay_window = true;
- }
- }
- }
- }
- SceneNodeDataVal::ShellSurface(_) | SceneNodeDataVal::OverrideRedirect(_) => {
- is_app_surface = true;
- }
- _ => {}
+ pub unsafe fn touch_up(&mut self, id: i32, time_msec: u32) {
+ (*self.seat).handle_activity();
+ let Some(point) = self.touch_points.remove(&id) else { return };
+ match point.route {
+ TouchRoute::Client { .. } => {
+ ffi::wlr_seat_touch_notify_up((*self.seat).wlr_seat, time_msec, id);
}
+ TouchRoute::Pointer => self.inject_button(BTN_LEFT, false),
+ TouchRoute::Ignored => {}
}
- let should_block_touch = (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview && is_app_surface && !is_overlay_window;
+ }
- if !should_block_touch {
- ffi::wlr_seat_touch_notify_up(
- seat.wlr_seat,
- (*event).time_msec,
- (*event).touch_id,
- );
+ /// The device gave up on a finger (a palm, a gesture the kernel took).
+ /// A client is told `wl_touch.cancel`, which voids its whole sequence; a
+ /// finger driving the pointer releases the button — a cancelled tap
+ /// still clicks, but a held button must never be left behind.
+ pub unsafe fn touch_cancel(&mut self, id: i32) {
+ (*self.seat).handle_activity();
+ let Some(point) = self.touch_points.remove(&id) else { return };
+ match point.route {
+ TouchRoute::Client { .. } => ffi::river_wlr_seat_touch_cancel_point((*self.seat).wlr_seat, id),
+ TouchRoute::Pointer => self.inject_button(BTN_LEFT, false),
+ TouchRoute::Ignored => {}
+ }
+ }
+
+ pub unsafe fn touch_frame(&mut self) {
+ // The emulated pointer frames each of its own events; only touch
+ // clients are waiting on this one.
+ if self.touch_points.values().any(|p| matches!(p.route, TouchRoute::Client { .. }))
+ || ffi::wlr_seat_touch_num_points((*self.seat).wlr_seat) > 0
+ {
+ ffi::wlr_seat_touch_notify_frame((*self.seat).wlr_seat);
}
}
+
+ /// Layout coordinates of a touch event's normalized `x, y`, through the
+ /// device's output mapping (`map_to_output` / `map_to_region`).
+ unsafe fn touch_layout_coords(&self, touch: *mut ffi::wlr_touch, x: f64, y: f64) -> (f64, f64) {
+ let wlr_device = if touch.is_null() {
+ std::ptr::null_mut()
+ } else {
+ &mut (*touch).base as *mut ffi::wlr_input_device
+ };
+ let mut lx = 0.0;
+ let mut ly = 0.0;
+ ffi::wlr_cursor_absolute_to_layout_coords(self.wlr_cursor, wlr_device, x, y, &mut lx, &mut ly);
+ (lx, ly)
+ }
}
-unsafe extern "C" fn handle_touch_cancel(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
- let cursor = &mut *crate::container_of!(listener, Cursor, touch_cancel_listener);
-
- let seat = &mut *cursor.seat;
- seat.handle_activity();
+unsafe extern "C" fn handle_touch_down(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let cursor = &mut *crate::container_of!(listener, Cursor, touch_down_listener);
+ let event = data as *mut ffi::wlr_touch_down_event;
+ let (lx, ly) = cursor.touch_layout_coords((*event).touch, (*event).x, (*event).y);
+ cursor.touch_down((*event).touch_id, lx, ly, (*event).time_msec);
+}
+
+unsafe extern "C" fn handle_touch_motion(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let cursor = &mut *crate::container_of!(listener, Cursor, touch_motion_listener);
+ let event = data as *mut ffi::wlr_touch_motion_event;
+ let (lx, ly) = cursor.touch_layout_coords((*event).touch, (*event).x, (*event).y);
+ cursor.touch_motion((*event).touch_id, lx, ly, (*event).time_msec);
+}
- cursor.touch_points.clear();
+unsafe extern "C" fn handle_touch_up(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let cursor = &mut *crate::container_of!(listener, Cursor, touch_up_listener);
+ let event = data as *mut ffi::wlr_touch_up_event;
+ cursor.touch_up((*event).touch_id, (*event).time_msec);
+}
- ffi::river_wlr_seat_touch_cancel_all(seat.wlr_seat);
+unsafe extern "C" fn handle_touch_cancel(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let cursor = &mut *crate::container_of!(listener, Cursor, touch_cancel_listener);
+ let event = data as *mut ffi::wlr_touch_cancel_event;
+ cursor.touch_cancel((*event).touch_id);
}
unsafe extern "C" fn handle_touch_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
let cursor = &mut *crate::container_of!(listener, Cursor, touch_frame_listener);
-
- let seat = &mut *cursor.seat;
- seat.handle_activity();
-
- ffi::wlr_seat_touch_notify_frame(seat.wlr_seat);
+ cursor.touch_frame();
}
/// A directional swipe bind fires once the accumulated travel (libinput
diff --git a/src/server/drag_icon.rs b/src/server/drag_icon.rs
index 53a5653e..2e37e76f 100644
--- a/src/server/drag_icon.rs
+++ b/src/server/drag_icon.rs
@@ -66,7 +66,7 @@ impl DragIcon {
}
ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_TOUCH => {
let touch_id = ffi::river_wlr_drag_get_touch_id((*self.wlr_drag_icon).drag);
- if let Some(&(lx, ly)) = (*cursor).touch_points.get(&touch_id) {
+ if let Some(&crate::cursor::TouchPoint { lx, ly, .. }) = (*cursor).touch_points.get(&touch_id) {
ffi::wlr_scene_node_set_position(
self.scene_drag_icon as *mut ffi::wlr_scene_node,
lx as i32,
diff --git a/src/server/seat.rs b/src/server/seat.rs
index cfcbca15..a039e085 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -93,6 +93,16 @@ pub struct Seat {
pub wlr_seat: *mut ffi::wlr_seat,
pub cursor: Cursor,
pub focused: Focus,
+ /// Touchscreens attached to this seat. The seat offers the touch
+ /// capability only while there is one: a client that sees it binds
+ /// `wl_touch`, and from then on a finger on its surface reaches it as
+ /// touch instead of an emulated pointer (`cursor::TouchRoute`). A
+ /// toolkit with no touch handling would otherwise take a capability it
+ /// cannot use and get nothing.
+ pub touch_devices: u32,
+ /// `ccectl touch` has run: offer touch as if a touchscreen were here,
+ /// so a headless shadow can exercise the touch-client route.
+ pub touch_injected: bool,
pub relay: crate::input_relay::InputRelay,
pub layer_shell: crate::layer_shell::LayerShellSeat,
pub xkb_bindings_seat: crate::xkb_bindings::XkbBindingsSeat,
@@ -172,6 +182,8 @@ impl Seat {
wlr_seat,
cursor: Cursor::default(),
focused: Focus::None,
+ touch_devices: 0,
+ touch_injected: false,
relay: std::mem::zeroed(),
layer_shell: crate::layer_shell::LayerShellSeat::default(),
xkb_bindings_seat: crate::xkb_bindings::XkbBindingsSeat::default(),
@@ -390,6 +402,9 @@ impl Seat {
ffi::wlr_cursor_attach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
}
ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH | ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {
+ if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH {
+ self.touch_devices += 1;
+ }
ffi::wlr_cursor_attach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
if !(*device).config.map_to_output.is_null() {
ffi::wlr_cursor_map_input_to_output(self.cursor.wlr_cursor, (*device).wlr_device, (*device).config.map_to_output);
@@ -405,6 +420,17 @@ impl Seat {
ffi::wlr_cursor_detach_input_device(self.cursor.wlr_cursor, (*device).wlr_device);
let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+ if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH {
+ self.touch_devices = self.touch_devices.saturating_sub(1);
+ // An unplugged touchscreen sends no up for the fingers it had
+ // down; one driving the pointer would leave the button held.
+ if self.touch_devices == 0 && !self.touch_injected {
+ let ids: Vec<i32> = self.cursor.touch_points.keys().copied().collect();
+ for id in ids {
+ self.cursor.touch_cancel(id);
+ }
+ }
+ }
if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
let keyboard = (*device).destroy_data as *mut crate::keyboard::Keyboard;
if !keyboard.is_null() {
@@ -428,8 +454,11 @@ impl Seat {
}
pub unsafe fn update_capabilities(&mut self) {
- let caps = ffi::wl_seat_capability_WL_SEAT_CAPABILITY_POINTER
+ let mut caps = ffi::wl_seat_capability_WL_SEAT_CAPABILITY_POINTER
| ffi::wl_seat_capability_WL_SEAT_CAPABILITY_KEYBOARD;
+ if self.touch_devices > 0 || self.touch_injected {
+ caps |= ffi::wl_seat_capability_WL_SEAT_CAPABILITY_TOUCH;
+ }
ffi::wlr_seat_set_capabilities(self.wlr_seat, caps);
}
@@ -2007,6 +2036,13 @@ unsafe extern "C" fn handle_request_set_cursor(
};
let is_wm = !wm_client.is_null() && event_client == wm_client;
+ // While a touch has the image off, nothing may put one back: the
+ // emulated pointer's every enter draws this request.
+ // `Cursor::unhide_after_touch` re-enters the surface, so the client
+ // asks again once the image is wanted.
+ if seat.cursor.hidden_by_touch {
+ return;
+ }
if focused_client == (*event).seat_client || is_wm {
// The client owns the cursor image from here; a compositor-driven
// xcursor animation would paint over it on its next tick.
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 753e48a5..f8892d7d 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -6485,6 +6485,62 @@ impl WindowManager {
"error: invalid x or y\n".to_string()
}
}
+ // ── Synthetic touch (`ccectl touch …`), in layout pixels. Runs the
+ // same `Cursor::touch_*` routing a touchscreen does; the first use
+ // offers the touch capability as a touchscreen would, so clients
+ // in a headless shadow bind `wl_touch` and the client route can be
+ // exercised too.
+ "touch" => {
+ let usage = "error: usage: touch down <id> <x> <y> | motion <id> <x> <y> | up <id> | cancel <id> | tap <x> <y>\n";
+ let id = |i: usize| parts.get(i).and_then(|v| v.parse::<i32>().ok());
+ let num = |i: usize| parts.get(i).and_then(|v| parse_finite(v).ok());
+ let stage = parts.get(1).copied().unwrap_or("");
+ let time = crate::util::msec_timestamp();
+ let ok = match stage {
+ "down" | "motion" => match (id(2), num(3), num(4)) {
+ (Some(id), Some(x), Some(y)) => {
+ self.for_each_seat_touch(|cursor| {
+ if stage == "down" {
+ cursor.touch_down(id, x, y, time);
+ } else {
+ cursor.touch_motion(id, x, y, time);
+ }
+ cursor.touch_frame();
+ });
+ true
+ }
+ _ => false,
+ },
+ "up" | "cancel" => match id(2) {
+ Some(id) => {
+ self.for_each_seat_touch(|cursor| {
+ if stage == "up" {
+ cursor.touch_up(id, time);
+ } else {
+ cursor.touch_cancel(id);
+ }
+ cursor.touch_frame();
+ });
+ true
+ }
+ None => false,
+ },
+ "tap" => match (num(2), num(3)) {
+ (Some(x), Some(y)) => {
+ self.for_each_seat_touch(|cursor| {
+ cursor.touch_down(0, x, y, time);
+ cursor.touch_frame();
+ cursor.touch_up(0, time);
+ cursor.touch_frame();
+ });
+ true
+ }
+ _ => false,
+ },
+ _ => false,
+ };
+ if ok { "ok\n".to_string() } else { usage.to_string() }
+ }
"pointer-move-by" => {
if parts.len() < 3 { return "error: usage: pointer-move-by <dx> <dy>\n".to_string(); }
if let (Ok(dx), Ok(dy)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
@@ -6775,6 +6831,23 @@ impl WindowManager {
}
}
+ /// `for_each_cursor` for `ccectl touch`: marks each seat as having had
+ /// touch injected first, which offers clients the touch capability.
+ unsafe fn for_each_seat_touch(&mut self, mut f: impl FnMut(&mut crate::cursor::Cursor)) {
+ let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+ let mut curr_seat = (*seats_list).next;
+ while curr_seat != seats_list {
+ let next_seat = (*curr_seat).next;
+ let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
+ if !(*seat).touch_injected {
+ (*seat).touch_injected = true;
+ (*seat).update_capabilities();
+ }
+ f(&mut (*seat).cursor);
+ curr_seat = next_seat;
+ }
+ }
+
/// Button-name/evdev-code parsing for the pointer commands; a missing argument
/// means the left button, like wlrctl.
fn parse_pointer_button(arg: Option<&str>) -> Option<u32> {
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index a1ab0ab6..99ca06e8 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -645,9 +645,12 @@ struct wlr_tablet_tool *river_wlr_tablet_v2_tablet_tool_get_wlr_tool(struct wlr_
return tool->wlr_tool;
}
-void river_wlr_seat_touch_cancel_all(struct wlr_seat *wlr_seat) {
- struct wlr_touch_point *point, *tmp;
- wl_list_for_each_safe(point, tmp, &wlr_seat->touch_state.touch_points, link) {
+// wl_touch.cancel is per client: it voids every point that client holds,
+// and wlr_seat_touch_notify_cancel destroys them all. So cancelling one
+// finger cancels its client's whole sequence — the protocol's unit.
+void river_wlr_seat_touch_cancel_point(struct wlr_seat *wlr_seat, int32_t touch_id) {
+ struct wlr_touch_point *point = wlr_seat_touch_get_point(wlr_seat, touch_id);
+ if (point != NULL && point->client != NULL) {
wlr_seat_touch_notify_cancel(wlr_seat, point->client);
}
}
diff --git a/wrapper.h b/wrapper.h
index 0a28bb2b..a2a17c6c 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -250,7 +250,7 @@ bool river_wlr_tablet_v2_tablet_tool_get_is_down(struct wlr_tablet_v2_tablet_too
size_t river_wlr_tablet_v2_tablet_tool_get_num_buttons(struct wlr_tablet_v2_tablet_tool *tool);
struct wlr_tablet_tool *river_wlr_tablet_v2_tablet_tool_get_wlr_tool(struct wlr_tablet_v2_tablet_tool *tool);
-void river_wlr_seat_touch_cancel_all(struct wlr_seat *wlr_seat);
+void river_wlr_seat_touch_cancel_point(struct wlr_seat *wlr_seat, int32_t touch_id);
struct wlr_surface *river_wlr_seat_get_keyboard_focused_surface(struct wlr_seat *seat);
int river_wlr_surface_get_width(struct wlr_surface *surface);