use "collections"
primitive Horizontal
"""
Pack children along the horizontal axis (left-to-right).
"""
primitive Vertical
"""
Pack children along the vertical axis (top-to-bottom).
"""
type PackAxis is (Horizontal | Vertical)
class val Allocation
"""
Result of packing: a child's content placement and slot region within
the container. Content (x, y, width, height) is where the child renders.
Slot (slot_x, slot_y, slot_width, slot_height) is the full region the
child claims — identical to content except for PackExpand, where the
slot is larger and the content is centered within it.
"""
let x: USize
let y: USize
let width: USize
let height: USize
let slot_x: USize
let slot_y: USize
let slot_width: USize
let slot_height: USize
new val create(
x': USize, y': USize, width': USize, height': USize,
slot_x': USize, slot_y': USize,
slot_width': USize, slot_height': USize)
=>
"""
Create an allocation with content placement and slot region.
"""
x = x'
y = y'
width = width'
height = height'
slot_x = slot_x'
slot_y = slot_y'
slot_width = slot_width'
slot_height = slot_height'
primitive Packer
"""
GTK2-style box packing algorithm. Computes child allocations given
container dimensions, packing axis, and children's size hints + pack options.
When expanding children have more space than needed, extra is distributed
equally. When there is less space than needed, expanding children shrink
proportionally. Non-expanding children always get their preferred size
(truncated only if they physically exceed remaining space).
"""
fun pack(
axis: PackAxis,
container_w: USize,
container_h: USize,
children: Array[(USize, USize, PackOption, Bool)] val,
align: Alignment = AlignStart)
: Array[Allocation] val
=>
"""
Compute child allocations within a container. Handles pack_start/pack_end,
expand/fill, padding, proportional shrinking, and container alignment.
"""
let container_main = match axis
| Horizontal => container_w
| Vertical => container_h
end
// Separate into pack_start and pack_end, preserving original indices.
let start_indices = Array[USize]
let end_indices = Array[USize]
for ci in Range(0, children.size()) do
try
(_, _, _, let from_end) = children(ci)?
if from_end then
end_indices.push(ci)
else
start_indices.push(ci)
end
end
end
// Calculate totals for space distribution.
var total_preferred: USize = 0
var total_padding: USize = 0
var expand_count: USize = 0
var expand_preferred: USize = 0
var fixed_preferred: USize = 0
for ti in Range(0, children.size()) do
try
(let pw, let ph, let opt, _) = children(ti)?
let pref = match axis
| Horizontal => pw
| Vertical => ph
end
total_preferred = total_preferred + pref
total_padding = total_padding + opt.padding
match opt.mode
| PackExpand | PackFill =>
expand_count = expand_count + 1
expand_preferred = expand_preferred + pref
else
fixed_preferred = fixed_preferred + pref
end
end
end
// Space available after padding and fixed children.
let space_for_expanding: USize =
if container_main > (total_padding + fixed_preferred) then
container_main - total_padding - fixed_preferred
else
0
end
// Build result array with placeholder allocations.
let result = recover iso
let arr = Array[Allocation](children.size())
for _ in Range(0, children.size()) do
arr.push(Allocation(0, 0, 0, 0, 0, 0, 0, 0))
end
arr
end
// Compute effective main-axis size for each child.
// Expanding children share space_for_expanding proportionally.
// expand_remainder distributes leftover pixels to avoid rounding loss.
var expand_allocated: USize = 0
var expand_seen: USize = 0
// Place pack_start children left-to-right (or top-to-bottom).
var cursor: USize = 0
for i in start_indices.values() do
try
(let pw, let ph, let opt, _) = children(i)?
let pref = match axis
| Horizontal => pw
| Vertical => ph
end
cursor = cursor + opt.padding
let remaining = if cursor < container_main then
container_main - cursor
else
0
end
var alloc_space: USize = pref
let is_expanding = match opt.mode | PackExpand | PackFill => true else false end
if is_expanding and (expand_count > 0) then
expand_seen = expand_seen + 1
if expand_seen == expand_count then
// Last expanding child gets whatever is left
alloc_space = if space_for_expanding > expand_allocated then
space_for_expanding - expand_allocated
else
0
end
else
// Proportional share: (pref / expand_preferred) * space_for_expanding
alloc_space = if expand_preferred > 0 then
(pref * space_for_expanding) / expand_preferred
else
space_for_expanding / expand_count
end
expand_allocated = expand_allocated + alloc_space
end
end
let clamped_space = alloc_space.min(remaining)
let main_pos: USize = match opt.mode
| PackExpand =>
let actual_size = pref.min(clamped_space)
let centered_offset = (clamped_space - actual_size) / 2
cursor + centered_offset
else
cursor
end
let main_size: USize = match opt.mode
| PackExpand => pref.min(clamped_space)
else
clamped_space
end
let cross_size = match axis
| Horizontal => container_h
| Vertical => container_w
end
let slot_start = cursor
let alloc = match axis
| Horizontal =>
Allocation(main_pos, 0, main_size, cross_size,
slot_start, 0, clamped_space, cross_size)
| Vertical =>
Allocation(0, main_pos, cross_size, main_size,
0, slot_start, cross_size, clamped_space)
end
result(i)? = consume alloc
cursor = cursor + clamped_space
end
end
// Place pack_end children right-to-left (or bottom-to-top).
// Reset expand tracking for pack_end group.
var end_expand_allocated: USize = 0
var end_expand_seen: USize = 0
var end_expand_count: USize = 0
var end_expand_preferred: USize = 0
var end_fixed_preferred: USize = 0
var end_total_padding: USize = 0
for i in end_indices.values() do
try
(let pw, let ph, let opt, _) = children(i)?
let pref = match axis
| Horizontal => pw
| Vertical => ph
end
end_total_padding = end_total_padding + opt.padding
match opt.mode
| PackExpand | PackFill =>
end_expand_count = end_expand_count + 1
end_expand_preferred = end_expand_preferred + pref
else
end_fixed_preferred = end_fixed_preferred + pref
end
end
end
// Space available for pack_end expanding children is from end_cursor backward.
// end_cursor starts at container_main (or at cursor if pack_start used some).
var end_cursor: USize = container_main
let end_space_for_expanding: USize =
if end_cursor > (end_total_padding + end_fixed_preferred) then
end_cursor - end_total_padding - end_fixed_preferred
else
0
end
for i in end_indices.values() do
try
(let pw, let ph, let opt, _) = children(i)?
let pref = match axis
| Horizontal => pw
| Vertical => ph
end
var alloc_space: USize = pref
let is_expanding = match opt.mode | PackExpand | PackFill => true else false end
if is_expanding and (end_expand_count > 0) then
end_expand_seen = end_expand_seen + 1
if end_expand_seen == end_expand_count then
alloc_space = if end_space_for_expanding > end_expand_allocated then
end_space_for_expanding - end_expand_allocated
else
0
end
else
alloc_space = if end_expand_preferred > 0 then
(pref * end_space_for_expanding) / end_expand_preferred
else
end_space_for_expanding / end_expand_count
end
end_expand_allocated = end_expand_allocated + alloc_space
end
end
let size = if end_cursor >= (alloc_space + opt.padding) then
alloc_space
else
if end_cursor >= opt.padding then
end_cursor - opt.padding
else
0
end
end
end_cursor = if end_cursor >= (size + opt.padding) then
end_cursor - size - opt.padding
else
0
end
let main_pos = end_cursor
let cross_size = match axis
| Horizontal => container_h
| Vertical => container_w
end
let alloc = match axis
| Horizontal =>
Allocation(main_pos, 0, size, cross_size,
main_pos, 0, size, cross_size)
| Vertical =>
Allocation(0, main_pos, cross_size, size,
0, main_pos, cross_size, size)
end
result(i)? = consume alloc
end
end
// Apply container alignment offset to all pack_start children.
// pack_end children are already positioned from the end.
match align
| AlignStart => None
| let _: (AlignCenter | AlignEnd) =>
// Find the rightmost/bottommost extent of pack_start children
// Use slot bounds since they cover the full region including expand padding
var max_extent: USize = 0
for si in start_indices.values() do
try
let a = result(si)?
let extent = match axis
| Horizontal => a.slot_x + a.slot_width
| Vertical => a.slot_y + a.slot_height
end
if extent > max_extent then max_extent = extent end
end
end
let remaining = if container_main > max_extent then
container_main - max_extent
else
0
end
let offset = match align
| AlignCenter => remaining / 2
| AlignEnd => remaining
else
0
end
if offset > 0 then
for si in start_indices.values() do
try
let a = result(si)?
let shifted = match axis
| Horizontal =>
Allocation(a.x + offset, a.y, a.width, a.height,
a.slot_x + offset, a.slot_y, a.slot_width, a.slot_height)
| Vertical =>
Allocation(a.x, a.y + offset, a.width, a.height,
a.slot_x, a.slot_y + offset, a.slot_width, a.slot_height)
end
result(si)? = consume shifted
end
end
end
end
consume result