docs

Laying Out Panels

Arrange web views into rows, columns, and resizable split panes inside an AppCore window.

On this page

You can arrange multiple web views across an AppCore window using a tree of panels and containers. Each panel hosts a single view, while containers organize them into rows and columns.

You declare sizes once— the window re-resolves the layout whenever it resizes or its display scale changes.

Panels tile the window surface. To float overlays like toasts or dialogs above the layout, see Floating Panels.

Building the Layout

You can build a window layout step by step using panels and containers, or declare the entire tree at once.

Adding a Single Panel

Call Window::AddPanel() with no arguments to create a panel that fills the entire window:

C++
window->AddPanel()->view()->LoadURL("file:///app.html");

Call Panel::view() to access the hosted View and load web content.

For the equivalent C functions, see AppCore in C.

Arranging Rows and Columns

Call Window::layout() to access the root container, then add child rows or columns to split the space:

C++
///
/// Split the window into a fixed-width sidebar and a fluid content pane.
///
RefPtr<Container> body = window->layout()->AddRow({ .key = "body" });
RefPtr<Panel> sidebar =
    body->AddPanel({ .key = "sidebar", .size = "240px", .min_size = "160px" });
RefPtr<Panel> content = body->AddPanel({ .key = "content" });

sidebar->view()->LoadURL("file:///sidebar.html");
content->view()->LoadURL("file:///content.html");

The window's root container is a column that fills the window. Calling Container::AddRow() or Container::AddColumn() nests containers inside it, while Container::AddPanel() appends a panel to host web content.

Setting a key on any node lets you look it up later using Window::FindPanel() or Container::Find().

Single-Expression Layouts

Declare an entire layout tree in a single expression using Window::BuildLayout() alongside panel(), row(), and column():

C++
RefPtr<Panel> toolbar, sidebar, content;

window->BuildLayout({},
  panel({ .key = "toolbar", .size = "44px", .fixed = true }, &toolbar),
  row({ .key = "body", .resizable = true },
    panel({ .key = "sidebar", .size = "240px" }, &sidebar),
    panel({ .key = "content" }, &content)));

toolbar->view()->LoadURL("file:///toolbar.html");

Passing pointers to node handles populates them directly during construction, so you don't need to look them up by key later.

👍 Appending Layout Elements

Window::BuildLayout() and Container::Build() append children to the existing tree— calling either method a second time appends duplicate children. Call layout()->RemoveAll() first if you want to rebuild from a clean slate.

Sizing Panels

You can control how nodes share space across a container and let users resize panes with draggable dividers.

Sizing Panels and Containers

A child node accepts fixed, percentage, or flexible sizes along its parent container's axis:

Value Description
"240px" or 240 Logical pixels.
"30%" Percent of the container's free space.
"2fr" Flex share of the free space.

A child created without an explicit size receives a single flex share (1fr).

You can constrain child dimensions with min_size and max_size. Containers also accept gap and padding, which take values in logical pixels only.

Call LayoutNode::SetSize(), LayoutNode::SetMinSize(), or LayoutNode::SetMaxSize() to adjust sizes at runtime. The library batches layout updates into a single pass on an upcoming frame— LayoutNode::bounds() reflects the updated geometry once that pass completes.

Creating Resizable Split Panes

Set resizable = true on a container to place draggable dividers between its direct children:

C++
RefPtr<Container> body =
    window->layout()->AddRow({ .key = "body", .resizable = true });

Dragging a divider resizes the panes on either side. Double-clicking a divider restores both neighboring panes to their declared sizes.

You can set min_size to prevent drags from collapsing a pane, or set fixed = true on a child so divider drags never resize it.

Styling Dividers

Customize divider appearance across the window with Window::SetDividerStyle(), or per container with Container::SetDividerStyle():

C++
window->SetDividerStyle({ .visual_thickness = 1, .color = "#3c3c3c" });

You can configure the pointer hit width, visual line thickness, line color, and hover highlight color. Leaving the line color unset paints no line, allowing the window background to show through.

Hiding and Removing Nodes

Call LayoutNode::Hide() to hide a node and redistribute its space to sibling nodes. Calling LayoutNode::Show() restores the node at its previous size.

Hiding a container hides its entire subtree. When a panel is hidden, its hosted View suspends painting and animations, and the page receives a visibilitychange event.

To detach a node, call Container::Remove() on its parent container. Removing a panel drops its reference to the hosted View, but any View you keep in a RefPtr survives and can be adopted into another panel with Container::AdoptPanel().

📘 Node Handle Lifetimes

Node handles reference nodes without keeping them or their window alive— releasing a handle does not remove the node. After a node is removed or its window closes, handles remain safe to use— mutations do nothing, and Panel::view() returns null.

Embedding Custom Content

An empty container reserves layout space without painting anything, providing a dedicated region for native rendering (such as a 3D game viewport).

Tracking Region Bounds

Register a layout listener with LayoutNode::OnLayoutChange() to receive updated geometry whenever the node moves or resizes:

C++
///
/// Reserve the right-hand pane for the engine's 3D viewport.
///
RefPtr<Container> viewport = body->AddColumn({ .key = "viewport" });

///
/// The callback fires for as long as viewport_tracker_ is kept.
///
viewport_tracker_ = viewport->OnLayoutChange([](LayoutNode& node) {
  ResizeViewport(node.device_bounds());
});

Call LayoutNode::device_bounds() to get the node's rectangle in window back-buffer device pixels (the coordinate space used for native drawing), or LayoutNode::bounds() for container-local logical pixels. Both values remain zero until the first layout resolve completes.

The LayoutNode::OnLayoutChange() callback fires at most once per frame when bounds change. Store the returned LayoutCallback instance— destroying it unregisters the listener.

Rendering Beneath and Above

Implement WindowListener::OnClear() to render beneath web views, or WindowListener::OnPaint() to render above them.

When rendering from these callbacks, draw through the GPU driver into the window's render buffer (accessible via Window::render_buffer_id()), or onto the native window surface for CPU-rendered windows. Native drawing must never present the frame, and you cannot rely on GPU pipeline state persisting past the callback.

To keep animations updating when no web view has changed, call Window::Invalidate() to schedule the next frame.

Overriding Container Layout

Call Container::SetLayoutOverride() to install a layout delegate that places direct children manually. This lets you implement custom positioning schemes that rows and columns cannot express.

Managing Keyboard Focus

Clicking a panel grants it keyboard focus. To set focus programmatically, call Panel::Focus(). Attempting to focus a hidden panel is ignored with a warning.

To inspect which panel currently owns focus, call Window::focused_panel().

🚧 Focusing Panels

Never call View::Focus() directly on a panel's view. Focusing the view directly prevents the window from routing keyboard input to the active panel.

For handling text input methods and on-screen keyboards, see Keyboard Focus and Editable State.