From fc72cf4abb81de1b91a09b90eb7a4960daa6dafc Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 4 Mar 2025 19:53:28 +0100 Subject: [PATCH] ref(container): split size and position calculations --- examples/demo.zig | 7 +- examples/elements/button.zig | 3 +- examples/elements/input.zig | 3 +- examples/elements/scrollable.zig | 3 +- examples/errors.zig | 3 +- examples/layouts/grid.zig | 3 +- examples/layouts/horizontal.zig | 3 +- examples/layouts/mixed.zig | 3 +- examples/layouts/vertical.zig | 3 +- examples/styles/palette.zig | 3 +- examples/styles/text.zig | 3 +- src/container.zig | 335 +++++++++++++++++-------------- src/element.zig | 7 +- src/testing.zig | 6 +- 14 files changed, 205 insertions(+), 180 deletions(-) diff --git a/examples/demo.zig b/examples/demo.zig index 6c78795..15684f3 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -92,11 +92,11 @@ pub fn main() !void { .color = .light_blue, .sides = .all, }, - .fixed_size = .{ .x = 200 }, + .size = .{ .x = 200 }, }, .{})); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, - .fixed_size = .{ .x = 30 }, + .size = .{ .x = 30 }, }, .{})); defer container.deinit(); // also de-initializes the children @@ -146,8 +146,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/elements/button.zig b/examples/elements/button.zig index b40f5b2..a5b3a9d 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -142,8 +142,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/elements/input.zig b/examples/elements/input.zig index a5b0db0..3a86832 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -156,8 +156,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index c5c2a44..6dd052a 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -178,8 +178,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/errors.zig b/examples/errors.zig index 15b2ec1..67ddaac 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -154,8 +154,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index 583bf34f..c1767a7 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -108,8 +108,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index e27f852..3ffe40e 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -100,8 +100,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index bc71ae1..5a06a21 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -112,8 +112,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index 9ceee31..46240ba 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -99,8 +99,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 686ccb7..279bcc3 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -95,8 +95,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/examples/styles/text.zig b/examples/styles/text.zig index 02a0af3..77f040c 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -148,8 +148,7 @@ pub fn main() !void { } try renderer.resize(); - container.reposition(.{}); - container.resize(renderer.size); + container.resize(.{}, renderer.size); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/src/container.zig b/src/container.zig index 4695eaa..b6b1830 100644 --- a/src/container.zig +++ b/src/container.zig @@ -572,7 +572,10 @@ pub fn Container(comptime Event: type) type { border: Border = .{}, rectangle: Rectangle = .{}, layout: Layout = .{}, - fixed_size: Point = .{}, + /// size which should be used by the `Container` + size: Point = .{}, + /// should size grow to the available space? + grow: bool = true, }; pub fn init( @@ -601,170 +604,204 @@ pub fn Container(comptime Event: type) type { try this.elements.append(element); } - pub fn reposition(this: *@This(), origin: Point) void { - log.debug("origin: .{{ .x = {d}, .y = {d} }}", .{ origin.x, origin.y }); + fn reposition(this: *@This(), origin: Point) void { + const layout = this.properties.layout; this.origin = origin; this.element.reposition(origin); + + var offset: Point = Point.add(origin, .{ + .x = layout.padding.left, + .y = layout.padding.top, + }); + + const sides = this.properties.border.sides; + if (sides.left) offset.x += 1; + if (sides.top) offset.y += 1; + + for (this.elements.items) |*child| { + child.reposition(offset); + + switch (layout.direction) { + .horizontal => offset.x += child.size.x + layout.gap, + .vertical => offset.y += child.size.y + layout.gap, + } + + if (layout.separator.enabled) switch (layout.direction) { + .horizontal => offset.x += 1, + .vertical => offset.y += 1, + }; + } } - pub fn resize(this: *@This(), size: Point) void { - log.debug("Event .size: {{ .x = {d}, .y = {d} }}", .{ size.x, size.y }); - this.size = size; - if (this.properties.fixed_size.x > 0 and size.x < this.properties.fixed_size.x) return; - if (this.properties.fixed_size.y > 0 and size.y < this.properties.fixed_size.y) return; - - this.element.resize(size); - - if (this.elements.items.len == 0) return; - + fn fit_resize(this: *@This()) Point { + // NOTE this is supposed to be a simple and easy to understand alogrithm, there are currently no optimizations done const layout = this.properties.layout; - var fixed_size_elements: u16 = 0; - var fixed_size: Point = .{}; - for (this.elements.items) |element| { - switch (layout.direction) { - .horizontal => if (element.properties.fixed_size.x > 0) { - fixed_size_elements += 1; - }, - .vertical => if (element.properties.fixed_size.y > 0) { - fixed_size_elements += 1; - }, - } - fixed_size = fixed_size.add(element.properties.fixed_size); - } - // check if the available screen is large enough - switch (layout.direction) { - .horizontal => if (fixed_size.x > size.x) return, - .vertical => if (fixed_size.y > size.y) return, - } + var size: Point = switch (layout.direction) { + .horizontal => .{ .x = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) }, + .vertical => .{ .y = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) }, + }; + size = Point.add(size, this.properties.size); + + if (this.elements.items.len > 0) switch (layout.direction) { + .horizontal => size.x += layout.padding.left + layout.padding.right, + .vertical => size.y += layout.padding.top + layout.padding.bottom, + }; + const sides = this.properties.border.sides; - const padding = layout.padding; - var gap = layout.gap; - if (layout.separator.enabled) gap += 1; + if (sides.right) size.x -|= 1; + if (sides.bottom) size.y -|= 1; - const len: u16 = @truncate(this.elements.items.len); - const element_x = blk: { - var x = size.x - fixed_size.x - gap * (len - 1); - if (sides.left) x -= 1; - if (sides.right) x -= 1; - x -= padding.left + padding.right; - if (fixed_size_elements == len) break :blk 0; - if (fixed_size_elements == 0) { - break :blk @divTrunc(x, len); - } else { - break :blk @divTrunc(x, len - fixed_size_elements); - } - }; - const element_y = blk: { - var y = size.y - fixed_size.y - gap * (len - 1); - if (sides.top) y -= 1; - if (sides.bottom) y -= 1; - y -= padding.top + padding.bottom; - if (fixed_size_elements == len) break :blk 0; - if (fixed_size_elements == 0) { - break :blk @divTrunc(y, len); - } else { - break :blk @divTrunc(y, len - fixed_size_elements); - } - }; - var offset: u16 = switch (layout.direction) { - .horizontal => padding.left, - .vertical => padding.top, - }; - var overflow = switch (layout.direction) { - .horizontal => blk: { - var x = size.x - fixed_size.x - gap * (len - 1); - if (sides.left) x -= 1; - if (sides.right) x -= 1; - x -= padding.left + padding.right; - if (fixed_size_elements == len) break :blk 0; - if (fixed_size_elements == 0) { - break :blk x - element_x * len; - } else { - break :blk x - element_x * (len - fixed_size_elements); - } - }, - .vertical => blk: { - var y = size.y - fixed_size.y - gap * (len - 1); - if (sides.top) y -= 1; - if (sides.bottom) y -= 1; - y -= padding.top + padding.bottom; - if (fixed_size_elements == len) break :blk 0; - if (fixed_size_elements == 0) { - break :blk y - element_y * len; - } else { - break :blk y - element_y * (len - fixed_size_elements); - } - }, + if (layout.separator.enabled) switch (layout.direction) { + .horizontal => size.x += @as(u16, @truncate(this.elements.items.len -| 1)), + .vertical => size.y += @as(u16, @truncate(this.elements.items.len -| 1)), }; - for (this.elements.items) |*element| { - var element_size: Point = undefined; - var element_origin: Point = undefined; + for (this.elements.items) |*child| { + const child_size = child.fit_resize(); switch (layout.direction) { .horizontal => { - // TODO this should not always be the max size property! - var x = blk: { - if (element.properties.fixed_size.x > 0) break :blk element.properties.fixed_size.x; - break :blk element_x; - }; - if (overflow > 0) { - overflow -|= 1; - x += 1; - } - element_origin = .{ - .x = this.origin.x + offset, - .y = this.origin.y, - }; - element_size = .{ - .x = x, - .y = size.y, - }; - // border - if (sides.top) element_size.y -= 1; - if (sides.bottom) element_size.y -= 1; - // padding - element_origin.y += padding.top; - element_size.y -= padding.top + padding.bottom; - // gap - offset += gap; - offset += x; + size.x += child_size.x; + size.y = @max(size.y, child_size.y); }, .vertical => { - var y = blk: { - if (element.properties.fixed_size.y > 0) break :blk element.properties.fixed_size.y; - break :blk element_y; - }; - if (overflow > 0) { - overflow -|= 1; - y += 1; - } - element_origin = .{ - .x = this.origin.x, - .y = this.origin.y + offset, - }; - element_size = .{ - .x = size.x, - .y = y, - }; - // border - if (sides.left) element_size.x -= 1; - if (sides.right) element_size.x -= 1; - // padding - element_origin.x += padding.left; - element_size.x -= padding.left + padding.right; - // gap - offset += gap; - offset += y; + size.x = @max(size.x, child_size.x); + size.y += child_size.y; }, } - - // border resizing - if (sides.top) element_origin.y += 1; - if (sides.left) element_origin.x += 1; - - element.reposition(element_origin); - element.resize(element_size); } + + // assign currently calculated size + this.size = size; + return size; + } + + // growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen + fn grow_resize(this: *@This()) void { + const layout = this.properties.layout; + var remainder = switch (layout.direction) { + .horizontal => this.size.x -| (layout.padding.left + layout.padding.right), + .vertical => this.size.y -| (layout.padding.top + layout.padding.bottom), + }; + remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)); + + if (layout.separator.enabled) remainder -|= @as(u16, @truncate(this.elements.items.len -| 1)); + + var available = switch (layout.direction) { + .horizontal => this.size.y -| (layout.padding.top + layout.padding.bottom), + .vertical => this.size.x -| (layout.padding.left + layout.padding.right), + }; + + const sides = this.properties.border.sides; + switch (layout.direction) { + .horizontal => { + if (sides.bottom) { + available -|= 1; + remainder -|= 1; + } + if (sides.top) { + available -|= 1; + remainder -|= 1; + } + }, + .vertical => { + if (sides.right) { + available -|= 1; + remainder -|= 1; + } + if (sides.left) { + available -|= 1; + remainder -|= 1; + } + }, + } + + for (this.elements.items) |child| { + remainder -|= switch (layout.direction) { + .horizontal => child.size.x, + .vertical => child.size.y, + }; + } + + var growable_children: usize = 0; + var first_growable_child: *@This() = undefined; + for (this.elements.items) |*child| { + if (child.properties.grow) { + if (growable_children == 0) first_growable_child = child; + growable_children += 1; + + switch (layout.direction) { + .horizontal => child.size.y = available, + .vertical => child.size.x = available, + } + } + } + + while (growable_children > 0 and remainder > 0) { + var smallest_size = switch (layout.direction) { + .horizontal => first_growable_child.size.x, + .vertical => first_growable_child.size.y, + }; + var second_smallest_size: u16 = std.math.maxInt(u16); + var size_to_correct = remainder; + + for (this.elements.items) |child| { + if (!child.properties.grow) continue; + + const size = switch (layout.direction) { + .horizontal => child.size.x, + .vertical => child.size.y, + }; + + if (size < smallest_size) { + second_smallest_size = smallest_size; + smallest_size = size; + } else if (size > smallest_size) { + second_smallest_size = @min(size, second_smallest_size); + size_to_correct = second_smallest_size -| smallest_size; + } + } + + size_to_correct = @min(size_to_correct, remainder / growable_children); + var overflow: u16 = 0; + if (size_to_correct == 0 and remainder > 0) { + // there is some overflow + overflow = remainder; + } + for (this.elements.items) |*child| { + const child_size = switch (layout.direction) { + .horizontal => child.size.x, + .vertical => child.size.y, + }; + if (child.properties.grow and child_size == smallest_size) { + switch (layout.direction) { + .horizontal => child.size.x += size_to_correct, + .vertical => child.size.y += size_to_correct, + } + if (overflow > 0) { + switch (layout.direction) { + .horizontal => child.size.x += 1, + .vertical => child.size.y += 1, + } + overflow -|= 1; + remainder -|= 1; + } + remainder -|= size_to_correct; + } + } + } + + this.element.resize(this.size); + for (this.elements.items) |*child| child.grow_resize(); + } + + pub fn resize(this: *@This(), origin: Point, size: Point) void { + // NOTE assume that this function is only called for the root `Container` + this.size = size; // total screen size; + const fit_size = this.fit_resize(); + this.size = size; // total screen size + _ = fit_size; + this.grow_resize(); + this.reposition(origin); } pub fn handle(this: *@This(), event: Event) !void { diff --git a/src/element.zig b/src/element.zig index 6d7f435..32d5cdf 100644 --- a/src/element.zig +++ b/src/element.zig @@ -107,8 +107,7 @@ pub fn Scrollable(Event: type) type { this.size = size; // TODO scrollbar space - depending on configuration and only if necessary? this.container_size = Point.max(size, this.min_size); - this.container_origin = size; // TODO the size should be a provided origin - this.container.resize(this.container_size); + this.container.resize(.{}, this.container_size); } fn reposition(ctx: *anyopaque, origin: Point) void { @@ -247,7 +246,7 @@ test "scrollable vertical" { var renderer: testing.Renderer = .init(allocator, size); defer renderer.deinit(); - container.resize(size); + container.resize(.{}, size); try renderer.render(Container(event.SystemEvent), &container); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen); @@ -321,7 +320,7 @@ test "scrollable horizontal" { var renderer: testing.Renderer = .init(allocator, size); defer renderer.deinit(); - container.resize(size); + container.resize(.{}, size); try renderer.render(Container(event.SystemEvent), &container); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen); diff --git a/src/testing.zig b/src/testing.zig index 2971429..e20e17f 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -34,7 +34,7 @@ pub const Renderer = struct { pub fn resize(this: *@This(), size: Point) !void { this.size = size; - const n = @as(usize, size.cols) * @as(usize, size.y); + const n = @as(usize, size.x) * @as(usize, size.y); this.allocator.free(this.screen); this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory."); @@ -120,8 +120,8 @@ pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEven var renderer: Renderer = .init(allocator, size); defer renderer.deinit(); - container.reposition(.{}); - container.resize(size); + try renderer.resize(size); + container.resize(.{}, size); try renderer.render(Container(event.SystemEvent), container); try expectEqualCells(.{}, renderer.size, expected, renderer.screen);