Skip to content

Layout

Fleury lays out whole terminal cells. Parents describe the space available, children choose a size within it, and the parent positions them. In practice, most interfaces come from a small set of composable widgets:

You need to…Use
Arrange children horizontally or verticallyRow and Column
Give a child the remaining spaceExpanded
Add a fixed gap or boundSizedBox
Add space inside a regionPadding
Adapt to the local widthLayoutBuilder

This workspace uses fixed gaps, weighted panes, and one local breakpoint. The same child becomes two panes when wide and a vertical stack when narrow:

You write · editable
/// Two panes side by side when there is room, stacked when there is not.
class _Workspace extends StatelessWidget {
const _Workspace();
@override
Widget build(BuildContext context) {
const files = Panel(
title: 'Files',
child: Padding(
padding: EdgeInsets.all(1),
child: Text('README.md\nlib/\ntest/'),
),
);
const preview = Panel(
title: 'Preview',
child: Padding(
padding: EdgeInsets.all(1),
child: Text('# Fleury\n\nA framework for terminal apps.'),
),
);
return LayoutBuilder(
builder: (context, constraints) {
final wide = (constraints.maxCols ?? 0) >= 60;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
wide ? 'WIDE · TWO PANES' : 'NARROW · STACKED',
style: const CellStyle(bold: true),
),
const SizedBox(height: 1),
Expanded(
child: wide
? const Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 2, child: files),
SizedBox(width: 1),
Expanded(flex: 3, child: preview),
],
)
: const Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: files),
SizedBox(height: 1),
Expanded(child: preview),
],
),
),
],
);
},
);
}
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

Activate Narrow and Wide to change only the workspace’s parent width. The heading confirms which branch LayoutBuilder selected. This is the same behavior a pane gets when its window, neighboring sidebar, or embedding surface changes size.

Row lays out children horizontally and Column vertically. Their main axis follows that direction; the cross axis runs across it:

Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Project'),
Text(project.name),
],
)

Use mainAxisAlignment to distribute children along the main axis and crossAxisAlignment to align them across it. stretch fills the available cross-axis space. Set mainAxisSize: MainAxisSize.min when a row or column should hug its contents instead of taking all available space.

Children start at their natural size. Wrap a child in Expanded when it should take the main-axis space left after fixed children and gaps:

Row(
children: [
const SizedBox(width: 18, child: Sidebar()),
const SizedBox(width: 1),
const Expanded(child: Editor()),
],
)

Multiple expanded children divide the space by flex. In the workspace demo, 2 and 3 make Files receive two-fifths and Preview three-fifths. A cell left over from integer division goes to the earliest flexible child.

Flexible(fit: FlexFit.loose) offers a child up to its share without forcing it to fill. Spacer() is an expanded empty gap; SizedBox is a fixed gap or bound.

Padding adds interior space in whole cells:

Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
child: ProjectDetails(),
)

Use Container when size, margin, padding, background, border, and alignment belong to one visual region:

Container.filled(
width: 28,
padding: const EdgeInsets.all(1),
border: const BoxBorder(style: BorderStyle.rounded),
child: const Text('Build complete'),
)

Container.filled paints the theme’s surface color behind its content. The plain Container constructor paints no background unless you pass color, so it can sit on a region that is already styled. Container.framed fills like Container.filled and also draws the theme’s border.

Container width and height are outer dimensions, including the border. A 28-cell bordered container has 26 interior columns before padding.

LayoutBuilder reads the constraints of the exact place where it appears. Use it when a reusable pane should respond to its own width, as in the demo. An unbounded maximum, such as the width inside a horizontal scroll view, is null. That is why the demo reads constraints.maxCols ?? 0.

MediaQuery.sizeOf(context) reads the entire surface size and rebuilds on resize. Use it for app-wide decisions such as hiding global chrome below a terminal width; prefer LayoutBuilder for components nested inside that app.

final surface = MediaQuery.sizeOf(context);
final compact = surface.cols < 80;

Both react automatically when a terminal or browser surface resizes. You do not listen to resize events yourself.

ConstrainedBox adds minimum or maximum dimensions without overriding a tighter parent:

ConstrainedBox(
minWidth: 24,
maxWidth: 48,
child: SearchPanel(results: results),
)

IntrinsicWidth and IntrinsicHeight ask a child for its natural extent. They are useful for matching a small group to its widest label, but require an extra measurement pass; do not wrap large subtrees speculatively.

Wrap flows children into another run when the next one no longer fits:

Wrap(
spacing: 1,
runSpacing: 1,
children: tags.map((tag) => Tag(label: tag)).toList(),
)

Stack paints children at one origin, with later children above earlier ones. Use Positioned to place a child at a fixed left and top offset from the stack’s top-left corner, optionally with a fixed width or height. Use IndexedStack when one child is visible but inactive children must remain mounted—for example, stateful tab bodies.

AspectRatio uses cell counts, not physical pixels. Terminal cells are usually taller than they are wide, so aspectRatio: 2.0 reads as visually square more often than 1.0. Treat that as a presentation choice, not guaranteed terminal geometry.

When a row or column cannot fit its children, Fleury clips them. With Dart assertions enabled, for example fleury run --enable-asserts, it also paints a red ▓ marker on the overflowing edge; release builds and a plain dart run only clip. Fix the constraint mismatch: add an Expanded, choose a compact layout, bound the child, or make the content scroll.

APIWhat it does
Row, Column, FlexArrange children on one main axis
Expanded, Flexible, SpacerAllocate remaining main-axis space
SizedBox, ConstrainedBoxAdd fixed or bounded dimensions
Padding, ContainerAdd spacing and visual framing
Center, AlignPosition one child within available space
LayoutBuilderBuilds from local CellConstraints
MediaQuery.sizeOfReads the full surface size in cells
Wrap, Stack, IndexedStackFlow, layer, or preserve alternate children