scrolling in the screen (no scrollback yet)

This commit is contained in:
Mitchell Hashimoto
2022-05-21 15:48:21 -07:00
parent 7de79d1f2b
commit 1a31f8c8be

View File

@ -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);
}