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.