add(sizing): grow configuration
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m14s

Currently the 'grid' and 'mixed' examples are not working yet.
This commit is contained in:
2025-03-05 22:53:28 +01:00
parent 9ec335cad8
commit e3551fa624
6 changed files with 121 additions and 52 deletions

View File

@@ -61,7 +61,9 @@ pub fn main() !void {
.padding = .vertical(2), .padding = .vertical(2),
.direction = .vertical, .direction = .vertical,
}, },
.size = .{ .y = 90 }, .size = .{
.dim = .{ .y = 90 },
},
}, .{}); }, .{});
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
@@ -91,11 +93,15 @@ pub fn main() !void {
.color = .light_blue, .color = .light_blue,
.sides = .all, .sides = .all,
}, },
.size = .{ .x = 100 }, .size = .{
.dim = .{ .x = 100 },
},
}, .{})); }, .{}));
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
.size = .{ .x = 30 }, .size = .{
.dim = .{ .x = 30 },
},
}, .{})); }, .{}));
defer container.deinit(); // also de-initializes the children defer container.deinit(); // also de-initializes the children

View File

@@ -98,15 +98,21 @@ pub fn main() !void {
}, .{}); }, .{});
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
.size = .{ .y = 30 }, .size = .{
.dim = .{ .y = 30 },
},
}, .{})); }, .{}));
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
.size = .{ .y = 5 }, .size = .{
.dim = .{ .y = 5 },
},
}, element)); }, element));
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
.size = .{ .y = 2 }, .size = .{
.dim = .{ .y = 2 },
},
}, .{})); }, .{}));
defer top_box.deinit(); defer top_box.deinit();
@@ -123,6 +129,9 @@ pub fn main() !void {
.direction = .vertical, .direction = .vertical,
.padding = .vertical(1), .padding = .vertical(1),
}, },
.size = .{
.dim = .{ .y = 30 },
},
}, .{}); }, .{});
try bottom_box.append(try App.Container.init(allocator, .{ try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey }, .rectangle = .{ .fill = .grey },

View File

@@ -64,7 +64,7 @@ pub fn main() !void {
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{})); 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 container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start(); try app.start();

View File

@@ -114,10 +114,7 @@ pub fn main() !void {
}, text_styles.element()); }, text_styles.element());
defer box.deinit(); defer box.deinit();
var scrollable: App.Scrollable = .init(box, .{ var scrollable: App.Scrollable = .{ .container = 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
try container.append(try App.Container.init(allocator, .{}, scrollable.element())); try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start(); try app.start();

View File

@@ -352,12 +352,15 @@ pub const Layout = packed struct {
}; };
const gap: u16 = (this.gap + 1) / 2; const gap: u16 = (this.gap + 1) / 2;
log.debug("origin: {any}, size: {any}", .{ origin, size });
for (0..children.len - 1) |idx| { for (0..children.len - 1) |idx| {
const child = children[idx]; const child = children[idx];
const anchor = switch (this.direction) { 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), .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, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) -| @as(usize, origin.x), .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) { switch (this.direction) {
.horizontal => for (0..child.size.y) |row| { .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 { pub fn Container(comptime Event: type) type {
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: 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 = .{}, border: Border = .{},
rectangle: Rectangle = .{}, rectangle: Rectangle = .{},
layout: Layout = .{}, layout: Layout = .{},
/// size which should be used by the `Container` size: Size = .{},
size: Point = .{},
/// should size grow to the available space?
grow: bool = true,
}; };
pub fn init( pub fn init(
@@ -609,7 +620,7 @@ pub fn Container(comptime Event: type) type {
this.origin = origin; this.origin = origin;
this.element.reposition(origin); this.element.reposition(origin);
var offset: Point = Point.add(origin, .{ var offset = origin.add(.{
.x = layout.padding.left, .x = layout.padding.left,
.y = layout.padding.top, .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 // NOTE this is supposed to be a simple and easy to understand algorithm, there are currently no optimizations done
const layout = this.properties.layout; const layout = this.properties.layout;
var size: Point = switch (layout.direction) { var size: Point = switch (layout.direction) {
@@ -647,8 +659,10 @@ pub fn Container(comptime Event: type) type {
}; };
const sides = this.properties.border.sides; const sides = this.properties.border.sides;
if (sides.right) size.x -|= 1; if (sides.left) size.x += 1;
if (sides.bottom) size.y -|= 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) { if (layout.separator.enabled) switch (layout.direction) {
.horizontal => size.x += @as(u16, @truncate(this.elements.items.len -| 1)), .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 // 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; return this.size;
} }
// growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen // 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; const layout = this.properties.layout;
var remainder = switch (layout.direction) { var remainder = switch (layout.direction) {
.horizontal => this.size.x -| (layout.padding.left + layout.padding.right), .horizontal => max_size.x -| (layout.padding.left + layout.padding.right),
.vertical => this.size.y -| (layout.padding.top + layout.padding.bottom), .vertical => max_size.y -| (layout.padding.top + layout.padding.bottom),
}; };
remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)); remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1));
if (layout.separator.enabled) remainder -|= @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) { var available = switch (layout.direction) {
.horizontal => this.size.y -| (layout.padding.top + layout.padding.bottom), .horizontal => max_size.y -| (layout.padding.top + layout.padding.bottom),
.vertical => this.size.x -| (layout.padding.left + layout.padding.right), .vertical => max_size.x -| (layout.padding.left + layout.padding.right),
}; };
const sides = this.properties.border.sides; const sides = this.properties.border.sides;
switch (layout.direction) { switch (layout.direction) {
.horizontal => { .horizontal => {
if (sides.bottom) { if (sides.top) {
available -|= 1; available -|= 1;
remainder -|= 1; remainder -|= 1;
} }
if (sides.top) { if (sides.bottom) {
available -|= 1; available -|= 1;
remainder -|= 1; remainder -|= 1;
} }
}, },
.vertical => { .vertical => {
if (sides.right) { if (sides.left) {
available -|= 1; available -|= 1;
remainder -|= 1; remainder -|= 1;
} }
if (sides.left) { if (sides.right) {
available -|= 1; available -|= 1;
remainder -|= 1; remainder -|= 1;
} }
@@ -724,18 +749,23 @@ pub fn Container(comptime Event: type) type {
var growable_children: usize = 0; var growable_children: usize = 0;
var first_growable_child: *@This() = undefined; var first_growable_child: *@This() = undefined;
for (this.elements.items) |*child| { for (this.elements.items) |*child| {
if (child.properties.grow) { if (child.properties.size.grow != .fixed) {
if (growable_children == 0) first_growable_child = child; if (growable_children == 0) first_growable_child = child;
growable_children += 1; growable_children += 1;
switch (layout.direction) { switch (layout.direction) {
.horizontal => child.size.y = available, .horizontal => if (child.properties.size.grow != .vertical) {
.vertical => child.size.x = available, 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) { var smallest_size = switch (layout.direction) {
.horizontal => first_growable_child.size.x, .horizontal => first_growable_child.size.x,
.vertical => first_growable_child.size.y, .vertical => first_growable_child.size.y,
@@ -744,7 +774,7 @@ pub fn Container(comptime Event: type) type {
var size_to_correct = remainder; var size_to_correct = remainder;
for (this.elements.items) |child| { for (this.elements.items) |child| {
if (!child.properties.grow) continue; if (child.properties.size.grow == .fixed) continue;
const size = switch (layout.direction) { const size = switch (layout.direction) {
.horizontal => child.size.x, .horizontal => child.size.x,
@@ -771,18 +801,28 @@ pub fn Container(comptime Event: type) type {
.horizontal => child.size.x, .horizontal => child.size.x,
.vertical => child.size.y, .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) { switch (layout.direction) {
.horizontal => child.size.x += size_to_correct, .horizontal => if (child.properties.size.grow != .vertical) {
.vertical => child.size.y += size_to_correct, child.size.x += size_to_correct;
},
.vertical => if (child.properties.size.grow != .horizontal) {
child.size.y += size_to_correct;
},
} }
if (overflow > 0) { if (overflow > 0) {
switch (layout.direction) { switch (layout.direction) {
.horizontal => child.size.x += 1, .horizontal => if (child.properties.size.grow != .vertical) {
.vertical => child.size.y += 1, 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; remainder -|= size_to_correct;
} }
@@ -790,16 +830,28 @@ pub fn Container(comptime Event: type) type {
} }
this.element.resize(this.size); 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 { pub fn resize(this: *@This(), origin: Point, size: Point) void {
// NOTE assume that this function is only called for the root `Container` // NOTE assume that this function is only called for the root `Container`
this.size = size; // total screen size;
const fit_size = this.fit_resize(); const fit_size = this.fit_resize();
this.size = size; // total screen size log.debug("fit_size: {any}; size: {any}", .{ fit_size, size });
_ = fit_size; // if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space");
this.grow_resize(); 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); this.reposition(origin);
} }
@@ -822,6 +874,7 @@ pub fn Container(comptime Event: type) type {
@memset(cells, .{}); @memset(cells, .{});
errdefer this.allocator.free(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.layout.contents(@This(), cells, this.origin, this.size, this.elements.items);
this.properties.border.contents(cells, this.size); this.properties.border.contents(cells, this.size);
this.properties.rectangle.contents(cells, this.size); this.properties.rectangle.contents(cells, this.size);

View File

@@ -94,8 +94,8 @@ pub fn Scrollable(Event: type) type {
this.size = size; this.size = size;
// TODO scrollbar space - depending on configuration and only if necessary? // TODO scrollbar space - depending on configuration and only if necessary?
this.container_size = Point.max(size, this.container.fit_resize()); this.container.resize(.{}, this.size);
this.container.resize(.{}, this.container_size); this.container_size = this.container.size;
} }
fn handle(ctx: *anyopaque, event: Event) !void { fn handle(ctx: *anyopaque, event: Event) !void {
@@ -206,7 +206,9 @@ test "scrollable vertical" {
.direction = .vertical, .direction = .vertical,
.padding = .all(1), .padding = .all(1),
}, },
.size = .{ .y = size.y + 15 }, .size = .{
.dim = .{ .y = size.y + 15 },
},
}, .{}); }, .{});
try box.append(try .init(allocator, .{ try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey }, .rectangle = .{ .fill = .grey },
@@ -281,7 +283,9 @@ test "scrollable horizontal" {
.direction = .horizontal, .direction = .horizontal,
.padding = .all(1), .padding = .all(1),
}, },
.size = .{ .x = size.x + 15 }, .size = .{
.dim = .{ .x = size.x + 15 },
},
}, .{}); }, .{});
try box.append(try .init(allocator, .{ try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey }, .rectangle = .{ .fill = .grey },