terminal: add configurable behaviors based on click count
parent
82a73f2bf1
commit
7d4d1e5819
|
|
@ -3961,6 +3961,11 @@ pub fn mouseButtonCallback(
|
||||||
.max_distance = @floatFromInt(self.size.cell.width),
|
.max_distance = @floatFromInt(self.size.cell.width),
|
||||||
.repeat_interval = self.config.mouse_interval,
|
.repeat_interval = self.config.mouse_interval,
|
||||||
.word_boundary_codepoints = self.config.selection_word_chars,
|
.word_boundary_codepoints = self.config.selection_word_chars,
|
||||||
|
.behaviors = &.{
|
||||||
|
.cell,
|
||||||
|
.word,
|
||||||
|
if (mods.ctrlOrSuper()) .output else .line,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The gesture owns the standard single/double/triple-click selection
|
// The gesture owns the standard single/double/triple-click selection
|
||||||
|
|
@ -3984,12 +3989,7 @@ pub fn mouseButtonCallback(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cmd/Ctrl triple-click selects semantic command output instead of
|
3 => {},
|
||||||
// the standard line selection returned by the gesture.
|
|
||||||
3 => {
|
|
||||||
if (mods.ctrlOrSuper()) press_selection =
|
|
||||||
self.io.terminal.screens.active.selectOutput(pin);
|
|
||||||
},
|
|
||||||
|
|
||||||
// We should be bounded by 1 to 3
|
// We should be bounded by 1 to 3
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@
|
||||||
///
|
///
|
||||||
/// Double- and triple-click gestures use the same event flow. Repeated presses
|
/// Double- and triple-click gestures use the same event flow. Repeated presses
|
||||||
/// inside `Press.repeat_interval` and within `Press.max_distance` increment the
|
/// inside `Press.repeat_interval` and within `Press.max_distance` increment the
|
||||||
/// internal click count up to three. A single press returns null to clear any
|
/// internal click count up to three. `Press.behaviors` maps single-, double-,
|
||||||
/// existing selection, a double-click returns a word selection, and a
|
/// and triple-clicks to behavior. By default, a single press returns null to
|
||||||
/// triple-click returns a line selection. A drag after a double-click expands by
|
/// clear any existing selection, a double-click returns a word selection, and a
|
||||||
/// word; a drag after a triple-click expands by line. A new press that is too
|
/// triple-click returns a line selection. Drags use the behavior selected by the
|
||||||
/// late, too far away, or on another active screen starts a new single-click
|
/// corresponding press. A new press that is too late, too far away, or on
|
||||||
/// gesture.
|
/// another active screen starts a new single-click gesture.
|
||||||
///
|
///
|
||||||
/// # Resetting and lifetime
|
/// # Resetting and lifetime
|
||||||
///
|
///
|
||||||
|
|
@ -89,6 +89,9 @@ left_click_screen_generation: usize,
|
||||||
left_click_count: u3,
|
left_click_count: u3,
|
||||||
left_click_time: ?std.time.Instant,
|
left_click_time: ?std.time.Instant,
|
||||||
|
|
||||||
|
/// The selection behavior chosen for the active left-click gesture.
|
||||||
|
left_click_behavior: Behavior,
|
||||||
|
|
||||||
/// The starting xpos/ypos of the left click. Note that if scrolling occurs,
|
/// The starting xpos/ypos of the left click. Note that if scrolling occurs,
|
||||||
/// these will point to different cells, but the xpos/ypos will stay
|
/// these will point to different cells, but the xpos/ypos will stay
|
||||||
/// stable during scrolling relative to the surface.
|
/// stable during scrolling relative to the surface.
|
||||||
|
|
@ -116,6 +119,28 @@ left_drag_autoscroll: Autoscroll,
|
||||||
/// wants to drag the viewport.
|
/// wants to drag the viewport.
|
||||||
pub const Autoscroll = enum { none, up, down };
|
pub const Autoscroll = enum { none, up, down };
|
||||||
|
|
||||||
|
/// The selection behavior for a click and subsequent drag.
|
||||||
|
pub const Behavior = enum {
|
||||||
|
/// Cell-granular drag selection. Press returns null to clear selection.
|
||||||
|
cell,
|
||||||
|
|
||||||
|
/// Word selection on press and word-granular drag selection.
|
||||||
|
word,
|
||||||
|
|
||||||
|
/// Line selection on press and line-granular drag selection.
|
||||||
|
line,
|
||||||
|
|
||||||
|
/// Semantic command output selection on press and drag.
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Standard terminal selection behavior for single-, double-, and triple-clicks.
|
||||||
|
///
|
||||||
|
/// A single click uses cell behavior, which returns null on press so callers can
|
||||||
|
/// clear any existing selection and then drag by cell. A double-click selects and
|
||||||
|
/// drags by word. A triple-click selects and drags by line.
|
||||||
|
pub const default_behaviors: [3]Behavior = .{ .cell, .word, .line };
|
||||||
|
|
||||||
/// Distance from the top or bottom surface edge, in pixels, where dragging
|
/// Distance from the top or bottom surface edge, in pixels, where dragging
|
||||||
/// should request autoscroll. This preserves the historical 1px buffer used
|
/// should request autoscroll. This preserves the historical 1px buffer used
|
||||||
/// so fullscreen-edge drags can still trigger autoscroll.
|
/// so fullscreen-edge drags can still trigger autoscroll.
|
||||||
|
|
@ -125,6 +150,7 @@ pub const init: SelectionGesture = .{
|
||||||
.left_click_pin = null,
|
.left_click_pin = null,
|
||||||
.left_click_count = 0,
|
.left_click_count = 0,
|
||||||
.left_click_time = null,
|
.left_click_time = null,
|
||||||
|
.left_click_behavior = .cell,
|
||||||
.left_click_screen = .primary,
|
.left_click_screen = .primary,
|
||||||
.left_click_screen_generation = 0,
|
.left_click_screen_generation = 0,
|
||||||
.left_click_xpos = 0,
|
.left_click_xpos = 0,
|
||||||
|
|
@ -157,6 +183,7 @@ pub fn deinit(self: *SelectionGesture, t: *Terminal) void {
|
||||||
pub fn reset(self: *SelectionGesture, t: *Terminal) void {
|
pub fn reset(self: *SelectionGesture, t: *Terminal) void {
|
||||||
self.left_click_count = 0;
|
self.left_click_count = 0;
|
||||||
self.left_click_time = null;
|
self.left_click_time = null;
|
||||||
|
self.left_click_behavior = .cell;
|
||||||
self.left_click_dragged = false;
|
self.left_click_dragged = false;
|
||||||
self.left_drag_autoscroll = .none;
|
self.left_drag_autoscroll = .none;
|
||||||
self.untrackPin(t);
|
self.untrackPin(t);
|
||||||
|
|
@ -213,6 +240,9 @@ pub const Press = struct {
|
||||||
|
|
||||||
/// The codepoints that delimit words for double-click selection.
|
/// The codepoints that delimit words for double-click selection.
|
||||||
word_boundary_codepoints: []const u21,
|
word_boundary_codepoints: []const u21,
|
||||||
|
|
||||||
|
/// Selection behaviors for single-, double-, and triple-clicks.
|
||||||
|
behaviors: *const [3]Behavior = &default_behaviors,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Record a press event and return the standard selection for this click.
|
/// Record a press event and return the standard selection for this click.
|
||||||
|
|
@ -227,11 +257,11 @@ pub const Press = struct {
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// Examples:
|
||||||
///
|
///
|
||||||
/// * first press: `left_click_count == 1`, returns null to clear selection;
|
/// * first press: `left_click_count == 1`, defaults to cell behavior;
|
||||||
/// * second nearby press within the repeat interval: `left_click_count == 2`,
|
/// * second nearby press within the repeat interval: `left_click_count == 2`,
|
||||||
/// returns a word selection and later drags select by word;
|
/// defaults to word behavior;
|
||||||
/// * third nearby press within the repeat interval: `left_click_count == 3`,
|
/// * third nearby press within the repeat interval: `left_click_count == 3`,
|
||||||
/// returns a line selection and later drags select by line;
|
/// defaults to line behavior;
|
||||||
/// * press after the interval, too far away, or after a screen generation
|
/// * press after the interval, too far away, or after a screen generation
|
||||||
/// change: starts over at `left_click_count == 1` and returns null.
|
/// change: starts over at `left_click_count == 1` and returns null.
|
||||||
pub fn press(
|
pub fn press(
|
||||||
|
|
@ -336,10 +366,8 @@ pub fn drag(
|
||||||
else
|
else
|
||||||
.none;
|
.none;
|
||||||
|
|
||||||
return switch (self.left_click_count) {
|
return switch (self.left_click_behavior) {
|
||||||
0 => unreachable, // handled above
|
.cell => dragSelection(
|
||||||
|
|
||||||
1 => dragSelection(
|
|
||||||
click_pin.*,
|
click_pin.*,
|
||||||
d.pin,
|
d.pin,
|
||||||
@intFromFloat(@max(0, self.left_click_xpos)),
|
@intFromFloat(@max(0, self.left_click_xpos)),
|
||||||
|
|
@ -348,19 +376,24 @@ pub fn drag(
|
||||||
d.geometry,
|
d.geometry,
|
||||||
),
|
),
|
||||||
|
|
||||||
2 => dragSelectionWord(
|
.word => dragSelectionWord(
|
||||||
t.screens.active,
|
t.screens.active,
|
||||||
click_pin.*,
|
click_pin.*,
|
||||||
d.pin,
|
d.pin,
|
||||||
d.word_boundary_codepoints,
|
d.word_boundary_codepoints,
|
||||||
),
|
),
|
||||||
|
|
||||||
3 => dragSelectionLine(
|
.line => dragSelectionLine(
|
||||||
|
t.screens.active,
|
||||||
|
click_pin.*,
|
||||||
|
d.pin,
|
||||||
|
),
|
||||||
|
|
||||||
|
.output => dragSelectionOutput(
|
||||||
t.screens.active,
|
t.screens.active,
|
||||||
click_pin.*,
|
click_pin.*,
|
||||||
d.pin,
|
d.pin,
|
||||||
),
|
),
|
||||||
else => unreachable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,6 +478,7 @@ pub fn deepPress(
|
||||||
|
|
||||||
self.left_click_count = 0;
|
self.left_click_count = 0;
|
||||||
self.left_click_time = null;
|
self.left_click_time = null;
|
||||||
|
self.left_click_behavior = .cell;
|
||||||
self.left_click_dragged = true;
|
self.left_click_dragged = true;
|
||||||
self.left_drag_autoscroll = .none;
|
self.left_drag_autoscroll = .none;
|
||||||
self.untrackPin(t);
|
self.untrackPin(t);
|
||||||
|
|
@ -512,6 +546,7 @@ fn pressInitial(
|
||||||
}
|
}
|
||||||
errdefer comptime unreachable;
|
errdefer comptime unreachable;
|
||||||
self.left_click_count = 1;
|
self.left_click_count = 1;
|
||||||
|
self.left_click_behavior = p.behaviors[0];
|
||||||
self.left_click_xpos = p.xpos;
|
self.left_click_xpos = p.xpos;
|
||||||
self.left_click_ypos = p.ypos;
|
self.left_click_ypos = p.ypos;
|
||||||
self.left_click_time = p.time;
|
self.left_click_time = p.time;
|
||||||
|
|
@ -526,6 +561,7 @@ fn pressRepeat(
|
||||||
) error{PressRequiresReset}!void {
|
) error{PressRequiresReset}!void {
|
||||||
errdefer {
|
errdefer {
|
||||||
self.left_click_count = 0;
|
self.left_click_count = 0;
|
||||||
|
self.left_click_behavior = .cell;
|
||||||
self.untrackPin(t);
|
self.untrackPin(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -564,6 +600,7 @@ fn pressRepeat(
|
||||||
self.left_click_count + 1,
|
self.left_click_count + 1,
|
||||||
3, // We only support triple clicks max
|
3, // We only support triple clicks max
|
||||||
);
|
);
|
||||||
|
self.left_click_behavior = p.behaviors[self.left_click_count - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pressSelection(
|
fn pressSelection(
|
||||||
|
|
@ -571,12 +608,11 @@ fn pressSelection(
|
||||||
screen: *Screen,
|
screen: *Screen,
|
||||||
p: Press,
|
p: Press,
|
||||||
) ?Selection {
|
) ?Selection {
|
||||||
return switch (self.left_click_count) {
|
return switch (self.left_click_behavior) {
|
||||||
0 => unreachable,
|
.cell => null,
|
||||||
1 => null,
|
.word => screen.selectWord(p.pin, p.word_boundary_codepoints),
|
||||||
2 => screen.selectWord(p.pin, p.word_boundary_codepoints),
|
.line => screen.selectLine(.{ .pin = p.pin }),
|
||||||
3 => screen.selectLine(.{ .pin = p.pin }),
|
.output => screen.selectOutput(p.pin),
|
||||||
else => unreachable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -784,6 +820,26 @@ fn dragSelectionLine(
|
||||||
return sel;
|
return sel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the appropriate semantic-output-wise selection for an output
|
||||||
|
/// drag. This expands from the output block under the click point to the output
|
||||||
|
/// block under the current drag point. If the drag point is not output, keep the
|
||||||
|
/// original output selection.
|
||||||
|
fn dragSelectionOutput(
|
||||||
|
screen: *Screen,
|
||||||
|
click_pin: Pin,
|
||||||
|
drag_pin: Pin,
|
||||||
|
) ?Selection {
|
||||||
|
var sel = screen.selectOutput(click_pin) orelse return null;
|
||||||
|
const current = screen.selectOutput(drag_pin) orelse return sel;
|
||||||
|
|
||||||
|
if (drag_pin.before(click_pin)) {
|
||||||
|
sel.startPtr().* = current.start();
|
||||||
|
} else {
|
||||||
|
sel.endPtr().* = current.end();
|
||||||
|
}
|
||||||
|
return sel;
|
||||||
|
}
|
||||||
|
|
||||||
fn untrackPin(self: *SelectionGesture, t: *Terminal) void {
|
fn untrackPin(self: *SelectionGesture, t: *Terminal) void {
|
||||||
// Can't untrack unless we have a pin.
|
// Can't untrack unless we have a pin.
|
||||||
const pin = self.left_click_pin orelse return;
|
const pin = self.left_click_pin orelse return;
|
||||||
|
|
@ -1294,6 +1350,74 @@ test "SelectionGesture press returns standard click selections" {
|
||||||
), (try gesture.press(&t, event)).?);
|
), (try gesture.press(&t, event)).?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "SelectionGesture press behaviors choose press and drag behavior" {
|
||||||
|
var t = try Terminal.init(testing.allocator, .{ .cols = 20, .rows = 5 });
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
try t.printString("alpha beta\none two\nthree four");
|
||||||
|
|
||||||
|
var gesture: SelectionGesture = .init;
|
||||||
|
defer gesture.deinit(&t);
|
||||||
|
|
||||||
|
const time = try std.time.Instant.now();
|
||||||
|
var event = testPress(&t, 1, 0, time);
|
||||||
|
event.behaviors = &.{ .cell, .line, .word };
|
||||||
|
event.word_boundary_codepoints = &.{ ' ' };
|
||||||
|
|
||||||
|
_ = try gesture.press(&t, event);
|
||||||
|
try testing.expectEqual(.cell, gesture.left_click_behavior);
|
||||||
|
|
||||||
|
const double_click = (try gesture.press(&t, event)).?;
|
||||||
|
try testing.expectEqual(.line, gesture.left_click_behavior);
|
||||||
|
try testing.expectEqualDeep(Selection.init(
|
||||||
|
testPin(&t, 0, 0),
|
||||||
|
testPin(&t, 9, 0),
|
||||||
|
false,
|
||||||
|
), double_click);
|
||||||
|
|
||||||
|
const line_drag = gesture.drag(&t, testDrag(&t, 2, 2, 20, 50)).?;
|
||||||
|
try testing.expectEqualDeep(Selection.init(
|
||||||
|
testPin(&t, 0, 0),
|
||||||
|
testPin(&t, 9, 2),
|
||||||
|
false,
|
||||||
|
), line_drag);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "SelectionGesture output behavior selects and drags semantic output" {
|
||||||
|
var t = try Terminal.init(testing.allocator, .{ .cols = 10, .rows = 6 });
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
const screen = t.screens.active;
|
||||||
|
screen.cursorSetSemanticContent(.output);
|
||||||
|
try screen.testWriteString("out1\n");
|
||||||
|
screen.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||||
|
try screen.testWriteString("$ ");
|
||||||
|
screen.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||||
|
try screen.testWriteString("cmd\n");
|
||||||
|
screen.cursorSetSemanticContent(.output);
|
||||||
|
try screen.testWriteString("out2");
|
||||||
|
|
||||||
|
var gesture: SelectionGesture = .init;
|
||||||
|
defer gesture.deinit(&t);
|
||||||
|
|
||||||
|
var event = testPress(&t, 1, 0, try std.time.Instant.now());
|
||||||
|
event.behaviors = &.{ .output, .word, .line };
|
||||||
|
|
||||||
|
const press_selection = (try gesture.press(&t, event)).?;
|
||||||
|
try testing.expectEqual(.output, gesture.left_click_behavior);
|
||||||
|
try testing.expectEqualDeep(Selection.init(
|
||||||
|
testPin(&t, 0, 0),
|
||||||
|
testPin(&t, 3, 0),
|
||||||
|
false,
|
||||||
|
), press_selection);
|
||||||
|
|
||||||
|
const output_drag = gesture.drag(&t, testDrag(&t, 1, 2, 10, 50)).?;
|
||||||
|
try testing.expectEqualDeep(Selection.init(
|
||||||
|
testPin(&t, 0, 0),
|
||||||
|
testPin(&t, 3, 2),
|
||||||
|
false,
|
||||||
|
), output_drag);
|
||||||
|
}
|
||||||
|
|
||||||
test "SelectionGesture drag returns selection and records autoscroll" {
|
test "SelectionGesture drag returns selection and records autoscroll" {
|
||||||
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue