Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BLayoutBuilder: Compose Layout Trees Without Losing Ownership

Compose Haiku group, grid, split, and card layouts with BLayoutBuilder while managing nested builder scope, view ownership, and mutation order.

BLayoutBuilder is a set of C++ templates that constructs Haiku layout trees through chained calls. It complements BGroupLayout, BGridLayout, and related layout classes by making nested structure readable at the point where views are assembled. The builder is not a different layout engine: the resulting layout objects and the Interface Kit’s preferred-size negotiation still determine geometry.

The builder’s nesting model is stack-like. AddGroup(), AddGrid(), and related methods return a builder for the nested layout; End() returns to the parent builder. Understanding that scope is essential when a chain becomes long. A method call made at the wrong nesting level may attach a view to the wrong layout while still compiling.

Start with the container that owns the root layout

Choose the root view or window first, then attach the builder to that target. A group layout represents a horizontal or vertical sequence; a grid represents row and column relationships; a split layout communicates adjustable panes; a cards layout manages alternate pages. Use the simplest structure that captures the UI’s semantic organization instead of choosing a builder only because a method chain looks compact.

BView* content = new BView("content", B_WILL_DRAW);
BButton* apply = new BButton("apply", "Apply",
    new BMessage(kApplyChanges));
BTextControl* name = new BTextControl("name", "Name:", "",
    new BMessage(kNameChanged));

BLayoutBuilder::Group<>(content, B_VERTICAL)
    .SetInsets(B_USE_DEFAULT_SPACING)
    .Add(name)
    .AddGroup(B_HORIZONTAL)
        .AddGlue()
        .Add(apply)
    .End();

This sketch assumes the usual Haiku Interface Kit declarations and a valid message constant. It illustrates builder scope: AddGroup() pushes the horizontal child builder, and End() returns to the outer vertical group. Check allocation and message ownership in production code. Once views are inserted into a layout/view tree, follow the documented ownership behavior and do not delete a child while its parent still owns it.

You can retrieve the layout object when later configuration needs APIs beyond the builder chain. Keep the root layout’s lifetime tied to its target view and avoid retaining raw layout pointers after that view is destroyed. A chained expression does not make ownership implicit or remove the need for teardown discipline.

Treat nested builder scope as a type-level stack

Each nested builder is a type associated with its parent. That is why End() returns the correct builder type rather than a generic layout handle. When reading a chain, indent according to nesting and call End() at the point the child subtree is complete. If a function mixes builder scopes with conditionals, split it into helpers that return or configure one coherent subtree.

Do not assume every Add() overload applies to every layout type. Group, grid, split, and card builders expose different operations and constraints. If a method is unavailable or geometry is surprising, inspect the exact builder class and the underlying layout’s contract rather than casting to another type. The builder’s compile-time interface is part of the API’s safety model.

The builder can accept views or layout items. Know which object is being transferred into the layout and who owns it afterward. When a nested helper creates a view and returns it, document whether the caller or parent view assumes responsibility. Avoid deleting a child from an error path after insertion succeeded unless it has first been removed safely from the parent.

Avoid side effects in chained arguments

The official LayoutBuilder documentation warns that C++ does not impose sequence points between function arguments in a method chain. Expressions such as row++ passed to successive Add() calls can be evaluated in an unexpected order. Use explicit indices computed before the chain, or put one operation per statement when ordering matters. This is not merely a style preference; it can place views into the wrong cells.

int32 row = 0;
BLayoutBuilder::Grid<>(gridView)
    .Add(nameLabel, 0, row)
    .Add(nameControl, 1, row);
row++;
BLayoutBuilder::Grid<>(gridView)
    .Add(pathLabel, 0, row)
    .Add(pathControl, 1, row);

The excerpt prioritizes explicit sequencing. In a larger form, prefer a helper that takes the row number by value and returns the next row, or construct a logical row in one function. Do not share a mutable counter across chained calls and infer execution order from visual indentation.

Builder chains can also become hard to debug if every view is created inline. Give important controls names and variables, build the tree in logical sections, and validate that each insertion succeeded when the API returns a status or Boolean result. The method chain should clarify the layout, not hide failed allocations or partial setup.

Dynamic changes and window locking

Layout trees are mutable, but a layout update can change preferred sizes, focus order, and the visible hierarchy. Perform view-tree mutations on the window’s looper thread or use the documented locking/message pattern for the target view. Keep slow I/O and network work outside the window lock. If a background result adds a row, send a message to the window and apply the change in its handler.

When removing a view, detach it from the layout using the supported view/layout APIs and update any pointers held by the model or callbacks. End() is a builder-scope operation; it is not a runtime detach operation. Do not try to reuse a finished builder to mutate a live layout unless the builder API explicitly supports that construction path. For substantial dynamic changes, a dedicated helper that owns a stable layout pointer may be clearer.

Set insets and spacing intentionally. The builder’s SetInsets() configures the underlying layout’s margins; it does not replace each control’s preferred size or label relationship. Test long translated labels, minimum window sizes, and enlarged fonts. Fixed pixel widths embedded in an otherwise flexible tree can cause clipping as content changes.

Verification and compatibility

Test the root view’s minimum and preferred size, nested groups, grid spans, split weights, card switching, and focus traversal. Resize the window in both directions and confirm every control remains visible and actionable. Destroy the window while a callback is pending and verify that the view tree does not retain or access deleted children.

The official source documentation marks BLayoutBuilder as available since Haiku R1. Still, compile against the SDK version actually targeted by the application and review current headers before relying on a specific overload. Keep layout code localized so changes in template interfaces do not spread across the application’s data model.

Treat a builder expression as construction code, not as a declarative transaction. If a child constructor fails or an optional control is absent, branch before adding it and decide how the remaining rows should realign. Keep the model and view creation separate enough that a failed layout allocation does not leave application state half-initialized. For dynamic forms, rebuild only the portion whose controls changed, then test focus order and tab traversal after insertion or removal; a visually correct row can still leave keyboard navigation in an unexpected sequence.

Establish an acceptance test for each layout state rather than checking only the initial window. If validation reveals an error row, make sure the new row participates in preferred-size calculation and does not push the primary action outside the minimum window. If a control becomes unavailable, remove its layout item and also clear any callback or model reference that would still target it. The layout builder creates the initial tree; the owning view remains responsible for later model-to-view consistency.

Acceptance criteria

Accept a builder-based UI when nested scope is visually clear, layout insertion and ownership are explicit, side effects are not hidden in argument expressions, and resizing/focus tests pass. Confirm the same UI behaves correctly when content strings grow and optional controls are added or removed.

BLayoutBuilder improves composition syntax for Haiku layouts, but it does not eliminate layout ownership, UI-thread, preferred-size, or compatibility responsibilities.

Related:

Sources:

Comments