Selkie.git | docs/api/ | Selkie--Layout--Allocate.md
NAME
====
Selkie::Layout::Allocate - Shared sizing-allocation pass for box layouts
SYNOPSIS
========
```raku
use Selkie::Layout::Allocate;
use Selkie::Sizing;
# A custom container that arranges children along the row axis:
my @allocs = allocate-along-axis(@kids, self.rows);
# `@allocs` is parallel to `@kids`; @allocs[$i] is the cell-count
# the layout assigns to the corresponding child. Position the children
# yourself — Allocate doesn't know about reposition / set-viewport.
# With a one-cell gutter between children:
my @spaced = allocate-along-axis(@kids, self.rows, :gap(1));
my $gutter = gap-reserve(@kids, 1); # cells the gaps will consume
# The other axis: how wide is the child, and where does it sit?
my $extent = resolve-cross-extent($child, self.cols);
my $align = effective-cross-align($child, self.align-items);
my $offset = cross-axis-offset($align, $extent, self.cols);
```
DESCRIPTION
===========
`allocate-along-axis` runs the three-pass sizing algorithm that both [Selkie::Layout::VBox](Selkie--Layout--VBox.md) and [Selkie::Layout::HBox](Selkie--Layout--HBox.md) use to decide how much room each child gets:
* **Pass 1.** Walk the children. Children with `Sizing.fixed($n)` take `$n` cells (clamped by remaining space). Children with `Sizing.percent($n)` take `$n%` of the original axis total (also clamped). Flex children defer; their flex factors are accumulated into a running total.
* **Pass 2.** Distribute whatever space remains among flex children, weighted by their flex factor relative to the total flex weight. Each flex share is floored, so several flex children can leave a few cells unspent.
* **Pass 3.** Hand any rounding remainder to the highest-index flex child. This keeps the box exactly filled and avoids rounding drift on resizes.
The function returns an `Array[UInt]` aligned with `@kids`; callers are responsible for positioning and propagating viewport bounds, since those depend on which axis is being laid out.
Gaps
----
Pass `:gap($n)` to reserve `$n` cells *between* children. The reservation is taken off the top: `gap-reserve` computes the total, and the three passes above then run over the **content box** — what's left of `$total` once the gutters are subtracted.
```raku
# 20 rows, three children, one row of gutter between each:
# gap-reserve = (3 - 1) * 1 = 2
# content box = 20 - 2 = 18
my @allocs = allocate-along-axis(@kids, 20, :gap(1));
```
Two consequences worth internalising:
* **Percent means percent-of-content-box.** `Sizing.percent(50)` in the example above resolves against 18, not 20 — it gets 9 rows, not 10. That is the same rule CSS applies to a flex container's content box, and it's what makes `percent` children sum to the space actually available for content rather than overflowing by the gutter total.
* **Gap cells are never allocated to anybody.** They're not in any child's allocation, so nothing paints them; the container plane's base cell shows through. That's what makes a gap read as breathing room rather than as a differently-coloured strip.
Children sized `Sizing.fixed(0)` — the "hidden child" idiom, where a widget stays in the tree but collapses to nothing — do **not** count towards the gutter total. Hiding a child by zeroing its sizing therefore removes its gutter too, instead of leaving a mysterious double gap behind.
`gap-reserve` is exported separately so containers can reason about the gutter total without running a full allocation (and so it can be tested in isolation).
`:gap(0)`, the default, is bit-identical to calling `allocate-along-axis` without the argument at all.
The cross axis
--------------
`allocate-along-axis` answers "how much of the *main* axis does each child get?". Three more free subs answer the two cross-axis questions — how big is the child on the axis the container doesn't stack on, and where does that size sit:
* `resolve-cross-extent($child, $container)` — resolves the child's `cross-sizing` against the container's cross extent. Fixed is a cell count, percent a share, flex (and undefined `cross-sizing`) the whole extent. Always clamped to `0 .. $container`.
* `effective-cross-align($child, $container-align)` — the child's `align-self` if it has one, otherwise the container's `align-items`.
* `cross-axis-offset($align, $extent, $container)` — the leading offset: 0 for start and fill, the floored half of the slack for centre, all of the slack for end.
```raku
# A 20-column child, centred in a 50-column VBox:
my $extent = resolve-cross-extent($child, 50); # 20
my $align = effective-cross-align($child, CrossCenter); # CrossCenter
my $offset = cross-axis-offset($align, $extent, 50); # 15
```
Gap and alignment are orthogonal by construction: the gutter total comes off the *main* axis before allocation, and these three subs only ever see the cross extent. A `VBox` with `gap` and `align-items` set applies both without either affecting the other's arithmetic.
Splitting the rule into free subs — rather than letting each box roll its own — is what keeps VBox and HBox from drifting apart, and lets the tests exercise the arithmetic with no notcurses planes in sight.
Why a free sub and not a base role?
-----------------------------------
VBox and HBox differ only in axis: VBox stacks rows, HBox stacks columns. Pass 3 (positioning) is axis-specific — it has to call `reposition($cy, 0)` versus `reposition(0, $cx)`, plus `set-viewport` with axis-specific named args. Bridging that into a shared role would obscure the layout code without saving lines, so the extraction stops at the axis-agnostic part: the allocation math.
### sub gap-reserve
```raku
sub gap-reserve(
@kids,
Int $gap where { ... }
) returns UInt
```
The total number of cells that `$gap`-wide gutters between `@kids` will consume: `((countable - 1) max 0) * $gap`. "Countable" excludes children sized `Sizing.fixed(0)`. That's the hidden-child idiom — a widget kept in the tree but collapsed to nothing — and a hidden child should take its gutter with it rather than leave a double gap between its visible neighbours. Never negative, and always 0 for an empty list, a single child, or `$gap == 0`.
### sub allocate-along-axis
```raku
sub allocate-along-axis(
@kids,
Int $total where { ... },
Int :$gap where { ... } = 0
) returns Array
```
Compute per-child allocations along a single axis, given the total axis size. Returns an Array[UInt] where `@allocs[$i]` is the cell count for `@kids[$i]`. Sum of allocations equals the content box (`$total` minus the gutter reservation) when flex children are present and the content box is non-zero; otherwise allocations may sum to less. Algorithm: =item The content box is `($total - gap-reserve(@kids, $gap)) max 0`. With the default `:gap(0)` that is exactly `$total`, and everything below behaves as it always has. =item Fixed children take `value` cells (clamped by remaining). =item Percent children take `value%` of the **content box** (also clamped) — see the `Gaps` section of the module docs. =item Flex children share whatever remains, weighted by `value`; the highest-index flex child collects any rounding remainder. Positioning the gutters is the caller's job: this function only withholds the cells. See `Selkie::Layout::VBox`'s layout pass for the `$placed`-flag idiom that keeps gaps strictly *between* non-collapsed children.
### sub resolve-cross-extent
```raku
sub resolve-cross-extent(
Selkie::Widget $child,
Int $container where { ... }
) returns UInt
```
How many cells `$child` occupies on the container's **cross** axis — columns for a `VBox`, rows for an `HBox` — given that the container has `$container` of them. The child's `cross-sizing` decides, reusing the `Selkie::Sizing` vocabulary: =item Undefined `cross-sizing` (the default) — the full `$container`. This is what Selkie's layouts have always done, and why adding cross-axis alignment moves nothing in an existing app. =item `Sizing.fixed($n)` — exactly `$n`, clamped to `$container`. =item `Sizing.percent($n)` — `$n%` of `$container`, floored, and also clamped. Percent resolves against the container's cross extent, which for a gapped box is the full extent: gutters come off the main axis only. =item `Sizing.flex` — the full `$container`. There is nothing to share a cross axis with, so flex and "fill" mean the same thing here; `flex` is accepted so a widget can carry one `Sizing` object for both axes. The result is never negative and never exceeds `$container`. A child that resolves to 0 (`Sizing.fixed(0)`, or a `percent` that floors to nothing) is parked by the box rather than given a zero-extent plane — but it keeps its main-axis allocation and its gutter, so collapsing a child on the cross axis never reflows its siblings on the main one.
### sub effective-cross-align
```raku
sub effective-cross-align(
Selkie::Widget $child,
CrossAlign $container-align
) returns CrossAlign
```
The `CrossAlign` that actually governs `$child`: its own `align-self` when it has one, the container's `align-items` otherwise. `align-self` is undefined by default, which is what makes `align-items` a real container-level policy rather than a default that every child silently overrides. Both boxes route through this sub so the inheritance rule can't drift between them.
### sub cross-axis-offset
```raku
sub cross-axis-offset(
CrossAlign $a,
Int $extent where { ... },
Int $container where { ... }
) returns UInt
```
The leading offset — column in a `VBox`, row in an `HBox` — at which an item of `$extent` cells sits inside a `$container`-cell slot under alignment `$a`: =item `CrossStart` and `CrossFill` — 0. (A `CrossFill` child normally *is* the container's extent; when it also declares a `cross-sizing`, size wins and the leftover goes on the trailing side, exactly like `CrossStart`.) =item `CrossCenter` — `((container - extent) / 2).floor`, so an odd slack lands the extra cell on the trailing side. =item `CrossEnd` — `container - extent`. An `$extent` larger than `$container` — which `resolve-cross-extent` never produces, but a caller doing its own arithmetic might — clamps to 0 rather than underflowing into a negative (and, for the `UInt` return, fatal) offset. An undefined `$a` is treated as `CrossFill`.