Skip to content

input_group: Add InputGroup - #3042

Merged
huacnlee merged 12 commits into
longbridge:mainfrom
suxiaoshao:codex/2863-input-group
Sep 17, 2026
Merged

huacnlee merged 12 commits into
longbridge:mainfrom
suxiaoshao:codex/2863-input-group

Conversation

@suxiaoshao

@suxiaoshao suxiaoshao commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2863

Description

Add composable input groups for multiple inline addons, block toolbars, and textareas that share one frame. Existing Input::prefix and Input::suffix remain available for simple standalone adornments.

Composition and reuse boundary

The caller keeps the retained InputState / TextareaState; the group owns no editing state. The control placed in the frame is the ordinary styled Input (or Textarea, which renders one), drawn without its own border, background and focus ring — the group draws those around the whole frame. So InputGroupInput and InputGroupTextarea are Input and Textarea under the group's names, and every builder those controls have (aria_label, content_type, on_paste, cleanable, mask_toggle, Styled) works inside a group without being mirrored. There are no gpui-base changes, and Input itself is untouched beyond two crate-private accessors.

The public parts follow shadcn/ui and live in gpui_component::input next to the controls they frame: InputGroup, InputGroupInput, InputGroupTextarea, InputGroupAddon, InputGroupButton, and InputGroupText. Rust uses a typed .input(...) slot for one control and ordered .addon(...) / .child(...) composition:

InputGroup::new("message")
    .input(InputGroupTextarea::new(&self.message).aria_label("Message"))
    .addon(InputGroupAddon::new("message-header")
        .align(InputGroupAddonAlignment::BlockStart)
        .child(InputGroupText::new().child("New message")))
    .addon(InputGroupAddon::new("message-actions")
        .align(InputGroupAddonAlignment::BlockEnd)
        .child(InputGroupButton::new("send").label("Send")
            .on_click(cx.listener(|this, _, window, cx| this.send(window, cx)))))

InputGroupButton wraps a Button with compact presentation and sizes through Sizable: xsmall (default) and small are the compact sizes, and a button with only an icon is square at either. Focus and invalid appearance come from the theme — the focus ring keeps the theme's ring color and the invalid ring the danger color — and the border and background colors transition with motion_tokens(). Block addons share the control's horizontal inset so a leading icon or a trailing button lines up with the text; an inline addon keeps a compact button the same clearance from the frame that it has from the top and bottom. The group coordinates disabled/read-only behavior and addon click-to-focus while preserving native button actions.

The same six parts are registered in the JavaScript shell with retained state bindings. Native and JavaScript Stories cover the compositions and working actions; English and Chinese documentation explain ownership, usage, and styling.

Breaking Changes

None; existing input APIs are unchanged. Textarea gains accessibility_id. The following is an optional change when a simple prefix grows into a shared-frame composition:

- Input::new(&query).prefix(Icon::new(IconName::Search))
+ InputGroup::new("search")
+     .input(InputGroupInput::new(&query))
+     .addon(InputGroupAddon::new("search-icon")
+         .child(Icon::new(IconName::Search)))

How to Test

Validated on macOS on top of the latest main:

cargo fmt --check
cargo clippy --workspace --all-targets -- --deny warnings
cargo test -p gpui-component --lib --features test-support
cargo test -p gpui-component-shell
npm --prefix crates/component-shell/tests/types test
cargo run -- "Input Group"

The input_group tests cover the builder, appearance precedence, focus routing, embedded button actions, disabled/read-only behavior, textarea growth and scrolling between block addons, popup composition, the paste hook, and the long-press edit menu — the last two exercising capabilities the group inherits from Input rather than implements. The shell host tests cover state callbacks, addon actions and the rejected TypeScript contracts.

In the native Story, check the phone and search-scope triggers (+1 ⌄, Documentation ⌄) sit clear of the frame, the script.js and comment toolbars line up with the editor text, and a focused group shows the theme's soft ring.

The original implementation was developed with Codex assistance; the rework onto Input and Sizable was written with Claude Code and reviewed by hand.

@huacnlee

Copy link
Copy Markdown
Member

Code review

Found 1 issue:

  1. The InputControl extraction has no place for two features main added to input.rs after this branch forked, and input.rs is the file that conflicts: Input::on_paste / paste_handler / the Paste capture_action from input: Add on_paste to handle images from clipboard instead of silently dropping them #3087 (3f4fc75), and render_touch_selection() appended to overlays.floating from mobile: Add touch selection handles and an edit menu to Input and TextView #3073 (9031c5e). On main, Textarea::render and Editor::render both delegate to Input::from_state(...).on_paste(...), so resolving the conflict by keeping this PR's structure either fails to compile textarea.rs / editor.rs or, if those calls are dropped, silently loses image paste and touch selection for all three controls. Both need a home in the new render path — prepare ends at render_overlays with nothing appended, and Input has no paste_handler.

menu.show(position, window, cx);
}),
cx,
);
state.render_overlays(window, cx)
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

input.rs keeps the InputControl extraction and takes on main's paste
handler filter and touch-selection overlays after prepare(), which now
owns the native menu and registry wiring those lines used to sit next to.
@huacnlee huacnlee changed the title input_group: Add composable input groups input_group: Add composable InputGroup Sep 17, 2026
@huacnlee huacnlee changed the title input_group: Add composable InputGroup input_group: Add InputGroup Sep 17, 2026
Adapt the features added on main after the InputGroup branch diverged. Share paste interception and touch overlays through InputControl, and expose on_paste on grouped inputs. Cover editability, clipboard payloads, and touch menus with focused interaction tests and update both documentation locales.
@suxiaoshao

Copy link
Copy Markdown
Contributor Author

@huacnlee Addressed in 3791c0d after merging main in ca1b3ce. Both features landed on main after this branch diverged, so this was an integration change needed when resolving the merge conflict.

Paste handling and touch-selection overlays now use the shared InputControl, and both InputGroup input parts expose on_paste. Existing Input, Textarea, and Editor behavior is preserved. All 51 focused tests, Clippy, formatting, and spelling checks passed locally.

huacnlee and others added 3 commits September 17, 2026 14:08
The group used to place a private `InputControl` in its frame and mirror a
subset of the input's builders on `InputGroupInput` / `InputGroupTextarea`,
and a subset of the button's on `InputGroupButton` behind `with_button`.
Anything the real controls gained had to be mirrored by hand — the paste
hook and the touch edit menu already were not.

`InputGroupInput` and `InputGroupTextarea` are now `Input` and `Textarea`
under the group's names; the group renders the control without its frame
and draws the border, background and ring itself, so every capability of
the input reaches a group as it lands. `InputControl` and the style hooks
on Icon, Spinner and Button go away with it, as does the shell's Style
argument mechanism they needed.

`InputGroupButton` sizes through `Sizable` like every other control:
`xsmall` (default) and `small` are the compact sizes, and a button with
only an icon is square at either. `InputGroupButtonSize`, `with_button`,
`label_style` and `icon_style` are gone, as are the group's `focused_style`,
`invalid_style` and `disabled_style`: state appearance comes from the theme,
and the focus ring keeps the theme's ring color.

Insets follow the input: block addons share `input_px(size)` so a leading
icon or a trailing button lines up with the text, and inline addons keep a
compact button the same clearance from the frame that it has from the top
and bottom. The color transitions read `motion_tokens()`. The paste and
long-press tests from the branch stay and pass unchanged in intent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Superseded: the group renders Input itself now, so both come with it. The
tests from that commit are kept in the previous commit.
`InputGroup` frames an `Input`, so it is reached the same way:
`gpui_component::input::{InputGroup, InputGroupAddon, …}`. The source
moves to `input/group.rs`, which also lets its tests reach the private
`state` module without widening it. The shell inventory lists the group
and its parts under `input`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@huacnlee
huacnlee enabled auto-merge (squash) September 17, 2026 06:29
@huacnlee

Copy link
Copy Markdown
Member

Thank you.

@huacnlee
huacnlee merged commit 142e401 into longbridge:main Sep 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add an InputGroup component

2 participants