9.0 KiB
Nebula GPUI Foundation
nebula_gpui is an isolated GPUI component gallery. It remains a validation
surface, but it no longer owns the product terminal or workspace runtime.
Those implementations moved into nebula_app/src/gpui_shell/ in P3 and run as
the product main window through nebula.exe --gpui.
It is a laboratory, not a home: the single maintained product is
nebula_app (nebula.exe), and modules proven here are integrated into
nebula_app rather than accumulating product code in this crate. The
staged plan lives in the repo-root ROADMAP.md.
Adoption & Rollback Discipline
GPUI adoption is direct: once a surface is judged genuinely better served by GPUI components than by the hand-drawn UI, the GPUI implementation becomes that surface's code path — no runtime switches, no long-lived feature flags keeping two UI paths alive. Parallel paths rot; the component library's value is precisely that we stop maintaining hand-drawn widgets.
Rollback is provided by code retention and git history instead:
- The winit shell and the existing hand-drawn UI modules stay in the tree and stay buildable until the GPUI shell has passed the three gates (IME, performance baseline, CJK clarity) plus a stabilization period. Deleting them is the final step of the migration, not a side effect of it.
- Shared logic (terminal engine, render contract, config, sessions) lives in shared crates, so moving between shells never loses behavior.
- Each migration step is an isolated commit; if GPUI develops serious bugs, rollback = reverting commits. The old code is still there and still compiles.
The gpui-shell cargo feature currently gates the product GPUI path as well as
the older dual-runtime spike. It is still scaffolding: after the product-form
gates pass and GPUI becomes the default entry point, the feature gate and
NEBULA_GPUI_SHELL spike are removed rather than becoming rollback mechanisms.
Direct Component Use
Product views should import approved upstream components from
gpui_shell::prelude:
use crate::gpui_shell::prelude::*;
let action = Button::new("save").primary().label("保存");
let input = Input::new(&input_state);
prelude deliberately re-exports upstream types without changing their
behavior. Do not create one-to-one wrappers such as NebulaButton or
NebulaInput. A local component is justified only when it owns a Nebula
business contract, such as a terminal pane, an atomic-save editor workflow, or
a session-aware dock panel.
Ownership Boundaries
gpui_shell::run_shellowns the product GPUI/component-library initialization.gpui_shell::themeowns Nebula visual tokens, fonts, density, and global defaults.gpui_shell::preludeis the approved direct-component import surface for product views.- View code may use GPUI entities and window APIs.
- Terminal, SSH, SFTP, configuration, history, and other core crates must not
expose
gpui::Entity,gpui::Window, or GPUI actions in their public APIs.
This preserves direct UI composition while keeping the product core portable to another UI library. UI adapters translate core state and commands at the application boundary.
Dependency Policy
The v1.16.1 migration baseline was pinned by both package version and immutable
Git revision. The revisions below record that historical migration, not the
active dependency pins. Current revisions and patch rationale are maintained
in the root Cargo.toml, with resolution in
Cargo.lock:
gpui = "=0.2.2"
gpui_platform = "=0.1.0"
gpui-component = "=0.5.2"
gpui-component-assets = "=0.5.1"
gpuiandgpui_platformcome fromKuddev/zedcommiteb8e1c8b5502b7007465fbbc465f4a736fa39210. The commit is byte-identical to the officialzed-industries/zedv1.16.1tag.gpui-componentand its assets come fromKuddev/gpui-componentcommitc20dbef8ed0d4c0fbebbf2e8f02d39e4fa0ac0a7onnebula-v1.16.1-math. It extends the owned baseline with the app-owned math bridge, GFM reader semantics, and the compact popover-shadow helper. Math fitting, caching, rasterization, and product rendering remain in Nebula.- The
nebula-v1.16.1-owned-basebranch and tag in both Kuddev forks are audit and recovery anchors. Cargo dependencies always use the full commit SHA, never those movable names. Earliernebula-v1.16.1-baserefs remain intact as provenance records and are not active dependency targets.
Cargo source identity includes the repository URL. All Zed crates therefore use the Kuddev URL, including transitive dependencies from gpui-component; mixing the official and Kuddev URLs would create two incompatible copies of GPUI even when both URLs point to the same commit.
Do not use wildcard versions, Git main, or branch-only pins. Verify every
dependency revision change with:
cargo check -p nebula --features gpui-shell --locked --offline
cargo tree -p nebula --features gpui-shell -i gpui --locked --offline
GPUI upgrades happen in a dedicated branch with component acceptance testing; they are not bundled with feature work. Each required Nebula component patch must be restored in its own commit, advance the exact component revision, name the blocked behavior, and define its upstream or removal condition. The fixed baseline branch and tag must never move.
Workspace Boundary
nebula_gpui is a member of the root Cargo workspace (merged 2026-08-12).
The former blocker — two packages holding links = "fontconfig" — was
resolved by aligning both sides on yeslogic-fontconfig-sys ^6.0:
gpui 0.2.2→zed-font-kit 0.14.1-zed→yeslogic-fontconfig-sys ^6.0nebula→crossfont 0.9.0(upstream bumped to^6.0in 0.9.0)- The winit Wayland decoration feature switched from the crossfont flavor to
wayland-csd-adwaita(ab_glyph), sosctk-adwaita 0.10no longer dragscrossfont ^0.8(and with ityeslogic ^5) back into the graph. This is Linux-only decoration text; Windows/macOS builds are unaffected.
One lockfile now holds gpui 0.2.2, crossfont 0.9.0, and a single
yeslogic-fontconfig-sys 6.0.1, and both binaries compile from the merged
workspace. crossfont still leaves the graph entirely when the legacy
OpenGL renderer is retired; the decoration switch was the first consumer
removed on that path.
Terminal Vertical Slice
nebula_app/src/gpui_shell/terminal/ hosts the product terminal integration and proves the hardest
migration claim: nebula_terminal (PTY, VT parser, grid, selection) renders
inside GPUI as a custom Element without forking either side.
session.rs— owns the ConPTY +EventLoopwiring. Terminal events cross into GPUI through one futures channel; the PTY I/O thread never touches GPUI types.element.rs— the paint hot path. OneFairMutexlock per frame builds a plain-data snapshot (background runs, styled text segments, cursor, selection), then paints withpaint_quad+shape_line(force_width)so CJK wide cells stay grid-aligned.view.rs— focus, keyboard, mouse selection, wheel scrolling, clipboard, and IME viaEntityInputHandler(marked text drawn at the cursor cell, candidate window anchored throughbounds_for_range).keymap.rs— encodes only control/modified keys; printable text (including IME commits) flows through the text-input path so nothing is sent twice.colors.rs— resolves vte colors againstTerm::colors()overrides, then a default palette.
The boundary rule stays symmetric: no nebula_terminal type escapes
src/terminal/, and no GPUI type enters nebula_terminal.
Multi-Tab Terminal Workspace
nebula_app/src/gpui_shell/workspace.rs is the product shell: a custom
TitleBar, sidebar and center content surface owning terminal and settings
tabs. WorkspaceTab::Terminal is the authoritative split owner; it holds the
pane entities, nebula_split::SplitTree, focused pane, zoom state, and
broadcast state. Terminal behavior stays in each TerminalView, while
nebula_split remains a pure-data layout and navigation crate.
Lifecycle rules proven by manual acceptance:
- New tab: title-bar button or
ctrl-shift-t; the new tab activates and focuses its terminal. - Close: sidebar close action,
ctrl-shift-w, or shellexitconverge on the workspace tab-removal path. - Session teardown: explicit shutdown remains the primary path and
TerminalView::dropis the backstop. - Pane close first removes the leaf from the split tree.
WasRootcloses the whole tab;Collapsedremoves and shuts down only that pane, then transfers focus to the surviving leaf. - Detaching a pane into a new tab moves the existing pane entity and PTY. It must not call the close path or recreate the terminal, so scrollback and process lifetime remain intact.
- Divider movement updates only
preview_ratio; mouse release commits the final ratio. A drag-close target is valid only when the compressed child is a single leaf, never an entire nested subtree.
App-level hotkeys must be let through by the terminal key handler
(view.rs passes ctrl-shift-t/w up instead of encoding them as C0 bytes).