mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
screen2: scrolling (to a certain extent), copying in tests
This commit is contained in:
@ -73,7 +73,7 @@ pub const Cell = struct {
|
||||
/// additional codepoints can be looked up in the hash map on the
|
||||
/// Screen. Since multi-codepoints graphemes are rare, we don't want to
|
||||
/// waste memory for every cell, so we use a side lookup for it.
|
||||
char: u32,
|
||||
char: u32 = 0,
|
||||
|
||||
/// Foreground and background color. attrs.has_{bg/fg} must be checked
|
||||
/// to see if these are useful values.
|
||||
@ -131,6 +131,16 @@ pub const Row = struct {
|
||||
self.storage[0].header.wrap = v;
|
||||
}
|
||||
|
||||
/// Clear the row, making all cells empty.
|
||||
pub fn clear(self: Row) void {
|
||||
self.fill(.{});
|
||||
}
|
||||
|
||||
/// Fill the entire row with a copy of a single cell.
|
||||
pub fn fill(self: Row, cell: Cell) void {
|
||||
std.mem.set(StorageCell, self.storage[1..], .{ .cell = cell });
|
||||
}
|
||||
|
||||
/// Get a pointr to the cell at column x (0-indexed). This always
|
||||
/// assumes that the cell was modified, notifying the renderer on the
|
||||
/// next call to re-render this cell. Any change detection to avoid
|
||||
@ -299,7 +309,8 @@ pub fn init(
|
||||
// * Our buffer size is preallocated to fit double our visible space
|
||||
// or the maximum scrollback whichever is smaller.
|
||||
// * We add +1 to cols to fit the row header
|
||||
const buf_size = (rows + @minimum(max_scrollback, rows)) * (cols + 1);
|
||||
const buf_size = (rows + max_scrollback) * (cols + 1);
|
||||
//const buf_size = (rows + @minimum(max_scrollback, rows)) * (cols + 1);
|
||||
|
||||
return Screen{
|
||||
.alloc = alloc,
|
||||
@ -315,6 +326,11 @@ pub fn deinit(self: *Screen) void {
|
||||
self.storage.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Returns true if the viewport is scrolled to the bottom of the screen.
|
||||
pub fn viewportIsBottom(self: Screen) bool {
|
||||
return self.viewport >= RowIndexTag.history.maxLen(&self);
|
||||
}
|
||||
|
||||
/// Returns an iterator that can be used to iterate over all of the rows
|
||||
/// from index zero of the given row index type. This can therefore iterate
|
||||
/// from row 0 of the active area, history, viewport, etc.
|
||||
@ -341,9 +357,12 @@ pub fn getRow(self: *Screen, index: RowIndex) Row {
|
||||
/// invalid.
|
||||
fn rowOffset(self: Screen, index: RowIndex) usize {
|
||||
// +1 for row header
|
||||
return index.toScreen().screen * (self.cols + 1);
|
||||
return index.toScreen(&self).screen * (self.cols + 1);
|
||||
}
|
||||
|
||||
/// Returns the number of rows that have actually been written to the
|
||||
/// screen. This assumes a row is "written" if getRow was ever called
|
||||
/// on the row.
|
||||
fn rowsWritten(self: Screen) usize {
|
||||
// The number of rows we've actually written into our buffer
|
||||
// This should always be cleanly divisible since we only request
|
||||
@ -352,6 +371,118 @@ fn rowsWritten(self: Screen) usize {
|
||||
return self.storage.len() / (self.cols + 1);
|
||||
}
|
||||
|
||||
/// The number of rows our backing storage supports. This should
|
||||
/// always be self.rows but we use the backing storage as a source of truth.
|
||||
fn rowsCapacity(self: Screen) usize {
|
||||
assert(@mod(self.storage.capacity(), self.cols + 1) == 0);
|
||||
return self.storage.capacity() / (self.cols + 1);
|
||||
}
|
||||
|
||||
/// Scroll behaviors for the scroll function.
|
||||
pub const Scroll = union(enum) {
|
||||
/// Scroll to the top of the scroll buffer. The first line of the
|
||||
/// viewport will be the top line of the scroll buffer.
|
||||
top: void,
|
||||
|
||||
/// Scroll to the bottom, where the last line of the viewport
|
||||
/// will be the last line of the buffer. TODO: are we sure?
|
||||
bottom: void,
|
||||
|
||||
/// Scroll up (negative) or down (positive) some fixed amount.
|
||||
/// Scrolling direction (up/down) describes the direction the viewport
|
||||
/// moves, not the direction text moves. This is the colloquial way that
|
||||
/// scrolling is described: "scroll the page down".
|
||||
delta: isize,
|
||||
|
||||
/// Same as delta but scrolling down will not grow the scrollback.
|
||||
/// Scrolling down at the bottom will do nothing (similar to how
|
||||
/// delta at the top does nothing).
|
||||
delta_no_grow: isize,
|
||||
};
|
||||
|
||||
/// Scroll the screen by the given behavior. Note that this will always
|
||||
/// "move" the screen. It is up to the caller to determine if they actually
|
||||
/// want to do that yet (i.e. are they writing to the end of the screen
|
||||
/// or not).
|
||||
pub fn scroll(self: *Screen, behavior: Scroll) void {
|
||||
switch (behavior) {
|
||||
// Setting viewport offset to zero makes row 0 be at self.top
|
||||
// which is the top!
|
||||
.top => self.viewport = 0,
|
||||
|
||||
// Bottom is the end of the history area (end of history is the
|
||||
// top of the active area).
|
||||
.bottom => self.viewport = RowIndexTag.history.maxLen(self),
|
||||
|
||||
// TODO: deltas greater than the entire scrollback
|
||||
.delta => |delta| self.scrollDelta(delta, true),
|
||||
.delta_no_grow => |delta| self.scrollDelta(delta, false),
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollDelta(self: *Screen, delta: isize, grow: bool) void {
|
||||
// If we're scrolling up, then we just subtract and we're done.
|
||||
// We just clamp at 0 which blocks us from scrolling off the top.
|
||||
if (delta < 0) {
|
||||
self.viewport -|= @intCast(usize, -delta);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're scrolling down and not growing, then we just
|
||||
// add to the viewport and clamp at the bottom.
|
||||
const viewport_max = RowIndexTag.history.maxLen(self);
|
||||
if (!grow) {
|
||||
self.viewport = @minimum(
|
||||
viewport_max,
|
||||
self.viewport +| @intCast(usize, delta),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// {
|
||||
// const rows_capacity = self.rowsCapacity();
|
||||
// const rows_written = self.rowsWritten();
|
||||
// log.warn("rows_written={} rows_capacity={} vp={} vp_new={}", .{
|
||||
// rows_written,
|
||||
// rows_capacity,
|
||||
// self.viewport,
|
||||
// self.viewport + @intCast(usize, delta),
|
||||
// });
|
||||
// }
|
||||
|
||||
// Add our delta to our viewport. If we're less than the max currently
|
||||
// allowed to scroll to the bottom (the end of the history), then we
|
||||
// have space and we just return.
|
||||
self.viewport +|= @intCast(usize, delta);
|
||||
if (self.viewport <= viewport_max) return;
|
||||
|
||||
// Our viewport is bigger than our max. The number of new rows we need
|
||||
// in our buffer is our value minus the max.
|
||||
const new_rows_needed = self.viewport - viewport_max;
|
||||
|
||||
// If we can fit this into our existing capacity, then just grow to it.
|
||||
const rows_capacity = self.rowsCapacity();
|
||||
const rows_written = self.rowsWritten();
|
||||
if (rows_written + new_rows_needed <= rows_capacity) {
|
||||
// Ensure we have "written" this data into the circular buffer.
|
||||
_ = self.storage.getPtrSlice(
|
||||
self.viewport * (self.cols + 1),
|
||||
self.cols + 1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't fit our new rows into the capacity, so the amount
|
||||
// between what we need and the capacity needs to be deleted. We
|
||||
// scroll "up" by that much to offset this.
|
||||
const rows_to_delete = (rows_written + new_rows_needed) - rows_capacity;
|
||||
self.viewport -= rows_to_delete;
|
||||
self.storage.deleteOldest(rows_to_delete * (self.cols + 1));
|
||||
|
||||
// If we grew down like this, we must be at the bottom.
|
||||
assert(self.viewportIsBottom());
|
||||
}
|
||||
|
||||
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
||||
/// each row. If a line is longer than the available columns, soft-wrapping
|
||||
/// will occur. This will automatically handle basic wide chars.
|
||||
@ -372,8 +503,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
// If we're writing past the end of the active area, scroll.
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
|
||||
// Get our row
|
||||
@ -386,8 +516,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
@ -413,8 +542,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
@ -473,18 +601,269 @@ pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8
|
||||
return try alloc.realloc(buf, str.len);
|
||||
}
|
||||
|
||||
test {
|
||||
test "Screen" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
var s = try init(alloc, 5, 5, 0);
|
||||
defer s.deinit();
|
||||
try testing.expect(s.rowsWritten() == 0);
|
||||
|
||||
// Sanity check that our test helpers work
|
||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||
s.testWriteString(str);
|
||||
try testing.expect(s.rowsWritten() == 3);
|
||||
{
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
|
||||
// Test the row iterator
|
||||
var count: usize = 0;
|
||||
var iter = s.rowIterator(.viewport);
|
||||
while (iter.next()) |row| {
|
||||
// Rows should be pointer equivalent to getRow
|
||||
const row_other = s.getRow(.{ .viewport = count });
|
||||
try testing.expectEqual(row.storage.ptr, row_other.storage.ptr);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Should go through all rows
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
|
||||
// Should be able to easily clear screen
|
||||
{
|
||||
var it = s.rowIterator(.viewport);
|
||||
while (it.next()) |row| row.fill(.{ .char = 'A' });
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("AAAAA\nAAAAA\nAAAAA", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: scrolling" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit();
|
||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
try testing.expect(s.viewportIsBottom());
|
||||
|
||||
// Scroll down, should still be bottom
|
||||
s.scroll(.{ .delta = 1 });
|
||||
try testing.expect(s.viewportIsBottom());
|
||||
|
||||
// Test our row index
|
||||
try testing.expectEqual(@as(usize, 0), s.rowOffset(.{ .active = 0 }));
|
||||
try testing.expectEqual(@as(usize, 6), s.rowOffset(.{ .active = 1 }));
|
||||
try testing.expectEqual(@as(usize, 12), s.rowOffset(.{ .active = 2 }));
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling to the bottom does nothing
|
||||
s.scroll(.{ .bottom = {} });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: scroll down from 0" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit();
|
||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
|
||||
// Scrolling up does nothing, but allows it
|
||||
s.scroll(.{ .delta = -1 });
|
||||
try testing.expect(s.viewportIsBottom());
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 1);
|
||||
defer s.deinit();
|
||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
s.scroll(.{ .delta = 1 });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling to the bottom
|
||||
s.scroll(.{ .bottom = {} });
|
||||
try testing.expect(s.viewportIsBottom());
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling back should make it visible again
|
||||
s.scroll(.{ .delta = -1 });
|
||||
try testing.expect(!s.viewportIsBottom());
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling back again should do nothing
|
||||
s.scroll(.{ .delta = -1 });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling to the bottom
|
||||
s.scroll(.{ .bottom = {} });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling forward with no grow should do nothing
|
||||
s.scroll(.{ .delta_no_grow = 1 });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Scrolling to the top should work
|
||||
s.scroll(.{ .top = {} });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
// Should be able to easily clear active area only
|
||||
var it = s.rowIterator(.active);
|
||||
while (it.next()) |row| row.clear();
|
||||
{
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD", contents);
|
||||
}
|
||||
|
||||
// Scrolling to the bottom
|
||||
s.scroll(.{ .bottom = {} });
|
||||
|
||||
{
|
||||
// Test our contents rotated
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: scrollback empty" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 50);
|
||||
defer s.deinit();
|
||||
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
s.scroll(.{ .delta_no_grow = 1 });
|
||||
|
||||
{
|
||||
// Test our contents
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: history region with no scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 1, 5, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// Write a bunch that WOULD invoke scrollback if exists
|
||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||
s.testWriteString(str);
|
||||
{
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
const expected = "3IJKL";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
// Verify no scrollback
|
||||
var it = s.rowIterator(.history);
|
||||
var count: usize = 0;
|
||||
while (it.next()) |_| count += 1;
|
||||
try testing.expect(count == 0);
|
||||
}
|
||||
|
||||
test "Screen: history region with scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 1, 5, 2);
|
||||
defer s.deinit();
|
||||
|
||||
// Write a bunch that WOULD invoke scrollback if exists
|
||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||
s.testWriteString(str);
|
||||
{
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
const expected = "3IJKL";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
{
|
||||
// Test our contents
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
|
||||
}
|
||||
|
||||
{
|
||||
var contents = try s.testString(alloc, .history);
|
||||
defer alloc.free(contents);
|
||||
const expected = "1ABCD\n2EFGH";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ pub fn CircBuf(comptime T: type) type {
|
||||
/// the offset can only be within the size of the buffer.
|
||||
pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T {
|
||||
assert(slice_len > 0);
|
||||
assert(offset + slice_len <= self.capacity());
|
||||
|
||||
// End offset is the last offset (exclusive) for our slice.
|
||||
// We use exclusive because it makes the math easier and it
|
||||
|
Reference in New Issue
Block a user