//! 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); }