# Layout and smart containers

> Place blocks on portrait and landscape screens with SmartContainer records and breakpoints, then lay out what is inside them with flexbox. pixi-silk/ui/layout.

Source: https://pixi-silk.schmooky.dev/docs/layout/

`SmartContainer` places a whole block on the screen, such as a corner readout, the main view or a
bottom bar, on portrait and landscape screens. `FlexContainer` arranges children inside a block with flexbox. Both come from
`pixi-silk/ui/layout`.

## Place blocks on portrait and landscape screens

Demo sources run in the docs demo runner, `defineDemo({ setup({ stage, params, tick }) })`. In your own app, `stage` is `app.stage` (or a container on it), `params` holds the values of the demo controls, and `tick(fn)` is `app.ticker.add`, with `t` and `dt` in seconds.

Live demo `layout-hud` (source):

```ts
import { Container } from 'pixi.js';
import { SilkGraphics, vertical } from 'pixi-silk';
import { SilkText } from 'pixi-silk/text';
import {
    DEVICES,
    FlexContainer,
    layoutDebug,
    SmartContainer,
    type SmartContainerOptions,
    simulateViewport,
} from 'pixi-silk/ui/layout';
import { defineDemo } from './runtime';

// A media player HUD built from SmartContainers, each placed by a portrait and a landscape record.
// The track title pins to the top left corner, the menu to the top right, the artwork stays
// centred and fits, and the bar stretches along the bottom with a flex row inside. Pick a device:
// the page previews it letterboxed, and the HUD glides to the new breakpoint. Press L, or tick
// debug, for the layout overlay. Hover a box for its readout. Press play to run the clock.

const font = 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif';

/** Cover art for the track: a sun over hills, 1040x640 design units. */
function artwork(): Container {
    const g = new SilkGraphics();

    g.roundRect(0, 0, 1040, 640, 40, 0.6)
        .fill({ gradient: vertical([0x1e1b4b, 0x7c3aed, 0xf472b6]) })
        .circle(520, 360, 170)
        .fill({ gradient: vertical([0xffd60a, 0xff9f0a]) });
    // the hills are cut to the card
    g.boolean(
        'intersect',
        (g) => g.roundRect(0, 0, 1040, 640, 40, 0.6),
        (g) => g.ellipse(260, 640, 420, 190),
    ).fill(0x2e1065);
    g.boolean(
        'intersect',
        (g) => g.roundRect(0, 0, 1040, 640, 40, 0.6),
        (g) => g.ellipse(820, 660, 420, 220),
    ).fill(0x3b0764);
    g.roundRect(0, 0, 1040, 640, 40, 0.6).stroke({ width: 2, color: 0xffffff, alpha: 0.12, alignment: 'inside' });

    return g;
}

/** A pill with a label and a value. The value shrinks to fit the pill, never below 0.55. */
function pill(label: string, value: string, color: number): { box: FlexContainer; value: SilkText } {
    const box = new FlexContainer({ layout: { direction: 'column', padding: [14, 28], gap: 2 } });
    const bg = new SilkGraphics();
    const v = new SilkText({ text: value, fontFamily: font, fontWeight: 700, fontSize: 60, fill: color });

    box.add(bg, {
        position: 'absolute',
        inset: 0,
        onSize: (w, h) => bg.clear().roundRect(0, 0, w, h, 30, 0.6).fill(0x1c1c1e),
    });
    box.add(new SilkText({ text: label, fontFamily: font, fontSize: 38, fill: 0xa1a1aa }));
    box.add(v, { fit: 'scale-down', minScale: 0.55 });

    return { box, value: v };
}

/** A round button with a label on it, `size` design units tall. */
function button(radius: number, color: number, text: string, size: number): Container {
    const c = new Container();

    c.addChild(
        new SilkGraphics().circle(0, 0, radius).fill(color),
        new SilkText({ text, fontFamily: font, fontWeight: 800, fontSize: size, fill: 0xffffff, anchor: 0.5 }),
    );
    c.eventMode = 'static';
    c.cursor = 'pointer';

    return c;
}

const clock = (s: number) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
const LENGTH = 238;

export default defineDemo({
    title: 'Responsive player HUD',
    size: [920, 520],
    // phones: a tall area, so a portrait device shows at about 0.85 of its real size
    phone: { size: [360, 700] },
    controls: {
        device: {
            type: 'select',
            label: 'device',
            options: Object.keys(DEVICES),
            value: 'desktop',
            phoneValue: 'phone portrait',
        },
        notch: { type: 'toggle', label: 'notch', value: false },
        debug: { type: 'toggle', label: 'debug', value: false },
    },
    setup({ silk, stage, params, tick, width, height }) {
        // a device window previewed inside the page. Every SmartContainer follows it.
        const sim = simulateViewport({ area: { x: 0, y: 0, width, height }, device: 'desktop' });
        const debug = layoutDebug(silk.app, { key: 'KeyL' });
        const all: SmartContainer[] = [];
        const make = (label: string, options: SmartContainerOptions) => {
            const c = new SmartContainer({
                ticker: silk.app.ticker,
                viewport: sim,
                // apps usually snap. The demo glides on the switch to show where things go.
                switchTransition: { stiffness: 160, damping: 22 },
                ...options,
            });

            c.label = label;
            all.push(c);
            sim.content.addChild(c);

            return c;
        };

        // background: covers the whole window, under the notch too
        const bg = make('background', {
            portraitData: { width: 1080, height: 1920, fitCover: true, ignoreSafeArea: true },
            landscapeData: { width: 1920, height: 1080, fitCover: true, ignoreSafeArea: true },
        });

        bg.addChild(new SilkGraphics().rect(0, 0, 1920, 1920).fill(0x111113));

        // artwork: centred, fit to the reference resolution, moved up to leave room for the bar
        make('artwork', {
            portraitData: { width: 1040, height: 640, correctionOffsetY: -200 },
            landscapeData: { width: 1040, height: 640, correctionOffsetY: -50 },
        }).addChild(artwork());

        // track title pinned to the top left corner, menu to the top right
        const corner = make('title', {
            layout: { padding: 24 },
            portraitData: { align: 'left', valign: 'top', width: 440, height: 170 },
            landscapeData: { align: 'left', valign: 'top', width: 440, height: 170 },
        });

        corner.add(pill('NOW PLAYING', 'Night Drive', 0xffffff).box, { grow: 1 });

        make('menu', {
            layout: { justifyContent: 'center', alignItems: 'center' },
            portraitData: { align: 'right', valign: 'top', width: 140, height: 140 },
            landscapeData: { align: 'right', valign: 'top', width: 140, height: 140 },
        }).add(button(60, 0x2c2c2e, 'MENU', 34));

        // the bar: along the bottom and as wide as the window, with a flex row inside
        const bar = make('bar', {
            layout: {
                padding: [16, 32, 28],
                gap: 24,
                alignItems: 'center',
                justifyContent: 'space-between',
                media: [{ breakpoint: 'portrait', style: { wrap: 'wrap', justifyContent: 'center', rowGap: 16 } }],
            },
            portraitData: { valign: 'bottom', width: 1080, height: 420, extendWidth: true },
            landscapeData: { valign: 'bottom', width: 1920, height: 200, extendWidth: true },
        });
        const time = pill('TIME', `0:00 / ${clock(LENGTH)}`, 0xffffff);
        const volume = pill('VOLUME', '72%', 0x64d2ff);
        const play = button(76, 0x30d158, 'PLAY', 36);
        const playLabel = play.children[1] as SilkText;
        let playing = false;
        let position = 0;

        // in portrait the play button takes a row of its own above the two pills
        bar.add(play, { media: [{ breakpoint: 'portrait', style: { basis: '100%' } }] });
        bar.add(time.box, { grow: 1, basis: 0, maxWidth: 440 });
        bar.add(volume.box, { grow: 1, basis: 0, maxWidth: 440 });

        play.on('pointertap', () => {
            playing = !playing;
            playLabel.text = playing ? 'PAUSE' : 'PLAY';
        });

        stage.addChild(sim);

        let last = '';
        let debugParam = false;

        tick((_t, dt) => {
            if (playing) {
                const before = Math.floor(position);

                position = (position + dt) % LENGTH;
                if (Math.floor(position) !== before) {
                    time.value.text = `${clock(position)} / ${clock(LENGTH)}`;
                    // new text, new size: measure it again
                    time.box.invalidate(time.value);
                }
            }
            // the checkbox and the L key both toggle the overlay
            if (params.debug !== debugParam) {
                debugParam = params.debug;
                if (debug.enabled !== debugParam) debug.toggle();
            }
            const key = `${params.device}|${params.notch}`;

            if (key === last) return;
            last = key;
            const [w, h] = DEVICES[params.device];

            for (const c of all)
                c.setSafeArea(params.notch ? (w > h ? { left: 44, right: 44 } : { top: 47, bottom: 34 }) : {});
            sim.setDevice(params.device);
        });

        return () => {
            debug.destroy();
            for (const c of all) c.destroy({ children: true });
        };
    },
});
```

The screen above is five SmartContainers. One readout pins to the top left and the menu to the top
right. The main view stays centred, the bar runs along the bottom and the background covers the
whole window.

```ts

const main = new SmartContainer({
    viewport: app,
    portraitData: { width: 1040, height: 640, correctionOffsetY: -200 },
    landscapeData: { width: 1040, height: 640, correctionOffsetY: -50 },
});

app.stage.addChild(main);
```

The container follows `app` and lays out again on every resize. It uses `portraitData` when the
window is taller than wide and `landscapeData` otherwise. Without a `viewport` a SmartContainer
does nothing: it has no window to place itself in.

The scale is the window size over the reference resolution, the smaller ratio, or the larger one
with `fitCover`. Stretch replaces it per axis, and the corrections apply last. `placeBox(record, window)` gives the same placement without a container.

### Layout records

| Field | Default | What it does |
| --- | --- | --- |
| `width`, `height` | 0 | The design box, in design units |
| `viewportWidth`, `viewportHeight` | 1080x1920 portrait, 1920x1080 landscape | The reference resolution |
| `align`, `valign` | `center` | Anchor of the scaled box: `left`, `center`, `right` and `top`, `center`, `bottom` |
| `fitCover` | false | Cover the window instead of fitting inside it |
| `stretchWidth`, `stretchHeight` | false | Scale one axis to span the window. Distorts the content, which suits backgrounds |
| `extendWidth`, `extendHeight` | false | Keep the uniform scale and grow the box to the window. A flex layout spreads out |
| `correctionScaleX`, `correctionScaleY` | 1 | Multiply the scale |
| `correctionOffsetX`, `correctionOffsetY` | 0 | Move the box, in design units |
| `ignoreSafeArea` | false | Place the box in the whole window, under notches |
| `layout` | the container's | Flex style for the children while this record is active |

### Container options

| Option | Default | What it does |
| --- | --- | --- |
| `viewport` | none | An Application or renderer (follows `resize`), a signal of a rectangle, or a plain rectangle (call `updateContainer()` after a change) |
| `portraitData`, `landscapeData` | none | The two orientation records |
| `breakpoints` | none | Named records with a `when(width, height)` test, tried in order before the orientations |
| `safeArea` | none | Insets in screen pixels that shrink the window boxes are placed in |
| `switchTransition` | false | Glide on a spring to the new place and scale when the breakpoint changes. Resizes inside one breakpoint snap |
| `layout` | none | Turns the design box into a flex container |

```ts

const background = new SmartContainer({
    viewport: app.renderer,
    safeArea: { top: 44, bottom: 34 },
    breakpoints: [
        { name: 'ultrawide', when: (w, h) => w / h > 2.1, data: { width: 2560, height: 1080, viewportWidth: 2560, fitCover: true } },
    ],
    portraitData: { width: 1080, height: 1920, fitCover: true, ignoreSafeArea: true },
    landscapeData: { width: 1920, height: 1080, fitCover: true, ignoreSafeArea: true },
});
```

On a window wider than 2.1:1 the `ultrawide` record wins. Otherwise the orientation decides.

### Lay out the children of a box

Without a `layout`, children stay where you put them, in design units. With one, the design box is
a flex container, and `media` rules can switch its style per breakpoint:

```ts

const bar = new SmartContainer({
    viewport: app,
    layout: {
        padding: [16, 32, 28],
        gap: 24,
        alignItems: 'center',
        justifyContent: 'space-between',
        media: [{ breakpoint: 'portrait', style: { wrap: 'wrap', justifyContent: 'center' } }],
    },
    portraitData: { valign: 'bottom', width: 1080, height: 420, extendWidth: true },
    landscapeData: { valign: 'bottom', width: 1920, height: 200, extendWidth: true },
});
const play = new Container();

bar.add(play, { media: [{ breakpoint: 'portrait', style: { basis: '100%' } }] });
```

## Arrange children with flexbox

Live demo `layout-stack` (source):

```ts
import { Container } from 'pixi.js';
import { SilkGraphics } from 'pixi-silk';
import type { AlignItems, FlexDirection, JustifyContent } from 'pixi-silk/layout';
import { SilkText } from 'pixi-silk/text';
import { FlexContainer, layoutDebug } from 'pixi-silk/ui/layout';
import { defineDemo, INK, PAD, SERIES } from './runtime';

// An auto-layout stack, like a Figma frame. Change the item count, direction, gap, padding and
// alignment: cards glide to their new places on springs. The debug overlay draws the boxes and
// cells (blue), the padding (green), the gaps (pink) and real bounds (white). Tick debug, then
// hover a card.

const COLORS = SERIES;
const W = 720;
const H = 420;

/** A card of its own size that redraws at its cell size, so `align: stretch` shows. */
function card(frame: FlexContainer, i: number): Container {
    const c = new Container();
    const g = new SilkGraphics();
    const t = new SilkText({ text: String(i + 1), fontWeight: 700, fontSize: 22, fill: 0xffffff, anchor: 0.5 });

    c.addChild(g, t);

    return frame.add(c, {
        minWidth: 92 + (i % 3) * 16,
        minHeight: 64 + (i % 2) * 16,
        onSize: (w, h) => {
            g.clear()
                .roundRect(0, 0, w, h, 14, 0.6)
                .fill(COLORS[i % COLORS.length]);
            t.position.set(w / 2, h / 2);
        },
    });
}

export default defineDemo({
    title: 'Flex stack',
    size: [W, H],
    controls: {
        items: { type: 'range', label: 'items', min: 1, max: 14, step: 1, value: 8 },
        direction: {
            type: 'select',
            label: 'direction',
            options: ['row', 'column', 'row-reverse', 'column-reverse'],
            value: 'row',
        },
        wrap: { type: 'toggle', label: 'wrap', value: true },
        gap: { type: 'range', label: 'gap', min: 0, max: 40, step: 1, value: 12, unit: 'px' },
        padding: { type: 'range', label: 'padding', min: 0, max: 60, step: 1, value: 24, unit: 'px' },
        justify: {
            type: 'select',
            label: 'justify',
            options: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
            value: 'center',
        },
        align: { type: 'select', label: 'align', options: ['start', 'center', 'end', 'stretch'], value: 'center' },
        debug: { type: 'toggle', label: 'debug', value: false },
    },
    setup({ silk, stage, params, tick }) {
        const frame = new FlexContainer({ ticker: silk.app.ticker, transition: true });
        // the layout debug overlay: boxes, padding, gaps, cells against real bounds. Hover a card.
        const debug = layoutDebug(silk.app, { enabled: false });
        const bg = new SilkGraphics();
        const cards: Container[] = [];
        let last = '';

        frame.resize(W - 2 * PAD, H - 2 * PAD);
        frame.position.set(PAD, PAD);
        frame.add(bg, {
            position: 'absolute',
            inset: 0,
            animate: false,
            onSize: (w, h) => bg.clear().roundRect(0, 0, w, h, 20, 0.6).fill(INK.surface),
        });
        stage.addChild(frame);

        tick(() => {
            const key = JSON.stringify(params);

            if (key === last) return;
            last = key;
            while (cards.length < params.items) cards.push(card(frame, cards.length));
            while (cards.length > params.items) cards.pop()?.destroy({ children: true });
            frame.setStyle({
                direction: params.direction as FlexDirection,
                wrap: params.wrap ? 'wrap' : 'nowrap',
                gap: params.gap,
                padding: params.padding,
                justifyContent: params.justify as JustifyContent,
                alignItems: params.align as AlignItems,
                alignContent: 'center',
            });
            if (debug.enabled !== params.debug) debug.toggle();
        });

        return () => {
            debug.destroy();
            frame.destroy({ children: true });
        };
    },
});
```

```ts

const row = new FlexContainer({ layout: { gap: 12, padding: 16, alignItems: 'center' } });

row.add(Sprite.from('coin.png'), { width: 48, height: 48, fit: 'contain' });
row.add(new Text({ text: 'Balance' }), { grow: 1 });
row.resize(480, 80);
```

The coin takes a 48 by 48 box and the label takes the rest of the row. Call `resize(width, height)`
on a root FlexContainer, or nest it in another FlexContainer or a SmartContainer. A SmartContainer
inside a FlexContainer places itself and takes no space.

Give a child a style with `add(child, style)` or `setLayout(child, style)`. A child is measured by
its local bounds, or by the style's `measure` (for wrapped text, use the `wrapText` helper). Pixi
`Text` and `SilkText` both report real text metrics. The style fields follow CSS flexbox and are
listed in [Layout engine](/docs/layout-engine/#styles).

The container lays out once per frame, and only after a change: a child added, removed or
restyled, a new size, or `invalidate(child)` after new text or a new texture. An idle container is
off the ticker.

| Option | Default | What it does |
| --- | --- | --- |
| `layout` | none | The container's own flex style |
| `transition` | false | Children glide to their new places on springs, scale included. `true` is stiffness 170, damping 26 |
| `watch` | false | Check the children's bounds every frame and lay out again when they change |
| `onLayout` | none | Called after every layout pass |
| `ticker` | `Ticker.shared` | `null` to call `tick()` yourself |

A child with `animate: false` never glides. A child with `onSize` is called with its box size
whenever that changes, which suits a panel background. Such a child measures as zero, so size it
with `inset`, `grow` or a size:

```ts

const panel = new FlexContainer({ layout: { padding: 16 } });
const bg = new SilkGraphics();

panel.add(bg, {
    position: 'absolute',
    inset: 0,
    onSize: (w, h) => bg.clear().roundRect(0, 0, w, h, 18, 0.6).fill(0x1c1c1e),
});
```

## Fit art and shrink text into a box

Live demo `layout-fit` (source):

```ts
import type { Container } from 'pixi.js';
import { SilkGraphics } from 'pixi-silk';
import { SilkText } from 'pixi-silk/text';
import { type FitMode, FlexContainer } from 'pixi-silk/ui/layout';
import { defineDemo, INK, PALETTE } from './runtime';

// The same 200x120 artwork in five boxes, one per fit mode, like CSS object-fit. Resize the
// boxes and move the alignment. The last box shrinks a long title to fit, down to a floor.

const MODES: FitMode[] = ['none', 'contain', 'cover', 'scale-down', 'fill'];
const ALIGN: Record<string, [number, number]> = { center: [0.5, 0.5], start: [0, 0], end: [1, 1] };

/** A 200x120 card: sky, sun and hills. */
function artwork(): Container {
    const g = new SilkGraphics();

    // everything stays inside the card, so its bounds are exactly 200x120
    g.roundRect(0, 0, 200, 120, 16, 0.6).fill(PALETTE.blue);
    g.circle(150, 38, 18).fill(PALETTE.yellow);
    g.poly([0, 84, 50, 62, 110, 86, 160, 70, 200, 82, 200, 104, 184, 120, 16, 120, 0, 104]).fill(PALETTE.green);

    return g;
}

export default defineDemo({
    title: 'Fit modes',
    size: [720, 360],
    // phones: three rows of two boxes, and labels one step larger
    phone: { size: [440, 520] },
    controls: {
        width: { type: 'range', label: 'box width', min: 60, max: 200, step: 1, value: 170, unit: 'px' },
        height: { type: 'range', label: 'box height', min: 50, max: 120, step: 1, value: 100, unit: 'px' },
        align: { type: 'select', label: 'fitAlign', options: ['center', 'start', 'end'], value: 'center' },
    },
    setup({ silk, stage, params, tick, width, height, phone }) {
        // two rows of three boxes, or three rows of two on phones
        const perRow = phone ? 2 : 3;
        const grid = new FlexContainer({
            ticker: silk.app.ticker,
            layout: {
                direction: 'column',
                gap: 18,
                padding: phone ? 16 : 24,
                alignItems: 'center',
                ...(phone ? { justifyContent: 'center' as const } : {}),
            },
        });
        const rows = Array.from({ length: 6 / perRow }, () =>
            grid.add(
                new FlexContainer({ layout: { gap: phone ? 16 : 24, justifyContent: 'center', alignItems: 'start' } }),
            ),
        );
        const boxes: { box: FlexContainer; art: Container }[] = [];
        const label = (text: string) => new SilkText({ text, fontSize: phone ? 14 : 13, fill: INK.muted });
        const makeBox = (title: string) => {
            const column = rows[Math.floor(boxes.length / perRow)].add(
                new FlexContainer({ layout: { direction: 'column', gap: 10, alignItems: 'center' } }),
            );
            const box = column.add(new FlexContainer());
            const frame = new SilkGraphics();
            const clip = new SilkGraphics();

            // the box outline, drawn at its size
            box.add(frame, {
                position: 'absolute',
                inset: 0,
                onSize: (w, h) => {
                    frame.clear().roundRect(0, 0, w, h, 10).fill(INK.surface).stroke({ width: 1, color: INK.line });
                    clip.clear().roundRect(0, 0, w, h, 10).fill(0xffffff);
                },
            });
            // the mask clips cover and fill to the box. It goes on the box, not on the child:
            // a masked child measures only what its mask shows.
            box.add(clip, { position: 'absolute', inset: 0, onSize: () => {} });
            box.mask = clip;
            column.add(label(title));

            return box;
        };

        for (const fit of MODES) {
            const box = makeBox(fit);

            boxes.push({ box, art: box.add(artwork(), { fit }) });
        }
        // text shrink-to-fit: scale-down with a minimum scale
        const textBox = makeBox('scale-down text');
        const text = textBox.add(
            new SilkText({ text: 'Quarterly report', fontWeight: 800, fontSize: 30, fill: INK.text }),
            {
                fit: 'scale-down',
                minScale: 0.35,
            },
        );

        stage.addChild(grid);
        grid.resize(width, height);

        let last = '';

        tick(() => {
            const key = JSON.stringify(params);

            if (key === last) return;
            last = key;
            const fitAlign = ALIGN[params.align];

            for (const { box, art } of boxes) {
                box.setStyle({ width: params.width, height: params.height });
                box.setLayout(art, { ...box.getLayout(art), fitAlign });
            }
            textBox.setStyle({ width: params.width, height: params.height });
            textBox.setLayout(text, { ...textBox.getLayout(text), fitAlign });
        });

        return () => grid.destroy({ children: true });
    },
});
```

`fit` scales a child into its box, like CSS `object-fit`.

| Value | What it does |
| --- | --- |
| `none` | Keeps the child's size. The default |
| `contain` | Fits inside the box |
| `cover` | Fills the box and overflows. Mask the box, not the child: a masked child measures only what its mask shows |
| `scale-down` | `contain` that never enlarges |
| `fill` | Stretches both axes |

`fitAlign` places the child in its box, from `[0, 0]` (top left) to `[1, 1]` (bottom right). The
default is the centre. `minScale` and `maxScale` clamp the fit scale.

Text shrink-to-fit is `fit: 'scale-down'` with a floor. The text keeps its size while it fits and
scales down when its box gets narrow, never below `minScale`:

```ts

const pill = new FlexContainer({ layout: { direction: 'column', padding: [10, 18] } });

pill.add(new SilkText({ text: '$1,250,000.00', fontSize: 30 }), { fit: 'scale-down', minScale: 0.55 });
```

## Responsive styles

Any style can carry `portrait` and `landscape` values, and `media` rules. A rule matches an
`orientation`, a `breakpoint` name, or a `minWidth` and `maxWidth` in design units. Rules apply in
order over the base values. Width and orientation rules test the root container's layout area.

```ts

const main = new FlexContainer({
    layout: {
        gap: 20,
        landscape: { direction: 'row' },
        portrait: { direction: 'column' },
        media: [{ maxWidth: 600, style: { gap: 10 } }],
    },
});
const banner = new FlexContainer();

main.add(banner, { landscape: { display: 'none' } });
```

The banner shows in portrait only. A child with `display: 'none'` is hidden as well as taken out of the flow.

## Debug and preview devices

```ts

const debug = layoutDebug(app, { key: 'KeyL' });
const sim = simulateViewport({ area: app.screen, device: 'phone portrait' });
const hud = new SmartContainer({ viewport: sim, portraitData: { width: 1080, height: 300, valign: 'bottom' } });

sim.content.addChild(hud);
app.stage.addChild(sim);
sim.setDevice('phone landscape');
```

`layoutDebug` draws over the stage and is off until you toggle it with the key. For a
SmartContainer it shows the design box with its breakpoint and scale, the reference resolution in
purple, the safe area in orange and stretched axes in yellow. For a FlexContainer it shows boxes in
blue, padding in green, gaps in pink and real bounds in white, or red when a child overflows.

`simulateViewport` shows a device window inside a desktop page, scaled to fit and letterboxed.
Pass it as the `viewport` of your SmartContainers and add them to its `content`. `DEVICES` lists
the preset sizes, and a `[width, height]` pair works too.

## Where it stops

The layout runs on the CPU in your frame, once per change. Rotated children keep their box but
are measured unrotated. The engine has no `order`, `overflow` or borders, and an item's default
minimum size is 0, not its content. [Layout engine](/docs/layout-engine/#differences-from-css)
lists every difference from CSS.
