a73x

ce9c391f

Add Wayland pointer and clipboard source support

a73x   2026-04-09 18:21

Commit message
Add Wayland pointer and clipboard source support

src/wayland.zig
Old New
@@ -35,6 +35,153 @@ pub const Modifiers = struct {
35 super: bool = false, 35 super: bool = false,
36 }; 36 };
37 37
38 pub const PointerEvent = union(enum) {
39 enter: struct {
40 serial: u32,
41 x: f64,
42 y: f64,
43 },
44 leave: struct {
45 serial: u32,
46 },
47 motion: struct {
48 time: u32,
49 x: f64,
50 y: f64,
51 },
52 button_press: struct {
53 serial: u32,
54 time: u32,
55 button: u32,
56 },
57 button_release: struct {
58 serial: u32,
59 time: u32,
60 button: u32,
61 },
62 };
63
64 const PointerState = struct {
65 has_focus: bool = false,
66 surface_x: f64 = 0,
67 surface_y: f64 = 0,
68 last_serial: u32 = 0,
69
70 fn pushEnter(
71 self: *PointerState,
72 alloc: std.mem.Allocator,
73 queue: *std.ArrayList(PointerEvent),
74 serial: u32,
75 x: f64,
76 y: f64,
77 ) !void {
78 self.has_focus = true;
79 self.surface_x = x;
80 self.surface_y = y;
81 self.last_serial = serial;
82 try queue.append(alloc, .{
83 .enter = .{
84 .serial = serial,
85 .x = x,
86 .y = y,
87 },
88 });
89 }
90
91 fn pushLeave(
92 self: *PointerState,
93 alloc: std.mem.Allocator,
94 queue: *std.ArrayList(PointerEvent),
95 serial: u32,
96 ) !void {
97 self.has_focus = false;
98 self.last_serial = serial;
99 try queue.append(alloc, .{
100 .leave = .{
101 .serial = serial,
102 },
103 });
104 }
105
106 fn pushMotion(
107 self: *PointerState,
108 alloc: std.mem.Allocator,
109 queue: *std.ArrayList(PointerEvent),
110 time: u32,
111 x: f64,
112 y: f64,
113 ) !void {
114 self.surface_x = x;
115 self.surface_y = y;
116 try queue.append(alloc, .{
117 .motion = .{
118 .time = time,
119 .x = x,
120 .y = y,
121 },
122 });
123 }
124
125 fn pushButton(
126 self: *PointerState,
127 alloc: std.mem.Allocator,
128 queue: *std.ArrayList(PointerEvent),
129 serial: u32,
130 time: u32,
131 button: u32,
132 state: wl.Pointer.ButtonState,
133 ) !void {
134 self.last_serial = serial;
135 try queue.append(alloc, switch (state) {
136 .pressed => .{
137 .button_press = .{
138 .serial = serial,
139 .time = time,
140 .button = button,
141 },
142 },
143 .released => .{
144 .button_release = .{
145 .serial = serial,
146 .time = time,
147 .button = button,
148 },
149 },
150 else => unreachable,
151 });
152 }
153 };
154
155 pub const Pointer = struct {
156 alloc: std.mem.Allocator,
157 wl_pointer: *wl.Pointer,
158 event_queue: std.ArrayList(PointerEvent),
159 state: PointerState = .{},
160
161 pub fn init(alloc: std.mem.Allocator, seat: *wl.Seat) !*Pointer {
162 const pointer = try alloc.create(Pointer);
163 errdefer alloc.destroy(pointer);
164
165 var event_queue: std.ArrayList(PointerEvent) = .empty;
166 errdefer event_queue.deinit(alloc);
167 try event_queue.ensureTotalCapacity(alloc, 64);
168
169 pointer.* = .{
170 .alloc = alloc,
171 .wl_pointer = try seat.getPointer(),
172 .event_queue = event_queue,
173 };
174 pointer.wl_pointer.setListener(*Pointer, pointerListener, pointer);
175 return pointer;
176 }
177
178 pub fn deinit(self: *Pointer) void {
179 self.wl_pointer.release();
180 self.event_queue.deinit(self.alloc);
181 self.alloc.destroy(self);
182 }
183 };
184
38 pub const Keyboard = struct { 185 pub const Keyboard = struct {
39 alloc: std.mem.Allocator, 186 alloc: std.mem.Allocator,
40 wl_keyboard: *wl.Keyboard, 187 wl_keyboard: *wl.Keyboard,
@@ -146,12 +293,48 @@ const ClipboardOffer = struct {
146 } 293 }
147 }; 294 };
148 295
296 const OwnedSelectionState = struct {
297 text: ?[]u8 = null,
298 serial: u32 = 0,
299
300 fn deinit(self: *OwnedSelectionState, alloc: std.mem.Allocator) void {
301 self.clear(alloc);
302 }
303
304 fn replaceText(
305 self: *OwnedSelectionState,
306 alloc: std.mem.Allocator,
307 text: []const u8,
308 serial: u32,
309 ) !void {
310 const dup = try alloc.dupe(u8, text);
311 self.clear(alloc);
312 self.text = dup;
313 self.serial = serial;
314 }
315
316 fn clear(self: *OwnedSelectionState, alloc: std.mem.Allocator) void {
317 if (self.text) |text| alloc.free(text);
318 self.text = null;
319 self.serial = 0;
320 }
321
322 fn canServeMime(self: *const OwnedSelectionState, mime: []const u8) bool {
323 return self.text != null and
324 (std.mem.eql(u8, mime, "text/plain;charset=utf-8") or
325 std.mem.eql(u8, mime, "text/plain"));
326 }
327 };
328
149 pub const Clipboard = struct { 329 pub const Clipboard = struct {
150 alloc: std.mem.Allocator, 330 alloc: std.mem.Allocator,
151 display: *wl.Display, 331 display: *wl.Display,
332 data_device_manager: *wl.DataDeviceManager,
152 data_device: *wl.DataDevice, 333 data_device: *wl.DataDevice,
153 pending_offer: ?ClipboardOffer = null, 334 pending_offer: ?ClipboardOffer = null,
154 selection_offer: ?ClipboardOffer = null, 335 selection_offer: ?ClipboardOffer = null,
336 owned_selection: OwnedSelectionState = .{},
337 selection_source: ?*wl.DataSource = null,
155 338
156 pub fn init( 339 pub fn init(
157 alloc: std.mem.Allocator, 340 alloc: std.mem.Allocator,
@@ -165,6 +348,7 @@ pub const Clipboard = struct {
165 clipboard.* = .{ 348 clipboard.* = .{
166 .alloc = alloc, 349 .alloc = alloc,
167 .display = display, 350 .display = display,
351 .data_device_manager = manager,
168 .data_device = try manager.getDataDevice(seat), 352 .data_device = try manager.getDataDevice(seat),
169 }; 353 };
170 clipboard.data_device.setListener(*Clipboard, dataDeviceListener, clipboard); 354 clipboard.data_device.setListener(*Clipboard, dataDeviceListener, clipboard);
@@ -174,6 +358,8 @@ pub const Clipboard = struct {
174 pub fn deinit(self: *Clipboard) void { 358 pub fn deinit(self: *Clipboard) void {
175 if (self.pending_offer) |*offer| offer.deinit(self.alloc); 359 if (self.pending_offer) |*offer| offer.deinit(self.alloc);
176 if (self.selection_offer) |*offer| offer.deinit(self.alloc); 360 if (self.selection_offer) |*offer| offer.deinit(self.alloc);
361 if (self.selection_source) |source| source.destroy();
362 self.owned_selection.deinit(self.alloc);
177 self.data_device.release(); 363 self.data_device.release();
178 self.alloc.destroy(self); 364 self.alloc.destroy(self);
179 } 365 }
@@ -202,6 +388,49 @@ pub const Clipboard = struct {
202 388
203 return try drainSelectionPipeThenRoundtrip(alloc, pipefds[0], &roundtrip_ctx); 389 return try drainSelectionPipeThenRoundtrip(alloc, pipefds[0], &roundtrip_ctx);
204 } 390 }
391
392 pub fn setSelectionText(self: *Clipboard, text: []const u8, serial: u32) !void {
393 const source = try self.data_device_manager.createDataSource();
394 errdefer source.destroy();
395
396 try self.owned_selection.replaceText(self.alloc, text, serial);
397
398 source.setListener(*Clipboard, dataSourceListener, self);
399 source.offer("text/plain;charset=utf-8");
400 source.offer("text/plain");
401
402 if (self.selection_source) |old_source| old_source.destroy();
403 self.selection_source = source;
404 self.data_device.setSelection(source, serial);
405 _ = self.display.flush();
406 }
407
408 fn handleCancelledSource(self: *Clipboard, source: *wl.DataSource) void {
409 if (self.selection_source == source) {
410 self.selection_source = null;
411 source.destroy();
412 self.owned_selection.clear(self.alloc);
413 return;
414 }
415 source.destroy();
416 }
417
418 fn sendOwnedSelection(self: *Clipboard, mime_type: [*:0]const u8, fd: i32) void {
419 defer _ = c.close(fd);
420
421 if (!self.owned_selection.canServeMime(std.mem.span(mime_type))) return;
422 const text = self.owned_selection.text orelse return;
423
424 var written: usize = 0;
425 while (written < text.len) {
426 const n = posix.write(fd, text[written..]) catch |err| {
427 std.log.err("clipboard write failed: {any}", .{err});
428 return;
429 };
430 if (n == 0) return;
431 written += n;
432 }
433 }
205 }; 434 };
206 435
207 pub fn drainSelectionPipeThenRoundtrip( 436 pub fn drainSelectionPipeThenRoundtrip(
@@ -482,6 +711,51 @@ fn keyboardListener(_: *wl.Keyboard, event: wl.Keyboard.Event, kb: *Keyboard) vo
482 } 711 }
483 } 712 }
484 713
714 fn pointerListener(_: *wl.Pointer, event: wl.Pointer.Event, pointer: *Pointer) void {
715 switch (event) {
716 .enter => |e| {
717 pointer.state.pushEnter(
718 pointer.alloc,
719 &pointer.event_queue,
720 e.serial,
721 e.surface_x.toDouble(),
722 e.surface_y.toDouble(),
723 ) catch |err| {
724 std.log.err("pointer enter queue append failed: {any}", .{err});
725 };
726 },
727 .leave => |e| {
728 pointer.state.pushLeave(pointer.alloc, &pointer.event_queue, e.serial) catch |err| {
729 std.log.err("pointer leave queue append failed: {any}", .{err});
730 };
731 },
732 .motion => |m| {
733 pointer.state.pushMotion(
734 pointer.alloc,
735 &pointer.event_queue,
736 m.time,
737 m.surface_x.toDouble(),
738 m.surface_y.toDouble(),
739 ) catch |err| {
740 std.log.err("pointer motion queue append failed: {any}", .{err});
741 };
742 },
743 .button => |b| {
744 pointer.state.pushButton(
745 pointer.alloc,
746 &pointer.event_queue,
747 b.serial,
748 b.time,
749 b.button,
750 b.state,
751 ) catch |err| {
752 std.log.err("pointer button queue append failed: {any}", .{err});
753 };
754 },
755 .axis, .frame, .axis_source, .axis_stop, .axis_discrete, .axis_value120, .axis_relative_direction => {},
756 }
757 }
758
485 fn dataOfferListener(_: *wl.DataOffer, event: wl.DataOffer.Event, clipboard: *Clipboard) void { 759 fn dataOfferListener(_: *wl.DataOffer, event: wl.DataOffer.Event, clipboard: *Clipboard) void {
486 switch (event) { 760 switch (event) {
487 .offer => |mime| { 761 .offer => |mime| {
@@ -496,6 +770,14 @@ fn dataOfferListener(_: *wl.DataOffer, event: wl.DataOffer.Event, clipboard: *Cl
496 } 770 }
497 } 771 }
498 772
773 fn dataSourceListener(source: *wl.DataSource, event: wl.DataSource.Event, clipboard: *Clipboard) void {
774 switch (event) {
775 .send => |send| clipboard.sendOwnedSelection(send.mime_type, send.fd),
776 .cancelled => clipboard.handleCancelledSource(source),
777 .target, .dnd_drop_performed, .dnd_finished, .action => {},
778 }
779 }
780
499 fn dataDeviceListener(_: *wl.DataDevice, event: wl.DataDevice.Event, clipboard: *Clipboard) void { 781 fn dataDeviceListener(_: *wl.DataDevice, event: wl.DataDevice.Event, clipboard: *Clipboard) void {
500 switch (event) { 782 switch (event) {
501 .data_offer => |offer| { 783 .data_offer => |offer| {
@@ -534,6 +816,64 @@ test "choosePreferredMime preserves sentinel-terminated mime type" {
534 try std.testing.expect(mime[mime.len] == 0); 816 try std.testing.expect(mime[mime.len] == 0);
535 } 817 }
536 818
819 test "PointerState queues enter motion and button transitions" {
820 var state = PointerState{};
821 var queue: std.ArrayList(PointerEvent) = .empty;
822 defer queue.deinit(std.testing.allocator);
823
824 try state.pushEnter(std.testing.allocator, &queue, 11, 12.5, 9.25);
825 try state.pushMotion(std.testing.allocator, &queue, 27, 18.0, 4.0);
826 try state.pushButton(std.testing.allocator, &queue, 11, 33, 0x110, .pressed);
827 try state.pushButton(std.testing.allocator, &queue, 11, 34, 0x110, .released);
828 try state.pushLeave(std.testing.allocator, &queue, 12);
829
830 try std.testing.expectEqual(@as(usize, 5), queue.items.len);
831 try std.testing.expect(!state.has_focus);
832 try std.testing.expectEqual(@as(u32, 12), state.last_serial);
833 try std.testing.expectApproxEqAbs(18.0, state.surface_x, 0.001);
834 try std.testing.expectApproxEqAbs(4.0, state.surface_y, 0.001);
835
836 switch (queue.items[0]) {
837 .enter => |ev| {
838 try std.testing.expectEqual(@as(u32, 11), ev.serial);
839 try std.testing.expectApproxEqAbs(12.5, ev.x, 0.001);
840 try std.testing.expectApproxEqAbs(9.25, ev.y, 0.001);
841 },
842 else => return error.TestUnexpectedResult,
843 }
844 switch (queue.items[1]) {
845 .motion => |ev| {
846 try std.testing.expectEqual(@as(u32, 27), ev.time);
847 try std.testing.expectApproxEqAbs(18.0, ev.x, 0.001);
848 try std.testing.expectApproxEqAbs(4.0, ev.y, 0.001);
849 },
850 else => return error.TestUnexpectedResult,
851 }
852 try std.testing.expect(queue.items[2] == .button_press);
853 try std.testing.expect(queue.items[3] == .button_release);
854 try std.testing.expect(queue.items[4] == .leave);
855 }
856
857 test "OwnedSelectionState replaces text and recognizes offered mime types" {
858 var state = OwnedSelectionState{};
859 defer state.deinit(std.testing.allocator);
860
861 try state.replaceText(std.testing.allocator, "first", 17);
862 try std.testing.expectEqualStrings("first", state.text.?);
863 try std.testing.expectEqual(@as(u32, 17), state.serial);
864 try std.testing.expect(state.canServeMime("text/plain;charset=utf-8"));
865 try std.testing.expect(state.canServeMime("text/plain"));
866 try std.testing.expect(!state.canServeMime("text/html"));
867
868 try state.replaceText(std.testing.allocator, "second", 21);
869 try std.testing.expectEqualStrings("second", state.text.?);
870 try std.testing.expectEqual(@as(u32, 21), state.serial);
871
872 state.clear(std.testing.allocator);
873 try std.testing.expect(state.text == null);
874 try std.testing.expectEqual(@as(u32, 0), state.serial);
875 }
876
537 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void { 877 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
538 switch (event) { 878 switch (event) {
539 .ping => |p| wm_base.pong(p.serial), 879 .ping => |p| wm_base.pong(p.serial),