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:
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:
///
/// 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():
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()andContainer::Build()append children to the existing tree— calling either method a second time appends duplicate children. Calllayout()->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:
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():
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:
///
/// 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.