From bbe6f4741e4dcdafbfa2ea770932b4e50a7023be Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Wed, 12 Feb 2025 22:33:03 +0100 Subject: [PATCH] WIP: use viewport to allow sizes of scroll to extend further than renderable screen --- examples/container.zig | 12 +++-- src/app.zig | 2 +- src/cell.zig | 1 + src/container.zig | 111 +++++++++++++++++++++++++++++++---------- src/event.zig | 2 +- src/render.zig | 12 ++--- src/size.zig | 12 ++--- src/terminal.zig | 5 +- src/zterm.zig | 13 +++-- 9 files changed, 119 insertions(+), 51 deletions(-) diff --git a/examples/container.zig b/examples/container.zig index e945f38..81babee 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -31,17 +31,19 @@ pub fn main() !void { .separator = .{ .enabled = false }, }, .layout = .{ - // .gap = 1, - .sizing = .{ - .width = .{ .percent = 50 }, - .height = .{ .percent = 50 }, - }, + .gap = 1, .padding = .all(5), .direction = .horizontal, }, }); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, + .layout = .{ + .sizing = .{ + .width = .{ .fixed = 70 }, + .height = .{ .fixed = 18 }, + }, + }, })); try container.append(try App.Container.init(allocator, .{ .border = .{ .color = .light_blue, .corners = .squared }, diff --git a/src/app.zig b/src/app.zig index 2762e09..13a72d9 100644 --- a/src/app.zig +++ b/src/app.zig @@ -7,7 +7,7 @@ const mergeTaggedUnions = event.mergeTaggedUnions; const isTaggedUnion = event.isTaggedUnion; const Key = @import("key.zig"); -const Size = @import("size.zig"); +const Size = @import("size.zig").Size; const Queue = @import("queue.zig").Queue; const log = std.log.scoped(.app); diff --git a/src/cell.zig b/src/cell.zig index 4aecd83..f0a4541 100644 --- a/src/cell.zig +++ b/src/cell.zig @@ -4,6 +4,7 @@ const Style = @import("style.zig"); pub const Cell = @This(); style: Style = .{ .attributes = &.{} }, +// TODO: embrace `zg` dependency more due to utf-8 encoding cp: u21 = ' ', pub fn eql(this: Cell, other: Cell) bool { diff --git a/src/container.zig b/src/container.zig index a0dbf28..76f49c4 100644 --- a/src/container.zig +++ b/src/container.zig @@ -4,13 +4,13 @@ const isTaggedUnion = @import("event.zig").isTaggedUnion; const Cell = @import("cell.zig"); const Color = @import("color.zig").Color; -const Size = @import("size.zig"); +const Size = @import("size.zig").Size; const Style = @import("style.zig"); const log = std.log.scoped(.container); /// Border configuration struct -pub const Border = struct { +pub const Border = packed struct { pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; /// Color to use for the border @@ -40,10 +40,10 @@ pub const Border = struct { } } = .{}, /// Configure separator borders between child element to added to the layout - separator: struct { + separator: packed struct { enabled: bool = false, color: Color = .white, - line: enum { + line: enum(u1) { line, dotted, // TODO: add more variations which could be used for the separator @@ -52,7 +52,7 @@ pub const Border = struct { // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` pub fn contents(this: @This(), cells: []Cell, size: Size, layout: Layout, len: u16) void { - std.debug.assert(cells.len == size.cols * size.rows); + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); const frame = switch (this.corners) { .rounded => Border.rounded_border, @@ -62,7 +62,7 @@ pub const Border = struct { // render top and bottom border for (0..size.cols) |col| { - const last_row = (size.rows - 1) * size.cols; + const last_row = @as(usize, size.rows - 1) * @as(usize, size.cols); if (this.sides.left and col == 0) { // top left corner if (this.sides.top) cells[col].cp = frame[0]; @@ -170,7 +170,7 @@ pub const Border = struct { }; /// Rectangle configuration struct -pub const Rectangle = struct { +pub const Rectangle = packed struct { /// `Color` to use to fill the `Rectangle` with /// NOTE: used as background color when rendering! such that it renders the /// children accordingly without removing the coloring of the `Rectangle` @@ -178,7 +178,7 @@ pub const Rectangle = struct { // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` pub fn contents(this: @This(), cells: []Cell, size: Size) void { - std.debug.assert(cells.len == size.cols * size.rows); + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); for (0..size.rows) |row| { for (0..size.cols) |col| { @@ -200,8 +200,21 @@ pub const Scroll = packed struct { // TODO: rendering enhancements: // - render corresponding scroll-bars? + + // TODO: does the `size` then actually need to be part of the `Scroll`, as the user should not tamper with the value anyway.. + + // TODO: should the container size be 'virtual' i.e. the complete + // - content size (which might be larger than the available screen) + // but how would a scrollable small portion of the screen work? + // -> this means that the size can remain and have its meaning, but the + // content needs another abstraction for the scroll + size: Size = .{}, + // anchor => position in view of scrollable contents + // cols / rows => view port dimensions + // render => window in window }; +// TODO: can `Layout` become `packed`? -> for that the sizing cannot be a tagged enum! /// Layout configuration struct pub const Layout = struct { /// control the direction in which child elements are laid out @@ -240,8 +253,8 @@ pub const Layout = struct { // NOTE: `sizing` cannot be *packed* because of the tagged unions? is this necessary -> I would need to measure the size differences /// Sizing to be used for the width and height of this element to use sizing: struct { - width: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit, - height: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit, + width: union(enum(u2)) { fit, grow, fixed: u16, percent: u16 } = .fit, + height: union(enum(u2)) { fit, grow, fixed: u16, percent: u16 } = .fit, } = .{}, }; @@ -251,7 +264,8 @@ pub fn Container(comptime Event: type) type { } return struct { allocator: std.mem.Allocator, - size: Size, + /// Size of actual columns and rows used to render this `Container` + viewport: Size, properties: Properties, elements: std.ArrayList(@This()), @@ -268,7 +282,7 @@ pub fn Container(comptime Event: type) type { pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() { return .{ .allocator = allocator, - .size = .{ .cols = 0, .rows = 0 }, + .viewport = .{}, .properties = properties, .elements = std.ArrayList(@This()).init(allocator), }; @@ -289,8 +303,16 @@ pub fn Container(comptime Event: type) type { switch (event) { .init => log.debug(".init event", .{}), .resize => |s| resize: { + log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + s.anchor.col, + s.anchor.row, + s.cols, + s.rows, + }); + this.viewport = s; // sizing var size = s; + size.anchor = .{}; // reset relative anchor of size! const sizing = this.properties.layout.sizing; switch (sizing.width) { .fit => { @@ -301,7 +323,7 @@ pub fn Container(comptime Event: type) type { // NOTE: this is pretty much the current implementation }, .fixed => |fix| { - std.debug.assert(fix <= size.cols); + // NOTE: fixed may now even define a larger column / row span for a container (but not everything might be rendered) size.cols = fix; }, .percent => |percent| { @@ -312,13 +334,16 @@ pub fn Container(comptime Event: type) type { switch (sizing.height) { .fit => { // use as much space as necessary (but nothing more than necessary) + // TODO: I need to work out the representation between the + // - `Size` and `Size.Position` with the actual screen size + // - the virtual screen may be larger than the actual screen (can I do this?) }, .grow => { // grow use as much space as available by the parent (i.e. the entire width) // NOTE: this is pretty much the current implementation }, .fixed => |fix| { - std.debug.assert(fix <= size.rows); + // NOTE: fixed may now even define a larger column / row span for a container (but not everything might be rendered) size.rows = fix; }, .percent => |percent| { @@ -326,14 +351,7 @@ pub fn Container(comptime Event: type) type { size.rows = @divTrunc(size.rows * percent, 100); }, } - - log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ - size.anchor.col, - size.anchor.row, - size.cols, - size.rows, - }); - this.size = size; + this.properties.scroll.size = size; if (this.elements.items.len == 0) break :resize; @@ -446,10 +464,51 @@ pub fn Container(comptime Event: type) type { } pub fn contents(this: *const @This()) ![]const Cell { - const cells = try this.allocator.alloc(Cell, this.size.cols * this.size.rows); - @memset(cells, .{}); // reset all cells - this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); - this.properties.rectangle.contents(cells, this.size); + const content = try this.allocator.alloc(Cell, @as(usize, this.properties.scroll.size.cols) * @as(usize, this.properties.scroll.size.rows)); + defer this.allocator.free(content); + @memset(content, .{}); + + const cells = try this.allocator.alloc(Cell, @as(usize, this.viewport.cols) * @as(usize, this.viewport.rows)); + @memset(cells, .{}); + + this.properties.border.contents(content, this.properties.scroll.size, this.properties.layout, @truncate(this.elements.items.len)); + this.properties.rectangle.contents(content, this.properties.scroll.size); + + const cols = blk: { + var cols: u16 = this.properties.scroll.size.cols; + if (this.properties.scroll.size.cols > this.viewport.cols) { + cols = this.viewport.cols; + } + break :blk cols; + }; + const rows = blk: { + var rows: u16 = this.properties.scroll.size.rows; + if (this.properties.scroll.size.rows > this.viewport.rows) { + rows = this.viewport.rows; + } + break :blk rows; + }; + const anchor = (this.properties.scroll.size.anchor.row * this.properties.scroll.size.cols) + this.properties.scroll.size.anchor.col; + log.debug("viewport = .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + this.viewport.anchor.col, + this.viewport.anchor.row, + this.viewport.cols, + this.viewport.rows, + }); + log.debug("size = .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + this.properties.scroll.size.anchor.col, + this.properties.scroll.size.anchor.row, + this.properties.scroll.size.cols, + this.properties.scroll.size.rows, + }); + for (0..rows) |row| { + for (0..cols) |col| { + // TODO: try to do this with @memcpy instead to improve performance + const cell = content[anchor + (row * this.properties.scroll.size.cols) + col]; + cells[row * this.viewport.cols + col].cp = cell.cp; + cells[row * this.viewport.cols + col].style = cell.style; + } + } return cells; } }; diff --git a/src/event.zig b/src/event.zig index ec80c05..7824afb 100644 --- a/src/event.zig +++ b/src/event.zig @@ -3,7 +3,7 @@ const std = @import("std"); const terminal = @import("terminal.zig"); -const Size = @import("size.zig"); +const Size = @import("size.zig").Size; const Key = @import("key.zig"); /// System events available to every `zterm.App` diff --git a/src/render.zig b/src/render.zig index 3d9c9a1..424b983 100644 --- a/src/render.zig +++ b/src/render.zig @@ -2,8 +2,8 @@ const std = @import("std"); const terminal = @import("terminal.zig"); const Cell = @import("cell.zig"); -const Size = @import("size.zig"); -const Position = Size.Position; +const Position = @import("size.zig").Position; +const Size = @import("size.zig").Size; /// Double-buffered intermediate rendering pipeline pub const Buffered = struct { @@ -62,17 +62,17 @@ pub const Buffered = struct { /// Render provided cells at size (anchor and dimension) into the *virtual screen*. pub fn render(this: *@This(), comptime T: type, container: *T) !void { - const size: Size = container.size; + const viewport: Size = container.viewport; const cells: []const Cell = try container.contents(); if (cells.len == 0) return; var idx: usize = 0; var vs = this.virtual_screen; - const anchor = (size.anchor.row * this.size.cols) + size.anchor.col; + const anchor = (viewport.anchor.row * this.size.cols) + viewport.anchor.col; - blk: for (0..size.rows) |row| { - for (0..size.cols) |col| { + blk: for (0..viewport.rows) |row| { + for (0..viewport.cols) |col| { const cell = cells[idx]; idx += 1; diff --git a/src/size.zig b/src/size.zig index 7e7278f..0ada6dc 100644 --- a/src/size.zig +++ b/src/size.zig @@ -1,10 +1,10 @@ -pub const Size = @This(); +pub const Size = packed struct { + anchor: Position = .{}, + cols: u16 = 0, + rows: u16 = 0, +}; -pub const Position = struct { +pub const Position = packed struct { col: u16 = 0, row: u16 = 0, }; - -anchor: Position = .{}, -cols: u16, -rows: u16, diff --git a/src/terminal.zig b/src/terminal.zig index 4bebad3..c7bfe7f 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -2,7 +2,8 @@ const std = @import("std"); pub const code_point = @import("code_point"); const Key = @import("key.zig"); -const Size = @import("size.zig"); +const Position = @import("size.zig").Position; +const Size = @import("size.zig").Size; const Cell = @import("cell.zig"); const log = std.log.scoped(.terminal); @@ -78,7 +79,7 @@ pub fn writer() Writer { return .{ .context = .{} }; } -pub fn setCursorPosition(pos: Size.Position) !void { +pub fn setCursorPosition(pos: Position) !void { var buf: [64]u8 = undefined; const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col }); _ = try std.posix.write(std.posix.STDIN_FILENO, value); diff --git a/src/zterm.zig b/src/zterm.zig index 7bb0018..c30f339 100644 --- a/src/zterm.zig +++ b/src/zterm.zig @@ -1,5 +1,9 @@ +// private imports const container = @import("container.zig"); +const color = @import("color.zig"); +const size = @import("size.zig"); +// public exports pub const App = @import("app.zig").App; pub const Renderer = @import("render.zig"); @@ -10,18 +14,19 @@ pub const Scroll = container.Scroll; pub const Layout = container.Layout; pub const Cell = @import("cell.zig"); -pub const Color = @import("color.zig").Color; +pub const Color = color.Color; pub const Key = @import("key.zig"); -pub const Size = @import("size.zig"); +pub const Size = size.Size; pub const Style = @import("style.zig"); test { _ = @import("terminal.zig"); _ = @import("queue.zig"); + _ = color; + _ = size; + _ = Cell; - _ = Color; _ = Key; - _ = Size; _ = Style; }