mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +03:00
terminal/new: first grow cols reflow work, not done
This commit is contained in:
@ -406,8 +406,195 @@ fn resizeGrowCols(self: *PageList, cols: size.CellCountInt) !void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@panic("TODO: wrapped");
|
// Slow path, we have a wrapped row. We need to reflow the text.
|
||||||
|
// This is painful because we basically need to rewrite the entire
|
||||||
|
// page sequentially.
|
||||||
|
try self.reflowPage(cap, chunk.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If our total rows is less than our active rows, we need to grow.
|
||||||
|
// This can happen if you're growing columns such that enough active
|
||||||
|
// rows unwrap that we no longer have enough.
|
||||||
|
var node_it = self.pages.first;
|
||||||
|
var total: usize = 0;
|
||||||
|
while (node_it) |node| : (node_it = node.next) {
|
||||||
|
total += node.data.size.rows;
|
||||||
|
if (total >= self.rows) break;
|
||||||
|
} else {
|
||||||
|
for (total..self.rows) |_| _ = try self.grow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a cursor to track where we are in the src/dst. This is very
|
||||||
|
// similar to Screen.Cursor, so see that for docs on individual fields.
|
||||||
|
// We don't use a Screen because we don't need all the same data and we
|
||||||
|
// do our best to optimize having direct access to the page memory.
|
||||||
|
const ReflowCursor = struct {
|
||||||
|
x: size.CellCountInt,
|
||||||
|
y: size.CellCountInt,
|
||||||
|
pending_wrap: bool,
|
||||||
|
page: *pagepkg.Page,
|
||||||
|
page_row: *pagepkg.Row,
|
||||||
|
page_cell: *pagepkg.Cell,
|
||||||
|
|
||||||
|
fn init(page: *pagepkg.Page) ReflowCursor {
|
||||||
|
const rows = page.rows.ptr(page.memory);
|
||||||
|
return .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.pending_wrap = false,
|
||||||
|
.page = page,
|
||||||
|
.page_row = &rows[0],
|
||||||
|
.page_cell = &rows[0].cells.ptr(page.memory)[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursorForward(self: *ReflowCursor) void {
|
||||||
|
if (self.x == self.page.size.cols - 1) {
|
||||||
|
self.pending_wrap = true;
|
||||||
|
} else {
|
||||||
|
const cell: [*]pagepkg.Cell = @ptrCast(self.page_cell);
|
||||||
|
self.page_cell = @ptrCast(cell + 1);
|
||||||
|
self.x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursorScroll(self: *ReflowCursor) void {
|
||||||
|
// Scrolling requires that we're on the bottom of our page.
|
||||||
|
// We also assert that we have capacity because reflow always
|
||||||
|
// works within the capacity of the page.
|
||||||
|
assert(self.y == self.page.size.rows - 1);
|
||||||
|
assert(self.page.size.rows < self.page.capacity.rows);
|
||||||
|
|
||||||
|
// Increase our page size
|
||||||
|
self.page.size.rows += 1;
|
||||||
|
|
||||||
|
// With the increased page size, safely move down a row.
|
||||||
|
const rows: [*]pagepkg.Row = @ptrCast(self.page_row);
|
||||||
|
const row: *pagepkg.Row = @ptrCast(rows + 1);
|
||||||
|
self.page_row = row;
|
||||||
|
self.page_cell = &row.cells.ptr(self.page.memory)[0];
|
||||||
|
self.pending_wrap = false;
|
||||||
|
self.x = 0;
|
||||||
|
self.y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursorAbsolute(
|
||||||
|
self: *ReflowCursor,
|
||||||
|
x: size.CellCountInt,
|
||||||
|
y: size.CellCountInt,
|
||||||
|
) void {
|
||||||
|
assert(x < self.page.size.cols);
|
||||||
|
assert(y < self.page.size.rows);
|
||||||
|
|
||||||
|
const rows: [*]pagepkg.Row = @ptrCast(self.page_row);
|
||||||
|
const row: *pagepkg.Row = switch (std.math.order(y, self.y)) {
|
||||||
|
.eq => self.page_row,
|
||||||
|
.lt => @ptrCast(rows - (self.y - y)),
|
||||||
|
.gt => @ptrCast(rows + (y - self.y)),
|
||||||
|
};
|
||||||
|
self.page_row = row;
|
||||||
|
self.page_cell = &row.cells.ptr(self.page.memory)[x];
|
||||||
|
self.pending_wrap = false;
|
||||||
|
self.x = x;
|
||||||
|
self.y = y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reflow the given page into the new capacity. The new capacity can have
|
||||||
|
/// any number of columns and rows. This will create as many pages as
|
||||||
|
/// necessary to fit the reflowed text and will remove the old page.
|
||||||
|
///
|
||||||
|
/// Note a couple edge cases:
|
||||||
|
///
|
||||||
|
/// 1. If the first set of rows of this page are a wrap continuation, then
|
||||||
|
/// we will reflow the continuation rows but will not traverse back to
|
||||||
|
/// find the initial wrap.
|
||||||
|
///
|
||||||
|
/// 2. If the last row is wrapped then we will traverse forward to reflow
|
||||||
|
/// all the continuation rows.
|
||||||
|
///
|
||||||
|
/// As a result of the above edge cases, the pagelist may end up removing
|
||||||
|
/// an indefinite number of pages. In the most pathological cases (the screen
|
||||||
|
/// is one giant wrapped line), this can be a very expensive operation. That
|
||||||
|
/// doesn't really happen in typical terminal usage so its not a case we
|
||||||
|
/// optimize for today. Contributions welcome to optimize this.
|
||||||
|
fn reflowPage(
|
||||||
|
self: *PageList,
|
||||||
|
cap: Capacity,
|
||||||
|
node: *List.Node,
|
||||||
|
) !void {
|
||||||
|
assert(cap.cols > self.cols);
|
||||||
|
|
||||||
|
// The cursor tracks where we are in the source page.
|
||||||
|
var src_cursor = ReflowCursor.init(&node.data);
|
||||||
|
|
||||||
|
// Our new capacity when growing columns may also shrink rows. So we
|
||||||
|
// need to do a loop in order to potentially make multiple pages.
|
||||||
|
while (true) {
|
||||||
|
// Create our new page and our cursor restarts at 0,0 in the new page.
|
||||||
|
// The new page always starts with a size of 1 because we know we have
|
||||||
|
// at least one row to copy from the src.
|
||||||
|
const dst_node = try self.createPage(cap);
|
||||||
|
dst_node.data.size.rows = 1;
|
||||||
|
var dst_cursor = ReflowCursor.init(&dst_node.data);
|
||||||
|
|
||||||
|
// Our new page goes before our src node. This will append it to any
|
||||||
|
// previous pages we've created.
|
||||||
|
self.pages.insertBefore(node, dst_node);
|
||||||
|
|
||||||
|
// Continue traversing the source until we're out of space in our
|
||||||
|
// destination or we've copied all our intended rows.
|
||||||
|
for (src_cursor.y..src_cursor.page.size.rows) |src_y| {
|
||||||
|
if (src_y > 0) {
|
||||||
|
// We're done with this row, if this row isn't wrapped, we can
|
||||||
|
// move our destination cursor to the next row.
|
||||||
|
if (!src_cursor.page_row.wrap) {
|
||||||
|
dst_cursor.cursorScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
src_cursor.cursorAbsolute(src_cursor.x, @intCast(src_y));
|
||||||
|
|
||||||
|
for (src_cursor.x..src_cursor.page.size.cols) |src_x| {
|
||||||
|
assert(src_cursor.x == src_x);
|
||||||
|
|
||||||
|
if (dst_cursor.pending_wrap) {
|
||||||
|
@panic("TODO");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (src_cursor.page_cell.content_tag) {
|
||||||
|
// These are guaranteed to have no styling data and no
|
||||||
|
// graphemes, a fast path.
|
||||||
|
.bg_color_palette,
|
||||||
|
.bg_color_rgb,
|
||||||
|
=> {
|
||||||
|
assert(!src_cursor.page_cell.hasStyling());
|
||||||
|
assert(!src_cursor.page_cell.hasGrapheme());
|
||||||
|
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
||||||
|
},
|
||||||
|
|
||||||
|
.codepoint => {
|
||||||
|
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
||||||
|
// TODO: style copy
|
||||||
|
},
|
||||||
|
|
||||||
|
else => @panic("TODO"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move both our cursors forward
|
||||||
|
src_cursor.cursorForward();
|
||||||
|
dst_cursor.cursorForward();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We made it through all our source rows, we're done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, remove the old page.
|
||||||
|
self.pages.remove(node);
|
||||||
|
self.destroyPage(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
||||||
@ -2325,3 +2512,56 @@ test "PageList resize reflow more cols no wrapped rows" {
|
|||||||
try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint);
|
try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList resize reflow more cols wrapped rows" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 2, 4, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
try testing.expect(s.pages.first == s.pages.last);
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
for (0..s.rows) |y| {
|
||||||
|
if (y % 2 == 0) {
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.row.wrap = true;
|
||||||
|
} else {
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.row.wrap_continuation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..s.cols) |x| {
|
||||||
|
const rac = page.getRowAndCell(x, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = 'A' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
try s.resize(.{ .cols = 4, .reflow = true });
|
||||||
|
try testing.expectEqual(@as(usize, 4), s.cols);
|
||||||
|
try testing.expectEqual(@as(usize, 4), s.totalRows());
|
||||||
|
|
||||||
|
// Active should still be on top
|
||||||
|
{
|
||||||
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
} }, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var it = s.rowIterator(.{ .screen = .{} }, null);
|
||||||
|
{
|
||||||
|
// First row should be unwrapped
|
||||||
|
const offset = it.next().?;
|
||||||
|
const rac = offset.rowAndCell(0);
|
||||||
|
const cells = offset.page.data.getCells(rac.row);
|
||||||
|
try testing.expect(!rac.row.wrap);
|
||||||
|
try testing.expectEqual(@as(usize, 4), cells.len);
|
||||||
|
try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint);
|
||||||
|
try testing.expectEqual(@as(u21, 'A'), cells[2].content.codepoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -767,6 +767,10 @@ pub const Cell = packed struct(u64) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hasStyling(self: Cell) bool {
|
||||||
|
return self.style_id != style.default_id;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if the cell has no text or styling.
|
/// Returns true if the cell has no text or styling.
|
||||||
pub fn isEmpty(self: Cell) bool {
|
pub fn isEmpty(self: Cell) bool {
|
||||||
return switch (self.content_tag) {
|
return switch (self.content_tag) {
|
||||||
|
Reference in New Issue
Block a user