a73x

751dce3d

refactor: separate native clipboard shortcuts

a73x   2026-09-07 08:48

Commit message
refactor: separate native clipboard shortcuts

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();