From 09a659ba70955d02ac1c865784d5e509081d3d2b Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sat, 15 Feb 2025 09:55:30 +0100 Subject: [PATCH] ref(event): split resize event into viewport and size event Viewport event reflects the absolut position and size of a given container (and propagates them to its children). While the size event propagates the content size to its children (and sets their corresponding member values accordingly). Both events are currently only emitted by `Container`s meaning that they don't need to be part of the event loop and that they might be removed later. --- README.md | 11 ++ examples/container.zig | 8 +- src/container.zig | 366 ++++++++++++++++++++++++++--------------- src/event.zig | 4 + 4 files changed, 250 insertions(+), 139 deletions(-) diff --git a/README.md b/README.md index 0b22e58..5ab239a 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,17 @@ cells of the content (and may be overwritten by child elements contents). The border of an element should be around independent of the scrolling of the contents, just like padding. +### Positioning and Rendering + +I need to find a way to render the contents of each container independently on the screen. Currently the application has two (yet still married) sizes which it keeps track of: + + - **viewport**: The `Size` of the content on screen. It contains the information about the location (*anchor*) where on the screen and the size of the content to show. + - **size**: The actual size of the contents for a given container. This `Size` does contain information about the total content size (rows & cols which may not be equal to the screen space available (i.e. the *viewport*)) and the current *anchor* for the viewport of the contents. + +Manipulating the *size* of a container has implications on the *viewport* of the child elements which actually causes problems when enlarging containers (children lose correct screen location information). + +Should I split the *anchor* and the *size* information from another? + ### Input How is the user input handled in the containers? Should there be active diff --git a/examples/container.zig b/examples/container.zig index 62c6dee..a8bf3c9 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -32,14 +32,14 @@ pub fn main() !void { }, .layout = .{ .padding = .all(5), - .direction = .vertical, + .direction = .horizontal, }, }); var box = try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .layout = .{ .gap = 1, - .direction = .horizontal, + .direction = .vertical, .padding = .vertical(1), }, }); @@ -92,9 +92,7 @@ pub fn main() !void { }); } }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg }); - }, + .err => |err| log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, } diff --git a/src/container.zig b/src/container.zig index 596a1db..586964a 100644 --- a/src/container.zig +++ b/src/container.zig @@ -240,7 +240,7 @@ pub fn Container(comptime Event: type) type { viewport: Size, /// Size of the contents columns and rows used for the contents of this `Container` /// The anchor of this `Size` corresponds to the contents inside of itself. - size: Size = .{}, + size: Size, properties: Properties, elements: std.ArrayList(@This()), @@ -258,6 +258,7 @@ pub fn Container(comptime Event: type) type { return .{ .allocator = allocator, .viewport = .{}, + .size = .{}, .properties = properties, .elements = std.ArrayList(@This()).init(allocator), }; @@ -277,137 +278,241 @@ pub fn Container(comptime Event: type) type { pub fn handle(this: *@This(), event: Event) !void { 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; - - const size: Size = .{ - .cols = s.cols, - .rows = s.rows, - }; - this.size = size; - - if (this.elements.items.len == 0) break :resize; - - const separator = this.properties.border.separator; - const sides = this.properties.border.sides; - const padding = this.properties.layout.padding; - var gap = this.properties.layout.gap; - if (separator.enabled) gap += 1; // the gap will be used for the rendering of the separator line - - const len: u16 = @truncate(this.elements.items.len); - const element_cols = blk: { - var cols = size.cols - gap * (len - 1); - if (sides.left) cols -= 1; - if (sides.right) cols -= 1; - cols -= padding.left + padding.right; - break :blk @divTrunc(cols, len); - }; - const element_rows = blk: { - var rows = size.rows - gap * (len - 1); - if (sides.top) rows -= 1; - if (sides.bottom) rows -= 1; - rows -= padding.top + padding.bottom; - break :blk @divTrunc(rows, len); - }; - var offset: u16 = switch (this.properties.layout.direction) { - .horizontal => padding.left, - .vertical => padding.top, - }; - var overflow = switch (this.properties.layout.direction) { - .horizontal => blk: { - var cols = size.cols - gap * (len - 1); - if (sides.left) cols -= 1; - if (sides.right) cols -= 1; - cols -= padding.left + padding.right; - break :blk cols - element_cols * len; - }, - .vertical => blk: { - var rows = size.rows - gap * (len - 1); - if (sides.top) rows -= 1; - if (sides.bottom) rows -= 1; - rows -= padding.top + padding.bottom; - break :blk rows - element_rows * len; - }, - }; - // TODO: make sure that items cannot underflow in size! - // - make their size and position still according (even if outside of the visible space!) - // - don't render them then accordingly -> avoid index out of bounce accesses! - for (this.elements.items) |*element| { - var element_size: Size = undefined; - switch (this.properties.layout.direction) { - .horizontal => { - var cols = element_cols; - if (overflow > 0) { - overflow -|= 1; - cols += 1; - } - element_size = .{ - .anchor = .{ - .col = this.viewport.anchor.col + offset, - .row = this.viewport.anchor.row, - }, - .cols = cols, - .rows = size.rows, - }; - // border - if (sides.top) element_size.rows -= 1; - if (sides.bottom) element_size.rows -= 1; - // padding - element_size.anchor.row += padding.top; - element_size.rows -= padding.top + padding.bottom; - // gap - offset += gap; - offset += cols; - }, - .vertical => { - var rows = element_rows; - if (overflow > 0) { - overflow -|= 1; - rows += 1; - } - element_size = .{ - .anchor = .{ - .col = this.viewport.anchor.col, - .row = this.viewport.anchor.row + offset, - }, - .cols = size.cols, - .rows = rows, - }; - // border - if (sides.left) element_size.cols -= 1; - if (sides.right) element_size.cols -= 1; - // padding - element_size.anchor.col += padding.left; - element_size.cols -= padding.left + padding.right; - // gap - offset += gap; - offset += rows; - }, - } - - // border resizing - if (sides.top) { - element_size.anchor.row += 1; - } - if (sides.left) { - element_size.anchor.col += 1; - } - - // padding resizing - - try element.handle(.{ .resize = element_size }); - } + .resize => |size| { + // initial resize event (handled by the root container) + try this.content_layout(size); + try this.viewport_layout(size); }, + .size => |size| try this.content_layout(size), + .viewport => |size| try this.viewport_layout(size), else => for (this.elements.items) |*element| try element.handle(event), } } + fn viewport_layout(this: *@This(), size: Size) anyerror!void { + // NOTE: do not manipulate the values here! + log.debug("Event .viewport: .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + size.anchor.col, + size.anchor.row, + size.cols, + size.rows, + }); + this.viewport = size; + + // without child elements no size information needs to be propagated further + if (this.elements.items.len == 0) return; + + const separator = this.properties.border.separator; + const sides = this.properties.border.sides; + const padding = this.properties.layout.padding; + + var gap = this.properties.layout.gap; + if (separator.enabled) gap += 1; // the gap will be used for the rendering of the separator line + + const len: u16 = @truncate(this.elements.items.len); + const element_cols = blk: { + var cols = size.cols - gap * (len - 1); + if (sides.left) cols -= 1; + if (sides.right) cols -= 1; + cols -= padding.left + padding.right; + break :blk @divTrunc(cols, len); + }; + const element_rows = blk: { + var rows = size.rows - gap * (len - 1); + if (sides.top) rows -= 1; + if (sides.bottom) rows -= 1; + rows -= padding.top + padding.bottom; + break :blk @divTrunc(rows, len); + }; + var offset: u16 = switch (this.properties.layout.direction) { + .horizontal => padding.left, + .vertical => padding.top, + }; + var overflow = switch (this.properties.layout.direction) { + .horizontal => blk: { + var cols = size.cols - gap * (len - 1); + if (sides.left) cols -= 1; + if (sides.right) cols -= 1; + cols -= padding.left + padding.right; + break :blk cols - element_cols * len; + }, + .vertical => blk: { + var rows = size.rows - gap * (len - 1); + if (sides.top) rows -= 1; + if (sides.bottom) rows -= 1; + rows -= padding.top + padding.bottom; + break :blk rows - element_rows * len; + }, + }; + // TODO: make sure that items cannot underflow in size! + // - make their size and position still according (even if outside of the visible space!) + // - don't render them then accordingly -> avoid index out of bounce accesses! + for (this.elements.items) |*element| { + var element_size: Size = undefined; + switch (this.properties.layout.direction) { + .horizontal => { + var cols = element_cols; + if (overflow > 0) { + overflow -|= 1; + cols += 1; + } + element_size = .{ + .anchor = .{ + .col = size.anchor.col + offset, + .row = size.anchor.row, + }, + .cols = cols, + .rows = size.rows, + }; + // border + if (sides.top) element_size.rows -= 1; + if (sides.bottom) element_size.rows -= 1; + // padding + element_size.anchor.row += padding.top; + element_size.rows -= padding.top + padding.bottom; + // gap + offset += gap; + offset += cols; + }, + .vertical => { + var rows = element_rows; + if (overflow > 0) { + overflow -|= 1; + rows += 1; + } + element_size = .{ + .anchor = .{ + .col = size.anchor.col, + .row = size.anchor.row + offset, + }, + .cols = size.cols, + .rows = rows, + }; + // border + if (sides.left) element_size.cols -= 1; + if (sides.right) element_size.cols -= 1; + // padding + element_size.anchor.col += padding.left; + element_size.cols -= padding.left + padding.right; + // gap + offset += gap; + offset += rows; + }, + } + + // border resizing + if (sides.top) element_size.anchor.row += 1; + if (sides.left) element_size.anchor.col += 1; + + try element.handle(.{ .viewport = element_size }); + } + } + + fn content_layout(this: *@This(), s: Size) anyerror!void { + // TODO: overwrite size with configuration values (i.e. fixed, min size, max size, etc.) + const size = s; + log.debug("Event .size: .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + size.anchor.col, + size.anchor.row, + size.cols, + size.rows, + }); + this.size = size; + + // without child elements no size information needs to be propagated further + if (this.elements.items.len == 0) return; + + const separator = this.properties.border.separator; + const sides = this.properties.border.sides; + const padding = this.properties.layout.padding; + + var gap = this.properties.layout.gap; + if (separator.enabled) gap += 1; // the gap will be used for the rendering of the separator line + + const len: u16 = @truncate(this.elements.items.len); + const element_cols = blk: { + var cols = size.cols - gap * (len - 1); + if (sides.left) cols -= 1; + if (sides.right) cols -= 1; + cols -= padding.left + padding.right; + break :blk @divTrunc(cols, len); + }; + const element_rows = blk: { + var rows = size.rows - gap * (len - 1); + if (sides.top) rows -= 1; + if (sides.bottom) rows -= 1; + rows -= padding.top + padding.bottom; + break :blk @divTrunc(rows, len); + }; + var offset: u16 = switch (this.properties.layout.direction) { + .horizontal => padding.left, + .vertical => padding.top, + }; + var overflow = switch (this.properties.layout.direction) { + .horizontal => blk: { + var cols = size.cols - gap * (len - 1); + if (sides.left) cols -= 1; + if (sides.right) cols -= 1; + cols -= padding.left + padding.right; + break :blk cols - element_cols * len; + }, + .vertical => blk: { + var rows = size.rows - gap * (len - 1); + if (sides.top) rows -= 1; + if (sides.bottom) rows -= 1; + rows -= padding.top + padding.bottom; + break :blk rows - element_rows * len; + }, + }; + // TODO: make sure that items cannot underflow in size! + // - make their size and position still according (even if outside of the visible space!) + // - don't render them then accordingly -> avoid index out of bounce accesses! + for (this.elements.items) |*element| { + var element_size: Size = undefined; + switch (this.properties.layout.direction) { + .horizontal => { + var cols = element_cols; + if (overflow > 0) { + overflow -|= 1; + cols += 1; + } + element_size = .{ + .cols = cols, + .rows = size.rows, + }; + // border + if (sides.top) element_size.rows -= 1; + if (sides.bottom) element_size.rows -= 1; + // padding + element_size.rows -= padding.top + padding.bottom; + // gap + offset += gap; + offset += cols; + }, + .vertical => { + var rows = element_rows; + if (overflow > 0) { + overflow -|= 1; + rows += 1; + } + element_size = .{ + .cols = size.cols, + .rows = rows, + }; + // border + if (sides.left) element_size.cols -= 1; + if (sides.right) element_size.cols -= 1; + // padding + element_size.cols -= padding.left + padding.right; + // gap + offset += gap; + offset += rows; + }, + } + try element.handle(.{ .size = element_size }); + } + } + pub fn contents(this: *const @This()) ![]const Cell { const content_cells = try this.allocator.alloc(Cell, @as(usize, this.size.cols) * @as(usize, this.size.rows)); defer this.allocator.free(content_cells); @@ -419,13 +524,6 @@ pub fn Container(comptime Event: type) type { this.properties.border.contents(content_cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); this.properties.rectangle.contents(content_cells, this.size); - log.debug("Content::contents .scroll.size: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ - this.size.anchor.col, - this.size.anchor.row, - this.size.cols, - this.size.rows, - }); - const cols = blk: { var cols: u16 = this.size.cols - this.size.anchor.col; if (cols > this.viewport.cols) { diff --git a/src/event.zig b/src/event.zig index 7824afb..200f75b 100644 --- a/src/event.zig +++ b/src/event.zig @@ -21,6 +21,10 @@ pub const SystemEvent = union(enum) { }, /// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in resize: Size, + /// `Size` event emitted by `Container`'s to provide child elements their appropriate viewport information + viewport: Size, + /// `Size` event emitted by `Container`'s to provide child elements with their available content size + size: Size, /// Input key event received from the user key: Key, /// Focus event for mouse interaction