Layout system
A LayoutManager is attached to a container Component. On every doLayout() call it reads the size hints of each child, resolves each child's LayoutConstraints, and writes pixel-level position and size values to the children.
The framework ships 12 layout managers covering the desktop UI repertoire — Border, HBox, Grid, Split, and so on. This page is about the underlying mechanics they share.
What runs in a layout pass
parent.doLayout()
│
├── layoutManager.doLayout() reads:
│ container.getInnerSize() ─→ available rectangle
│ child.getPreferredSize() ─→ size hint per child
│ container.getLayoutConstraints(child) ─→ constraint object
│
├── for each child:
│ compute (x, y, width, height)
│ child.setPosition(x, y)
│ child.setSize(width, height)
│
└── recurse: each child layout-manager runs against its subtreeThe layout pass walks the tree top-down. Each child's setSize triggers its own layout pass for its children, recursively.
Constraints
Constraints are the second argument to addComponent. The shape depends on the layout manager:
panel.addComponent(child, { region: Placement.NORTH }); // Border
panel.addComponent(child, { fill: FillType.HORIZONTAL }); // HBox / VBox
panel.addComponent(child, new AccordionConstraints('Section 1')); // Accordion
panel.addComponent(child); // Absolute, Fit, Card, GridaddComponents accepts the same per-child constraints by wrapping each child in a ConstrainedComponent pair:
panel.addComponents(
{ component: header, constraints: { region: Placement.NORTH } },
{ component: content, constraints: { region: Placement.CENTER } },
footer // bare component, no constraints
);The same pair shape can appear in a constructor's components option for a fully declarative tree.
The shared base LayoutConstraints carries fill (FillType) and anchor (AnchorType) for managers that honour them. See the Constraints reference.
Fill and anchor resolution
When a layout assigns a child a cell larger than the child's preferred size, fill and anchor decide the result:
fill: NONE+anchor: NORTHEAST→ child stays at its preferred size, pinned to the top-right of the cell.fill: HORIZONTAL→ child stretches to the cell's width but keeps preferred height;anchordecides vertical placement.fill: BOTH→ child fills the cell entirely;anchorbecomes irrelevant.
This is the behaviour that drives HBox, VBox, Row, Column, and Grid.
Triggering layout
Three kinds of trigger exist:
- Initial render — the first time a component is laid out by its parent.
- Viewport resize —
Bodylistens onwindow.resizeand re-runs layout from the root. - Explicit — call
parent.doLayout()after mutating children.
rAF-coalesced scheduling
Setters call an internal scheduleLayout() instead of doLayout() directly. The framework queues all dirty components and flushes them once per animation frame. Components whose ancestor is also dirty get pruned because the ancestor's pass will recurse into them anyway. This means you can setPreferredSize ten components in a single tick and get one layout pass, not ten.
When to call doLayout manually
Almost never. The cases:
- After mutating a
LayoutManager's configuration (e.g.hbox.setSpacing(8)) — usually the manager handles it. - After changing a child's preferred size from outside the framework's setter pipeline (e.g. you wrote directly to the DOM and need to resync).
- Inside
pauseLayout/resumeLayoutblocks —resumeLayoutalready triggers adoLayout.
For most code, setPreferredSize and friends do the right thing automatically.
Building a custom layout manager
If none of the built-in managers fit, subclass LayoutManager:
import { Component } from '@jimka/typescript-ui/core';
import { LayoutManager } from '@jimka/typescript-ui/layout';
class FlowLayout extends LayoutManager {
private hgap = 8;
private vgap = 8;
doLayout(): void {
const container = this.getContainer();
if (!container) return;
const inner = container.getInnerSize();
if (!inner) return;
let x = 0;
let y = 0;
let rowHeight = 0;
for (const child of container.getComponents()) {
const pref = child.getPreferredSize() ?? { width: 100, height: 24 };
if (x + pref.width > inner.width) {
x = 0;
y += rowHeight + this.vgap;
rowHeight = 0;
}
child.setPosition(x, y);
child.setSize(pref.width, pref.height);
x += pref.width + this.hgap;
rowHeight = Math.max(rowHeight, pref.height);
}
}
}LayoutManager itself handles:
- Storing per-child constraints in a
Map<string, LayoutConstraints>(one entry per child id). - Resolving the active
fillandanchoragainstgetInnerSize(). - Notifying the framework when the layout's preferred size changes.
You typically only need to override doLayout; helper methods like placeComponent(child, x, y, w, h) apply fill / anchor consistently if you delegate to them.
Common pitfalls
- Forgot the layout manager. A bare
Componentwith children but no manager defaults toAbsolute, which means none of them get positioned. Set a manager explicitly. - Querying size before render.
getSize()returnsnullfor components that haven't been laid out yet. Wait until the parent has had a chance to lay out the subtree. - Mutating during
doLayout. Adding or removing components from inside a layout callback re-enters the layout pass. The rAF queue handles this safely (changes coalesce into the next frame), but the immediate layout call you triggered won't see them.