ghostty/src/terminal/Selection.zig
2024-04-26 20:52:08 -07:00

1401 lines
45 KiB
Zig

//! Represents a single selection within the terminal (i.e. a highlight region).
const Selection = @This();
const std = @import("std");
const assert = std.debug.assert;
const page = @import("page.zig");
const point = @import("point.zig");
const PageList = @import("PageList.zig");
const Screen = @import("Screen.zig");
const Pin = PageList.Pin;
// NOTE(mitchellh): I'm not very happy with how this is implemented, because
// the ordering operations which are used frequently require using
// pointFromPin which -- at the time of writing this -- is slow. The overall
// style of this struct is due to porting it from the previous implementation
// which had an efficient ordering operation.
//
// While reimplementing this, there were too many callers that already
// depended on this behavior so I kept it despite the inefficiency. In the
// future, we should take a look at this again!
/// The bounds of the selection.
bounds: Bounds,
/// Whether or not this selection refers to a rectangle, rather than whole
/// lines of a buffer. In this mode, start and end refer to the top left and
/// bottom right of the rectangle, or vice versa if the selection is backwards.
rectangle: bool = false,
/// The bounds of the selection. A selection bounds can be either tracked
/// or untracked. Untracked bounds are unsafe beyond the point the terminal
/// screen may be modified, since they may point to invalid memory. Tracked
/// bounds are always valid and will be updated as the screen changes, but
/// are more expensive to exist.
///
/// In all cases, start and end can be in any order. There is no guarantee that
/// start is before end or vice versa. If a user selects backwards,
/// start will be after end, and vice versa. Use the struct functions
/// to not have to worry about this.
pub const Bounds = union(enum) {
untracked: struct {
start: Pin,
end: Pin,
},
tracked: struct {
start: *Pin,
end: *Pin,
},
};
/// Initialize a new selection with the given start and end pins on
/// the screen. The screen will be used for pin tracking.
pub fn init(
start_pin: Pin,
end_pin: Pin,
rect: bool,
) Selection {
return .{
.bounds = .{ .untracked = .{
.start = start_pin,
.end = end_pin,
} },
.rectangle = rect,
};
}
pub fn deinit(
self: Selection,
s: *Screen,
) void {
switch (self.bounds) {
.tracked => |v| {
s.pages.untrackPin(v.start);
s.pages.untrackPin(v.end);
},
.untracked => {},
}
}
/// Returns true if this selection is equal to another selection.
pub fn eql(self: Selection, other: Selection) bool {
return self.start().eql(other.start()) and
self.end().eql(other.end()) and
self.rectangle == other.rectangle;
}
/// The starting pin of the selection. This is NOT ordered.
pub fn startPtr(self: *Selection) *Pin {
return switch (self.bounds) {
.untracked => |*v| &v.start,
.tracked => |v| v.start,
};
}
/// The ending pin of the selection. This is NOT ordered.
pub fn endPtr(self: *Selection) *Pin {
return switch (self.bounds) {
.untracked => |*v| &v.end,
.tracked => |v| v.end,
};
}
pub fn start(self: Selection) Pin {
return switch (self.bounds) {
.untracked => |v| v.start,
.tracked => |v| v.start.*,
};
}
pub fn end(self: Selection) Pin {
return switch (self.bounds) {
.untracked => |v| v.end,
.tracked => |v| v.end.*,
};
}
/// Returns true if this is a tracked selection.
pub fn tracked(self: *const Selection) bool {
return switch (self.bounds) {
.untracked => false,
.tracked => true,
};
}
/// Convert this selection a tracked selection. It is asserted this is
/// an untracked selection. The tracked selection is returned.
pub fn track(self: *const Selection, s: *Screen) !Selection {
assert(!self.tracked());
// Track our pins
const start_pin = self.bounds.untracked.start;
const end_pin = self.bounds.untracked.end;
const tracked_start = try s.pages.trackPin(start_pin);
errdefer s.pages.untrackPin(tracked_start);
const tracked_end = try s.pages.trackPin(end_pin);
errdefer s.pages.untrackPin(tracked_end);
return .{
.bounds = .{ .tracked = .{
.start = tracked_start,
.end = tracked_end,
} },
.rectangle = self.rectangle,
};
}
/// Returns the top left point of the selection.
pub fn topLeft(self: Selection, s: *const Screen) Pin {
return switch (self.order(s)) {
.forward => self.start(),
.reverse => self.end(),
.mirrored_forward => pin: {
var p = self.start();
p.x = self.end().x;
break :pin p;
},
.mirrored_reverse => pin: {
var p = self.end();
p.x = self.start().x;
break :pin p;
},
};
}
/// Returns the bottom right point of the selection.
pub fn bottomRight(self: Selection, s: *const Screen) Pin {
return switch (self.order(s)) {
.forward => self.end(),
.reverse => self.start(),
.mirrored_forward => pin: {
var p = self.end();
p.x = self.start().x;
break :pin p;
},
.mirrored_reverse => pin: {
var p = self.start();
p.x = self.end().x;
break :pin p;
},
};
}
/// The order of the selection:
///
/// * forward: start(x, y) is before end(x, y) (top-left to bottom-right).
/// * reverse: end(x, y) is before start(x, y) (bottom-right to top-left).
/// * mirrored_[forward|reverse]: special, rectangle selections only (see below).
///
/// For regular selections, the above also holds for top-right to bottom-left
/// (forward) and bottom-left to top-right (reverse). However, for rectangle
/// selections, both of these selections are *mirrored* as orientation
/// operations only flip the x or y axis, not both. Depending on the y axis
/// direction, this is either mirrored_forward or mirrored_reverse.
///
pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse };
pub fn order(self: Selection, s: *const Screen) Order {
const start_pt = s.pages.pointFromPin(.screen, self.start()).?.screen;
const end_pt = s.pages.pointFromPin(.screen, self.end()).?.screen;
if (self.rectangle) {
// Reverse (also handles single-column)
if (start_pt.y > end_pt.y and start_pt.x >= end_pt.x) return .reverse;
if (start_pt.y >= end_pt.y and start_pt.x > end_pt.x) return .reverse;
// Mirror, bottom-left to top-right
if (start_pt.y > end_pt.y and start_pt.x < end_pt.x) return .mirrored_reverse;
// Mirror, top-right to bottom-left
if (start_pt.y < end_pt.y and start_pt.x > end_pt.x) return .mirrored_forward;
// Forward
return .forward;
}
if (start_pt.y < end_pt.y) return .forward;
if (start_pt.y > end_pt.y) return .reverse;
if (start_pt.x <= end_pt.x) return .forward;
return .reverse;
}
/// Returns the selection in the given order.
///
/// The returned selection is always a new untracked selection.
///
/// Note that only forward and reverse are useful desired orders for this
/// function. All other orders act as if forward order was desired.
pub fn ordered(self: Selection, s: *const Screen, desired: Order) Selection {
if (self.order(s) == desired) return Selection.init(
self.start(),
self.end(),
self.rectangle,
);
const tl = self.topLeft(s);
const br = self.bottomRight(s);
return switch (desired) {
.forward => Selection.init(tl, br, self.rectangle),
.reverse => Selection.init(br, tl, self.rectangle),
else => Selection.init(tl, br, self.rectangle),
};
}
/// Returns true if the selection contains the given point.
///
/// This recalculates top left and bottom right each call. If you have
/// many points to check, it is cheaper to do the containment logic
/// yourself and cache the topleft/bottomright.
pub fn contains(self: Selection, s: *const Screen, pin: Pin) bool {
const tl_pin = self.topLeft(s);
const br_pin = self.bottomRight(s);
// This is definitely not very efficient. Low-hanging fruit to
// improve this.
const tl = s.pages.pointFromPin(.screen, tl_pin).?.screen;
const br = s.pages.pointFromPin(.screen, br_pin).?.screen;
const p = s.pages.pointFromPin(.screen, pin).?.screen;
// If we're in rectangle select, we can short-circuit with an easy check
// here
if (self.rectangle)
return p.y >= tl.y and p.y <= br.y and p.x >= tl.x and p.x <= br.x;
// If tl/br are same line
if (tl.y == br.y) return p.y == tl.y and
p.x >= tl.x and
p.x <= br.x;
// If on top line, just has to be left of X
if (p.y == tl.y) return p.x >= tl.x;
// If on bottom line, just has to be right of X
if (p.y == br.y) return p.x <= br.x;
// If between the top/bottom, always good.
return p.y > tl.y and p.y < br.y;
}
/// Get a selection for a single row in the screen. This will return null
/// if the row is not included in the selection.
pub fn containedRow(self: Selection, s: *const Screen, pin: Pin) ?Selection {
const tl_pin = self.topLeft(s);
const br_pin = self.bottomRight(s);
// This is definitely not very efficient. Low-hanging fruit to
// improve this.
const tl = s.pages.pointFromPin(.screen, tl_pin).?.screen;
const br = s.pages.pointFromPin(.screen, br_pin).?.screen;
const p = s.pages.pointFromPin(.screen, pin).?.screen;
if (p.y < tl.y or p.y > br.y) return null;
// Rectangle case: we can return early as the x range will always be the
// same. We've already validated that the row is in the selection.
if (self.rectangle) return init(
s.pages.pin(.{ .screen = .{ .y = p.y, .x = tl.x } }).?,
s.pages.pin(.{ .screen = .{ .y = p.y, .x = br.x } }).?,
true,
);
if (p.y == tl.y) {
// If the selection is JUST this line, return it as-is.
if (p.y == br.y) {
return init(tl_pin, br_pin, false);
}
// Selection top-left line matches only.
return init(
tl_pin,
s.pages.pin(.{ .screen = .{ .y = p.y, .x = s.pages.cols - 1 } }).?,
false,
);
}
// Row is our bottom selection, so we return the selection from the
// beginning of the line to the br. We know our selection is more than
// one line (due to conditionals above)
if (p.y == br.y) {
assert(p.y != tl.y);
return init(
s.pages.pin(.{ .screen = .{ .y = p.y, .x = 0 } }).?,
br_pin,
false,
);
}
// Row is somewhere between our selection lines so we return the full line.
return init(
s.pages.pin(.{ .screen = .{ .y = p.y, .x = 0 } }).?,
s.pages.pin(.{ .screen = .{ .y = p.y, .x = s.pages.cols - 1 } }).?,
false,
);
}
/// Possible adjustments to the selection.
pub const Adjustment = enum {
left,
right,
up,
down,
home,
end,
page_up,
page_down,
};
/// Adjust the selection by some given adjustment. An adjustment allows
/// a selection to be expanded slightly left, right, up, down, etc.
pub fn adjust(
self: *Selection,
s: *const Screen,
adjustment: Adjustment,
) void {
// Note that we always adjusts "end" because end always represents
// the last point of the selection by mouse, not necessarilly the
// top/bottom visually. So this results in the right behavior
// whether the user drags up or down.
const end_pin = self.endPtr();
switch (adjustment) {
.up => if (end_pin.up(1)) |new_end| {
end_pin.* = new_end;
} else {
end_pin.x = 0;
},
.down => {
// Find the next non-blank row
var current = end_pin.*;
while (current.down(1)) |next| : (current = next) {
const rac = next.rowAndCell();
const cells = next.page.data.getCells(rac.row);
if (page.Cell.hasTextAny(cells)) {
end_pin.* = next;
break;
}
} else {
// If we're at the bottom, just go to the end of the line
end_pin.x = end_pin.page.data.size.cols - 1;
}
},
.left => {
var it = end_pin.cellIterator(.left_up, null);
_ = it.next();
while (it.next()) |next| {
const rac = next.rowAndCell();
if (rac.cell.hasText()) {
end_pin.* = next;
break;
}
}
},
.right => {
// Step right, wrapping to the next row down at the start of each new line,
// until we find a non-empty cell.
var it = end_pin.cellIterator(.right_down, null);
_ = it.next();
while (it.next()) |next| {
const rac = next.rowAndCell();
if (rac.cell.hasText()) {
end_pin.* = next;
break;
}
}
},
.page_up => if (end_pin.up(s.pages.rows)) |new_end| {
end_pin.* = new_end;
} else {
self.adjust(s, .home);
},
.page_down => if (end_pin.down(s.pages.rows)) |new_end| {
end_pin.* = new_end;
} else {
self.adjust(s, .end);
},
.home => end_pin.* = s.pages.pin(.{ .screen = .{
.x = 0,
.y = 0,
} }).?,
.end => {
var it = s.pages.rowIterator(
.left_up,
.{ .screen = .{} },
null,
);
while (it.next()) |next| {
const rac = next.rowAndCell();
const cells = next.page.data.getCells(rac.row);
if (page.Cell.hasTextAny(cells)) {
end_pin.* = next;
end_pin.x = @intCast(cells.len - 1);
break;
}
}
},
}
}
test "Selection: adjust right" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A1234\nB5678\nC1234\nD5678");
// Simple movement right
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 3,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// Already at end of the line.
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 4, .y = 2 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 3,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// Already at end of the screen
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 4, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 3,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust left" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A1234\nB5678\nC1234\nD5678");
// Simple movement left
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 2,
.y = 3,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// Already at beginning of the line.
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 2,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust left skips blanks" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A1234\nB5678\nC12\nD56");
// Same line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 4, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 2,
.y = 3,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// Edge
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 2,
.y = 2,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust up" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A\nB\nC\nD\nE");
// Not on the first line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .up);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 2,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// On the first line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .up);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust down" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A\nB\nC\nD\nE");
// Not on the first line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 4,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// On the last line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 4 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 4,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust down with not full screen" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A\nB\nC");
// On the last line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 2,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust home" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A\nB\nC");
// On the last line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .home);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 1,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust end with not full screen" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A\nB\nC");
// On the last line
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 4, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .end);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 2,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: order, standard" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 100, 100, 1);
defer s.deinit();
{
// forward, multi-line
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, multi-line
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, same-line
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// forward, single char
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single line
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
}
test "Selection: order, rectangle" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 100, 100, 1);
defer s.deinit();
// Conventions:
// TL - top left
// BL - bottom left
// TR - top right
// BR - bottom right
{
// forward (TL -> BR)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse (BR -> TL)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// mirrored_forward (TR -> BL)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .mirrored_forward);
}
{
// mirrored_reverse (BL -> TR)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .mirrored_reverse);
}
{
// forward, single line (left -> right )
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single line (right -> left)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, single column (top -> bottom)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single column (bottom -> top)
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, single cell
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
}
test "topLeft" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
{
// forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
const tl = sel.topLeft(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
.y = 1,
} }, s.pages.pointFromPin(.screen, tl));
}
{
// reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
const tl = sel.topLeft(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
.y = 1,
} }, s.pages.pointFromPin(.screen, tl));
}
{
// mirrored_forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
const tl = sel.topLeft(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
.y = 1,
} }, s.pages.pointFromPin(.screen, tl));
}
{
// mirrored_reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
const tl = sel.topLeft(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
.y = 1,
} }, s.pages.pointFromPin(.screen, tl));
}
}
test "bottomRight" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
{
// forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
const br = sel.bottomRight(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 1,
} }, s.pages.pointFromPin(.screen, br));
}
{
// reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
const br = sel.bottomRight(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 1,
} }, s.pages.pointFromPin(.screen, br));
}
{
// mirrored_forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
const br = sel.bottomRight(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 3,
} }, s.pages.pointFromPin(.screen, br));
}
{
// mirrored_reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
const br = sel.bottomRight(&s);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 3,
} }, s.pages.pointFromPin(.screen, br));
}
}
test "ordered" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
{
// forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
false,
);
const sel_reverse = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
try testing.expect(sel.ordered(&s, .forward).eql(sel));
try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel));
}
{
// reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
const sel_forward = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
false,
);
try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
try testing.expect(sel.ordered(&s, .reverse).eql(sel));
try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel_forward));
}
{
// mirrored_forward
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
true,
);
const sel_forward = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
true,
);
const sel_reverse = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
try testing.expect(sel.ordered(&s, .mirrored_reverse).eql(sel_forward));
}
{
// mirrored_reverse
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
const sel_forward = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
true,
);
const sel_reverse = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel_forward));
}
}
test "Selection: contains" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
false,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?));
}
// Reverse
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
false,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?));
}
// Single line
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 10, .y = 1 } }).?,
false,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 12, .y = 1 } }).?));
}
}
test "Selection: contains, rectangle" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 15, 15, 0);
defer s.deinit();
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 7, .y = 9 } }).?,
true,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 6 } }).?)); // Center
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 3, .y = 6 } }).?)); // Left border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 7, .y = 6 } }).?)); // Right border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 3 } }).?)); // Top border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 9 } }).?)); // Bottom border
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?)); // Above center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 10 } }).?)); // Below center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?)); // Left center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 6 } }).?)); // Right center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 3 } }).?)); // Just right of top right
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 9 } }).?)); // Just left of bottom left
}
// Reverse
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 7, .y = 9 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
true,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 6 } }).?)); // Center
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 3, .y = 6 } }).?)); // Left border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 7, .y = 6 } }).?)); // Right border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 3 } }).?)); // Top border
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 9 } }).?)); // Bottom border
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?)); // Above center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 10 } }).?)); // Below center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?)); // Left center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 6 } }).?)); // Right center
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 3 } }).?)); // Just right of top right
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 9 } }).?)); // Just left of bottom left
}
// Single line
// NOTE: This is the same as normal selection but we just do it for brevity
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 10, .y = 1 } }).?,
true,
);
try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?));
try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 12, .y = 1 } }).?));
}
}
test "Selection: containedRow" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 10, 5, 0);
defer s.deinit();
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
false,
);
// Not contained
try testing.expect(sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 4 } }).?,
) == null);
// Start line
try testing.expectEqual(Selection.init(
sel.start(),
s.pages.pin(.{ .screen = .{ .x = s.pages.cols - 1, .y = 1 } }).?,
false,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
).?);
// End line
try testing.expectEqual(Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
sel.end(),
false,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
).?);
// Middle line
try testing.expectEqual(Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = s.pages.cols - 1, .y = 2 } }).?,
false,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
).?);
}
// Rectangle
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 3 } }).?,
true,
);
// Not contained
try testing.expect(sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 4 } }).?,
) == null);
// Start line
try testing.expectEqual(Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?,
true,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
).?);
// End line
try testing.expectEqual(Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 3 } }).?,
true,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
).?);
// Middle line
try testing.expectEqual(Selection.init(
s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 2 } }).?,
true,
), sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
).?);
}
// Single-line selection
{
const sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?,
false,
);
// Not contained
try testing.expect(sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
) == null);
try testing.expect(sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?,
) == null);
// Contained
try testing.expectEqual(sel, sel.containedRow(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
).?);
}
}