git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commiteb604152b2c01e763a5d67f7d06d024b6868e7bc
parent83cde3679a
authorIsaac Freund <[email protected]>
date2025-01-09 16:45
ShellSurface: implement input/focus handling

 protocol/river-window-management-v1.xml | 21 ++++++++++++++++++++
 river/Cursor.zig                        |  6 +++++-
 river/IdleInhibitManager.zig            |  4 ++--
 river/InputPopup.zig                    |  3 ++-
 river/SceneNodeData.zig                 |  2 ++
 river/Seat.zig                          | 35 +++++++++++++++++++++++----------
 river/ShellSurface.zig                  |  4 ++++
 rivercompat/Seat.zig                    |  4 ++++
 rivercompat/main.zig                    |  3 +++
 9 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 7a41e70..ae93d19 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1161,6 +1161,27 @@
       <arg name="window" type="object" interface="river_window_v1"/>
     </event>
 
+    <event name="shell_surface_interaction">
+      <description summary="a shell surface has been interacted with">
+        A shell surface has been interacted with beyond the pointer merely
+        passing over it. This event might be sent due to a pointer button press
+        or due to a touch/tablet tool interaction with the shell_surface.
+
+        There are no guarantees regarding how this event is sent in relation to
+        the pointer_enter and pointer_leave events as the interaction may use
+        touch or tablet tool input.
+
+        Rationale: While the shell surface does receive all wl_pointer,
+        wl_touch, etc. input events for the surface directly, these events do
+        not necessarily trigger an update sequence and therefore do not allow
+        the window manager to update focus in a race-free way.
+
+        This event is double-buffered state and will be followed by a
+        river_window_manager_v1.update event.
+      </description>
+      <arg name="shell_surface" type="object" interface="river_shell_surface_v1"/>
+    </event>
+
     <request name="op_start_serial">
       <description summary="start an interactive operation with a serial">
         Start an interactive seat operation with a serial from either the
diff --git a/river/Cursor.zig b/river/Cursor.zig
index d8ced5b..67d22a3 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -483,7 +483,11 @@ pub fn processAxis(cursor: *Cursor, event: *const wlr.Pointer.event.Axis) void {
 fn interact(cursor: Cursor, result: Scene.AtResult) void {
     switch (result.data) {
         .window => |window| {
-            cursor.seat.pending.window_interaction = window;
+            cursor.seat.pending.interaction = .{ .window = window };
+            server.wm.dirtyPending();
+        },
+        .shell_surface => |shell_surface| {
+            cursor.seat.pending.interaction = .{ .shell_surface = shell_surface };
             server.wm.dirtyPending();
         },
         .lock_surface => |lock_surface| {
diff --git a/river/IdleInhibitManager.zig b/river/IdleInhibitManager.zig
index 7ba4f99..a131787 100644
--- a/river/IdleInhibitManager.zig
+++ b/river/IdleInhibitManager.zig
@@ -54,10 +54,10 @@ pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
         const node_data = SceneNodeData.fromSurface(node.data.wlr_inhibitor.surface) orelse continue;
         switch (node_data.data) {
             .window => {
-                inhibited = true;
+                inhibited = true; // XXX be strict
                 break;
             },
-            .lock_surface, .override_redirect => {
+            .shell_surface, .lock_surface, .override_redirect => {
                 inhibited = true;
                 break;
             },
diff --git a/river/InputPopup.zig b/river/InputPopup.zig
index 84975a2..7c6c576 100644
--- a/river/InputPopup.zig
+++ b/river/InputPopup.zig
@@ -108,7 +108,7 @@ pub fn update(input_popup: *InputPopup) void {
     const focused = SceneNodeData.fromSurface(focused_surface) orelse return;
 
     const output = switch (focused.data) {
-        .window => @panic("TODO"),
+        .window, .shell_surface => @panic("TODO"),
         .lock_surface => |lock_surface| lock_surface.getOutput(),
         // Xwayland doesn't use the text-input protocol
         .override_redirect => unreachable,
@@ -116,6 +116,7 @@ pub fn update(input_popup: *InputPopup) void {
 
     const popup_tree = switch (focused.data) {
         .window => |window| window.popup_tree,
+        .shell_surface => @panic("TODO"),
         .lock_surface => |_| server.scene.layers.popups, // XXX Do we need per-lock-surface popup trees?
         // Xwayland doesn't use the text-input protocol
         .override_redirect => unreachable,
diff --git a/river/SceneNodeData.zig b/river/SceneNodeData.zig
index d830f21..aae8273 100644
--- a/river/SceneNodeData.zig
+++ b/river/SceneNodeData.zig
@@ -25,10 +25,12 @@ const util = @import("util.zig");
 const LockSurface = @import("LockSurface.zig");
 const InputPopup = @import("InputPopup.zig");
 const Window = @import("Window.zig");
+const ShellSurface = @import("ShellSurface.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 pub const Data = union(enum) {
     window: *Window,
+    shell_surface: *ShellSurface,
     lock_surface: *LockSurface,
     override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
 };
diff --git a/river/Seat.zig b/river/Seat.zig
index ee8b659..9907f2f 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -39,6 +39,7 @@ const LockSurface = @import("LockSurface.zig");
 const Output = @import("Output.zig");
 const PointerBinding = @import("PointerBinding.zig");
 const PointerConstraint = @import("PointerConstraint.zig");
+const ShellSurface = @import("ShellSurface.zig");
 const Switch = @import("Switch.zig");
 const Tablet = @import("Tablet.zig");
 const Window = @import("Window.zig");
@@ -87,18 +88,20 @@ pub const WmState = struct {
 pub const WmFocus = union(enum) {
     none,
     window: *Window,
-    // TODO shell_surface: *ShellSurface,
+    shell_surface: *ShellSurface,
 };
 
 pub const Focus = union(enum) {
     none,
     window: *Window,
+    shell_surface: *ShellSurface,
     override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
     lock_surface: *LockSurface,
 
     pub fn surface(target: Focus) ?*wlr.Surface {
         return switch (target) {
             .window => |window| window.rootSurface(),
+            .shell_surface => |shell_surface| shell_surface.surface,
             .override_redirect => |override_redirect| override_redirect.xsurface.surface,
             .lock_surface => |lock_surface| lock_surface.wlr_lock_surface.surface,
             .none => null,
@@ -125,7 +128,7 @@ pending: struct {
     /// The window entered/hovered by the pointer, if any
     window: ?*Window = null,
     /// The window clicked on, touched, etc.
-    window_interaction: ?*Window = null,
+    interaction: WmFocus = .none,
 } = .{},
 
 /// State sent to the window manager client in the latest update sequence.
@@ -365,11 +368,18 @@ pub fn sendDirty(seat: *Seat) void {
             }
         }
 
-        if (seat.pending.window_interaction) |window| {
-            if (window.object) |window_v1| {
-                seat_v1.sendWindowInteraction(window_v1);
-                seat.pending.window_interaction = null;
-            }
+        switch (seat.pending.interaction) {
+            .none => {},
+            .window => |window| {
+                if (window.object) |window_v1| {
+                    seat_v1.sendWindowInteraction(window_v1);
+                    seat.pending.interaction = .none;
+                }
+            },
+            .shell_surface => |shell_surface| {
+                seat_v1.sendShellSurfaceInteraction(shell_surface.object);
+                seat.pending.interaction = .none;
+            },
         }
 
         {
@@ -442,7 +452,11 @@ fn handleRequest(
             const window: *Window = @ptrCast(@alignCast(data));
             seat.uncommitted.focus = .{ .window = window };
         },
-        .focus_shell_surface => {},
+        .focus_shell_surface => |args| {
+            const data = args.shell_surface.getUserData() orelse return;
+            const shell_surface: *ShellSurface = @ptrCast(@alignCast(data));
+            seat.uncommitted.focus = .{ .shell_surface = shell_surface };
+        },
         .clear_focus => seat.uncommitted.focus = .none,
 
         .op_start_serial => {},
@@ -522,6 +536,7 @@ pub fn applyCommitted(seat: *Seat) void {
     switch (seat.committed.focus) {
         .none => seat.focus(.none),
         .window => |window| seat.focus(.{ .window = window }),
+        .shell_surface => |shell_surface| seat.focus(.{ .shell_surface = shell_surface }),
     }
 
     switch (seat.committed.op) {
@@ -601,12 +616,12 @@ pub fn focus(seat: *Seat, new_focus: Focus) void {
     // First clear the current focus
     switch (seat.focused) {
         .window => |window| window.destroyPopups(),
-        .override_redirect, .lock_surface, .none => {},
+        .shell_surface, .override_redirect, .lock_surface, .none => {},
     }
 
     // Set the new focus
     switch (new_focus) {
-        .window => assert(server.lock_manager.state != .locked),
+        .window, .shell_surface => assert(server.lock_manager.state != .locked),
         .lock_surface => assert(server.lock_manager.state != .unlocked),
         .override_redirect, .none => {},
     }
diff --git a/river/ShellSurface.zig b/river/ShellSurface.zig
index 0336e6e..11d33f2 100644
--- a/river/ShellSurface.zig
+++ b/river/ShellSurface.zig
@@ -79,6 +79,8 @@ pub fn create(
     const surfaces = try Scene.SaveableSurfaces.init(tree);
     _ = try surfaces.tree.createSceneSubsurfaceTree(surface);
 
+    try SceneNodeData.attach(&tree.node, .{ .shell_surface = shell_surface });
+
     shell_surface.* = .{
         .object = shell_surface_v1,
         .surface = surface,
@@ -97,6 +99,8 @@ fn handleDestroy(_: *river.ShellSurfaceV1, shell_surface: *ShellSurface) void {
     shell_surface.node.makeInert();
     shell_surface.node.deinit();
 
+    shell_surface.tree.node.destroy();
+
     util.gpa.destroy(shell_surface);
 }
 
diff --git a/rivercompat/Seat.zig b/rivercompat/Seat.zig
index dc48e96..5583d31 100644
--- a/rivercompat/Seat.zig
+++ b/rivercompat/Seat.zig
@@ -96,6 +96,10 @@ fn handleEvent(seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) v
             const window: *Window = @ptrCast(@alignCast(window_v1.getUserData()));
             seat.focus(window);
         },
+        .shell_surface_interaction => |args| {
+            const shell_surface_v1 = args.shell_surface orelse return;
+            seat_v1.focusShellSurface(shell_surface_v1);
+        },
     }
 }
 
diff --git a/rivercompat/main.zig b/rivercompat/main.zig
index e8d07e1..681a7c8 100644
--- a/rivercompat/main.zig
+++ b/rivercompat/main.zig
@@ -56,6 +56,9 @@ const Globals = struct {
                     globals.viewporter = registry.bind(global.name, wp.Viewporter, 1) catch return;
                 } else if (mem.orderZ(u8, global.interface, wp.SinglePixelBufferManagerV1.interface.name) == .eq) {
                     globals.single_pixel = registry.bind(global.name, wp.SinglePixelBufferManagerV1, 1) catch return;
+                } else if (mem.orderZ(u8, global.interface, wl.Seat.interface.name) == .eq) {
+                    const wl_seat = registry.bind(global.name, wl.Seat, 1) catch return;
+                    _ = wl_seat.getKeyboard() catch return;
                 }
             },
             .global_remove => {},