Rework mouse selection logic (#7444)

This PR fixes the problem discussed in #5058 and #7434 by reworking the
selection logic in a way that better handles edge cases as well as being
generally cleaner.

This rework does change how selection behaves slightly, especially
rectangular selection, but the new behavior of rectangular selection is
more in line with other terminals I tested (Terminal.app, Kitty).

There are some TODO comments for adding unit tests- I ran out of steam
tonight, but if this PR is still open tomorrow I'll go ahead and add
them.
This commit is contained in:
Mitchell Hashimoto
2025-05-27 10:12:31 -07:00
committed by GitHub
4 changed files with 645 additions and 151 deletions

View File

@ -3676,165 +3676,162 @@ fn dragLeftClickTriple(
fn dragLeftClickSingle(
self: *Surface,
drag_pin: terminal.Pin,
xpos: f64,
drag_x: f64,
) !void {
// NOTE(mitchellh): This logic super sucks. There has to be an easier way
// to calculate this, but this is good for a v1. Selection isn't THAT
// common so its not like this performance heavy code is running that
// often.
// TODO: unit test this, this logic sucks
// If we were selecting, and we switched directions, then we restart
// calculations because it forces us to reconsider if the first cell is
// selected.
self.checkResetSelSwitch(drag_pin);
// Our logic for determining if the starting cell is selected:
//
// - The "xboundary" is 60% the width of a cell from the left. We choose
// 60% somewhat arbitrarily based on feeling.
// - If we started our click left of xboundary, backwards selections
// can NEVER select the current char.
// - If we started our click right of xboundary, backwards selections
// ALWAYS selected the current char, but we must move the cursor
// left of the xboundary.
// - Inverted logic for forwards selections.
//
// Our clicking point
const click_pin = self.mouse.left_click_pin.?.*;
// the boundary point at which we consider selection or non-selection
const cell_width_f64: f64 = @floatFromInt(self.size.cell.width);
const cell_xboundary = cell_width_f64 * 0.6;
// first xpos of the clicked cell adjusted for padding
const left_padding_f64: f64 = @as(f64, @floatFromInt(self.size.padding.left));
const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64;
const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64;
// If this is the same cell, then we only start the selection if weve
// moved past the boundary point the opposite direction from where we
// started.
if (click_pin.eql(drag_pin)) {
// Ensuring to adjusting the cursor position for padding
const cell_xpos = xpos - cell_xstart - left_padding_f64;
const selected: bool = if (cell_start_xpos < cell_xboundary)
cell_xpos >= cell_xboundary
else
cell_xpos < cell_xboundary;
try self.setSelection(if (selected) terminal.Selection.init(
drag_pin,
drag_pin,
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
) else null);
return;
}
// If this is a different cell and we haven't started selection,
// we determine the starting cell first.
if (self.io.terminal.screen.selection == null) {
// - If we're moving to a point before the start, then we select
// the starting cell if we started after the boundary, else
// we start selection of the prior cell.
// - Inverse logic for a point after the start.
const start: terminal.Pin = if (dragLeftClickBefore(
drag_pin,
click_pin,
self.mouse.mods,
)) start: {
if (cell_start_xpos >= cell_xboundary) break :start click_pin;
if (click_pin.x > 0) break :start click_pin.left(1);
var start = click_pin.up(1) orelse click_pin;
start.x = self.io.terminal.screen.pages.cols - 1;
break :start start;
} else start: {
if (cell_start_xpos < cell_xboundary) break :start click_pin;
if (click_pin.x < self.io.terminal.screen.pages.cols - 1)
break :start click_pin.right(1);
var start = click_pin.down(1) orelse click_pin;
start.x = 0;
break :start start;
};
try self.setSelection(terminal.Selection.init(
start,
drag_pin,
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
));
return;
}
// TODO: detect if selection point is passed the point where we've
// actually written data before and disallow it.
// We moved! Set the selection end point. The start point should be
// set earlier.
assert(self.io.terminal.screen.selection != null);
const sel = self.io.terminal.screen.selection.?;
try self.setSelection(terminal.Selection.init(
sel.start(),
// This logic is in a separate function so that it can be unit tested.
try self.setSelection(mouseSelection(
self.mouse.left_click_pin.?.*,
drag_pin,
sel.rectangle,
@intFromFloat(@max(0.0, self.mouse.left_click_xpos)),
@intFromFloat(@max(0.0, drag_x)),
self.mouse.mods,
self.size,
));
}
// Resets the selection if we switched directions, depending on the select
// mode. See dragLeftClickSingle for more details.
fn checkResetSelSwitch(
self: *Surface,
/// Calculates the appropriate selection given pins and pixel x positions for
/// the click point and the drag point, as well as mouse mods and screen size.
fn mouseSelection(
click_pin: terminal.Pin,
drag_pin: terminal.Pin,
) void {
const screen = &self.io.terminal.screen;
const sel = screen.selection orelse return;
const sel_start = sel.start();
const sel_end = sel.end();
click_x: u32,
drag_x: u32,
mods: input.Mods,
size: rendererpkg.Size,
) ?terminal.Selection {
// Explanation:
//
// # Normal selections
//
// ## Left-to-right selections
// - The clicked cell is included if it was clicked to the left of its
// threshold point and the drag location is right of the threshold point.
// - The cell under the cursor (the "drag cell") is included if the drag
// location is right of its threshold point.
//
// ## Right-to-left selections
// - The clicked cell is included if it was clicked to the right of its
// threshold point and the drag location is left of the threshold point.
// - The cell under the cursor (the "drag cell") is included if the drag
// location is left of its threshold point.
//
// # Rectangular selections
//
// Rectangular selections are handled similarly, except that
// entire columns are considered rather than individual cells.
var reset: bool = false;
if (sel.rectangle) {
// When we're in rectangle mode, we reset the selection relative to
// the click point depending on the selection mode we're in, with
// the exception of single-column selections, which we always reset
// on if we drift.
if (sel_start.x == sel_end.x) {
reset = drag_pin.x != sel_start.x;
} else {
reset = switch (sel.order(screen)) {
.forward => drag_pin.x < sel_start.x or drag_pin.before(sel_start),
.reverse => drag_pin.x > sel_start.x or sel_start.before(drag_pin),
.mirrored_forward => drag_pin.x > sel_start.x or drag_pin.before(sel_start),
.mirrored_reverse => drag_pin.x < sel_start.x or sel_start.before(drag_pin),
// 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.
const threshold_point: u32 = @intFromFloat(@round(
@as(f64, @floatFromInt(size.cell.width)) * 0.6,
));
// We use this to clamp the pixel positions below.
const max_x = size.grid().columns * size.cell.width - 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.
const drag_x_frac = @min(max_x, drag_x -| size.padding.left) % size.cell.width;
// We figure out the fractional part of the click x position similarly.
const click_x_frac = @min(max_x, click_x -| size.padding.left) % size.cell.width;
// Whether or not this is a rectangular selection.
const rectangle_selection = SurfaceMouse.isRectangleSelectState(mods);
// Whether the click pin and drag pin are equal.
const same_pin = drag_pin.eql(click_pin);
// Whether or not the end point of our selection is before the start point.
const end_before_start = ebs: {
if (same_pin) {
break :ebs drag_x_frac < click_x_frac;
}
// Special handling for rectangular selections, we only use x position.
if (rectangle_selection) {
break :ebs switch (std.math.order(drag_pin.x, click_pin.x)) {
.eq => drag_x_frac < click_x_frac,
.lt => true,
.gt => false,
};
}
} else {
// Normal select uses simpler logic that is just based on the
// selection start/end.
reset = if (sel_end.before(sel_start))
sel_start.before(drag_pin)
break :ebs drag_pin.before(click_pin);
};
// Whether or not the the click pin cell
// should be included in the selection.
const include_click_cell = if (end_before_start)
click_x_frac >= threshold_point
else
click_x_frac < threshold_point;
// Whether or not the the drag pin cell
// should be included in the selection.
const include_drag_cell = if (end_before_start)
drag_x_frac < threshold_point
else
drag_x_frac >= threshold_point;
// If the click cell should be included in the selection then it's the
// start, otherwise we get the previous or next cell to it depending on
// the type and direction of the selection.
const start_pin =
if (include_click_cell)
click_pin
else if (end_before_start)
if (rectangle_selection)
click_pin.leftClamp(1)
else
click_pin.leftWrap(1) orelse click_pin
else if (rectangle_selection)
click_pin.rightClamp(1)
else
drag_pin.before(sel_start);
click_pin.rightWrap(1) orelse click_pin;
// Likewise for the end pin with the drag cell.
const end_pin =
if (include_drag_cell)
drag_pin
else if (end_before_start)
if (rectangle_selection)
drag_pin.rightClamp(1)
else
drag_pin.rightWrap(1) orelse drag_pin
else if (rectangle_selection)
drag_pin.leftClamp(1)
else
drag_pin.leftWrap(1) orelse drag_pin;
// If the click cell is the same as the drag cell and the click cell
// shouldn't be included, or if the cells are adjacent such that the
// start or end pin becomes the other cell, and that cell should not
// be included, then we have no selection, so we set it to null.
//
// If in rectangular selection mode, we compare columns as well.
//
// TODO(qwerasd): this can/should probably be refactored, it's a bit
// repetitive and does excess work in rectangle mode.
if ((!include_click_cell and same_pin) or
(!include_click_cell and rectangle_selection and click_pin.x == drag_pin.x) or
(!include_click_cell and end_pin.eql(click_pin)) or
(!include_click_cell and rectangle_selection and end_pin.x == click_pin.x) or
(!include_drag_cell and start_pin.eql(drag_pin)) or
(!include_drag_cell and rectangle_selection and start_pin.x == drag_pin.x))
{
return null;
}
// Nullifying a selection can't fail.
if (reset) self.setSelection(null) catch unreachable;
}
// TODO: Clamp selection to the screen area, don't
// let it extend past the last written row.
// Handles how whether or not the drag screen point is before the click point.
// When we are in rectangle select, we only interpret the x axis to determine
// where to start the selection (before or after the click point). See
// dragLeftClickSingle for more details.
fn dragLeftClickBefore(
drag_pin: terminal.Pin,
click_pin: terminal.Pin,
mods: input.Mods,
) bool {
if (mods.ctrlOrSuper() and mods.alt) {
return drag_pin.x < click_pin.x;
}
return drag_pin.before(click_pin);
return .init(
start_pin,
end_pin,
rectangle_selection,
);
}
/// Call to notify Ghostty that the color scheme for the terminal has
@ -4819,3 +4816,430 @@ fn presentSurface(self: *Surface) !void {
{},
);
}
/// Utility function for the unit tests for mouse selection logic.
///
/// Tests a click and drag on a 10x5 cell grid, x positions are given in
/// fractional cells, e.g. 3.1 would be 10% through the cell at x = 3.
///
/// NOTE: The size tested with has 10px wide cells, meaning only one digit
/// after the decimal place has any meaning, e.g. 3.14 is equal to 3.1.
///
/// The provided start_x/y and end_x/y are the expected start and end points
/// of the resulting selection.
fn testMouseSelection(
click_x: f64,
click_y: u32,
drag_x: f64,
drag_y: u32,
start_x: terminal.size.CellCountInt,
start_y: u32,
end_x: terminal.size.CellCountInt,
end_y: u32,
rect: bool,
) !void {
assert(builtin.is_test);
// Our screen size is 10x5 cells that are
// 10x20 px, with 5px padding on all sides.
const size: rendererpkg.Size = .{
.cell = .{ .width = 10, .height = 20 },
.padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 },
.screen = .{ .width = 110, .height = 110 },
};
var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0);
defer screen.deinit();
// We hold both ctrl and alt for rectangular
// select so that this test is platform agnostic.
const mods: input.Mods = .{
.ctrl = rect,
.alt = rect,
};
try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods));
const click_pin = screen.pages.pin(.{
.viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y },
}) orelse unreachable;
const drag_pin = screen.pages.pin(.{
.viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y },
}) orelse unreachable;
const cell_width_f64: f64 = @floatFromInt(size.cell.width);
const click_x_pos: u32 =
@as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) +
size.padding.left;
const drag_x_pos: u32 =
@as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) +
size.padding.left;
const start_pin = screen.pages.pin(.{
.viewport = .{ .x = start_x, .y = start_y },
}) orelse unreachable;
const end_pin = screen.pages.pin(.{
.viewport = .{ .x = end_x, .y = end_y },
}) orelse unreachable;
try std.testing.expectEqualDeep(terminal.Selection{
.bounds = .{ .untracked = .{
.start = start_pin,
.end = end_pin,
} },
.rectangle = rect,
}, mouseSelection(
click_pin,
drag_pin,
click_x_pos,
drag_x_pos,
mods,
size,
));
}
/// Like `testMouseSelection` but checks that the resulting selection is null.
///
/// See `testMouseSelection` for more details.
fn testMouseSelectionIsNull(
click_x: f64,
click_y: u32,
drag_x: f64,
drag_y: u32,
rect: bool,
) !void {
assert(builtin.is_test);
// Our screen size is 10x5 cells that are
// 10x20 px, with 5px padding on all sides.
const size: rendererpkg.Size = .{
.cell = .{ .width = 10, .height = 20 },
.padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 },
.screen = .{ .width = 110, .height = 110 },
};
var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0);
defer screen.deinit();
// We hold both ctrl and alt for rectangular
// select so that this test is platform agnostic.
const mods: input.Mods = .{
.ctrl = rect,
.alt = rect,
};
try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods));
const click_pin = screen.pages.pin(.{
.viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y },
}) orelse unreachable;
const drag_pin = screen.pages.pin(.{
.viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y },
}) orelse unreachable;
const cell_width_f64: f64 = @floatFromInt(size.cell.width);
const click_x_pos: u32 =
@as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) +
size.padding.left;
const drag_x_pos: u32 =
@as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) +
size.padding.left;
try std.testing.expectEqual(
null,
mouseSelection(
click_pin,
drag_pin,
click_x_pos,
drag_x_pos,
mods,
size,
),
);
}
test "Surface: selection logic" {
// We disable format to make these easier to
// read by pairing sets of coordinates per line.
// zig fmt: off
// -- LTR
// single cell selection
try testMouseSelection(
3.0, 3, // click
3.9, 3, // drag
3, 3, // expected start
3, 3, // expected end
false, // regular selection
);
// including click and drag pin cells
try testMouseSelection(
3.0, 3, // click
5.9, 3, // drag
3, 3, // expected start
5, 3, // expected end
false, // regular selection
);
// including click pin cell but not drag pin cell
try testMouseSelection(
3.0, 3, // click
5.0, 3, // drag
3, 3, // expected start
4, 3, // expected end
false, // regular selection
);
// including drag pin cell but not click pin cell
try testMouseSelection(
3.9, 3, // click
5.9, 3, // drag
4, 3, // expected start
5, 3, // expected end
false, // regular selection
);
// including neither click nor drag pin cells
try testMouseSelection(
3.9, 3, // click
5.0, 3, // drag
4, 3, // expected start
4, 3, // expected end
false, // regular selection
);
// empty selection (single cell on only left half)
try testMouseSelectionIsNull(
3.0, 3, // click
3.1, 3, // drag
false, // regular selection
);
// empty selection (single cell on only right half)
try testMouseSelectionIsNull(
3.8, 3, // click
3.9, 3, // drag
false, // regular selection
);
// empty selection (between two cells, not crossing threshold)
try testMouseSelectionIsNull(
3.9, 3, // click
4.0, 3, // drag
false, // regular selection
);
// -- RTL
// single cell selection
try testMouseSelection(
3.9, 3, // click
3.0, 3, // drag
3, 3, // expected start
3, 3, // expected end
false, // regular selection
);
// including click and drag pin cells
try testMouseSelection(
5.9, 3, // click
3.0, 3, // drag
5, 3, // expected start
3, 3, // expected end
false, // regular selection
);
// including click pin cell but not drag pin cell
try testMouseSelection(
5.9, 3, // click
3.9, 3, // drag
5, 3, // expected start
4, 3, // expected end
false, // regular selection
);
// including drag pin cell but not click pin cell
try testMouseSelection(
5.0, 3, // click
3.0, 3, // drag
4, 3, // expected start
3, 3, // expected end
false, // regular selection
);
// including neither click nor drag pin cells
try testMouseSelection(
5.0, 3, // click
3.9, 3, // drag
4, 3, // expected start
4, 3, // expected end
false, // regular selection
);
// empty selection (single cell on only left half)
try testMouseSelectionIsNull(
3.1, 3, // click
3.0, 3, // drag
false, // regular selection
);
// empty selection (single cell on only right half)
try testMouseSelectionIsNull(
3.9, 3, // click
3.8, 3, // drag
false, // regular selection
);
// empty selection (between two cells, not crossing threshold)
try testMouseSelectionIsNull(
4.0, 3, // click
3.9, 3, // drag
false, // regular selection
);
// -- Wrapping
// LTR, wrap excluded cells
try testMouseSelection(
9.9, 2, // click
0.0, 4, // drag
0, 3, // expected start
9, 3, // expected end
false, // regular selection
);
// RTL, wrap excluded cells
try testMouseSelection(
0.0, 4, // click
9.9, 2, // drag
9, 3, // expected start
0, 3, // expected end
false, // regular selection
);
}
test "Surface: rectangle selection logic" {
// We disable format to make these easier to
// read by pairing sets of coordinates per line.
// zig fmt: off
// -- LTR
// single column selection
try testMouseSelection(
3.0, 2, // click
3.9, 4, // drag
3, 2, // expected start
3, 4, // expected end
true, //rectangle selection
);
// including click and drag pin columns
try testMouseSelection(
3.0, 2, // click
5.9, 4, // drag
3, 2, // expected start
5, 4, // expected end
true, //rectangle selection
);
// including click pin column but not drag pin column
try testMouseSelection(
3.0, 2, // click
5.0, 4, // drag
3, 2, // expected start
4, 4, // expected end
true, //rectangle selection
);
// including drag pin column but not click pin column
try testMouseSelection(
3.9, 2, // click
5.9, 4, // drag
4, 2, // expected start
5, 4, // expected end
true, //rectangle selection
);
// including neither click nor drag pin columns
try testMouseSelection(
3.9, 2, // click
5.0, 4, // drag
4, 2, // expected start
4, 4, // expected end
true, //rectangle selection
);
// empty selection (single column on only left half)
try testMouseSelectionIsNull(
3.0, 2, // click
3.1, 4, // drag
true, //rectangle selection
);
// empty selection (single column on only right half)
try testMouseSelectionIsNull(
3.8, 2, // click
3.9, 4, // drag
true, //rectangle selection
);
// empty selection (between two columns, not crossing threshold)
try testMouseSelectionIsNull(
3.9, 2, // click
4.0, 4, // drag
true, //rectangle selection
);
// -- RTL
// single column selection
try testMouseSelection(
3.9, 2, // click
3.0, 4, // drag
3, 2, // expected start
3, 4, // expected end
true, //rectangle selection
);
// including click and drag pin columns
try testMouseSelection(
5.9, 2, // click
3.0, 4, // drag
5, 2, // expected start
3, 4, // expected end
true, //rectangle selection
);
// including click pin column but not drag pin column
try testMouseSelection(
5.9, 2, // click
3.9, 4, // drag
5, 2, // expected start
4, 4, // expected end
true, //rectangle selection
);
// including drag pin column but not click pin column
try testMouseSelection(
5.0, 2, // click
3.0, 4, // drag
4, 2, // expected start
3, 4, // expected end
true, //rectangle selection
);
// including neither click nor drag pin columns
try testMouseSelection(
5.0, 2, // click
3.9, 4, // drag
4, 2, // expected start
4, 4, // expected end
true, //rectangle selection
);
// empty selection (single column on only left half)
try testMouseSelectionIsNull(
3.1, 2, // click
3.0, 4, // drag
true, //rectangle selection
);
// empty selection (single column on only right half)
try testMouseSelectionIsNull(
3.9, 2, // click
3.8, 4, // drag
true, //rectangle selection
);
// empty selection (between two columns, not crossing threshold)
try testMouseSelectionIsNull(
4.0, 2, // click
3.9, 4, // drag
true, //rectangle selection
);
// -- Wrapping
// LTR, do not wrap
try testMouseSelection(
9.9, 2, // click
0.0, 4, // drag
9, 2, // expected start
0, 4, // expected end
true, //rectangle selection
);
// RTL, do not wrap
try testMouseSelection(
0.0, 4, // click
9.9, 2, // drag
0, 4, // expected start
9, 2, // expected end
true, //rectangle selection
);
}

View File

@ -1563,7 +1563,7 @@ fn gtkMouseMotion(
const scaled = self.scaledCoordinates(x, y);
const pos: apprt.CursorPos = .{
.x = @floatCast(@max(0, scaled.x)),
.x = @floatCast(scaled.x),
.y = @floatCast(scaled.y),
};

View File

@ -3572,6 +3572,74 @@ pub const Pin = struct {
return result;
}
/// Move the pin left n columns, stopping at the start of the row.
pub fn leftClamp(self: Pin, n: size.CellCountInt) Pin {
var result = self;
result.x -|= n;
return result;
}
/// Move the pin right n columns, stopping at the end of the row.
pub fn rightClamp(self: Pin, n: size.CellCountInt) Pin {
var result = self;
result.x = @min(self.x +| n, self.node.data.size.cols - 1);
return result;
}
/// Move the pin left n cells, wrapping to the previous row as needed.
///
/// If the offset goes beyond the top of the screen, returns null.
///
/// TODO: Unit tests.
pub fn leftWrap(self: Pin, n: usize) ?Pin {
// NOTE: This assumes that all pages have the same width, which may
// be violated under certain circumstances by incomplete reflow.
const cols = self.node.data.size.cols;
const remaining_in_row = self.x;
if (n <= remaining_in_row) return self.left(n);
const extra_after_remaining = n - remaining_in_row;
const rows_off = 1 + extra_after_remaining / cols;
switch (self.upOverflow(rows_off)) {
.offset => |v| {
var result = v;
result.x = @intCast(cols - extra_after_remaining % cols);
return result;
},
.overflow => return null,
}
}
/// Move the pin right n cells, wrapping to the next row as needed.
///
/// If the offset goes beyond the bottom of the screen, returns null.
///
/// TODO: Unit tests.
pub fn rightWrap(self: Pin, n: usize) ?Pin {
// NOTE: This assumes that all pages have the same width, which may
// be violated under certain circumstances by incomplete reflow.
const cols = self.node.data.size.cols;
const remaining_in_row = cols - self.x - 1;
if (n <= remaining_in_row) return self.right(n);
const extra_after_remaining = n - remaining_in_row;
const rows_off = 1 + extra_after_remaining / cols;
switch (self.downOverflow(rows_off)) {
.offset => |v| {
var result = v;
result.x = @intCast(extra_after_remaining % cols - 1);
return result;
},
.overflow => return null,
}
}
/// Move the pin down a certain number of rows, or return null if
/// the pin goes beyond the end of the screen.
pub fn down(self: Pin, n: usize) ?Pin {

View File

@ -3,10 +3,12 @@ const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const size = @import("size.zig");
/// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple
/// things: it is in the current visible viewport? the current active
/// area of the screen where the cursor is? the entire scrollback history?
/// etc. This tag is used to differentiate those cases.
/// The possible reference locations for a point. When someone says "(42, 80)"
/// in the context of a terminal, that could mean multiple things: it is in the
/// current visible viewport? the current active area of the screen where the
/// cursor is? the entire scrollback history? etc.
///
/// This tag is used to differentiate those cases.
pub const Tag = enum {
/// Top-left is part of the active area where a running program can
/// jump the cursor and make changes. The active area is the "editable"