Skip to the content.

GPUI / Rust Migration — Feasibility Study

A feasibility analysis of rebuilding the suite’s UI stack (Mural, and the Plexus workbench that hosts it) natively in Rust on GPUI — Zed’s GPU-accelerated UI framework — in place of the current TypeScript stack that renders to SVG/DOM.

This document records what was proven with working code, what remains open, and the strategy and cost of executing the migration. It is written for engineers evaluating or planning that migration; every performance number here came from a throwaway probe crate run on Windows 11, not from estimation.

Verdict in one paragraph

The native GPUI foundation is real and was validated end-to-end on Windows: the framework builds and runs, paints Mural’s own retained visual tree through a single low-level canvas (bypassing GPUI’s own element model), sustains real-time frame rates, renders text and complex paths with holes, and lets us own z-ordered, shape-accurate hit-testing, dragging, clipping, scrolling, images, SVG, and hi-DPI. A minimal but real code editor — the one component with no GPUI-native equivalent — was also built and works. The remaining risk is not capability; it is scope: porting ~130–145k lines of Rust and standing up the service layers that a browser or native shell cannot host in-process. Planning figure for an agent-assisted port is on the order of several hundred million tokens (midpoint ≈ 0.5B), best replaced with a measured rate from a calibration port of Fresco before committing.

Background and the decision

The current stack is layered: todl-runtime (a zero-dependency TypeScript core that owns the reactive Observable base) sits beneath Mural (the UI framework — property system, .mu markup, controls, theming, diagrams), which sits beneath TODL (the language and compiler) and the Plexus workbench. Fresco is a self-contained hierarchical layout engine feeding Mural’s diagrams.

Four motivations drove considering Rust: render/layout performance, a cross-platform core, decoupling Mural from TODL’s runtime, and memory-safety in the mutation-heavy core. The literal reading — “rewrite everything in Rust” — was rejected: most of Mural is exactly where TypeScript is the right tool. The idea sharpened into two questions:

  1. Can GPUI act as a dumb GPU canvas hosting Mural’s own retained tree (Option A), or does adopting GPUI force Mural onto GPUI’s element and flexbox model (Option B)? Option A preserves Mural’s identity; Option B is a rewrite of its semantics.
  2. What is the true cost of the code editor that must replace Monaco, which cannot cross to a native target?

The target chosen was native desktop (macOS/Linux/Windows); web was explicitly deprioritized (see Web deployment for why that matters). Everything below answers question 1 as Option A, and question 2 as bounded, not a wall.

What GPUI is, and how it was consumed

GPUI is a hybrid immediate/retained, GPU-accelerated Rust UI framework developed in the Zed monorepo and published to crates.io as gpui (the study used version 0.2.2). It exposes three registers: a declarative element/view model (its Tailwind-like div builder over a flexbox layout), a state/model layer, and a low-level element register that gives “total control over how elements are rendered.” That third register is the one Option A depends on.

The key seam is the canvas element: canvas(prepaint, paint) hands you a raw paint closure over a pixel-space bounds, from which you can issue primitives directly — window.paint_quad, window.paint_path, window.paint_svg, shaped text via window.text_system().shape_line(...).paint(...) — at absolute coordinates, with no div/flexbox involved. A single such element becomes the “Mural surface,” painting the entire foreign visual tree.

The spike: what was proven, with numbers

All results are from a throwaway crate (mural-gpui-probe, ~10 small binaries) on Windows 11 with the MSVC toolchain. GPUI’s first build pulled its Windows platform backend, the Blade graphics layer, and the lyon path tessellator, and produced a ~26 MB executable — GPUI’s supposed macOS-first weakness did not materialize.

Rendering and performance

Text

A deliberately pathological test shaped and painted 29,040 glyphs per frame, re-shaped from scratch every frame (the frame counter was baked into each line so no shaping cache could ever hit), across 120 dense lines. It held ~50 FPS with zero paint failures, resolving a Windows system font and Unicode glyphs. Real text never re-shapes wholesale each frame — Mural, like Zed, retains shaped lines — so ~50 FPS is a floor, not a ceiling.

Interaction — owned by our model

GPUI supplies only the raw event; hit-testing and state live in our tree.

Clipping, scroll, DPI, media

Custom window chrome

Probed and confirmed on Windows 11 (research/rust_migration/window-chrome-probe), and a strong yes: Zed’s entire window chrome is custom-drawn on GPUI — custom title bars, min/max/close, rounded corners, transparency — so this is a first-class capability, not a stretch. The current Plexus approach (a frameless Electron window with a mural-painted title bar and a draggable region) maps over directly: GPUI client-side decorations turn off the OS titlebar; the title bar becomes an ordinary element in the GPUI view tree (the existing HeaderContent/PragmaticWindowChrome concept, painted by GPUI instead of DOM); window.start_window_move() replaces the -webkit-app-region drag region; and window-control buttons call GPUI’s minimize/zoom/close. Client-decorated windows also support rounded corners, custom shadow, and transparent/blurred backgrounds.

Two platform nuances are the actual work: on macOS, keep and position the native traffic lights rather than reinventing them; on Windows 11, the maximize-button Snap Layouts hover requires reporting custom caption buttons to the OS via window hit-testing (WM_NCHITTEST).

The probe settled the Windows side. A frameless window (TitlebarOptions { appears_transparent: true } plus client decorations) carries a Plexus-style title bar — app icon, in-bar buttons, a drag region, and minimize/maximize/close — implemented twice and switchable at runtime:

GPUI answers WM_NCHITTEST from those hitboxes with HTCAPTION / HTMINBUTTON / HTMAXBUTTON / HTCLOSE, so the OS owns the drag, Aero Snap, double-click-maximize, the Snap Layouts flyout, and the min/max/close actions themselves (GPUI performs them on the non-client button-up). Resize edges come from the default frame hit-test. Our app still receives the non-client mouse-down as an ordinary MouseDownEvent, so it can observe chrome clicks — but it must not stop propagation there, or the native action is cancelled. Acrylic backdrop (WindowBackgroundAppearance::Blurred) and active/inactive title dimming (observe_window_activation) also work; maximize state comes from observe_window_bounds + is_maximized() to swap the restore glyph (Segoe Fluent Icons E921/E922/E923/E8BB).

Three gotchas found in GPUI 0.2.2’s Windows backend:

Cross-platform input

The input event model is cross-platform by design — GPUI’s per-OS backends (Cocoa, Win32, Wayland/X11) normalize native input into one set of event types (MouseDownEvent, KeyDownEvent/Keystroke, ScrollWheelEvent, Modifiers), so the interaction code above is write-once. Verified on Windows only this session; macOS is GPUI’s most mature backend. Three portability notes: shortcuts must key off the platform modifier (Cmd on macOS, Ctrl elsewhere), not control directly; scroll feel differs by hardware (trackpad pixel/inertial vs wheel line steps) even though pixel_delta normalizes the number; and correct text entry (dead keys, CJK/IME) uses GPUI’s input-handler path, not raw keystrokes. All three are application-level concerns, not GPUI limitations.

Multi-window and panel tear-off

Probed and confirmed on Windows 11 (research/rust_migration/multi-window-probe). One GPUI App hosts several OS windows, and panels move between them as the same live entity — a panel is an Entity<PanelView> owned by an app-level dock model, and a window is just a place that renders it. Moving a panel therefore preserves everything with no serialization: its state (a keyboard counter kept its value), its FocusHandle (re-focusable and typing in the new window), its animation (request_animation_frame keeps running per window, including while the OS is dragging that window), and its subscriptions (one shared model observed by every panel updated all windows at once). This maps directly onto Plexus’s existing model/view split and the planned tear-off backlog item.

The interaction is the Chrome/VS Code style:

The gap is small and platform-local: GPUI 0.2.2 exposes no window-move or set-position API on Windows (start_window_move is a no-op there), so the live-follow drag and the cross-window cursor test use ~60 lines of Win32 via the windows crate and the window’s raw-window-handle. A production implementation would wrap these behind a small per-OS trait (macOS: performWindowDragWithEvent; Linux: the compositor move request GPUI already implements). Tearing off without a drag (open the window at a position) is pure GPUI.

Still unproven at the substrate level

Custom window chrome and multi-window tear-off are proven on Windows but not yet on macOS (traffic-light positioning, window-drag hand-off) or Linux; and the 150%-scaling crispness was confirmed only numerically, not stress-tested at higher fractional scales. None is a blocker; all are “known GPUI features to wire up.”

The code editor — replacing Monaco

Monaco is not a Mural component; it is a separate web/DOM editor embedded in the Plexus shell for authoring .todl and .mu. A native target cannot carry it, so the migration has two independent workstreams: Mural → GPUI, and a native-code editor. There is no drop-in code editor for GPUI — Zed’s own editor crate is deeply coupled to its monorepo and is not a reusable library — so this is a build.

A minimal but real editor was built on GPUI in 430 lines: a multi-line text buffer, keyboard editing (typing, Enter, Backspace, Delete, Tab), caret movement (arrows, Home, End), selection (Shift+arrows and mouse drag), click-to-place caret, vertical scroll with clipping, a line-number gutter, and TODL syntax coloring. It compiled and ran first try.

What GPUI provided for free — the expensive parts: multi-run colored text shaping (shape_line + TextRun), exact caret and click mapping (LineLayout::x_for_index / closest_index_for_x, correct for any font), keyboard input with layout and modifiers resolved, focus, mouse, wheel, and content-mask clipping. What was written (~300 lines): the buffer, edit operations, cursor and selection model, click-to-position, scroll, and a tokenizer.

The remaining tail to reach Monaco parity, and why each is bounded:

Piece Effort Note
Undo / redo Low An engine already exists in the codebase; wire it
Clipboard Low GPUI has clipboard APIs
LSP: diagnostics, completion, hover Medium The TODL language server already exists and speaks stdio; connect it as a client, host the popups as GPUI elements
IME / dead keys / CJK Medium The spike used keystroke input (Latin only); the production path is GPUI’s input handler, which Zed uses for full IME
Soft-wrap + horizontal scroll Medium Supported by GPUI’s line wrapping
Large-file virtualization Medium Swap the line vector for a rope; offscreen lines are already culled
Multi-cursor, find/replace, bracket match Low–Med each Additive to the model

Two gotchas found by touch, both easy to miss: the spacebar arrives as a key named “space” with an empty character field and must be handled explicitly; and programming ligatures (turning ==>, ->, != into single glyphs) are off by default — they are enabled per-font via FontFeatures (calt/liga), exactly as Zed exposes them, and the operator characters must be grouped into a single shaped run, or per-character syntax coloring splits the ligature apart.

Estimate: a usable single-DSL editor is roughly 1–2 weeks (the spike is a day of it); Monaco parity for TODL/.mu is plausibly 4–8 weeks — sharply reduced because it targets one DSL rather than a general IDE, and because the existing LSP and undo engine transfer directly. Zed itself is proof the ceiling is very high.

Where syntax features come from

Symbol navigation, IntelliSense, hover, diagnostics, folding, and semantic highlighting are split: the language server supplies the AST-derived facts (definitions, references, completions, diagnostics, semantic tokens, folding ranges) as position-based data; the editor client hosts every piece of UI, manages buffer sync and cancellation, maps LSP’s UTF-16 positions to buffer offsets, and actually performs the fold/jump/insert. Some features (folding, basic highlighting, bracket matching) can also be done client-side without the server, typically via tree-sitter — an incremental, error-tolerant parser that keeps a live syntax tree per keystroke, used by Zed and the natural choice for local highlighting and structure on GPUI. The server half already exists for TODL; the migration builds the client half.

Rust modularity and composition

Rust composes at compile time (static types, monomorphization, no stable ABI), where TypeScript composes at runtime. This suits the codebase’s existing constructor-injection discipline almost exactly.

WASM: migration hybrid vs. web deployment

Two distinct WASM questions, with opposite answers.

Web deployment

Because the target chosen was native desktop, web became a secondary surface. The suite’s current web story is DOM-based (light, accessible). A GPUI web build is a heavy canvas app. If web is ever first-class, the realistic options are to keep a DOM front end for web while GPUI serves native, or to accept the canvas tradeoffs and host the agent/LSP/compiler as backend services. This should be decided explicitly rather than assumed to come free with the native migration.

Token budget for the migration

Grounded in a real line count of the workspace.

Codebase Source LOC Tests
Mural 134,900 30,700
TODL compiler 26,800 20,600
Plexus shell 13,100 —
Fresco 6,200 1,000
todl-runtime 1,600 1,200
Total ~182,500 ~53,500

Not all of this becomes Rust. Three adjustments dominate:

Net Rust to write: roughly 115–145k lines of code plus 40–60k of tests, i.e. ~160–200k delivered lines. At an all-in agentic rate of ~1.5k–4k tokens per delivered line (context reloading dominates; Rust’s borrow checker and GPUI’s pre-1.0 churn add fix cycles a straight TS→TS port would not), plus 20–40% for design, hard-debugging spikes, and integration:

Planning figure: on the order of 300M–1B tokens, midpoint ≈ 0.5B, with roughly 3× error bars. The single most effective way to tighten this is to calibrate: port Fresco (6.2k lines, self-contained, the natural first chunk) end-to-end with the exact agent workflow the migration will use, measure the tokens it actually consumes, and multiply out — converting a 3× guess into a ~1.3× estimate. Two caveats: tokens are rarely the binding constraint (human review bandwidth and wall-clock are — a half-billion-token migration is many months of reviewed work), and the ongoing cost of tracking GPUI’s pre-1.0 churn is separate from the one-time port.

Immediate next step

Run the Fresco calibration port: port Fresco to Rust end-to-end with the production agent workflow, and measure. It is the ideal first migration chunk (self-contained, pure compute, a simple serializable boundary), it replaces the token estimate above with a measured rate, and — validated as Rust→WASM loaded into the current app — it de-risks the port in place before any commitment to the larger rewrite.

Appendix: GPUI API notes discovered