mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
1401 lines
45 KiB
Zig
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 } }).?,
|
|
).?);
|
|
}
|
|
}
|