aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/SelectionGesture.zig
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:28:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:29:28 +0300
commit4a3302d19a2baa3a33e910e0088c24e780905b90 (patch)
treedcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/SelectionGesture.zig
parent550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff)
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/SelectionGesture.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/SelectionGesture.zig119
1 files changed, 112 insertions, 7 deletions
diff --git a/vendor/libghostty-vt/src/terminal/SelectionGesture.zig b/vendor/libghostty-vt/src/terminal/SelectionGesture.zig
index f0e11f63..2be1e8aa 100644
--- a/vendor/libghostty-vt/src/terminal/SelectionGesture.zig
+++ b/vendor/libghostty-vt/src/terminal/SelectionGesture.zig
@@ -228,7 +228,8 @@ pub fn validatedLeftClickPin(
}
pub const Press = struct {
- /// The time when the press event occurred. Use a monotonic timer.
+ /// The time when the press event occurred. Prefer a monotonic timer;
+ /// backwards timestamps reset the repeat sequence.
/// This can be null if you're on a system that doesn't support
/// time for some reason. In that case, we only support single clicks.
time: ?Time,
@@ -387,8 +388,8 @@ pub fn drag(
.cell => dragSelection(
click_pin.*,
d.pin,
- @intFromFloat(@max(0, self.left_click_xpos)),
- @intFromFloat(@max(0, d.xpos)),
+ pixelFromFloat(self.left_click_xpos),
+ pixelFromFloat(d.xpos),
d.rectangle,
d.geometry,
),
@@ -630,7 +631,8 @@ fn pressRepeat(
const time = p.time orelse return error.PressRequiresReset;
{
const prev_time = self.left_click_time orelse return error.PressRequiresReset;
- const since = timeSince(time, prev_time);
+ const since = timeSince(time, prev_time) orelse
+ return error.PressRequiresReset;
if (since > p.repeat_interval) return error.PressRequiresReset;
}
@@ -664,11 +666,29 @@ fn pressRepeat(
self.left_click_behavior = p.behaviors[self.left_click_count - 1];
}
-fn timeSince(time: Time, prev_time: Time) u64 {
- if (comptime freestanding_wasm) return time -| prev_time;
+fn timeSince(time: Time, prev_time: Time) ?u64 {
+ if (comptime freestanding_wasm) {
+ if (time < prev_time) return null;
+ return time - prev_time;
+ }
+
+ if (time.order(prev_time) == .lt) return null;
return time.since(prev_time);
}
+/// Convert a caller-provided floating-point position to a pixel coordinate.
+/// Negative and NaN values clamp to the origin, matching the drag behavior
+/// outside the left edge of the surface.
+fn pixelFromFloat(value: f64) u32 {
+ if (std.math.isNan(value) or value <= 0) return 0;
+
+ // @intFromFloat requires a value representable by the destination type.
+ // Saturate first so positive infinity and oversized coordinates are safe.
+ const max: f64 = @floatFromInt(std.math.maxInt(u32));
+ if (value >= max) return std.math.maxInt(u32);
+ return @intFromFloat(value);
+}
+
fn pressSelection(
self: *const SelectionGesture,
screen: *Screen,
@@ -713,6 +733,8 @@ fn dragSelection(
// Rectangular selections are handled similarly, except that
// entire columns are considered rather than individual cells.
+ if (geometry.columns == 0 or geometry.cell_width == 0) return null;
+
// We only include cells in the selection if the threshold point lies
// between the start and end points of the selection. A threshold of
// 60% of the cell width was chosen empirically because it felt good.
@@ -721,7 +743,12 @@ fn dragSelection(
));
// We use this to clamp the pixel positions below.
- const max_x = geometry.columns * geometry.cell_width - 1;
+ const pixel_span = std.math.mul(
+ u32,
+ geometry.columns,
+ geometry.cell_width,
+ ) catch std.math.maxInt(u32);
+ const max_x = pixel_span - 1;
// We need to know how far across in the cell the drag pos is, so
// we subtract the padding and then take it modulo the cell width.
@@ -935,6 +962,16 @@ fn testPress(t: *Terminal, x: u16, y: u32, time: ?std.time.Instant) Press {
};
}
+fn testInstant(ns: u64) std.time.Instant {
+ return switch (builtin.os.tag) {
+ .windows, .uefi, .wasi => .{ .timestamp = ns },
+ else => .{ .timestamp = .{
+ .sec = @intCast(ns / std.time.ns_per_s),
+ .nsec = @intCast(ns % std.time.ns_per_s),
+ } },
+ };
+}
+
fn testDrag(t: *Terminal, x: u16, y: u32, xpos: f64, ypos: f64) Drag {
return .{
.pin = t.screens.active.pages.pin(.{ .active = .{
@@ -1532,6 +1569,58 @@ test "SelectionGesture drag returns selection and records autoscroll" {
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
}
+test "SelectionGesture drag clamps unrepresentable positions" {
+ var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(testing.allocator);
+
+ var gesture: SelectionGesture = .init;
+ defer gesture.deinit(&t);
+
+ var press_event = testPress(&t, 1, 1, try std.time.Instant.now());
+ press_event.xpos = 10;
+ _ = try gesture.press(&t, press_event);
+
+ const positive = gesture.drag(
+ &t,
+ testDrag(&t, 1, 1, std.math.inf(f64), 50),
+ ).?;
+ try testing.expect((testPin(&t, 1, 1)).eql(positive.start()));
+ try testing.expect((testPin(&t, 1, 1)).eql(positive.end()));
+
+ try testing.expectEqual(null, gesture.drag(
+ &t,
+ testDrag(&t, 1, 1, std.math.nan(f64), 50),
+ ));
+}
+
+test "SelectionGesture drag saturates overflowing geometry" {
+ var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(testing.allocator);
+
+ var gesture: SelectionGesture = .init;
+ defer gesture.deinit(&t);
+
+ _ = try gesture.press(&t, testPress(&t, 1, 1, try std.time.Instant.now()));
+ var drag_event = testDrag(&t, 1, 1, 10, 50);
+ drag_event.geometry.columns = std.math.maxInt(u32);
+ drag_event.geometry.cell_width = std.math.maxInt(u32);
+ try testing.expectEqual(null, gesture.drag(&t, drag_event));
+}
+
+test "SelectionGesture drag rejects empty geometry" {
+ var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(testing.allocator);
+
+ var gesture: SelectionGesture = .init;
+ defer gesture.deinit(&t);
+
+ _ = try gesture.press(&t, testPress(&t, 1, 1, try std.time.Instant.now()));
+ var drag_event = testDrag(&t, 1, 1, 10, 50);
+ drag_event.geometry.columns = 0;
+ drag_event.geometry.cell_width = 0;
+ try testing.expectEqual(null, gesture.drag(&t, drag_event));
+}
+
test "SelectionGesture release clears autoscroll and records drag" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);
@@ -1965,6 +2054,22 @@ test "SelectionGesture expired repeat resets click count" {
try testing.expectEqual(@as(u3, 1), gesture.left_click_count);
}
+test "SelectionGesture backwards repeat time resets click count" {
+ var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(testing.allocator);
+
+ var gesture: SelectionGesture = .init;
+ defer gesture.deinit(&t);
+
+ const earlier = testInstant(std.time.ns_per_s);
+ const later = testInstant(2 * std.time.ns_per_s);
+ _ = try gesture.press(&t, testPress(&t, 1, 1, later));
+ _ = try gesture.press(&t, testPress(&t, 1, 1, earlier));
+
+ try testing.expectEqual(@as(u3, 1), gesture.left_click_count);
+ try testing.expectEqual(.eq, gesture.left_click_time.?.order(earlier));
+}
+
test "SelectionGesture screen switch resets click count" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);