layout_engine 0.1.0
layout_engine: ^0.1.0 copied to clipboard
Framework-agnostic layout engine extracted from artisanal. Provides BoxConstraints, RenderObject, RenderBox, RenderRow/RenderColumn, and RenderPadding — no dependencies on any rendering backend.
layout_engine #
Framework-agnostic, constraint-based layout for Dart.
Geometry, render objects (flex, padding, wrap, stack, scroll, align, constrain),
and an optional Element/State tree — no painting or windowing dependency.
Host toolkits (e.g. window_toolkit) adapt paintables into
RenderBox nodes, call layout, then paint from size / offset.
Install #
dependencies:
layout_engine: ^0.1.0
import 'package:layout_engine/layout_engine.dart';
Mental model #
- Parent offers
BoxConstraints(min/max width & height). - Child picks a
Sizeinside those bounds. - Parent places the child with an
Offset.
Always re-layout when the parent size changes. Do not stick to a previous frame’s width when the max constraint grows (window resize, larger viewport).
┌─ parent constraints ─────────────────────┐
│ minW ≤ child.width ≤ maxW │
│ minH ≤ child.height ≤ maxH │
│ ┌─ child ─┐ │
│ │ size │ offset from parent │
│ └─────────┘ │
└──────────────────────────────────────────┘
Quick examples #
Fixed leaf + row with flex #
class FixedBox extends RenderBox {
FixedBox(this.preferred);
final Size preferred;
@override
void layout(BoxConstraints constraints) {
this.constraints = constraints;
size = constraints.constrain(preferred);
}
}
final row = RenderRow(gap: 8, mainAxisSize: MainAxisSize.max);
final a = FixedBox(const Size(40, 24));
final flex = FixedBox(const Size(0, 24))
..parentData = const FlexParentData(flex: 1);
final b = FixedBox(const Size(40, 24));
row
..attach(a)
..attach(flex)
..attach(b);
row.layout(const BoxConstraints(maxWidth: 300, maxHeight: 48));
// a at x=0, flex takes leftover, b after flex + gap
Padding + stretched column #
final col = RenderColumn(
gap: 6,
crossAxisAlignment: CrossAxisAlignment.stretch,
);
col.attach(FixedBox(const Size(20, 16)));
col.attach(FixedBox(const Size(80, 16)));
final pad = RenderPadding(padding: const EdgeInsets.all(12));
pad.attach(col);
pad.layout(const BoxConstraints(maxWidth: 200));
// Both children laid out with tight width = content width inside padding
Align / center #
final child = FixedBox(const Size(40, 20));
final align = RenderPositionedBox(alignment: Alignment.center)
..attach(child);
align.layout(const BoxConstraints.tightFor(width: 200, height: 100));
// child.offset ≈ Offset(80, 40)
Sized box / constraints #
final box = RenderConstrainedBox(
additionalConstraints: BoxConstraints.tightFor(width: 64, height: 32),
)..attach(FixedBox(const Size(10, 10)));
box.layout(const BoxConstraints(maxWidth: 400, maxHeight: 400));
// box.size == Size(64, 32)
Vertical scroll #
final content = FixedBox(const Size(200, 2000));
final ctrl = ViewportScrollController();
final vp = RenderViewport(
controller: ctrl,
scrollDirection: Axis.vertical,
)..attach(content);
vp.layout(const BoxConstraints(maxWidth: 200, maxHeight: 300));
// Child: tight width 200, unbounded height
// Viewport size: 200×300
ctrl.scrollBy(50);
Wrap #
final wrap = RenderWrap(spacing: 8, runSpacing: 8);
for (var i = 0; i < 6; i++) {
wrap.attach(FixedBox(Size(40.0 + i * 10, 24)));
}
wrap.layout(const BoxConstraints(maxWidth: 120));
// Children flow into multiple runs when they exceed maxWidth
Stack + positioned child #
final stack = RenderStack(alignment: Alignment.center);
final bg = FixedBox(const Size(100, 100));
final badge = FixedBox(const Size(16, 16))
..parentData = const StackParentData(right: 4, top: 4);
stack
..attach(bg)
..attach(badge);
stack.layout(const BoxConstraints(maxWidth: 100, maxHeight: 100));
Custom single-child layout #
final custom = RenderCustomSingleChildLayout((child, constraints, setSize, setOffset) {
child!.layout(const BoxConstraints.tightFor(width: 24, height: 24));
setSize(const Size(100, 100));
setOffset(const Offset(76, 76)); // bottom-right-ish badge
});
custom.attach(FixedBox(const Size(24, 24)));
custom.layout(const BoxConstraints(maxWidth: 200, maxHeight: 200));
Element tree (stateful rebuilds) #
class Counter extends StatefulWidget {
@override
State createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int n = 0;
@override
ElementWidget build(BuildContext context) {
// Host toolkit turns this into a paintables widget.
return /* Label('Count: $n') */ this as ElementWidget; // illustrative
}
void inc() => setState(() => n++);
}
final tree = ElementTree()
..onNeedsBuild = () { /* schedule repaint */ }
..mount(Counter());
tree.build();
API map #
Geometry (geometry.dart) #
| Type | Purpose |
|---|---|
Offset, Size, Rect |
Positions and extents |
EdgeInsets |
Padding/margin; deflateSize / inflateSize |
BoxConstraints |
min/max ranges; tighten, loosen, enforce, deflate |
HitTestResult / HitTestEntry |
Hit-test path |
Useful factories:
BoxConstraints.tightFor(width:, height:)— fixed sizeBoxConstraints.expand()— fillBoxConstraints.loose— unbounded
Render objects #
| Object | Role |
|---|---|
RenderObject / RenderBox |
Tree node: layout, size, offset, parentData |
RenderDelegateBox |
Callback-based leaf/host adapter |
RenderRow / RenderColumn |
Flex; flex factor via FlexParentData |
RenderPadding |
Insets via EdgeInsets |
RenderConstrainedBox |
SizedBox / ConstrainedBox |
RenderPositionedBox |
Align / Center |
RenderAspectRatio |
Fixed aspect |
RenderLimitedBox |
Soft max when parent unbounded |
RenderFractionallySizedBox |
Fraction of parent |
RenderRotatedBox |
Quarter-turn layout (axes swap) |
RenderCustomSingleChildLayout |
One-off geometry |
RenderStack |
Layered children; StackParentData |
RenderWrap |
Flow into runs |
RenderViewport |
Clip + scroll; tight cross-axis |
RenderList |
Vertical list content for scroll |
Flex notes #
- Set
child.parentData = FlexParentData(flex: n)for expandable children. - Space is split by total flex factor, not head-count.
- Flex does not expand on an unbounded main axis (intrinsic only).
CrossAxisAlignment.stretchre-lays out children with a tight cross-axis.
Viewport notes #
Vertical scroll gives the child:
- tight width = viewport width
- unbounded height
Horizontal scroll swaps axes. Viewport size is the constraint box, not the content size.
Text measurement #
Some hosts may set a global measure for future text-aware layout:
TextMeasureScope.set(MySkiaTextMeasure());
// TextMeasureScope.current.textWidth('Hello')
Widget framework #
ElementWidget,StatelessWidget,StatefulWidget,State,InheritedWidgetElementTree,BuildOwner,BuildContext- Keys:
WidgetKey,ValueWidgetKey,UniqueWidgetKey
Used for lifecycle (initState / setState / dispose) independent of paint.
Integrating with window_toolkit #
window_toolkit adapters implement RenderBox.layout by calling
Widget.performLayout and reporting size. Containers such as VBox, Padding,
ScrollArea, and Card host these layout objects so sizing math stays in this
package.
Rules of thumb for adapters:
- Always honor parent constraints (never sticky previous width).
- Wire
FlexParentDatafor flex children. - Leaf controls keep intrinsic size under loose max; containers fill max.
- After layout, copy
offset/sizeback onto paintables.
Examples #
Runnable samples live under example/:
cd packages/layout_engine
dart run example/layout_engine_example.dart
Tree dumps #
Debug helpers live in this package (not the host toolkit):
// RenderObject
row.dumpTree(); // stderr
print(row.formatTree());
// Element
elementTree.root?.dumpTree();
// Generic (any host tree)
TreeDump.write(
label: 'root',
children: kids,
labelOf: (n) => n.toString(),
childrenOf: (n) => ...,
);
window_toolkit [Widget.dumpTree] / [Widget.formatTree] delegate to [TreeDump].
Tests #
dart test
Documentation #
Generate API docs:
dart doc .
# open doc/api/index.html
License #
Same as the monorepo workspace.