diff --git a/examples/demo.zig b/examples/demo.zig index 3548074..3148ad1 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -61,7 +61,9 @@ pub fn main() !void { .padding = .vertical(2), .direction = .vertical, }, - .size = .{ .y = 90 }, + .size = .{ + .dim = .{ .y = 90 }, + }, }, .{}); try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, @@ -91,11 +93,15 @@ pub fn main() !void { .color = .light_blue, .sides = .all, }, - .size = .{ .x = 100 }, + .size = .{ + .dim = .{ .x = 100 }, + }, }, .{})); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, - .size = .{ .x = 30 }, + .size = .{ + .dim = .{ .x = 30 }, + }, }, .{})); defer container.deinit(); // also de-initializes the children diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index a8ab113..fe785d9 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -98,15 +98,21 @@ pub fn main() !void { }, .{}); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - .size = .{ .y = 30 }, + .size = .{ + .dim = .{ .y = 30 }, + }, }, .{})); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - .size = .{ .y = 5 }, + .size = .{ + .dim = .{ .y = 5 }, + }, }, element)); try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - .size = .{ .y = 2 }, + .size = .{ + .dim = .{ .y = 2 }, + }, }, .{})); defer top_box.deinit(); @@ -123,6 +129,9 @@ pub fn main() !void { .direction = .vertical, .padding = .vertical(1), }, + .size = .{ + .dim = .{ .y = 30 }, + }, }, .{}); try bottom_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .grey }, diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 279bcc3..4adaa02 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -64,7 +64,7 @@ pub fn main() !void { if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{})); } - var scrollable: App.Scrollable = .init(box, .{ .x = 3 * std.meta.fields(zterm.Color).len }); // ensure enough columns to render all colors -> scrollable otherwise + var scrollable: App.Scrollable = .{ .container = box }; try container.append(try App.Container.init(allocator, .{}, scrollable.element())); try app.start(); diff --git a/examples/styles/text.zig b/examples/styles/text.zig index 77f040c..38f3c7a 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -114,10 +114,7 @@ pub fn main() !void { }, text_styles.element()); defer box.deinit(); - var scrollable: App.Scrollable = .init(box, .{ - .x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len, - .y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2), - }); // ensure enough rows and/or columns to render all text styles -> scrollable otherwise + var scrollable: App.Scrollable = .{ .container = box }; try container.append(try App.Container.init(allocator, .{}, scrollable.element())); try app.start(); diff --git a/src/container.zig b/src/container.zig index 6aa6a62..d7934e5 100644 --- a/src/container.zig +++ b/src/container.zig @@ -352,12 +352,15 @@ pub const Layout = packed struct { }; const gap: u16 = (this.gap + 1) / 2; + log.debug("origin: {any}, size: {any}", .{ origin, size }); for (0..children.len - 1) |idx| { const child = children[idx]; const anchor = switch (this.direction) { - .horizontal => ((@as(usize, child.origin.y) -| @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) + @as(usize, child.size.x) + gap -| @as(usize, origin.x), - .vertical => ((@as(usize, child.origin.y) + @as(usize, child.size.y) + gap -| @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) -| @as(usize, origin.x), + .horizontal => (@as(usize, child.origin.y) * @as(usize, size.x)) + @as(usize, child.origin.x) + @as(usize, child.size.x) + gap, + .vertical => ((@as(usize, child.origin.y) + @as(usize, child.size.y) + gap) * @as(usize, size.x)) + @as(usize, child.origin.x), }; + log.debug("child.origin: {any}, child.size: {any}", .{ child.origin, child.size }); + log.debug("anchor: {d}", .{anchor}); switch (this.direction) { .horizontal => for (0..child.size.y) |row| { @@ -553,6 +556,17 @@ pub const Layout = packed struct { } }; +/// Sizing options which should be used by the `Container` +pub const Size = packed struct { + dim: Point = .{}, + grow: enum(u2) { + both, + fixed, + vertical, + horizontal, + } = .both, +}; + pub fn Container(comptime Event: type) type { if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); @@ -572,10 +586,7 @@ pub fn Container(comptime Event: type) type { border: Border = .{}, rectangle: Rectangle = .{}, layout: Layout = .{}, - /// size which should be used by the `Container` - size: Point = .{}, - /// should size grow to the available space? - grow: bool = true, + size: Size = .{}, }; pub fn init( @@ -609,7 +620,7 @@ pub fn Container(comptime Event: type) type { this.origin = origin; this.element.reposition(origin); - var offset: Point = Point.add(origin, .{ + var offset = origin.add(.{ .x = layout.padding.left, .y = layout.padding.top, }); @@ -633,7 +644,8 @@ pub fn Container(comptime Event: type) type { } } - pub fn fit_resize(this: *@This()) Point { + /// Resize all fit sized `Containers` to the necessary size required by its child elements. + fn fit_resize(this: *@This()) Point { // NOTE this is supposed to be a simple and easy to understand algorithm, there are currently no optimizations done const layout = this.properties.layout; var size: Point = switch (layout.direction) { @@ -647,8 +659,10 @@ pub fn Container(comptime Event: type) type { }; const sides = this.properties.border.sides; - if (sides.right) size.x -|= 1; - if (sides.bottom) size.y -|= 1; + if (sides.left) size.x += 1; + if (sides.right) size.x += 1; + if (sides.top) size.y += 1; + if (sides.bottom) size.y += 1; if (layout.separator.enabled) switch (layout.direction) { .horizontal => size.x += @as(u16, @truncate(this.elements.items.len -| 1)), @@ -670,44 +684,55 @@ pub fn Container(comptime Event: type) type { } // assign currently calculated size - this.size = Point.max(size, this.properties.size); + this.size = switch (this.properties.size.grow) { + .both => Point.max(size, this.properties.size.dim), + .fixed => this.properties.size.dim, + .horizontal => .{ + .x = @max(size.x, this.properties.size.dim.x), + .y = size.y, + }, + .vertical => .{ + .x = size.x, + .y = @max(size.y, this.properties.size.dim.y), + }, + }; return this.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 { + fn grow_resize(this: *@This(), max_size: Point) 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), + .horizontal => max_size.x -| (layout.padding.left + layout.padding.right), + .vertical => max_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), + .horizontal => max_size.y -| (layout.padding.top + layout.padding.bottom), + .vertical => max_size.x -| (layout.padding.left + layout.padding.right), }; const sides = this.properties.border.sides; switch (layout.direction) { .horizontal => { - if (sides.bottom) { + if (sides.top) { available -|= 1; remainder -|= 1; } - if (sides.top) { + if (sides.bottom) { available -|= 1; remainder -|= 1; } }, .vertical => { - if (sides.right) { + if (sides.left) { available -|= 1; remainder -|= 1; } - if (sides.left) { + if (sides.right) { available -|= 1; remainder -|= 1; } @@ -724,18 +749,23 @@ pub fn Container(comptime Event: type) type { var growable_children: usize = 0; var first_growable_child: *@This() = undefined; for (this.elements.items) |*child| { - if (child.properties.grow) { + if (child.properties.size.grow != .fixed) { if (growable_children == 0) first_growable_child = child; growable_children += 1; switch (layout.direction) { - .horizontal => child.size.y = available, - .vertical => child.size.x = available, + .horizontal => if (child.properties.size.grow != .vertical) { + child.size.y = available; + }, + .vertical => if (child.properties.size.grow != .horizontal) { + child.size.x = available; + }, } } } - while (growable_children > 0 and remainder > 0) { + var count: usize = this.elements.items.len; + while (growable_children > 0 and remainder > 0 and count > 0) : (count -|= 1) { var smallest_size = switch (layout.direction) { .horizontal => first_growable_child.size.x, .vertical => first_growable_child.size.y, @@ -744,7 +774,7 @@ pub fn Container(comptime Event: type) type { var size_to_correct = remainder; for (this.elements.items) |child| { - if (!child.properties.grow) continue; + if (child.properties.size.grow == .fixed) continue; const size = switch (layout.direction) { .horizontal => child.size.x, @@ -771,18 +801,28 @@ pub fn Container(comptime Event: type) type { .horizontal => child.size.x, .vertical => child.size.y, }; - if (child.properties.grow and child_size == smallest_size) { + if (child.properties.size.grow != .fixed and child_size == smallest_size) { switch (layout.direction) { - .horizontal => child.size.x += size_to_correct, - .vertical => child.size.y += size_to_correct, + .horizontal => if (child.properties.size.grow != .vertical) { + child.size.x += size_to_correct; + }, + .vertical => if (child.properties.size.grow != .horizontal) { + child.size.y += size_to_correct; + }, } if (overflow > 0) { switch (layout.direction) { - .horizontal => child.size.x += 1, - .vertical => child.size.y += 1, + .horizontal => if (child.properties.size.grow != .vertical) { + child.size.x += 1; + overflow -|= 1; + remainder -|= 1; + }, + .vertical => if (child.properties.size.grow != .horizontal) { + child.size.y += 1; + overflow -|= 1; + remainder -|= 1; + }, } - overflow -|= 1; - remainder -|= 1; } remainder -|= size_to_correct; } @@ -790,16 +830,28 @@ pub fn Container(comptime Event: type) type { } this.element.resize(this.size); - for (this.elements.items) |*child| child.grow_resize(); + for (this.elements.items) |*child| child.grow_resize(this.size); } 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(); + log.debug("fit_size: {any}; size: {any}", .{ fit_size, size }); + // if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space"); + switch (this.properties.size.grow) { + .both => this.size = Point.max(size, fit_size), + .fixed => {}, + .horizontal => this.size = .{ + .x = @max(size.x, fit_size.x), + .y = size.y, + }, + .vertical => this.size = .{ + .x = size.x, + .y = @max(size.y, fit_size.y), + }, + } + log.debug("this.size: {any}", .{this.size}); + this.grow_resize(this.size); this.reposition(origin); } @@ -822,6 +874,7 @@ pub fn Container(comptime Event: type) type { @memset(cells, .{}); errdefer this.allocator.free(cells); + log.debug("contents: this.size: {any}", .{this.size}); this.properties.layout.contents(@This(), cells, this.origin, this.size, this.elements.items); this.properties.border.contents(cells, this.size); this.properties.rectangle.contents(cells, this.size); diff --git a/src/element.zig b/src/element.zig index cfa7504..0a6eddb 100644 --- a/src/element.zig +++ b/src/element.zig @@ -94,8 +94,8 @@ 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.container.fit_resize()); - this.container.resize(.{}, this.container_size); + this.container.resize(.{}, this.size); + this.container_size = this.container.size; } fn handle(ctx: *anyopaque, event: Event) !void { @@ -206,7 +206,9 @@ test "scrollable vertical" { .direction = .vertical, .padding = .all(1), }, - .size = .{ .y = size.y + 15 }, + .size = .{ + .dim = .{ .y = size.y + 15 }, + }, }, .{}); try box.append(try .init(allocator, .{ .rectangle = .{ .fill = .grey }, @@ -281,7 +283,9 @@ test "scrollable horizontal" { .direction = .horizontal, .padding = .all(1), }, - .size = .{ .x = size.x + 15 }, + .size = .{ + .dim = .{ .x = size.x + 15 }, + }, }, .{}); try box.append(try .init(allocator, .{ .rectangle = .{ .fill = .grey },