Selkie.git | docs/api/ | Selkie--Layout--VBox.md


NAME
====

Selkie::Layout::VBox - Arrange children top to bottom

SYNOPSIS
========

```raku
use Selkie::Layout::VBox;
use Selkie::Sizing;

my $vbox = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$vbox.add: $header;    # Sizing.fixed(1)
$vbox.add: $body;      # Sizing.flex
$vbox.add: $footer;    # Sizing.fixed(1)
```

DESCRIPTION
===========

`VBox` stacks children vertically and allocates rows according to each child's [Selkie::Sizing](Selkie--Sizing.md):

  * **Fixed** children get exactly the rows they ask for.

  * **Percent** children get `n%` of the parent's total rows.

  * **Flex** children share whatever rows are left over, weighted by flex factor.

Columns are set to the full parent width for every child, unless the child says otherwise with `cross-sizing` — see [/Cross-axis alignment](/Cross-axis alignment).

VBox is a [Selkie::Container](Selkie--Container.md), so it inherits `add`, `remove`, `clear`, and focusable-descendants handling. All children must compose `Selkie::Widget`.

Gaps
----

`gap` reserves rows *between* children — the idiomatic way to give a stack some breathing room without padding every child by hand:

```raku
my $form = Selkie::Layout::VBox.new(sizing => Sizing.flex, gap => 1);
$form.add: $name-row;      # Sizing.fixed(1)
$form.add: $email-row;     # Sizing.fixed(1)
$form.add: $submit;        # Sizing.fixed(1)

$form.set-gap(2);          # marks dirty; relayout on the next render
```

The gutters come off the top of the row budget before any child is sized, so `Sizing.percent` resolves against the content box (rows minus gutters) — see [Selkie::Layout::Allocate](Selkie--Layout--Allocate.md). Three of the rules are worth knowing by heart:

  * There is never a leading or trailing gap; `gap` is strictly *between* children.

  * A child allocated zero rows is parked, and no gap is placed beside it. Collapsing a child with `Sizing.fixed(0)` therefore removes its gutter as well, instead of leaving a double gap behind.

  * Gap rows are left unpainted, so the VBox's own plane base shows through. If you want a visible rule between panes rather than empty space, use [Selkie::Layout::Split](Selkie--Layout--Split.md) (which owns a divider) or add a one-row `Text` child.

`gap` defaults to 0, which allocates and positions exactly as VBox always has.

[Selkie::Layout::Split](Selkie--Layout--Split.md) deliberately has no `gap`: its two panes are separated by a divider row/column it draws and drags itself, and a second, invisible gutter next to that would be a footgun rather than a feature.

Cross-axis alignment
--------------------

A VBox stacks rows, so its *cross* axis is columns. `align-items` says where a child sits horizontally; the child's `cross-sizing` says how wide it is:

```raku
my $page = Selkie::Layout::VBox.new(
    sizing      => Sizing.flex,
    gap         => 1,
    align-items => CrossCenter,
);

# A 40-column card, centred in however wide the page happens to be.
$page.add: Selkie::Widget::Border.new(
    title        => 'Sign in',
    sizing       => Sizing.fixed(9),
    cross-sizing => Sizing.fixed(40),
);

# …and one child that opts out of the container's rule.
$page.add: my $footer = Selkie::Widget::Text.new(
    text         => 'v1.2.0',
    sizing       => Sizing.fixed(1),
    cross-sizing => Sizing.fixed(10),
    align-self   => CrossEnd,
);
```

The rules, in full:

  * `cross-sizing` is a [Selkie::Sizing](Selkie--Sizing.md) resolved against the box's width: `fixed` is a column count, `percent` a share of the width, `flex` (and the default, undefined) the whole width.

  * `align-items` defaults to `CrossFill` and `align-self` is undefined by default, so a plain VBox lays out exactly as it always has — full-width children at column 0.

  * `CrossFill` and `CrossStart` both place a child at column 0; they differ only in that `CrossFill` is the "no opinion" value, and a child that declares a `cross-sizing` keeps it under either.

  * A child that resolves to zero columns is parked — a zero-width plane can't be resized, and a stale one would paint over its neighbours — but it **keeps its rows and its gutter**. Collapsing a child on the cross axis must not reflow the stack on the main one; `sizing =E<gt> Sizing.fixed(0)` is still how you take a child out of the flow entirely.

  * Alignment and `gap` never interact: the gutters come off the row budget, the alignment maths only ever sees columns.

EXAMPLES
========

Classic three-pane stack
------------------------

```raku
my $root = Selkie::Layout::VBox.new(sizing => Sizing.flex);

$root.add: Selkie::Widget::Text.new(
    text   => ' Selkie App',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x7AA2F7, bold => True),
);

$root.add: $main-content;   # sizing => Sizing.flex — fills middle

$root.add: Selkie::Widget::Text.new(
    text   => ' Ctrl+Q: quit',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x666666),
);
```

Weighted distribution
---------------------

```raku
my $vbox = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$vbox.add: $preview;   # Sizing.flex(2) — gets two-thirds
$vbox.add: $output;    # Sizing.flex    — gets one-third
```

SEE ALSO
========

  * [Selkie::Layout::HBox](Selkie--Layout--HBox.md) — horizontal version of the same layout

  * [Selkie::Layout::Split](Selkie--Layout--Split.md) — two-pane split with a draggable divider ratio

  * [Selkie::Sizing](Selkie--Sizing.md) — the fixed/percent/flex sizing model

  * [Selkie::Align](Selkie--Align.md) — the `CrossAlign` values `align-items` takes

  * [Selkie::Layout::Allocate](Selkie--Layout--Allocate.md) — the shared allocation and cross-axis maths

### has UInt $.gap

Rows of empty space reserved between adjacent children. Defaults to 0 — no gutter, and layout identical to a gap-free VBox. The total reservation is `gap-reserve` from [Selkie::Layout::Allocate](Selkie--Layout--Allocate.md): children collapsed to `Sizing.fixed(0)` don't get a gutter.

### method set-gap

```raku
method set-gap(
    Int:D $g where { ... }
) returns Nil
```

Change the inter-child gutter and mark the box dirty so the next render re-runs the allocation. `0` restores the gap-free layout.

### has CrossAlign $.align-items

Where children sit horizontally — the VBox's **cross** axis. Defaults to `CrossFill`: every child is given the box's full width at column 0, exactly as VBox has always laid out. A child overrides this for itself with `align-self`, and controls *how wide* it is with `cross-sizing`. See [Selkie::Align](Selkie--Align.md).

### method set-align-items

```raku
method set-align-items(
    CrossAlign:D $a
) returns Nil
```

Change the cross-axis alignment policy for children that haven't set their own `align-self`, and mark the box dirty so the next render re-runs the layout.

### method render

```raku
method render() returns Mu
```

Perform layout and render each child. Called automatically by the render cycle. The layout pass allocates rows according to every child's `Sizing`: fixed first, then percent, then flex shares the rest.

### method handle-resize

```raku
method handle-resize(
    Int $rows where { ... },
    Int $cols where { ... }
) returns Mu
```

Re-layout children when the parent resizes. Re-runs the same fixed → percent → flex allocation as the initial layout, so children's relative sizing is preserved across resizes. Idempotent on no-size changes.