mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
terminal: add Screen.cursorScrollAbove and tests
This commit is contained in:
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const charsets = @import("charsets.zig");
|
const charsets = @import("charsets.zig");
|
||||||
|
const fastmem = @import("../fastmem.zig");
|
||||||
const kitty = @import("kitty.zig");
|
const kitty = @import("kitty.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
const unicode = @import("../unicode/main.zig");
|
const unicode = @import("../unicode/main.zig");
|
||||||
@ -741,6 +742,100 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This scrolls the active area at and above the cursor. The lines below
|
||||||
|
/// the cursor are not scrolled.
|
||||||
|
pub fn cursorScrollAbove(self: *Screen) !void {
|
||||||
|
// If the cursor is on the bottom of the screen, its faster to use
|
||||||
|
// our specialized function for that case.
|
||||||
|
if (self.cursor.y == self.pages.rows - 1) {
|
||||||
|
return try self.cursorDownScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
defer self.assertIntegrity();
|
||||||
|
|
||||||
|
// Logic below assumes we always have at least one row that isn't moving
|
||||||
|
assert(self.cursor.y < self.pages.rows - 1);
|
||||||
|
|
||||||
|
const old_pin = self.cursor.page_pin.*;
|
||||||
|
if (try self.pages.grow()) |new_page_node| {
|
||||||
|
// We allocated a new page and went to it. In this case, our new
|
||||||
|
// empty line is at the top of this page.
|
||||||
|
|
||||||
|
// Prev is never null because pagelist asserts that we always have
|
||||||
|
// memory for at least two pages. This is an assertion.
|
||||||
|
assert(new_page_node.prev.? == old_pin.page);
|
||||||
|
const prev_page = &old_pin.page.data;
|
||||||
|
const new_page = &new_page_node.data;
|
||||||
|
|
||||||
|
const prev_rows = prev_page.rows.ptr(prev_page.memory.ptr);
|
||||||
|
const new_rows = new_page.rows.ptr(new_page.memory.ptr);
|
||||||
|
const prev_last_row = &prev_rows[prev_page.size.rows - 1];
|
||||||
|
|
||||||
|
// First, copy the last row of the previous page to the top
|
||||||
|
// of our current page.
|
||||||
|
try new_page.cloneRowFrom(
|
||||||
|
prev_page,
|
||||||
|
&new_rows[0],
|
||||||
|
prev_last_row,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update our cursor metadata now. We call methods below that assert
|
||||||
|
// integrity in debug modes so we want to put ourselves in a
|
||||||
|
// consistent state first.
|
||||||
|
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||||
|
self.cursorChangePin(self.cursor.page_pin.*);
|
||||||
|
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||||
|
self.cursor.page_row = page_rac.row;
|
||||||
|
self.cursor.page_cell = page_rac.cell;
|
||||||
|
|
||||||
|
// Third, clear the last row of the previous page.
|
||||||
|
self.clearCells(
|
||||||
|
prev_page,
|
||||||
|
prev_last_row,
|
||||||
|
prev_page.getCells(prev_last_row),
|
||||||
|
);
|
||||||
|
var dirty = prev_page.dirtyBitSet();
|
||||||
|
dirty.set(prev_page.size.rows - 1);
|
||||||
|
} else {
|
||||||
|
// In this case, it means grow() didn't allocate a new page. This
|
||||||
|
// allows us to perform a fast path by rotating rows on the same page.
|
||||||
|
assert(old_pin.page == self.cursor.page_pin.page);
|
||||||
|
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||||
|
|
||||||
|
const pin = self.cursor.page_pin;
|
||||||
|
const page = &self.cursor.page_pin.page.data;
|
||||||
|
|
||||||
|
// Rotate the rows so that the newly created empty row is at the
|
||||||
|
// beginning. e.g. [ 0 1 2 3 ] in to [ 3 0 1 2 ].
|
||||||
|
var rows = page.rows.ptr(page.memory.ptr);
|
||||||
|
fastmem.rotateOnceR(Row, rows[pin.y..page.size.rows]);
|
||||||
|
|
||||||
|
// Mark all our rotated rows as dirty.
|
||||||
|
var dirty = page.dirtyBitSet();
|
||||||
|
dirty.setRangeValue(.{ .start = pin.y, .end = page.size.rows }, true);
|
||||||
|
|
||||||
|
// Setup our cursor caches after the rotation so it points to the
|
||||||
|
// correct data
|
||||||
|
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||||
|
self.cursor.page_row = page_rac.row;
|
||||||
|
self.cursor.page_cell = page_rac.cell;
|
||||||
|
|
||||||
|
// Note: we don't need to call cursorChangePin here because
|
||||||
|
// the pin page is the same so there is no accounting to do for
|
||||||
|
// styles or any of that.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.cursor.style_id != style.default_id) {
|
||||||
|
// The newly created line needs to be styled according to
|
||||||
|
// the bg color if it is set.
|
||||||
|
if (self.cursor.style.bgCell()) |blank_cell| {
|
||||||
|
const cell_current: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
||||||
|
const cells = cell_current - self.cursor.x;
|
||||||
|
@memset(cells[0..self.pages.cols], blank_cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Move the cursor down if we're not at the bottom of the screen. Otherwise
|
/// Move the cursor down if we're not at the bottom of the screen. Otherwise
|
||||||
/// scroll. Currently only used for testing.
|
/// scroll. Currently only used for testing.
|
||||||
fn cursorDownOrScroll(self: *Screen) !void {
|
fn cursorDownOrScroll(self: *Screen) !void {
|
||||||
@ -3817,6 +3912,125 @@ test "Screen: scroll and clear ignore blank lines" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above same page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||||
|
// (not just scroll).
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above creates new page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// We need to get the cursor to a new page
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
|
||||||
|
// Ensure we're still on the first page
|
||||||
|
try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?);
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
// const stderr = std.io.getStdErr().writer();
|
||||||
|
// try s.pages.diagram(stderr);
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1 is dirty because they are the ones that CHANGED contents
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above no scrollback bottom of page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||||
|
// (not just scroll).
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
test "Screen: clone" {
|
test "Screen: clone" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
Reference in New Issue
Block a user