ce9c391f
Add Wayland pointer and clipboard source support
a73x 2026-04-09 18:21
Commit message
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), |