ghostty/src/terminal/Screen.zig
Mitchell Hashimoto c5cdc68466 screen resize
2022-05-22 14:45:10 -07:00

411 lines
12 KiB
Zig

const Screen = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const color = @import("color.zig");
const log = std.log.scoped(.screen);
/// A row is a set of cells.
pub const Row = []Cell;
/// Cell is a single cell within the screen.
pub const Cell = struct {
/// Each cell contains exactly one character. The character is UTF-32
/// encoded (just the Unicode codepoint).
char: u32,
/// Foreground and background color. null means to use the default.
fg: ?color.RGB = null,
bg: ?color.RGB = null,
/// True if the cell should be skipped for drawing
pub fn empty(self: Cell) bool {
return self.char == 0;
}
};
pub const RowIterator = struct {
screen: *const Screen,
index: usize,
pub fn next(self: *RowIterator) ?Row {
if (self.index >= self.screen.rows) return null;
const res = self.screen.getRow(self.index);
self.index += 1;
return res;
}
};
/// The full list of rows, including any scrollback.
storage: []Cell,
/// The first visible row.
zero: usize,
/// The number of rows and columns in the visible space.
rows: usize,
cols: usize,
/// Initialize a new screen.
pub fn init(alloc: Allocator, rows: usize, cols: usize) !Screen {
// Allocate enough storage to cover every row and column in the visible
// area. This wastes some up front memory but saves allocations later.
const buf = try alloc.alloc(Cell, rows * cols);
std.mem.set(Cell, buf, .{ .char = 0 });
return Screen{
.storage = buf,
.zero = 0,
.rows = rows,
.cols = cols,
};
}
pub fn deinit(self: *Screen, alloc: Allocator) void {
alloc.free(self.storage);
self.* = undefined;
}
/// Returns an iterator that can be used to iterate over all of the rows
/// from index zero.
pub fn rowIterator(self: *const Screen) RowIterator {
return .{ .screen = self, .index = 0 };
}
/// Get the visible portion of the screen.
pub fn getVisible(self: Screen) []Cell {
return self.storage;
}
/// Get a single row by index (0-indexed).
pub fn getRow(self: Screen, idx: usize) Row {
// Get the index of the first byte of the the row at index.
const real_idx = self.rowIndex(idx);
// The storage is sliced to return exactly the number of columns.
return self.storage[real_idx .. real_idx + self.cols];
}
/// Get a single cell in the visible area. row and col are 0-indexed.
pub fn getCell(self: Screen, row: usize, col: usize) *Cell {
assert(row < self.rows);
assert(col < self.cols);
const row_idx = self.rowIndex(row);
return &self.storage[row_idx + col];
}
/// Returns the index for the given row (0-indexed) into the underlying
/// storage array.
pub fn rowIndex(self: Screen, idx: usize) usize {
assert(idx < self.rows);
const val = (self.zero + idx) * self.cols;
if (val < self.storage.len) return val;
return val - self.storage.len;
}
/// Scroll the screen up (positive) or down (negative). Scrolling direction
/// is the direction text would move. For example, scrolling down would
/// move existing text downward.
pub fn scroll(self: *Screen, count: isize) void {
if (count < 0) {
const amount = @mod(@intCast(usize, -count), self.rows);
if (amount > self.zero) {
self.zero = self.rows - amount;
} else {
self.zero -|= amount;
}
} else {
self.zero = @mod(self.zero + @intCast(usize, count), self.rows);
}
}
/// Copy row at src to dst.
pub fn copyRow(self: *Screen, dst: usize, src: usize) void {
const src_row = self.getRow(src);
const dst_row = self.getRow(dst);
std.mem.copy(Cell, dst_row, src_row);
}
/// Resize the screen. The rows or cols can be bigger or smaller. Due to
/// the internal representation of a screen, this usually involves a significant
/// amount of copying compared to any other operations.
///
/// This will trim data if the size is getting smaller. It is expected that
/// callers will reflow the text prior to calling this.
pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void {
// Make a copy so we can access the old indexes.
const old = self.*;
// Reallocate the storage
self.storage = try alloc.alloc(Cell, rows * cols);
self.zero = 0;
self.rows = rows;
self.cols = cols;
// If we're increasing height, then copy all rows (start at 0).
// Otherwise start at the latest row that includes the bottom row,
// aka trip the top.
var y: usize = if (rows >= old.rows) 0 else old.rows - rows;
const start = y;
const col_end = @minimum(old.cols, cols);
while (y < old.rows) : (y += 1) {
// Copy the old row into the new row, just losing the columsn
// if we got thinner.
const old_row = old.getRow(y);
const new_row = self.getRow(y - start);
std.mem.copy(Cell, new_row, old_row[0..col_end]);
// If our new row is wider, then we copy zeroes into the rest.
if (new_row.len > old_row.len) {
std.mem.set(Cell, new_row[old_row.len..], .{ .char = 0 });
}
}
// If we grew rows, then set the remaining data to zero.
if (rows > old.rows) {
std.mem.set(Cell, self.storage[self.rowIndex(old.rows)..], .{ .char = 0 });
}
// Free the old data
alloc.free(old.storage);
}
/// Turns the screen into a string.
pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
const buf = try alloc.alloc(u8, self.storage.len + self.rows);
var i: usize = 0;
var y: usize = 0;
var rows = self.rowIterator();
while (rows.next()) |row| {
defer y += 1;
if (y > 0) {
buf[i] = '\n';
i += 1;
}
for (row) |cell| {
// TODO: handle character after null
if (cell.char > 0) {
i += try std.unicode.utf8Encode(@intCast(u21, cell.char), buf[i..]);
}
}
}
// Never render the final newline
const str = std.mem.trimRight(u8, buf[0..i], "\n");
return try alloc.realloc(buf, str.len);
}
/// Writes a basic string into the screen for testing. Newlines (\n) separate
/// each row.
fn testWriteString(self: *Screen, text: []const u8) void {
var y: usize = 0;
var x: usize = 0;
var row = self.getRow(y);
for (text) |c| {
if (c == '\n') {
y += 1;
x = 0;
row = self.getRow(y);
continue;
}
assert(x < self.cols);
row[x].char = @intCast(u32, c);
x += 1;
}
}
test "Screen" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
// Sanity check that our test helpers work
const str = "1ABCD\n2EFGH\n3IJKL";
s.testWriteString(str);
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings(str, contents);
// Test the row iterator
var count: usize = 0;
var iter = s.rowIterator();
while (iter.next()) |row| {
// Rows should be pointer equivalent to getRow
const row_other = s.getRow(count);
try testing.expectEqual(row.ptr, row_other.ptr);
count += 1;
}
// Should go through all rows
try testing.expectEqual(@as(usize, 3), count);
}
test "Screen: scrolling" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
s.scroll(1);
// Test our row index
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
try testing.expectEqual(@as(usize, 0), s.rowIndex(2));
{
// Test our contents rotated
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL\n1ABCD", contents);
}
// Scroll by a multiple
s.scroll(@intCast(isize, s.rows) * 4);
// Test our row index
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
try testing.expectEqual(@as(usize, 0), s.rowIndex(2));
{
// Test our contents rotated
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL\n1ABCD", contents);
}
}
test "Screen: scroll down from 0" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
s.scroll(-1);
// Test our row index
try testing.expectEqual(@as(usize, 10), s.rowIndex(0));
try testing.expectEqual(@as(usize, 0), s.rowIndex(1));
try testing.expectEqual(@as(usize, 5), s.rowIndex(2));
{
// Test our contents rotated
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("3IJKL\n1ABCD\n2EFGH", contents);
}
// Scroll by a multiple
s.scroll(-4 * @intCast(isize, s.rows));
// Test our row index
try testing.expectEqual(@as(usize, 10), s.rowIndex(0));
try testing.expectEqual(@as(usize, 0), s.rowIndex(1));
try testing.expectEqual(@as(usize, 5), s.rowIndex(2));
{
// Test our contents rotated
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("3IJKL\n1ABCD\n2EFGH", contents);
}
}
test "Screen: row copy" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Copy
s.scroll(1);
s.copyRow(2, 0);
// Test our contents
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents);
}
test "Screen: resize more rows" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
const str = "1ABCD\n2EFGH\n3IJKL";
s.testWriteString(str);
try s.resize(alloc, 10, 5);
{
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings(str, contents);
}
}
test "Screen: resize less rows" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
const str = "1ABCD\n2EFGH\n3IJKL";
s.testWriteString(str);
try s.resize(alloc, 2, 5);
{
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
}
}
test "Screen: resize more cols" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
const str = "1ABCD\n2EFGH\n3IJKL";
s.testWriteString(str);
try s.resize(alloc, 3, 10);
{
var contents = try s.testString(alloc);
defer alloc.free(contents);
try testing.expectEqualStrings(str, contents);
}
}
test "Screen: resize less cols" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5);
defer s.deinit(alloc);
const str = "1ABCD\n2EFGH\n3IJKL";
s.testWriteString(str);
try s.resize(alloc, 3, 4);
{
var contents = try s.testString(alloc);
defer alloc.free(contents);
const expected = "1ABC\n2EFG\n3IJK";
try testing.expectEqualStrings(expected, contents);
}
}