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:

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.