diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index 0270a3038..e0476f7db 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -402,6 +402,9 @@ typedef enum GHOSTTY_ENUM_TYPED { /** Press event for ghostty_selection_gesture_press(). */ GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS = 0, + /** Release event for ghostty_selection_gesture_release(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE = 1, + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttySelectionGestureEventType; @@ -414,7 +417,12 @@ typedef enum GHOSTTY_ENUM_TYPED { * @ingroup selection */ typedef enum GHOSTTY_ENUM_TYPED { - /** Grid reference under the pointer: GhosttyGridRef*. */ + /** + * Grid reference under the pointer: GhosttyGridRef*. + * + * Required for PRESS events. Optional for RELEASE events; when unset or + * cleared, release records that the pointer did not map to a valid cell. + */ GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF = 0, /** Surface-space pointer position: GhosttySurfacePosition*. */ @@ -508,6 +516,12 @@ GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set( * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF set before calling this function. * All other press options use their initialized defaults when unset or cleared. * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE, only + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF is valid. It is optional; if unset or + * cleared, release records that the pointer did not map to a valid cell. Release + * events update gesture state but do not produce a selection, so this function + * returns GHOSTTY_NO_VALUE after applying them. + * * 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 df823e62e..39ad8b489 100644 --- a/src/terminal/c/selection_gesture.zig +++ b/src/terminal/c/selection_gesture.zig @@ -28,6 +28,7 @@ const EventWrapper = struct { alloc: std.mem.Allocator, event: union(EventType) { press: SelectionGesture.Press, + release: SelectionGesture.Release, }, // Press.pin has no safe sentinel value: PageList.Pin contains a non-null @@ -51,6 +52,7 @@ const EventWrapper = struct { fn init(self: *EventWrapper, event_type: EventType) void { self.event = switch (event_type) { .press => .{ .press = self.defaultPress() }, + .release => .{ .release = self.defaultRelease() }, }; } @@ -67,6 +69,11 @@ const EventWrapper = struct { }; } + fn defaultRelease(self: *EventWrapper) SelectionGesture.Release { + _ = self; + return .{ .pin = null }; + } + fn deinit(self: *EventWrapper) void { if (self.word_boundary_codepoints) |cps| { if (cps.len > 0) self.alloc.free(cps); @@ -109,6 +116,7 @@ pub const Data = enum(c_int) { /// C: GhosttySelectionGestureEventType pub const EventType = enum(c_int) { press = 0, + release = 1, }; /// C: GhosttySelectionGestureEventOption @@ -222,6 +230,10 @@ pub fn handle_event( } else if (sel == null) return .no_value; return .success; }, + .release => |release| { + wrapper.gesture.release(t, release); + return .no_value; + }, }; } @@ -324,6 +336,7 @@ fn eventSetTyped( const event = event_ orelse return .invalid_value; return switch (event.event) { .press => |*press| pressSetTyped(event, press, option, value), + .release => |*release| releaseSetTyped(release, option, value), }; } @@ -392,6 +405,32 @@ fn pressSetTyped( return .success; } +fn releaseSetTyped( + release: *SelectionGesture.Release, + comptime option: EventOption, + value: ?*const option.Type(), +) Result { + switch (option) { + .ref => { + const v = value orelse { + release.pin = null; + return .success; + }; + release.pin = v.toPin() orelse return .invalid_value; + }, + + .position, + .repeat_distance, + .time_ns, + .repeat_interval_ns, + .word_boundary_codepoints, + .behaviors, + => return .invalid_value, + } + + return .success; +} + fn clearPressCodepoints(event: *EventWrapper, press: *SelectionGesture.Press) void { if (event.word_boundary_codepoints) |cps| { if (cps.len > 0) event.alloc.free(cps); @@ -716,6 +755,104 @@ test "selection gesture event null output still reports no selection" { try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null)); } +test "selection gesture event applies release" { + 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 press_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &press_event, + .press, + )); + defer event_free(press_event); + + var release_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &release_event, + .release, + )); + defer event_free(release_event); + + var ref: grid_ref.CGridRef = undefined; + try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{ + .tag = .active, + .value = .{ .active = .{ .x = 1, .y = 0 } }, + }, &ref)); + try testing.expectEqual(Result.success, event_set(press_event, .ref, &ref)); + try testing.expectEqual(Result.success, event_set(release_event, .ref, &ref)); + + try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null)); + try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, release_event, null)); + + var dragged = true; + 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(release_event, .position, &pos)); +} + +test "selection gesture release without ref marks dragged" { + 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 press_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &press_event, + .press, + )); + defer event_free(press_event); + + var release_event: Event = null; + try testing.expectEqual(Result.success, event_new( + &lib.alloc.test_allocator, + &release_event, + .release, + )); + defer event_free(release_event); + + var ref: grid_ref.CGridRef = undefined; + try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{ + .tag = .active, + .value = .{ .active = .{ .x = 1, .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)); + try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, release_event, null)); + + var dragged = false; + try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged)); + try testing.expect(dragged); +} + test "selection gesture free null" { free(null, null); }