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

@@ -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 {