Docking and DevTools
ui.dock and ui.devtools are the editor-tooling layer. They give you the dockable panels and Chrome-style inspector of a game-engine editor.
Both of these work on the element tree from ui.Layout (built with a Layout.Builder and drawn with a ui.Canvas). They don't plug into Context. Your app builds the tree, draws it, and passes input to them itself. The sections below describe that contract.
Docking
What it can do
- Splits and tab stacks. Panels sit in a tree of horizontal and vertical splits that the user can resize. Each leaf is a stack of tabs.
- Drag to rearrange. Dragging a tab shows a compass over the stack under the pointer. Dropping on the centre adds it as a tab, and dropping on an edge splits that side. Dropping on a tab bar puts the tab between the tabs under the pointer, so tabs reorder within a stack too.
- Floating windows. A panel can float inside the main window, with its own title bar buttons to close, minimise (fold to the tab bar) and maximise (fill the window).
- Pop out to OS windows. A panel can move into its own operating-system window and later dock back. If your app can't open extra windows, set
os_windows = falseand pop-outs float instead. - Tab menu. Every stack has a menu with pop out, float, dock back and close. Middle-clicking a tab closes it too (
closeTab). A closed panel comes back withadd; the gallery lists every panel in its Windows menu. - Native-looking buttons. Floating windows order and style their buttons from
buttons. Useweeoui_sdl3.buttonLayoutto read the desktop's layout. - Accessibility. Splitters are announced as sliders, and tabs and buttons are focusable.
| Limit | Value |
|---|---|
| Panel ids | Nonzero and below dock.max_panel (0x10000) |
| Nodes (splits and stacks) | 64 |
| Tabs per stack | 16 |
| Floating windows | dock.max_floating (8) |
| OS windows | dock.max_windows (8) |
Setting up the dock
Store one DockSpace in your app state and number your panels:
const Panel = enum(u32) { scene = 1, inspector, console, assets };
var dock = ui.dock.DockSpace.init();
// Build the starting arrangement once:
try dock.add(@intFromEnum(Panel.scene), null, .center);
try dock.add(@intFromEnum(Panel.inspector), @intFromEnum(Panel.scene), .right);
try dock.add(@intFromEnum(Panel.console), @intFromEnum(Panel.scene), .bottom);
try dock.add(@intFromEnum(Panel.assets), @intFromEnum(Panel.console), .center); // a tab beside console
add(panel, near, side) puts panel on side of the stack that holds near. When near is null, it uses the root. Other operations you can call from code:
remove(panel)move(panel, node, side)moveTab(panel, node, slot), which puts the tab before tabslotof a stack (also within its own stack)float(panel, rect)detach(panel), which moves the panel into an OS windowredock(host)nodeOf(panel)andisVisible(panel)
Building the dock each frame
build takes a provider: any value that has title and content methods.
const Panels = struct {
app: *App,
pub fn title(_: Panels, panel: u32) []const u8 {
return switch (@as(Panel, @enumFromInt(panel))) {
.scene => "Scene",
.inspector => "Inspector",
.console => "Console",
.assets => "Assets",
};
}
pub fn content(self: Panels, b: ui.Layout.Builder, panel: u32, rect: ui.Rect) !*ui.Layout.Element {
return switch (@as(Panel, @enumFromInt(panel))) {
.scene => b.node(0, .{ .width = rect.w, .height = rect.h }, .none, &.{}), // your viewport goes here
else => b.column(&.{try b.label("...")}),
};
}
};
const root = try dock.build(b, .main, viewport, Panels{ .app = app });
root.layout(viewport, &font);
try root.draw(&canvas);
build(b, .main, ...) builds the main window, including its floating windows. For each popped-out panel, check dock.windows[i]. When it is set, open an OS window of the given size (you can keep your own handle in handle) and build that window with .{ .window = i }. When the user closes such a window, call dock.redock(.{ .window = i }).
Routing input
Every id the dock creates lies at or above ui.dock.first_id. Your hit testing gives you the id under the pointer, and you pass events on like this:
| Event | Call |
|---|---|
| Pointer pressed on an id | dock.press(id, x, y). Returns true if the dock handled it. |
| Middle button pressed on an id | dock.closeTab(id). Returns true if it was a tab, and closes its panel. |
Pointer moved while dock.dragging() | dock.dragTo(x, y) |
| Pointer released while dragging | dock.release(x, y) |
| Enter or Space on a focused id | dock.activate(id). Returns true if handled. |
| Escape | dock.dismissMenu(). Returns true if a menu was open. |
| Choosing the pointer shape | Use dock.cursor() when it isn't null, for example during splitter drags. |
DevTools
DevTools inspects any element tree. It has three tools, each shown in a panel you choose, typically a dock panel:
- Elements: the whole tree, a box model, and editable style properties.
- Performance: frame times and vertex counts.
- Console: your app's log output.
Initialising
var devtools: ui.devtools.Devtools = .{};
defer devtools.deinit(gpa);
To send std.log output to the Console, install the log function and point it at your Devtools:
pub const std_options: std.Options = .{ .logFn = ui.devtools.logFn };
// once devtools exists:
ui.devtools.console = &devtools;
logFn still prints to stderr as usual. It also copies info, warning and error messages into the Console. You can write to the Console directly with devtools.log(.warn, "fmt", .{...}).
Each frame
The page is rebuilt every frame, so property edits made in DevTools are stored as overrides and reapplied every frame. Call these in this order:
// 1. Build and lay out your page, then reapply the user's edits.
const page = try buildPage(b);
page.layout(viewport, &font);
if (devtools.apply(page)) page.layout(viewport, &font);
// 2. Inspect it. `page` has to stay alive until `highlight`.
try devtools.prepare(gpa, page);
// 3. Build each visible tool into the area its panel occupies.
const elements = try devtools.view(b, gpa, .elements, panel_rect);
elements.layout(panel_rect, &font);
// 4. Draw the page and the panels, then the inspector overlay last.
try root.draw(&canvas);
try devtools.highlight(&canvas, gpa);
// 5. Feed the Performance tool.
devtools.recordFrame(frame_ms);
devtools.vertices = canvas.len;
Routing input
DevTools ids lie at or above ui.devtools.first_id.
| Event | Call |
|---|---|
| Click or keyboard activation on an id | devtools.activate(gpa, id) |
| Pointer motion over the page | devtools.pointerMove(x, y) (it highlights the element under the pointer while inspecting) |
| Pointer pressed on the page | devtools.pointerDown(x, y). Returns true when it picked an element. |
Typing into a property field (isField(id)) | insertText(id, text), editKey(gpa, id, key, extend, word), then commit(gpa, id) on Enter |
| Focus leaving a field | devtools.blur(gpa) |
| Escape | devtools.cancel(), then turn off inspecting |
| Arrow keys in the tree | devtools.treeKey(gpa, id, key) returns the row to focus next |
| Tab order | Skip ids for which devtools.skipInTabOrder(id) returns true |
Set devtools.inspecting = true to enter pick mode. In pick mode, hovering highlights elements and a click selects one. Show a crosshair cursor while it's on.
Menu bar
ui.widgets.windowMenubar is the app menu for the top of a window: the same menus as ui.widgets.menubar, as a flush strip window_menubar_height tall instead of a card. Set its style.width to the window's and put your content below it. The gallery (examples/gallery) also auto-hides it like macOS does: View > Auto-hide Menu Bar gives the panels the whole window and slides the bar over them while the pointer is at the top edge or a menu is open.
Selectable text
Static text isn't selectable by default. To let people highlight and copy it, keep a ui.TextSelection. Start one when a drag begins on no control (anchor and focus at the pointer), move its focus with the pointer, and draw with canvas.text_selection set to paint the highlight. root.selectedText(selection, &font) returns the highlighted characters for Ctrl+C. A selection stays inside the text element where it started. The gallery turns this on with View > Selectable Text. Its right-click text menu (Cut, Copy, Paste and Select All, or Copy for highlighted text) is in examples/gallery/demo.zig too: a ui.widgets.menuItems popup placed at the pointer, which hands the clipboard command to the app.
Title bars
ui.titlebar.bar draws a custom title bar with close, minimise and maximise buttons laid out like the desktop's. With SDL3:
weeoui_sdl3.useCustomFrameremoves the system frame.weeoui_sdl3.windowActioncarries out a button press.weeoui_sdl3.buttonLayoutreads the desktop's button order.