GPUI Box GitHub

Migration guide

This guide moves an existing GPUI application onto the gpui-box framework and gpui-box-kit component library without a big-bang shell rewrite.

[dependencies]
gpui = { package = "gpui-box", version = "0.1" }
gpui_kit = { package = "gpui-box-kit", version = "0.1" }

Native windows and platform views

GPUI has two different window-handle contracts. The inherent gpui::Window::window_handle() method returns GPUI's AnyWindowHandle, which updates a window through the application context. The raw_window_handle::HasWindowHandle implementation returns the operating system handle. Rust resolves an inherent method before a trait method, so importing HasWindowHandle is not enough when obtaining a Win32 or AppKit handle. Call the trait explicitly:

let raw = raw_window_handle::HasWindowHandle::window_handle(window)?;

For a native child such as WKWebView or WebView2, keep construction, navigation, focus, and destruction in the application, convert the native view once, and let GPUI own its placement:

// macOS, after constructing a retained NSView/WKWebView:
let handle = unsafe { gpui::PlatformViewHandle::from_ns_view(native_view) };

// Windows, after constructing a child HWND:
let handle = unsafe { gpui::PlatformViewHandle::from_hwnd(child_hwnd) };

// Both platforms:
gpui::platform_view(handle).size_full()

Remove direct addSubview/SetParent placement code and per-frame native geometry synchronization. The application remains the native view's owner; GPUI owns layout, clipping, stacking, visibility, and detach timing. See the complete macOS example in crates/gpui/examples/native_webview.rs.

A partially visible view retains the full frame GPUI laid out. GPUI applies the current content mask through a clipped AppKit container or the Windows host region, so scrolling crops native content rather than changing its layout. A view painted more than once uses its last paint and native views are restacked in paint order every time that order changes.

When a controller or service destroys the native view on drop, attach that owner to the handle so the detach frame cannot race destruction:

let handle = unsafe { gpui::PlatformViewHandle::from_hwnd(child_hwnd) }
    .keep_alive(native_controller.clone());

keep_alive does not transfer native ownership to GPUI. It retains the caller-owned value until the platform host drops its final handle clone, on the window's platform thread.

1. Inventory

Classify current code:

  • raw palette and repeated metrics;
  • product-neutral primitives;
  • reusable interaction patterns;
  • product view models and actions;
  • host, persistence, process, and credential authority;
  • automation and screenshot infrastructure.

Only the first three categories migrate to the kit.

2. Establish visual baselines

Capture fixed viewports for important states before changing dependencies. Record hover, selected, disabled, focus, loading, empty, error, stale, popover, and dialog states.

3. Map tokens

Add a theme document or map the existing design to studio-dark. Do not keep an old theme global active beside gpui_kit_theme::Theme.

Replace:

const PANEL: u32 = 0x0d0d0d;

with:

let theme = Theme::get(cx);
theme.colors.panel

Theme is a cheaply cloned handle to immutable ThemeData. Existing reads stay direct, but code that derived an adjusted theme by writing a cloned field must use the copy-on-write boundary:

let branded = Theme::get(cx)
    .clone()
    .modify(|theme| theme.colors.accent = brand);

This is also the adjustment callback expected by ThemeOverlay. Do not mutate the raw palette outside Theme::modify; that method rebuilds the pre-resolved palette and variant-ramp lookups when the palette changes.

4. Install assets and theme

let app = gpui_platform::application()
    .with_assets(gpui_kit::assets::Assets);

app.run(|cx| {
    gpui_kit::install(cx);
    // Open application windows.
});

Applications with their own assets can compose or delegate an AssetSource.

5. Migrate low-coupling primitives

Move in this order:

  1. badges and status;
  2. buttons;
  3. cards and settings scaffolding;
  4. loaders and motion;
  5. popovers and dialogs;
  6. frost and edge fade.

After each move, delete the application copy. Do not retain two primitive sets.

6. Add semantic IDs

Install Kit once. Its SemanticCoordinator creates and removes one frame context per GPUI window, but records nodes only while a diagnostic consumer is armed. A harness, inspector, or automation host retains SemanticCoordinator::global(cx).arm() for its lifetime; ordinary installation deliberately leaves diagnostic recording dormant while native accessibility remains active. At the top of each root render call SemanticCoordinator::global(cx).begin_frame(window), then attach NodeSpec to every action and assertion target with semantic_in. Keep application IDs in the application; the library does not define product vocabulary.

7. Preserve the host boundary

Convert product models into view models before rendering. Components must not gain direct host references merely to simplify migration.

8. Replace automation

Use semantic generation to wait for frames, target controls by stable id, and capture the owned window. Keep any RPC server and input injection debug-only.

9. Remove duplicates

Search for:

  • second Theme globals;
  • copied RGB or alpha values;
  • duplicate Loadable enums;
  • duplicate popover geometry;
  • multiple semantic registries;
  • full-desktop screenshot code.

The migration is complete only after the old implementation is gone.

Forge-specific note

Forge integration is intentionally not part of this repository's initial implementation. A future Forge migration should happen through a reviewed dependency update while preserving Forge's product and host authority.