Rust GUI components for building fantastic cross-platform desktop application by using GPUI.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
windows: Set the default stack size to 8M on Windows. (#228) | 1 年前 | |
chore: Add a /release-notes command (#3117) ## Summary Adds a project command, `/release-notes [tag]`, that writes a version's GitHub release notes in the style of the existing releases. It reads the previous release for structure and tone, generates the "What's Changed" list with the releases API, takes API names and `## Breaking Changes` diff blocks from the PR bodies rather than the titles, and writes the themed summary above the verbatim list. It saves the document to the scratchpad and does not create or edit a release on its own. v0.6.2's release notes were written with it. ## Test Plan - `/release-notes v0.6.2` produced the notes now published at https://github.com/longbridge/gpui-kit/releases/tag/v0.6.2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 3 天前 | |
website: Load shared WASM examples from root (#3165) ## Description - load Component and Base WASM examples from the single root deployment for every documentation version - keep the development WASM middleware mounted at the same root paths - add a non-root `/versions/test/` build to CI so the regression test reproduces versioned publishing ## How to Test ```bash cd website SHOWCASES_DIR=/path/to/gpui-kit-showcases bun run build bun test tests/*.test.ts SHOWCASES_DIR=/path/to/gpui-kit-showcases bun run test:versioned-examples bash -n ../script/build-website-versions ``` ## Checklist - [x] I have read the CONTRIBUTING document and followed the guidelines. - [x] Reviewed the AI-assisted changes and confirmed they are accurate. - [x] Website root and versioned builds pass with 27 tests and the focused versioned regression test. - [x] Native story and platform-specific testing are not applicable to this website routing change. | 4 小时前 | |
input: Scroll straight to a far-off caret after an edit (#3150) ## Description Editing at a caret that sits far outside the textarea viewport left it offscreen. Reproduction: paste a hundred-line text into an auto-grow `Textarea`, scroll back to the top to read it, then type at the end. Each keystroke moved the viewport by one line, so the caret and the typed text stayed out of view. `TextElement::layout_cursors` follows the active caret only on the frame the selection changes. It then adjusted `scroll_offset.y` by one `line_height`, which is enough when the caret moves one row (Enter at the bottom edge) but not for a far-off caret. Cursor movement is unaffected because it already reveals through `scroll_to`. Place the caret's line at the viewport edge instead of stepping. The edge uses the same `top_bottom_margin` as before, so the resting position for one-row moves is unchanged. Everything else in the cursor-follow (horizontal follow, selection-endpoint follow, the `auto_scrolling` suppression) is untouched. ## How to Test - `cargo test -p gpui-base --lib input::` — 283 passed. The new `test_edit_reveals_far_offscreen_caret` fails on `main` with `caret top 2549.95px outside viewport height 400px (scroll -26px)` and passes with this change. - `cargo test -p gpui-base --lib` — 993 passed; `cargo fmt --check` and `cargo clippy -p gpui-base` clean. - Manual: in the Textarea story (or any auto-grow `Textarea`), paste 100 lines, scroll to the top, type one character. Before: the viewport moves one line and the caret stays hidden. After: the first keystroke reveals the caret line. Scrolling without editing is unaffected. ## AI Disclosure The fix and the regression test were written with Claude Fable 5.1. I reviewed the diff, confirmed the root cause in `layout_cursors` and ran the tests above. ## 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. - [ ] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) Co-authored-by: Tryanks <tryanks473@gmail.com> | 5 小时前 | |
questionnaire: Add a Questionnaire component (#2878) ## Summary - Add a composable Questionnaire state model and styled parts for single and multiple choice, freeform answers, skip, validation, shortcuts, focus, progress, navigation, and submission. - Reuse Radio, Checkbox, Input, Button, Kbd, semantic theme tokens, and Size throughout the presentation layer. - Add localized strings, comprehensive Story coverage, and synchronized English and Chinese documentation. ## Validation - `cargo test -p gpui-component questionnaire --lib` (15 passed) - `cargo clippy -p gpui-component --lib --tests -- --deny warnings` - Native and nightly WASM Story checks - VitePress build and `git diff --check` Visual acceptance was intentionally not performed. This implementation was created with AI assistance and reviewed through targeted tests. Fixes #2860 --------- Co-authored-by: Floyd Wang <gassnake999@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 天前 | |
Version 0.6.5 | 6 小时前 | |
website: Publish versioned website builds (#3159) ## Summary - publish the latest release at `/` and keep `main` at `/versions/main` - publish release documentation under `/versions/<tag>` with version switching - keep App Stories only on the latest site and mark non-latest versions `noindex` - cache historical version builds by release tag and website build fingerprint Closes #3157 ## Test Plan - `bun run build` with latest-release environment - `bun run test:seo` - `bunx --bun astro build` with `/versions/main/` and `noindex` environment - `bun test website/tests/showcases.test.ts website/tests/showcase-browser.test.ts` - `bash -n script/build-website-versions` --------- Co-authored-by: Codex <codex@openai.com> | 5 小时前 | |
dialog: Merge `button_props` instead of replacing them (#3126) ## Problem Reported downstream (ai-desktop / pilot): `AlertDialog::confirm()` only sets `show_cancel` on the dialog's button props, and `Dialog::on_ok` / `on_cancel` / `on_close` write their callbacks into that same value — but `button_props` assigned the whole value over it. So this quietly lost the Cancel button: ```rust alert .confirm() .button_props(DialogButtonProps::default().ok_text("Delete").ok_variant(Danger)) ``` and this quietly lost the callback: ```rust alert.on_ok(|_, _, _| true).button_props(DialogButtonProps::default().ok_text("Delete")) ``` Downstream had to repeat `.show_cancel(true)` at every call site and leave a comment saying "button_props replaces the whole value". `Dialog::button_props` had the same behavior. ## Change - Every field of `DialogButtonProps` is unset until a builder sets it (`ok_variant`, `cancel_variant`, `show_cancel` and the three callbacks became `Option`), and `button_props` merges rather than assigns: the fields the value sets win, the rest of what the dialog already carries survives, in any call order. Unset fields fall back at render time to the defaults they had before — `Primary` for the OK variant, `ButtonVariant::default()` for Cancel, no Cancel button, callbacks that close the dialog. - `AlertDialog` gains `ok_text`, `ok_variant`, `cancel_text` and `cancel_variant`, so the common dangerous confirmation no longer has to build a props bundle: ```rust alert.title("Delete “Roadmap”?").confirm().ok_text("Delete").ok_variant(ButtonVariant::Danger) ``` They are on `AlertDialog` only. A plain `Dialog` renders its buttons through `footer`, so the same builders there would compile and do nothing — exactly the kind of API a downstream reader (or model) would mistake for a working one. - `AlertDialog` now keeps one copy of the button props, on the base dialog, instead of a second copy it had to reconcile while building the surface. - A cancelled `AlertDialog` now reports `on_close` as well as `on_cancel`, the pair a cancelled `Dialog` already reported. The second copy's replace had dropped the alert's `on_close` on the way to the surface; the shell host test (`window_effects_host.rs`) had encoded that with a close count of four and now expects five. The `DialogButtonProps` builder signatures are unchanged and `Default` still works, so this is not a breaking change — no `Breaking Changes` section needed. ## Tests `crates/component/src/dialog/alert_dialog.rs` and `dialog.rs`: - `.confirm()` followed by `.button_props(…)` keeps the Cancel button; - `.button_props(…)` followed by `.confirm()` does too; - `.on_ok(…)` followed by `.button_props(…)` still runs the callback once; - the direct builders resolve to the same props as a `button_props` value; - successive `Dialog::button_props` calls merge with what the dialog carries. The first, third and fifth fail against the old replace semantics (verified by temporarily restoring them); the other two only compile with the new builders. ``` cargo test -p gpui-component --lib dialog # 11 passed cargo test -p gpui-component --lib # 529 passed cargo clippy -p gpui-component -p gpui-component-story -p gpui-component-shell --all-targets -- --deny warnings # clean cargo fmt --check # clean typos crates/component/src/dialog website # clean ``` ## Docs `website/component/alert-dialog.md` and its `website/zh-CN/component/` counterpart: the dangerous-confirmation examples now use the direct builders, and the page states that `button_props` overrides only the fields the value sets. `website/component/dialog.md` (both locales) gets a one-paragraph "Action Buttons" note: a `Dialog` puts its own buttons in `footer` and has them dispatch `Confirm`/`Cancel`, `on_ok`/`on_cancel` decide Enter/Esc, and a confirmation with default buttons is `AlertDialog`. `skills/gpui-kit/references/usage.md` and the AlertDialog story follow the same style. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 2 天前 | |
chart: Animate the hover, cache heavy geometry and rework the gallery cards (#3112) ## Description Charts were static: a tooltip snapped from datum to datum and nothing else responded to the cursor. Every chart with an `id` now animates its hover with `gpui_base::motion`, following ECharts / G2 / d3 conventions: - **Line / Area / Radar** — the crosshair and one dot per series glide along the line to the hovered point on a fast spring; the dot grows a halo (`Dot::halo`). - **Bar / Candlestick** — the highlight band slides to the hovered bar; the other bars fade behind it (`ScaleBand::step` normalizes the hand-over as the band travels). - **Pie** — new hit-testing (`Arc::contains`) and tooltip (value and share); the hovered slice lifts out of the ring on the control spring while the others fade. - **Sankey** — new node hit-testing and tooltip; links not attached to the hovered node fade. - The whole overlay — crosshair, dots, box — fades in when the cursor lands on a datum and out after it leaves, and honors reduced motion. Timing comes from `MotionTokens`: pointers use a critically damped spring on `duration_fast` (matching ECharts' 200 ms exponential-out axis pointer), the slice lift uses `spring_control`, the fade `duration_fast` with the enter/exit easings. ### Plot API - `Plot::hover(&mut self, Option<&PlotHover>, window, cx)` — a new default method, run each frame before `tooltip` and `paint` with the datum in focus. `PlotHover` carries the `TooltipState` (unchanged, still a plain struct) and lingers after the cursor leaves while `focus()` eases back to zero; `is_hovered()` and `is_entering()` tell a plot when to snap a pointer instead of travelling. The `IntoPlot` derive tracks this (`plot::tooltip::track_hover`), and `Tooltip` reads the tracked fade from the plot's element state itself, so custom plots get the fade for free. - `Tooltip::focus(f32)` overrides that fade; `Dot::halo(size)` draws the ring. - `PieChart::id` / `name`, `CandlestickChart::id`, `SankeyChart::id` opt those charts into tooltips. - The derive now resolves the component crate path (`gpui_kit::component` / `gpui_component` / `crate`) next to the GPUI one. ### Caching GPUI's `cached` only applies to entity-backed views (busted by `notify`), so it cannot wrap a chart, which is a value rebuilt every render. Instead an identified chart keeps its heavy geometry in element state: `LineChart` / `AreaChart` adopt the existing `paint_cached`, pie slices get `Arc::paint_cached`, and `SankeyChart` moves its placement into `prepaint` behind a key over the data, settings, label lines and bounds size (which also lets `tooltip_state`, which has no window, hit-test the placed nodes). Charts without an `id` rebuild everything, since sibling charts would otherwise share one cache slot. ### Gallery - The sidebar collapses off-canvas from a toggle at the left of the status bar (animated by `Sidebar`'s own transition); the resizable wrapper is gone. - The chart story gets realistic datasets (a year of SaaS metrics, traffic sources, browser share, plan mix, regions, products, pages, product review scores, 40 trading sessions) with data-driven footers ("Trending up by 5.2% this month" is computed), legends, a donut with its headline in the ring, single-hue shade ramps for categorical charts, and width-wise bar shading instead of length-wise gradients. The gallery binary registers `AllAssets` for the trend icons. Docs: a new "Hover and Tooltips" section (motion, caching, custom plots) in `website/component/chart.md` and `website/zh-CN/component/chart.md`. ### Theme palette `default-theme.json` spelled the palette `chart_1`..`chart_5` while the schema read `chart.1`..`chart.5`, so the listed ramp was never applied and every theme fell back to lightened/darkened `blue`. The theme files now use the dotted keys the schema reads, which align with the `chart_1`..`chart_5` fields; the Rust API is unchanged. `chart_bullish` / `chart_bearish` are keyed `chart.bullish` / `chart.bearish` like the other dotted keys, and `CandlestickChart::bullish()` / `bearish()` override them for markets that read a rise as red. `.theme-schema.json`, the theme viewer story and the docs follow. ## Breaking Changes None in the Rust API. Theme files key the candle colors like the other dotted keys: ```diff - "chart_bullish": "green-600", - "chart_bearish": "red-600", + "chart.bullish": "green-600", + "chart.bearish": "red-600", ``` ## How to Test - `cargo test -p gpui-component --lib -- plot:: chart::` — `Arc::contains` hit-testing, `ScaleBand::step`, `TooltipState` hover readers, alongside the existing plot tests. - `cargo test -p gpui-component-story --lib` — the story's number formatting and trend maths. - `cargo run -- Chart`: hover every chart kind; the pointer glides between data, the pie slice lifts, sankey links fade, the overlay fades out after the cursor leaves. Toggle the sidebar from the status bar. - [x] Passed `cargo run` for story tests related to the changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 4 天前 | |
website: Load shared WASM examples from root (#3165) ## Description - load Component and Base WASM examples from the single root deployment for every documentation version - keep the development WASM middleware mounted at the same root paths - add a non-root `/versions/test/` build to CI so the regression test reproduces versioned publishing ## How to Test ```bash cd website SHOWCASES_DIR=/path/to/gpui-kit-showcases bun run build bun test tests/*.test.ts SHOWCASES_DIR=/path/to/gpui-kit-showcases bun run test:versioned-examples bash -n ../script/build-website-versions ``` ## Checklist - [x] I have read the CONTRIBUTING document and followed the guidelines. - [x] Reviewed the AI-assisted changes and confirmed they are accurate. - [x] Website root and versioned builds pass with 27 tests and the focused versioned regression test. - [x] Native story and platform-specific testing are not applicable to this website routing change. | 4 小时前 | |
chore: Move the tested consumer crate into the workspace (#3118) ## Summary `examples/ai_recipes` was its own workspace with its own `Cargo.lock`, so every version bump left that lockfile behind and `cargo check --locked` failed in CI — the 0.6.2 release broke the `Recipes` job ([run](https://github.com/longbridge/gpui-kit/actions/runs/35328358614/job/105546651071)). The isolation guarded against feature unification hiding a missing feature, but every recipe only uses what `gpui-kit`'s default features already expose. - `examples/ai_recipes` becomes an ordinary workspace member; its separate `Cargo.lock` is gone, so releases need no extra step. - The `test` job runs its interaction tests on all three platforms instead of excluding it with the other examples; the `lint` job type-checks it. - `script/check-ai-recipes` keeps only the documentation fragment check; `script/check-ai rust` keeps the `gpui-component --no-default-features` contract tests. - The former `Recipes` CI job is renamed `No default features`, which is what its remaining steps test. - README, skill references, and comments updated to match (the skill doc also pointed at `src/lib.rs`/`src/main.rs`; the sources are `settings.rs`/`bootstrap.rs`). ## Verification - `python3 script/check-ai docs` — 9 fragments in sync, 6 tests pass - `cargo test -p gpui-kit-recipes --locked` — 6 tests pass - `cargo clippy -p gpui-kit-recipes --all-targets --locked -- --deny warnings`, `cargo fmt --check`, `cargo machete`, `typos` — pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 3 天前 | |
input: Fix context menu crash when Input in a Popover or Select. (#2082) Closes #1963 To disable context menu in Popover or Select Dropdown Menu. | 6 个月前 | |
chart: Animate the hover, cache heavy geometry and rework the gallery cards (#3112) ## Description Charts were static: a tooltip snapped from datum to datum and nothing else responded to the cursor. Every chart with an `id` now animates its hover with `gpui_base::motion`, following ECharts / G2 / d3 conventions: - **Line / Area / Radar** — the crosshair and one dot per series glide along the line to the hovered point on a fast spring; the dot grows a halo (`Dot::halo`). - **Bar / Candlestick** — the highlight band slides to the hovered bar; the other bars fade behind it (`ScaleBand::step` normalizes the hand-over as the band travels). - **Pie** — new hit-testing (`Arc::contains`) and tooltip (value and share); the hovered slice lifts out of the ring on the control spring while the others fade. - **Sankey** — new node hit-testing and tooltip; links not attached to the hovered node fade. - The whole overlay — crosshair, dots, box — fades in when the cursor lands on a datum and out after it leaves, and honors reduced motion. Timing comes from `MotionTokens`: pointers use a critically damped spring on `duration_fast` (matching ECharts' 200 ms exponential-out axis pointer), the slice lift uses `spring_control`, the fade `duration_fast` with the enter/exit easings. ### Plot API - `Plot::hover(&mut self, Option<&PlotHover>, window, cx)` — a new default method, run each frame before `tooltip` and `paint` with the datum in focus. `PlotHover` carries the `TooltipState` (unchanged, still a plain struct) and lingers after the cursor leaves while `focus()` eases back to zero; `is_hovered()` and `is_entering()` tell a plot when to snap a pointer instead of travelling. The `IntoPlot` derive tracks this (`plot::tooltip::track_hover`), and `Tooltip` reads the tracked fade from the plot's element state itself, so custom plots get the fade for free. - `Tooltip::focus(f32)` overrides that fade; `Dot::halo(size)` draws the ring. - `PieChart::id` / `name`, `CandlestickChart::id`, `SankeyChart::id` opt those charts into tooltips. - The derive now resolves the component crate path (`gpui_kit::component` / `gpui_component` / `crate`) next to the GPUI one. ### Caching GPUI's `cached` only applies to entity-backed views (busted by `notify`), so it cannot wrap a chart, which is a value rebuilt every render. Instead an identified chart keeps its heavy geometry in element state: `LineChart` / `AreaChart` adopt the existing `paint_cached`, pie slices get `Arc::paint_cached`, and `SankeyChart` moves its placement into `prepaint` behind a key over the data, settings, label lines and bounds size (which also lets `tooltip_state`, which has no window, hit-test the placed nodes). Charts without an `id` rebuild everything, since sibling charts would otherwise share one cache slot. ### Gallery - The sidebar collapses off-canvas from a toggle at the left of the status bar (animated by `Sidebar`'s own transition); the resizable wrapper is gone. - The chart story gets realistic datasets (a year of SaaS metrics, traffic sources, browser share, plan mix, regions, products, pages, product review scores, 40 trading sessions) with data-driven footers ("Trending up by 5.2% this month" is computed), legends, a donut with its headline in the ring, single-hue shade ramps for categorical charts, and width-wise bar shading instead of length-wise gradients. The gallery binary registers `AllAssets` for the trend icons. Docs: a new "Hover and Tooltips" section (motion, caching, custom plots) in `website/component/chart.md` and `website/zh-CN/component/chart.md`. ### Theme palette `default-theme.json` spelled the palette `chart_1`..`chart_5` while the schema read `chart.1`..`chart.5`, so the listed ramp was never applied and every theme fell back to lightened/darkened `blue`. The theme files now use the dotted keys the schema reads, which align with the `chart_1`..`chart_5` fields; the Rust API is unchanged. `chart_bullish` / `chart_bearish` are keyed `chart.bullish` / `chart.bearish` like the other dotted keys, and `CandlestickChart::bullish()` / `bearish()` override them for markets that read a rise as red. `.theme-schema.json`, the theme viewer story and the docs follow. ## Breaking Changes None in the Rust API. Theme files key the candle colors like the other dotted keys: ```diff - "chart_bullish": "green-600", - "chart_bearish": "red-600", + "chart.bullish": "green-600", + "chart.bearish": "red-600", ``` ## How to Test - `cargo test -p gpui-component --lib -- plot:: chart::` — `Arc::contains` hit-testing, `ScaleBand::step`, `TooltipState` hover readers, alongside the existing plot tests. - `cargo test -p gpui-component-story --lib` — the story's number formatting and trend maths. - `cargo run -- Chart`: hover every chart kind; the pointer glides between data, the pie slice lifts, sankey links fade, the overlay fades out after the cursor leaves. Toggle the sidebar from the status bar. - [x] Passed `cargo run` for story tests related to the changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 4 天前 | |
input: Add atomic inline tokens to Input and Textarea (#3113) Closes #3110 ## Description Add optional atomic inline tokens to Input and Textarea for mentions, commands, file and image references. A token can display “Alice” while the input value and the clipboard contain `@alice`. Users select, delete and undo the whole token; the application maps its ID back to the resource it names. ## Model and API The input stays a plain-text control: the value is the text, and each token is an annotation over a byte range that follows edits — the same model as Draft.js entity ranges and the VS Code chat input's parts over Monaco decorations, rather than a document tree with atom nodes. Base owns the behavior (atomic caret movement, selection and deletion, history with token deltas, mixed text/element layout and wrapping, activation); Component owns the default appearance. ```rust // Insert a reference at the caret or over the selection; `?` reports why it was refused. state.replace_with_token(InlineToken::new("person:alice", "@alice").with_label("Alice"), window, cx)?; // Save and restore a draft with its tokens; `set_value` takes text or content. let draft = state.content(); state.set_value(draft, window, cx); // Build content from storage; every token is validated as it is attached. let draft = InputContent::new("Ask @alice") .with_token(4..10, InlineToken::new("person:alice", "@alice").with_label("Alice"))?; ``` - `InlineToken` carries a resource ID, its text and a display label. The ID names the resource and may occur more than once in one input. - `InputContent` is what `content()` returns and what `set_value` accepts; plain text converts into content without tokens. `InputContent::with_token` returns `Result`, so a content value is always consistent before it reaches the input. - `Input` / `Textarea` (Base and Component) take a `token` slot and `on_token_click`. A click selects the whole token and then runs the listener; dragging or Shift-selecting does not open it. `ActivateToken` opens an exactly selected token from a key binding or assistive technology. - Component renders tokens as `InputToken` by default, with `icon` and `Styled`; a selected token paints its own selected state, and the text selection highlight stops at its edges. The Base token element uses the arrow cursor, and tokens are measured whenever they render, so an element that grows reflows on the next frame. - The JavaScript API mirrors this on `InputState` / `TextareaState` and on the registered components: `content()`, `tokens()`, `set_value(string | InputContent)`, `replace_with_token`, `replace_range_with_token`, `token`, `on_token_click`. Ranges are UTF-16 string offsets, and validation errors carry `error.code`. Tokens remain opt-in. Editor, NumberInput, formatted masks and password fields do not accept token insertion. Copy/paste uses ordinary text; resource lookup, delimiters around an inserted reference and submission remain application decisions. ## Story and docs The Story is a chat composer shared by Input and Textarea: an `InputGroup` with a toolbar that inserts a `/commit-pr` command, an `[Image 1]` attachment, a `$gpui-kit` skill and an `@alice` mention as tokens, a Send action, draft save/restore, read-only and disabled toggles and a readout of the text, references and last action. The JavaScript Story has the same example. English and Chinese documentation cover insertion, appearance, drafts, validation and the JavaScript API for Input, Textarea and InputGroup. ## Public API ### `gpui-base` (`gpui_base::input`) Data: - `InlineToken` — a reference rendered as one editing unit. `new(id, text)`, `with_label(label)`; readers `id()`, `text()`, `label()`. Derives `Clone, Debug, PartialEq, Eq, Hash`. - `InlineTokenSpan` — a token at its current UTF-8 byte range: `range() -> Range<usize>`, `token() -> &InlineToken`. - `InputContent` — text with its tokens, what `content()` returns and `set_value` accepts. `new(text)`, `with_token(range, token) -> Result<Self, InlineTokenError>` (validates boundary, overlap and text match), `text()`, `tokens()`. `From` for every text type `set_value` accepted before (`&str`, `String`, `SharedString`, `Cow<str>`, `Arc<str>`, `Box<str>`, `char`, and their references). - `InlineTokenError` — `#[non_exhaustive]`: `InvalidRange`, `InvalidBoundary`, `InvalidToken`, `OverlappingTokens`, `TextMismatch`, `UnsupportedMode`, `ValidationRejected`, `CompositionActive`. Implements `Error` and `Display`. - `InlineTokenContext` — what a token renderer sees: `token()`, `range()`, `is_selected()`, `is_disabled()`, `is_readonly()`, `line_height()`, `available_width()`. - `InlineTokenClickEvent` — what a click listener receives: `token()`, `range()`, `bounds() -> Bounds<Pixels>`, `click() -> &ClickEvent`. - `ActivateToken` — action that opens an exactly selected token (key bindings, assistive technology). `InputState` / `TextareaState` (`InputBaseState<InputMode | TextareaMode>`): - `replace_with_token(token, window, cx) -> Result<(), InlineTokenError>` — replace the selection, or insert at the caret, with a token. - `replace_range_with_token(range, token, window, cx) -> Result<(), InlineTokenError>` — replace a byte range, expanding overlaps to whole tokens. - `tokens() -> &[InlineTokenSpan]` — the current tokens in document order. - `content() -> InputContent` — the text with its tokens. - Changed: `set_value(value: impl Into<InputContent>, window, cx)` — was `impl Into<SharedString>`; plain text still converts, content also restores tokens. `Input` / `Textarea` elements: - `token(render: impl Fn(&InlineTokenContext, &mut Window, &mut App) -> impl IntoElement)` — the element each token renders as. - `on_token_click(listener: impl Fn(&InlineTokenClickEvent, &mut Window, &mut App))` — runs after a completed, unconsumed click on a token. Hidden (`#[doc(hidden)]`, for styled controls only): `InlineTokenRenderer`, `InlineTokenClickListener`, `InputBaseState::install_token_presentation`. ### `gpui-component` (`gpui_component::input`) - `InputToken` — the element a token renders as by default: `new(&InlineTokenContext)`, `icon(impl Into<Icon>)`, `Styled`. Selected, disabled and readonly states come from the context. - `Input` / `Textarea`: `token(..)` and `on_token_click(..)` as in Base; `InputGroupInput` / `InputGroupTextarea` inherit them. - Re-exports of the Base data types above and `ActivateToken`. ### `gpui-shell` (host integration) - `StateMethodDescriptor` — an opt-in operation on retained state exposed to scripts: `new::<T>(name, signature, call)`, `with_readonly(bool)`, `name()`, `signature()`, `is_readonly()`. - `StateDescriptor::with_methods(Vec<StateMethodDescriptor>)`, `methods()`. - `ComponentElementCallback::build_interactive_data_with(args, window, cx) -> Result<Option<AnyElement>>` — build a frame-owned inline subtree whose child callbacks retire with that frame. - `ComponentCallback::invoke_data_with(args, window, cx) -> Result<()>`. - `InlineTokenCallbacks` — script callbacks adapted to an element's `token` / `on_token_click` builders: `new(&Entity<InputBaseState<M>>, renderer, listener)`, `apply(element, token, on_token_click)`. - `inline_token_context_data`, `inline_token_click_data`, `input_token_state_methods()`, `textarea_token_state_methods()`. ### JavaScript (`gpui-base` / `gpui-component`) - `InputState` / `TextareaState`: `content(): InputContent`, `tokens(): InlineTokenSpan[]`, `replace_with_token(token)`, `replace_range_with_token(range, token)`, `set_selected_range(range)`, `replace(text)`; changed: `set_value(next: string | InputContent)`. Ranges are UTF-16 offsets; validation errors carry `error.code`. - `Input` / `Textarea` / `InputGroupInput` / `InputGroupTextarea`: `token(render)`, `on_token_click(listener)`. - Types: `InputRange`, `InlineToken`, `InlineTokenSpan`, `InputContent`, `InlineTokenContext`, `InlineTokenClickEvent`. ## Breaking Changes None. Existing input APIs and `InputEvent` variants are unchanged; the token APIs are additive. `set_value` now takes `impl Into<InputContent>`, and every text type it accepted before converts into content, so existing calls compile and behave as they did. ## How to Test Validated on macOS on top of the latest `main`: ```sh cargo fmt --all --check cargo clippy --workspace --all-targets -- --deny warnings cargo test -p gpui-base --lib input:: cargo test -p gpui-shell --lib component_callback_value_tests cargo test -p gpui-component-shell --test inline_tokens_host --test input_group_host --test layout_host cargo test -p gpui-component-story --lib token_story GPUI_COMPONENT_SHELL_BIN=target/debug/gpui-component-shell node crates/component-shell/tests/types/run.mjs cargo run ``` The Base tests cover token insertion and rejection, history with token deltas, boundary and word movement, IME composition, mode gates, wrapping and re-measurement, click selection and re-entrant activation. The Story test drives the composer through insertion, partial-selection deletion, undo, save/restore, sending and the read-only/disabled gates for both controls. Host tests cover UTF-16 emoji boundaries, atomic rejection, re-render retention, token activation and custom child callbacks. In the Story gallery, open Input or Textarea and scroll to “Atomic inline tokens”: click a token to select it and read the status line, insert the same reference twice, select part of a token and delete it, undo, and save and restore the draft. Real IME candidate/cancellation behavior on Windows and Linux remains unverified. ## AI Assistance The implementation, tests and documentation were written with Codex assistance; the API review and rework (resource IDs, `set_value(content)`, the presentation seam, click selection, naming and the composer Story) were done with Claude Code and reviewed by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Jason Lee <huacnlee@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 3 天前 | |
kit: Pin gpui-pre to the exact snapshot each release is built against (#3163) ## Summary gpui-kit 0.6.4 on crates.io declares `gpui-pre ^0.3.5`. The weekly Release GPUI run published 0.3.6 this morning with a changed `register_inspector_element` signature, so every fresh build of 0.6.4 now resolves 0.3.6 and fails with E0593 in gpui-component's `inspector.rs` (#3156). Main was already adapted to 0.3.6 in #3147; only the published crates are broken, because they carry the workspace requirement to crates.io verbatim. - Pin every `gpui-pre-*` snapshot crate (and the hand-published `gpui-pre-reqwest`) with `=x.y.z` in the workspace `Cargo.toml`, so a gpui-kit release keeps resolving the snapshot it was tested with and a new snapshot only reaches applications through a release that bumps the pin. - Add `script/check-gpui-pin.ts` (Bun), run in the CI `checks` job. It fails on a requirement that is not exact, on snapshot crates pinned to more than one version, and on a manifest with no snapshot dependency at all. - Teach `bump-gpui.ts`'s kit check to move the workspace pins onto the staged version for the duration of the check, since a `[patch]` only applies to a source that satisfies the requirement, and to restore `Cargo.toml` afterwards. The rewrite is covered by `--self-test`. - Document the pin policy and the bump procedure in `CONTRIBUTING.md`, which still described publication as paused on an incompatible snapshot (it publishes either way since #3148). `Cargo.lock` is unchanged: it already resolves 0.3.6. A 0.6.5 release from `main` is what fixes crates.io users; until then 0.6.4 builds with `cargo update -p gpui-pre -p gpui-pre-platform -p gpui-pre-web -p gpui-pre-macros -p gpui-pre-sum-tree -p gpui-pre-reqwest-client --precise 0.3.5`. Closes #3156 ## Test Plan - `bun script/check-gpui-pin.ts` passes on the repository manifest and fails on fixtures with caret requirements, mixed snapshot versions, and no snapshot dependency - `bun script/bump-gpui.ts --self-test` - `cargo metadata --locked` and `cargo check -p gpui-kit --locked` (lockfile unchanged) - `typos`, `git diff --check` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 6 小时前 | |
Version 0.6.5 | 6 小时前 | |
Version 0.6.5 | 6 小时前 | |
refactor: introduce the internal gpui-base architecture (#2677) ## Summary This PR introduces the internal `gpui-base` foundation crate and migrates reusable GPUI behavior out of `gpui-component` while keeping the styled UI facade as the canonical full-component layer. ## Before / After | Area | Before | After | | --- | --- | --- | | Crate boundary | Reusable behavior and styled components lived together in `gpui-component` | `gpui-component -> gpui-base -> gpui` is a one-way dependency; `gpui-base` is workspace-internal and is not a publishing milestone | | Shared infrastructure | Geometry, events, focus trap, styled helpers, history, index paths, auto-scroll, virtual lists, and scrollbar behavior were owned by UI | Generic implementations live in `gpui-base`; existing `gpui_component` paths re-export the same type identity where applicable | | Scrollbar | State, painting, dragging, handles, visibility, and theme styling were coupled in `crates/ui/src/scroll/scrollbar.rs` | The complete primitive lives in `gpui_base::Scrollbar`; UI re-exports it directly from `scroll/mod.rs`; project-specific `Scrollable` and `ScrollableMask` remain in UI | | Scrollbar API | `ScrollbarShow` and `.scrollbar_show(...)` | `ScrollbarMode` and `.mode(...)`; no compatibility alias is retained | | Scrollbar styling | Appearance was fixed by the legacy UI theme | Fluent `ScrollbarStyles` covers track/thumb normal, hover, and active appearance and geometry; Base has a minimal fallback and UI Theme overrides it | | Base globals | Styled helpers depended on an ad-hoc `StyledTheme` projection | One `gpui_base::Theme` global owns semantic tokens and module defaults such as `ScrollbarTheme { mode, styles }` | | Theme updates | Direct Story mutation could leave Base scrollbar state stale | `Theme::set_scrollbar_mode` synchronizes UI Theme and Base Theme; old serialized `scrollbar_show` state is accepted through a serde migration alias | | Controls | Behavior, presentation, content, and state styling were interleaved | Base primitives own reusable interaction/state contracts; UI keeps variants, sizing, labels/icons, layout, theme values, tooltips, and presentation | | Style boundary | `ui/styled.rs` mixed generic and UI-specific helpers | Generic GPUI styled/focus helpers moved to Base; UI keeps sizing and component traits; `ui/styled.rs` is re-export-only | | Specifications | Decisions were spread across working notes | RFC, style/motion design, milestones, validation state, and per-control review criteria live under `specs/` | ## Compatibility and deliberate changes - Existing styled component module paths remain available through `gpui_component`. - Button, Checkbox, Switch, and Slider were manually checked in Story during migration. - `ScrollbarShow` is intentionally removed in favor of `ScrollbarMode`. - Canonical Toggle and Switch keyboard/focus behavior was adopted after explicit review; these exceptions are documented in the checklist. - Base controls remain presentation-free. Scrollbar is the intentional exception because GPUI does not provide one; it has only a minimal default paint that application/UI styles can override. - Radio vertical integration, overlay/popup extraction, Dock extraction, and Registry rollout remain deferred. - `Scrollable` and `ScrollableMask` intentionally remain in `crates/ui`. - The intentionally updated Apache license files are preserved. ## Test Plan - `cargo fmt --all --check` - `cargo check -p gpui-base -p gpui-component -p gpui-component-story` - `cargo check -p gpui-component --no-default-features` - `cargo clippy -p gpui-base -p gpui-component --all-targets -- --deny warnings` - `cargo test -p gpui-base --lib` — 71 passed - `cargo test -p gpui-component --lib` — 441 passed - `cargo test -p gpui-component --test base_compat --test legacy_button_compat --test legacy_controls_compat` — 18 passed - `cargo test -p gpui-component-cli --test cli` — 9 passed - `git diff --check` - Manual Story verification: Button, Switch, Checkbox, and Slider Publishing/package verification for `gpui-base` is intentionally outside this phase. --------- Co-authored-by: Codex <codex@openai.com> | 1 个月前 | |
notification: Fix center notification stacks reliably on web (#3149) ## Summary - position horizontally centered notification stacks inside a full-width flex wrapper - avoid relying on conflicting absolute offsets and automatic margins - add a layout regression test for `Anchor::TopCenter` Closes #3137 ## Test Plan - `cargo test -p gpui-component notification::tests --lib` - `cargo clippy -p gpui-component --lib --tests -- -D warnings` - `git diff --check` --------- Co-authored-by: Codex <codex@openai.com> | 9 小时前 | |
dock: Remove the tiles canvas (#3036) The dock had three container shapes: `Split`, `Tabs`, and `Tiles`, a canvas of freely positioned panels. Only Longbridge's "custom layout" ever used the canvas, and it is moving into that application as an ordinary dock panel, so this removes the third shape from every layer: the layout tree and its edits, the persisted schema (`PanelInfo::Tiles`, `TileMeta`), `DockArea`'s reconciliation and zoom, the component skin and its theme fields, the shell's script API, the `example-tiles` program, and the docs. What a host needs instead is one small, general hook: `gpui_component::dock::Panel::title_bar` (default `true`). A panel that carries its own chrome returns `false`, and a tab group holding only that panel draws no title bar above it. A group with several panels still draws its tabs. `DropTarget` loses its `Canvas` variant and becomes a struct — a host-owned drop can only land on a tab group now — read through `node()` and `placement()`. ## Breaking Changes Describing a layout: ```diff - DockLayout::tiles().tile(panel, bounds) - DockLayout::tiles().tile_view(panel_handle(panel), bounds, cx) + // Place panels in tab groups; a freeform canvas is now a panel the host implements. + DockLayout::tabs().panel_view(panel_handle(panel), cx) ``` Editing a live area: ```diff - area.add_tile(panel, DockPlacement::Center, bounds, window, cx); - area.add_tile_view(handle, DockPlacement::Center, bounds, window, cx); - area.zoomed_tile(); + area.add_panel(panel, DockPlacement::Center, None, window, cx); + area.add_panel_view(handle, DockPlacement::Center, None, window, cx); + area.zoomed_group(); ``` Reading a tree: ```diff match node.kind() { PaneRef::Split { .. } => .., PaneRef::Tabs { .. } => .., - PaneRef::Tiles { panels } => .., } - InsertTarget::Tile { node, bounds } - TilePanel - tree.set_tile_bounds(panel, bounds); - tree.bring_to_front(panel); ``` Persisted state (a host that stored tiles must migrate the `"tiles"` info tag itself): ```diff - PanelInfo::Tiles { metas: Vec<TileMeta> } // serde tag "tiles" - TileMeta { bounds, z_index } ``` Renderers and contexts: ```diff impl DockAreaRenderer for MySkin { fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> { .. } - fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> { .. } } - impl TilesRenderer for MySkin { .. } - TileContext, TilesState, TilesEvent, ResizeSide, DRAG_BAR_HEIGHT, HANDLE_SIZE ``` Host-owned drops: ```diff DockEvent::DragDrop { item, target } => match target { - DropTarget::Canvas => .., - DropTarget::Group { node, placement } => .., + target => (target.node(), target.placement()), } ``` Skin settings and theme: ```diff - skin.set_tiles_scrollbar_mode(Some(ScrollbarMode::Always), cx); - cx.theme().tiles // ThemeColor, "tiles.background" in theme JSON - cx.theme().tile_grid_size - cx.theme().tile_shadow - cx.theme().tile_radius ``` Shell, Rust side (`gpui_kit::shell::dock`): ```diff - DockChrome::tile_drag_bar / DockChrome::tile_resize_handles - DockCommand::MoveTile / ResizeTile / RaiseTile / ToggleTileZoom / CloseTile - tile_data(tile, cx) ``` Shell script API: ```diff - area.add_panel(view, { name, placement, bounds: { x, y, width, height } }) + area.add_panel(view, { name, placement, size }) - dock_area(area).tile_drag_bar(tile => ..).tile_resize_handles(tile => ..) - element.move_tile(tile) / resize_tile(tile, side) / raise_tile(tile) / toggle_tile_zoom(tile) / close_tile(tile) - DockTile, TileResizeSide ``` New: ```diff pub trait Panel: gpui_base::dock::Panel { + /// Whether the tab group draws a title bar above this panel when it is + /// the only panel in its group. + fn title_bar(&self, cx: &App) -> bool { true } } ``` --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 10 天前 | |
dock: Remove the tiles canvas (#3036) The dock had three container shapes: `Split`, `Tabs`, and `Tiles`, a canvas of freely positioned panels. Only Longbridge's "custom layout" ever used the canvas, and it is moving into that application as an ordinary dock panel, so this removes the third shape from every layer: the layout tree and its edits, the persisted schema (`PanelInfo::Tiles`, `TileMeta`), `DockArea`'s reconciliation and zoom, the component skin and its theme fields, the shell's script API, the `example-tiles` program, and the docs. What a host needs instead is one small, general hook: `gpui_component::dock::Panel::title_bar` (default `true`). A panel that carries its own chrome returns `false`, and a tab group holding only that panel draws no title bar above it. A group with several panels still draws its tabs. `DropTarget` loses its `Canvas` variant and becomes a struct — a host-owned drop can only land on a tab group now — read through `node()` and `placement()`. ## Breaking Changes Describing a layout: ```diff - DockLayout::tiles().tile(panel, bounds) - DockLayout::tiles().tile_view(panel_handle(panel), bounds, cx) + // Place panels in tab groups; a freeform canvas is now a panel the host implements. + DockLayout::tabs().panel_view(panel_handle(panel), cx) ``` Editing a live area: ```diff - area.add_tile(panel, DockPlacement::Center, bounds, window, cx); - area.add_tile_view(handle, DockPlacement::Center, bounds, window, cx); - area.zoomed_tile(); + area.add_panel(panel, DockPlacement::Center, None, window, cx); + area.add_panel_view(handle, DockPlacement::Center, None, window, cx); + area.zoomed_group(); ``` Reading a tree: ```diff match node.kind() { PaneRef::Split { .. } => .., PaneRef::Tabs { .. } => .., - PaneRef::Tiles { panels } => .., } - InsertTarget::Tile { node, bounds } - TilePanel - tree.set_tile_bounds(panel, bounds); - tree.bring_to_front(panel); ``` Persisted state (a host that stored tiles must migrate the `"tiles"` info tag itself): ```diff - PanelInfo::Tiles { metas: Vec<TileMeta> } // serde tag "tiles" - TileMeta { bounds, z_index } ``` Renderers and contexts: ```diff impl DockAreaRenderer for MySkin { fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> { .. } - fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> { .. } } - impl TilesRenderer for MySkin { .. } - TileContext, TilesState, TilesEvent, ResizeSide, DRAG_BAR_HEIGHT, HANDLE_SIZE ``` Host-owned drops: ```diff DockEvent::DragDrop { item, target } => match target { - DropTarget::Canvas => .., - DropTarget::Group { node, placement } => .., + target => (target.node(), target.placement()), } ``` Skin settings and theme: ```diff - skin.set_tiles_scrollbar_mode(Some(ScrollbarMode::Always), cx); - cx.theme().tiles // ThemeColor, "tiles.background" in theme JSON - cx.theme().tile_grid_size - cx.theme().tile_shadow - cx.theme().tile_radius ``` Shell, Rust side (`gpui_kit::shell::dock`): ```diff - DockChrome::tile_drag_bar / DockChrome::tile_resize_handles - DockCommand::MoveTile / ResizeTile / RaiseTile / ToggleTileZoom / CloseTile - tile_data(tile, cx) ``` Shell script API: ```diff - area.add_panel(view, { name, placement, bounds: { x, y, width, height } }) + area.add_panel(view, { name, placement, size }) - dock_area(area).tile_drag_bar(tile => ..).tile_resize_handles(tile => ..) - element.move_tile(tile) / resize_tile(tile, side) / raise_tile(tile) / toggle_tile_zoom(tile) / close_tile(tile) - DockTile, TileResizeSide ``` New: ```diff pub trait Panel: gpui_base::dock::Panel { + /// Whether the tab group draws a title bar above this panel when it is + /// the only panel in its group. + fn title_bar(&self, cx: &App) -> bool { true } } ``` --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 10 天前 | |
assets: share Lucide icon names and preserve default icons (#3020) ## Description Provide the complete Lucide catalog through `gpui-kit-assets::IconName` so Base and other presentation layers can share it without depending on GPUI Component. The package includes all 1,818 Lucide 1.43.0 SVGs and the 12 retained GPUI Kit icons. Keep the existing application workflow: default `Assets` embeds the original 101 component icons, and applications provide additional icons through their own `AssetSource`. Full native embedding is an explicit `AllAssets` choice. The optional `icon_assets!(ExtraIcons, [...])` macro creates a selected source that can be composed with the default bundle. WASM keeps its existing on-demand CDN loader; selected sources can embed bytes directly. ## Compatibility Existing component imports, exhaustive matches, and `IconName::Search.view(cx)` calls remain valid without additional trait imports. Component retains its original enum as a compatibility adapter; `Icon::new(...)` accepts both this enum and the new shared assets enum. Legacy names can convert into shared names using `.into()`. The extension trait is only an optional convenience for the new shared enum. Preserve `Assets::get`, `Assets::iter`, `Assets::new`, the original default path set, asset-source behavior, and Cargo icon-directory metadata. The complete catalog and default component bundle are generated separately; adding Lucide files does not expand default embedding. ## Documentation and maintenance README and both language versions of Icon / Icons & Assets have a prominent NOTE distinguishing the catalog from default embedding, plus a measured comparison table. A default-plus-10-extra-icons example adds 19,648 bytes to a Linux release/stripped resource program; those extra SVGs total 3,903 bytes. The measurement includes source composition/lookup code and is not a fixed per-icon cost or a RAM estimate. `bun script/sync-lucide.ts` replaces the Python updater. It verifies the pinned archive SHA-256, supports offline `--archive` and read-only `--check`, copies the upstream license, and preserves custom icons and the default bundle selection. ## Validation - `cargo test -p gpui-kit-assets --test icons`: 5 tests covering complete/default catalogs, selected resources, empty selection and native compatibility. - `cargo test -p gpui-kit --test assets`: 2 tests covering shared names and unchanged legacy views/conversions; 1 test also passes with `--no-default-features --features assets`. - `cargo clippy -p gpui-component -p gpui-kit-assets -p gpui-component-story -- --deny warnings`; `cargo fmt --all --check`. - `cargo check -p gpui-component --no-default-features`. - `cargo check -p gpui-kit-assets --lib --example selected_assets --target wasm32-unknown-unknown`. - `cargo package -p gpui-kit-assets --allow-dirty` verifies packaged build generation. - `cargo run -p gpui-kit-assets --release --example extra_assets -- icons/accessibility.svg` and `icons/search.svg` verify application and component fallback paths. - `bun test script/tests/sync-lucide.test.ts`: 14 assertions covering check-only behavior, synchronization, custom-file preservation, count validation and corrupt-archive rejection. Online and offline checks verify all 1,818 upstream SVGs and their license. - Website production build passes; generated HTML contains the comparison table inside each of the four NOTE callouts. - Binary inspection confirms 101 SVG payloads by default, 103 with two extras, 111 with ten extras, and 1,830 only with explicit `AllAssets`. | Native resource configuration | Embedded SVG size (KiB) | Binary increase over default (KiB) | | --- | ---: | ---: | | Default (101 icons) | 44.28 | 0.00 | | Default + 2 extras | 45.04 | 15.19 | | Default + 10 extras | 48.09 | 19.19 | | Explicit AllAssets | 731.45 | 1045.16 | 1 KiB = 1,024 bytes. Measured with Rust 1.98.0 on Linux x86_64, release optimization and stripped symbols. Every program retains the same shared-name lookup and runtime path; extra sources fall back to the default and merge/sort/deduplicate their lists. The documentation names the exact icons and explains limitations. ## Checklist - [x] Read the contribution guidelines. - [x] Reviewed the implementation, including AI-generated code. - [x] Ran targeted tests and resource examples. - [ ] Ran the interactive story gallery. - [ ] Tested native macOS and Windows behavior (Linux and WASM compilation validated). | 13 天前 | |
chore: fix flake dynamic libraries, add binary target (#2212) | 5 个月前 | |
chore: Provide IBM Plex Sans to fix story app lag to nix (#2592) ## Summary - add IBM Plex Sans to the Nix development environment - provide a deterministic Fontconfig configuration when running the story app with Cargo - apply the same font configuration to the packaged executable wrapper ## Why GPUI resolves the Linux system UI font to IBM Plex Sans. On NixOS, that font was not available inside the development shell, so text rendering repeatedly took the missing-font fallback path. With `RUST_BACKTRACE=1` enabled by the shell, those fallback errors also captured backtraces and caused high single-thread CPU usage during scrolling and other UI interactions. Providing the expected font removes that hot path. In an 8-second runtime sample, CPU time dropped from approximately 4.05 seconds to 0.97 seconds, and profiling no longer showed unwind/backtrace work. ## Testing - `nix flake check --no-build` - `nix develop --command cargo run --release` - verified `fc-match "IBM Plex Sans"` resolves to `IBMPlexSans-Regular.otf` inside the dev shell - `nix build .#defaultPackage.x86_64-linux` - launched the packaged executable through its generated wrapper Related issue: #1621 | 1 个月前 |
GPUI Kit
使用 Rust 和 GPUI 构建出色、高性能的桌面应用。
GPUI Kit 是一个综合性的 Rust 桌面应用开发框架。它将生产级 UI 系统、应用级数据与布局能力、编辑能力,以及可复用的行为、状态和基础设施整合在一起, 并让交付后的应用可以被 JavaScript 扩展。
gpui-kit 应用唯一需要依赖的 crate
├── gpui-base 无样式的行为、状态与基础设施
└── gpui-component GPUI Component:完整的带样式 UI 系统
gpui-kit 会固定配套的 GPUI 版本并导出 GPUI、base、component 和 assets,Rust 应用只需声明这一个依赖。JavaScript 扩展宿主另行依赖 gpui-shell;gpui-component-shell 提供带样式的组件目录。
特性
- 60+ 组件:覆盖表单、导航、浮层、反馈和布局等场景,提供成熟交互与高效默认值。
- 生产就绪:从第一天起用于构建 Longbridge Pro,并在公开发布的商业桌面应用中持续打磨。
- 原生体验:现代控件设计灵感来自 macOS 与 Windows,并提供语义化主题和多种尺寸。
- 120 FPS:GPU 加速界面,在高负载下依然保持流畅。
- 数据表格:虚拟滚动、固定列、列宽调整、排序与单元格选择,可承载数十万行数据。
- 虚拟列表:只渲染可见区域,并支持不同尺寸的列表项。
- 代码编辑器:20 万行规模下仍保持稳定,集成 Tree-sitter 高亮与 LSP 诊断、补全和悬浮提示。
- Dock 布局:可调整面板、可拖拽标签、嵌套分割、边缘停靠,并可序列化保存。
- 丰富内容:原生 Markdown 与 HTML 渲染、语法高亮和内置图表。
- 设计自由:使用完整视觉系统,或基于
gpui-base的行为与基础设施构建自己的系统。 - JavaScript 扩展:
gpui-shell让已发布的 Rust 宿主以脚本方式加载面板与业务逻辑,每项能力都需显式授予。 - 跨平台:通过一份 Rust 代码交付 macOS、Windows 和 Linux。
框架架构
三层架构,一个生态
使用 gpui-component,让整个应用保持统一、完整的视觉与交互风格;当产品需要创建并拥有自己的设计系统时,使用 gpui-base;当应用需要在交付后仍可被 JavaScript 扩展时,使用 gpui-shell。
gpui-component |
gpui-base |
gpui-shell |
|---|---|---|
| 完整且带样式的组件 | 无预设样式的行为与基础设施 | 由 Rust 托管的 JavaScript 运行时 |
| 开箱即用,并支持主题定制 | 完全掌控结构与视觉设计 | 能力逐项授予 |
| 适合直接构建应用 | 适合构建设计系统 | 适合插件与脚本化应用 |
APPLICATION
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ gpui-component │ │ Your Design │ │ gpui-shell │
│ Styled UI │ │ System │ │ JS extensions │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌──────────────────┐
│ gpui-base │
│ Behavior · State │
│ Infrastructure │
└────────┬─────────┘
▼
GPUI
行为属于基础层,呈现属于应用。
如果希望使用精致、开箱即用且风格统一的控件,请选择 gpui-component。如果应用需要拥有组件源码、布局、样式和动效,同时复用复杂且可靠的交互行为,请直接构建于 gpui-base。如果希望贡献者无需 fork、也无需发新版本就能扩展产品,请加入 gpui-shell。
这种分层方式与 shadcn 生态的灵活性来源一致:
| GPUI Kit 生态 | Web 生态 |
|---|---|
| GPUI | HTML + Tailwind CSS |
gpui-base |
Base UI |
gpui-component |
shadcn 的完整样式组件层 |
Showcase
GPUI Kit 从第一天起就用于构建 Longbridge Pro。 这个框架不是脱离应用场景凭空设计出来的,而是从一款公开发布的商业桌面应用中持续提炼而成。
GPUI 为渲染打下基础,Longbridge 为生产实践打下基础。
Usage
[dependencies]
gpui-kit = "0.6"
gpui-kit 始终引入 GPUI 和 gpui-base;gpui-component 和默认图标集默认开启。只想保留部分层时关闭默认 feature 按需选择即可。gpui-component 的 feature(inspector、decimal、tree-sitter 及各 tree-sitter-<language>)在 gpui-kit 上同名可用。
基础示例
use gpui_kit::component::button::*;
use gpui_kit::component::*;
use gpui_kit::*;
pub struct HelloWorld;
impl Render for HelloWorld {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.gap_2()
.size_full()
.items_center()
.justify_center()
.child("Hello, World!")
.child(
Button::new("ok")
.primary()
.label("Let's Go!")
.on_click(|_, _, _| println!("Clicked!")),
)
}
}
fn main() {
gpui_kit::application().run(move |cx| {
// 使用任何 GPUI Component 功能之前必须先调用此函数。
gpui_kit::init(cx);
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
let view = cx.new(|_| HelloWorld);
// 窗口的第一层应该是一个 Root。
cx.new(|cx| Root::new(view, window, cx))
})
.expect("Failed to open window");
})
.detach();
});
}
图标
默认开启的 assets feature 会以 gpui-kit-assets 的形式内置 Lucide 图标集,通过 gpui_kit::application().with_assets(gpui_kit::assets::Assets) 交给应用即可。若想使用自己的图标,去掉该 feature,并按照 IconName 中的定义命名 SVG 文件。
AI 编码 Agent 技能 (Skills)
为你的 AI 编码助手(Cursor, Claude Code, Gemini CLI, Codex 等)安装 GPUI Kit 技能库:
npx skills add longbridge/gpui-kit
| 技能 | 描述 |
|---|---|
gpui-kit |
初始化、组件目录、常用使用模式、GPUI 机制(Element、Entity、异步、焦点、Actions、测试),以及 Coding Guides。 |
gpui-kit-design-guides |
Design Guides:布局、间距、层级、交互状态、浮层与界面文案的规范。 |
Development
桌面 Gallery(Story)
story crate 是一个展示所有可用组件的画廊应用程序,通过以下命令运行:
cargo run
Examples
一些较大的示例复用 story 画廊组件,并作为独立 package 运行:
# Dock 布局系统(面板、分割视图、标签页)
cargo run -p example-dock
# Markdown 渲染
cargo run -p example-markdown
# HTML 渲染
cargo run -p example-html
examples 目录还包含独立示例,每个示例专注于单一功能。每个示例是一个独立的 crate,使用 cargo run -p <name> 运行:
# 支持 LSP 和语法高亮的代码编辑器
cargo run -p example-editor
# 基础 Hello World
cargo run -p hello_world
# 系统监控器(实时 CPU/内存数据图表)
cargo run -p system_monitor
# 窗口标题自定义
cargo run -p window_title
查看 CONTRIBUTING.md 了解更多详情。
与其他框架对比
请查看站点上的与 Iced、egui、Qt 6 的对比。
许可证
Apache-2.0