terminal: clamp selection rows to page width

containedRowCached built full-row and rectangular selections using
the desired screen width. During incomplete reflow, an intermediate
narrower page received endpoints beyond its cell range, and consumers
could panic when resolving the returned pins.

Use the owning page width for full rows and clamp rectangular bounds to
that width. Every contained-row selection now returns resolvable pins.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 20:56:44 -07:00
parent fa8cae88b2
commit a9f5b7eba2

View File

@@ -326,6 +326,7 @@ pub fn containedRowCached(
br: point.Coordinate,
p: point.Coordinate,
) ?Selection {
_ = s;
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
@@ -333,12 +334,12 @@ pub fn containedRowCached(
if (self.rectangle) return init(
start: {
var copy: Pin = pin;
copy.x = tl.x;
copy.x = @min(tl.x, copy.node.cols() - 1);
break :start copy;
},
end: {
var copy: Pin = pin;
copy.x = br.x;
copy.x = @min(br.x, copy.node.cols() - 1);
break :end copy;
},
true,
@@ -355,7 +356,7 @@ pub fn containedRowCached(
tl_pin,
end: {
var copy: Pin = pin;
copy.x = s.pages.cols - 1;
copy.x = copy.node.cols() - 1;
break :end copy;
},
false,
@@ -387,7 +388,7 @@ pub fn containedRowCached(
},
end: {
var copy: Pin = pin;
copy.x = s.pages.cols - 1;
copy.x = copy.node.cols() - 1;
break :end copy;
},
false,
@@ -1602,3 +1603,46 @@ test "Selection: containedRow" {
).?);
}
}
test "Selection: containedRow clamps mixed-width pages" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, .{
.cols = 4,
.rows = 3,
.max_scrollback = 0,
});
defer s.deinit();
const first = s.pages.pages.first.?;
try s.pages.split(.{ .node = first, .y = 2 });
try s.pages.split(.{ .node = first, .y = 1 });
const middle = first.next.?;
const last = middle.next.?;
middle.page().size.cols = 2;
const linear = Selection.init(
.{ .node = first, .x = 1 },
.{ .node = last, .x = 1 },
false,
);
const linear_row = linear.containedRow(
&s,
.{ .node = middle },
).?;
_ = linear_row.end().rowAndCell();
try testing.expect((Pin{ .node = middle }).eql(linear_row.start()));
try testing.expect((Pin{ .node = middle, .x = 1 }).eql(linear_row.end()));
const rectangle = Selection.init(
.{ .node = first, .x = 1 },
.{ .node = last, .x = 3 },
true,
);
const rectangle_row = rectangle.containedRow(
&s,
.{ .node = middle },
).?;
_ = rectangle_row.end().rowAndCell();
try testing.expect((Pin{ .node = middle, .x = 1 }).eql(rectangle_row.start()));
try testing.expect((Pin{ .node = middle, .x = 1 }).eql(rectangle_row.end()));
}