mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-28 15:55:20 +00:00
terminal: SelectionGesture deep press
This commit is contained in:
@@ -4456,9 +4456,11 @@ pub fn mousePressureCallback(
|
||||
// Update our pressure stage.
|
||||
self.mouse.pressure_stage = stage;
|
||||
|
||||
// If our left mouse button is pressed and we're entering a deep
|
||||
// click then we want to start a selection. We treat this as a
|
||||
// word selection since that is typical macOS behavior.
|
||||
// A deep press is pressure-sensitive pointer input, such as macOS force
|
||||
// click / deep click on a trackpad, that occurs while the left mouse
|
||||
// button is already down. Treat it as the platform text-selection
|
||||
// affordance: select the pressed word, then consume the active gesture so
|
||||
// further cursor motion doesn't drag the selection.
|
||||
const left_idx = @intFromEnum(input.MouseButton.left);
|
||||
if (self.mouse.click_state[left_idx] == .press and
|
||||
stage == .deep)
|
||||
@@ -4466,14 +4468,21 @@ pub fn mousePressureCallback(
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// This should always be set in this state but we don't want
|
||||
// to handle state inconsistency here.
|
||||
const pin = self.mouse.activeLeftClickPin(&self.io.terminal.screens) orelse break :select;
|
||||
const sel = self.io.terminal.screens.active.selectWord(
|
||||
pin.*,
|
||||
self.config.selection_word_chars,
|
||||
) orelse break :select;
|
||||
try self.io.terminal.screens.active.select(sel);
|
||||
const sel = self.mouse.selection_gesture.deepPress(
|
||||
self.renderer_state.terminal,
|
||||
.{ .word_boundary_codepoints = self.config.selection_word_chars },
|
||||
);
|
||||
|
||||
// Deep press consumes the active drag gesture, so stop any pending
|
||||
// selection autoscroll timer that may have been started by the drag.
|
||||
if (self.selection_scroll_active) {
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
try self.io.terminal.screens.active.select(sel orelse break :select);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +243,10 @@ pub fn drag(
|
||||
/// Record a selection autoscroll tick for the active left-click drag gesture.
|
||||
/// This scrolls the viewport in the active autoscroll direction and then
|
||||
/// continues the drag at the provided position.
|
||||
///
|
||||
/// This always scrolls the viewport by exactly one row in the current
|
||||
/// autoscroll direction. If you want to scroll by more, increase your
|
||||
/// tick rate.
|
||||
pub fn autoscrollTick(
|
||||
self: *SelectionGesture,
|
||||
t: *Terminal,
|
||||
@@ -271,6 +275,46 @@ pub fn autoscrollTick(
|
||||
return self.drag(t, d);
|
||||
}
|
||||
|
||||
/// A pressure-based activation during an existing left-click gesture.
|
||||
///
|
||||
/// This is the terminal gesture model for platform features such as macOS
|
||||
/// force click / deep click on pressure-sensitive trackpads. It is not a
|
||||
/// distinct mouse button and it is not part of the normal single/double/triple
|
||||
/// click count sequence; it can only occur after a left press is already
|
||||
/// active.
|
||||
pub const DeepPress = struct {
|
||||
/// The codepoints that delimit words for the word selection produced by
|
||||
/// the deep press.
|
||||
word_boundary_codepoints: []const u21,
|
||||
};
|
||||
|
||||
/// Record a deep press event for the active left-click gesture.
|
||||
///
|
||||
/// A deep press is a force/pressure activation while the primary pointer is
|
||||
/// already down. Ghostty treats it like the platform text-selection affordance:
|
||||
/// select the word under the original press, then consume the gesture so
|
||||
/// further cursor movement while the button remains pressed does not drag or
|
||||
/// autoscroll the selection.
|
||||
pub fn deepPress(
|
||||
self: *SelectionGesture,
|
||||
t: *Terminal,
|
||||
p: DeepPress,
|
||||
) ?Selection {
|
||||
const click_pin = self.validatedLeftClickPin(&t.screens) orelse return null;
|
||||
const sel = t.screens.active.selectWord(
|
||||
click_pin.*,
|
||||
p.word_boundary_codepoints,
|
||||
);
|
||||
|
||||
self.left_click_count = 0;
|
||||
self.left_click_time = null;
|
||||
self.left_click_dragged = true;
|
||||
self.left_drag_autoscroll = .none;
|
||||
self.untrackPin(t);
|
||||
|
||||
return sel;
|
||||
}
|
||||
|
||||
pub const Release = struct {
|
||||
/// The cell where the release occurred, if the release position mapped to
|
||||
/// a valid cell. This is used synchronously to update gesture state and is
|
||||
@@ -1193,6 +1237,38 @@ test "SelectionGesture autoscroll tick stops with invalidated click" {
|
||||
try testing.expectEqual(@as(u3, 0), gesture.left_click_count);
|
||||
}
|
||||
|
||||
test "SelectionGesture deep press selects word and consumes drag" {
|
||||
var t = try Terminal.init(testing.allocator, .{ .cols = 20, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
try t.printString("alpha beta");
|
||||
|
||||
var gesture: SelectionGesture = .init;
|
||||
defer gesture.deinit(&t);
|
||||
|
||||
try gesture.press(&t, testPress(&t, 1, 0, try std.time.Instant.now()));
|
||||
_ = gesture.drag(&t, testDrag(&t, 1, 0, 10, 1));
|
||||
try testing.expectEqual(.up, gesture.left_drag_autoscroll);
|
||||
|
||||
const sel = gesture.deepPress(&t, .{
|
||||
.word_boundary_codepoints = &.{ ' ' },
|
||||
}).?;
|
||||
|
||||
try testing.expectEqualDeep(Selection.init(
|
||||
testPin(&t, 0, 0),
|
||||
testPin(&t, 4, 0),
|
||||
false,
|
||||
), sel);
|
||||
try testing.expectEqual(@as(u3, 0), gesture.left_click_count);
|
||||
try testing.expectEqual(@as(?std.time.Instant, null), gesture.left_click_time);
|
||||
try testing.expectEqual(true, gesture.left_click_dragged);
|
||||
try testing.expectEqual(.none, gesture.left_drag_autoscroll);
|
||||
try testing.expect(gesture.left_click_pin == null);
|
||||
|
||||
try testing.expectEqual(null, gesture.drag(&t, testDrag(&t, 7, 0, 70, 50)));
|
||||
gesture.release(&t, .{ .pin = testPin(&t, 7, 0) });
|
||||
try testing.expectEqual(true, gesture.left_click_dragged);
|
||||
}
|
||||
|
||||
test "SelectionGesture drag with invalidated click returns null" {
|
||||
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user