UI Layouts

The built-in layout layer is intentionally small. It focuses on high-value primitives that compose cleanly with layout-size policies: Stack for ordered composition, Centered and Frame for compact one-child composition, Viewport for clipped one-surface content, and ScrollArea for classic overflow-style scrolling with optional scroll bars. Use Sections for titled vertical regions, and Buttons for centered action-button rows.

Important

The UI Framework is currently in beta, and parts of the API may change in future releases.

Usage

Building Columns

Use a horizontal stack to place multiple surfaces next to each other. Each child keeps its own size policy, so fixed-width sidebars and one growing main area are easy to express.

auto columns = ui::Stack::create(Orientation::Horizontal);
auto sidebar = ui::Panel::create();
auto mainView = ui::Panel::create();

sidebar->editLayoutMetrics().setPreferredSize(Size{24, 0});
mainView->editLayoutMetrics().setSizePolicy(ui::SizePolicy{ui::SizePolicy::Grow});

columns->addSurface(sidebar);
columns->addSurface(mainView);

Hiding Layout Children

Layouts only allocate space for visible children. Hidden children keep their last rectangle and are ignored until they are shown again. This lets applications keep optional panels in the tree without constantly removing and re-adding them.

auto stack = ui::Stack::create(Orientation::Vertical);
auto header = ui::TextBox::create("Title");
auto details = ui::Panel::create();
auto body = ui::Panel::create();

header->editLayoutMetrics().setFixedHeight(1);
details->editLayoutMetrics().setFixedHeight(4);
body->editLayoutMetrics().setSizePolicy(ui::SizePolicy{ui::SizePolicy::Grow});

stack->addSurface(header);
stack->addSurface(details);
stack->addSurface(body);

details->flags().setVisible(false); // body now receives the space details would have used

How Layout Margins Work

Margins in LayoutMetrics are recommendations to the parent layout. They are different from padding: a surface owns its content and its own padding, while the parent that arranges child surfaces owns the spacing between those children. Metric sizes always describe the child content rectangle without margins.

When a layout places adjacent child surfaces, it collapses the two touching margins. The larger value wins and becomes spacing owned by the layouting parent. In a vertical stack, the previous child’s bottom margin collapses with the next child’s top margin. In a horizontal stack, the previous child’s right margin collapses with the next child’s left margin.

Margins around the outside of a layout depend on the role of that layout:

  • Pure layouts arrange peer surfaces. Stack and Buttons are pure layouts. They consume only the margins between children and propagate surrounding child margins through their own LayoutMetrics. This is what lets nested layouts collapse margins at the level where siblings actually meet.

  • Enclosing layouts place one selected or contained surface inside an area they own. Frame, Pages, and Viewport are enclosing layouts. They collapse their own padding or inset with the enclosed child’s margins and place the child inside the reduced rectangle. The enclosed child’s margins are not propagated further.

For example, in a horizontal stack of vertical stacks, the vertical stacks propagate their left and right child margins. The horizontal stack then collapses the margins between columns. If the vertical stacks consumed those margins themselves, the column spacing would be added instead of collapsed.

Clipping One Content Surface with Viewport

Viewport manages one content surface. The clearest API is setContentSurface(), but the generic child container works too as long as the layout still has at most one child. Removing the content is valid and leaves the viewport empty.

auto viewport = ui::Viewport::create();
auto map = ui::Panel::create();

map->editLayoutMetrics().setFixedSize(Size{200, 80});
viewport->setContentSurface(map);
viewport->setScrollOffset(Position{20, 10});
viewport->setAlignment(Alignment::Center);

viewport->surfaces().remove(map);
viewport->surfaces().add(map);

If the content is smaller than the viewport on an axis, the alignment anchors it inside the viewport. If the content is larger, the scrollOffset moves it relative to that regular placement. The offset is always clamped to the valid range.

Use the common scroll helpers from actions:

auto upAction = ui::Action::create("up");
upAction->addKey(Key::Up);
upAction->setTriggerFn([viewport] { viewport->scrollUp(); });
page->actions().add(upAction);

auto pageDownAction = ui::Action::create("page down");
pageDownAction->addKey(Key::PageDown);
pageDownAction->setTriggerFn([viewport] { viewport->pageDown(); });
page->actions().add(pageDownAction);

auto topAction = ui::Action::create("top");
topAction->addKey(Key::Home);
topAction->setTriggerFn([viewport] { viewport->scrollToTop(); });
page->actions().add(topAction);

Building Classic Overflow Areas

ScrollArea is the high-level layout for a scrollable child surface. It expands by default, owns a viewport, owns persistent scroll bar surfaces, and forwards the same scroll API as the viewport.

auto scrollArea = ui::ScrollArea::create();
auto document = ui::Panel::create();

document->editLayoutMetrics().setFixedSize(Size{120, 80});
scrollArea->setContentSurface(document);
scrollArea->setAlignment(Alignment::TopLeft);
scrollArea->setScrollBarMode(Orientation::Vertical, ui::ScrollBarMode::Automatic);
scrollArea->setScrollBarMode(Orientation::Horizontal, ui::ScrollBarMode::Hidden);

auto downAction = ui::Action::create("down");
downAction->addKey(Key::Down);
downAction->setTriggerFn([scrollArea] { scrollArea->scrollDown(); });
page->actions().add(downAction);

ScrollBarMode::Hidden hides a bar and reserves no space. ScrollBarMode::Automatic shows a bar only when content overflows on that axis. ScrollBarMode::Visible reserves a bar whenever the scroll area’s size permits it. Automatic mode uses a stable pass because one axis can force the other; for example, a vertical bar consumes one column and may make horizontal overflow appear.

The scroll bars and corner are persistent child surfaces. They are shown and hidden with the surface visibility flag instead of being recreated when the scroll area resizes. Access them for styling through horizontalScrollBar(), verticalScrollBar(), and scrollCorner(). These implementation children are protected from generic public mutation, so use setContentSurface() for the user content and leave the scroll area structure intact.

Centering and Framing One Child

Centered manages one content surface, leaves configurable padding around it, and centers the child in the remaining area. It is useful for dialogs and empty-state views.

Frame also manages one content surface. It paints a themed border and optional title, then lays the child into the inner rectangle after border and padding have been removed.

Both layouts inherit the same single-content behavior as Viewport: they tolerate zero children, accept one child through either setContentSurface() or surfaces().add(), and reject a second child with an exception.

auto centered = ui::Centered::create();
auto frame = ui::Frame::create();
auto body = ui::Stack::create(Orientation::Vertical);

centered->setPadding(Margins{4, 2});
centered->setContentSurface(frame);
frame->setTitle(String{"Prompt"});
frame->setPadding(Margins{1, 2});
frame->setContentSurface(body);

// Equivalent when the layout is empty:
// frame->surfaces().add(body);

Building Titled Sections

Use Sections when a vertical area should read as named blocks. Section metadata belongs to the layout, not to the child surface, so the same surface can stay simple and reusable.

auto sections = ui::Sections::create();
sections->addSection(
    ui::TextBox::create("Licensed to.........: Example GmbH"),
    ui::Sections::SectionOptions{String{"License Info"}, String{"[h]"}});

Section options are stored as layout data on the parent-child relation. If you add a child through sections->surfaces().add(surface), the layout creates default empty options for it. You can update them later with setSectionOptions().

The default theme draws ─⟨ title ⟩────. Customize the separator through the Sections theme element: Border defines the separator line and title/right insets, Title defines the title style and padding, and TitleBracket defines the bracket style and bracket glyphs. Title padding is painted as part of the title, while title margins collapse with adjacent one-line text parts. Use theme tags when only selected section groups should use a different decoration.

Laying Out Buttons

Buttons centers button actions and wraps them to additional rows when the available width is too small. Arrow keys, Tab, and BackTab move focus between enabled buttons.

Buttons require ButtonAction so the action and button surface can keep their enabled state synchronized. Generic Action remains the right choice for page actions, surface key bindings, help entries, and scheduled or non-button commands.

auto buttons = ui::Buttons::create();

auto openAction = ui::ButtonAction::create("Open");
openAction->addKey(U'o');
openAction->setTriggerFn([] { openDocument(); });

buttons->addAction(openAction);

Button spacing is resolved from theme::Element::Buttons and theme::Part::Spacing. The spacing part uses block-style content margins: left/right configure horizontal spacing between buttons, and top/bottom configure vertical spacing between wrapped rows.

Buttons also supports generic child mutation for actual surface::Button instances. Non-button children are rejected, and the layout keeps the represented actions synchronized as buttons are added, replaced, or removed.

Reference

class Stack : public erbsland::cterm::ui::Layout

A layout that stacks surfaces vertically or horizontally.

Public Functions

explicit Stack(Orientation orientation, ProtectedTag) noexcept

Create a stack layout with the given orientation.

Parameters:

orientation – The stacking direction for child surfaces.

virtual LayoutMetrics onMeasure(MeasureScope &scope, const LayoutProposal &proposal) noexcept override

Measure this stack from its visible children.

Parameters:
  • scope – Measurement access.

  • proposal – The proposed stack size.

virtual void onLayout(LayoutScope &scope) noexcept override

Recalculate all child rectangles inside this stack.

Parameters:

scope – The layout scope.

Public Static Functions

static StackPtr create(Orientation orientation)

Create a new stack layout.

Parameters:

orientation – The stacking direction for child surfaces.

Returns:

The new stack layout instance.

class Centered : public erbsland::cterm::ui::layout::SingleContentLayout

A one-child layout that centers its content inside padded available space.

Public Functions

explicit Centered(ProtectedTag) noexcept

Create a centered layout.

Margins padding() const noexcept

Access the padding around the centered content.

Returns:

The padding.

void setPadding(Margins padding) noexcept

Replace the padding around the centered content.

Parameters:

padding – The new padding.

virtual LayoutMetrics onMeasure(MeasureScope &scope, const LayoutProposal &proposal) noexcept override

Measure this surface for a proposed size.

The returned layout sizes describe the margin-free content rectangle. The optional margins in the returned metrics are recommendations for the parent layout and are not included in these sizes.

Parameters:
  • scope – Measurement access for child surfaces.

  • proposal – The proposed size.

Returns:

The measured layout metrics.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

Public Static Functions

static CenteredPtr create()

Create a centered layout.

Returns:

The new centered layout.

class Frame : public erbsland::cterm::ui::layout::SingleContentLayout

A one-child layout that surrounds its content with a themed frame and optional title.

Public Functions

explicit Frame(ProtectedTag) noexcept

Create a frame layout.

const String &title() const noexcept

Access the frame title.

Returns:

The current frame title.

void setTitle(String title)

Replace the frame title.

Parameters:

title – The new title.

Margins padding() const noexcept

Access the padding between frame and content.

Returns:

The padding.

void setPadding(Margins padding) noexcept

Replace the padding between frame and content.

Parameters:

padding – The new padding.

virtual LayoutMetrics onMeasure(MeasureScope &scope, const LayoutProposal &proposal) noexcept override

Measure this surface for a proposed size.

The returned layout sizes describe the margin-free content rectangle. The optional margins in the returned metrics are recommendations for the parent layout and are not included in these sizes.

Parameters:
  • scope – Measurement access for child surfaces.

  • proposal – The proposed size.

Returns:

The measured layout metrics.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

virtual bool isOpaque() const noexcept override

Test if this surface paints every cell in its rectangle.

Returns:

true if the surface is opaque.

virtual void onPaint(WritableBuffer &buffer, const PaintContext &context) noexcept override

Paint this surface into the target buffer.

Parameters:
  • buffer – The target buffer.

  • context – The paint context for the current paint pass.

Public Static Functions

static FramePtr create()

Create a frame layout.

Returns:

The new frame layout.

class SingleContentLayout : public erbsland::cterm::ui::Layout

Base class for layouts that manage one optional content surface.

Subclassed by erbsland::cterm::ui::layout::Centered, erbsland::cterm::ui::layout::Frame, erbsland::cterm::ui::layout::Viewport

Public Functions

void setContentSurface(SurfacePtr contentSurface)

Replace the content surface.

Parameters:

contentSurface – The new content surface, or an empty pointer to clear it.

const SurfacePtr &contentSurface() const noexcept

Access the current content surface.

Returns:

The current content surface.

class Viewport : public erbsland::cterm::ui::layout::SingleContentLayout

A layout surface that clips, positions, and scrolls one content surface.

Public Functions

explicit Viewport(ProtectedTag) noexcept

Create a viewport.

Alignment alignment() const noexcept

Get the content alignment for undersized content.

Returns:

The content alignment.

void setAlignment(Alignment alignment) noexcept

Replace the content alignment for undersized content.

Parameters:

alignment – The new alignment.

Position scrollOffset() const noexcept

Get the current scroll offset.

Returns:

The clamped scroll offset.

void setScrollOffset(Position scrollOffset) noexcept

Replace the scroll offset.

Parameters:

scrollOffset – The new scroll offset.

void scrollBy(Position delta) noexcept

Scroll by the given delta.

Parameters:

delta – The scroll delta.

void scrollUp(Coordinate count = 1) noexcept

Scroll one or more rows up.

Parameters:

count – The number of rows.

void scrollDown(Coordinate count = 1) noexcept

Scroll one or more rows down.

Parameters:

count – The number of rows.

void scrollLeft(Coordinate count = 1) noexcept

Scroll one or more columns left.

Parameters:

count – The number of columns.

void scrollRight(Coordinate count = 1) noexcept

Scroll one or more columns right.

Parameters:

count – The number of columns.

void pageUp() noexcept

Scroll one page up.

void pageDown() noexcept

Scroll one page down.

void pageLeft() noexcept

Scroll one page left.

void pageRight() noexcept

Scroll one page right.

void scrollToTop() noexcept

Scroll to the top edge.

void scrollToBottom() noexcept

Scroll to the bottom edge.

void scrollToLeftEdge() noexcept

Scroll to the left edge.

void scrollToRightEdge() noexcept

Scroll to the right edge.

void scrollIntoView(Rectangle contentRect) noexcept

Scroll by the smallest amount needed to make a content rectangle visible.

Parameters:

contentRect – The rectangle in content coordinates.

Size contentSizeForViewport(Size viewportSize) const noexcept

Resolve the content size for a candidate viewport.

Parameters:

viewportSize – The candidate viewport size.

Returns:

The content size.

Size contentSizeForViewport(Size viewportSize, LayoutScope &scope) noexcept

Resolve the content size for a candidate viewport using measurement.

Parameters:
  • viewportSize – The candidate viewport size.

  • scope – The measurement scope.

Returns:

The content size.

Size contentSize() const noexcept

Get the current content size.

Returns:

The current content size.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

Public Static Functions

static ViewportPtr create()

Create a viewport.

Returns:

The new viewport.

class ScrollArea : public erbsland::cterm::ui::surface::AbstractScrollArea

A scroll area layout that displays one content surface with optional scroll bars.

Public Functions

explicit ScrollArea(ProtectedTag) noexcept

Create a scroll area.

void setContentSurface(SurfacePtr contentSurface)

Replace the content surface.

Parameters:

contentSurface – The new content surface, or an empty pointer to clear it.

const SurfacePtr &contentSurface() const noexcept

Access the current content surface.

Returns:

The current content surface.

const ViewportPtr &viewport() const noexcept

Access the viewport.

Returns:

The viewport.

Alignment alignment() const noexcept

Get the content alignment for undersized content.

Returns:

The content alignment.

void setAlignment(Alignment alignment) noexcept

Replace the content alignment for undersized content.

Parameters:

alignment – The new alignment.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

Public Static Functions

static ScrollAreaPtr create()

Create a scroll area.

Returns:

The new scroll area.

class Sections : public erbsland::cterm::ui::Layout

A vertical layout that separates child surfaces with themed title lines.

Public Types

using SectionOptions = layout::SectionOptions

Section options type, kept here for source compatibility.

Public Functions

explicit Sections(ProtectedTag) noexcept

Create a sections layout.

void addSection(SurfacePtr surface)

Add one surface as a section.

Parameters:

surface – The section body surface.

void addSection(SurfacePtr surface, SectionOptions options)

Add one surface as a section.

Parameters:
  • surface – The section body surface.

  • options – The section metadata.

std::size_t sectionCount() const noexcept

Get the number of configured sections.

const SectionOptions &sectionOptions(std::size_t index) const

Access options for one section.

Parameters:

index – The section index.

Returns:

The section options.

void setSectionOptions(std::size_t index, SectionOptions options)

Replace options for one section.

Parameters:
  • index – The section index.

  • options – The new section options.

bool isTrailingSeparatorVisible() const noexcept

Test if a trailing separator is drawn after the last section.

void setTrailingSeparatorVisible(bool visible) noexcept

Change whether a trailing separator is drawn after the last section.

Parameters:

visible – The new visibility state.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

virtual void onPaint(WritableBuffer &buffer, const PaintContext &context) noexcept override

Paint this surface into the target buffer.

Parameters:
  • buffer – The target buffer.

  • context – The paint context for the current paint pass.

Public Static Functions

static SectionsPtr create()

Create a sections layout.

Returns:

The new sections layout.

class Buttons : public erbsland::cterm::ui::Layout

A horizontal button layout that centers rows and wraps when needed.

Public Functions

explicit Buttons(ProtectedTag) noexcept

Create a button layout.

surface::ButtonPtr addAction(ButtonActionPtr action)

Add an action-backed button.

Parameters:

action – The action represented by the new button.

Returns:

The created button surface.

void addButton(surface::ButtonPtr button)

Add an existing button.

Parameters:

button – The button to add.

void insertButton(std::size_t index, surface::ButtonPtr button)

Insert an existing button.

Parameters:
  • index – The insertion index.

  • button – The button to insert.

std::size_t buttonCount() const noexcept

Get the number of buttons in this layout.

surface::ButtonPtr button(std::size_t index) const

Access one button.

Parameters:

index – The button index.

virtual LayoutMetrics onMeasure(MeasureScope &scope, const LayoutProposal &proposal) noexcept override

Measure this surface for a proposed size.

The returned layout sizes describe the margin-free content rectangle. The optional margins in the returned metrics are recommendations for the parent layout and are not included in these sizes.

Parameters:
  • scope – Measurement access for child surfaces.

  • proposal – The proposed size.

Returns:

The measured layout metrics.

virtual void onLayout(LayoutScope &scope) noexcept override

Layout direct children within this surface.

Parameters:

scopeLayout access for measuring and placing child surfaces.

virtual void onKeyPress(KeyPressEvent &keyPressEvent) noexcept override

Handle a key press event.

This is the low-level event handler for key press events. If a surface handles a key press event, it should mark it as handled.

Parameters:

keyPressEvent – The key press event to handle.

Public Static Functions

static ButtonsPtr create()

Create a button layout.

Returns:

The new button layout.