From f0fcb104069647051b2e612c23c90e2414a0db58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2026 10:56:55 -0700 Subject: [PATCH] libghostty: selection gesture deep press --- include/ghostty/vt/selection.h | 17 ++- src/terminal/c/selection_gesture.zig | 150 +++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index bcba934b2..d42fa3c0e 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -418,18 +418,21 @@ typedef enum GHOSTTY_ENUM_TYPED { * @ingroup selection */ typedef enum GHOSTTY_ENUM_TYPED { - /** Press event for ghostty_selection_gesture_press(). */ + /** Press event for ghostty_selection_gesture_event(). */ GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS = 0, - /** Release event for ghostty_selection_gesture_release(). */ + /** Release event for ghostty_selection_gesture_event(). */ GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE = 1, - /** Drag event for ghostty_selection_gesture_drag(). */ + /** Drag event for ghostty_selection_gesture_event(). */ GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG = 2, - /** Autoscroll tick event for ghostty_selection_gesture_autoscroll_tick(). */ + /** Autoscroll tick event for ghostty_selection_gesture_event(). */ GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK = 3, + /** Deep press event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS = 4, + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttySelectionGestureEventType; @@ -477,7 +480,7 @@ typedef enum GHOSTTY_ENUM_TYPED { * The codepoints are copied into event-owned storage when set. If unset, * operations that need word boundaries use Ghostty's defaults. * - * Valid for PRESS, DRAG, and AUTOSCROLL_TICK. + * Valid for PRESS, DRAG, AUTOSCROLL_TICK, and DEEP_PRESS. */ GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS = 5, @@ -574,6 +577,10 @@ GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set( * rectangle, and word-boundary codepoints are optional and use initialized * defaults when unset or cleared. * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS, only + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS is valid. It is + * optional and uses initialized defaults when unset or cleared. + * * The returned selection is not installed as the terminal's current selection. * It is a snapshot with the same lifetime rules as GhosttySelection. * diff --git a/src/terminal/c/selection_gesture.zig b/src/terminal/c/selection_gesture.zig index 5fd16f33f..981bb0022 100644 --- a/src/terminal/c/selection_gesture.zig +++ b/src/terminal/c/selection_gesture.zig @@ -32,6 +32,7 @@ const EventWrapper = struct { release: SelectionGesture.Release, drag: SelectionGesture.Drag, autoscroll_tick: SelectionGesture.AutoscrollTick, + deep_press: SelectionGesture.DeepPress, }, // Press.pin has no safe sentinel value: PageList.Pin contains a non-null @@ -73,6 +74,7 @@ const EventWrapper = struct { .release => .{ .release = self.defaultRelease() }, .drag => .{ .drag = self.defaultDrag() }, .autoscroll_tick => .{ .autoscroll_tick = self.defaultAutoscrollTick() }, + .deep_press => .{ .deep_press = self.defaultDeepPress() }, }; } @@ -118,6 +120,13 @@ const EventWrapper = struct { }; } + fn defaultDeepPress(self: *EventWrapper) SelectionGesture.DeepPress { + _ = self; + return .{ + .word_boundary_codepoints = &selection_codepoints.default_word_boundaries, + }; + } + fn deinit(self: *EventWrapper) void { if (self.word_boundary_codepoints) |cps| { if (cps.len > 0) self.alloc.free(cps); @@ -163,6 +172,7 @@ pub const EventType = enum(c_int) { release = 1, drag = 2, autoscroll_tick = 3, + deep_press = 4, }; /// C: GhosttySelectionGestureEventOption @@ -324,6 +334,13 @@ pub fn handle_event( } else if (sel == null) return .no_value; return .success; }, + .deep_press => |deep_press| { + const sel = wrapper.gesture.deepPress(t, deep_press); + if (out_selection) |out| { + out.* = selection_c.CSelection.fromZig(sel orelse return .no_value); + } else if (sel == null) return .no_value; + return .success; + }, }; } @@ -429,6 +446,7 @@ fn eventSetTyped( .release => |*release| releaseSetTyped(release, option, value), .drag => |*drag| dragSetTyped(event, drag, option, value), .autoscroll_tick => |*tick| autoscrollTickSetTyped(event, tick, option, value), + .deep_press => |*deep_press| deepPressSetTyped(event, deep_press, option, value), }; } @@ -648,6 +666,55 @@ fn autoscrollTickSetTyped( return .success; } +fn deepPressSetTyped( + event: *EventWrapper, + deep_press: *SelectionGesture.DeepPress, + comptime option: EventOption, + value: ?*const option.Type(), +) Result { + const v = value orelse { + switch (option) { + .word_boundary_codepoints => clearWordBoundaryCodepoints( + event, + &deep_press.word_boundary_codepoints, + ), + + .ref, + .position, + .repeat_distance, + .time_ns, + .repeat_interval_ns, + .behaviors, + .rectangle, + .geometry, + .viewport, + => return .invalid_value, + } + return .success; + }; + + switch (option) { + .word_boundary_codepoints => return trySetWordBoundaryCodepoints( + event, + &deep_press.word_boundary_codepoints, + v, + ), + + .ref, + .position, + .repeat_distance, + .time_ns, + .repeat_interval_ns, + .behaviors, + .rectangle, + .geometry, + .viewport, + => return .invalid_value, + } + + return .success; +} + fn trySetWordBoundaryCodepoints( event: *EventWrapper, target: *[]const u21, @@ -1330,6 +1397,89 @@ test "selection gesture autoscroll tick requires viewport and geometry" { try testing.expectEqual(Result.invalid_value, event_set(tick_event, .ref, &ref)); } +test "selection gesture event applies deep press" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(terminal); + + terminal_c.vt_write(terminal, "abcde", 5); + + var gesture: Gesture = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &gesture, + )); + defer free(gesture, terminal); + + var press_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &press_event, + .press, + )); + defer event_free(press_event); + + var deep_press_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &deep_press_event, + .deep_press, + )); + defer event_free(deep_press_event); + + var ref: grid_ref.CGridRef = undefined; + try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{ + .tag = .active, + .value = .{ .active = .{ .x = 2, .y = 0 } }, + }, &ref)); + try testing.expectEqual(Result.success, event_set(press_event, .ref, &ref)); + try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null)); + + var sel: selection_c.CSelection = undefined; + try testing.expectEqual(Result.success, handle_event(gesture, terminal, deep_press_event, &sel)); + try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x); + try testing.expectEqual(@as(u16, 4), sel.end.toPin().?.x); + + var dragged = false; + try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged)); + try testing.expect(dragged); + + const pos: types.SurfacePosition = .{ .x = 0, .y = 0 }; + try testing.expectEqual(Result.invalid_value, event_set(deep_press_event, .position, &pos)); +} + +test "selection gesture deep press without active anchor returns no value" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(terminal); + + var gesture: Gesture = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &gesture, + )); + defer free(gesture, terminal); + + var deep_press_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &deep_press_event, + .deep_press, + )); + defer event_free(deep_press_event); + + var sel: selection_c.CSelection = undefined; + try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, deep_press_event, &sel)); +} + test "selection gesture free null" { free(null, null); }