From 6917bfa1598cd89cac7fcaa0806d51b005cb6af5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Mar 2024 13:42:45 -0800 Subject: [PATCH] terminal2: screen uses pins --- src/terminal2/PageList.zig | 313 ++++++++++++++++--------------------- src/terminal2/Screen.zig | 130 +++++++-------- src/terminal2/Terminal.zig | 42 ++--- 3 files changed, 226 insertions(+), 259 deletions(-) diff --git a/src/terminal2/PageList.zig b/src/terminal2/PageList.zig index 7ff536798..adf792c9a 100644 --- a/src/terminal2/PageList.zig +++ b/src/terminal2/PageList.zig @@ -373,20 +373,13 @@ pub const Resize = struct { /// be truncated if the new size is smaller than the old size. reflow: bool = true, - /// Set this to a cursor position and the resize will retain the - /// cursor position and update this so that the cursor remains over - /// the same original cell in the reflowed environment. - cursor: ?*Cursor = null, + /// Set this to the current cursor position in the active area. Some + /// resize/reflow behavior depends on the cursor position. + cursor: ?Cursor = null, pub const Cursor = struct { x: size.CellCountInt, y: size.CellCountInt, - - /// The row offset of the cursor. This is assumed to be correct - /// if set. If this is not set, then the row offset will be - /// calculated from the x/y. Calculating the row offset is expensive - /// so if you have it, you should set it. - offset: ?RowOffset = null, }; }; @@ -405,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, opts.cursor); + try self.resizeCols(cols); try self.resizeWithoutReflow(opts); }, @@ -418,7 +411,7 @@ pub fn resize(self: *PageList, opts: Resize) !void { break :opts copy; }); - try self.resizeCols(cols, opts.cursor); + try self.resizeCols(cols); }, } } @@ -427,27 +420,12 @@ 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 are given a cursor, we need to calculate the row offset. - if (cursor) |c| { - if (c.offset == null) { - const tl = self.getTopLeft(.active); - c.offset = tl.forward(c.y) orelse fail: { - // This should never happen, but its not critical enough to - // set an assertion and fail the program. The caller should ALWAYS - // input a valid x/y.. - log.err("cursor offset not found, resize will set wrong cursor", .{}); - break :fail null; - }; - } - } - // Go page by page and shrink the columns on a per-page basis. var it = self.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { @@ -462,7 +440,7 @@ fn resizeCols( if (row.wrap) break :wrapped true; } else false; if (!wrapped) { - try self.resizeWithoutReflowGrowCols(cap, chunk, cursor); + try self.resizeWithoutReflowGrowCols(cap, chunk); continue; } } @@ -470,7 +448,7 @@ fn resizeCols( // Note: we can do a fast-path here if all of our rows in this // page already fit within the new capacity. In that case we can // do a non-reflow resize. - try self.reflowPage(cap, chunk.page, cursor); + try self.reflowPage(cap, chunk.page); } // If our total rows is less than our active rows, we need to grow. @@ -485,50 +463,6 @@ fn resizeCols( for (total..self.rows) |_| _ = try self.grow(); } - // If we have a cursor, we need to update the correct y value. I'm - // not at all happy about this, I wish we could do this in a more - // efficient way as we resize the pages. But at the time of typing this - // I can't think of a way and I'd rather get things working. Someone please - // help! - // - // The challenge is that as rows are unwrapped, we want to preserve the - // cursor. So for examle if you have "A\nB" where AB is soft-wrapped and - // the cursor is on 'B' (x=0, y=1) and you grow the columns, we want - // the cursor to remain on B (x=1, y=0) as it grows. - // - // The easy thing to do would be to count how many rows we unwrapped - // and then subtract that from the original y. That's how I started. The - // challenge is that if we unwrap with scrollback, our scrollback is - // "pulled down" so that the original (x=0,y=0) line is now pushed down. - // Detecting this while resizing seems non-obvious. This is a tested case - // so if you change this logic, you should see failures or passes if it - // works. - // - // The approach I take instead is if we have a cursor offset, I work - // backwards to find the offset we marked while reflowing and update - // the y from that. This is _not terrible_ because active areas are - // generally small and this is a more or less linear search. Its just - // kind of clunky. - if (cursor) |c| cursor: { - const offset = c.offset orelse break :cursor; - var active_it = self.rowIterator(.{ .active = .{} }, null); - var y: size.CellCountInt = 0; - while (active_it.next()) |it_offset| { - if (it_offset.page == offset.page and - it_offset.row_offset == offset.row_offset) - { - c.y = y; - break :cursor; - } - - y += 1; - } else { - // Cursor moved off the screen into the scrollback. - c.x = 0; - c.y = 0; - } - } - // Update our cols self.cols = cols; } @@ -659,7 +593,6 @@ fn reflowPage( self: *PageList, cap: Capacity, node: *List.Node, - cursor: ?*Resize.Cursor, ) !void { // The cursor tracks where we are in the source page. var src_cursor = ReflowCursor.init(&node.data); @@ -742,7 +675,7 @@ fn reflowPage( src_cursor.page_cell.wide == .wide and dst_cursor.x == cap.cols - 1) { - self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); + self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node); dst_cursor.page_cell.* = .{ .content_tag = .codepoint, @@ -758,7 +691,7 @@ fn reflowPage( src_cursor.page_cell.wide == .spacer_head and dst_cursor.x != cap.cols - 1) { - self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); + self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node); src_cursor.cursorForward(); continue; } @@ -846,7 +779,7 @@ fn reflowPage( // If our original cursor was on this page, this x/y then // we need to update to the new location. - self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); + self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node); // Move both our cursors forward src_cursor.cursorForward(); @@ -870,22 +803,6 @@ fn reflowPage( p.page = dst_node; p.y = dst_cursor.y; } - - // If we have no cursor, nothing to update. - const c = cursor orelse break :cursor; - const offset = c.offset orelse break :cursor; - - // If our cursor is on this page, and our x is greater than - // our end, we update to the edge. - if (&offset.page.data == src_cursor.page and - offset.row_offset == src_cursor.y and - c.x >= cols_len) - { - c.offset = .{ - .page = dst_node, - .row_offset = dst_cursor.y, - }; - } } } else { // We made it through all our source rows, we're done. @@ -903,7 +820,6 @@ fn reflowPage( /// x/y (see resizeCols). fn reflowUpdateCursor( self: *const PageList, - cursor: ?*Resize.Cursor, src_cursor: *const ReflowCursor, dst_cursor: *const ReflowCursor, dst_node: *List.Node, @@ -920,42 +836,6 @@ fn reflowUpdateCursor( p.x = dst_cursor.x; p.y = dst_cursor.y; } - - const c = cursor orelse return; - - // If our original cursor was on this page, this x/y then - // we need to update to the new location. - const offset = c.offset orelse return; - if (&offset.page.data != src_cursor.page or - offset.row_offset != src_cursor.y or - c.x != src_cursor.x) return; - - // std.log.warn("c.x={} c.y={} dst_x={} dst_y={} src_y={}", .{ - // c.x, - // c.y, - // dst_cursor.x, - // dst_cursor.y, - // src_cursor.y, - // }); - - // Column always matches our dst x - c.x = dst_cursor.x; - - // Our y is more complicated. The cursor y is the active - // area y, not the row offset. Our cursors are row offsets. - // Instead of calculating the active area coord, we can - // better calculate the CHANGE in coordinate by subtracting - // our dst from src which will calculate how many rows - // we unwrapped to get here. - // - // Note this doesn't handle when we pull down scrollback. - // See the cursor updates in resizeGrowCols for that. - //c.y -|= src_cursor.y - dst_cursor.y; - - c.offset = .{ - .page = dst_node, - .row_offset = dst_cursor.y, - }; } fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { @@ -975,15 +855,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // behavior because it seemed fine in an ocean of differing behavior // between terminal apps. I'm completely open to changing it as long // as resize behavior isn't regressed in a user-hostile way. - const trimmed = self.trimTrailingBlankRows(self.rows - rows); - - // If we have a cursor, we want to preserve the y value as - // best we can. We need to subtract the number of rows that - // moved into the scrollback. - if (opts.cursor) |cursor| { - const scrollback = self.rows - rows - trimmed; - cursor.y -|= scrollback; - } + _ = self.trimTrailingBlankRows(self.rows - rows); // If we didn't trim enough, just modify our row count and this // will create additional history. @@ -1025,12 +897,6 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { for (count..rows) |_| _ = try self.grow(); } - // Update our cursor. W - if (opts.cursor) |cursor| { - const grow_len: size.CellCountInt = @intCast(rows -| count); - cursor.y += rows - self.rows - grow_len; - } - self.rows = rows; }, } @@ -1057,9 +923,12 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { page.size.cols = cols; } - if (opts.cursor) |cursor| { - // If our cursor is off the edge we trimmed, update to edge - if (cursor.x >= cols) cursor.x = cols - 1; + // Update all our tracked pins. If they have an X + // beyond the edge, clamp it. + var pin_it = self.tracked_pins.keyIterator(); + while (pin_it.next()) |p_ptr| { + const p = p_ptr.*; + if (p.x >= cols) p.x = cols - 1; } self.cols = cols; @@ -1073,7 +942,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { var it = self.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { - try self.resizeWithoutReflowGrowCols(cap, chunk, opts.cursor); + try self.resizeWithoutReflowGrowCols(cap, chunk); } self.cols = cols; @@ -1086,7 +955,6 @@ fn resizeWithoutReflowGrowCols( self: *PageList, cap: Capacity, chunk: PageIterator.Chunk, - cursor: ?*Resize.Cursor, ) !void { assert(cap.cols > self.cols); const page = &chunk.page.data; @@ -1138,19 +1006,15 @@ fn resizeWithoutReflowGrowCols( // Insert our new page self.pages.insertBefore(chunk.page, new_page); - // If we have a cursor, we need to update the row offset if it - // matches what we just copied. - if (cursor) |c| cursor: { - const offset = c.offset orelse break :cursor; - if (offset.page == chunk.page and - offset.row_offset >= y_start and - offset.row_offset < y_end) - { - c.offset = .{ - .page = new_page, - .row_offset = offset.row_offset - y_start, - }; - } + // Update our tracked pins that pointed to this previous page. + var pin_it = self.tracked_pins.keyIterator(); + while (pin_it.next()) |p_ptr| { + const p = p_ptr.*; + if (p.page != chunk.page or + p.y < y_start or + p.y >= y_end) continue; + p.page = new_page; + p.y -= y_start; } } assert(copied == page.size.rows); @@ -1921,6 +1785,14 @@ pub const Pin = struct { y: usize = 0, x: usize = 0, + pub fn rowAndCell(self: Pin) struct { + row: *pagepkg.Row, + cell: *pagepkg.Cell, + } { + const rac = self.page.data.getRowAndCell(self.x, self.y); + return .{ .row = rac.row, .cell = rac.cell }; + } + /// Move the pin down a certain number of rows, or return null if /// the pin goes beyond the end of the screen. pub fn down(self: Pin, n: usize) ?Pin { @@ -1953,6 +1825,7 @@ pub const Pin = struct { if (n <= rows) return .{ .offset = .{ .page = self.page, .y = n + self.y, + .x = self.x, } }; // Need to traverse page links to find the page @@ -1960,12 +1833,17 @@ pub const Pin = struct { var n_left: usize = n - rows; while (true) { page = page.next orelse return .{ .overflow = .{ - .end = .{ .page = page, .y = page.data.size.rows - 1 }, + .end = .{ + .page = page, + .y = page.data.size.rows - 1, + .x = self.x, + }, .remaining = n_left, } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, .y = n_left - 1, + .x = self.x, } }; n_left -= page.data.size.rows; } @@ -1984,6 +1862,7 @@ pub const Pin = struct { if (n <= self.y) return .{ .offset = .{ .page = self.page, .y = self.y - n, + .x = self.x, } }; // Need to traverse page links to find the page @@ -1991,12 +1870,13 @@ pub const Pin = struct { var n_left: usize = n - self.y; while (true) { page = page.prev orelse return .{ .overflow = .{ - .end = .{ .page = page, .y = 0 }, + .end = .{ .page = page, .y = 0, .x = self.x }, .remaining = n_left, } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, .y = page.data.size.rows - n_left, + .x = self.x, } }; n_left -= page.data.size.rows; } @@ -3054,6 +2934,58 @@ test "PageList resize (no reflow) less rows" { } } +test "PageList resize (no reflow) less rows cursor on bottom" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + try testing.expectEqual(@as(usize, 10), s.totalRows()); + + // This is required for our writing below to work + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write into all rows so we don't get trim behavior + for (0..s.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Put a tracked pin in the history + const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 9 } }).?); + defer s.untrackPin(p); + { + const cursor = s.pointFromPin(.active, p.*).?.active; + const get = s.getCell(.{ .active = .{ + .x = cursor.x, + .y = cursor.y, + } }).?; + try testing.expectEqual(@as(u21, 9), get.cell.content.codepoint); + } + + // Resize + try s.resize(.{ .rows = 5, .reflow = false }); + try testing.expectEqual(@as(usize, 5), s.rows); + try testing.expectEqual(@as(usize, 10), s.totalRows()); + + // Our cursor should move since it's in the scrollback + try testing.expectEqual(point.Point{ .active = .{ + .x = 0, + .y = 4, + } }, s.pointFromPin(.active, p.*).?); + + { + const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } +} test "PageList resize (no reflow) less rows cursor in scrollback" { const testing = std.testing; const alloc = testing.allocator; @@ -3227,6 +3159,35 @@ test "PageList resize (no reflow) less cols" { } } +test "PageList resize (no reflow) less cols pin in trimmed cols" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + // Put a tracked pin in the history + const p = try s.trackPin(s.pin(.{ .active = .{ .x = 8, .y = 2 } }).?); + defer s.untrackPin(p); + + // Resize + try s.resize(.{ .cols = 5, .reflow = false }); + try testing.expectEqual(@as(usize, 5), s.cols); + try testing.expectEqual(@as(usize, 10), s.totalRows()); + + var it = s.rowIterator(.{ .screen = .{} }, null); + while (it.next()) |offset| { + const rac = offset.rowAndCell(0); + const cells = offset.page.data.getCells(rac.row); + try testing.expectEqual(@as(usize, 5), cells.len); + } + + try testing.expectEqual(point.Point{ .active = .{ + .x = 4, + .y = 2, + } }, s.pointFromPin(.active, p.*).?); +} + test "PageList resize (no reflow) less cols clears graphemes" { const testing = std.testing; const alloc = testing.allocator; @@ -3431,9 +3392,6 @@ test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" } }, pt); } - // Let's say our cursor is at the bottom - var cursor: Resize.Cursor = .{ .x = 0, .y = s.rows - 2 }; - // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = s.rows - 2 } }).?); defer s.untrackPin(p); @@ -3447,7 +3405,11 @@ test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" } // Resize - try s.resizeWithoutReflow(.{ .rows = 10, .reflow = false, .cursor = &cursor }); + try s.resizeWithoutReflow(.{ + .rows = 10, + .reflow = false, + .cursor = .{ .x = 0, .y = s.rows - 2 }, + }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 10), s.rows); @@ -4338,17 +4300,20 @@ test "PageList resize reflow less cols cursor in final blank cell" { } } - // Set our cursor to be in the final cell of our resized - var cursor: Resize.Cursor = .{ .x = 3, .y = 0 }; + // Put a tracked pin in the history + const p = try s.trackPin(s.pin(.{ .active = .{ .x = 3, .y = 0 } }).?); + defer s.untrackPin(p); // Resize - try s.resize(.{ .cols = 4, .reflow = true, .cursor = &cursor }); + try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); // Our cursor should move to the first row - try testing.expectEqual(@as(size.CellCountInt, 3), cursor.x); - try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y); + try testing.expectEqual(point.Point{ .active = .{ + .x = 3, + .y = 0, + } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow less cols blank lines" { diff --git a/src/terminal2/Screen.zig b/src/terminal2/Screen.zig index 5326fb7e8..02918a836 100644 --- a/src/terminal2/Screen.zig +++ b/src/terminal2/Screen.zig @@ -88,7 +88,7 @@ pub const Cursor = struct { /// The pointers into the page list where the cursor is currently /// located. This makes it faster to move the cursor. - page_offset: PageList.RowOffset, + page_pin: *PageList.Pin, page_row: *pagepkg.Row, page_cell: *pagepkg.Cell, }; @@ -143,14 +143,10 @@ pub fn init( var pages = try PageList.init(alloc, cols, rows, max_scrollback); errdefer pages.deinit(); - // The active area is guaranteed to be allocated and the first - // page in the list after init. This lets us quickly setup the cursor. - // This is MUCH faster than pages.rowOffset. - const page_offset: PageList.RowOffset = .{ - .page = pages.pages.first.?, - .row_offset = 0, - }; - const page_rac = page_offset.rowAndCell(0); + // Create our tracked pin for the cursor. + const page_pin = try pages.trackPin(.{ .page = pages.pages.first.? }); + errdefer pages.untrackPin(page_pin); + const page_rac = page_pin.rowAndCell(); return .{ .alloc = alloc, @@ -159,7 +155,7 @@ pub fn init( .cursor = .{ .x = 0, .y = 0, - .page_offset = page_offset, + .page_pin = page_pin, .page_row = page_rac.row, .page_cell = page_rac.cell, }, @@ -248,8 +244,9 @@ pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell { assert(self.cursor.y > 0); - const page_offset = self.cursor.page_offset.backward(1).?; - const page_rac = page_offset.rowAndCell(self.pages.cols - 1); + var page_pin = self.cursor.page_pin.up(1).?; + page_pin.x = self.pages.cols - 1; + const page_rac = page_pin.rowAndCell(); return page_rac.cell; } @@ -260,6 +257,7 @@ pub fn cursorRight(self: *Screen, n: size.CellCountInt) void { const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); self.cursor.page_cell = @ptrCast(cell + n); + self.cursor.page_pin.x += n; self.cursor.x += n; } @@ -269,6 +267,7 @@ pub fn cursorLeft(self: *Screen, n: size.CellCountInt) void { const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); self.cursor.page_cell = @ptrCast(cell - n); + self.cursor.page_pin.x -= n; self.cursor.x -= n; } @@ -278,9 +277,9 @@ pub fn cursorLeft(self: *Screen, n: size.CellCountInt) void { pub fn cursorUp(self: *Screen, n: size.CellCountInt) void { assert(self.cursor.y >= n); - const page_offset = self.cursor.page_offset.backward(n).?; - const page_rac = page_offset.rowAndCell(self.cursor.x); - self.cursor.page_offset = page_offset; + const page_pin = self.cursor.page_pin.up(n).?; + const page_rac = page_pin.rowAndCell(); + self.cursor.page_pin.* = page_pin; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; self.cursor.y -= n; @@ -289,8 +288,8 @@ pub fn cursorUp(self: *Screen, n: size.CellCountInt) void { pub fn cursorRowUp(self: *Screen, n: size.CellCountInt) *pagepkg.Row { assert(self.cursor.y >= n); - const page_offset = self.cursor.page_offset.backward(n).?; - const page_rac = page_offset.rowAndCell(self.cursor.x); + const page_pin = self.cursor.page_pin.up(n).?; + const page_rac = page_pin.rowAndCell(); return page_rac.row; } @@ -302,9 +301,9 @@ pub fn cursorDown(self: *Screen, n: size.CellCountInt) void { // We move the offset into our page list to the next row and then // get the pointers to the row/cell and set all the cursor state up. - const page_offset = self.cursor.page_offset.forward(n).?; - const page_rac = page_offset.rowAndCell(self.cursor.x); - self.cursor.page_offset = page_offset; + const page_pin = self.cursor.page_pin.down(n).?; + const page_rac = page_pin.rowAndCell(); + self.cursor.page_pin.* = page_pin; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; @@ -316,7 +315,8 @@ pub fn cursorDown(self: *Screen, n: size.CellCountInt) void { pub fn cursorHorizontalAbsolute(self: *Screen, x: size.CellCountInt) void { assert(x < self.pages.cols); - const page_rac = self.cursor.page_offset.rowAndCell(x); + self.cursor.page_pin.x = x; + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_cell = page_rac.cell; self.cursor.x = x; } @@ -326,14 +326,15 @@ pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt) assert(x < self.pages.cols); assert(y < self.pages.rows); - const page_offset = if (y < self.cursor.y) - self.cursor.page_offset.backward(self.cursor.y - y).? + var page_pin = if (y < self.cursor.y) + self.cursor.page_pin.up(self.cursor.y - y).? else if (y > self.cursor.y) - self.cursor.page_offset.forward(y - self.cursor.y).? + self.cursor.page_pin.down(y - self.cursor.y).? else - self.cursor.page_offset; - const page_rac = page_offset.rowAndCell(x); - self.cursor.page_offset = page_offset; + self.cursor.page_pin.*; + page_pin.x = x; + const page_rac = page_pin.rowAndCell(); + self.cursor.page_pin.* = page_pin; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; self.cursor.x = x; @@ -344,13 +345,24 @@ pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt) /// so it should only be done in cases where the pointers are invalidated /// in such a way that its difficult to recover otherwise. pub fn cursorReload(self: *Screen) void { - const get = self.pages.getCell(.{ .active = .{ - .x = self.cursor.x, - .y = self.cursor.y, - } }).?; - self.cursor.page_offset = .{ .page = get.page, .row_offset = get.row_idx }; - self.cursor.page_row = get.row; - self.cursor.page_cell = get.cell; + // Our tracked pin is ALWAYS accurate, so we derive the active + // point from the pin. If this returns null it means our pin + // points outside the active area. In that case, we update the + // pin to be the top-left. + const pt: point.Point = self.pages.pointFromPin( + .active, + self.cursor.page_pin.*, + ) orelse reset: { + const pin = self.pages.pin(.{ .active = .{} }).?; + self.cursor.page_pin.* = pin; + break :reset self.pages.pointFromPin(.active, pin).?; + }; + + self.cursor.x = @intCast(pt.active.x); + self.cursor.y = @intCast(pt.active.y); + const page_rac = self.cursor.page_pin.rowAndCell(); + self.cursor.page_row = page_rac.row; + self.cursor.page_cell = page_rac.cell; } /// Scroll the active area and keep the cursor at the bottom of the screen. @@ -363,10 +375,11 @@ pub fn cursorDownScroll(self: *Screen) !void { // Erase rows will shift our rows up self.pages.eraseRows(.{ .active = .{} }, .{ .active = .{} }); - // We need to reload our cursor because the pointers are now invalid. - const page_offset = self.cursor.page_offset; - const page_rac = page_offset.rowAndCell(self.cursor.x); - self.cursor.page_offset = page_offset; + // We need to move our cursor down one because eraseRows will + // preserve our pin directly and we're erasing one row. + const page_pin = self.cursor.page_pin.down(1).?; + const page_rac = page_pin.rowAndCell(); + self.cursor.page_pin.* = page_pin; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; @@ -374,17 +387,17 @@ pub fn cursorDownScroll(self: *Screen) !void { // we never write those rows again. Active erasing is a bit // different so we manually clear our one row. self.clearCells( - &page_offset.page.data, + &page_pin.page.data, self.cursor.page_row, - page_offset.page.data.getCells(self.cursor.page_row), + page_pin.page.data.getCells(self.cursor.page_row), ); } else { // Grow our pages by one row. The PageList will handle if we need to // allocate, prune scrollback, whatever. _ = try self.pages.grow(); - const page_offset = self.cursor.page_offset.forward(1).?; - const page_rac = page_offset.rowAndCell(self.cursor.x); - self.cursor.page_offset = page_offset; + const page_pin = self.cursor.page_pin.down(1).?; + const page_rac = page_pin.rowAndCell(); + self.cursor.page_pin.* = page_pin; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; @@ -392,9 +405,9 @@ pub fn cursorDownScroll(self: *Screen) !void { // if we have a bg color at all. if (self.cursor.style.bg_color != .none) { self.clearCells( - &page_offset.page.data, + &page_pin.page.data, self.cursor.page_row, - page_offset.page.data.getCells(self.cursor.page_row), + page_pin.page.data.getCells(self.cursor.page_row), ); } } @@ -623,19 +636,12 @@ fn resizeInternal( // No matter what we mark our image state as dirty self.kitty_images.dirty = true; - // Create a resize cursor. The resize operation uses this to keep our - // cursor over the same cell if possible. - var cursor: PageList.Resize.Cursor = .{ - .x = self.cursor.x, - .y = self.cursor.y, - }; - // Perform the resize operation. This will update cursor by reference. try self.pages.resize(.{ .rows = rows, .cols = cols, .reflow = reflow, - .cursor = &cursor, + .cursor = .{ .x = self.cursor.x, .y = self.cursor.y }, }); // If we have no scrollback and we shrunk our rows, we must explicitly @@ -647,11 +653,7 @@ fn resizeInternal( // If our cursor was updated, we do a full reload so all our cursor // state is correct. - if (cursor.x != self.cursor.x or cursor.y != self.cursor.y) { - self.cursor.x = cursor.x; - self.cursor.y = cursor.y; - self.cursorReload(); - } + self.cursorReload(); } /// Set a style attribute for the current cursor. @@ -798,7 +800,7 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { /// Call this whenever you manually change the cursor style. pub fn manualStyleUpdate(self: *Screen) !void { - var page = &self.cursor.page_offset.page.data; + var page = &self.cursor.page_pin.page.data; // Remove our previous style if is unused. if (self.cursor.style_ref) |ref| { @@ -1056,7 +1058,7 @@ test "Screen read and write scrollback" { } } -test "Screen read and write no scrollback" { +test "Screen read and write no scrollback small" { const testing = std.testing; const alloc = testing.allocator; @@ -1103,7 +1105,7 @@ test "Screen style basics" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = s.cursor.page_offset.page.data; + const page = s.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); // Set a new style @@ -1125,7 +1127,7 @@ test "Screen style reset to default" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = s.cursor.page_offset.page.data; + const page = s.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); // Set a new style @@ -1145,7 +1147,7 @@ test "Screen style reset with unset" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = s.cursor.page_offset.page.data; + const page = s.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); // Set a new style @@ -1199,7 +1201,7 @@ test "Screen clearRows active styled line" { try s.setAttribute(.{ .unset = {} }); // We should have one style - const page = s.cursor.page_offset.page.data; + const page = s.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); s.clearRows(.{ .active = .{} }, null, false); diff --git a/src/terminal2/Terminal.zig b/src/terminal2/Terminal.zig index 8afa666f3..3347fed7e 100644 --- a/src/terminal2/Terminal.zig +++ b/src/terminal2/Terminal.zig @@ -270,7 +270,7 @@ pub fn print(self: *Terminal, c: u21) !void { var state: unicode.GraphemeBreakState = .{}; var cp1: u21 = prev.cell.content.codepoint; if (prev.cell.hasGrapheme()) { - const cps = self.screen.cursor.page_offset.page.data.lookupGrapheme(prev.cell).?; + const cps = self.screen.cursor.page_pin.page.data.lookupGrapheme(prev.cell).?; for (cps) |cp2| { // log.debug("cp1={x} cp2={x}", .{ cp1, cp2 }); assert(!unicode.graphemeBreak(cp1, cp2, &state)); @@ -342,7 +342,7 @@ pub fn print(self: *Terminal, c: u21) !void { } log.debug("c={x} grapheme attach to left={}", .{ c, prev.left }); - try self.screen.cursor.page_offset.page.data.appendGrapheme( + try self.screen.cursor.page_pin.page.data.appendGrapheme( self.screen.cursor.page_row, prev.cell, c, @@ -399,7 +399,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (!emoji) return; } - try self.screen.cursor.page_offset.page.data.appendGrapheme( + try self.screen.cursor.page_pin.page.data.appendGrapheme( self.screen.cursor.page_row, prev, c, @@ -540,7 +540,7 @@ fn printCell( // If the prior value had graphemes, clear those if (cell.hasGrapheme()) { - self.screen.cursor.page_offset.page.data.clearGrapheme( + self.screen.cursor.page_pin.page.data.clearGrapheme( self.screen.cursor.page_row, cell, ); @@ -571,7 +571,7 @@ fn printCell( // Slow path: we need to lookup this style so we can decrement // the ref count. Since we've already loaded everything, we also // just go ahead and GC it if it reaches zero, too. - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; if (page.styles.lookupId(page.memory, prev_style_id)) |prev_style| { // Below upsert can't fail because it should already be present const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable; @@ -1284,7 +1284,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { } // Left/right scroll margins we have to copy cells, which is much slower... - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; page.moveCells( src, self.scrolling_region.left, @@ -1300,7 +1300,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { const row: *Row = @ptrCast(top + i); // Clear the src row. - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; const cells = page.getCells(row); const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1]; self.screen.clearCells(page, row, cells_write); @@ -1378,7 +1378,7 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { } // Left/right scroll margins we have to copy cells, which is much slower... - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; page.moveCells( src, self.scrolling_region.left, @@ -1394,7 +1394,7 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { const row: *Row = @ptrCast(y); // Clear the src row. - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; const cells = page.getCells(row); const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1]; self.screen.clearCells(page, row, cells_write); @@ -1442,7 +1442,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell); - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; // Remaining cols from our cursor to the right margin. const rem = self.scrolling_region.right - self.screen.cursor.x + 1; @@ -1515,7 +1515,7 @@ pub fn deleteChars(self: *Terminal, count: usize) void { // left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell); - var page = &self.screen.cursor.page_offset.page.data; + var page = &self.screen.cursor.page_pin.page.data; // If our X is a wide spacer tail then we need to erase the // previous cell too so we don't split a multi-cell character. @@ -1609,7 +1609,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { // mode was not ISO we also always ignore protection attributes. if (self.screen.protected_mode != .iso) { self.screen.clearCells( - &self.screen.cursor.page_offset.page.data, + &self.screen.cursor.page_pin.page.data, self.screen.cursor.page_row, cells[0..end], ); @@ -1624,7 +1624,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { const cell: *Cell = @ptrCast(&cell_multi[0]); if (cell.protected) continue; self.screen.clearCells( - &self.screen.cursor.page_offset.page.data, + &self.screen.cursor.page_pin.page.data, self.screen.cursor.page_row, cell_multi[0..1], ); @@ -1694,7 +1694,7 @@ pub fn eraseLine( // to fill the entire line. if (!protected) { self.screen.clearCells( - &self.screen.cursor.page_offset.page.data, + &self.screen.cursor.page_pin.page.data, self.screen.cursor.page_row, cells[start..end], ); @@ -1706,7 +1706,7 @@ pub fn eraseLine( const cell: *Cell = @ptrCast(&cell_multi[0]); if (cell.protected) continue; self.screen.clearCells( - &self.screen.cursor.page_offset.page.data, + &self.screen.cursor.page_pin.page.data, self.screen.cursor.page_row, cell_multi[0..1], ); @@ -3703,7 +3703,7 @@ test "Terminal: insertLines handles style refs" { try t.setAttribute(.{ .unset = {} }); // verify we have styles in our style map - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); t.setCursorPos(2, 2); @@ -4378,7 +4378,7 @@ test "Terminal: eraseChars handles refcounted styles" { try t.print('C'); // verify we have styles in our style map - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); t.setCursorPos(1, 1); @@ -5695,7 +5695,7 @@ test "Terminal: garbage collect overwritten" { } // verify we have no styles in our style map - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); } @@ -5717,7 +5717,7 @@ test "Terminal: do not garbage collect old styles in use" { } // verify we have no styles in our style map - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); } @@ -6024,7 +6024,7 @@ test "Terminal: insertBlanks deleting graphemes" { try t.print(0x1F467); // We should have one cell with graphemes - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1); @@ -6058,7 +6058,7 @@ test "Terminal: insertBlanks shift graphemes" { try t.print(0x1F467); // We should have one cell with graphemes - const page = t.screen.cursor.page_offset.page.data; + const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1);