mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
scrolling in the screen (no scrollback yet)
This commit is contained in:
@ -1,11 +1,14 @@
|
||||
const Screen = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const color = @import("color.zig");
|
||||
|
||||
/// A line is a set of cells.
|
||||
pub const Line = []Cell;
|
||||
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 {
|
||||
@ -23,45 +26,39 @@ pub const Cell = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const LineIterator = struct {
|
||||
pub const RowIterator = struct {
|
||||
screen: *const Screen,
|
||||
index: usize,
|
||||
|
||||
pub fn next(self: *LineIterator) ?Line {
|
||||
if (self.index >= self.screen.lines) return null;
|
||||
|
||||
// Get the index of the first byte of the the line at index.
|
||||
const idx = self.screen.lineIndex(self.index) * self.screen.cols;
|
||||
|
||||
// The storage is sliced to return exactly the number of columns.
|
||||
const line = self.screen.storage[idx .. idx + self.screen.cols];
|
||||
|
||||
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 line;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
/// The full list of lines, including any scrollback.
|
||||
/// The full list of rows, including any scrollback.
|
||||
storage: []Cell,
|
||||
|
||||
/// The first visible line.
|
||||
/// The first visible row.
|
||||
zero: usize,
|
||||
|
||||
/// The number of lines and columns in the visible space.
|
||||
lines: usize,
|
||||
/// The number of rows and columns in the visible space.
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
|
||||
/// Initialize a new screen.
|
||||
pub fn init(alloc: Allocator, lines: usize, cols: usize) !Screen {
|
||||
// Allocate enough storage to cover every line and column in the visible
|
||||
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, lines * cols);
|
||||
const buf = try alloc.alloc(Cell, rows * cols);
|
||||
std.mem.set(Cell, buf, .{ .char = 0 });
|
||||
|
||||
return Screen{
|
||||
.storage = buf,
|
||||
.zero = 0,
|
||||
.lines = lines,
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
}
|
||||
@ -71,32 +68,68 @@ pub fn deinit(self: *Screen, alloc: Allocator) void {
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Returns an iterator that can be used to iterate over all of the lines
|
||||
/// Returns an iterator that can be used to iterate over all of the rows
|
||||
/// from index zero.
|
||||
pub fn lineIterator(self: *const Screen) LineIterator {
|
||||
pub fn rowIterator(self: *const Screen) RowIterator {
|
||||
return .{ .screen = self, .index = 0 };
|
||||
}
|
||||
|
||||
/// Returns the index for the given line (0-indexed) into the underlying
|
||||
/// 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 lineIndex(self: Screen, line: usize) usize {
|
||||
const idx = self.zero + line;
|
||||
if (idx < self.storage.len) return idx;
|
||||
return idx - self.storage.len;
|
||||
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 (negative) or down (positive). Scrolling direction
|
||||
/// is the direction opposite text would move. For example, scrolling down would
|
||||
/// move existing text upward. This sounds confusing but is the natural way
|
||||
/// that humans scroll a screen.
|
||||
pub fn scroll(self: *Screen, count: isize) void {
|
||||
if (count < 0) {
|
||||
self.zero -|= @intCast(usize, -count);
|
||||
} else {
|
||||
self.zero += @intCast(usize, count);
|
||||
}
|
||||
if (self.zero > self.storage.len) {
|
||||
self.zero -= self.storage.len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns the screen into a string.
|
||||
fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
||||
const buf = try alloc.alloc(u8, self.storage.len + self.lines);
|
||||
const buf = try alloc.alloc(u8, self.storage.len + self.rows);
|
||||
var i: usize = 0;
|
||||
var lines = self.lineIterator();
|
||||
for (lines.next()) |line, y| {
|
||||
var y: usize = 0;
|
||||
var rows = self.rowIterator();
|
||||
while (rows.next()) |row| {
|
||||
defer y += 1;
|
||||
|
||||
if (y > 0) {
|
||||
buf[i] = '\n';
|
||||
i += 1;
|
||||
}
|
||||
|
||||
for (line) |cell| {
|
||||
for (row) |cell| {
|
||||
// Turn NUL into space.
|
||||
const char = if (cell.char == 0) 0x20 else cell.char;
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||
@ -106,16 +139,70 @@ fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
||||
return buf[0..i];
|
||||
}
|
||||
|
||||
/// 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, 5, 10);
|
||||
var s = try init(alloc, 3, 5);
|
||||
defer s.deinit(alloc);
|
||||
|
||||
var i: usize = 0;
|
||||
var iter = s.lineIterator();
|
||||
while (iter.next() != null) i += 1;
|
||||
// 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);
|
||||
|
||||
try testing.expectEqual(@as(usize, 5), i);
|
||||
// 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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user