1
0
Fork 0
zenolith/src/widgets/Box.zig

265 lines
9.6 KiB
Zig

//! A widget which lays out it's children using a FlowBox-like algorithm in a given direction.
const std = @import("std");
const attreebute = @import("../attreebute.zig");
const treev = @import("../treevent.zig");
const layout = @import("../layout.zig");
const Widget = @import("../widget.zig").Widget;
pub const Direction = enum {
vertical,
horizontal,
};
/// Specifies how children should be aligned on the secondary axis.
/// In horizontal mode, left means top and right means bottom.
pub const ChildPositioning = enum {
left,
center,
right,
};
pub const Child = struct {
widget: *Widget,
/// Offset from the start of the box. Used for positioning.
offset: u31 = 0,
pos: ChildPositioning,
};
/// Direction to lay out children in.
/// - vertical/column
/// - horizontal/row
direction: Direction,
children: std.MultiArrayList(Child),
/// If set to true, the box will expand in the direction orthogonal to the direction field
/// to fill the constraints.
orth_expand: bool = false,
const Box = @This();
pub fn init(alloc: std.mem.Allocator, direction: Direction) !*Widget {
const self = Box{
.direction = direction,
.children = std.MultiArrayList(Child){},
};
return try Widget.init(alloc, self);
}
pub fn deinit(self: *Box, selfw: *Widget) void {
for (self.children.items(.widget)) |child| {
child.deinit();
}
self.children.deinit(selfw.data.allocator);
}
pub fn treevent(self: *Box, selfw: *Widget, tv: anytype) anyerror!void {
switch (@TypeOf(tv)) {
treev.LayoutSize => {
const slice = self.children.slice();
// The maximum size of the children in the direction orthogonal to that of the Box.
var max_orth_size: u31 = if (self.orth_expand) switch (self.direction) {
.vertical => tv.constraints.max.width,
.horizontal => tv.constraints.max.height,
} else 0;
var cur_pos: u31 = 0;
// first pass, initial sizes
{
for (slice.items(.widget)) |child| {
var child_cons = layout.Constraints{
.max = tv.constraints.max,
.min = layout.Size.zero,
};
switch (self.direction) {
.vertical => {
child_cons.max.height -|= cur_pos;
},
.horizontal => {
child_cons.max.width -|= cur_pos;
},
}
try child.treevent(treev.LayoutSize{
.constraints = child_cons,
// we only do a second pass on this child if it's flex
.final = child.getFlexExpand() == 0,
});
cur_pos += switch (self.direction) {
.vertical => child.data.size.height,
.horizontal => child.data.size.width,
};
max_orth_size = @max(max_orth_size, switch (self.direction) {
.vertical => child.data.size.width,
.horizontal => child.data.size.height,
});
}
}
// second pass, flex widgets
{
const remaining_space = switch (self.direction) {
.vertical => tv.constraints.max.height,
.horizontal => tv.constraints.max.width,
} -| cur_pos;
cur_pos = 0;
const flex_extra_space = try selfw.data.allocator.alloc(?f64, self.children.len);
defer selfw.data.allocator.free(flex_extra_space);
@memset(flex_extra_space, null);
var flex_sum: f64 = 0;
for (slice.items(.widget)) |child| {
flex_sum += @floatFromInt(child.getFlexExpand());
}
// calculate the height/width the flex children will get.
for (flex_extra_space, slice.items(.widget)) |*fes, child| {
if (child.getFlexExpand() > 0) {
fes.* = @as(f64, @floatFromInt(child.getFlexExpand())) / flex_sum;
fes.*.? *= @floatFromInt(remaining_space);
}
}
for (
slice.items(.widget),
slice.items(.offset),
flex_extra_space,
) |child, *offset, maybe_fes| {
if (maybe_fes) |fes| {
const child_cons = switch (self.direction) {
.vertical => v: {
const child_height = @as(u31, @intFromFloat(fes)) +
child.data.size.height;
break :v layout.Constraints.tight(.{
.width = child.data.size.width,
.height = child_height,
});
},
.horizontal => h: {
const child_width = @as(u31, @intFromFloat(fes)) +
child.data.size.width;
break :h layout.Constraints.tight(.{
.width = child_width,
.height = child.data.size.height,
});
},
};
try child.treevent(treev.LayoutSize{
.constraints = child_cons,
.final = true,
});
}
offset.* = cur_pos;
cur_pos += switch (self.direction) {
.vertical => child.data.size.height,
.horizontal => child.data.size.width,
};
}
}
selfw.data.size = tv.constraints.clamp(switch (self.direction) {
.vertical => .{
.width = max_orth_size,
.height = cur_pos,
},
.horizontal => .{
.width = cur_pos,
.height = max_orth_size,
},
});
},
treev.LayoutPosition => {
const slice = self.children.slice();
selfw.data.position = tv.position;
for (
slice.items(.widget),
slice.items(.offset),
slice.items(.pos),
) |child, offset, positioning| {
const child_pos = switch (self.direction) {
.vertical => switch (positioning) {
.left => .{ .x = tv.position.x, .y = tv.position.y + offset },
.center => .{
.x = tv.position.x +
@divTrunc(selfw.data.size.width, 2) - @divTrunc(child.data.size.width, 2),
.y = tv.position.y + offset,
},
.right => .{
.x = tv.position.x + selfw.data.size.width - child.data.size.width,
.y = tv.position.y + offset,
},
},
.horizontal => switch (positioning) {
.left => .{ .x = tv.position.x + offset, .y = tv.position.y },
.center => .{
.x = tv.position.x + offset,
.y = tv.position.y +
@divTrunc(selfw.data.size.height, 2) - @divTrunc(child.data.size.height, 2),
},
.right => .{
.x = tv.position.x + offset,
.y = tv.position.y + selfw.data.size.height - child.data.size.height,
},
},
};
try child.treevent(treev.LayoutPosition{ .position = child_pos });
}
},
treev.Draw => {
const style: *const attreebute.BoxStyle = selfw.getAttreebute(attreebute.BoxStyle) orelse &.{};
try style.background.drawBackground(
tv.painter,
.{ .pos = selfw.data.position, .size = selfw.data.size },
);
try tv.dispatch(selfw);
},
else => try tv.dispatch(selfw),
}
}
pub fn children(self: *Box, _: *Widget) []const *Widget {
return self.children.items(.widget);
}
pub fn addChild(self: *Box, selfw: *Widget, position: ?usize, child: *Widget) !void {
try self.addChildPositioned(selfw, position, child, .left);
}
/// Same as the normal Widget.addChild function, except a positioning for the child is set.
pub fn addChildPositioned(
self: *Box,
selfw: *Widget,
idx: ?usize,
child: *Widget,
positioning: ChildPositioning,
) !void {
if (idx) |i| {
try self.children.insert(selfw.data.allocator, i, .{ .widget = child, .pos = positioning });
} else {
try self.children.append(selfw.data.allocator, .{ .widget = child, .pos = positioning });
}
}
pub fn removeChild(self: *Box, selfw: *Widget, position: ?usize) *Widget {
_ = selfw;
if (position) |pos| {
const old = self.children.get(pos).widget;
self.children.orderedRemove(pos);
return old;
} else {
return self.children.pop().widget;
}
}