From 4c374d79770ef96f137fe61acda1a8c574640b5b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 24 Feb 2024 21:16:56 -0800 Subject: [PATCH] terminal/new: PageList scrolling --- src/terminal/new/PageList.zig | 170 +++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index 02823f63a..be9e305be 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -70,6 +70,10 @@ pub const Viewport = union(enum) { /// for this instead of tracking the row offset, we eliminate a number of /// memory writes making scrolling faster. active, + + /// The viewport is pinned to the top of the screen, or the farthest + /// back in the scrollback history. + top, }; /// Initialize the page. The top of the first page in the list is always the @@ -129,6 +133,31 @@ pub fn deinit(self: *PageList) void { self.pool.deinit(); } +/// Scroll options. +pub const Scroll = union(enum) { + /// Scroll to the active area. This is also sometimes referred to as + /// the "bottom" of the screen. This makes it so that the end of the + /// screen is fully visible since the active area is the bottom + /// rows/cols of the screen. + active, + + /// Scroll to the top of the screen, which is the farthest back in + /// the scrollback history. + top, +}; + +/// Scroll the viewport. This will never create new scrollback, allocate +/// pages, etc. This can only be used to move the viewport within the +/// previously allocated pages. +pub fn scroll(self: *PageList, behavior: Scroll) void { + switch (behavior) { + .active => self.viewport = .{ .active = {} }, + .top => self.viewport = .{ .top = {} }, + } +} + +/// Grow the page list by exactly one page and return the new page. The +/// newly allocated page will be size 0 (but capacity is set). pub fn grow(self: *PageList) !*List.Node { const next_page = try self.createPage(); // we don't errdefer this because we've added it to the linked @@ -228,8 +257,8 @@ fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset { .screen, .history => .{ .page = self.pages.first.? }, .viewport => switch (self.viewport) { - // If the viewport is in the active area then its the same as active. .active => self.getTopLeft(.active), + .top => self.getTopLeft(.screen), }, // The active area is calculated backwards from the last page. @@ -252,6 +281,42 @@ fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset { }; } +/// The total rows in the screen. This is the actual row count currently +/// and not a capacity or maximum. +/// +/// This is very slow, it traverses the full list of pages to count the +/// rows, so it is not pub. This is only used for testing/debugging. +fn totalRows(self: *const PageList) usize { + var rows: usize = 0; + var page = self.pages.first; + while (page) |p| { + rows += p.data.size.rows; + page = p.next; + } + + return rows; +} + +/// Grow the number of rows available in the page list by n. +/// This is only used for testing so it isn't optimized. +fn growRows(self: *PageList, n: usize) !void { + var page = self.pages.last.?; + var n_rem: usize = n; + if (page.data.size.rows < page.data.capacity.rows) { + const add = @min(n_rem, page.data.capacity.rows - page.data.size.rows); + page.data.size.rows += add; + if (n_rem == add) return; + n_rem -= add; + } + + while (n_rem > 0) { + page = try self.grow(); + const add = @min(n_rem, page.data.capacity.rows); + page.data.size.rows = add; + n_rem -= add; + } +} + /// Represents some y coordinate within the screen. Since pages can /// be split at any row boundary, getting some Y-coordinate within /// any part of the screen may map to a different page and row offset @@ -362,6 +427,28 @@ const Cell = struct { cell: *pagepkg.Cell, row_idx: usize, col_idx: usize, + + /// Gets the screen point for the given cell. + /// + /// This is REALLY expensive/slow so it isn't pub. This was built + /// for debugging and tests. If you have a need for this outside of + /// this file then consider a different approach and ask yourself very + /// carefully if you really need this. + fn screenPoint(self: Cell) point.Point { + var x: usize = self.col_idx; + var y: usize = self.row_idx; + var page = self.page; + while (page.prev) |prev| { + x += prev.data.size.cols; + y += prev.data.size.rows; + page = prev; + } + + return .{ .screen = .{ + .x = x, + .y = y, + } }; + } }; test "PageList" { @@ -372,6 +459,7 @@ test "PageList" { defer s.deinit(); try testing.expect(s.viewport == .active); try testing.expect(s.pages.first != null); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); // Active area should be the top try testing.expectEqual(RowOffset{ @@ -379,3 +467,83 @@ test "PageList" { .row_offset = 0, }, s.getTopLeft(.active)); } + +test "PageList active after grow" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 1000); + defer s.deinit(); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + try s.growRows(10); + try testing.expectEqual(@as(usize, s.rows + 10), s.totalRows()); + + // Make sure all points make sense + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + { + const pt = s.getCell(.{ .screen = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + { + const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } +} + +test "PageList scroll top" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 1000); + defer s.deinit(); + try s.growRows(10); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + s.scroll(.{ .top = {} }); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + s.scroll(.{ .active = {} }); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } +}