The app loop, regions & jobs
Event-driven, idle-cheap
App runs the main loop. It is event-driven: it blocks waiting for input (or a worker wakeup()), then repaints only the regions that are dirty. An idle window stays idle, doing zero GPU work and zero CPU spin: the OS wakes the loop when something actually happens.
ui::App app(*sys, *window);
app.add(std::move(menuBar)); // top-level regions, painted in order
app.add(std::move(dock));
app.setLayout([&](Rect r){ /* position regions within the window */ });
app.run();
Each frame, the loop drains all queued events, dispatches them, applies any notifications, and redraws once, so a burst of mouse-moves coalesces into a single repaint. That bounds latency to a frame and skips redundant work.
Regions
The screen is divided into regions. A Region is a rectangle (in logical points) that knows how to draw, handle an event, and react to a notification (onNotify); it carries a dirty flag. Events route by hit-test to the region under the cursor, with a capture slot so a modal drag (a slider, a divider) keeps receiving moves until it finishes. A WidgetView is a region that hosts a widget tree, and the docking shell is a region that tiles sub-regions.
Per-region offscreen buffers
The reason panning one panel does not repaint the whole window: each region renders to its own persistent GPU texture, and a frame only re-renders the regions whose dirty flag is set; the rest are re-composited from their cached textures. Move the viewport and only the viewport's layer is repainted, while the inspector and outliner are blitted untouched. This keeps the toolkit fluid, and it is automatic: a region opts in by being a dock editor.
Animation
Smooth motion is a loop concern, not a per-widget afterthought. Each frame the loop advances every region's animate(now) with a single frame-coherent timestamp, so all motion shares one clock, and it keeps ticking at frame cadence only while something is actually animating — otherwise the loop idles and animation costs nothing at rest. A small easing library (gpu/Easing.h: ease-in/out curves plus a Tween<T> that interpolates a value over a time window) is the shared math behind scroll-fling momentum, the overlay scrollbar's fade, and any panel, popup or focus-ring transition. A widget tweens inside its animate(now) and reports busy while the tween runs; when every tween settles, the loop blocks for the next event again.
Jobs and notifiers
Long work must never block the loop. Two pieces handle that:
- The job system runs work off the main thread. A worker reports progress and results through a notifier, a thread-safe "this changed, redraw later" signal, and can
wakeup()the loop so the result appears promptly. - The notifier queue decouples what changed from when we draw: a notification marks regions dirty and schedules a repaint, rather than drawing synchronously from wherever the change happened.
This is exactly how the Editor keeps the UI responsive while a render runs or an agent edits over the network: the heavy or cross-thread work posts a notifier, and the loop redraws the affected regions on its own schedule. Input arrives as raw events; the event-value model turns it into intent before your widgets ever see it.