diff --git a/docs/gpui-base-migration.md b/docs/gpui-base-migration.md index 5ef4c8e7..e4b6d0a3 100644 --- a/docs/gpui-base-migration.md +++ b/docs/gpui-base-migration.md @@ -26,6 +26,12 @@ repository at `main`, and this workspace's `Cargo.lock` (zed at `4b47ceb`, were 1,592, and nothing outside `crates/ui` changed. The behavioural differences are named in phase 3 below; the largest is that base's toast stack replaces the fork's notification list. +- **Phase 4: landed.** `switch`, `button`, `scroll/`, and `resizable/` run on base's + controls; `avatar` was checked and deliberately left alone. `crates/ui/src` went + from 12,922 lines to 10,830, and again nothing outside `crates/ui` changed. The + only files inside `crates/ui` outside the migrated modules are the two dock files + that consume `resizable`. The differences are named in phase 4 below; the one that + wants an eye on it is a button's inherited line height. - One pre-existing, unrelated breakage was found; see [A pre-existing wasm blocker](#a-pre-existing-wasm-blocker). @@ -88,7 +94,7 @@ which is the case when `ui::init` runs ahead of `theme::init`; `Theme::change` i hook that actually keeps the projection current. It projects the color roles base can act on — the focus ring, the wash under selected -text, scrollbars, and overlay backdrops — and nothing else: +text, and overlay backdrops — and nothing else: | `gpui_base::ColorTokens` | coop `ThemeColors` | | --- | --- | @@ -105,11 +111,14 @@ text, scrollbars, and overlay backdrops — and nothing else: It also sets `ThemeAppearance` from coop's mode, `ScrollbarTheme`'s mode from coop's `scrollbar_mode`, and `TypographyTokens::sans` from coop's `font_family`. Radius, -spacing, typography sizes, shadows, and scrollbar geometry keep their base defaults: -coop has a single `radius`/`radius_lg`/`font_size` where base has six-point scales, so -any mapping would be invented rather than derived. `ResizableTheme` needs nothing — -base's documented `None` fallback already resolves to `border` at rest and `ring` while -dragging, both of which are projected. +spacing, typography sizes, shadows, and scrollbar geometry keep their base defaults +here: coop has a single `radius`/`radius_lg`/`font_size` where base has six-point +scales, so any mapping would be invented rather than derived. Two things base would +otherwise paint from its own defaults are instead supplied by `crates/ui` at the call +site, because they are per-element rather than global: `scroll/` passes coop's rail and +thumb geometry through `ScrollbarStyles`, and `resizable/` paints the divider with a +`ResizeHandleRenderer`. `ResizableTheme` and `ScrollbarStyles` are therefore never +projected. ## What each module becomes @@ -121,15 +130,15 @@ LOC is the count before the work; a module whose phase has landed reads | `input/` (input, clear_button) | 6,573 | Replace; keep `ui::input::{Input, InputEvent, InputState}` as the import path. 321 lines remain, and the engine paints itself through `InputEditorStyle` | `InputState`/`TextareaState` (`InputBaseState` in two modes) plus the `InputBase` frame | | `list/` | 1,477 | Delete | GPUI's own `list` (already in use) | | `checkbox.rs` | 312 | Delete | `Checkbox` | -| `scroll/` (scrollbar, scrollable, scrollable_mask) | 1,332 | Replace; keep the `ScrollableElement` and `Scrollbar` names | `Scrollbar`, `ScrollableMask` | -| `resizable/` | 927 | Replace; base exports the same names (`h_resizable`, `v_resizable`, `resizable_panel`, `PANEL_MIN_SIZE`, `resize_handle`) | `Resizable` + `ResizeHandleRenderer` for the coop hairline | +| `scroll/` (scrollbar, scrollable, scrollable_mask) | 1,332 → 232 | Replace; keep the `ScrollableElement` and `Scrollbar` names. The two scrollbar files are gone and `scrollable.rs` is a thin layer over base | `Scrollbar` (`ScrollableMask` had no callers and is dropped) | +| `resizable/` | 927 → 37 | Replace with a re-export of base's identically named API plus a `ResizeHandleRenderer` for the coop hairline | `Resizable*`, `resize_handle`, `PANEL_MIN_SIZE` | | `modal.rs` | 540 → 500 | Port onto base parts; `Modal`, `ModalButtonProps`, and `window.open_modal` unchanged. `Root` still owns the stack | `Dialog` — focus trap, Escape/Enter/backdrop dispatch, layer priority, deferred host | | `notification.rs` | 584 → 663 | Port; `Notification`, `NotificationKind`, and `window.push_notification` unchanged | `ToastManager` (storage, ids, timers, exit), `ToastStack` (geometry, motion), `Toast` (`Role::Alert`) | | `popover.rs` | 432 → 234 | Coop's builder over base's element; `PopoverState` is base's, re-exported | `Popover`, `Popup`, `Positioner` | | `tooltip.rs` | 36 → 37 | Coop's view rooted at base's element | `Tooltip` (`Role::Tooltip`) | -| `button.rs` | 626 | Skin: base behavior plus coop's existing variant tables | `Button`, `StateStyle` | -| `switch.rs` | 287 | Skin | `Switch`, `SwitchTrack`, `SwitchThumb` | -| `avatar.rs` | 141 | Skin | `Avatar`, `AvatarImage`, `AvatarFallback` | +| `button.rs` | 626 → 610 | Skin: base behavior plus coop's existing variant tables | `Button`, `StateStyle` | +| `switch.rs` | 287 → 188 | Skin: base owns the toggle and its semantics; coop keeps the geometry, the label and the description | `Switch`, `SwitchTrack`, `SwitchThumb`, `spring` | +| `avatar.rs` | 141 | Keep. Base's `Avatar` is an unstyled `Div` holding an image slot *or* a fallback slot, and its `AvatarImage` exposes neither `grayscale` nor `Img::with_fallback`; the fork needs a runtime load failure to swap in `brand/avatar.png`, which is a different thing from having no image. Nothing base offers is reachable from this module | `Avatar` (unusable here) | | `history.rs` | 184 | Delete. Base's input keeps its own `UndoManager`, and `UndoHistory` is a separate public utility the input never touches, so nothing has to be re-based. Its only consumer was `input/state.rs` | — | | `index_path.rs`, `element_ext.rs`, `event.rs`, `focusable.rs` | 156 | Delete | `IndexPath`, `ElementExt`, `InteractiveElementExt`. `FocusableCycle` has no counterpart — base's `FocusableExt` is a different concept (whether a component draws a focus ring) — so it is dropped rather than re-based | | `styled.rs`, `actions.rs`, `animation.rs` | 305 | Keep `ui::StyledExt`, `Size`, and `Sizable` as the app's import. `Selectable`, `Disableable`, and `Collapsible` now come from `gpui_base::component_traits`; the local three-line `h_flex`/`v_flex` wrappers stay rather than delegating to base's identical ones | `styled`, `StateStyle` | @@ -138,7 +147,9 @@ LOC is the count before the work; a module whose phase has landed reads | `dock/` + `tab/` | 3,356 | Keep for now; see phase 5 | base dock (different contract) | | `root.rs`, `window_ext.rs`, `title_bar.rs` | 965 | Keep; app shell. `Root` continues to host the dialog and toast layers. Its `focused_input` field and the two `WindowExtension` methods that read it are gone — the only thing that ever set them was the deleted input paint hook, and no crate consumed them | — | -Roughly 10k lines are removed, 3k are re-expressed as thin skins, and 8k are kept. +Once phases 1–4 have landed, `crates/ui/src` is 39 files and 10,830 lines where it +began at 71 and 21,876: roughly 11k lines removed, 2k re-expressed as thin skins, +and the rest kept as the design system. ## Dependency change @@ -394,7 +405,7 @@ import, restore and screening modals (a modal with a textarea, and one with `push_notification` site — sending an empty message, a failed upload with its retry action, and the device-approval notification that never auto-hides. -### Phase 4 — leaf controls, scroll, and resizable (one module per pull request) +### Phase 4 — leaf controls, scroll, and resizable (one module per pull request) — landed Order: `avatar`, `switch`, `button`, `scroll/`, `resizable/`. `button` is the largest skin: the `ButtonVariants` and `ButtonCustomVariant` tables, the `compact`, @@ -407,6 +418,52 @@ for the coop hairline. Each of these is independently shippable. Acceptance for each: no change outside `crates/ui`, and the surfaces that use the module are pixel-identical before and after. +What each module turned into: + +| Module | LOC | Base now owns | Stayed in `ui` | +| --- | --- | --- | --- | +| `switch.rs` | 287 → 188 | `Role::Switch`, `aria_toggled`, focus tracking, Enter/Space and pointer activation, disabled inertness, the thumb's travel | both slots of the control, the label and description, the sizes and radii | +| `button.rs` | 626 → 610 | `Role::Button`, focus tracking, Enter/Space and pointer activation, the disabled and selected precedence, the disabled `mouse_down` veto | every variant colour, the size and padding table, icon-only mode, `loading`, `caret`, `indicator`, `compact`, `rounded`, the tooltip | +| `scroll/` | 1,332 → 232 | the whole scrollbar: geometry, fade, drag, hover and active states, and the handle traits for `ScrollHandle`, `UniformListScrollHandle` and `ListState` | `ScrollableElement`/`Scrollable` (base has no trait for attaching a scrollbar to an arbitrary element) and the projection of coop's rails and colours | +| `resizable/` | 927 → 37 | the group and panel elements, the state, the drag arithmetic, the minimum-size clamp, the handle's hit area and cursor | the hover-only 1px hairline and its two colours | + +`crates/ui` changed in three places outside those modules: `dock/dock.rs` and +`dock/stack_panel.rs`, the two consumers of `resizable` (`resizable` is consumed +nowhere else — it is `dock`'s machinery), and nowhere else. No call site outside +`crates/ui` changed. + +Behavioural differences, all of them base's: + +- **`switch`**: activation moves from the fork's mouse-down on the whole row to + base's click, Enter and Space on the switch. The label is inside the switch, so + clicking it still toggles, and the control is now a tab stop that announces + itself and its toggled state. It also no longer stops the press from reaching a + parent; base stops it only while disabled. The thumb's 150 ms slide becomes base's + critically damped 0.15 s spring, so the easing differs slightly, and reduced-motion + is now honoured. +- **`button`**: Enter and Space now activate the button and `Role::Button` is set + with the label as its accessible name. Nothing draws a focus ring, which is + unchanged. Base's root carries `line_height(relative(1.))` where the fork inherited + GPUI's default `phi()`: the label already pins `relative(1.)` so it does not move, + but text handed to `.child(..)` now sits tighter. This is the one difference a + screenshot review should look for. +- **`scroll`**: coop's rail and thumb are projected explicitly — a 10px rail, a 6px + thumb inset by 1px with a 3px radius (8/1/4 whenever the mode is not `Scrolling`), + a 48px minimum length, and the thumb's two palette colours — so the resting + appearance does not move. Base's own fade timings replace the fork's + `FADE_OUT_DURATION`/`FADE_OUT_DELAY`. `ui::scroll::ScrollbarState`, `PrepaintState` + and `AxisPrepaintState` are gone; nothing outside `crates/ui/src/scroll` ever named + them. The `is_inspector_picking` guard went with them, as coop never enables the + inspector. +- **`resizable`**: base keeps `sync_panels_count`, `update_panel_size` and + `replace_panel` private, so the dock uses `reset_panel` for the one it needed. Base's + built-in divider is always painted and takes its colours from `ResizableTheme`, + which `sync_base` leaves at the `border`/`ring` fallback, so the divider is drawn by + a `ResizeHandleRenderer` instead: the fork paints nothing at rest and `border` on + hover, `border_selected` while dragging. `ResizeHandle::placement` takes + `gpui_base::Side` rather than `DockPlacement`; the dock maps it, and base only + distinguishes `Left`. + ### Phase 5 — dock, tab, and menu (deliberately later) Base has a full dock, but its contract is "layout is data, and the application @@ -432,7 +489,12 @@ There is no UI test suite to lean on, so each phase gets the same treatment: - Launch the app and walk the surfaces the phase touched. The settings dialog is the densest single smoke surface (Button, GroupBox, Switch, Input, DropdownMenu, PopupMenuItem), followed by the chat panel and the sidebar. -- For phase 4, record before/after screenshots per module. +- For phase 4, record before/after screenshots per module. These are still owed: the + module work is compiled and linted but has not been walked by eye. The settings + dialog covers `switch` and `button`, the chat list and the sidebar cover `scroll`, + and any docked panel divider covers `resizable`. For `button`, watch text passed + through `.child(..)` rather than `.label(..)`, which is the one place the inherited + line height changes. - Keep the call-site diff at zero where the phase claims it — phases 1 and 3–4 do; if a call site has to change because base has no equivalent, list it in the pull request. Phase 2 needed six, tabulated above, and the list is the record of what "no equivalent" @@ -483,7 +545,11 @@ compile for `wasm32-unknown-unknown`". | 2 | Phase 1: base wiring, `sync_base`, deletions | `crates/theme` | landed | | 3 | Phase 2: input, plus `history.rs` and the `ropey`/`sum_tree`/`lsp-types`/`regex`/`unicode-segmentation`/`tree-sitter` pruning | `crates/workspace`, `crates/chat_ui` (six call sites); no manifest outside `crates/ui` | landed | | 4 | Phase 3: popover, modal, notification, tooltip | none | landed | -| 5–10 | Phase 4: one leaf module each | none | not started | +| 5 | Phase 4: `switch` | none | landed | +| 6 | Phase 4: `button` | none | landed | +| 7 | Phase 4: `scroll/` | none | landed | +| 8 | Phase 4: `resizable/`, plus its two `dock` consumers | none; `dock/dock.rs` and `dock/stack_panel.rs` are inside `crates/ui` | landed | +| — | Phase 4: `avatar` | none | no change; see phase 4 | | later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` | not started | The end state: the application keeps its design system and its call sites, `crates/ui`