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