Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Seat: start integration with rwm state machine
build.zig.zon | 4 +-
protocol/river-window-management-v1.xml | 20 +-
river/Cursor.zig | 578 ++++++++++++++------------------
river/ForeignToplevelHandle.zig | 16 +-
river/InputManager.zig | 28 +-
river/Keyboard.zig | 35 +-
river/LockManager.zig | 14 +-
river/LockSurface.zig | 16 +-
river/Output.zig | 7 +-
river/Seat.zig | 302 +++++++++++++++--
river/Server.zig | 2 +-
river/Window.zig | 38 ++-
river/WindowManager.zig | 50 ++-
river/XdgToplevel.zig | 4 +-
river/XwaylandOverrideRedirect.zig | 5 +-
rivercompat/WindowManager.zig | 3 +
16 files changed, 668 insertions(+), 454 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 9237e55..8aa09e0 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -12,8 +12,8 @@
.hash = "12206015b1750462788de094b8154eab733a36523639124ace437d4631ca01b5ba18",
},
.@"zig-wlroots" = .{
- .url = "https://codeberg.org/ifreund/zig-wlroots/archive/a2372a124d9b8568775849a2ea60da36c4f5b0dd.tar.gz",
- .hash = "122019ca283db70a0f4932e54b5195e2392b9220c7f97808d0e74c14be30aea4a458",
+ .url = "https://codeberg.org/ifreund/zig-wlroots/archive/70a4de5cd6886003e54f55239d2753b025271bc9.tar.gz",
+ .hash = "122026eea643c568b68cb58d925344033bc143b8ff2f43b0851a4577f68c83b9f96c",
},
.@"zig-xkbcommon" = .{
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz",
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 1523008..91a2365 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1021,22 +1021,18 @@
<arg name="window" type="object" interface="river_window_v1"/>
</event>
- <event name="pointer_motion">
- <description summary="pointer moved">
- The seat's pointer moved. The x and y coordinates are in the
- compositor's logical coordinate space.
+ <event name="pointer_activity">
+ <description summary="the pointer was moved">
+ The seat's pointer was moved.
- Rationale: pointer motion events in some form are necessary to implement
- the "always" style of focus-follows-cursor. Choosing to expose global
- pointer position/motion information to the window manager rather than
- motion events relative to a window allows other potentially interesting
- features, for example hot corners.
+ Rationale: The motivating window manager feature for this event is the
+ "always" style of focus-follows-cursor. Waiting for the window manager
+ to ack and commit in response to every single pointer motion event is
+ noisy, wasteful, and unnecessary.
This event is double-buffered state and will be followed by a
river_window_manager_v1.update event.
</description>
- <arg name="x" type="int"/>
- <arg name="y" type="int"/>
</event>
<event name="window_interaction">
@@ -1077,7 +1073,7 @@
<request name="pointer_resize_window">
<description summary="start interactive pointer resize of a window">
- This request causes the a window to be resized from the specified edges,
+ This request causes the window to be resized from the specified edges,
updating the position/dimensions of the window as the pointer is moved.
Multiple windows may be interactively resized at the same time.
diff --git a/river/Cursor.zig b/river/Cursor.zig
index dadb308..38c900e 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 The River Developers
+// Copyright 2020-2024 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -43,6 +43,8 @@ const TabletTool = @import("TabletTool.zig");
const Window = @import("Window.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
+const log = std.log.scoped(.cursor);
+
const Mode = union(enum) {
passthrough: void,
down: struct {
@@ -107,8 +109,6 @@ const LayoutPoint = struct {
ly: f64,
};
-const log = std.log.scoped(.cursor);
-
/// Current cursor mode as well as any state needed to implement that mode
mode: Mode = .passthrough,
@@ -139,28 +139,31 @@ constraint: ?*PointerConstraint = null,
/// This information is necessary for proper touch dnd support if there are multiple touch points.
touch_points: std.AutoHashMapUnmanaged(i32, LayoutPoint) = .{},
-axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(handleAxis),
-frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(handleFrame),
-button: wl.Listener(*wlr.Pointer.event.Button) =
- wl.Listener(*wlr.Pointer.event.Button).init(handleButton),
-motion_absolute: wl.Listener(*wlr.Pointer.event.MotionAbsolute) =
- wl.Listener(*wlr.Pointer.event.MotionAbsolute).init(handleMotionAbsolute),
-motion: wl.Listener(*wlr.Pointer.event.Motion) =
- wl.Listener(*wlr.Pointer.event.Motion).init(handleMotion),
-pinch_begin: wl.Listener(*wlr.Pointer.event.PinchBegin) =
- wl.Listener(*wlr.Pointer.event.PinchBegin).init(handlePinchBegin),
-pinch_update: wl.Listener(*wlr.Pointer.event.PinchUpdate) =
- wl.Listener(*wlr.Pointer.event.PinchUpdate).init(handlePinchUpdate),
-pinch_end: wl.Listener(*wlr.Pointer.event.PinchEnd) =
- wl.Listener(*wlr.Pointer.event.PinchEnd).init(handlePinchEnd),
request_set_cursor: wl.Listener(*wlr.Seat.event.RequestSetCursor) =
wl.Listener(*wlr.Seat.event.RequestSetCursor).init(handleRequestSetCursor),
+
+motion_relative: wl.Listener(*wlr.Pointer.event.Motion) =
+ wl.Listener(*wlr.Pointer.event.Motion).init(queueMotionRelative),
+motion_absolute: wl.Listener(*wlr.Pointer.event.MotionAbsolute) =
+ wl.Listener(*wlr.Pointer.event.MotionAbsolute).init(queueMotionAbsolute),
+button: wl.Listener(*wlr.Pointer.event.Button) =
+ wl.Listener(*wlr.Pointer.event.Button).init(queueButton),
+axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(queueAxis),
+frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(queueFrame),
+
swipe_begin: wl.Listener(*wlr.Pointer.event.SwipeBegin) =
- wl.Listener(*wlr.Pointer.event.SwipeBegin).init(handleSwipeBegin),
+ wl.Listener(*wlr.Pointer.event.SwipeBegin).init(queueSwipeBegin),
swipe_update: wl.Listener(*wlr.Pointer.event.SwipeUpdate) =
- wl.Listener(*wlr.Pointer.event.SwipeUpdate).init(handleSwipeUpdate),
+ wl.Listener(*wlr.Pointer.event.SwipeUpdate).init(queueSwipeUpdate),
swipe_end: wl.Listener(*wlr.Pointer.event.SwipeEnd) =
- wl.Listener(*wlr.Pointer.event.SwipeEnd).init(handleSwipeEnd),
+ wl.Listener(*wlr.Pointer.event.SwipeEnd).init(queueSwipeEnd),
+
+pinch_begin: wl.Listener(*wlr.Pointer.event.PinchBegin) =
+ wl.Listener(*wlr.Pointer.event.PinchBegin).init(queuePinchBegin),
+pinch_update: wl.Listener(*wlr.Pointer.event.PinchUpdate) =
+ wl.Listener(*wlr.Pointer.event.PinchUpdate).init(queuePinchUpdate),
+pinch_end: wl.Listener(*wlr.Pointer.event.PinchEnd) =
+ wl.Listener(*wlr.Pointer.event.PinchEnd).init(queuePinchEnd),
touch_down: wl.Listener(*wlr.Touch.event.Down) =
wl.Listener(*wlr.Touch.event.Down).init(handleTouchDown),
@@ -199,23 +202,21 @@ pub fn init(cursor: *Cursor, seat: *Seat) !void {
};
try cursor.setTheme(null, null);
- // wlr_cursor *only* displays an image on screen. It does not move around
- // when the pointer moves. However, we can attach input devices to it, and
- // it will generate aggregate events for all of them. In these events, we
- // can choose how we want to process them, forwarding them to clients and
- // moving the cursor around.
- wlr_cursor.events.axis.add(&cursor.axis);
+ seat.wlr_seat.events.request_set_cursor.add(&cursor.request_set_cursor);
+
+ wlr_cursor.events.motion.add(&cursor.motion_relative);
+ wlr_cursor.events.motion_absolute.add(&cursor.motion_absolute);
wlr_cursor.events.button.add(&cursor.button);
+ wlr_cursor.events.axis.add(&cursor.axis);
wlr_cursor.events.frame.add(&cursor.frame);
- wlr_cursor.events.motion_absolute.add(&cursor.motion_absolute);
- wlr_cursor.events.motion.add(&cursor.motion);
+
wlr_cursor.events.swipe_begin.add(&cursor.swipe_begin);
wlr_cursor.events.swipe_update.add(&cursor.swipe_update);
wlr_cursor.events.swipe_end.add(&cursor.swipe_end);
+
wlr_cursor.events.pinch_begin.add(&cursor.pinch_begin);
wlr_cursor.events.pinch_update.add(&cursor.pinch_update);
wlr_cursor.events.pinch_end.add(&cursor.pinch_end);
- seat.wlr_seat.events.request_set_cursor.add(&cursor.request_set_cursor);
wlr_cursor.events.touch_down.add(&cursor.touch_down);
wlr_cursor.events.touch_motion.add(&cursor.touch_motion);
@@ -282,42 +283,164 @@ pub fn setXcursor(cursor: *Cursor, name: [*:0]const u8) void {
cursor.xcursor_name = name;
}
+fn handleRequestSetCursor(
+ listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor),
+ event: *wlr.Seat.event.RequestSetCursor,
+) void {
+ // This event is rasied by the seat when a client provides a cursor image
+ const cursor: *Cursor = @fieldParentPtr("request_set_cursor", listener);
+ const focused_client = cursor.seat.wlr_seat.pointer_state.focused_client;
+
+ // This can be sent by any client, so we check to make sure this one is
+ // actually has pointer focus first.
+ if (focused_client == event.seat_client) {
+ // Once we've vetted the client, we can tell the cursor to use the
+ // provided surface as the cursor image. It will set the hardware cursor
+ // on the output that it's currently on and continue to do so as the
+ // cursor moves between outputs.
+ log.debug("focused client set cursor", .{});
+ cursor.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
+ cursor.xcursor_name = null;
+ }
+}
+
fn clearFocus(cursor: *Cursor) void {
cursor.setXcursor("default");
cursor.seat.wlr_seat.pointerNotifyClearFocus();
}
-/// Axis event is a scroll wheel or similiar
-fn handleAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Pointer.event.Axis) void {
- const cursor: *Cursor = @fieldParentPtr("axis", listener);
- const device: *InputDevice = @ptrFromInt(event.device.data);
+pub fn processMotionRelative(cursor: *Cursor, event: *const wlr.Pointer.event.Motion) void {
+ server.input_manager.relative_pointer_manager.sendRelativeMotion(
+ cursor.seat.wlr_seat,
+ @as(u64, event.time_msec) * 1000,
+ event.delta_x,
+ event.delta_y,
+ event.unaccel_dx,
+ event.unaccel_dy,
+ );
- cursor.seat.handleActivity();
+ var dx: f64 = event.delta_x;
+ var dy: f64 = event.delta_y;
- // Notify the client with pointer focus of the axis event.
- cursor.seat.wlr_seat.pointerNotifyAxis(
- event.time_msec,
- event.orientation,
- event.delta * device.config.scroll_factor,
- @intFromFloat(math.clamp(
- @round(@as(f32, @floatFromInt(event.delta_discrete)) * device.config.scroll_factor),
- // It seems that clamping to exactly the bounds of an i32 is insufficient to make the
- // @intFromFloat() call safe due to the max/min i32 not being exactly representable
- // by an f32. Dividing by 2 is a low effort way to ensure the value is in bounds and
- // allow users to set their scroll-factor to inf without crashing river.
- math.minInt(i32) / 2,
- math.maxInt(i32) / 2,
- )),
- event.source,
- event.relative_direction,
- );
+ if (cursor.constraint) |constraint| {
+ if (constraint.state == .active) {
+ switch (constraint.wlr_constraint.type) {
+ .locked => return,
+ .confined => constraint.confine(&dx, &dy),
+ }
+ }
+ }
+
+ switch (cursor.mode) {
+ .passthrough, .down => {
+ cursor.wlr_cursor.move(event.device, dx, dy);
+
+ switch (cursor.mode) {
+ .passthrough => {
+ cursor.passthrough(event.time_msec);
+ },
+ .down => |data| {
+ cursor.seat.wlr_seat.pointerNotifyMotion(
+ event.time_msec,
+ data.sx + (cursor.wlr_cursor.x - data.lx),
+ data.sy + (cursor.wlr_cursor.y - data.ly),
+ );
+ },
+ else => unreachable,
+ }
+
+ cursor.updateDragIcons();
+
+ if (cursor.constraint) |constraint| {
+ constraint.maybeActivate();
+ }
+ },
+ .move => |*data| {
+ dx += data.delta_x;
+ dy += data.delta_y;
+ data.delta_x = dx - @trunc(dx);
+ data.delta_y = dy - @trunc(dy);
+
+ // XXX move window
+
+ server.wm.dirtyPending();
+ },
+ .resize => |*data| {
+ dx += data.delta_x;
+ dy += data.delta_y;
+ data.delta_x = dx - @trunc(dx);
+ data.delta_y = dy - @trunc(dy);
+
+ data.x += @intFromFloat(dx);
+ data.y += @intFromFloat(dy);
+
+ if (true) return; // XXX resize window
+
+ // Modify width/height of the pending box, taking constraints into account
+ // The x/y coordinates of the window will be adjusted as needed in Window.resizeCommit()
+ // based on the dimensions actually committed by the client.
+ const border_width = if (data.window.pending.ssd) server.config.border_width else 0;
+
+ // TODO
+ const output_width: i32 = 1920;
+ const output_height: i32 = 1080;
+
+ const constraints = &data.window.constraints;
+ const box = &data.window.pending.box;
+
+ if (data.edges.left) {
+ const x2 = box.x + box.width;
+ box.width = data.initial_width - data.x;
+ box.width = @max(box.width, constraints.min_width);
+ box.width = @min(box.width, constraints.max_width);
+ box.width = @min(box.width, x2 - border_width);
+ data.x = data.initial_width - box.width;
+ } else if (data.edges.right) {
+ box.width = data.initial_width + data.x;
+ box.width = @max(box.width, constraints.min_width);
+ box.width = @min(box.width, constraints.max_width);
+ box.width = @min(box.width, output_width - border_width - box.x);
+ data.x = box.width - data.initial_width;
+ }
+
+ if (data.edges.top) {
+ const y2 = box.y + box.height;
+ box.height = data.initial_height - data.y;
+ box.height = @max(box.height, constraints.min_height);
+ box.height = @min(box.height, constraints.max_height);
+ box.height = @min(box.height, y2 - border_width);
+ data.y = data.initial_height - box.height;
+ } else if (data.edges.bottom) {
+ box.height = data.initial_height + data.y;
+ box.height = @max(box.height, constraints.min_height);
+ box.height = @min(box.height, constraints.max_height);
+ box.height = @min(box.height, output_height - border_width - box.y);
+ data.y = box.height - data.initial_height;
+ }
+
+ server.wm.dirtyPending();
+ },
+ }
}
-fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.Pointer.event.Button) void {
- const cursor: *Cursor = @fieldParentPtr("button", listener);
+pub fn processMotionAbsolute(cursor: *Cursor, event: *const wlr.Pointer.event.MotionAbsolute) void {
+ var lx: f64 = undefined;
+ var ly: f64 = undefined;
+ cursor.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &lx, &ly);
- cursor.seat.handleActivity();
+ const dx = lx - cursor.wlr_cursor.x;
+ const dy = ly - cursor.wlr_cursor.y;
+ cursor.processMotionRelative(&.{
+ .device = event.device,
+ .time_msec = event.time_msec,
+ .delta_x = dx,
+ .delta_y = dy,
+ .unaccel_dx = dx,
+ .unaccel_dy = dy,
+ });
+}
+pub fn processButton(cursor: *Cursor, event: *const wlr.Pointer.event.Button) void {
if (event.state == .released) {
assert(cursor.pressed_count > 0);
cursor.pressed_count -= 1;
@@ -357,7 +480,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
return;
}
- cursor.updateKeyboardFocus(result);
+ cursor.interact(result);
_ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
@@ -372,19 +495,37 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
};
}
}
+}
- server.wm.dirtyPending();
+pub fn processAxis(cursor: *Cursor, event: *const wlr.Pointer.event.Axis) void {
+ const device: *InputDevice = @ptrFromInt(event.device.data);
+ cursor.seat.wlr_seat.pointerNotifyAxis(
+ event.time_msec,
+ event.orientation,
+ event.delta * device.config.scroll_factor,
+ @intFromFloat(math.clamp(
+ @round(@as(f32, @floatFromInt(event.delta_discrete)) * device.config.scroll_factor),
+ // It seems that clamping to exactly the bounds of an i32 is insufficient to make the
+ // @intFromFloat() call safe due to the max/min i32 not being exactly representable
+ // by an f32. Dividing by 2 is a low effort way to ensure the value is in bounds and
+ // allow users to set their scroll-factor to inf without crashing river.
+ math.minInt(i32) / 2,
+ math.maxInt(i32) / 2,
+ )),
+ event.source,
+ event.relative_direction,
+ );
}
-/// Requires a call to WindowManager.dirtyPending()
-fn updateKeyboardFocus(cursor: Cursor, result: Scene.AtResult) void {
+fn interact(cursor: Cursor, result: Scene.AtResult) void {
switch (result.data) {
.window => |window| {
- cursor.seat.focus(window);
+ cursor.seat.pending.window_interaction = window;
+ server.wm.dirtyPending();
},
.lock_surface => |lock_surface| {
assert(server.lock_manager.state != .unlocked);
- cursor.seat.setFocusRaw(.{ .lock_surface = lock_surface });
+ cursor.seat.focus(.{ .lock_surface = lock_surface });
},
.override_redirect => |override_redirect| {
assert(server.lock_manager.state != .locked);
@@ -393,82 +534,6 @@ fn updateKeyboardFocus(cursor: Cursor, result: Scene.AtResult) void {
}
}
-fn handlePinchBegin(
- listener: *wl.Listener(*wlr.Pointer.event.PinchBegin),
- event: *wlr.Pointer.event.PinchBegin,
-) void {
- const cursor: *Cursor = @fieldParentPtr("pinch_begin", listener);
- server.input_manager.pointer_gestures.sendPinchBegin(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.fingers,
- );
-}
-
-fn handlePinchUpdate(
- listener: *wl.Listener(*wlr.Pointer.event.PinchUpdate),
- event: *wlr.Pointer.event.PinchUpdate,
-) void {
- const cursor: *Cursor = @fieldParentPtr("pinch_update", listener);
- server.input_manager.pointer_gestures.sendPinchUpdate(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.dx,
- event.dy,
- event.scale,
- event.rotation,
- );
-}
-
-fn handlePinchEnd(
- listener: *wl.Listener(*wlr.Pointer.event.PinchEnd),
- event: *wlr.Pointer.event.PinchEnd,
-) void {
- const cursor: *Cursor = @fieldParentPtr("pinch_end", listener);
- server.input_manager.pointer_gestures.sendPinchEnd(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.cancelled,
- );
-}
-
-fn handleSwipeBegin(
- listener: *wl.Listener(*wlr.Pointer.event.SwipeBegin),
- event: *wlr.Pointer.event.SwipeBegin,
-) void {
- const cursor: *Cursor = @fieldParentPtr("swipe_begin", listener);
- server.input_manager.pointer_gestures.sendSwipeBegin(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.fingers,
- );
-}
-
-fn handleSwipeUpdate(
- listener: *wl.Listener(*wlr.Pointer.event.SwipeUpdate),
- event: *wlr.Pointer.event.SwipeUpdate,
-) void {
- const cursor: *Cursor = @fieldParentPtr("swipe_update", listener);
- server.input_manager.pointer_gestures.sendSwipeUpdate(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.dx,
- event.dy,
- );
-}
-
-fn handleSwipeEnd(
- listener: *wl.Listener(*wlr.Pointer.event.SwipeEnd),
- event: *wlr.Pointer.event.SwipeEnd,
-) void {
- const cursor: *Cursor = @fieldParentPtr("swipe_end", listener);
- server.input_manager.pointer_gestures.sendSwipeEnd(
- cursor.seat.wlr_seat,
- event.time_msec,
- event.cancelled,
- );
-}
-
fn handleTouchDown(
listener: *wl.Listener(*wlr.Touch.event.Down),
event: *wlr.Touch.event.Down,
@@ -487,7 +552,7 @@ fn handleTouchDown(
};
if (server.scene.at(lx, ly)) |result| {
- cursor.updateKeyboardFocus(result);
+ cursor.interact(result);
if (result.surface) |surface| {
_ = cursor.seat.wlr_seat.touchNotifyDown(
@@ -499,8 +564,6 @@ fn handleTouchDown(
);
}
}
-
- server.wm.dirtyPending();
}
fn handleTouchMotion(
@@ -617,8 +680,9 @@ fn handleTabletToolButton(
/// Handle the mapping for the passed button if any. Returns true if there
/// was a mapping and the button was handled.
-fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, _: *Window) bool {
+fn handlePointerMapping(cursor: *Cursor, event: *const wlr.Pointer.event.Button, _: *Window) bool {
const wlr_keyboard = cursor.seat.wlr_seat.getKeyboard() orelse return false;
+ // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
const modifiers = wlr_keyboard.getModifiers();
return for (server.config.pointer_mappings.items) |mapping| {
@@ -629,71 +693,6 @@ fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, _: *W
} else false;
}
-/// Frame events are sent after regular pointer events to group multiple
-/// events together. For instance, two axis events may happen at the same
-/// time, in which case a frame event won't be sent in between.
-fn handleFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void {
- const cursor: *Cursor = @fieldParentPtr("frame", listener);
- cursor.seat.wlr_seat.pointerNotifyFrame();
-}
-
-/// This event is forwarded by the cursor when a pointer emits an _absolute_
-/// motion event, from 0..1 on each axis. This happens, for example, when
-/// wlroots is running under a Wayland window rather than KMS+DRM, and you
-/// move the mouse over the window. You could enter the window from any edge,
-/// so we have to warp the mouse there. There is also some hardware which
-/// emits these events.
-fn handleMotionAbsolute(
- listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute),
- event: *wlr.Pointer.event.MotionAbsolute,
-) void {
- const cursor: *Cursor = @fieldParentPtr("motion_absolute", listener);
-
- cursor.seat.handleActivity();
-
- var lx: f64 = undefined;
- var ly: f64 = undefined;
- cursor.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &lx, &ly);
-
- const dx = lx - cursor.wlr_cursor.x;
- const dy = ly - cursor.wlr_cursor.y;
- cursor.processMotion(event.device, event.time_msec, dx, dy, dx, dy);
-}
-
-/// This event is forwarded by the cursor when a pointer emits a _relative_
-/// pointer motion event (i.e. a delta)
-fn handleMotion(
- listener: *wl.Listener(*wlr.Pointer.event.Motion),
- event: *wlr.Pointer.event.Motion,
-) void {
- const cursor: *Cursor = @fieldParentPtr("motion", listener);
-
- cursor.seat.handleActivity();
-
- cursor.processMotion(event.device, event.time_msec, event.delta_x, event.delta_y, event.unaccel_dx, event.unaccel_dy);
-}
-
-fn handleRequestSetCursor(
- listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor),
- event: *wlr.Seat.event.RequestSetCursor,
-) void {
- // This event is rasied by the seat when a client provides a cursor image
- const cursor: *Cursor = @fieldParentPtr("request_set_cursor", listener);
- const focused_client = cursor.seat.wlr_seat.pointer_state.focused_client;
-
- // This can be sent by any client, so we check to make sure this one is
- // actually has pointer focus first.
- if (focused_client == event.seat_client) {
- // Once we've vetted the client, we can tell the cursor to use the
- // provided surface as the cursor image. It will set the hardware cursor
- // on the output that it's currently on and continue to do so as the
- // cursor moves between outputs.
- log.debug("focused client set cursor", .{});
- cursor.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
- cursor.xcursor_name = null;
- }
-}
-
pub fn startMove(cursor: *Cursor, window: *Window) void {
if (cursor.constraint) |constraint| {
if (constraint.state == .active) constraint.deactivate();
@@ -790,120 +789,6 @@ fn enterMode(cursor: *Cursor, mode: Mode, window: *Window, xcursor_name: [*:0]co
server.wm.dirtyPending();
}
-fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void {
- server.input_manager.relative_pointer_manager.sendRelativeMotion(
- cursor.seat.wlr_seat,
- @as(u64, time) * 1000,
- delta_x,
- delta_y,
- unaccel_dx,
- unaccel_dy,
- );
-
- var dx: f64 = delta_x;
- var dy: f64 = delta_y;
-
- if (cursor.constraint) |constraint| {
- if (constraint.state == .active) {
- switch (constraint.wlr_constraint.type) {
- .locked => return,
- .confined => constraint.confine(&dx, &dy),
- }
- }
- }
-
- switch (cursor.mode) {
- .passthrough, .down => {
- cursor.wlr_cursor.move(device, dx, dy);
-
- switch (cursor.mode) {
- .passthrough => {
- cursor.passthrough(time);
- },
- .down => |data| {
- cursor.seat.wlr_seat.pointerNotifyMotion(
- time,
- data.sx + (cursor.wlr_cursor.x - data.lx),
- data.sy + (cursor.wlr_cursor.y - data.ly),
- );
- },
- else => unreachable,
- }
-
- cursor.updateDragIcons();
-
- if (cursor.constraint) |constraint| {
- constraint.maybeActivate();
- }
- },
- .move => |*data| {
- dx += data.delta_x;
- dy += data.delta_y;
- data.delta_x = dx - @trunc(dx);
- data.delta_y = dy - @trunc(dy);
-
- // XXX move window
-
- server.wm.dirtyPending();
- },
- .resize => |*data| {
- dx += data.delta_x;
- dy += data.delta_y;
- data.delta_x = dx - @trunc(dx);
- data.delta_y = dy - @trunc(dy);
-
- data.x += @intFromFloat(dx);
- data.y += @intFromFloat(dy);
-
- if (true) return; // XXX resize window
-
- // Modify width/height of the pending box, taking constraints into account
- // The x/y coordinates of the window will be adjusted as needed in Window.resizeCommit()
- // based on the dimensions actually committed by the client.
- const border_width = if (data.window.pending.ssd) server.config.border_width else 0;
-
- // TODO
- const output_width: i32 = 1920;
- const output_height: i32 = 1080;
-
- const constraints = &data.window.constraints;
- const box = &data.window.pending.box;
-
- if (data.edges.left) {
- const x2 = box.x + box.width;
- box.width = data.initial_width - data.x;
- box.width = @max(box.width, constraints.min_width);
- box.width = @min(box.width, constraints.max_width);
- box.width = @min(box.width, x2 - border_width);
- data.x = data.initial_width - box.width;
- } else if (data.edges.right) {
- box.width = data.initial_width + data.x;
- box.width = @max(box.width, constraints.min_width);
- box.width = @min(box.width, constraints.max_width);
- box.width = @min(box.width, output_width - border_width - box.x);
- data.x = box.width - data.initial_width;
- }
-
- if (data.edges.top) {
- const y2 = box.y + box.height;
- box.height = data.initial_height - data.y;
- box.height = @max(box.height, constraints.min_height);
- box.height = @min(box.height, constraints.max_height);
- box.height = @min(box.height, y2 - border_width);
- data.y = data.initial_height - box.height;
- } else if (data.edges.bottom) {
- box.height = data.initial_height + data.y;
- box.height = @max(box.height, constraints.min_height);
- box.height = @min(box.height, constraints.max_height);
- box.height = @min(box.height, output_height - border_width - box.y);
- data.y = box.height - data.initial_height;
- }
-
- server.wm.dirtyPending();
- },
- }
-}
-
/// Handle potential change in location of windows on the output, as well as
/// the target window of a cursor operation potentially being moved to a non-visible tag,
/// becoming fullscreen, etc.
@@ -1003,3 +888,58 @@ fn updateDragIcons(cursor: *Cursor) void {
}
}
}
+
+fn queueMotionRelative(listener: *wl.Listener(*wlr.Pointer.event.Motion), event: *wlr.Pointer.event.Motion) void {
+ const cursor: *Cursor = @fieldParentPtr("motion_relative", listener);
+ cursor.seat.queueEvent(.{ .pointer_motion_relative = event.* });
+}
+
+fn queueMotionAbsolute(listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute), event: *wlr.Pointer.event.MotionAbsolute) void {
+ const cursor: *Cursor = @fieldParentPtr("motion_absolute", listener);
+ cursor.seat.queueEvent(.{ .pointer_motion_absolute = event.* });
+}
+
+fn queueButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.Pointer.event.Button) void {
+ const cursor: *Cursor = @fieldParentPtr("button", listener);
+ cursor.seat.queueEvent(.{ .pointer_button = event.* });
+}
+
+fn queueAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Pointer.event.Axis) void {
+ const cursor: *Cursor = @fieldParentPtr("axis", listener);
+ cursor.seat.queueEvent(.{ .pointer_axis = event.* });
+}
+
+fn queueFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void {
+ const cursor: *Cursor = @fieldParentPtr("frame", listener);
+ cursor.seat.queueEvent(.pointer_frame);
+}
+
+fn queuePinchBegin(listener: *wl.Listener(*wlr.Pointer.event.PinchBegin), event: *wlr.Pointer.event.PinchBegin) void {
+ const cursor: *Cursor = @fieldParentPtr("pinch_begin", listener);
+ cursor.seat.queueEvent(.{ .pointer_pinch_begin = event.* });
+}
+
+fn queuePinchUpdate(listener: *wl.Listener(*wlr.Pointer.event.PinchUpdate), event: *wlr.Pointer.event.PinchUpdate) void {
+ const cursor: *Cursor = @fieldParentPtr("pinch_update", listener);
+ cursor.seat.queueEvent(.{ .pointer_pinch_update = event.* });
+}
+
+fn queuePinchEnd(listener: *wl.Listener(*wlr.Pointer.event.PinchEnd), event: *wlr.Pointer.event.PinchEnd) void {
+ const cursor: *Cursor = @fieldParentPtr("pinch_end", listener);
+ cursor.seat.queueEvent(.{ .pointer_pinch_end = event.* });
+}
+
+fn queueSwipeBegin(listener: *wl.Listener(*wlr.Pointer.event.SwipeBegin), event: *wlr.Pointer.event.SwipeBegin) void {
+ const cursor: *Cursor = @fieldParentPtr("swipe_begin", listener);
+ cursor.seat.queueEvent(.{ .pointer_swipe_begin = event.* });
+}
+
+fn queueSwipeUpdate(listener: *wl.Listener(*wlr.Pointer.event.SwipeUpdate), event: *wlr.Pointer.event.SwipeUpdate) void {
+ const cursor: *Cursor = @fieldParentPtr("swipe_update", listener);
+ cursor.seat.queueEvent(.{ .pointer_swipe_update = event.* });
+}
+
+fn queueSwipeEnd(listener: *wl.Listener(*wlr.Pointer.event.SwipeEnd), event: *wlr.Pointer.event.SwipeEnd) void {
+ const cursor: *Cursor = @fieldParentPtr("swipe_end", listener);
+ cursor.seat.queueEvent(.{ .pointer_swipe_end = event.* });
+}
diff --git a/river/ForeignToplevelHandle.zig b/river/ForeignToplevelHandle.zig
index beaa607..6cdb402 100644
--- a/river/ForeignToplevelHandle.zig
+++ b/river/ForeignToplevelHandle.zig
@@ -71,22 +71,18 @@ pub fn update(handle: *ForeignToplevelHandle) void {
const wlr_handle = handle.wlr_handle orelse return;
- wlr_handle.setActivated(window.inflight.focus != 0);
+ wlr_handle.setActivated(window.inflight.activated);
wlr_handle.setFullscreen(window.inflight.fullscreen);
}
-/// Only honors the request if the window is already visible on the seat's
-/// currently focused output.
fn handleForeignActivate(
- listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated),
- event: *wlr.ForeignToplevelHandleV1.event.Activated,
+ _: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated),
+ _: *wlr.ForeignToplevelHandleV1.event.Activated,
) void {
- const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_activate", listener);
- const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
- const seat: *Seat = @ptrFromInt(event.seat.data);
+ //const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_activate", listener);
+ //const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
- seat.focus(window);
- server.wm.dirtyPending();
+ // XXX Can I just delete this protocol?
}
fn handleForeignFullscreen(
diff --git a/river/InputManager.zig b/river/InputManager.zig
index 4ec86d3..db6ef2b 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -58,7 +58,7 @@ tablet_manager: *wlr.TabletManagerV2,
configs: std.ArrayList(InputConfig),
devices: wl.list.Head(InputDevice, .link),
-seats: std.TailQueue(Seat) = .{},
+seats: wl.list.Head(Seat, .link),
new_virtual_pointer: wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer) =
wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer).init(handleNewVirtualPointer),
@@ -72,9 +72,6 @@ new_text_input: wl.Listener(*wlr.TextInputV3) =
wl.Listener(*wlr.TextInputV3).init(handleNewTextInput),
pub fn init(input_manager: *InputManager) !void {
- const seat_node = try util.gpa.create(std.TailQueue(Seat).Node);
- errdefer util.gpa.destroy(seat_node);
-
input_manager.* = .{
// These are automatically freed when the display is destroyed
.idle_notifier = try wlr.IdleNotifierV1.create(server.wl_server),
@@ -89,11 +86,12 @@ pub fn init(input_manager: *InputManager) !void {
.configs = std.ArrayList(InputConfig).init(util.gpa),
.devices = undefined,
+ .seats = undefined,
};
input_manager.devices.init();
+ input_manager.seats.init();
- input_manager.seats.prepend(seat_node);
- try seat_node.data.init(default_seat_name);
+ try Seat.create(default_seat_name);
if (build_options.xwayland) {
if (server.xwayland) |xwayland| {
@@ -119,9 +117,8 @@ pub fn deinit(input_manager: *InputManager) void {
input_manager.new_input_method.link.remove();
input_manager.new_text_input.link.remove();
- while (input_manager.seats.pop()) |seat_node| {
- seat_node.data.deinit();
- util.gpa.destroy(seat_node);
+ while (input_manager.seats.first()) |seat| {
+ seat.destroy();
}
for (input_manager.configs.items) |*config| {
@@ -130,8 +127,17 @@ pub fn deinit(input_manager: *InputManager) void {
input_manager.configs.deinit();
}
-pub fn defaultSeat(input_manager: InputManager) *Seat {
- return &input_manager.seats.first.?.data;
+pub fn defaultSeat(input_manager: *InputManager) *Seat {
+ return input_manager.seats.first().?;
+}
+
+pub fn processEvents(input_manager: *InputManager) void {
+ assert(server.wm.state == .idle);
+
+ var it = input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ seat.processEvents();
+ }
}
/// Reconfigures all devices' libinput configuration as well as their output mapping.
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index 258d089..799e423 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -31,6 +31,11 @@ const InputDevice = @import("InputDevice.zig");
const log = std.log.scoped(.keyboard);
+pub const Event = union(enum) {
+ key: wlr.Keyboard.event.Key,
+ modifiers: wlr.Keyboard.Modifiers,
+};
+
const KeyConsumer = enum {
mapping,
im_grab,
@@ -85,8 +90,8 @@ device: InputDevice,
/// Pressed keys along with where their press event has been sent
pressed: Pressed = .{},
-key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(handleKey),
-modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers),
+key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(queueKey),
+modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(queueModifiers),
pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
keyboard.* = .{
@@ -144,9 +149,7 @@ pub fn deinit(keyboard: *Keyboard) void {
keyboard.* = undefined;
}
-fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
- // This event is raised when a key is pressed or released.
- const keyboard: *Keyboard = @fieldParentPtr("key", listener);
+pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) void {
const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
// If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
@@ -157,6 +160,7 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
// Translate libinput keycode -> xkbcommon
const keycode = event.keycode + 8;
+ // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
const modifiers = wlr_keyboard.getModifiers();
const released = event.state == .released;
@@ -229,12 +233,7 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
if (released) _ = keyboard.device.seat.handleMapping(keycode, modifiers, released, xkb_state);
}
-fn isModifier(keysym: xkb.Keysym) bool {
- return @intFromEnum(keysym) >= xkb.Keysym.Shift_L and @intFromEnum(keysym) <= xkb.Keysym.Hyper_R;
-}
-
-fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
- const keyboard: *Keyboard = @fieldParentPtr("modifiers", listener);
+pub fn processModifiers(keyboard: *Keyboard, modifiers: *const wlr.Keyboard.Modifiers) void {
const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
// If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
@@ -242,10 +241,10 @@ fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void
if (keyboard.getInputMethodGrab()) |keyboard_grab| {
keyboard_grab.setKeyboard(keyboard_grab.keyboard);
- keyboard_grab.sendModifiers(&wlr_keyboard.modifiers);
+ keyboard_grab.sendModifiers(modifiers);
} else {
keyboard.device.seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
- keyboard.device.seat.wlr_seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers);
+ keyboard.device.seat.wlr_seat.keyboardNotifyModifiers(modifiers);
}
}
@@ -283,3 +282,13 @@ fn getInputMethodGrab(keyboard: Keyboard) ?*wlr.InputMethodV2.KeyboardGrab {
}
return null;
}
+
+fn queueKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
+ const keyboard: *Keyboard = @fieldParentPtr("key", listener);
+ keyboard.device.seat.queueEvent(.{ .keyboard_key = .{ .keyboard = keyboard, .key = event.* } });
+}
+
+fn queueModifiers(listener: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) void {
+ const keyboard: *Keyboard = @fieldParentPtr("modifiers", listener);
+ keyboard.device.seat.queueEvent(.{ .keyboard_modifiers = .{ .keyboard = keyboard, .modifiers = wlr_keyboard.modifiers } });
+}
diff --git a/river/LockManager.zig b/river/LockManager.zig
index 466fa6b..98ecbb8 100644
--- a/river/LockManager.zig
+++ b/river/LockManager.zig
@@ -110,10 +110,9 @@ fn handleLock(listener: *wl.Listener(*wlr.SessionLockV1), lock: *wlr.SessionLock
};
{
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) {
- const seat = &node.data;
- seat.setFocusRaw(.none);
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ seat.focus(.none);
}
}
} else {
@@ -196,10 +195,9 @@ fn handleUnlock(listener: *wl.Listener(void)) void {
server.scene.locked_tree.node.setEnabled(true);
{
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) {
- const seat = &node.data;
- seat.setFocusRaw(.none);
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ seat.focus(.none);
}
}
diff --git a/river/LockSurface.zig b/river/LockSurface.zig
index b146dea..3192ff7 100644
--- a/river/LockSurface.zig
+++ b/river/LockSurface.zig
@@ -62,16 +62,15 @@ pub fn create(wlr_lock_surface: *wlr.SessionLockSurfaceV1, lock: *wlr.SessionLoc
pub fn destroy(lock_surface: *LockSurface) void {
{
var surface_it = lock_surface.lock.surfaces.iterator(.forward);
- const new_focus: Seat.FocusTarget = while (surface_it.next()) |surface| {
+ const new_focus: Seat.Focus = while (surface_it.next()) |surface| {
if (surface != lock_surface.wlr_lock_surface)
break .{ .lock_surface = @ptrFromInt(surface.data) };
} else .none;
- var seat_it = server.input_manager.seats.first;
- while (seat_it) |node| : (seat_it = node.next) {
- const seat = &node.data;
+ var seat_it = server.input_manager.seats.iterator(.forward);
+ while (seat_it.next()) |seat| {
if (seat.focused == .lock_surface and seat.focused.lock_surface == lock_surface) {
- seat.setFocusRaw(new_focus);
+ seat.focus(new_focus);
}
seat.cursor.updateState();
}
@@ -117,11 +116,10 @@ fn handleMap(listener: *wl.Listener(void)) void {
}
fn updateFocus(lock_surface: *LockSurface) void {
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) {
- const seat = &node.data;
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
if (seat.focused != .lock_surface) {
- seat.setFocusRaw(.{ .lock_surface = lock_surface });
+ seat.focus(.{ .lock_surface = lock_surface });
}
seat.cursor.updateState();
}
diff --git a/river/Output.zig b/river/Output.zig
index a9d2d09..9b846ab 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -239,13 +239,16 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
server.wm.dirtyPending();
}
-pub fn sendDirty(output: *Output) !void {
+pub fn sendDirty(output: *Output) void {
switch (output.pending.state) {
.enabled, .disabled_soft => {
if (server.wm.object) |wm_v1| {
const new = output.object == null;
const output_v1 = output.object orelse blk: {
- const output_v1 = try river.OutputV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0);
+ const output_v1 = river.OutputV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch {
+ log.err("out of memory", .{});
+ return; // try again next update
+ };
output.object = output_v1;
output_v1.setHandler(*Output, handleRequest, null, output);
diff --git a/river/Seat.zig b/river/Seat.zig
index c1694bd..15b2b6a 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 - 2024 The River Developers
+// Copyright 2020-2024 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -20,7 +20,9 @@ const build_options = @import("build_options");
const std = @import("std");
const assert = std.debug.assert;
const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
const xkb = @import("xkbcommon");
const server = &@import("main.zig").server;
@@ -44,13 +46,52 @@ const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const log = std.log.scoped(.seat);
-pub const FocusTarget = union(enum) {
+pub const Event = union(enum) {
+ keyboard_key: struct {
+ keyboard: *Keyboard,
+ key: wlr.Keyboard.event.Key,
+ },
+ keyboard_modifiers: struct {
+ keyboard: *Keyboard,
+ modifiers: wlr.Keyboard.Modifiers,
+ },
+
+ pointer_motion_relative: wlr.Pointer.event.Motion,
+ pointer_motion_absolute: wlr.Pointer.event.MotionAbsolute,
+ pointer_button: wlr.Pointer.event.Button,
+ pointer_axis: wlr.Pointer.event.Axis,
+ pointer_frame: void,
+
+ pointer_swipe_begin: wlr.Pointer.event.SwipeBegin,
+ pointer_swipe_update: wlr.Pointer.event.SwipeUpdate,
+ pointer_swipe_end: wlr.Pointer.event.SwipeEnd,
+
+ pointer_pinch_begin: wlr.Pointer.event.PinchBegin,
+ pointer_pinch_update: wlr.Pointer.event.PinchUpdate,
+ pointer_pinch_end: wlr.Pointer.event.PinchEnd,
+};
+
+pub const WmState = struct {
+ focus: WmFocus = .none,
+ // TODO pointer move/resize state
+ // TODO confine region
+ // TODO pointer warp
+ // TODO xkb/pointer bindings
+};
+
+pub const WmFocus = union(enum) {
+ none,
+ window: *Window,
+ // TODO shell_surface: *ShellSurface,
+};
+
+pub const Focus = union(enum) {
window: *Window,
override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
lock_surface: *LockSurface,
none: void,
- pub fn surface(target: FocusTarget) ?*wlr.Surface {
+ pub fn surface(target: Focus) ?*wlr.Surface {
return switch (target) {
.window => |window| window.rootSurface(),
.override_redirect => |override_redirect| override_redirect.xsurface.surface,
@@ -60,16 +101,49 @@ pub const FocusTarget = union(enum) {
}
};
+/// XXX experiment with different sizes here, consider making dynamic
+/// XXX Clean out events for destroyed input devices
+const EventQueue = std.fifo.LinearFifo(Event, .{ .Static = 1024 });
+
wlr_seat: *wlr.Seat,
-/// Multiple mice are handled by the same Cursor
+link: wl.list.Link,
+
+destroying: bool = false,
+
+object: ?*river.SeatV1 = null,
+
+event_queue: EventQueue = EventQueue.init(),
+
+/// State to be sent to the window manager client in the next update sequence.
+pending: struct {
+ /// The window entered/hovered by the pointer, if any
+ window: ?*Window = null,
+ /// The window clicked on, touched, etc.
+ window_interaction: ?*Window = null,
+} = .{},
+link_pending: wl.list.Link,
+
+/// State sent to the window manager client in the latest update sequence.
+sent: struct {
+ /// The window entered/hovered by the pointer, if any
+ window: ?*Window = null,
+} = .{},
+link_sent: wl.list.Link,
+
+/// State requested by the window manager client but not yet committed.
+uncommitted: WmState = .{},
+/// State requested by the window manager client and committed.
+committed: WmState = .{},
+
+/// Multiple physical mice are handled by the same Cursor
cursor: Cursor,
-/// Input Method handling
+
relay: InputRelay,
keyboard_groups: std.TailQueue(KeyboardGroup) = .{},
-focused: FocusTarget = .none,
+focused: Focus = .none,
/// The currently in progress drag operation type.
drag: enum {
@@ -87,15 +161,26 @@ drag_destroy: wl.Listener(*wlr.Drag) = wl.Listener(*wlr.Drag).init(handleDragDes
request_set_primary_selection: wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection) =
wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection).init(handleRequestSetPrimarySelection),
-pub fn init(seat: *Seat, name: [*:0]const u8) !void {
+pub fn create(name: [*:0]const u8) !void {
+ const seat = try util.gpa.create(Seat);
+ errdefer util.gpa.destroy(seat);
+
seat.* = .{
// This will be automatically destroyed when the display is destroyed
.wlr_seat = try wlr.Seat.create(server.wl_server, name),
+ .link = undefined,
+ .link_pending = undefined,
+ .link_sent = undefined,
.cursor = undefined,
.relay = undefined,
};
seat.wlr_seat.data = @intFromPtr(seat);
+ server.input_manager.seats.append(seat);
+ server.wm.pending.seats.append(seat);
+ seat.link_sent.init();
+ server.wm.dirtyPending();
+
try seat.cursor.init(seat);
seat.relay.init();
@@ -105,12 +190,16 @@ pub fn init(seat: *Seat, name: [*:0]const u8) !void {
seat.wlr_seat.events.request_set_primary_selection.add(&seat.request_set_primary_selection);
}
-pub fn deinit(seat: *Seat) void {
+pub fn destroy(seat: *Seat) void {
{
var it = server.input_manager.devices.iterator(.forward);
while (it.next()) |device| assert(device.seat != seat);
}
+ seat.link.remove();
+ seat.link_pending.remove();
+ seat.link_sent.remove();
+
seat.cursor.deinit();
while (seat.keyboard_groups.first) |node| {
@@ -124,25 +213,179 @@ pub fn deinit(seat: *Seat) void {
seat.request_set_primary_selection.link.remove();
}
-/// Set the current focus. If a visible window is passed it will be focused.
-/// If null is passed, the top window in the stack of the focused output will be focused.
-/// Requires a call to WindowManager.dirtyPending()
-pub fn focus(seat: *Seat, target: ?*Window) void {
- // Views may not receive focus while locked.
- if (server.lock_manager.state != .unlocked) return;
+pub fn queueEvent(seat: *Seat, event: Event) void {
+ seat.handleActivity();
- // Focus the target window or clear the focus if target is null
- if (target) |window| {
- seat.setFocusRaw(.{ .window = window });
- } else {
- seat.setFocusRaw(.{ .none = {} });
+ seat.event_queue.writeItem(event) catch {
+ log.err("dropping {s} event, no space in event queue", .{@tagName(event)});
+ return;
+ };
+
+ if (server.wm.state == .idle) {
+ seat.processEvents();
+ }
+}
+
+pub fn processEvents(seat: *Seat) void {
+ assert(server.wm.state == .idle);
+
+ var expect_frame: bool = false;
+ while (seat.event_queue.readItem()) |event| {
+ const pg = server.input_manager.pointer_gestures;
+ switch (event) {
+ .keyboard_key => |ev| ev.keyboard.processKey(&ev.key),
+ .keyboard_modifiers => |ev| ev.keyboard.processModifiers(&ev.modifiers),
+
+ .pointer_motion_relative => |ev| seat.cursor.processMotionRelative(&ev),
+ .pointer_motion_absolute => |ev| seat.cursor.processMotionAbsolute(&ev),
+ .pointer_button => |ev| seat.cursor.processButton(&ev),
+ .pointer_axis => |ev| seat.cursor.processAxis(&ev),
+ .pointer_frame => seat.wlr_seat.pointerNotifyFrame(),
+
+ .pointer_swipe_begin => |ev| pg.sendSwipeBegin(seat.wlr_seat, ev.time_msec, ev.fingers),
+ .pointer_swipe_update => |ev| pg.sendSwipeUpdate(seat.wlr_seat, ev.time_msec, ev.dx, ev.dy),
+ .pointer_swipe_end => |ev| pg.sendSwipeEnd(seat.wlr_seat, ev.time_msec, ev.cancelled),
+
+ .pointer_pinch_begin => |ev| pg.sendPinchBegin(seat.wlr_seat, ev.time_msec, ev.fingers),
+ .pointer_pinch_update => |ev| pg.sendPinchUpdate(seat.wlr_seat, ev.time_msec, ev.dx, ev.dy, ev.scale, ev.rotation),
+ .pointer_pinch_end => |ev| pg.sendPinchEnd(seat.wlr_seat, ev.time_msec, ev.cancelled),
+ }
+
+ // Don't split up pointer events grouped by a frame event
+ switch (event) {
+ .pointer_motion_relative,
+ .pointer_motion_absolute,
+ .pointer_button,
+ .pointer_axis,
+ => {
+ expect_frame = true;
+ continue;
+ },
+ .pointer_frame => expect_frame = false,
+ else => assert(!expect_frame),
+ }
+
+ if (server.wm.pending.dirty) {
+ // Wait for feedback from the window manager before further processing.
+ // The window manager might decide to change focus or redefine keyboard/pointer bindings.
+ break;
+ }
+ }
+}
+
+pub fn sendDirty(seat: *Seat) void {
+ if (seat.destroying) {
+ if (seat.object) |seat_v1| {
+ seat_v1.sendRemoved();
+ seat_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+ seat.object = null;
+ }
+
+ seat.link_pending.remove();
+ seat.link_sent.remove();
+ seat.link_pending.init();
+ seat.link_sent.init();
+
+ seat.destroy();
+ return;
+ }
+
+ if (server.wm.object) |wm_v1| {
+ const new = seat.object == null;
+ const seat_v1 = seat.object orelse blk: {
+ const seat_v1 = river.SeatV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch {
+ log.err("out of memory", .{});
+ return; // try again next update
+ };
+ seat.object = seat_v1;
+
+ seat_v1.setHandler(*Seat, handleRequest, null, seat);
+ wm_v1.sendSeat(seat_v1);
+
+ seat.link_sent.remove();
+ server.wm.sent.seats.append(seat);
+
+ break :blk seat_v1;
+ };
+ errdefer comptime unreachable;
+
+ if (new) {
+ if (seat.pending.window) |window| {
+ if (window.object) |window_v1| {
+ seat_v1.sendPointerEnter(window_v1);
+ seat.sent.window = seat.pending.window;
+ }
+ }
+ } else if (seat.pending.window != seat.sent.window) {
+ if (seat.sent.window) |window| {
+ if (window.object) |window_v1| {
+ seat_v1.sendPointerLeave(window_v1);
+ seat.sent.window = null;
+ }
+ }
+ if (seat.pending.window) |window| {
+ if (window.object) |window_v1| {
+ seat_v1.sendPointerEnter(window_v1);
+ seat.sent.window = window;
+ }
+ }
+ }
+
+ if (seat.pending.window_interaction) |window| {
+ if (window.object) |window_v1| {
+ seat_v1.sendWindowInteraction(window_v1);
+ seat.pending.window_interaction = null;
+ }
+ }
+ }
+}
+
+fn handleRequestInert(
+ seat_v1: *river.SeatV1,
+ request: river.SeatV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) seat_v1.destroy();
+}
+
+fn handleRequest(
+ seat_v1: *river.SeatV1,
+ request: river.SeatV1.Request,
+ seat: *Seat,
+) void {
+ assert(seat.object == seat_v1);
+ switch (request) {
+ .destroy => {}, // XXX send protocol error
+ .focus_window => |args| {
+ const data = args.window.getUserData() orelse return;
+ const window: *Window = @ptrCast(@alignCast(data));
+ seat.uncommitted.focus = .{ .window = window };
+ },
+ .focus_shell_surface => {},
+ .clear_focus => seat.uncommitted.focus = .none,
+ .pointer_move_window => {},
+ .pointer_resize_window => {},
+ .pointer_confine_to_region => {},
+ .pointer_warp => {},
+ .define_xkb_binding => {},
+ .define_pointer_binding => {},
}
}
-/// Switch focus to the target, handling unfocus and input inhibition
-/// properly. This should only be called directly if dealing with layers or
-/// override redirect xwayland windows.
-pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
+pub fn commitWmState(seat: *Seat) void {
+ seat.committed = seat.uncommitted;
+}
+
+pub fn applyCommitted(seat: *Seat) void {
+ if (server.lock_manager.state == .unlocked) {
+ switch (seat.committed.focus) {
+ .none => seat.focus(.none),
+ .window => |window| seat.focus(.{ .window = window }),
+ }
+ }
+}
+
+pub fn focus(seat: *Seat, new_focus: Focus) void {
// If the target is already focused, do nothing
if (std.meta.eql(new_focus, seat.focused)) return;
@@ -150,19 +393,13 @@ pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
// First clear the current focus
switch (seat.focused) {
- .window => |window| {
- //window.pending.focus -= 1; XXX update focus to send activated state
- window.destroyPopups();
- },
+ .window => |window| window.destroyPopups(),
.override_redirect, .lock_surface, .none => {},
}
// Set the new focus
switch (new_focus) {
- .window => |_| {
- assert(server.lock_manager.state != .locked);
- //target_window.pending.focus += 1; XXX update focus to send activated state
- },
+ .window => assert(server.lock_manager.state != .locked),
.lock_surface => assert(server.lock_manager.state != .unlocked),
.override_redirect, .none => {},
}
@@ -217,6 +454,7 @@ fn keyboardNotifyEnter(seat: *Seat, wlr_surface: *wlr.Surface) void {
seat.wlr_seat.keyboardNotifyEnter(
wlr_surface,
keycodes.constSlice(),
+ // XXX this is not ok, use our own stored modifiers
&wlr_keyboard.modifiers,
);
} else {
diff --git a/river/Server.zig b/river/Server.zig
index 76347bb..2a892b7 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -175,12 +175,12 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
server.xwayland.?.events.new_surface.add(&server.new_xsurface);
}
+ try server.wm.init();
try server.scene.init();
try server.om.init();
try server.input_manager.init();
try server.idle_inhibit_manager.init();
try server.lock_manager.init();
- try server.wm.init();
server.xdg_shell.events.new_toplevel.add(&server.new_xdg_toplevel);
server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration);
diff --git a/river/Window.zig b/river/Window.zig
index 54c2564..7bc65ff 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 The River Developers
+// Copyright 2020-2024 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -69,8 +69,8 @@ pub const State = struct {
/// The output-relative coordinates of the window and dimensions requested by river.
box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
hidden: bool = false,
- /// Number of seats currently focusing the window
- focus: u32 = 0,
+ /// True if the window has keyboard focus from at least one seat.
+ activated: bool = false,
ssd: bool = false,
border: Border = .{},
tiled: river.WindowV1.Edges = .{},
@@ -228,7 +228,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
assert(window.impl == .none);
assert(!window.mapped);
- assert(window.object == null);
+ assert(window.pending.state == .closing);
window.destroying = true;
@@ -293,7 +293,7 @@ pub fn setFullscreenRequested(window: *Window, fullscreen_requested: bool) void
}
/// Send dirty pending state as part of an in progress update sequence.
-pub fn sendDirty(window: *Window) !void {
+pub fn sendDirty(window: *Window) void {
assert(window.pending.state != .init);
switch (window.pending.state) {
@@ -324,7 +324,10 @@ pub fn sendDirty(window: *Window) !void {
const wm_v1 = server.wm.object orelse return;
const new = window.object == null;
const window_v1 = window.object orelse blk: {
- const window_v1 = try river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0);
+ const window_v1 = river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch {
+ log.err("out of memory", .{});
+ return; // try again next update
+ };
window.object = window_v1;
window_v1.setHandler(*Window, handleRequest, null, window);
wm_v1.sendWindow(window_v1);
@@ -452,11 +455,12 @@ pub fn resizeUpdatePosition(window: *Window, width: i32, height: i32) void {
assert(window.inflight.resizing);
const data = blk: {
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) {
- const cursor = &node.data.cursor;
- if (cursor.inflight_mode == .resize and cursor.inflight_mode.resize.window == window) {
- break :blk cursor.inflight_mode.resize;
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (seat.cursor.inflight_mode == .resize and
+ seat.cursor.inflight_mode.resize.window == window)
+ {
+ break :blk seat.cursor.inflight_mode.resize;
}
} else {
// The window resizing state should never be set when the window is
@@ -602,6 +606,16 @@ pub fn configure(window: *Window) bool {
assert(!window.destroying);
+ const activated = blk: {
+ var it = server.wm.sent.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (seat.committed.focus == .window and seat.committed.focus.window == window) {
+ break :blk true;
+ }
+ }
+ break :blk false;
+ };
+
const committed = &window.committed;
window.inflight = .{
.box = .{
@@ -611,7 +625,7 @@ pub fn configure(window: *Window) bool {
.height = if (committed.proposed) |p| p.height else window.pending.box.height,
},
.hidden = committed.hidden,
- .focus = 0, // XXX
+ .activated = activated,
.ssd = committed.ssd,
.border = committed.border,
.tiled = committed.tiled,
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index e91d7e9..0e7b1d1 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -26,6 +26,7 @@ const server = &@import("main.zig").server;
const util = @import("util.zig");
const Output = @import("Output.zig");
+const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
const WmNode = @import("WmNode.zig");
@@ -59,12 +60,16 @@ pending: struct {
outputs: wl.list.Head(Output, .link_pending),
output_config: ?*wlr.OutputConfigurationV1 = null,
+
+ seats: wl.list.Head(Seat, .link_pending),
},
/// State sent to the wm in the latest update sequence.
sent: struct {
outputs: wl.list.Head(Output, .link_sent),
output_config: ?*wlr.OutputConfigurationV1 = null,
+
+ seats: wl.list.Head(Seat, .link_sent),
},
/// State sent by the wm but not yet committed with a commit request.
@@ -74,6 +79,7 @@ uncommitted: struct {
/// State sent by the wm and committed with a commit request.
committed: struct {
+ // The wm has committed state since state was last sent to windows.
dirty: bool = false,
render_list: wl.list.Head(WmNode, .link_committed),
},
@@ -99,9 +105,11 @@ pub fn init(wm: *WindowManager) !void {
.pending = .{
.dirty_windows = undefined,
.outputs = undefined,
+ .seats = undefined,
},
.sent = .{
.outputs = undefined,
+ .seats = undefined,
},
.uncommitted = .{
.render_list = undefined,
@@ -117,7 +125,9 @@ pub fn init(wm: *WindowManager) !void {
wm.windows.init();
wm.pending.dirty_windows.init();
wm.pending.outputs.init();
+ wm.pending.seats.init();
wm.sent.outputs.init();
+ wm.sent.seats.init();
wm.uncommitted.render_list.init();
wm.committed.render_list.init();
wm.inflight.render_list.init();
@@ -147,7 +157,7 @@ fn bind(client: *wl.Client, wm: *WindowManager, version: u32, id: u32) void {
wm.object = object;
object.setHandler(*WindowManager, handleRequest, null, wm);
- // XXX send existing windows?
+ // XXX send existing windows outputs and seats, including output dimensions
}
fn handleRequestInert(
@@ -195,6 +205,11 @@ fn handleRequest(
}
}
+ {
+ var it = wm.sent.seats.iterator(.forward);
+ while (it.next()) |seat| seat.commitWmState();
+ }
+
wm.committed.dirty = true;
switch (wm.state) {
.idle, .update_acked => {
@@ -204,7 +219,6 @@ fn handleRequest(
.update_sent, .inflight_configures => {},
}
},
- .get_seat => |_| {},
.get_shell_surface => |_| {},
}
}
@@ -239,17 +253,10 @@ fn sendUpdate(wm: *WindowManager) void {
log.debug("sending update to window manager", .{});
- // XXX send all dirty pending state
-
wm.autoLayoutOutputs();
{
var it = wm.pending.outputs.safeIterator(.forward);
- while (it.next()) |output| {
- output.sendDirty() catch {
- log.err("out of memory", .{});
- continue; // Try again next update
- };
- }
+ while (it.next()) |output| output.sendDirty();
}
assert(wm.sent.output_config == null);
@@ -258,12 +265,12 @@ fn sendUpdate(wm: *WindowManager) void {
{
var it = wm.pending.dirty_windows.safeIterator(.forward);
- while (it.next()) |window| {
- window.sendDirty() catch {
- log.err("out of memory", .{});
- continue; // Try again next update
- };
- }
+ while (it.next()) |window| window.sendDirty();
+ }
+
+ {
+ var it = wm.pending.seats.safeIterator(.forward);
+ while (it.next()) |seat| seat.sendDirty();
}
wm.pending.dirty = false;
@@ -319,6 +326,11 @@ fn sendConfigures(wm: *WindowManager) void {
assert(wm.committed.dirty);
wm.committed.dirty = false;
+ {
+ var it = wm.sent.seats.iterator(.forward);
+ while (it.next()) |seat| seat.applyCommitted();
+ }
+
wm.state = .{ .inflight_configures = 0 };
{
var it = wm.committed.render_list.iterator(.forward);
@@ -414,8 +426,8 @@ fn commitTransaction(wm: *WindowManager) void {
server.om.commitOutputState();
{
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) node.data.cursor.updateState();
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| seat.cursor.updateState();
}
server.idle_inhibit_manager.checkActive();
@@ -426,5 +438,7 @@ fn commitTransaction(wm: *WindowManager) void {
wm.sendConfigures();
} else if (wm.pending.dirty) {
wm.sendUpdate();
+ } else {
+ server.input_manager.processEvents();
}
}
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 517766e..ce7df68 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -147,7 +147,7 @@ pub fn configure(toplevel: *XdgToplevel) bool {
const wlr_toplevel = toplevel.wlr_toplevel;
- _ = wlr_toplevel.setActivated(inflight.focus != 0);
+ _ = wlr_toplevel.setActivated(inflight.activated);
_ = wlr_toplevel.setTiled(.{
.top = inflight.tiled.top,
.bottom = inflight.tiled.bottom,
@@ -205,7 +205,7 @@ fn needsConfigure(toplevel: *XdgToplevel) bool {
return true;
}
- if ((inflight.focus != 0) != (current.focus != 0)) return true;
+ if (inflight.activated != current.activated) return true;
if (inflight.ssd != current.ssd) return true;
if (!std.meta.eql(inflight.tiled, current.tiled)) return true;
if (!std.meta.eql(inflight.capabilities, current.capabilities)) return true;
diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig
index 639dc38..c3f5161 100644
--- a/river/XwaylandOverrideRedirect.zig
+++ b/river/XwaylandOverrideRedirect.zig
@@ -165,9 +165,8 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
// If the unmapped surface is currently focused, pass keyboard focus
// to the most appropriate surface.
- var seat_it = server.input_manager.seats.first;
- while (seat_it) |seat_node| : (seat_it = seat_node.next) {
- const seat = &seat_node.data;
+ var seat_it = server.input_manager.seats.iterator(.forward);
+ while (seat_it.next()) |seat| {
if (seat.focused == .window and seat.focused.window.impl == .xwayland and
seat.focused.window.impl.xwayland.xsurface.pid == override_redirect.xsurface.pid and
seat.wlr_seat.keyboard_state.focused_surface == override_redirect.xsurface.surface)
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index 44d4311..ce7d5b8 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -47,5 +47,8 @@ fn handleEvent(wm_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event
.output => |args| {
_ = args;
},
+ .seat => |args| {
+ _ = args;
+ },
}
}