migrate input

This commit is contained in:
2026-09-17 16:12:31 +07:00
parent 7118565b4d
commit 75a25a9a9d
27 changed files with 219 additions and 7016 deletions
+84 -28
View File
@@ -18,7 +18,10 @@ repository at `main`, and this workspace's `Cargo.lock` (zed at `4b47ceb`,
of dead weight are gone. `history.rs` moved to phase 2 once it turned out its
only consumer is `input/state.rs`. No dependency became unused, so the pruning
step is a no-op (four dependencies were already unused before this work).
- **Phases 2-5: not started.**
- **Phase 2: landed.** `input/` runs on base's editing engine. 6,715 lines of
engine and history are gone and 306 are written, taking `crates/ui/src/input`
from 6,573 lines to 321. Six call sites changed, all named in phase 2 below.
- **Phases 3-5: not started.**
- One pre-existing, unrelated breakage was found; see
[A pre-existing wasm blocker](#a-pre-existing-wasm-blocker).
@@ -46,7 +49,7 @@ deeper than `ui::<module>::<Item>`:
| Module | Items | Consumer files |
| --- | --- | --- |
| crate root (`Icon`, `IconName`, `h_flex`, `v_flex`, `divider`, `Root`, `TitleBar`, `Sizable`, `Selectable`, `Disableable`, `StyledExt`, `WindowExtension`, `InteractiveElementExt`) | 13 | 17 |
| `input` (`InputState`, `Input`, `InputEvent`) | 3 | 10 |
| `input` (`InputState`, `Input`, `InputEvent`; plus `TextareaState` and `Textarea` after phase 2) | 3, then 5 | 10 |
| `button` (`Button`, `ButtonVariants`) | 2 | 14 |
| `dock` (`Panel`, `PanelView`, `DockArea`, `DockItem`, `DockPlacement`, `PanelEvent`, `ClosePanel`) | 7 | 10 |
| `notification`, `avatar`, `menu`, `scroll`, `group_box`, `indicator`, `switch`, `modal`, `tooltip` | 12 | 16 |
@@ -108,7 +111,7 @@ dragging, both of which are projected.
| `ui` module | LOC | Plan | `gpui-base` counterpart |
| --- | --- | --- | --- |
| `input/` (state, element, display_map, rope_ext, mask_pattern, movement, selection, indent, mode, change, cursor, blink_cursor, clear_button) | 6,929 | Replace; keep `ui::input::{Input, InputEvent, InputState}` as the import path | `Input`/`InputState`, `Textarea`/`TextareaState`, `Editor` |
| `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` |
@@ -120,13 +123,13 @@ dragging, both of which are projected.
| `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` |
| `history.rs` | 184 | Defer to phase 2 | `UndoHistory`, not `History`: base's `History` is navigation (back/forward), while `UndoHistory` is the grouped undo/redo with `max_undos`, `group_interval`, `start_grouping`/`end_grouping`, and `set_ignoring` in place of the fork's `pub(crate) ignore` field. Its only consumer is `input/state.rs`, which phase 2 replaces |
| `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` |
| `icon.rs`, `kbd.rs`, `divider.rs`, `skeleton.rs`, `group_box.rs`, `indicator.rs` | 1,023 | Keep; no base equivalent, these are the design system | — |
| `menu/` | 2,208 | Keep; base has no menu. Optional later: re-base anchoring and dismissal on `Popup`/`Positioner` | `Popup` (optional) |
| `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 and `focused_input` | — |
| `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.
@@ -230,30 +233,82 @@ touched are the two manifests, `ui/src/lib.rs`, `ui/src/styled.rs`, and
acceptance — launching the app and walking the settings dialog and chat panel — has to
be done by hand and has not been run.
### Phase 2 — `input/` (the largest single win, ~6.9k lines)
### Phase 2 — `input/` (the largest single win) — landed
The mapping is close to 1:1 with what the app actually uses:
`crates/ui/src/input` is three files and 321 lines: a rewritten 299-line `input.rs`, a
7-line `mod.rs` that re-exports base, and the untouched 15-line `clear_button.rs`. Deleted:
`state.rs`, `element.rs`, `display_map/`, `rope_ext.rs`, `mask_pattern.rs`, `movement.rs`,
`selection.rs`, `indent.rs`, `mode.rs`, `change.rs`, `cursor.rs`, `blink_cursor.rs`, and
`history.rs` — 6,715 lines.
| Coop today | `gpui-base` |
The names the application imports are unchanged, but two of them are base's now:
| Coop before | `ui::input` now |
| --- | --- |
| `InputState::new(window, cx).placeholder(..)` | same |
| `.auto_grow(1, 20)` (chat composer) | `TextareaState::auto_grow(2, 8)` with `Textarea` |
| `.masked(true)` (nsec, password, key) | `InputState::masked(true)`, `unmask_value()` |
| `.set_value(value, window, cx)` | `set_value(value, window, cx)` |
| `InputEvent::{Change, PressEnter, Focus, Blur}` | identical variants |
| `Input::new(&state).appearance(false)` | coop's `Input` keeps these chrome options |
| `InputState`, one struct that became multi-line through `auto_grow`/`multi_line` | `InputState` = `InputBaseState<InputMode>` and `TextareaState` = `InputBaseState<TextareaMode>`; multi-line is a property of the state's kind |
| `Input`, one element that rendered whatever kind of state it was given | `Input` for `InputState` and `Textarea` for `TextareaState` — one generic element, two names |
| `InputEvent::{Change, PressEnter, Focus, Blur}` | identical |
| `history::History` and `HistoryItem` | gone; `Change` keeps no trait impl |
Known gaps to reconcile here, verified against the 0.6.1 source before starting:
`clean_on_escape()`, `set_loading()` (called from `crates/workspace/src/sidebar/mod.rs`),
and the `InputEditorStyle` hook that has to be filled from coop tokens. Everything else
in `input/``display_map`, `rope_ext`, `mask_pattern`, `movement`, `selection`,
`indent`, `mode`, `element` — is deleted. Afterwards, `ropey`, `sum_tree`,
`lsp-types`, `tree-sitter`, `regex`, `unicode-segmentation`, and `uuid` can probably
leave `crates/ui`'s manifest.
The styled element is a frame around base's engine rather than the engine itself. Base's
`InputBaseState::render` registers the key context, the focus handle, every editing
action, the text element and the editor scrollbar, so the coop element no longer carries
any of it. What is left is chrome — background, radius, font size, prefix and suffix
slots, clear button, mask toggle, loading indicator — plus three projections onto the
state:
Surfaces to re-verify: the chat composer (auto-grow, Enter to send, IME), the subject
line, the settings dialog, profile, relay and messaging lists, the import/restore/backup
dialogs, and sidebar search.
- `set_editor_style(InputEditorStyle)`, filling `foreground`, `muted_foreground`,
`selection` and `caret` from `text`, `text_muted`, `selection` and `cursor`. Base
resolves any color left transparent from its own palette, and that palette is only a
projection of coop's, so every color coop paints with is named rather than left to
resolve. The remaining fields stay at base's defaults: coop configures no highlighter,
no diagnostics and no gutter.
- `set_editor_paddings(Edges<Pixels>)`, for multi-line only, resolved from the same `Size`
table the single-line frame applies, through the window's rem size. Base puts that
padding on the text element itself so the text, the gutter and the scrollbar share one
inset; putting it on the frame *and* passing it here would double it. Passing the
frame's own value is also what keeps the scrollbar where the fork drew it.
- `set_disabled` and `set_text_align`, replacing the fork's direct writes to `state.size`,
`state.disabled` and `state.text_align`.
`history.rs` folded in as predicted, with one correction: it did not need re-basing at
all. Base's engine owns an `UndoManager`, and `gpui_base::UndoHistory` — the grouped
undo/redo, not the back/forward `History` — is a separate utility the engine never
reaches for. Removing `pub mod history` is safe because nothing outside `crates/ui`
referenced it.
The gaps named before the phase started all resolved in base's favor: `clean_on_escape()`
and `set_loading()` both exist in 0.6.1, and `InputEditorStyle` is the third piece.
**Six call sites changed, and none of them is churn:**
| Call site | Change | Why |
| --- | --- | --- |
| `chat_ui` composer | `InputState``TextareaState`, `Input::new``Textarea::new` | multi-line is the state's kind, not a layout flag |
| `workspace`, profile bio | the same, and `.multi_line(true)` is dropped | the same; `auto_grow(3, 8)` is unchanged |
| `workspace`, sidebar | `set_loading(status, cx)``set_loading(status, window, cx)` | base's signature takes the window |
| `workspace`, sidebar | the `.loading` field read → `.presentation().is_loading()` | `loading` is private; `InputPresentation` is the facade for reading it |
| `ui::window_ext` | `focused_input` and `has_focused_input` are removed | their only implementation was the deleted paint hook, and no crate consumed them |
| `ui::init` | `input::init(cx)` is removed | `gpui_base::init` binds the same keys, and its set is a strict superset |
Three visible differences survive, all of them base's, none of them a color, radius or
spacing value:
- **The mask character is `•`, not `*`.** Base's `MASK_CHAR` is a private constant, so the
fork's `*` cannot be restored. It shows only in masked inputs.
- **The caret is `0.85 × line_height` at every size.** The fork scaled it by `Size` (0.75
at small, 1.0 at large) from a `size` field base does not have.
- **Inputs are tab stops.** Base builds the state's focus handle with `tab_stop(true)`;
the fork's frame was not a tab stop, so Tab skipped text fields and now lands on them.
Two things came along with `InputBase` that the fork's plain `div` did not do: the frame
carries the `TextInput` accessibility role, and a left click anywhere in the frame —
including the padding outside the text element — focuses the input. The second has to be
restated on the frame because base handles its own mouse events on the inner element only.
Surfaces to re-verify by hand: the chat composer (auto-grow, Enter to send, IME), the
profile bio, the subject line, the settings dialog, the relay and messaging lists, the
import/restore/backup dialogs, and the sidebar search field.
### Phase 3 — overlays and feedback
@@ -298,9 +353,10 @@ There is no UI test suite to lean on, so each phase gets the same treatment:
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.
- Keep the call-site diff at zero for phases 1, 3, and 4; if a call site has to change
because base has no equivalent (`set_loading` is the known candidate), list it in the
pull request.
- Keep the call-site diff at zero where the phase claims it — phases 1 and 34 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"
turned out to mean in practice.
### A pre-existing wasm blocker
@@ -345,7 +401,7 @@ compile for `wasm32-unknown-unknown`".
| --- | --- | --- | --- |
| 1 | Phase 0: `gpui` moves to the `gpui-pre` package, `gpui_tokio` vendored | root `Cargo.toml`, `Cargo.lock`, `web/Cargo.toml`, new `crates/gpui_tokio`; `crates/state` needed no edit | landed |
| 2 | Phase 1: base wiring, `sync_base`, deletions | `crates/theme` | landed |
| 3 | Phase 2: input, plus `history.rs``UndoHistory` and the `ropey`/`sum_tree`/… pruning | none, or the named gaps | not started |
| 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 | not started |
| 510 | Phase 4: one leaf module each | none | not started |
| later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` | not started |