terminal2: resize cols blank row preservation

This commit is contained in:
Mitchell Hashimoto
2024-03-07 20:22:01 -08:00
parent 17cfdc0487
commit 7fd85bd177
3 changed files with 170 additions and 9 deletions

View File

@ -7454,6 +7454,7 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
try testing.expectEqual(@as(usize, 2), s.cursor.y);
}
// X
test "Screen: resize less cols with scrollback keeps cursor row" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -398,7 +398,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
.gt => {
// We grow rows after cols so that we can do our unwrapping/reflow
// before we do a no-reflow grow.
try self.resizeCols(cols);
try self.resizeCols(cols, opts.cursor);
try self.resizeWithoutReflow(opts);
},
@ -411,7 +411,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
break :opts copy;
});
try self.resizeCols(cols);
try self.resizeCols(cols, opts.cursor);
},
}
}
@ -420,12 +420,35 @@ pub fn resize(self: *PageList, opts: Resize) !void {
fn resizeCols(
self: *PageList,
cols: size.CellCountInt,
cursor: ?Resize.Cursor,
) !void {
assert(cols != self.cols);
// Our new capacity, ensure we can fit the cols
const cap = try std_capacity.adjust(.{ .cols = cols });
// If we have a cursor position (x,y), then we try under any col resizing
// to keep the same number remaining active rows beneath it. This is a
// very special case if you can imagine clearing the screen (i.e.
// scrollClear), having an empty active area, and then resizing to less
// cols then we don't want the active area to "jump" to the bottom and
// pull down scrollback.
const preserved_cursor: ?struct {
tracked_pin: *Pin,
remaining_rows: usize,
} = if (cursor) |c| cursor: {
const p = self.pin(.{ .active = .{
.x = c.x,
.y = c.y,
} }) orelse break :cursor null;
break :cursor .{
.tracked_pin = try self.trackPin(p),
.remaining_rows = self.rows - c.y - 1,
};
} else null;
defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin);
// Go page by page and shrink the columns on a per-page basis.
var it = self.pageIterator(.right_down, .{ .screen = .{} }, null);
while (it.next()) |chunk| {
@ -463,6 +486,39 @@ fn resizeCols(
for (total..self.rows) |_| _ = try self.grow();
}
// See preserved_cursor setup for why.
if (preserved_cursor) |c| cursor: {
const active_pt = self.pointFromPin(
.active,
c.tracked_pin.*,
) orelse break :cursor;
// We need to determine how many rows we wrapped from the original
// and subtract that from the remaining rows we expect because if
// we wrap down we don't want to push our original row contents into
// the scrollback.
const wrapped = wrapped: {
var wrapped: usize = 0;
var row_it = c.tracked_pin.rowIterator(.left_up, null);
_ = row_it.next(); // skip ourselves
while (row_it.next()) |next| {
const row = next.rowAndCell().row;
if (!row.wrap) break;
wrapped += 1;
}
break :wrapped wrapped;
};
// If we wrapped more than we expect, do nothing.
if (wrapped >= c.remaining_rows) break :cursor;
const desired = c.remaining_rows - wrapped;
const current = self.rows - (active_pt.active.y + 1);
if (current >= desired) break :cursor;
for (0..desired - current) |_| _ = try self.grow();
}
// Update our cols
self.cols = cols;
}
@ -628,17 +684,48 @@ fn reflowPage(
// row is wrapped then we don't trim trailing empty cells because
// the empty cells can be meaningful.
const trailing_empty = src_cursor.countTrailingEmptyCells();
const cols_len = src_cursor.page.size.cols - trailing_empty;
const cols_len = cols_len: {
var cols_len = src_cursor.page.size.cols - trailing_empty;
if (cols_len > 0) break :cols_len cols_len;
if (cols_len == 0) {
// If the row is empty, we don't copy it. We count it as a
// blank line and continue to the next row.
blank_lines += 1;
continue;
}
// If a tracked pin is in this row then we need to keep it
// even if it is empty, because it is somehow meaningful
// (usually the screen cursor), but we do trim the cells
// down to the desired size.
//
// The reason we do this logic is because if you do a scroll
// clear (i.e. move all active into scrollback and reset
// the screen), the cursor is on the top line again with
// an empty active. If you resize to a smaller col size we
// don't want to "pull down" all the scrollback again. The
// user expects we just shrink the active area.
var it = self.tracked_pins.keyIterator();
while (it.next()) |p_ptr| {
const p = p_ptr.*;
if (&p.page.data != src_cursor.page or
p.y != src_cursor.y) continue;
// If our tracked pin is outside our resized cols, we
// trim it to the last col, we don't want to wrap blanks.
if (p.x >= cap.cols) p.x = cap.cols - 1;
// We increase our col len to at least include this pin
cols_len = @max(cols_len, p.x + 1);
}
if (cols_len == 0) {
// If the row is empty, we don't copy it. We count it as a
// blank line and continue to the next row.
blank_lines += 1;
continue;
}
break :cols_len cols_len;
};
// We have data, if we have blank lines we need to create them first.
for (0..blank_lines) |_| {
// TODO: cursor in here
dst_cursor.cursorScroll();
}
@ -4775,6 +4862,50 @@ test "PageList resize reflow less cols blank lines between" {
}
}
test "PageList resize reflow less cols cursor not on last line preserves location" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 1);
defer s.deinit();
try testing.expect(s.pages.first == s.pages.last);
const page = &s.pages.first.?.data;
for (0..s.rows) |y| {
for (0..2) |x| {
const rac = page.getRowAndCell(x, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x) },
};
}
}
// Grow blank rows to push our rows back into scrollback
try s.growRows(5);
try testing.expectEqual(@as(usize, 10), s.totalRows());
// Put a tracked pin in the history
const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 0 } }).?);
defer s.untrackPin(p);
// Resize
try s.resize(.{
.cols = 4,
.reflow = true,
// Important: not on last row
.cursor = .{ .x = 1, .y = 1 },
});
try testing.expectEqual(@as(usize, 4), s.cols);
try testing.expectEqual(@as(usize, 10), s.totalRows());
// Our cursor should move to the first row
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
.y = 0,
} }, s.pointFromPin(.active, p.*).?);
}
test "PageList resize reflow less cols copy style" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -3698,6 +3698,35 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
}
}
test "Screen: resize less cols with scrollback keeps cursor row" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5);
defer s.deinit();
const str = "1A\n2B\n3C\n4D\n5E";
try s.testWriteString(str);
// Lets do a scroll and clear operation
try s.scrollClear();
// Move our cursor to the beginning
s.cursorAbsolute(0, 0);
try s.resize(3, 3);
{
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
defer alloc.free(contents);
const expected = "";
try testing.expectEqualStrings(expected, contents);
}
// Cursor should be on the last line
try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.x);
try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);
}
test "Screen: resize more rows, less cols with reflow with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;