libghostty: selection gesture deep press
parent
603684ba11
commit
f0fcb10406
|
|
@ -418,18 +418,21 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||||
* @ingroup selection
|
* @ingroup selection
|
||||||
*/
|
*/
|
||||||
typedef enum GHOSTTY_ENUM_TYPED {
|
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,
|
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,
|
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,
|
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,
|
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,
|
GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||||
} GhosttySelectionGestureEventType;
|
} GhosttySelectionGestureEventType;
|
||||||
|
|
||||||
|
|
@ -477,7 +480,7 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||||
* The codepoints are copied into event-owned storage when set. If unset,
|
* The codepoints are copied into event-owned storage when set. If unset,
|
||||||
* operations that need word boundaries use Ghostty's defaults.
|
* 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,
|
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
|
* rectangle, and word-boundary codepoints are optional and use initialized
|
||||||
* defaults when unset or cleared.
|
* 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.
|
* The returned selection is not installed as the terminal's current selection.
|
||||||
* It is a snapshot with the same lifetime rules as GhosttySelection.
|
* It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const EventWrapper = struct {
|
||||||
release: SelectionGesture.Release,
|
release: SelectionGesture.Release,
|
||||||
drag: SelectionGesture.Drag,
|
drag: SelectionGesture.Drag,
|
||||||
autoscroll_tick: SelectionGesture.AutoscrollTick,
|
autoscroll_tick: SelectionGesture.AutoscrollTick,
|
||||||
|
deep_press: SelectionGesture.DeepPress,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Press.pin has no safe sentinel value: PageList.Pin contains a non-null
|
// Press.pin has no safe sentinel value: PageList.Pin contains a non-null
|
||||||
|
|
@ -73,6 +74,7 @@ const EventWrapper = struct {
|
||||||
.release => .{ .release = self.defaultRelease() },
|
.release => .{ .release = self.defaultRelease() },
|
||||||
.drag => .{ .drag = self.defaultDrag() },
|
.drag => .{ .drag = self.defaultDrag() },
|
||||||
.autoscroll_tick => .{ .autoscroll_tick = self.defaultAutoscrollTick() },
|
.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 {
|
fn deinit(self: *EventWrapper) void {
|
||||||
if (self.word_boundary_codepoints) |cps| {
|
if (self.word_boundary_codepoints) |cps| {
|
||||||
if (cps.len > 0) self.alloc.free(cps);
|
if (cps.len > 0) self.alloc.free(cps);
|
||||||
|
|
@ -163,6 +172,7 @@ pub const EventType = enum(c_int) {
|
||||||
release = 1,
|
release = 1,
|
||||||
drag = 2,
|
drag = 2,
|
||||||
autoscroll_tick = 3,
|
autoscroll_tick = 3,
|
||||||
|
deep_press = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// C: GhosttySelectionGestureEventOption
|
/// C: GhosttySelectionGestureEventOption
|
||||||
|
|
@ -324,6 +334,13 @@ pub fn handle_event(
|
||||||
} else if (sel == null) return .no_value;
|
} else if (sel == null) return .no_value;
|
||||||
return .success;
|
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),
|
.release => |*release| releaseSetTyped(release, option, value),
|
||||||
.drag => |*drag| dragSetTyped(event, drag, option, value),
|
.drag => |*drag| dragSetTyped(event, drag, option, value),
|
||||||
.autoscroll_tick => |*tick| autoscrollTickSetTyped(event, tick, 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;
|
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(
|
fn trySetWordBoundaryCodepoints(
|
||||||
event: *EventWrapper,
|
event: *EventWrapper,
|
||||||
target: *[]const u21,
|
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));
|
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" {
|
test "selection gesture free null" {
|
||||||
free(null, null);
|
free(null, null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue