From 466e00c16c52b0e5ff21945f7c505c97ba7b2874 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 4 Mar 2025 21:54:07 +0100 Subject: [PATCH] fix(element/scrollable): support deriving `Container` size of scrollable --- examples/demo.zig | 9 ++++----- examples/elements/scrollable.zig | 12 +++++++++--- src/container.zig | 8 ++++---- src/element.zig | 32 ++++++++------------------------ 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/examples/demo.zig b/examples/demo.zig index 15684f3..3548074 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -59,7 +59,9 @@ pub fn main() !void { .layout = .{ .gap = 1, .padding = .vertical(2), + .direction = .vertical, }, + .size = .{ .y = 90 }, }, .{}); try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, @@ -72,10 +74,7 @@ pub fn main() !void { }, .{})); defer box.deinit(); - var scrollable: App.Scrollable = .{ - .container = box, - .min_size = .{ .x = 60 }, - }; + var scrollable: App.Scrollable = .{ .container = box }; var container = try App.Container.init(allocator, .{ .layout = .{ @@ -92,7 +91,7 @@ pub fn main() !void { .color = .light_blue, .sides = .all, }, - .size = .{ .x = 200 }, + .size = .{ .x = 100 }, }, .{})); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index 6dd052a..a8ab113 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -88,19 +88,25 @@ pub fn main() !void { var top_box = try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .layout = .{ - .gap = 1, + .gap = 2, + .separator = .{ + .enabled = true, + }, .direction = .vertical, .padding = .vertical(1), }, }, .{}); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, + .size = .{ .y = 30 }, }, .{})); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, + .size = .{ .y = 5 }, }, element)); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, + .size = .{ .y = 2 }, }, .{})); defer top_box.deinit(); @@ -143,10 +149,10 @@ pub fn main() !void { defer container.deinit(); // place empty container containing the element of the scrollable Container. - var scrollable_top: App.Scrollable = .init(top_box, .{ .y = 50 }); + var scrollable_top: App.Scrollable = .{ .container = top_box }; try container.append(try App.Container.init(allocator, .{}, scrollable_top.element())); - var scrollable_bottom: App.Scrollable = .init(bottom_box, .{ .y = 30 }); + var scrollable_bottom: App.Scrollable = .{ .container = bottom_box }; try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element())); try app.start(); diff --git a/src/container.zig b/src/container.zig index b6b1830..5d1e49f 100644 --- a/src/container.zig +++ b/src/container.zig @@ -633,14 +633,13 @@ pub fn Container(comptime Event: type) type { } } - fn fit_resize(this: *@This()) Point { + pub 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 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, @@ -671,8 +670,8 @@ pub fn Container(comptime Event: type) type { } // assign currently calculated size - this.size = size; - return size; + this.size = Point.max(size, this.properties.size); + return this.size; } // growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen @@ -818,6 +817,7 @@ pub fn Container(comptime Event: type) type { } pub fn contents(this: *const @This()) ![]const Cell { + if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall; const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y)); @memset(cells, .{}); errdefer this.allocator.free(cells); diff --git a/src/element.zig b/src/element.zig index 32d5cdf..cfa7504 100644 --- a/src/element.zig +++ b/src/element.zig @@ -70,14 +70,9 @@ pub fn Scrollable(Event: type) type { /// `Size` of the actual contents where the anchor and the size is /// representing the size and location on screen. size: Point = .{}, - /// Minimal `Size` of the scrollable `Container` to be used. If the - /// actual scrollable container's size is larger it will be used instead - /// (no scrolling will be necessary for that screen size). - min_size: Point = .{}, /// `Size` of the `Container` content that is scrollable and mapped to /// the *size* of the `Scrollable` `Element`. container_size: Point = .{}, - container_origin: Point = .{}, /// Anchor of the viewport of the scrollable `Container`. anchor: Point = .{}, /// The actual `Container`, that is scrollable. @@ -88,31 +83,19 @@ pub fn Scrollable(Event: type) type { .ptr = this, .vtable = &.{ .resize = resize, - .reposition = reposition, .handle = handle, .content = content, }, }; } - pub fn init(container: Container(Event), min_size: Point) @This() { - return .{ - .container = container, - .min_size = min_size, - }; - } - fn resize(ctx: *anyopaque, size: Point) void { const this: *@This() = @ptrCast(@alignCast(ctx)); this.size = size; - // TODO scrollbar space - depending on configuration and only if necessary? - this.container_size = Point.max(size, this.min_size); - this.container.resize(.{}, this.container_size); - } - fn reposition(ctx: *anyopaque, origin: Point) void { - const this: *@This() = @ptrCast(@alignCast(ctx)); - this.container_origin = origin; // TODO the size should be a provided origin + // TODO scrollbar space - depending on configuration and only if necessary? + this.container_size = Point.max(size, this.container.fit_resize()); + this.container.resize(.{}, this.container_size); } fn handle(ctx: *anyopaque, event: Event) !void { @@ -170,7 +153,7 @@ pub fn Scrollable(Event: type) type { fn content(ctx: *anyopaque, cells: []Cell, origin: Point, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); - _ = origin; // this should be used + _ = origin; std.debug.assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y)); const container_size = this.container.size; @@ -183,7 +166,6 @@ pub fn Scrollable(Event: type) type { @memcpy(container_cells, container_cells_const); } - // FIX this is not resolving the rendering recursively! This means that the content is only shown for the first children for (this.container.elements.items) |child| try render_container(child, container_cells, container_origin, container_size); const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x); @@ -224,6 +206,7 @@ test "scrollable vertical" { .direction = .vertical, .padding = .all(1), }, + .size = .{ .y = size.y + 15 }, }, .{}); try box.append(try .init(allocator, .{ .rectangle = .{ .fill = .grey }, @@ -233,7 +216,7 @@ test "scrollable vertical" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .y = size.y + 15 }); + var scrollable: Scrollable(event.SystemEvent) = .{ .container = box }; var container: Container(event.SystemEvent) = try .init(allocator, .{ .border = .{ @@ -298,6 +281,7 @@ test "scrollable horizontal" { .direction = .horizontal, .padding = .all(1), }, + .size = .{ .x = size.x + 15 }, }, .{}); try box.append(try .init(allocator, .{ .rectangle = .{ .fill = .grey }, @@ -307,7 +291,7 @@ test "scrollable horizontal" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .x = size.x + 15 }); + var scrollable: Scrollable(event.SystemEvent) = .{ .container = box }; var container: Container(event.SystemEvent) = try .init(allocator, .{ .border = .{