Layout Algorithm

Understanding how Shaft’s layout system works under the hood.

The Rule

Shaft uses the same layout algorithm as Flutter, based on one simple rule:

Constraints go down. Sizes go up. Parent sets position.

This means:

  1. A widget gets constraints from its parent
  2. The widget decides its size within those constraints
  3. The widget tells its parent what size it chose
  4. The parent decides where to position the widget

What are Constraints?

A constraint is simply four numbers:

  • Minimum width and maximum width
  • Minimum height and maximum height
BoxConstraints(
    minWidth: 100,
    maxWidth: 400,
    minHeight: 50,
    maxHeight: 200
)

A widget can be any size between the minimum and maximum.

Types of Constraints

Tight Constraints

When min equals max, the widget must be exactly that size.

BoxConstraints.tight(Size(100, 100))
// minWidth = maxWidth = 100
// minHeight = maxHeight = 100

Example: The root widget of your app gets tight constraints matching the screen size.

Loose Constraints

When min is zero, the widget can be as small as zero or as large as max.

BoxConstraints.loose(Size(400, 600))
// minWidth = 0, maxWidth = 400
// minHeight = 0, maxHeight = 600

Example: Center gives loose constraints to its child, allowing it to be smaller.

Unbounded Constraints

When max is infinity, the widget can be any size.

BoxConstraints(
    minWidth: 0,
    maxWidth: .infinity,  // Unbounded!
    minHeight: 0,
    maxHeight: 200
)

Example: A Column inside a vertical ScrollView gets unbounded height.

Tight vs Loose: A Summary

ScenarioConstraint TypeWidget Behavior
Root of appTightMust match screen size
Inside CenterLooseCan be smaller
Inside ExpandedTightMust fill allocated space
Inside ScrollViewUnboundedCan be any size
Inside Column (height)UnboundedNatural size
Inside SizedBox(100, 100)TightMust be 100×100

Implementing Custom Layout

If you’re building custom layout widgets, you’ll work with RenderObject:

class RenderCustom: RenderBox {
    override func performLayout() {
        // 1. Receive constraints from parent
        let incoming = constraints
        
        // 2. Layout children with new constraints
        child.layout(
            BoxConstraints(
                minWidth: 0,
                maxWidth: incoming.maxWidth,
                minHeight: 0,
                maxHeight: incoming.maxHeight / 2
            ),
            parentUsesSize: true
        )
        
        // 3. Decide own size
        size = Size(
            width: incoming.maxWidth,
            height: child.size.height * 2
        )
    }
    
    override func paint(context: PaintingContext, offset: Offset) {
        // 4. Position and paint child
        context.paintChild(child, offset: Offset(0, 10))
    }
}

Learn More