Skip to content

toolbar: Add Toolbar and ToolbarGroup components - #3128

Merged
huacnlee merged 15 commits into
longbridge:mainfrom
bipinnatus2k:feat/toolbar
Sep 22, 2026
Merged

huacnlee merged 15 commits into
longbridge:mainfrom
bipinnatus2k:feat/toolbar

Conversation

@bipinnatus2k

@bipinnatus2k bipinnatus2k commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds horizontal Toolbar and ToolbarGroup components for application command rows, with behavior in gpui-base and density/theme presentation in gpui-component.

  • gpui-base owns behavior. Toolbar exposes the horizontal toolbar accessibility role and roving Left/Right focus across focusable descendants, including wrapping and disabled navigation. ToolbarGroup adds labeled semantic groups without imposing presentation.
  • gpui-component owns presentation. The styled Toolbar propagates one density to hosted controls, aligns mixed controls on one center line, and automatically presents hosted Buttons as ghost + compact commands.
  • Source-order composition. Controls and content render in the order they are appended. Flexible spacers such as content(div().flex_1()) handle trailing alignment; there are no left or right regions.
  • Application-oriented density. Toolbar defaults to Small, supports XSmall, Small, and Medium, uses equal padding on every edge, and treats Large as Medium.
  • Examples and documentation. The component Story covers document/history/formatting commands, Input, Select, Combobox, size options, and a Disabled toggle. The gpui-base showcase demonstrates groups, separators, a trailing Search input, keyboard behavior, and visible command feedback. English and Chinese documentation are included.

Public API

gpui-base

  • Toolbar::new(id) creates a horizontal toolbar.
  • Toolbar::disabled(value) disables roving keyboard navigation. Hosted controls remain owned by the caller.
  • ToolbarGroup::new(id) creates a semantic subgroup.
  • ToolbarGroup::label(label) supplies its accessible name.
  • Toolbar and ToolbarGroup implement the standard GPUI styling, parent, interaction, and rendering traits.

gpui-component

  • Toolbar::new(id) creates a transparent, borderless Small toolbar.
  • child / children append Sizable controls and propagate the final toolbar size.
  • content / contents append separators, labels, spacers, and custom content without resizing them.
  • disabled(value) disables Toolbar navigation; callers also disable hosted controls.
  • with_size(size) supports XSmall, Small, and Medium; Large falls back to Medium.
  • ToolbarGroup propagates its inherited size to grouped controls.
  • Hosted Button values automatically receive the compact ghost toolbar presentation.

Breaking Changes

None. These components are new in this PR.

Test Plan

  • cargo fmt --check
  • git diff --check
  • cargo test -p gpui-base toolbar --lib
  • cargo test -p gpui-component toolbar::tests --lib
  • cargo check -p gpui-component-story -p gpui-component-shell
  • cargo check --manifest-path crates/base/examples/native/Cargo.toml --bin components
  • Open the Toolbar Story and verify XSmall/Small/Medium, Disabled, mixed-control alignment, popup behavior, and keyboard focus.
  • Open the gpui-base Toolbar showcase and verify command feedback, groups, separators, trailing Search input, and Left/Right focus movement.

AI Assistance

The implementation and review were completed with agent assistance; commits include the corresponding Co-authored-by trailers.

bipinnatus2k and others added 15 commits September 18, 2026 22:12
An unstyled container that groups controls and owns the roving-focus
contract of an ARIA toolbar: `Role::Toolbar` with orientation, and
arrow keys that move focus among focusable descendants, wrapping at
the ends. Traversal walks the rendered tab stops constrained to the
container subtree, the same approach `Root` uses for focus traps, so
it works with any focusable children without their cooperation.

The container itself is not a tab stop. A non-wrapping mode would
need to tell a wrapped step from an ordinary one, which GPUI's public
tab-stop API cannot do, so wrapping is always on.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Follows the status_bar precedent: `toolbar.background` falls back to
the title bar background and `toolbar.border` to the title bar border,
so existing themes pick up sane values without per-theme edits.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
A themed command bar with left/right pinned regions and a middle
region whose alignment follows the pinned ends, matching StatusBar's
region contract. Sizes via `Sizable` scale bar height, spacing, and
text together (28/32/40/48 px).

Keyboard roving and toolbar semantics come from `gpui_base::Toolbar`;
the styled layer only adds tokens, regions, and sizing. Each instance
takes an explicit id, which keeps its focus state stable across
frames.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Documents three sections: a document command bar layout, the four
sizes (with hosted buttons matching each bar size), and the region
alignment cases for the dynamic middle.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Bilingual component page covering regions, sizes, keyboard roving
focus, theming, and the API reference, plus entries in both component
indexes.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
An unstyled container that groups a run of related toolbar items and
carries an accessible name, so assistive technology reads them as one
unit. The surrounding toolbar's roving arrow-key focus traverses
group items like direct children, since containment follows the
element tree.

Unlike Base UI's Toolbar.Group, the group cannot disable its
children: that API propagates through React context into Base UI's
own button primitives, which has no equivalent for arbitrary GPUI
children, and the a11y layer exposes no disabled state for a
container node.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Surface the base group through the styled toolbar module so
applications reach it next to Toolbar.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Group the history buttons under an accessible "History" label in the
document toolbar example.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Covers grouping with an accessible name, the spacing note, and the
boundary that groups cannot disable their children in GPUI.

Co-authored-by: GLM (ZCode) <noreply@z.ai>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
@huacnlee
huacnlee merged commit 486687b into longbridge:main Sep 22, 2026
12 checks passed
madcodelife pushed a commit that referenced this pull request Sep 25, 2026
#3235)

## Summary

Since #3128 every "Rust & WASM" example under `/base/` (motion, button,
dialog, …) came up blank on the website, with the console full of
`RefCell already borrowed`. The first error is a panic in
`TextSystem::resolve_font`:

failed to resolve font '.SystemUIFont' or any of the fallbacks:
.ZedMono, .ZedSans, Helvetica, ...

#3128 switched the examples' root font from `Inter Variable` to
`.SystemUIFont` so the native binaries pick up the OS font.
`gpui-pre-web` resolves that alias to "IBM Plex Sans" and starts with an
empty font database, so the first text layout panicked and the canvas
never painted. The bundled Inter no longer matched anything. This is the
same failure #2933 fixed for the gallery; that PR left the base examples
alone because they still rendered at the time.

- `crates/base/examples/showcase/mod.rs`,
`crates/base/examples/motion/mod.rs`: bundle the existing IBM Plex Sans
subset from `crates/story-web/fonts` instead of Inter for the web build,
so the alias resolves. Inter was not referenced by either example
anymore; the wasm download shrinks by about 40 KB.
- `crates/story-web/scripts/subset-fonts.py`: also scan
`crates/base/examples/motion` when subsetting, so text added to that
example stays covered.

## How to Test

1. `cd crates/base/examples/wasm && make build`, then `bun run dev` in
`website/` and open `/base/motion` and `/base/primitives/button`.
2. Before: the example frame stays blank, the console shows the panic
above followed by repeated `RefCell already borrowed`. Reproduced on
https://gpui-kit.com/base/motion and `/examples/base?component=button`.
3. After: the motion tabs and the button example render, with zero
panics. Verified with headless Chromium (WebGPU) against the local dev
server.
4. `rustfmt --check`, `typos` and `cargo clippy -p gpui-base-examples
--all-targets -- --deny warnings` are clean.

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.

Written with AI assistance (Claude Code): investigation, fix and
verification.

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: 李波 <bo.li@longbridge-inc.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants