GPUI Box GitHub

Graph layout and temporal viewport contracts

canvas::layered_layout_sized is an opt-in caller-side layout. It consumes stable identities, positive finite measured sizes and edges, returning bounds sorted by identity. Lexical ordering is independent of node/edge input order. Weak components occupy separate bands; column widths and row heights use the largest/actual dimensions rather than fixed center-to-center spacing.

GraphCyclePolicy::Reject returns no partial result on a cycle. BreakIncoming chooses the lexically first remaining node when no source exists, ignoring its incoming dependencies. Backward edges are expected in that mode; it does not claim a DAG ordering, SCC decomposition, crossing minimization or a Sugiyama implementation. Duplicate edges do not alter layout; unknown endpoints, duplicate identities, malformed dimensions/gaps and unrepresentable accumulated geometry are errors. The legacy layered_layout helper is unchanged.

GraphCyclePolicy::Condense uses two iterative DFS passes to identify strongly connected components, stacks each component's members by identity using their actual sizes, and layers the condensation DAG. Thus every edge between SCCs points forward, while internal cyclic edges need not. Eight alternating barycenter sweeps reduce adjacent-layer crossings; the best strict inversion count is retained, never worse than the initial lexical ordering. Shared edge endpoints are not counted as crossings. This is a bounded heuristic, not an optimal crossing guarantee: long edges are not split into dummy vertices and the crossing score excludes them. Legacy cycle policies retain their ordering.

NodeGraph continues to consume caller positions and propose editor actions. Routing uses measured node heights and measured socket rows. Cache comparison includes socket anchors/directions/colors, routing mode, complete edge data, resolved named colors and route metrics; an empty routing result is cacheable. Endpoint lookup, route visibility and node-to-port lookup avoid repeated full node scans. Cache hits compare borrowed slices without allocating new graph keys and share retained route storage; only visible routes are cloned for rendering. Owned input copies are retained only on a cache miss. Orthogonal lanes validate against all node rectangles, including unrelated cards, before searching for a detour. A graph-space bounds index narrows obstacle checks; heading-aware search discovers obstacle coordinate planes lazily and stores only visited labels rather than a complete Cartesian grid. Exact cardinal port stubs remain fixed. Rounded turns reserve conservative clearance.

Failed routing retains the caller connection and its caller-owned EdgeState. A separately identified, localized status warns either that no obstacle-free route was found in this routing model (obstructed) or that the search budget was exhausted (search-limited). The former is not a geometric impossibility proof. Coordinate construction and visited labels share limits of 32,768 work units per route and 1,048,576 per batch; these are not elapsed-time bounds. Curved routing retains its existing behavior and does not promise all-obstacle avoidance. Corner radius and stroke width participate in cache invalidation. The node-graph-routing exhibit includes a global detour, a caller-moved buried port and an adversarial search-budget fixture.

Segment/viewport intersection retains routes that cross the viewport even when both endpoint cards are outside it. GPUI's same-frame container query bounds card mounting from the first frame and after resize, without waiting for a cached size. Whole-graph fit still measures offscreen content; input building, geometry and cache comparison still scan the dataset. The isolated CPU workloads below do not establish dense-graph or native FPS support, or viewport-bounded total work.

NodeGraph::source(GraphSource) is an exclusive reusable input path. Cloned handles share identity and revision; accepted mutation advances the revision, but the host must request redraw. Canonical GraphNode metadata requires explicit finite positive dimensions. Opaque one-use children are rejected; set_content and set_thumbnail register factories built only for visible cards, after releasing the metadata borrow. Legacy consumed nodes remain supported. A later legacy node/edge builder switches back to that input path. Upsert preserves insertion order and factories; failed validation changes nothing. Removal drops factories, preserves caller edges (including dangling ones), and reinsertion appends without resurrecting removed content. Declared heights avoid offscreen mounting even for Fit; offscreen ports have no mounted controls. Explicit-height cards do not allocate height-measurement registry entries. A source keeps immutable canonical geometry snapshots keyed by source identity, accepted revision, resolved theme identity and measured port rows. Changed node revisions and changed port rows recompute only their own geometry; factory-only publications reuse the snapshot. Animation copies the snapshot only when displayed bounds differ, preserving current interaction geometry without mutating retained caller snapshots.

Node visibility queries use the displayed-geometry bounds index; visible source identities resolve directly to insertion ordinals before lazy construction. Stable-cardinality node/route index updates refit changed leaves and ancestors; after a node-count's worth of changed leaves (at least 64), a repartition limits accumulated spatial degradation. Cardinality changes rebuild. Exact inclusive query semantics and source painter order survive updates/removal/reinsertion. Other metadata/port-measurement and routing comparisons still scan the model, and a route-cache miss still performs global routing. These changes do not claim viewport-bounded total work or dense 100k interactive performance. The node-graph-layout exhibit publishes source retarget/removal/reinsertion.

Explicitly sized cards animate caller-applied bounds using the Navigation policy; .animate_layout(false) disables this. Retargeting starts from the displayed box. First mount, reduced motion and direct editor proposals snap. Cards keep current caller content, and sockets, route construction, culling, marquee and context-menu tests use displayed geometry. Automatic-height legacy cards retain instant measured layout rather than animating an estimated size. Changing motion preference settles node-state paint and does not restart already settled edge entrances. Route topology still recalculates during movement. Compatible lane corridors (same segment count and axis sequence) interpolate from their displayed sample using Navigation timing. Retargeting preserves the current corridor; endpoints and their first/last segment axes are pinned to current displayed ports. Timing changes sample the old specification first, then restart from that position with the new full duration and delay; settled routes do not restart. Each sample passes the router's global segment and rounded-corner clearance checks before paint, label placement, culling and semantic targets consume it. They never use a target-only hit box. Changed endpoint identity, incompatible topology, blocked/search-limited routes, and invalid intermediate geometry snap to the solved route. In particular, a route switching obstacle sides cannot crossfade through that obstacle. Curves continue following sampled node geometry with their existing no-obstacle-routing contract. Initial mount, source replacement, reduced/disabled motion and direct manipulation do not delay routes. node-graph-motion exposes a lane-only change to inspect intermediate motion and interrupted retargeting with fixed nodes.

Newly published or reinserted visible identities fade in with the Entrance policy. Metadata/factory updates keep their current opacity without delaying caller content. Initial mounting, viewport reentry, direct node manipulation, reduced motion and disabled layout motion snap. Removing an entering card freezes its last displayed paint, including the partial opacity, before exit.

Removed visible cards replay their last supported paint with the Exit policy. The retained object is a framework paint recording, never an element or lazy factory: text/button/image pixels may remain while handlers, focus targets and semantics retire immediately. The picture follows the current graph viewport. Optical glass, native surfaces, hosted views, deferred frames and any other recording/replay refusal retire immediately, with no substitute shell. The empty-graph branch also supports retirement of the last card. Reinsertion discards the old picture; completion, reduced motion, disabled layout motion and non-ready graph states release recordings. Panning offscreen drops cached pictures rather than treating the node as removed or retaining visited content forever. Source identity/revision avoids rebuilding the current-id set on hits; this does not change the full-model geometry and routing work described above.

Removed visible connections also retain only their recorded stroke/marker paint, including the current entrance reveal, colour crossover and traffic phase. Their labels, warnings, disconnect target and captured input retire immediately. The picture follows the current viewport; it does not continue claiming live traffic. Membership follows routable connections, so removing an endpoint retires the picture even when the caller retains a dangling edge. Reinsertion, offscreen visits, capture/replay refusal, completed exit, disabled layout motion and reduced motion release old recordings. The empty-graph path supports the last connection. This visual retirement does not add route-path morphing or change the caller's edge model.

Raw time and normalized compatibility

TraceSpan::new retains normalized start/end coordinates and the default viewport is [0, 1]. Raw callers set .time(start_ms, end_ms) on each span and .time_viewport([start_ms, end_ms]) on TraceView or SpanTimeline. Both use display::chart::scale::NumericScale with ScaleKind::Time: UTC Unix milliseconds, f64 mapping, fixed-duration ticks, no calendar or timezone guessing. Reversed viewport domains retain their orientation. Nonfinite or backward span intervals retain their row/identity but have no bar. Bars are clipped to the viewport and instantaneous intervals use a one-pixel mark.

.format_time supplies host wording for automatically generated ticks. Density follows the measured track width; labels occupy bounded 64-pixel seats, thin when those seats overlap, and truncate rather than collide. Legacy .axis and .ticks remain available for normalized host labels. Durations and status never derive from the clock or tick formatter.

Ctrl-wheel proposes pointer-anchored zoom; shift-wheel proposes time pan. Unmodified wheel scrolls the row list. Without .on_viewport no time gesture handler is installed. Proposals never accumulate behind a refusing host.

Caller-applied time windows animate through shared Transition<f64> and the Navigation motion policy. .animate_viewport(false) disables this; reduced motion and orientation reversal settle directly. Both bar geometry and tick mapping use the displayed window, while input proposals use the latest caller window. Retargeting starts at the displayed extent, not the previous target. The sampled extent must retain the target orientation: collapsed, nonfinite or overshooting inverted endpoints snap both transitions to the caller target. Built-in Navigation uses a tween; tests additionally force a crossing spring. The row tooltip and accessibility description report caller-formatted times (exact UTC milliseconds by default, or legacy normalized coordinates), the caller duration, status and optional detail. .format_time owns human-facing range/span readouts as well as ticks, including timezone wording and precision. Range semantic endpoint values and events retain exact raw f64 values regardless of that wording. interval semantic children publish measured bar geometry for intermediate-frame assertions.

Opt-in headless playback verified both themes at exact simulated times: one fixture bar had width 128px initially, 130.5px at 80ms, stayed 130.5px when retargeted at that same time, then measured 130px at 160ms. Reduced motion returned it to 128px without advancing time. Rendered default, intermediate and collapsed states were inspected; this is renderer evidence, not native FPS. Raw pointer hover followed by 1000ms simulated time displayed the decode interval and caller duration 235ms in an unclipped floating tooltip in both themes. The temporal fixture now formats its times relative to its epoch.

.selected_time(Option<[f64; 2]>) retains an ascending raw-time range; .on_time_selection receives shared RangeEvent update/commit/cancel events with create, move or raw start/end resize intent. The shared f64 interaction::range state machine owns arithmetic, acceptance and refusal; Trace supplies measured controls and the existing NumericScale adapter. Drag empty track to create, drag inside the range to move, shift-drag to replace, or drag a handle to resize. Arrow keys step one percent; home/end act on the focused endpoint or whole range. Reversed time viewports retain raw start/end identity. Captured outside release clamps to the time window; cancellation never rolls back caller-accepted values. Disabled/read-only controls install no edit handlers. Off-domain selected values remain visible in the exact readout with disabled handles, not silently rewritten. Unaccepted edits are explicitly labelled as previews. All built-in labels use the strings catalog. A Chinese/custom-format input test checks selected, preview and empty labels, asymmetric fractional epoch endpoints, outside release and refusal without changing the raw semantic data.

The displayed viewport and measured track freeze during capture, so animation cannot move the time coordinates beneath a held pointer. A caller viewport replacement cancels; normal release resumes navigation. Actual component removal cancels through framework capture retirement, without adapter pre-cancel or a second lifetime registry. Remount does not resurrect a draft. Raw pointer playback exercised acceptance, refusal, outside release, cancellation, clear, creation and intermediate/reduced-motion zoom in both themes (22 captures). The linked read-only timeline kept caller values during a refused preview.

Hierarchy is the caller's depth-first preorder and depth, with collapsed identities supplied by .collapsed. Disclosures and left/right keys propose expanded state through .on_toggle; right on an expanded branch selects its first child, and left on a leaf or collapsed branch selects its parent (even when caller depths skip levels). Up/down/home/end propose selection and scroll the target row into view. Focus moves to the persistent container when navigation unmounts a formerly focused row. Hosts retain all data authority.

Surviving mounted rows animate accepted hierarchy changes through shared FLIP. animate_layout(false) disables this independently of time-viewport animation; layout_animation overrides the theme's Tracking timing. New rows wait until survivors settle, then mount in their current slots. They install no handlers or semantic targets while waiting, and removed rows disappear immediately without retained paint. Retargeting starts at displayed positions. Caller selection changes, scrolling and reduced motion settle immediately; initial mounting and ordinary scrolling never wait. The retained publication set covers mounted rows, not the full source. Both-theme timed renderer checks assert pairwise row bounds do not overlap through collapse, interrupted expansion and final publication.

The default height is 12 rows; .visible_rows changes it. GPUI's uniform list mounts visible rows plus measurement work, rather than every span. .shared_spans(Rc<Vec<TraceSpan>>) avoids cloning input on redraw. Hierarchy subtree boundaries, parent links, identity lookup and duration-column discovery are built once per shared input identity. A retained range-cover tree supplies kth-visible rows and selection rank in O(log N), without a flattened copy. Each changed collapse identity updates its descendant interval in O(log N); nested collapse remains effective when an ancestor expands. Comparing the caller-owned collapse set is O(C), where C is its size. Replacing the Rc input rebuilds the O(N) index and releases the prior source; this is not incremental streaming ingestion. The index belongs to the existing window/owner registry, not a separate global lifetime.

Focused index tests compare all 256 collapse subsets of an asymmetric nested preorder in both transition directions against a direct filtering oracle. A 100,000-span branch collapse visits at most 70 segment-tree nodes; a retained same-source update visits none. A local debug workload measured index build / retained update at 1.24ms / 0.59µs (1k), 7.74ms / 1.38µs (10k), and 85.11ms / 1.52µs (100k). These measurements exclude mounting and painting. The integration harness separately verified accepted nested collapse/parent navigation and refused viewport/collapse proposals, with eight published rows and 123 paint calls at all three sizes. Those structural counts are not a frame-rate claim.

Focused workload evidence

Reproduce with an unoptimized all-feature build on the Linux test platform:

cargo test -p gpui-box-kit --all-features graph_workloads -- --ignored --nocapture
cargo test -p gpui-box-kit --all-features trace_workloads -- --ignored --nocapture
cargo test -p gpui-box-kit --all-features graph_mount_workload -- --ignored --nocapture
cargo test -p gpui-box-kit --all-features it::trace -- --nocapture
cargo run -p xtask -- performance check

One local run of a forest of 16-node chains (asymmetric dimensions) measured:

NodesEdgesLayoutRoutingBorrowed cache-hit comparison
1,0009377.17 ms15.27 ms0.13 ms
10,0009,37588.85 ms145.49 ms1.26 ms
100,00093,750947.97 ms1,581.13 ms13.57 ms

Those costs exclude mounting and painting. Separate explicit-size grid workloads close each window before the next case: Harness::frame refreshes all open windows, so dropping only the harness contaminated earlier reports. These isolated results supersede the earlier multi-window timings:

NodesEdgesMountStatic redrawPaint/prepaint calls
1,000049 ms18 ms272
10,0000153 ms69 ms272
100,00001,447 ms704 ms272
1,00095066 ms23 ms292
10,0009,500383 ms108 ms292
100,00095,0003,789 ms1,176 ms292

Connected cases are row chains, not dense graphs. These numbers remain evidence against claiming a responsive 100k-node editor.

The trace workload published eight rows and exactly 123 paint calls at each of 1k, 10k and 100k spans. An isolated-window run measured mount at 25.4, 19.7 and 94.7 ms, and static redraw at 5.8, 5.8 and 6.5 ms, respectively. CPU input construction, hierarchy processing, mount and redraw are reported separately by the tests. No GPU submission, frame-rate guarantee, dense graph benchmark or native macOS/Windows timing is claimed.

The repository performance ratchet also passes: node-graph-material uses 11,384 heap allocations against its unchanged 12,105 limit, with 1,005 paint/prepaint calls. This measures the existing 64-node material fixture, not large-graph latency.

node-graph-layout reviews unequal dimensions and disconnected components. trace-time reviews raw time, clipped intervals, virtual rows and shared controlled windows. trace preserves normalized usage. New code is original; no third-party source or framework primitive is imported by this stage.