Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341//! Sub-regions of a shared cell grid, ported from uv `window.go`. A `Window`//! is a rectangular area with a cell grid backing it. A root window owns its//! `Buffer`; sub-windows either own a fresh buffer or share their parent's//! (a *view*). Every window is itself a [`screen.Screen`], so the `screen`//! fast-path helpers and [`Context`](screen.Context) can draw into it.//!//! Window coordinates are absolute: a window's `bounds` is a *positioned*//! rectangle. The embedded `screen` field's vtable thunks translate//! window-local cell indices into the underlying buffer by offsetting with//! `bounds.min_x`/`bounds.min_y`, so a view into the middle of a parent's grid//! reads and writes the correct cells. The `Buffer` holds the full grid;//! windows carve out regions of it.//!//! Named departures from upstream (`window.go`)://! - Constructors return a value and propagate out-of-memory (uv returns//! `*Window` via `new`); [`deinit`](deinit) frees the buffer only when owned.//! - A window owns its buffer when made with [`newRoot`](newRoot) or//! [`newChild`](newChild), or via [`cloneArea`](cloneArea) (a fresh copy).//! Views and their buffers belong to the parent; never `deinit` a view before//! its parent, and never `deinit` a buffer the parent still borrows.//! - `MoveBy`/`MoveTo` take `usize` (uv takes `int`); bounds never go negative.const std = @import("std");const ir = @import("root.zig");const scr = @import("screen.zig");
const Allocator = std.mem.Allocator;const Buffer = ir.Buffer;const Cell = ir.Cell;const Method = ir.width.Method;const Rectangle = ir.Rectangle;const Screen = scr.Screen;const t = std.testing;
const Window = @This();
/// The backing grid. Owned (and freed on `deinit`) only when `owns_buffer`.buffer: *Buffer,owns_buffer: bool = false,method: Method = .wcwidth,/// Parent window, if a sub-window. Views and owned children may share the/// parent's buffer.parent: ?*Window = null,/// The window's positioned bounds, absolute in the buffer's coordinate space.window_bounds: Rectangle = .{},screen: scr.Screen = .{ .vtable = &.{ .bounds = impl.bounds, .cell_at = impl.cellAt, .set_cell = impl.setCell, .width_method = impl.widthMethod, .clear = impl.clear, .clear_area = impl.clearArea, .fill = impl.fill, .fill_area = impl.fillArea, .clone = impl.clone, .clone_area = impl.cloneArea, },},
/// Interface thunks: translate window-local indices into the backing buffer/// by adding the window's origin. `bounds` reports the window's positioned/// bounds, so `screen` helpers and `Border.draw` operate on exactly the/// window's region.const impl = struct { fn bounds(s: *Screen) Rectangle { const w: *Window = @fieldParentPtr("screen", s); return w.window_bounds; }
fn cellAt(s: *Screen, x: usize, y: usize) ?*Cell { const w: *Window = @fieldParentPtr("screen", s); if (x >= w.window_bounds.max_x - w.window_bounds.min_x or y >= w.window_bounds.max_y - w.window_bounds.min_y) return null; return w.buffer.cellAt(w.window_bounds.min_x + x, w.window_bounds.min_y + y); }
fn setCell(s: *Screen, x: usize, y: usize, cell: ?Cell) void { const w: *Window = @fieldParentPtr("screen", s); if (x >= w.window_bounds.max_x - w.window_bounds.min_x or y >= w.window_bounds.max_y - w.window_bounds.min_y) return; w.buffer.setCell(w.window_bounds.min_x + x, w.window_bounds.min_y + y, cell); }
fn widthMethod(s: *Screen) Method { const w: *Window = @fieldParentPtr("screen", s); return w.method; }
// The `screen` fast paths (clear/fill/clone) only make sense on a window // that owns its whole grid; a view's region is a sub-slice of its // parent's buffer, so we leave them unimplemented and let `screen`'s // fallback loops fill exactly the window's bounds. fn clear(_: *Screen) anyerror!void { return error.Unimplemented; }
fn clearArea(_: *Screen, _: Rectangle) anyerror!void { return error.Unimplemented; }
fn fill(_: *Screen, _: ?Cell) anyerror!void { return error.Unimplemented; }
fn fillArea(_: *Screen, _: ?Cell, _: Rectangle) anyerror!void { return error.Unimplemented; }
fn clone(s: *Screen, _: Allocator) anyerror!Buffer { const w: *Window = @fieldParentPtr("screen", s); return (try w.buffer.cloneArea(w.window_bounds)) orelse error.UnexpectedNull; }
fn cloneArea(s: *Screen, _: Allocator, area: Rectangle) anyerror!?Buffer { const w: *Window = @fieldParentPtr("screen", s); if (area.empty()) return null; // Clip the requested area to the window's bounds. if (area.min_x < w.window_bounds.min_x or area.min_y < w.window_bounds.min_y or area.max_x > w.window_bounds.max_x or area.max_y > w.window_bounds.max_y) return error.OutOfBounds; return w.buffer.cloneArea(area); }};
/// Creates a new root window with its own buffer, sized `w`×`h`, at (0, 0)./// Mirrors uv `NewWindow(width, height, method)` (nil defaults to wcwidth).pub fn newRoot(allocator: Allocator, w: usize, h: usize, method: Method) !Window { const buf = try allocator.create(Buffer); errdefer allocator.destroy(buf); buf.* = try .init(allocator, w, h); errdefer buf.deinit(); return .{ .buffer = buf, .owns_buffer = true, .method = method, .window_bounds = .new(0, 0, w, h), };}
/// Whether the window has a parent (uv `HasParent`).pub fn hasParent(self: *const Window) bool { return self.parent != null;}
/// The parent window, or null for a root window (uv `Parent`).pub fn getParent(self: *const Window) ?*Window { return self.parent;}
/// The width-method used to size grapheme clusters (uv `WidthMethod`).pub fn widthMethod(self: *const Window) Method { return self.method;}
/// Sets the width-method (uv `SetWidthMethod`).pub fn setWidthMethod(self: *Window, method: Method) void { self.method = method;}
/// The window's positioned bounds, absolute in the buffer's space (uv/// `Bounds`).pub fn bounds(self: *const Window) Rectangle { return self.window_bounds;}
/// Moves the window so its top-left corner is at `(x, y)` (uv `MoveTo`),/// keeping its size.pub fn moveTo(self: *Window, x: usize, y: usize) void { const size_x = self.window_bounds.max_x - self.window_bounds.min_x; const size_y = self.window_bounds.max_y - self.window_bounds.min_y; self.window_bounds.min_x = x; self.window_bounds.min_y = y; self.window_bounds.max_x = x + size_x; self.window_bounds.max_y = y + size_y;}
/// Moves the window by `(dx, dy)` (uv `MoveBy`).pub fn moveBy(self: *Window, dx: usize, dy: usize) void { self.window_bounds.min_x += dx; self.window_bounds.min_y += dy; self.window_bounds.max_x += dx; self.window_bounds.max_y += dy;}
/// Resizes the window to `(w, h)`, resizing the underlying buffer only if this/// window owns it (a view's grid belongs to its parent, so only the bounds/// change). Mirrors uv `Resize`.pub fn resize(self: *Window, w: usize, h: usize) !void { if (self.owns_buffer) { try self.buffer.resize(w, h); } self.window_bounds.max_x = self.window_bounds.min_x + w; self.window_bounds.max_y = self.window_bounds.min_y + h;}
/// Reads the cell at window-local `(x, y)`, or null when out of the window's/// bounds (uv `CellAt`).pub fn cellAt(self: *Window, x: usize, y: usize) ?*Cell { if (x >= self.window_bounds.max_x - self.window_bounds.min_x or y >= self.window_bounds.max_y - self.window_bounds.min_y) return null; return self.buffer.cellAt(self.window_bounds.min_x + x, self.window_bounds.min_y + y);}
/// Writes `cell` at window-local `(x, y)`, offset by the window's origin. A/// null cell clears (uv `SetCell` with a nil cell).pub fn setCell(self: *Window, x: usize, y: usize, cell: ?Cell) void { if (x >= self.window_bounds.max_x - self.window_bounds.min_x or y >= self.window_bounds.max_y - self.window_bounds.min_y) return; self.buffer.setCell(self.window_bounds.min_x + x, self.window_bounds.min_y + y, cell);}
/// A copy of the window's cells in `area` (window coordinates) as a fresh,/// owned window with the same parent and method, bounds rebuilt at the/// window's origin but limited to `area`'s size. Mirrors uv `CloneArea`.pub fn cloneArea(self: *const Window, allocator: Allocator, area: Rectangle) !Window { const src: Rectangle = .{ .min_x = self.window_bounds.min_x + area.min_x, .min_y = self.window_bounds.min_y + area.min_y, .max_x = self.window_bounds.min_x + area.max_x, .max_y = self.window_bounds.min_y + area.max_y, }; const grid = try allocator.create(Buffer); errdefer allocator.destroy(grid); grid.* = try self.buffer.cloneArea(src) orelse return error.UnexpectedNull; errdefer grid.deinit(); return .{ .buffer = grid, .owns_buffer = true, .method = self.method, .parent = self.parent, .window_bounds = .new(self.window_bounds.min_x, self.window_bounds.min_y, area.max_x - area.min_x, area.max_y - area.min_y), };}
/// A copy of the whole window as an owned window (uv `Clone`).pub fn clone(self: *const Window, allocator: Allocator) !Window { const size_x = self.window_bounds.max_x - self.window_bounds.min_x; const size_y = self.window_bounds.max_y - self.window_bounds.min_y; return self.cloneArea(allocator, .new(0, 0, size_x, size_y));}
/// Frees the heap-allocated buffer if this window owns it (root windows and/// clones). Views and children that share a parent's buffer must not be/// deinit-ed (or only after the parent).pub fn deinit(self: *Window) void { if (self.owns_buffer) { const allocator = self.buffer.allocator; self.buffer.deinit(); allocator.destroy(self.buffer); self.owns_buffer = false; self.buffer = undefined; }}
test "root window owns its grid" { var win: Window = try newRoot(t.allocator, 4, 3, .wcwidth); defer win.deinit(); try t.expect(win.hasParent() == false); try t.expectEqual(Rectangle.new(0, 0, 4, 3), win.bounds()); try t.expectEqual(.wcwidth, win.widthMethod()); win.setCell(1, 1, .of("x", 1)); try t.expectEqualStrings("x", win.cellAt(1, 1).?.content);}
test "window interface draws via screen helpers" { var win: Window = try newRoot(t.allocator, 5, 3, .wcwidth); defer win.deinit();
try win.screen.clear(); try t.expect(win.cellAt(0, 0).?.equal(.space)); try t.expect(win.cellAt(4, 2).?.equal(.space));
try win.screen.fillArea(.of("f", 1), .new(1, 1, 2, 1)); try t.expectEqualStrings("f", win.cellAt(1, 1).?.content); try t.expectEqualStrings("f", win.cellAt(2, 1).?.content); try t.expect(win.cellAt(3, 1).?.equal(.space));
var sub = (try win.screen.cloneArea(t.allocator, .new(1, 1, 2, 1))).?; defer sub.deinit(); try t.expectEqualStrings("f", sub.cellAt(0, 0).?.content);}
test "window cell read/write offsets by its origin" { var root: Window = try newRoot(t.allocator, 8, 4, .wcwidth); defer root.deinit(); root.setCell(0, 0, .of("a", 1));
// A view into the root window at (5,7)? No: a view's absolute origin // must stay within the shared grid. A view at (0,0)-(3,2) reads the // root's top-left. var view: Window = .{ .buffer = root.buffer, .method = root.method, .parent = &root, .window_bounds = .new(0, 0, 3, 2), }; try t.expectEqual(Rectangle.new(0, 0, 3, 2), view.bounds()); try t.expectEqualStrings("a", view.cellAt(0, 0).?.content); view.setCell(1, 1, .of("b", 1)); try t.expectEqualStrings("b", root.cellAt(1, 1).?.content);
// Moving the view keeps its size; cell access still offsets by the new // origin, so it now reads the root's cells at the shifted position. view.moveTo(5, 2); try t.expectEqual(Rectangle.new(5, 2, 3, 2), view.bounds()); try t.expectEqualStrings(" ", view.cellAt(0, 0).?.content); view.setCell(0, 0, .of("c", 1)); try t.expectEqualStrings("c", root.cellAt(5, 2).?.content); view.moveBy(1, 1); try t.expectEqual(Rectangle.new(6, 3, 3, 2), view.bounds());}
test "window shared buffer view sees parent's cells" { var root: Window = try newRoot(t.allocator, 8, 4, .wcwidth); defer root.deinit(); root.setCell(2, 1, .of("v", 1));
// A view into the root window at (2,1) of size (2,1) shares its grid. var view: Window = .{ .buffer = root.buffer, .method = root.method, .parent = &root, .window_bounds = .new(2, 1, 2, 1), };
try t.expect(view.hasParent()); try t.expectEqual(Rectangle.new(2, 1, 2, 1), view.bounds()); // The view's (0,0) is the root window's cell (2,1). try t.expectEqualStrings("v", view.cellAt(0, 0).?.content);
// Writes through the view hit the shared grid at the window origin. view.setCell(1, 0, .of("w", 1)); try t.expectEqualStrings("w", root.cellAt(3, 1).?.content);}
test "window clone copies its own grid" { var win: Window = try newRoot(t.allocator, 3, 2, .wcwidth); defer win.deinit(); win.setCell(0, 0, .of("c", 1));
var copy: Window = try win.clone(t.allocator); defer copy.deinit(); _ = © try t.expectEqual(Rectangle.new(0, 0, 3, 2), copy.bounds()); try t.expectEqualStrings("c", copy.cellAt(0, 0).?.content);}