ref(container): split size and position calculations
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 34s

This commit is contained in:
2025-03-04 19:53:28 +01:00
parent 65d7546efd
commit fc72cf4abb
14 changed files with 205 additions and 180 deletions

View File

@@ -92,11 +92,11 @@ pub fn main() !void {
.color = .light_blue, .color = .light_blue,
.sides = .all, .sides = .all,
}, },
.fixed_size = .{ .x = 200 }, .size = .{ .x = 200 },
}, .{})); }, .{}));
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
.fixed_size = .{ .x = 30 }, .size = .{ .x = 30 },
}, .{})); }, .{}));
defer container.deinit(); // also de-initializes the children defer container.deinit(); // also de-initializes the children
@@ -146,8 +146,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -142,8 +142,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -156,8 +156,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -178,8 +178,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -154,8 +154,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -108,8 +108,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -100,8 +100,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -112,8 +112,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -99,8 +99,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -95,8 +95,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -148,8 +148,7 @@ pub fn main() !void {
} }
try renderer.resize(); try renderer.resize();
container.reposition(.{}); container.resize(.{}, renderer.size);
container.resize(renderer.size);
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container);
try renderer.flush(); try renderer.flush();
} }

View File

@@ -572,7 +572,10 @@ pub fn Container(comptime Event: type) type {
border: Border = .{}, border: Border = .{},
rectangle: Rectangle = .{}, rectangle: Rectangle = .{},
layout: Layout = .{}, 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( pub fn init(
@@ -601,170 +604,204 @@ pub fn Container(comptime Event: type) type {
try this.elements.append(element); try this.elements.append(element);
} }
pub fn reposition(this: *@This(), origin: Point) void { fn reposition(this: *@This(), origin: Point) void {
log.debug("origin: .{{ .x = {d}, .y = {d} }}", .{ origin.x, origin.y }); const layout = this.properties.layout;
this.origin = origin; this.origin = origin;
this.element.reposition(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 { fn fit_resize(this: *@This()) Point {
log.debug("Event .size: {{ .x = {d}, .y = {d} }}", .{ size.x, size.y }); // NOTE this is supposed to be a simple and easy to understand alogrithm, there are currently no optimizations done
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;
const layout = this.properties.layout; const layout = this.properties.layout;
var fixed_size_elements: u16 = 0; var size: Point = switch (layout.direction) {
var fixed_size: Point = .{}; .horizontal => .{ .x = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) },
for (this.elements.items) |element| { .vertical => .{ .y = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) },
switch (layout.direction) { };
.horizontal => if (element.properties.fixed_size.x > 0) { size = Point.add(size, this.properties.size);
fixed_size_elements += 1;
}, if (this.elements.items.len > 0) switch (layout.direction) {
.vertical => if (element.properties.fixed_size.y > 0) { .horizontal => size.x += layout.padding.left + layout.padding.right,
fixed_size_elements += 1; .vertical => size.y += layout.padding.top + layout.padding.bottom,
}, };
}
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,
}
const sides = this.properties.border.sides; const sides = this.properties.border.sides;
const padding = layout.padding; if (sides.right) size.x -|= 1;
var gap = layout.gap; if (sides.bottom) size.y -|= 1;
if (layout.separator.enabled) gap += 1;
const len: u16 = @truncate(this.elements.items.len); if (layout.separator.enabled) switch (layout.direction) {
const element_x = blk: { .horizontal => size.x += @as(u16, @truncate(this.elements.items.len -| 1)),
var x = size.x - fixed_size.x - gap * (len - 1); .vertical => size.y += @as(u16, @truncate(this.elements.items.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);
}
},
}; };
for (this.elements.items) |*element| { for (this.elements.items) |*child| {
var element_size: Point = undefined; const child_size = child.fit_resize();
var element_origin: Point = undefined;
switch (layout.direction) { switch (layout.direction) {
.horizontal => { .horizontal => {
// TODO this should not always be the max size property! size.x += child_size.x;
var x = blk: { size.y = @max(size.y, child_size.y);
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;
}, },
.vertical => { .vertical => {
var y = blk: { size.x = @max(size.x, child_size.x);
if (element.properties.fixed_size.y > 0) break :blk element.properties.fixed_size.y; size.y += child_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;
}, },
} }
// 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 { pub fn handle(this: *@This(), event: Event) !void {

View File

@@ -107,8 +107,7 @@ 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.min_size); 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 { fn reposition(ctx: *anyopaque, origin: Point) void {
@@ -247,7 +246,7 @@ test "scrollable vertical" {
var renderer: testing.Renderer = .init(allocator, size); var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
container.resize(size); container.resize(.{}, size);
try renderer.render(Container(event.SystemEvent), &container); try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen); 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); var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
container.resize(size); container.resize(.{}, size);
try renderer.render(Container(event.SystemEvent), &container); try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);

View File

@@ -34,7 +34,7 @@ pub const Renderer = struct {
pub fn resize(this: *@This(), size: Point) !void { pub fn resize(this: *@This(), size: Point) !void {
this.size = size; 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.allocator.free(this.screen);
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory."); 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); var renderer: Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
container.reposition(.{}); try renderer.resize(size);
container.resize(size); container.resize(.{}, size);
try renderer.render(Container(event.SystemEvent), container); try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen); try expectEqualCells(.{}, renderer.size, expected, renderer.screen);