751dce3d
refactor: separate native clipboard shortcuts
a73x 2026-09-07 08:48
Commit message
src/gui/frame.zig
| Old | New | ||
|---|---|---|---|
| @@ -400,7 +400,9 @@ const Events = struct { | |||
| 400 | defer if (self.hook) |h| h.releaseText(ev.text.text); | 400 | defer if (self.hook) |h| h.releaseText(ev.text.text); |
| 401 | try self.ui.textInput(std.mem.span(ev.text.text)); | 401 | try self.ui.textInput(std.mem.span(ev.text.text)); |
| 402 | }, | 402 | }, |
| 403 | c.SDL_EVENT_KEY_DOWN => try self.ui.keyDown(interactionKey(ev.key)), | 403 | c.SDL_EVENT_KEY_DOWN => if (nativeShortcut(ev.key)) |shortcut| switch (shortcut) { |
| 404 | .copy => try self.ui.copyShortcut(ev.key.key), | ||
| 405 | } else try self.ui.keyDown(interactionKey(ev.key)), | ||
| 404 | c.SDL_EVENT_KEY_UP => self.ui.keyUp(ev.key.key), | 406 | c.SDL_EVENT_KEY_UP => self.ui.keyUp(ev.key.key), |
| 405 | c.SDL_EVENT_WINDOW_FOCUS_LOST => self.ui.focusLost(), | 407 | c.SDL_EVENT_WINDOW_FOCUS_LOST => self.ui.focusLost(), |
| 406 | c.SDL_EVENT_USER => if (ev.user.code == 0x4d4f4453) { | 408 | c.SDL_EVENT_USER => if (ev.user.code == 0x4d4f4453) { |
| @@ -496,10 +498,15 @@ fn grabPixels(logical: c_int, pixels: c_int) u32 { | |||
| 496 | if (logical <= 0 or pixels <= 0) return 0; | 498 | if (logical <= 0 or pixels <= 0) return 0; |
| 497 | return @intCast((@as(u64, @intCast(pixels)) * 6 + @as(u32, @intCast(logical)) - 1) / @as(u32, @intCast(logical))); | 499 | return @intCast((@as(u64, @intCast(pixels)) * 6 + @as(u32, @intCast(logical)) - 1) / @as(u32, @intCast(logical))); |
| 498 | } | 500 | } |
| 501 | const NativeShortcut = enum { copy }; | ||
| 502 | fn nativeShortcut(ev: c.SDL_KeyboardEvent) ?NativeShortcut { | ||
| 503 | if (ev.repeat) return null; | ||
| 504 | if (ev.key == c.SDLK_C and ev.mod & c.SDL_KMOD_CTRL != 0 and ev.mod & c.SDL_KMOD_SHIFT != 0 and ev.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0) return .copy; | ||
| 505 | return null; | ||
| 506 | } | ||
| 499 | fn interactionKey(ev: c.SDL_KeyboardEvent) interaction.KeyDown { | 507 | fn interactionKey(ev: c.SDL_KeyboardEvent) interaction.KeyDown { |
| 500 | const translated = if (ev.scancode != c.SDL_SCANCODE_UNKNOWN) c.SDL_GetKeyFromScancode(ev.scancode, ev.mod, false) else ev.key; | 508 | const translated = if (ev.scancode != c.SDL_SCANCODE_UNKNOWN) c.SDL_GetKeyFromScancode(ev.scancode, ev.mod, false) else ev.key; |
| 501 | const prefix = ev.key == c.SDLK_BACKSLASH and ev.mod & c.SDL_KMOD_CTRL != 0 and ev.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0; | 509 | const prefix = ev.key == c.SDLK_BACKSLASH and ev.mod & c.SDL_KMOD_CTRL != 0 and ev.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0; |
| 502 | const copy = ev.key == c.SDLK_C and ev.mod & c.SDL_KMOD_CTRL != 0 and ev.mod & c.SDL_KMOD_SHIFT != 0 and ev.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0; | ||
| 503 | return .{ | 510 | return .{ |
| 504 | .code = ev.key, | 511 | .code = ev.key, |
| 505 | .kind = switch (ev.key) { | 512 | .kind = switch (ev.key) { |
| @@ -526,7 +533,6 @@ fn interactionKey(ev: c.SDL_KeyboardEvent) interaction.KeyDown { | |||
| 526 | .prefix = prefix, | 533 | .prefix = prefix, |
| 527 | .modified = ev.mod & (c.SDL_KMOD_CTRL | c.SDL_KMOD_ALT | c.SDL_KMOD_GUI) != 0, | 534 | .modified = ev.mod & (c.SDL_KMOD_CTRL | c.SDL_KMOD_ALT | c.SDL_KMOD_GUI) != 0, |
| 528 | .repeat = ev.repeat, | 535 | .repeat = ev.repeat, |
| 529 | .copy = copy, | ||
| 530 | .terminal = keyEvent(if (prefix) ev.key else translated, ev.mod), | 536 | .terminal = keyEvent(if (prefix) ev.key else translated, ev.mod), |
| 531 | }; | 537 | }; |
| 532 | } | 538 | } |
| @@ -1346,22 +1352,23 @@ test "copy chord is exact while plain interrupt remains terminal input" { | |||
| 1346 | var copy_event = std.mem.zeroes(c.SDL_KeyboardEvent); | 1352 | var copy_event = std.mem.zeroes(c.SDL_KeyboardEvent); |
| 1347 | copy_event.key = c.SDLK_C; | 1353 | copy_event.key = c.SDLK_C; |
| 1348 | copy_event.mod = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT; | 1354 | copy_event.mod = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT; |
| 1349 | const copy = interactionKey(copy_event); | 1355 | try std.testing.expectEqual(NativeShortcut.copy, nativeShortcut(copy_event).?); |
| 1350 | try std.testing.expect(copy.copy); | ||
| 1351 | var interrupt_event = copy_event; | 1356 | var interrupt_event = copy_event; |
| 1352 | interrupt_event.mod = c.SDL_KMOD_CTRL; | 1357 | interrupt_event.mod = c.SDL_KMOD_CTRL; |
| 1353 | const interrupt = interactionKey(interrupt_event); | 1358 | const interrupt = interactionKey(interrupt_event); |
| 1354 | try std.testing.expect(!interrupt.copy); | 1359 | try std.testing.expect(nativeShortcut(interrupt_event) == null); |
| 1355 | try std.testing.expect(interrupt.terminal != null); | 1360 | try std.testing.expect(interrupt.terminal != null); |
| 1356 | for ([_]u16{ c.SDL_KMOD_LCTRL | c.SDL_KMOD_LSHIFT, c.SDL_KMOD_RCTRL | c.SDL_KMOD_RSHIFT, c.SDL_KMOD_LCTRL | c.SDL_KMOD_RSHIFT, c.SDL_KMOD_RCTRL | c.SDL_KMOD_LSHIFT }) |mods| { | 1361 | for ([_]u16{ c.SDL_KMOD_LCTRL | c.SDL_KMOD_LSHIFT, c.SDL_KMOD_RCTRL | c.SDL_KMOD_RSHIFT, c.SDL_KMOD_LCTRL | c.SDL_KMOD_RSHIFT, c.SDL_KMOD_RCTRL | c.SDL_KMOD_LSHIFT }) |mods| { |
| 1357 | copy_event.mod = mods; | 1362 | copy_event.mod = mods; |
| 1358 | try std.testing.expect(interactionKey(copy_event).copy); | 1363 | try std.testing.expectEqual(NativeShortcut.copy, nativeShortcut(copy_event).?); |
| 1359 | } | 1364 | } |
| 1360 | for ([_]u16{ c.SDL_KMOD_ALT, c.SDL_KMOD_GUI, c.SDL_KMOD_MODE, c.SDL_KMOD_RALT }) |extra| { | 1365 | for ([_]u16{ c.SDL_KMOD_ALT, c.SDL_KMOD_GUI, c.SDL_KMOD_MODE, c.SDL_KMOD_RALT }) |extra| { |
| 1361 | var excluded = copy_event; | 1366 | var excluded = copy_event; |
| 1362 | excluded.mod |= extra; | 1367 | excluded.mod |= extra; |
| 1363 | try std.testing.expect(!interactionKey(excluded).copy); | 1368 | try std.testing.expect(nativeShortcut(excluded) == null); |
| 1364 | } | 1369 | } |
| 1370 | copy_event.repeat = true; | ||
| 1371 | try std.testing.expect(nativeShortcut(copy_event) == null); | ||
| 1365 | } | 1372 | } |
| 1366 | 1373 | ||
| 1367 | test "later pane atlas growth precedes earlier pane UV generation" { | 1374 | test "later pane atlas growth precedes earlier pane UV generation" { |
src/gui/interaction.zig
| Old | New | ||
|---|---|---|---|
| @@ -19,7 +19,6 @@ pub const KeyDown = struct { | |||
| 19 | prefix: bool = false, | 19 | prefix: bool = false, |
| 20 | modified: bool = false, | 20 | modified: bool = false, |
| 21 | repeat: bool = false, | 21 | repeat: bool = false, |
| 22 | copy: bool = false, | ||
| 23 | terminal: ?app_input.Event = null, | 22 | terminal: ?app_input.Event = null, |
| 24 | }; | 23 | }; |
| 25 | pub const PendingEnd = struct { key: model.Attachment, request: u64 }; | 24 | pub const PendingEnd = struct { key: model.Attachment, request: u64 }; |
| @@ -501,6 +500,11 @@ pub const Controller = struct { | |||
| 501 | if (held) self.selection_gesture = 0; | 500 | if (held) self.selection_gesture = 0; |
| 502 | try self.queueSelection(live, range); | 501 | try self.queueSelection(live, range); |
| 503 | } | 502 | } |
| 503 | pub fn copyShortcut(self: *Controller, key: u32) !void { | ||
| 504 | self.consumed_key = key; | ||
| 505 | self.suppress_text = true; | ||
| 506 | try self.copySelection(); | ||
| 507 | } | ||
| 504 | pub fn pointerMove(self: *Controller, x: i64, y: i64) !void { | 508 | pub fn pointerMove(self: *Controller, x: i64, y: i64) !void { |
| 505 | if (self.drag == null and self.selection_drag.on() != null) { | 509 | if (self.drag == null and self.selection_drag.on() != null) { |
| 506 | if (x < 0 or y < 0) { | 510 | if (x < 0 or y < 0) { |
| @@ -627,12 +631,6 @@ pub const Controller = struct { | |||
| 627 | self.suppress_text = true; | 631 | self.suppress_text = true; |
| 628 | return; | 632 | return; |
| 629 | } | 633 | } |
| 630 | if (input.copy and !input.prefix and self.picker == null and self.recovery == null and !self.resize_mode and !self.command_mode) { | ||
| 631 | self.consumed_key = key; | ||
| 632 | self.suppress_text = true; | ||
| 633 | try self.copySelection(); | ||
| 634 | return; | ||
| 635 | } | ||
| 636 | if (self.recovery) |*menu| { | 634 | if (self.recovery) |*menu| { |
| 637 | try self.modal_held.put(self.rt.alloc, key, {}); | 635 | try self.modal_held.put(self.rt.alloc, key, {}); |
| 638 | self.suppress_text = true; | 636 | self.suppress_text = true; |
| @@ -736,6 +734,21 @@ fn menuKey(key: Key, editing: bool) ?picker_mod.Key { | |||
| 736 | }; | 734 | }; |
| 737 | } | 735 | } |
| 738 | 736 | ||
| 737 | test "copy shortcut owns its physical key until release" { | ||
| 738 | var rt = runtime.Runtime.init(std.testing.allocator, .{}); | ||
| 739 | defer rt.deinit(); | ||
| 740 | var ui: Controller = .{ .rt = &rt, .metrics = .{ .cell_w = 8, .cell_h = 16 }, .fb_w = 800, .fb_h = 600 }; | ||
| 741 | defer ui.deinit(); | ||
| 742 | ui.resize_mode = true; | ||
| 743 | try ui.copyShortcut(99); | ||
| 744 | try std.testing.expectEqual(@as(?u32, 99), ui.consumed_key); | ||
| 745 | try std.testing.expect(ui.suppress_text and ui.resize_mode); | ||
| 746 | try ui.keyDown(.{ .code = 99, .repeat = true }); | ||
| 747 | try std.testing.expect(ui.suppress_text and ui.resize_mode); | ||
| 748 | ui.keyUp(99); | ||
| 749 | try std.testing.expect(ui.consumed_key == null and !ui.suppress_text); | ||
| 750 | } | ||
| 751 | |||
| 739 | test "controller preserves exact held keys across modal dismissal and text edges" { | 752 | test "controller preserves exact held keys across modal dismissal and text edges" { |
| 740 | var rt = runtime.Runtime.init(std.testing.allocator, .{}); | 753 | var rt = runtime.Runtime.init(std.testing.allocator, .{}); |
| 741 | defer rt.deinit(); | 754 | defer rt.deinit(); |