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`.

