layout_engine 0.1.0 copy "layout_engine: ^0.1.0" to clipboard
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 #

  1. Parent offers BoxConstraints (min/max width & height).
  2. Child picks a Size inside those bounds.
  3. 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 size
  • BoxConstraints.expand() — fill
  • BoxConstraints.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.stretch re-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, InheritedWidget
  • ElementTree, 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:

  1. Always honor parent constraints (never sticky previous width).
  2. Wire FlexParentData for flex children.
  3. Leaf controls keep intrinsic size under loose max; containers fill max.
  4. After layout, copy offset / size back 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.

0
likes
140
points
13
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Framework-agnostic layout engine extracted from artisanal. Provides BoxConstraints, RenderObject, RenderBox, RenderRow/RenderColumn, and RenderPadding — no dependencies on any rendering backend.

Repository (GitHub)
View/report issues

License

MIT (license)

More

Packages that depend on layout_engine