Repository navigation
Conversation
## Description
A performance audit of gpui-base and gpui-component flagged these
issues. Each finding was checked against the code by an independent
reviewer before it was fixed.
- crates/component/src/rating.rs: the star on_mouse_move listener now
sets hovered_value and calls cx.notify() only when the hovered star
index actually changes. Before, every pointer move re-rendered the
parent view that observes the use_keyed_state entity.
- crates/component/src/rating.rs: the container on_hover leave branch
now notifies only when hovered_value was non-zero. on_mouse_move is
kept, so stars still re-highlight after a click exactly as before. No
test was added because the rendered output is unchanged and only
redundant redraws are removed.
## Performance
| case | before | after | change |
| --- | --- | --- | --- |
| rating_sweep_20_stars: time per pass | 66.85 ms | 6.94 ms | -89.6% |
| rating_sweep_20_stars: host renders per pass | 799 | 21 | -97.4% |
| rating_jitter_one_star: time per pass | 66.29 ms | 5.36 ms | -91.9% |
| rating_jitter_one_star: host renders per pass | 800 | 1 | -99.9% |
- **rating_sweep_20_stars**: a 20-star medium Rating inside a cached
600x40 host view; the pointer sweeps across all 20 stars in 0.5px steps,
800 moves per pass with a window draw after each move, so there are 20
real hover changes.
- **rating_jitter_one_star**: the same view; 800 moves jittering inside
the fifth star, so there is 1 real hover change.
Before the fix every pointer move notified the keyed Rating state, so
the host re-rendered on almost every frame (799 and 800 renders). After
it, the host renders once per real hover change plus the initial entry
(21 and 1), and wall-clock time per pass drops by roughly 10x to 12x.
Release profile, pinned to one performance core (taskset -c 8) on an
Intel Core i7-13700KF, Linux 7.2.5, rustc 1.98.1. Each run is gated on a
quiet machine (1-minute load < 1.5, no rustc or cargo running). Base
(merge-base with main) and branch are alternated for 5 rounds; each
round takes the median of its timed runs after warm-up, and the table
shows the median across rounds. Round-to-round spread was under 2%. Each
round does 2 warm-up passes and 9 timed passes per case; host render
counts were identical in every round.
<details><summary>Benchmark harness (not part of this diff)</summary>
Registered locally as `#[cfg(test)] mod rating_bench;` in
`crates/component/src/lib.rs` and run with `rating_bench --ignored
--nocapture --test-threads=1`.
```rust
//! Benchmark harness for `Rating` hover handling (not committed).
//!
//! Run with `--ignored --nocapture --test-threads=1`.
use std::{cell::Cell, rc::Rc, time::Instant};
use gpui::{
AppContext as _, Context, Entity, IntoElement, Modifiers, ParentElement as _, Render,
StyleRefinement, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px,
};
use crate::{h_flex, rating::Rating};
const STARS: usize = 20;
/// Medium star: 16px icon + 2px padding on each side.
const STAR_WIDTH: f32 = 20.;
const MOVES: usize = 800;
const WARMUP: usize = 2;
const RUNS: usize = 9;
struct RatingHost {
renders: Rc<Cell<usize>>,
}
impl Render for RatingHost {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
self.renders.set(self.renders.get() + 1);
h_flex()
.size_full()
.child(Rating::new("bench-rating").max(STARS).value(3))
}
}
struct BenchRoot {
host: Entity<RatingHost>,
}
impl Render for BenchRoot {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let mut style = StyleRefinement::default();
style.size.width = Some(px(600.).into());
style.size.height = Some(px(40.).into());
div().size_full().child(self.host.clone().cached(style))
}
}
fn draw(cx: &mut VisualTestContext) {
cx.update(|window, cx| window.draw(cx).clear(cx));
}
fn median(mut samples: Vec<f64>) -> f64 {
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
samples[samples.len() / 2]
}
/// Moves the pointer `MOVES` times, drawing a frame after each move the way
/// the platform frame loop would. Returns (elapsed_us, host_renders).
fn run_pass(
cx: &mut VisualTestContext,
renders: &Rc<Cell<usize>>,
position_at: impl Fn(usize) -> f32,
) -> (f64, usize) {
// Park the pointer outside the stars so every pass starts un-hovered.
cx.simulate_mouse_move(point(px(590.), px(10.)), None, Modifiers::default());
draw(cx);
let before = renders.get();
let start = Instant::now();
for i in 0..MOVES {
cx.simulate_mouse_move(point(px(position_at(i)), px(10.)), None, Modifiers::default());
draw(cx);
}
let elapsed = start.elapsed().as_secs_f64() * 1_000_000.;
(elapsed, renders.get() - before)
}
fn bench_case(
name: &str,
cx: &mut VisualTestContext,
renders: &Rc<Cell<usize>>,
position_at: impl Fn(usize) -> f32 + Copy,
) {
let runs = std::env::var("RATING_BENCH_RUNS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(RUNS);
for _ in 0..WARMUP {
run_pass(cx, renders, position_at);
}
let mut times = Vec::with_capacity(runs);
let mut render_counts = Vec::with_capacity(runs);
for _ in 0..runs {
let (us, n) = run_pass(cx, renders, position_at);
times.push(us);
render_counts.push(n);
}
let renders_per_pass = render_counts[render_counts.len() / 2];
println!(
"BENCH {name} median_us={:.1} runs={runs} moves={MOVES} host_renders={renders_per_pass}",
median(times)
);
}
#[gpui::test]
#[ignore]
fn bench_rating_hover(cx: &mut TestAppContext) {
cx.update(crate::init);
let renders = Rc::new(Cell::new(0));
let host = cx.update(|cx| {
cx.new(|_| RatingHost {
renders: renders.clone(),
})
});
let (_, cx) = cx.add_window_view(move |_, _| BenchRoot { host });
let cx: &mut VisualTestContext = cx;
draw(cx);
// Pointer sweeps across all 20 stars in 0.5px steps: 20 real hover changes.
let sweep_step = STARS as f32 * STAR_WIDTH / MOVES as f32;
bench_case("rating_sweep_20_stars", cx, &renders, move |i| {
1. + i as f32 * sweep_step
});
// Pointer jitters inside the fifth star: 1 real hover change.
bench_case("rating_jitter_one_star", cx, &renders, |i| {
4. * STAR_WIDTH + 3. + (i % 14) as f32
});
}
```
</details>
## How to Test
Run locally on this branch:
- `cargo clippy -p gpui-component --all-targets -- --deny warnings`:
clean.
- `cargo fmt --check`: reports diffs only in
`crates/component/src/form/tests.rs`, which this PR does not touch and
which is the same on main; CI Lint passes, so this is a local rustfmt
version difference. `rustfmt --check --edition 2024
crates/component/src/rating.rs`: clean.
- `cargo test -p gpui-component --lib`: 607 passed, 0 failed, 1 ignored
(the local benchmark). gpui-base is not touched by this PR.
CI: all 11 checks pass, including Lint and Test on aarch64-apple-darwin,
x86_64-linux-gnu and x86_64-windows-msvc.
To check by hand, open the Rating story, move the pointer across and
within the stars, click to set a value, and move away: the highlight
should follow the pointer and reset on leave exactly as before.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…3274) ## Description Requesting code actions again while the menu is open kept showing the previous actions, and Enter performed one of them. The styled overlay mirrors the Base `CodeActionMenuState` and rebuilds the menu only when its `revision` changes. `present_code_actions` bumps it, but the response handler in `handle_code_action_trigger` (`crates/base/src/input/editor/lsp/code_actions.rs`) did not. The first open still showed up because `open` went from false to true. A refresh with the menu already open changed neither `open` nor `revision`, so the overlay dropped it. The handler now bumps the revision when it replaces the items and when an empty response clears them. This came from the base split (longbridge#2677). Before it, every response called `CodeActionMenu::show` directly. Added the UI test `requesting_code_actions_again_replaces_the_open_menu`. It opens the menu for one selection, widens the selection, asks again, and checks that Enter performs the action for the new range. It fails on main (it performs the action for the old range `4..5`). ## How to Test ```sh script/test-input ``` Manually, in `cargo run -p example-editor`: select part of a word, press Ctrl/Cmd-., extend the selection with Shift-Left, then press Ctrl/Cmd-. again. The menu now lists the actions for the new selection. ## 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) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
## Description A performance audit of gpui-base and gpui-component flagged these issues. Each finding was checked against the code by an independent reviewer before it was fixed. - crates/component/src/command/state.rs: `install_model` now checks the incoming model against the installed one. It skips `update_matches` and does not set `needs_measure` when the searchable/filterable flags match and every entry passes `CommandEntry::same_layout`. It still stores the new model, so fresh callbacks and render data are used. Selection re-resolution runs exactly as before, including `reset_selection` and `pending_scroll`, so selection behaviour is unchanged. - crates/component/src/command/state.rs: new `pub(crate) fn set_options(&mut self, options: CommandOptions)` sets `needs_measure` when `options.style.text` changes, since measurement reads that value. crates/component/src/command/command.rs calls it instead of assigning `state.options` directly. - crates/component/src/command/item.rs: new `pub(crate) same_layout` on `CommandItem` and `CommandEntry`, plus a private one on `CommandGroup`. They compare label, keywords, whether an icon is present, checked, disabled, action (via `Action::partial_eq`), the custom `content` closure by pointer (`Rc::ptr_eq`), group heading and item count, and the entry variant (item, group or separator). Items built with `.child()` get a new closure on every render, so they are still measured every time, as today. - Added the regression test `reinstalling_an_unchanged_model_keeps_the_measured_rows` in state.rs. An equal model does not trigger measuring. Models with custom children, a changed label or a changed disabled flag still do, and matching and selection stay correct. Skipping `update_matches` is safe: matching depends only on the query, the searchable and filterable flags, and each item's label, keywords and disabled flag, and `same_layout` compares all of these. A follow-up review of the diff found issues, and they are fixed in this branch. ### Not in this PR - **Measure only custom rows (one fixed height per default row type)**: The finding explicitly left this out because it changes more behaviour. ## Public API None. `set_options` and `same_layout` are `pub(crate)` (and private on `CommandGroup`). ## Performance | case | before | after | change | | --- | ---: | ---: | ---: | | `command_rerender_unchanged_500`, per frame | 4.26 ms | 0.25 ms | -94.2% (17.3x faster) | | `command_rerender_unchanged_2000`, per frame | 17.75 ms | 0.60 ms | -96.6% (29.7x faster) | - **command_rerender_unchanged_500**: a host view re-renders a `Command` palette of 500 items (10 groups of 50, long labels, keywords, an action, an icon on every third item) from a freshly built but unchanged model; each run is 20 frames of `cx.notify()` + `window.draw()`, 9 timed runs after 2 warm-up runs. - **command_rerender_unchanged_2000**: the same palette with 2000 items (40 groups of 50) and the same 20 frames x 9 timed runs. Before the fix every re-render set `needs_measure`, so each frame laid out all N rows, not only the visible ones, and the cost grew linearly with N (4.26 ms at 500 items, 17.75 ms at 2000). After the fix an unchanged model keeps its measured row sizes, and what is left per frame is the model rebuild and the visible rows, which grows far more slowly (0.25 ms to 0.60 ms). Models with custom `.child()` rows are still measured every frame, as before. Release profile, pinned to one performance core (taskset -c 8) on an Intel Core i7-13700KF, Linux 7.2.5, rustc 1.98.1. Each run is gated on a quiet machine (1-minute load < 1.5, no rustc or cargo running). Base (merge-base with main, 452e72f) and branch are alternated for 5 rounds; each round takes the median of its timed runs after warm-up, and the table shows the median across rounds. Round-to-round spread was under 2.5% (largest: 2.3% for the branch at 2000 items). <details><summary>Benchmark harness (not part of this diff)</summary> Placed at `crates/component/src/command/perf_bench.rs` with `#[cfg(test)] mod perf_bench;` in `command/mod.rs`, identical in both trees. Run with `<test-binary> command::perf_bench --ignored --nocapture --test-threads=1`. ```rust //! Benchmark harness for PR longbridge#3268 (not committed). //! //! A host view renders a `Command` palette from a freshly built, unchanged //! model on every frame, the way an application re-renders its palette. //! Before the fix every re-render re-measured all rows (layout of every row, //! not only the visible ones); after it the measured row sizes are kept. //! //! Run: <test-binary> command::perf_bench --ignored --nocapture --test-threads=1 use std::time::Instant; use gpui::{ AppContext as _, Entity, IntoElement, ParentElement as _, Render, Styled as _, TestAppContext, Window, actions, div, }; use crate::{ Icon, IconName, command::{Command, CommandGroup, CommandItem, CommandState}, }; actions!(command_perf_bench, [BenchAction]); const GROUP_SIZE: usize = 50; const FRAMES_PER_RUN: usize = 20; const WARMUP_RUNS: usize = 2; const RUNS: usize = 9; struct PaletteHost { state: Entity<CommandState>, items: usize, } impl Render for PaletteHost { fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement { let mut command = Command::new(&self.state); for group_ix in 0..self.items.div_ceil(GROUP_SIZE) { let start = group_ix * GROUP_SIZE; let end = (start + GROUP_SIZE).min(self.items); let group = CommandGroup::new() .label(format!("Group {group_ix}")) .items((start..end).map(|ix| { let item = CommandItem::new() .label(format!("Command number {ix} with a descriptive label")) .keywords([format!("kw{ix}"), "bench".to_string()]) .action(Box::new(BenchAction)); if ix % 3 == 0 { item.icon(Icon::new(IconName::Check)) } else { item } })); command = command.group(group); } div().size_full().child(command) } } fn median(mut samples: Vec<f64>) -> f64 { samples.sort_by(|a, b| a.partial_cmp(b).unwrap()); samples[samples.len() / 2] } fn bench_rerender(cx: &mut TestAppContext, items: usize) { cx.update(crate::init); let (host, cx) = cx.add_window_view(|window, cx| PaletteHost { state: cx.new(|cx| CommandState::new(window, cx)), items, }); cx.run_until_parked(); cx.update(|window, cx| _ = window.draw(cx)); let mut per_frame_us = Vec::new(); for run in 0..WARMUP_RUNS + RUNS { let started = Instant::now(); for _ in 0..FRAMES_PER_RUN { cx.update(|window, cx| { host.update(cx, |_, cx| cx.notify()); _ = window.draw(cx); }); } let elapsed = started.elapsed().as_secs_f64() * 1e6 / FRAMES_PER_RUN as f64; if run >= WARMUP_RUNS { per_frame_us.push(elapsed); } } println!( "BENCH command_rerender_unchanged_{items} median_us={:.1} runs={RUNS} frames_per_run={FRAMES_PER_RUN}", median(per_frame_us) ); } #[gpui::test] #[ignore] fn bench_command_rerender_unchanged_500(cx: &mut TestAppContext) { bench_rerender(cx, 500); } #[gpui::test] #[ignore] fn bench_command_rerender_unchanged_2000(cx: &mut TestAppContext) { bench_rerender(cx, 2000); } ``` </details> ## How to Test Run locally on this branch (stable 1.98.1, the same toolchain CI uses): - `cargo fmt --check`: the PR's `state.rs` needed formatting; fixed in c43c340 ("command: Format measured row changes with rustfmt"), no logic changes. It still reports `crates/component/src/form/tests.rs`, which already fails on main and is not part of this PR. - `cargo clippy -p gpui-component --all-targets -- --deny warnings`: clean. - `cargo test -p gpui-component --lib`: 608 passed, 0 failed. - `cargo test -p gpui-component --lib command::`: 35 passed, including `reinstalling_an_unchanged_model_keeps_the_measured_rows`. - gpui-base is not touched by this PR. CI: all 11 checks pass (Checks, Lint, No default features, Test on macOS, Linux and Windows, and the GPUI Shell jobs). ```sh cargo clippy -p gpui-component --all-targets -- --deny warnings cargo fmt --check cargo test -p gpui-component --lib command:: ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
## Description
A performance audit of gpui-base and gpui-component flagged this issue.
An independent reviewer checked the finding against the code before it
was fixed.
- `crates/component/src/menu/dropdown_menu.rs`: the `DismissEvent`
subscription now captures `WeakEntity` handles for `PopoverState`
(`cx.entity().downgrade()`) and `DropdownMenuState`
(`menu_state.downgrade()`) instead of strong clones. This breaks the
`menu_state -> menu -> listener -> menu_state` cycle, which leaked
`PopupMenu` and `DropdownMenuState` when a trigger stopped rendering
with its menu open. The change mirrors the existing weak-handle pattern
in `context_menu.rs`. `.detach()` is kept; the listener is removed once
the menu entity is released.
- `crates/base/src/popover.rs`: `PopoverState::transition_to` stored its
dismiss subscription on the `PopoverState` it captured strongly, so an
open popover also kept itself alive once its trigger was gone, together
with its deferred-popover registration.
`GlobalState::is_in_deferred_context` then stayed `true` for the rest of
the session, so every later input right-click menu stepped aside as if a
popup were still open. The subscription now holds the state weakly. This
is a gpui-base change; the other `dismiss` call in `popover.rs` is a
per-frame `on_mouse_down_out` listener, not stored on the state, and is
unchanged.
- Test `open_without_dismiss_releases_the_menu` in the
`dropdown_menu.rs` tests: it opens the dropdown, removes the window
without dismissing the menu, then asserts that the `PopupMenu` was
released (which also shows its owning `DropdownMenuState` was released)
and that `GlobalState::is_in_deferred_context` is `false`, so the
popover's registration was released too. On main the menu assertion
fails; with only the dropdown change, the registration assertion still
fails.
A follow-up review found that `PopoverState` still leaked through its
own dismiss subscription in gpui-base, contrary to an earlier version of
this description; that is fixed in this branch.
## Performance
| case | before | after | change |
| --- | ---: | ---: | ---: |
| `dropdown_open_close_batch50`, median time per batch of 50 cycles |
22.34 ms | 21.51 ms | -3.7% (within noise) |
| `dropdown_leaked_popup_menus`, `PopupMenu` entities still alive after
all windows are closed | 355 | 0 | -355 (all released) |
| `dropdown_leaked_popup_menus`, `PopupMenu` entities opened (control) |
355 | 355 | 0 |
Scale: each cycle opens a window containing a `Button::dropdown_menu`
with 20 items, draws it, clicks the trigger so the menu opens and stays
open, then removes the window without dismissing the menu. A run is 5
warm-up cycles followed by 7 timed batches of 50 cycles, so 355 menus
are opened in total.
Release profile, pinned to one performance core (`taskset -c 8`) on an
Intel Core i7-13700KF, Linux 7.2.5, rustc 1.98.1. Each run is gated on a
quiet machine (1-minute load < 1.5, no rustc or cargo running). Base
(merge-base with main) and branch are alternated for 5 rounds; each
round takes the median of its timed runs after warm-up, and the table
shows the median across rounds. Round-to-round spread was under 2.5%
(2.2% base, 2.1% branch).
This is a leak fix, and the entity count is the result that matters. On
base, every dropdown menu that is open when its window or trigger goes
away stays alive forever (355 of 355). With this change, all of them are
released (0 of 355). A `PopupMenu` can only stay alive through the
`DropdownMenuState` that owns it, so this count also shows that
`DropdownMenuState` is released. The wall-clock difference in this
harness (about 17 µs per cycle, -3.7%) is within noise. Do not read it
as a speedup. What the fix improves is memory and behavior: leaked
menus, their states and popovers no longer build up over a long session,
and a leaked deferred-popover registration no longer suppresses later
right-click menus. The benchmark counts `PopupMenu` only and predates
the `popover.rs` change.
<details><summary>Benchmark harness (not part of this diff)</summary>
Appended to `crates/component/src/menu/dropdown_menu.rs` in both trees,
identical in each, and run with `leak_bench --ignored --nocapture
--test-threads=1`.
```rust
#[cfg(test)]
mod leak_bench {
//! Benchmark harness (not committed): opens a dropdown menu, leaves it
//! open, closes the window, and counts how many `PopupMenu` entities are
//! still alive afterwards. Also times each batch of open/close cycles.
use super::*;
use gpui::{
MouseButton, ParentElement as _, Render, TestAppContext, WeakEntity, actions, div, point,
px,
};
use std::cell::RefCell;
use std::time::Instant;
actions!(dropdown_menu_bench, [BenchCopy]);
struct BenchRoot {
menus: Rc<RefCell<Vec<WeakEntity<PopupMenu>>>>,
}
impl Render for BenchRoot {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let menus = self.menus.clone();
div().size_full().child(
Button::new("trigger")
.label("Edit")
.w(px(100.))
.h(px(30.))
.dropdown_menu(move |mut menu, _, cx| {
menus.borrow_mut().push(cx.weak_entity());
for i in 0..20 {
menu = menu.menu(format!("Item {i}"), Box::new(BenchCopy));
}
menu
}),
)
}
}
fn cycle(cx: &mut TestAppContext, menus: &Rc<RefCell<Vec<WeakEntity<PopupMenu>>>>) {
let (_, cx) = cx.add_window_view(|_, _| BenchRoot {
menus: menus.clone(),
});
cx.update(|window, cx| window.draw(cx).clear(cx));
cx.simulate_mouse_down(point(px(10.), px(10.)), MouseButton::Left, Default::default());
cx.run_until_parked();
cx.update(|window, _| window.remove_window());
cx.run_until_parked();
}
#[gpui::test]
#[ignore]
fn bench_dropdown_menu_open_close_leak(cx: &mut TestAppContext) {
cx.update(|cx| crate::init(cx));
let batch: usize = std::env::var("BENCH_BATCH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(50);
let runs: usize = std::env::var("BENCH_RUNS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(7);
let menus = Rc::new(RefCell::new(Vec::new()));
// Warm-up.
for _ in 0..batch.min(5) {
cycle(cx, &menus);
}
let mut times = Vec::with_capacity(runs);
for _ in 0..runs {
let start = Instant::now();
for _ in 0..batch {
cycle(cx, &menus);
}
times.push(start.elapsed().as_micros() as u64);
}
cx.run_until_parked();
times.sort_unstable();
let median = times[times.len() / 2];
let opened = menus.borrow().len();
let alive = menus.borrow().iter().filter(|m| m.upgrade().is_some()).count();
println!(
"BENCH dropdown_open_close_batch{batch} median_us={median} runs={runs} min_us={} max_us={}",
times[0],
times[times.len() - 1]
);
println!("BENCH dropdown_leaked_popup_menus live={alive} opened={opened}");
}
}
```
</details>
## How to Test
Run locally on this branch:
- `cargo clippy -p gpui-base -p gpui-component --tests -- --deny
warnings`: no warnings.
- `cargo test -p gpui-base --lib`: 1152 passed. `cargo test -p
gpui-component --lib menu::`: 13 passed, including
`open_without_dismiss_releases_the_menu`.
- `cargo test -p gpui-kit --features test-support,assets --test menu
--test overlays --test interactions`: all passed.
- rustfmt is clean for the changed files.
Manually, in `cargo run -- Menu`: open a dropdown, then check it still
closes on item click, Escape and a click outside, and that focus returns
to the trigger.
```sh
cargo clippy -p gpui-base -p gpui-component --tests -- --deny warnings
cargo test -p gpui-base --lib popover
cargo test -p gpui-component --lib menu
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
## Description Markdown source mappings are built eagerly during parsing. For a newline-free paragraph such as `"a ".repeat(n)`, each rendered space searches the remaining source for a newline before matching the literal space. Code newlines repeatedly count the remaining line endings, and unmapped characters can repeatedly search an unchanged suffix. This makes source alignment quadratic before the document can finish parsing. Cache horizontal-whitespace boundaries and the final LF boundary, merge source segments as they are emitted, and lazily index the remaining source after the first failed character search. Cache entity decoding while the source cursor is unchanged. Preserve soft-break precedence, CRLF/entity/escape mappings, final-newline handling, and alignment after unmapped characters. The diff is confined to `crates/base/src/text/format/markdown.rs`, including five regression tests. ## Performance Measured with the old parser extracted from the exact baseline source and the current production parser in the same `gpui-base` test binary. These timings include complete Markdown parsing and document construction, with no selection call. | Input | Complete parse before | Complete parse after | | --- | ---: | ---: | | 64 KiB repeated `a ` paragraph | 814.399 ms | 7.398 ms | | 128 KiB repeated `a ` paragraph | 3,248.564 ms | 16.151 ms | | 256 KiB repeated `a ` paragraph | 13,062.822 ms | 32.487 ms | | 256 KiB multiline code body | 7,622.079 ms | 2,640.968 ms | Alignment alone for the 256 KiB paragraph falls from 12,984.815 ms to 2.206 ms; for the code body, it falls from 5,129.759 ms to 1.988 ms. Complete multiline-code parsing retains other superlinear costs outside this change. For a 96 KiB contiguous paragraph, the returned segment vector has one element in both versions, while capacity falls from 98,303 to 4. Linux x86_64 / Ryzen 9 7950X / Rust 1.98.0. Only gpui-base uses `opt-level=3`; dependencies use the repository dev profile. Each operation has one warm-up and three measured calls; results use independent medians. Input construction, result verification and returned-result destruction are excluded. These are parser measurements, not GUI frame latency or a full release build. [Source snapshots, raw logs, samples, reproduction instructions, execution history and checksums](https://github.com/lurenjia534/gpui-component/blob/a3b1967e9a4fa11d9bf641699338d4cd735cb407/README.md) are preserved at an immutable commit on the fork's `evidence/markdown-source-mapping-20260927` branch. The PR diff contains only the implementation and regression tests. ## How to Test ```sh cargo test --locked -p gpui-base --lib text::format::markdown::tests cargo test --locked -p gpui-base --lib cargo fmt -p gpui-base -- --check cargo clippy --locked -p gpui-base --all-targets -- -D warnings git diff --check ``` The Markdown suite reports **57 passed, 0 failed**; the final Base library suite reports **1,157 passed, 0 failed**. Base formatting, Clippy and the code diff check pass. Full-workspace formatting has pre-existing differences in the unchanged `crates/component/src/form/tests.rs`, recorded in the evidence. The five new tests cover long whitespace runs and compact capacity, multiline code and final LF absorption, soft breaks and atomic entities, resumed alignment after gaps, and eager parsing of long text/code. Temporary validation also passed 2,018,720 real Rust old/new mapping comparisons and 23 performance cases. A headless GPUI test exercised public `TextViewState::markdown`, background completion and `set_text` replacement for approximately 96 KiB inputs without selection. That instrumentation is archived and removed from the fix. Native-window rendering, real executor starvation, and macOS/Windows validation were not performed. ## AI Assistance AI assisted with implementation, review, regression validation, benchmarking and PR preparation. ## Checklist - [x] I have read the contributing guide and kept this PR focused on one problem. - [x] Reviewed the implementation and AI-assisted code against the test evidence. - [ ] Passed `cargo run` for related Story tests (parser/state validation completed; no Story run). - [ ] Tested macOS, Windows and Linux performance (Linux parser measurements completed; no platform-specific code changes). Co-authored-by: Jason Lee <[email protected]>
…idge#3275) ## Description `InputState::context_menu(false)` was documented as ignored when the element defines a custom menu (`Input::context_menu`, and the `Textarea` and `Editor` builders that forward to it). Since the base split (longbridge#2677), it turns off the right-click menu entirely, custom menu included: the check lives in gpui-base, and the custom builder now lives on the styled element. This PR documents the shipped behavior rather than restoring the old one. Restoring it would need a new public API on the gpui-base seam for the element to report that it has a custom menu. That is not worth it, because the combination is never needed: a custom menu already replaces the built-in one, so to show only your own items you keep the menu enabled (the default) and set a builder. - `InputBaseState::context_menu`: the doc now says that disabling it turns off the whole right-click menu, custom menu included. - `Input::context_menu`, `Textarea::context_menu`, `Editor::context_menu`: the docs note that the custom menu shows only while the state's context menu is enabled. - `website/component/input.md`: the Context Menu example comments say the same. - `website/zh-CN/component/input.md`: adds the Context Menu section, which the Chinese page was missing. An earlier version of this PR restored the old behavior through a new `#[doc(hidden)]` `set_custom_context_menu`. That approach was dropped to avoid adding API. ## How to Test Documentation only; no behavior change. `rustfmt --check` and `typos` are clean for the changed files. ## 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) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Closes longbridge#3187 ## Description On `wasm32-unknown-unknown`, right-clicking any Input showed Paste greyed out no matter what was on the clipboard, while Cut / Copy / Select All and Ctrl/Cmd+V worked. Two places depended on the synchronous clipboard read, which the web platform returns `None` for by design (`gpui-pre-web`'s `read_from_clipboard`; the real read is `read_from_clipboard_async`): - The Input context menu enabled Paste only when `cx.read_from_clipboard().is_some()`, so it was permanently disabled on the web. - `InputBaseState::paste` bailed out on the same read, so un-greying the item alone would have left a clickable Paste that inserts nothing. (Keyboard paste works on the web because the DOM `paste` event reaches `EntityInputHandler::paste` directly and never goes through the `Paste` action.) This PR fixes both: - `crates/component/src/input/input.rs`: the context menu offers Paste whenever the input is editable, without peeking at the clipboard. This matches the touch-selection edit menu's existing rationale: an empty clipboard pastes nothing, and reading the clipboard just to decide enablement is a side effect on some platforms (the iOS paste banner). The `on_paste` doc comment now describes the web behaviour accurately. - `crates/base/src/input/base/state.rs`: `paste` tries the synchronous read first and, when it is `None`, falls back to `cx.read_from_clipboard_async()`. The async read is started synchronously inside the action handler so it stays under the user activation (the menu click) the browser requires, then the result is inserted through the same path via `cx.spawn_in`. The input's editability is re-checked when the read resolves, and `ClipboardReadError` is logged. The existing insert logic (atomic undo intent, newline stripping on single-line inputs, one-line-per-selection distribution) moved untouched into a private `insert_clipboard` helper. Desktop behaviour is unchanged: the synchronous read returns `Some` there, so the async branch is never reached. As a side effect the touch-selection menu's Paste, which dispatches the same action, now also works on the web instead of being a silent no-op. Out of scope, as noted in the issue: the `on_paste` hook still only sees the synchronous read, so pasting images from the context menu on the web is not supported yet; the input falls back to inserting the plain text itself. The code in this PR was AI-generated (Claude) and reviewed by hand. ## Screenshot No visual change on desktop. On the web backend the only change is that the Input context menu's Paste item is enabled instead of greyed out; I could not capture a wasm build for a screenshot. ## How to Test - `cargo clippy -p gpui-base -p gpui-component --all-targets -- --deny warnings` - `cargo test -p gpui-base --lib -- paste clipboard` (4 tests, all pass, covering atomic undo, multi-cursor distribution and the masked-input clipboard guard) - `cargo test -p gpui-component --lib -- paste context_menu` (7 tests, all pass) - Manual, desktop: `cargo run`, open an Input story, right-click an editable input → Paste is enabled and inserts the clipboard text; right-click a read-only or disabled input → Paste is disabled. - Manual, web: build `crates/story-web`, copy some text, right-click an Input → Paste is enabled; choosing it prompts for clipboard permission (first time) and inserts the text. The async branch has no automated test because the test platform always answers the synchronous read, so it needs the manual web check 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: Cursor <[email protected]>
…bridge#3277) ## Description The `gpui-base` Dialog host is a full-viewport `div` with no layout of its own, so a popup in normal flow lands in the top-left corner unless the caller centers it. `Dialog` could be centered by styling the host, but `AlertDialog` does not implement `Styled`, so an alert dialog could not be centered at all without wrapping the popup in a full-size flex container. Downstream libraries hit this (a gpui-omarchy alert dialog rendered in the top-left corner). This PR: - Centers the popup on both axes by default: the host is now `flex().items_center().justify_center()`. The caller's style is still applied last, so a different layout (for example `.items_start()` for a top-aligned dialog) remains possible. - Implements `Styled` for `AlertDialog`, forwarding to its inner `Dialog`, so alert dialogs can be laid out the same way. - Makes `DialogPopup` and `AlertDialogPopup` occlude. The backdrop sits under the popup and dismisses on press, and the popup did not block the mouse, so any press inside the popup fell through and closed the dialog. This was hidden in the showcase because its popup covered the whole window; it surfaced once the popup was centered at its own size. - Removes the centering wrappers from the `dialog` and `alert-dialog` showcase examples, which now use the popup part itself as the surface. `gpui-component`'s Dialog is unaffected: it positions its surface with `Positioner::corner`, which is absolutely positioned and offset from its own layout bounds, so the host's layout does not move it. ## Public API ### gpui-base - `impl Styled for gpui_base::AlertDialog` — lays out the alert dialog host, as `Dialog` already allows. ## Breaking Changes Behavioral only; no signatures change. - A popup in normal flow is now centered instead of placed at the top-left. Callers that relied on the top-left placement can restore it by styling the host: ```diff - Dialog::new(cx).popup(popup) + Dialog::new(cx).items_start().justify_start().popup(popup) ``` - `DialogPopup` and `AlertDialogPopup` now block presses from reaching the backdrop. A popup styled to cover the whole window (such as the old showcase's `absolute().inset_0()` wrapper) no longer dismisses on a press outside its visible content; size the popup to its content instead. ## How to Test ```bash cargo run -p gpui-base-examples -- dialog cargo run -p gpui-base-examples -- alert-dialog ``` Open each dialog: it is centered, and pressing inside it (title, description, input) keeps it open. For `dialog`, pressing the dimmed backdrop closes it. Results: - `cargo test -p gpui-base --lib`: 1169 passed, including the new `the_popup_is_centered_by_default`, `pressing_the_popup_does_not_dismiss` and `host_style_positions_the_popup`. `close_trigger_activates_once_and_respects_cancel_veto` now presses the popup at the viewport center, since the popup is no longer at the top-left. - `cargo test -p gpui-component dialog`: 12 passed. - `cargo clippy -p gpui-base -p gpui-base-examples --tests -- --deny warnings`: clean. - `cargo fmt`: applied. - Manually checked both examples on Linux (Hyprland). ## 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) This PR was written with AI assistance (Claude Code) and reviewed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…3278) ## Description `InputBase` is an unstyled frame shared by single-line inputs, textareas and editors, and it has no layout of its own. A single-line input therefore sat at the top of any frame taller than one line unless the caller remembered to add `.flex().items_center()` to the frame. The showcase added that to some inputs and not others (the combobox search and the dialog input sat at the top). The frame cannot center by default, because it does not know whether it holds a single line or a textarea/editor, which must start at the top. The input state does know, so: - The input state's root now always fills the frame height (multi-line already did), and in single-line mode it is `flex().items_center()`, so the line sits at the vertical center of whatever frame holds it. Multi-line layout is unchanged. - The manual centering on the input frames in the `input`, `toolbar` and `color-picker` showcase examples is removed. Two showcase fixes found while checking the examples: - The toolbar search input was missing from `refresh_editor_styles`, so after the window appearance changed it kept the previous palette's caret color (a near-black caret on a dark background on Linux, where the window reports light first and then dark). - The select, combobox, date picker and color picker drew their chevrons with the `⌄` / `⌃` glyphs, which sit at the baseline and cap height and so look off center even in a centered row. They now use a small `chevron()` helper that draws the chevron as a path in the current text color. - The combobox popup left its search input at the default text size, larger than the trigger, and framed it with a second border inside the popup's own. It now uses the trigger's `text_xs()`, and the search input is separated from the options by a bottom rule. `gpui-component`'s Input is unaffected in practice: its frame already centers the state, and a centered full-height child renders the line in the same place. ## Public API None. ## Breaking Changes Behavioral only; no signatures change. - A single-line input's element now fills its frame's height and centers its line, instead of taking one line's height at the top of the frame. A frame that relied on the top placement (for example, a tall frame with the line meant to sit at the top) would now show the line centered; give the frame the height of one line to keep the previous look. ## How to Test ```bash cargo run -p gpui-base-examples -- input cargo run -p gpui-base-examples -- combobox cargo run -p gpui-base-examples -- toolbar cargo run -p gpui-base-examples -- select cargo run -p gpui-base-examples -- date-picker cargo run -p gpui-base-examples -- color-picker cargo run -p gpui-base-examples -- textarea ``` Single-line inputs show their text vertically centered with no frame layout; the textarea still starts at the top; the chevrons sit at the center of their rows; the toolbar search caret matches the text color in dark mode. Results: - `cargo test -p gpui-base --lib`: 1167 passed, including the new `single_line_is_centered_in_a_taller_frame`, which fails without the change (the line's center is at 13px instead of 30px in a 60px frame). - `long_press_on_empty_input_places_caret_with_menu` pressed at a hard-coded `(20, 10)`, which assumed the line sits at the top of the window; it now presses at `caret_at(0)` like the other touch tests. - `cargo test -p gpui-component --lib`: 611 passed. - `cargo test -p gpui-base-examples`: passed. - `cargo clippy -p gpui-base -p gpui-base-examples --tests -- --deny warnings`: clean. - `cargo fmt --check`: clean. - [x] Checked the combobox example on Linux (Hyprland): the search text is centered and the popup has a single border. - [ ] Check the single-line centering in the input, toolbar and color picker examples, and that the textarea still starts at the top. - [ ] Check the chevrons (select, combobox, date picker, color picker) and the toolbar search caret in dark mode. ## 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) This PR was written with AI assistance (Claude Code) and reviewed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
## Description This is a second performance audit of every gpui-component component; the first round was longbridge#3260–longbridge#3270. Each finding was checked against the code by an independent reviewer before it was fixed, and each fix was reviewed again afterwards. There is one commit per component. gpui-base is not touched, and the public API is unchanged. - **pagination** (`crates/component/src/pagination.rs`): the ellipsis dropdown built one menu item per hidden page, and PopupMenu renders every item on every frame while it is open. The menu now lists at most 100 hidden pages, the ones nearest the current page. - **context menu** (`crates/component/src/menu/context_menu.rs`): triggers without an `ElementId` share one context menu element state. Rows rendered from one call site, such as every `Tree` row, therefore each built, rendered and laid out the open menu layer, and only the pressed trigger painted it. `DeferredMenu` now carries only the menu entity, position and anchor, and builds and prepaints the menu only in the trigger that draws it. Focusing the menu now also happens once. - **color_picker** (`crates/component/src/color_picker.rs`): a closed picker still built its whole popover body on every render, either about 111 palette swatches or 192 slider-track divs plus 4 sliders, and the popover then threw it away. The body is now built only while the picker is open. - **accordion** (`crates/component/src/accordion.rs`): every item kept its panel mounted, so `MotionReveal` laid out, prepainted and painted the full content of each closed item on every frame, only to clip it to zero height. The panel now unmounts once its spring settles closed. Under reduced motion the panel stays mounted as before, because MotionReveal learns the content height only from a frame in which the content was mounted. - **notification** (`crates/component/src/notification.rs`): the lifecycle loop ticked every 50 ms while any notification was mounted, so one `autohide(false)` toast woke the window at 20 Hz for as long as it stayed open. The clock now runs only while an entry is entering, exiting or counting down, and it restarts from `close`, `close_by_type`, `clear` and a dismiss request. - **searchable_list** (`crates/component/src/searchable_list/vec.rs`): `SearchableVec` cloned every matching item on each search, and the whole list on construction. The grouped variant cloned whole groups and called `matches()` twice for some rows. The filtered view is now a list of indices, and `matches()` runs once per item. ### Behavior changes - Pagination: when an ellipsis hides more than 100 pages, its menu lists the 100 pages nearest the current page. To reach a farther page, pick the farthest listed page, then open the next ellipsis. Gaps of 100 pages or fewer are unchanged. - Accordion: element or keyed state inside a closed item's content is dropped once the close animation settles, the same as `keep_mounted(false)`. State held in entities is unaffected. - Context menu: the menu is focused during prepaint instead of request_layout. It still draws focused on its first frame. ## Performance | case | before | after | | --- | --- | --- | | Pagination, 10,000 pages, right ellipsis menu open (release) | 9,996 menu items, ~254 ms/frame | 100 items, ~2 ms/frame | | Context menu, 50 rows sharing state, 20-item menu, 100 redraws (debug) | 50 `PopupMenu::render` calls/frame, 2.01 s | 1 call/frame, 0.13 s | | ColorPicker, 100 closed pickers, 50 redraws (release) | 555,000 swatches built, ~14.8 ms/frame | 0 swatches built, ~1.6 ms/frame | | Accordion, 200 items × 20 lines, 1 open, 20 redraws (release) | 4,000 content prepaints/frame, ~20 ms/frame | 20 prepaints/frame, ~6 ms/frame | | Notification, 1 persistent toast, 60 s test clock | 1,200 lifecycle ticks | 8 ticks (entry transition only) | | SearchableVec, 10k items, 11 incremental queries (release) | 82,333 item clones, ~3.0 ms | 0 clones, ~2.4 ms | Every row comes from a throwaway `#[gpui::test]` that counts renders, prepaints, ticks or clones with temporary counters and times the loop with `Instant`. None of this harness is part of the diff. The counts are deterministic, and the times are best-of or median across repeated runs. ## How to Test Checked locally on this branch: - `cargo clippy -p gpui-component --all-targets -- --deny warnings`: clean. - `cargo test -p gpui-component --lib`: 616 passed, 0 failed. This includes the new tests for the notification clock, accordion unmounting, the ellipsis page window and the SearchableVec index mapping. - `cargo test -p gpui-kit --features "test-support component" --test disclosure --test collections --test components --test controls`: all passed. To check by hand in the story gallery: - Pagination with many pages: open an ellipsis menu. - Tree: right-click a row, hover the menu items and click one. - ColorPicker: open, pick a color and close. - Accordion: expand and collapse, with and without reduced motion. - Notification: push a persistent notification with an action, then close it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…ge#3280) Closes longbridge#3276 ## Summary Long option labels in the Select dropdown overflowed the fixed-width menu instead of ending in an ellipsis, because the label container in a flex row could not shrink below its content width. - `SearchableListItemElement`: the label container is now `flex_1().min_w_0().overflow_hidden()`, and the check icon is `flex_shrink_0()` so it stays visible and aligned. - `SearchableListAdapter`: the default row content uses `truncate()` instead of `whitespace_nowrap()`, so long labels end in an ellipsis on a single line. Labels that fit are unchanged. No public API change; this applies to every dropdown built on the searchable list. Delegates that render a whole row via `render_item` get a shrinkable container, but their own text needs `truncate()` to show an ellipsis. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
## Summary
This PR moves the chart primitives from `gpui-component` into
`gpui-base` as `gpui_base::plot`:
- Scales (`ScaleLinear`, `ScaleBand`, `ScalePoint`, `ScaleOrdinal`),
shapes (`Bar`, `Line`, `Area`, `Arc`, `Pie`, `Stack`, `RadialLine`,
Sankey layout), `PlotAxis`, `Grid`, `PlotLabel`, `PathCaches`, and the
`Plot` trait.
- The element behind every plot, previously generated by
`#[derive(IntoPlot)]`, is now written once as
`gpui_base::plot::PlotElement<P>`.
- Hover tracking (`TooltipState`, `PlotHover`, the focus fade and
pointer glide) moves with it. Its timing comes from a new `PlotMotion`
on `gpui_base::Theme::plot`, which is motionless by default like
`ScrollbarMotion`.
`gpui-component` keeps the presentation: `plot::tooltip::{Tooltip,
CrossLine, Dot}`, the charts, and `#[derive(IntoPlot)]`. It re-exports
`gpui_base::plot` unchanged, and its theme projects its motion tokens
onto `gpui_base::Theme::plot`. Existing imports and chart behavior are
unchanged.
## Motivation
Plotting geometry and hover behavior are not design decisions; only
colors, the tooltip box and timing are. With them inside
`gpui-component`, a design system built on `gpui-base` alone — such as
[gpui-omarchy](https://github.com/huacnlee/gpui-omarchy), which
deliberately does not depend on the component facade — has to copy about
5,000 lines of scales, shapes and hover code to offer charts. After this
change it depends on `gpui_kit::base::plot`, projects its own
`PlotMotion`, and writes only its styled charts and tooltip.
## API review for 0.7.0
Moving plot into gpui-base puts it under the Base seam rules, so this PR
also tidies the API before 0.7.0:
- **No presentation in Base.** `ScaleBand` no longer caps bands at 30px
(the cap moves to `BarChart`/`CandlestickChart::max_band_width`, 30px by
default), and the x-axis gutter is `axis_gutter(font_size)` instead of a
fixed `AXIS_GAP`.
- **Order-independent builders.** `PlotAxis` labels were placed when
`x_label` was called, so calling it before `x`/`x_label_side` dropped or
misplaced them; labels are now placed at paint time.
- **Vocabulary.** Hover fade is `progress`, not `focus` (focus means
keyboard focus); `StrokeStyle` (a curve, not a dash style) is `Curve`.
- **Evolvable records.** Types with public fields are
`#[non_exhaustive]`.
- **A named value bound.** `PlotValue` (`f32`, `f64`, `Decimal`)
replaces the hidden `Sealed`, so charts now accept `f32`.
- **Consistent shapes.** `stroke`/`fill`/`dot_fill` take `impl
Into<Background>`; `Arc` paints without per-call radius overrides.
`TooltipState` keeps its name, fields and `Plot::tooltip_state`.
## Public API
### gpui-base
- `pub mod plot` — the primitives moved from `gpui_component::plot`.
- `plot::PlotElement<P: Plot + 'static>`, `PlotElement::new(plot: P) ->
Self` — the element a `Plot` renders through.
- `plot::hover_progress(window: &mut Window, cx: &mut App) -> f32` — how
far the enclosing plot's hover has faded in (`1` outside a plot).
- `plot::is_hover_entering(window: &mut Window, cx: &mut App) -> bool` —
first hovered frame (`false` outside a plot).
- `plot::pointer_spring(cx: &App) -> Spring` — the spring a crosshair,
band or dot follows the hovered datum with.
- `PlotHover::progress(&self) -> f32` — hover fade from 0 to 1.
- `PlotMotion` — `with_pointer(Spring)`, `with_enter(Transition)`,
`with_exit(Transition)`, `pointer()`, `enter()`, `exit()`; motionless by
default.
- `PlotTheme` — `new()`, `with_motion(PlotMotion)`, `motion()`;
`gpui_base::Theme::plot: PlotTheme`.
- `plot::PlotValue` — sealed value trait for scales and charts: `f32`,
`f64`, `rust_decimal::Decimal` (feature `decimal`).
- `ScaleLinear::new(domain: impl IntoIterator<Item = T>, range: [f32;
2])`, same shape for `ScalePoint::new` and `ScaleBand::new`.
- `Scale::nearest_index(&self, tick: f32) -> usize`.
- `ScaleBand::max_band_width(self, width: f32) -> Self` (unset by
default); `ScaleBand::band_count(self, count: usize) -> Self` is now
public.
- `plot::axis_gutter(font_size: Pixels) -> f32` — the height an x-axis
with labels of that size needs (18 at the default 10px).
- `PlotAxis::stroke` and `Grid::stroke` take `impl Into<Background>`;
`Grid::x`/`y` take `impl IntoIterator<Item = impl Into<Pixels>>`; `Grid:
Default`.
- `plot::Curve { Natural, Linear, StepAfter }`; `Line::curve(Curve)`,
`Area::curve(Curve)`.
- `dot_fill(impl Into<Background>)`, `dot_stroke(impl Into<Hsla>)`.
- `Arc::paint(arc, fill: impl Into<Background>, bounds, window)`,
`Arc::paint_cached(arc, fill, bounds, cache, window)`,
`Arc::contains(arc, position, bounds)`.
- `ArcData::new(data, index, value, start_angle, end_angle) -> Self`.
- `decimal` feature.
### gpui-component
- `plot` re-exports `gpui_base::plot::*`; `plot::tooltip` keeps
`Tooltip`, `CrossLine`, `CrossLineAxis`, `Dot` and re-exports
`TooltipState`, `PlotHover`.
- `Tooltip::progress(self, progress: f32) -> Self` — override the
overlay fade.
- `BarChart::max_band_width(self, width: impl Into<Pixels>) -> Self`,
`CandlestickChart::max_band_width(...)` — default 30px.
- Charts accept `f32` values (bound is `PlotValue`).
- `decimal` forwards to `gpui-base/decimal`; the theme projects its
motion tokens onto `gpui_base::Theme::plot`.
### gpui-component-macros
- `#[derive(IntoPlot)]` generates `impl IntoElement for T { type Element
= PlotElement<T>; … }`.
## Breaking Changes
Charts (`LineChart`, `BarChart`, …) keep their API. These affect custom
plots built on the primitives.
### `StrokeStyle` is renamed `Curve`
`StrokeStyle` chose how a `Line` or `Area` connects its points — a curve
interpolation (d3's `curveNatural` / `curveLinear` / `curveStepAfter`),
not a dash or solid stroke style, which is what "stroke style" usually
means. It is now `plot::Curve` with the same variants, and the shape
builder `stroke_style(..)` is `curve(..)`. `StrokeStyle` and
`stroke_style` are removed without deprecated aliases.
```diff
- use gpui_kit::base::plot::StrokeStyle;
+ use gpui_kit::base::plot::Curve;
- Line::new().stroke_style(StrokeStyle::StepAfter)
+ Line::new().curve(Curve::StepAfter)
- Area::new().stroke_style(StrokeStyle::Linear)
+ Area::new().curve(Curve::Linear)
```
`LineChart`/`AreaChart`'s `.natural()`, `.linear()` and `.step_after()`
are unchanged.
### Other changes
Deprecated aliases for this release: `AXIS_GAP` (→ `axis_gutter`),
`dot_fill_color`, `dot_stroke_color`, `PlotHover::focus`,
`Tooltip::focus`.
```diff
- let y = ScaleLinear::new(values.collect(), vec![height, 0.]);
+ let y = ScaleLinear::new(values, [height, 0.]);
- where Y: Copy + PartialOrd + Num + ToPrimitive + Sealed
+ where Y: PlotValue
- let ix = scale.least_index(tick);
+ let ix = scale.nearest_index(tick);
- arc.paint(&data, color, None, Some(radius), &bounds, window);
+ Arc::new().outer_radius(radius).paint(&data, color, &bounds, window);
- Grid::new().y(ticks.collect())
+ Grid::new().y(ticks)
```
- `ScaleBand::band_width` no longer caps at 30px; set `max_band_width`
for the old width.
- `least_index_with_domain` and the hidden `Sealed` are removed.
- `TooltipState`, `AxisText`, `label::Text`, `ArcData`, `StackPoint`,
`StackSeries`, `SankeyLink`, `SankeyNodeLayout`, `SankeyLinkLayout`,
`SankeyGraph` are `#[non_exhaustive]`: build them with their
constructors.
- `PlotAxis::default()` now equals `new()` (x-axis line shown).
- `#[derive(IntoPlot)]` sets `type Element = PlotElement<Self>` instead
of implementing `Element` on the plot.
- `gpui_component::plot::tooltip::track_hover` (hidden) is removed.
- `gpui_base::Theme` gains a `plot` field:
```diff
gpui_base::Theme {
appearance,
tokens,
scrollbar,
resizable,
+ plot: gpui_base::PlotTheme::default(),
}
```
## Test Plan
- [x] `cargo build --all`
- [x] `cargo clippy -p gpui-base -p gpui-component -p
gpui-component-macros -p gpui-kit -p gpui-component-story --all-targets
-- --deny warnings`
- [x] `cargo test -p gpui-base plot` (55 passed), `cargo test -p
gpui-component --lib -- chart plot progress theme` (87 passed),
including new tests for builder-order-independent axis labels,
`max_band_width`, `f32` scales and charts, reversed ranges and band
range start
- [x] `cargo check -p gpui-component --features decimal`
- [x] `typos` on changed paths
- [x] gpui-omarchy builds its charts on `gpui_base::plot` alone (no
`gpui-component`) via a path patch to this branch (before the API
cleanup; to be re-checked)
- [ ] Story gallery: line, bar, area, pie, radar, candlestick and sankey
charts render as before; hover fades in and out and the crosshair glides
between data
- [ ] Website: `/base/plot` and `/zh-CN/base/plot` render and link from
the Base index
`cargo fmt --check` reports only `crates/component/src/form/tests.rs`,
which is already unformatted on `main` and untouched here.
The code moves and the new `PlotElement`, hover and `PlotMotion` code
were written with Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…ge#3282) ## Description Release GPUI runs Kit compatibility checks against the staged upstream snapshot, producing errors such as the Criterion version mismatch even though the GPUI packages themselves build. Pass the existing `--skip-kit-check` flag in the release workflow. This skips Kit adaptation during GPUI publication while keeping the license audit and staged-package compilation. Update only the two comments that described building Kit. The final diff changes one workflow file; no script, CI, dependency, or documentation changes. ## How to Test - `bun script/bump-gpui.ts 0.3.7 --zed target/gpui-pre/zed --dry-run --skip-kit-check`, with Zed at `1a28cff4b409169bac058bca40dfbfeb7621d19b` — passed on Linux: all 25 crates package and build, the Kit compatibility check is skipped, and nothing is uploaded. - Hash checks confirmed Kit's manifests and Cargo.lock remained unchanged. - Workflow YAML parsing, `git diff --check`, and independent review passed. ## Checklist - [x] Read CONTRIBUTING.md and followed its guidelines. - [x] Reviewed and validated the AI-assisted workflow change.
Fixes longbridge#3041 ## Problem `Popover::trigger_style` stored the `StyleRefinement` and nothing ever read it, so a caller that sized its trigger container got nothing: ```rust Popover::new("styled-popover") .trigger_style(StyleRefinement::default().w_full()) .trigger(Button::new("trigger").label("Open").w_full()) .child("Popover content") ``` The element the parent lays out, and the one the popup measures to anchor itself, is the `Popup` root that wraps the trigger. The refinement has to land there: applied to anything nested inside it, `w_full` or `flex_1` resolves against a root that only wraps its content, and the trigger cannot fill its slot. ## Change - `gpui_base::popover::Popover` implements `Styled`, forwarding the refinement to the `Popup` root. It adds no visual defaults. - `gpui_component::popover::Popover` passes `trigger_style` to that root instead of wrapping the trigger in another element, so no extra element appears in any layout. - `DropdownMenu` no longer copies the Button's own style to the trigger container. Copying it applied padding, margin, background and border twice, and copying only some fields would make it implicit which ones reach the container. The Button's style stays on the Button, matching `main`, where the copy was stored and never applied. The test `trigger_style_is_applied_to_the_trigger_container` places a 40px trigger with `trigger_style(w_full())` in a 200px row, anchored `TopRight`, and asserts the popup follows the container's right edge. It fails when the refinement is applied to an element nested inside the root. ## Public API ### gpui-base - `impl Styled for gpui_base::popover::Popover` — styles the trigger container: the element laid out in the parent and measured to anchor the popup. `gpui_component::popover::Popover::trigger_style` keeps its signature; it now takes effect. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Yi-111-a <[email protected]> Co-authored-by: Jason Lee <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
## Description Update all six direct GPUI snapshot requirements from `=0.3.6` to `=0.3.7` and regenerate `Cargo.lock` so all 25 snapshot packages resolve to 0.3.7, including the new `gpui-pre-bench-metrics`. The separately published `gpui-pre-reqwest` fork remains at `=0.12.15`. The [Release GPUI workflow](https://github.com/longbridge/gpui-kit/actions/runs/36368804996) successfully published the 0.3.7 snapshot on its third attempt. This remains a draft because `gpui-base` benchmarks depend on Criterion 0.5, while GPUI 0.3.7 uses Criterion 0.8. Their `Measurement` traits are incompatible. The remaining adaptation is to align Base's benchmark development dependency to Criterion 0.8; the application's libraries, binaries, examples and test targets compile with the new snapshot. ## Validation - Passed: `bun script/check-gpui-pin.ts` — direct snapshot requirements remain exact and aligned. - Passed: `cargo update -p gpui-pre -p gpui-pre-platform -p gpui-pre-web -p gpui-pre-macros -p gpui-pre-reqwest-client -p gpui-pre-sum-tree`. - Passed: `cargo check --workspace --lib --bins --examples --tests --locked` on Linux, after clearing stale Component proc-macro build artifacts. - Passed: `git diff --check`. - Fails: `cargo check --workspace --all-targets --locked` — Base's `text_view_scroll` benchmark has a Criterion 0.5/0.8 `Measurement` mismatch. - Test binaries were compiled but not executed, per repository/user configuration. Cross-platform and WASM compilation have not been verified locally. ## Remaining work - Align Base's Criterion development dependency with upstream and repeat compilation with benchmarks included. - Let platform CI verify macOS, Windows and WASM. ## AI assistance The manifest pin and lockfile updates and this description were prepared with Codex and reviewed before submission.
) ## Summary Bordered `Command` draws a `radius_lg` frame + `border_1`, and its list has `p_1`, but every row used `theme.radius`. At the top/bottom corners the selected row's fill was rounder than the frame (8px frame, 5px inset → should be 3px, was 6px). Row radius is now `max(outer - rem * 0.25 - border, 0)`: - bordered: `outer` = the command's own radius if set, else `radius_lg`; border 1px - borderless with its own `.rounded(..)`: `outer` = that radius, no border - borderless without a radius: unchanged (`theme.radius`) — the frame belongs to the host The inset uses `window.rem_size()` so it tracks the user font size. ## Test plan - [x] `cargo clippy -p gpui-component` - [x] `cargo test -p gpui-component --lib command` (36 passed) Co-authored-by: Claude Opus 5.5 <[email protected]>
## Description
Website installation examples and asset API links still named Kit 0.6
after the 0.7 release, and diff blocks in release notes did not
distinguish additions from deletions.
Pass `GPUI_KIT_VERSION` to each versioned website build, deriving it
from the release tag or the Kit manifest for main. Resolve
`{{gpui_kit_version}}` in the affected English and Chinese pages,
including Markdown exports, and pass the same value to the homepage
installation and clipboard snippet. Local development falls back to the
checkout's manifest. Existing release-tag snapshots retain their
original content; future tags include the variable-based pages.
Enable diff highlighting in the shared documentation and release-notes
renderer, with green additions, red deletions and blue headers in both
light and dark themes.
## How to Test
- `bun test --cwd website --timeout 60000` — 39 tests passed.
- `GPUI_KIT_VERSION=0.8.1 PUBLIC_SITE_VERSION=v0.8.1 bun run --cwd
website build` — passed; verified homepage HTML, hydration props,
Markdown exports and asset links use 0.8.1 despite the local Cargo
version being 0.7.0.
- Verified generated release-notes HTML includes diff colors for both
themes.
- `bash -n script/build-website-versions` and `git diff --check` —
passed.
## Checklist
- [x] Read CONTRIBUTING and followed repository conventions.
- [x] AI-assisted changes reviewed and verified with focused regression
tests and the website build.
- [x] English and Chinese documentation synchronized.
Native Story and platform performance checks are not applicable to this
website change.
…ongbridge#3271) ## Problem `GlobalState::last_touch` is an `Option<std::time::Instant>`, and `note_touch` calls `std::time::Instant::now()` on every touch. On `wasm32-unknown-unknown` `std::time::Instant::now()` is unimplemented and panics: ``` panicked at library/std/src/sys/pal/wasm32_unsupported_time.rs: `time` not implemented on `wasm32-unknown-unknown` ``` Every tap/drag on a gpui web build dispatches `TouchInput` through the gesture recognizer, reaches `GlobalState::note_touch`, and kills the wasm instance — followed by cascading `RefCell already borrowed` errors from the dead-instance event handlers. Native builds are unaffected, so this only shows up on the web target. Most time-keeping modules in `gpui-base` already gate the clock by target (`motion.rs`, `scroll_bounce.rs`, `toast.rs`, `text/state.rs`, `text/range_highlight.rs`, `text/stream_fade.rs`): `std::time::Instant` natively, `web_time::Instant` on wasm. `global_state.rs` was missed. ## Fix Gate the `Instant` import by `target_family` the same way those modules do: ```rust #[cfg(not(target_family = "wasm"))] use std::time::Instant; #[cfg(target_family = "wasm")] use web_time::Instant; ``` Native keeps `std::time::Instant`, so its behavior is unchanged. ## Validation - Reproduced the exact panic + `RefCell` avalanche on a real Android Firefox device running a gpui-kit dashboard (gpui-base 0.6.6). - With `web_time::Instant` on wasm the same build survives touch taps and scroll gestures without panics. - `cargo check -p gpui-base` passes on native. ## Release Notes Fix a wasm panic on any touch input caused by `GlobalState::last_touch` using `std::time::Instant`. --------- Co-authored-by: yoyo930021 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
…ongbridge#3289) In a form, `ColorPicker` renders a bare swatch. It does not share the height or frame of the inputs around it, and an empty swatch reads like an unchecked Checkbox. `ColorSelect` draws the picker like a `Select`: a framed field with a swatch of the current color, its hex value and a caret. Clicking anywhere on the field opens the same popover as `ColorPicker`. Keep `ColorPicker` for compact swatches such as toolbars. | Size | Height | Swatch | Text | |---|---|---|---| | XSmall | 20 | 12 | 12 | | Small | 24 | 14 | 14 | | Medium | 32 | 16 | 14 | | Large | 44 | 20 | 16 | Height, padding and text follow Input and Select. While no color is selected, the field shows Select's placeholder ("Please select"), which `placeholder` overrides. Also in this PR: - `ColorPicker` now applies its `Styled` refinements to its root. It implemented `Styled`, but the refinements were accepted and ignored. - The ColorPicker story has a Color Select section, and the Form story uses `ColorSelect` for its theme color field. - The English and Chinese ColorPicker docs describe `ColorSelect`. ## Public API ### gpui-component - `pub struct ColorSelect` (in `color_picker`): a color picker drawn as a framed field that opens the picker popover. - `ColorSelect::new(state: &Entity<ColorPickerState>) -> Self`: creates the field for a picker state. - `ColorSelect::featured_colors(self, colors: Vec<Hsla>) -> Self`: sets the featured colors at the top of the palette. - `ColorSelect::placeholder(self, placeholder: impl Into<SharedString>) -> Self`: sets the text shown while no color is selected. - `ColorSelect::accessibility_label(self, label: impl Into<SharedString>) -> Self`: sets the name a screen reader announces. - `impl Sizable for ColorSelect`, `impl Styled for ColorSelect`, `impl Focusable for ColorSelect`, `impl RenderOnce for ColorSelect`. ## Breaking Changes None in the API. `ColorPicker` styles such as `.w_full()` or `.mt_2()` now take effect on its root; previously they were silently ignored. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
longbridge#3293) ## Problem A paragraph that renders through the inline flow can lay out lines wider than its wrap width. The inline flow handles paragraphs with inline code, inline objects, or images. The flow wraps with GPUI's `LineWrapper`, which sums widths measured one character at a time. CoreText sets an isolated full-width `,` / `。` / `:` at half width (7px at 14px). Mid-line the same character takes its full width (14px). So each full-width punctuation mark on a line is under-counted by half an em, and the line overflows. Inside a clipping parent, the last glyph of every such line disappears. Downstream, the AI reasoning card shows `前两` / `客户` with `大` missing. Measured with `.SystemUIFont` at 14px: `行情显示 HUT 近 5 日下跌约 18%,成交放大到均` - shaped width: 306.96px - per-char sum: 299.96px The wrapper therefore accepts the line at 300px, and it paints 7px past the edge. Plain paragraphs are not affected. They go through `layout_wrapped_line` / `compute_wrap_boundaries`, which use shaped glyph positions. ## Fix In `inline_flow::line_ranges`, each hard line's spans are shaped once, in context, with the same runs and font-size segments the renderer uses. Objects and images keep their fixed widths. For every line the wrapper proposes, the painted width is `x_for_index(end) - x_for_index(start)`, plus code padding and the widths of the objects the line contains. When that width exceeds the wrap width, the line is re-wrapped with a tighter budget until it fits. The budget step doubles while the break stays put. `LineWrapper` still decides where a line may break, so break rules (word chars, CJK, closing punctuation) are unchanged. Only the widths are corrected. A single glyph wider than the line still keeps its line, as before. ## Tests New Metal rendering test `wrapped_cjk_with_inline_code_stays_within_the_wrap_width` in `crates/kit/tests/rendering.rs` (harness = false, main thread): - The paragraph mixes CJK, full-width punctuation, and inline code. - It is rendered with `text_sm` at widths 300–480, and no pixel may be painted past the wrap width. That is the painted form of "every line's shaped width ≤ wrap_width". - It fails on `main` (`lines wrapped at 300px painted 11 device columns past the wrap width`) and passes with this change. Other checks: - `cargo test -p gpui-base`: 1225 tests pass, including the existing inline-flow wrap tests (inline code near the wrap width, links, highlights). They use a monospace test text system, where per-char and shaped widths agree, so the correction never triggers there. - **Unchanged output:** rendered before and after this change, a Latin paragraph with inline code near the edge, a link, and bold text is byte-identical at all seven widths. So is a plain CJK paragraph with a link and bold text. - `cargo clippy -p gpui-base -p gpui-kit --lib --tests -- --deny warnings` is clean. The `gpui-base` benches fail to compile on `main` already (criterion `BenchMeasurement`), unrelated to this change. ## Before / after Headless Metal render, 300px wrap width (red line). Before: lines 1–3 end past the line. After: every line ends inside. (The CLI cannot upload images; the before/after PNGs will be attached here by hand.) ## Performance The change adds one shaping pass per hard-line text segment. Release build: 400 forced relayouts of the test paragraph (the width changes every frame, so the flow's layout cache never hits), full frame including paint: - `main`: 53.8ms - this branch: 56.1–57.0ms That is about 7µs more per paragraph relayout, roughly 5% of the frame in this worst case. There is no extra work when the width is unchanged, because the cached layout is reused. The re-wrap loop runs only on lines that actually overflow. This PR was written with AI assistance (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…is off (longbridge#3299) Closes longbridge#3298 ## Problem With `Theme::focus_ring = false`, `focus_ring_style` only tinted the element's border with `theme.ring`. `Button` only has a border width on the `Default` variant and with `.outline()`, so Ghost / Text / Link and the filled variants (`Primary`, `Secondary`, `Danger`, …) showed no keyboard focus at all, even though they are Tab stops. ## Fix `focus_ring_style`, when the outer ring is off: - **Element has a border** (Default / outline buttons, Input, Checkbox, Radio, Switch track, Select, …): unchanged, the border is tinted. - **Element has no border**: an absolute child at `inset 0` draws a 1px `ring`-coloured border, with the element's own corner radii. - No layout change: no border width is added and the child is absolutely positioned. - It can't be clipped: it sits inside the element's bounds. - Radius: with no border or padding between them, the element's radius is already concentric with its edge. Theme radius 0 gives a square ring. - Only on keyboard focus: `Button` still calls `prevent_default` on mouse down, so a click never focuses it and never shows the ring. 1px matches the width of the tinted border the bordered variants already get, so every variant reads the same. Bordered elements keep the tinted border rather than switching to the inner ring: the tinted border already sits at the edge, and an extra inner line would double it. `focus_ring = true` is unchanged (outer 3px ring). The helper change also covers the other borderless callers of `focus_ring_style` (questionnaire options, carousel, time field). They now show the inner ring too when `focus_ring` is off. Refactor: the border-width / corner-radius reads in `focus_ring` were moved into small private helpers so the inner ring reuses them. No behaviour change there. ## Tests Two new Metal pixel tests in `crates/kit/tests/rendering.rs` (main thread, `harness = false`). They render Ghost (icon), Text, Link and Primary buttons with `focus_ring = false`: - `borderless_buttons_show_keyboard_focus_inside_when_focus_ring_is_off`: moves focus with Tab through Root (`focus_next` for the first stop, since Root's binding needs something inside it focused). For each button it asserts: no ring pixels while unfocused, ring pixels along the top edge inside its bounds when focused, and **zero** changed pixels outside its bounds. - `clicking_a_button_draws_no_focus_ring`: clicking each button leaves focus empty and draws no ring pixels. Without the fix, the first test fails on `ghost` ("a keyboard-focused `ghost` button must draw a ring inside its bounds"). ``` cargo test -p gpui-kit --features test-support,component,assets --test rendering cargo test -p gpui-component cargo clippy -p gpui-component -p gpui-kit --all-targets -- -D warnings ``` Co-authored-by: Claude Opus 5.5 <[email protected]>
## Summary
Charts now draw their data in the first time they are painted, over 1000
ms on Chart.js' default `easeOutQuart`. The mainstream defaults are
ECharts, Chart.js and Highcharts at 1000 ms, ApexCharts at 800 ms and
Recharts at 1500 ms. The design system's enter curve, an exponential
ease-out, made the draw-in read as a flash, so the appear uses its own
curve. Each effect follows what the mainstream libraries do (checked
against their sources):
| Chart | Appear | Path cache |
|---|---|---|
| Line / Area / Candlestick / Sankey | Revealed from the left under a
content mask (ECharts, Highcharts, Recharts) | Shapes unchanged, cache
stays warm |
| Bar | All bars grow out of the zero line together (Chart.js, Recharts,
ApexCharts); value labels fade with their bar | No cache (quads) |
| Pie | The ring sweeps clockwise (Chart.js `animateRotate`, Recharts);
leader-line labels fade in at the end | Uncached only while sweeping |
| Radar | Series grow out of the center (Chart.js, Recharts) | No cache
|
Axes, grid lines and tick labels are there from the first frame, and the
tooltip waits until the data is whole. New data paints in place, so
charts fed live quotes never replay. Reduced motion skips the appear.
The chart story gets a **Replay** button that re-keys the gallery so
every chart draws in again.
## Public API
### `gpui-base`
```rust
pub struct PlotAppear // this frame's appear of a plot
impl PlotAppear {
pub fn complete() -> Self // a finished appear
pub fn progress(&self) -> f32 // eased 0..=1 for the whole plot
pub fn is_appearing(&self) -> bool // still running
pub fn staggered(&self, index: usize, count: usize, spread: f32) -> f32 // per-mark progress, starts spread over the first `spread`
}
impl PlotMotion {
pub fn with_appear(self, appear: Transition) -> Self // how data draws in (zero by default)
pub fn appear(&self) -> &Transition
}
trait Plot {
fn interactive(&self) -> bool { true } // hover/tooltip, now separate from `id`
fn appear(&mut self, appear: PlotAppear, window: &mut Window, cx: &mut App) {} // receive the appear before hover/paint
fn appear_generation(&self) -> Option<u64> { None } // `Some` opts in; a new value replays
}
```
### `gpui-component`
On `LineChart`, `AreaChart`, `BarChart`, `PieChart`, `RadarChart`,
`CandlestickChart` and `SankeyChart`:
```rust
pub fn appear(self, appear: bool) -> Self // opt out, e.g. charts in list rows that remount on scroll
pub fn appear_key(self, key: impl Hash) -> Self // replay the appear when the key changes (symbol, period)
```
The theme projects `PlotMotion::with_appear(1000 ms, easeOutQuart)`.
## Breaking Changes
None at the source level. The three new `Plot` methods all have defaults
(`interactive` → `true`, `appear` → no-op, `appear_generation` →
`None`), so an existing hand-written plot compiles and behaves exactly
as before: with an id it tracks hover as it did, and it tracks no appear
and asks for no frames. `PlotMotion` gains a field behind a builder, and
its default adds no motion.
Behavior changes worth knowing:
- **Built-in charts draw in by default.** Opt out per chart with
`.appear(false)`.
- **`interactive(false)` charts now return an id.** `Plot::id` on them
is `Some` (it was `None`), and `Plot::interactive` is `false`. They
still get no hitbox, but they now keep element state under that id. Like
interactive charts, siblings built at one site without `.id(...)` share
the appear state and path caches. That stays correct, but it rebuilds
geometry more often.
```diff
let chart = LineChart::new(data).interactive(false);
- assert!(Plot::id(&chart).is_none());
+ assert!(Plot::id(&chart).is_some());
+ assert!(!Plot::interactive(&chart));
```
## Performance
- Once the appear is done, it costs nothing extra: no mask, no frame
requests, and `PlotAppear` returns early without sampling the easing.
The per-frame state key is allocation-free, and the theme is borrowed
rather than cloned.
- A chart with `.appear(false)` (or a hand-written plot that doesn't opt
in) tracks no state and requests no frames. Covered by tests.
- Timing harness (not committed) on 100 line + 100 bar + 50 pie charts,
200 points each, `--release`, avg CPU per frame:
| | during appear | steady |
|---|---|---|
| `main` | ~420 ms | ~411 ms |
| this branch | ~374 ms | ~404 ms |
No regression. Per type during the appear: line 140 ms, bar 139 ms, pie
0.86 ms (vs 0.32 ms steady, from re-tessellating while sweeping).
## Test plan
- [x] `cargo test -p gpui-base -p gpui-component -p gpui-kit`
- [x] New tests: appear timing over the theme duration, no frames once
finished, reduced motion, opt-out requests no frames, generation
replays, stagger math, `interactive(false)` keeps its id
- [x] `cargo clippy -p gpui-base -p gpui-component --lib --tests`
- [x] Ran the chart story: Replay restarts the appear. Mid-frame shows
the area revealing from the left and pies sweeping; after 1 s everything
is whole.
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
longbridge#3291) (longbridge#3294) ## Problem `TextView::markdown(id, text)` hands its text to the keyed state every frame, through `set_element_text` → `set_text`. When the new text extends the current one, `set_text` noticed `text.starts_with(self.text)`, but only used that to note stream fade before calling `increment_update(text, false, cx)`, which performed a full replacement reparse: - Up to `MAX_SYNC_FULL_REPLACE_BYTES` (4KB), it parsed synchronously on the UI thread and sent a baseline ack to the background parser; - Above 4KB, it parsed in the background and invalidated measured heights for all blocks upon landing; - It bumped `selection_revision`, resetting active selections on every streaming token growth; - Over a multi-token stream (e.g. streaming LLM outputs in `ai-chat`), every update cost $O(n)$, totaling $O(n^2)$ time over the entire stream. ## Solution In `TextViewState::set_text`: 1. When `!self.text.is_empty() && text.starts_with(self.text.as_str())`, take the incremental append path matching `push_str`: - Compute `delta = &text[self.text.len()..]`; - Note stream fade extension at `self.text.len()`; - Append `delta` to `self.text`; - Dispatch `self.increment_update(delta, true, cx)` so only the trailing block is reparsed without resetting selection or invalidating earlier measured block heights. 2. Keep full replacement for initial updates (`self.text.is_empty()`), text truncation, or non-prefix replacements. ## Validation - Added `set_text_prefix_extension_updates_incrementally_like_push_str` testing that prefix extensions via `set_text` retain `selection_revision` and `full_update_revision` (unlike replacements). - Added `set_text_streaming_matches_push_str_parsed_content` verifying parity between streaming chunks through `set_text` vs `push_str` across multi-paragraph, inline code, and list structures. - Ran `cargo test -p gpui-base text::state` (81 tests passed). - Ran `cargo clippy -p gpui-base` (0 errors). Closes longbridge#3291 Fixes longbridge#3291 --------- Co-authored-by: Floyd Wang <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
…#3300) Follow-up to longbridge#3299 (longbridge#3298). When `Theme::focus_ring` is off, longbridge#3299 draws a 1px `ring` line on a borderless button's edge. That has two visual problems: - **Text / Link**: these variants have no padding, so the line sits on the label and its underline. - **Filled variants** (Primary, Secondary, Danger, …): a `ring`-coloured line on the edge barely shows against dark fills. In the default light theme it is neutral-400 on neutral-900. ## Change Each borderless button now picks where its focus line goes, through a crate-private `FocusLine` passed to `styled::focus_style`: | Variant | Line | Colour | | --- | --- | --- | | Ghost | on the edge (unchanged) | `ring` | | Text, Link | 2px (0.125rem) **outside** the edge, radius = element radius + 2px | `ring` | | Primary / Secondary / Danger / Info / Success / Warning / Custom | 2px **inside** the fill, radius = max(element radius − 2px, 0) | the variant's own foreground at 60% | | Default, any `.outline()` | tinted border (unchanged) | `ring` | - **Filled variants use their own foreground.** The theme already keeps a button's foreground legible against its fill, in every variant and in both modes. The `ring` token is tuned for the page background, not for a primary fill. - **Why not a 2px `ring` line?** It is still low-contrast on fills. - **Why not an outside offset ring (background-coloured gap plus a ring line)?** It is exactly what `focus_ring = false` exists to avoid: containers clip it. - **Text/Link outside is safe in practice.** The outset is only 3px including antialiasing. Callers place these buttons with padding around them; the ai-chat HITL card's "other option" text button renders unclipped. A clipping ancestor flush with such a button would cut the line. - **Layout is unchanged in every case.** The line is an absolutely positioned child and no border width is added. - **Nothing else changes.** `focus_ring = true`, bordered variants, and other callers of `focus_ring_style` behave as before. ## Tests `crates/kit/tests/rendering.rs`. These run on the main thread (`harness = false`, Metal), with Ghost / Text / Link / Primary buttons and `focus_ring = false`. `focus_lines_stay_off_content_and_contrast_with_fills` runs in **light and dark**. Focus moves with Tab through Root. For each button: - **Every button:** no pixel changes outside the button (outside the button plus 3px for Text/Link). - **Ghost:** `ring`-coloured pixels on the top edge. - **Text / Link:** no pixel changes inside the button's bounds, which is where the label is (the four corner squares crossed by the line's arc are excluded). There are `ring`-coloured pixels 2px above the edge. - **Primary:** the line row 2px inside differs from the fill by ≥ 96/255 in some channel. `clicking_a_button_draws_no_focus_line`: clicking a hovered button leaves focus empty and the capture identical to the hovered one. These tests fail on `origin/main` before longbridge#3299 at `ghost`, and on longbridge#3299 alone at `text` (288 device pixels over the label). ``` cargo test -p gpui-kit --features test-support,component,assets --test rendering cargo test -p gpui-component cargo clippy -p gpui-component -p gpui-kit --features gpui-kit/test-support,gpui-kit/component,gpui-kit/assets --all-targets -- -D warnings ``` Co-authored-by: Claude Opus 5.5 <[email protected]>
## Summary The StatusBar demo on https://gpui-kit.com/component/status-bar/ was clipped on both sides. Each story section was fixed at `w(px(760.))`, wider than the content area of the ~748px website iframe, so the centered, overflow-hidden GroupBox content cut off both ends. Sections now use `max_w(px(760.))`: they fill narrower windows and stay 760px wide in the desktop gallery. Verified by building `story-web` locally and rendering `?story=StatusBar` at a 748px viewport; every bar shows both ends. ## Context Branch: `ihavecoke/fix-status-bar-demo` The change was written with AI assistance. Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: 李波 <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…e#3304) ## Description The README examples use `gpui_kit::open_window`, which was added in 0.7.0 (longbridge#3152). The install snippets next to them still say `gpui-kit = "0.6"`. For 0.x crates, Cargo treats `"0.6"` as `>=0.6.0, <0.7.0`, so anyone who copies the README gets 0.6.6. The example then fails to compile: ``` error[E0425]: cannot find function `open_window` in crate `gpui_kit` ``` This PR changes every hardcoded `gpui-kit` requirement in Markdown from `0.6` to `0.7`: - `README.md` - `README.zh-CN.md` - `crates/kit/README.md` (the crates.io readme; the published page updates with the next release) - `crates/base/README.md` - `examples/app_assets/README.md` - `skills/gpui-kit/references/usage.md` The website docs are not affected because they use `{{gpui_kit_version}}`. `script/bump-version.sh` only runs `cargo set-version`, which does not touch Markdown. These snippets will need a manual update again on the next minor release. ## How to Test - `git grep -nE 'gpui-kit = (\{ *version = )?"[0-9]' -- '*.md'` lists only `0.7`. - A new project with `gpui-kit = "0.7"` and the README example resolves to 0.7.0 and calls `gpui_kit::open_window`. ## Checklist - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [ ] 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) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…dge#3305) ## Description The web gallery's welcome story renders `README.md` through `include_str!`, but `crates/story-web/scripts/subset-fonts.py` only collected characters from `.rs` sources. The README's `├──` / `└──` crate tree lost its glyphs from the JetBrains Mono subset, and the `EmojiAndCjk` Canvas fallback does not cover box-drawing characters, so https://gpui-kit.com/gallery/ draws them as tofu. Desktop builds are unaffected: they use complete system monospace fonts with platform fallback. - `subset-fonts.py` now also reads every file a scanned source pulls in with `include_str!` (README, fixtures, theme JSON, `examples/fixtures/test.md`). - FontTools 4.66 keeps the input flavor when `--flavor` is not given, which wrote `Inter-Regular.ttf` as WOFF2. The script passes `--flavor=none` and pins `fonttools[woff]==4.66.0` so reruns produce the same files. - Regenerated the four subsets. They were also behind the current story sources: Noto Sans SC gains 70+ Han characters the stories already use (such as 探索 and 继续编辑), and `▸ ▾ ○`, no longer used anywhere, drop out. - Updated the Noto Sans SC subset size and the subset description in `webassembly.md` and `fonts.md` (`en` and `zh-CN`). | Font file | Before (bytes) | After (bytes) | | --- | ---: | ---: | | `Inter-Regular.ttf` | 88,036 | 94,432 | | `JetBrainsMono-Regular.ttf` | 84,160 | 89,508 | | `NotoSansSC-Regular-subset.ttf` | 25,484 | 42,092 | | `IBMPlexSans-Regular.ttf` | 48,676 | 49,124 | ### Before <img alt="before" src="https://github.com/user-attachments/assets/2e3628d9-3a76-465f-94b9-d9bec1eabebd" /> ### After <img alt="after" src="https://github.com/user-attachments/assets/ab84a9ec-77c3-42cf-8cb8-91cdfdd722d2" /> ## How to Test - Run `bun run --cwd crates/story-web/www fonts` twice: the generated files are byte-identical, and all four start with the TrueType `00010000` header. - Run `make dev` in `crates/story-web` and open `/gallery/`. The Introduction page draws the crate tree with box-drawing lines; the live site shows tofu in the same place. The script change, regenerated fonts and documentation edits in this PR were AI-generated. Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: 李波 <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…ge#3623) ## Description On a touch screen, the user can hide the keyboard (Android's back button, or the IME's Done) while an input keeps focus, as with a native text field. A later tap on that input doesn't change focus, and a tap on another input moves focus between two text inputs. In neither case does the platform learn that the keyboard is wanted again, so it stays hidden while the user taps the field. A touch press (`GlobalState::is_touch_press`) with the left button on an editable input (not disabled or read-only) now calls `Window::request_virtual_keyboard()`. Platforms without a virtual keyboard ignore it, and mouse clicks are unchanged. One thing to know: gpui-mobile's Android window doesn't implement `request_virtual_keyboard` yet (I'll send that there next), so on Android today this is a no-op, like on desktop. It doesn't depend on that change to build or to be correct. No public API changes. ## Screenshot None: there's nothing to see until a platform shows the keyboard on request. ## How to Test - `cargo test -p gpui-base --features test-support --lib input`: 370 passed - `cargo test -p gpui-kit --features test-support --test integration -- input:: input_focus::`: 210 passed - `cargo clippy -p gpui-base -p gpui-component -p gpui-kit --features gpui-kit/test-support --tests -- --deny warnings`, `cargo fmt --all -- --check`: clean - To check on Android you need a gpui-mobile that implements `request_virtual_keyboard`: hide the keyboard with back, then tap the still-focused field; the keyboard should come back. I haven't recorded that check yet. ## 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. (Not run: touch-only path.) - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) (Touch-only; desktop mouse clicks don't take this path.) Thanks for taking the time to review this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
…oard (longbridge#3624) ## Description On a phone the window is edge to edge: the status bar, the navigation bar, a display cutout and the on-screen keyboard draw over it. GPUI reports what stays visible as `Window::fully_visible_bounds()`. Sheets and toasts were laid out against the whole viewport, so a sheet's title and close button sat under the status bar, and toasts sat under the keyboard. - `window_border::window_safe_insets(window)` (crate-private) turns the fully visible bounds into per-side insets. - `Sheet` takes the larger of those and the window frame insets it already used. - The notification layer is inset by them, and `NotificationList` sizes itself from the visible area rather than the viewport. On desktop the fully visible bounds are the whole viewport, so the insets are zero and nothing changes. One thing to know: gpui-mobile's Android window still reports zero insets (I have a change for that to send there), so on Android today this has no effect until that lands. It doesn't depend on it to build or to be correct. No public API changes. ## Screenshot None yet for sheets and toasts. For the same fully visible bounds on a device: with my gpui-mobile fork reporting insets, the input reveal in longbridge#3625 keeps a focused field above the keyboard:  ## How to Test - New test `sheet_and_notifications_stay_in_the_fully_visible_area` (`overlays.rs`): a status bar above and a keyboard below, simulated with `simulate_window_visual_viewport_change`; the sheet and the toast must sit inside the visible area. - `cargo test -p gpui-kit --features test-support --test integration -- overlays:: sheet notification`: 8 passed (incl. the new test) - `cargo test -p gpui-base --features test-support --lib input`: 370 passed - `cargo clippy -p gpui-base -p gpui-component -p gpui-kit --features gpui-kit/test-support --tests -- --deny warnings`, `cargo fmt --all -- --check`: 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. - [ ] Passed `cargo run` for story tests related to the changes. (Not run: on desktop the insets are zero.) - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) (No effect on desktop.) Thanks for taking the time to review this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…st within one frame (longbridge#3648) ## Description `horizontal_scroll_area_claims_only_steps_that_move`, added in longbridge#3621, has failed on `main` on every platform since it landed: ``` panicked at crates/base/src/scrollable_mask.rs:1043:9: left: 0px right: 30px ``` The test seeds a transient overscroll (`set_offset(40px)`) and expects it to survive two `simulate_event` calls. With `test-support`, `flush_effects` draws every dirty window, so the wheel listener's `cx.notify` draws a frame after each `simulate_event`, and prepaint clamps the 30px overscroll back to 0 before the next step runs. The mask itself behaves as intended: the handler writes 30px and records no claim. The two overscroll steps are now dispatched with `window.dispatch_event` inside one `cx.update`, so they land in the same frame, as coalesced input does on a real platform. `scroll_bounce.rs` tests coalesced input the same way. The assertions are unchanged. Only the test changes. ## How to Test ```sh cargo test -p gpui-base --features test-support --lib scrollable_mask ``` ## 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: Claude Opus 5.5 <[email protected]>
…e#3629) Fixes longbridge#3619. Read the active resizing panel from retained state when move/up events arrive. A press, drag activation, movement and release delivered before the next paint now resize the panels and call `on_resize` once. Queued events after programmatic cancellation no longer reuse the previous frame’s active index. Adds two interaction regressions using the existing Base fixture: a complete gesture within one update, including repeated release and subsequent movement, and cancellation after an active drag has been painted. No public API changes. Validation on `c8f9ec7efc6845f19fb34a90ce1b789bdcbeebca`: - Both new regressions fail against unchanged main (`8b3631f0`): the fast gesture leaves sizes at `[150, 250]`, and a queued move after cancellation changes them to `[260, 140]`. - `cargo test -p gpui-base --features test-support --lib resizable:: --locked`: 21 passed. - `cargo clippy -p gpui-base --features test-support --lib --tests --locked -- --deny warnings`: passed. - `cargo fmt --check`, changed-file `typos`, and `git diff --check HEAD^ HEAD`: passed. Tests ran on Linux with temporary development libraries because system package directories are read-only. Native macOS/Windows and manual Story testing were not performed. Implementation and tests were written with ChatGPT assistance on behalf of @huacnlee. An independent ChatGPT review of the exact commit found no remaining issues; this does not claim human review.
## Description A `\t` in the code editor is handed to the platform shaper as is, and the platforms disagree on what it is: - **Windows (DirectWrite):** the tab gets no width. Tab-indented code (Go, PowerShell, Makefiles…) renders flush left, while the indent guides, which count a `\t` as `tab_size` columns, are still drawn at the indent levels and cut through the text. - **macOS (CoreText):** the tab gets CoreText's default stops, which ignore `TabSize`. Each line is now shaped as before, and then its tabs are widened to end on the next tab stop (`tab_size` × the width of a space) by moving the glyphs after them. Caret placement, hit testing and selection all read glyph positions, so they follow without further changes. - `input/base/tab_stops.rs` (new): `expand_tabs`, `expanded_width` and `tab_stop_width`, with unit tests. - `layout_lines` expands the tabs of single-line and soft-wrapped rows. - The longest-line width used for horizontal scrolling measures expanded tabs; `tab_size` is part of its cache key. - `TextWrapper` knows the tab size and measures wrap candidates with expanded tabs, so wrapping matches what is drawn. Each wrapped row starts its own tab stops, in both measuring and drawing. Not covered: the inline-token layout path (`layout_token_lines`) shapes tabs as before. ## Screenshot | Before | After | | ---------------------------- | --------------------------- | | <img width="624" height="425" alt="go-beforefix" src="https://github.com/user-attachments/assets/83c3f002-6b83-4ba8-a2e0-c5ad785fcde3" /> | <img width="657" height="422" alt="go-afterfix" src="https://github.com/user-attachments/assets/83738cb7-75a5-4153-a664-a13b1d2bc10b" /> | ## How to Test 1. Open a file indented with tabs, e.g. any Go file, in a code editor with indent guides on (`cargo run -p example-editor`). 2. On Windows, before this change the code is flush left and the guides cross the text; after it, the code is indented to `tab_size` and the guides sit in the indentation. 3. Change the tab size: the indentation follows it. `cargo test -p gpui-base --features test-support --lib` (1444 passed) and `cargo test -p gpui-component --features test-support --lib input` pass; `cargo clippy -p gpui-base --all-targets --features test-support -- --deny warnings` and `cargo fmt --check` 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. - [ ] 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: Jason Lee <[email protected]>
## Description Focused inputs can remain outside their ancestor's scroll viewport after gaining focus or after a window resize. This PR makes the input emit a reveal request; the ancestor remains responsible for executing it and for defining its visible viewport and scroll range. The input element now calls `Window::request_autoscroll` while prepainting when it's focused and has either just gained focus or the window's `fully_visible_bounds()` have changed since its last request. A keyboard that resizes the scroll container can therefore reveal the input. Inset changes also trigger a request, but inset-only keyboard avoidance requires additional support in the consuming scroll container; this PR alone does not provide that support. - The state remembers the bounds it last revealed in, so the request isn't repeated every frame, and the user can still scroll away from a focused field. Losing focus clears that cache even if the input is temporarily unmounted and does not prepaint while unfocused. - A single-line input asks for all of itself. A multi-line one asks for the caret's line (or its top 120 px when it has no caret), so a tall textarea doesn't drag the page to its bottom. - The request has an 8 px margin for the field's padding and border. Where it takes effect: stock GPUI `List` honours requests against its own layout bounds, so focus and actual container resizing are supported. It does not intersect those bounds with `fully_visible_bounds()` or adjust its scroll range for keyboard insets. An input that remains inside the List's layout bounds can therefore still be obscured by an inset-only keyboard. Correct inset-only avoidance requires a GPUI scroll-container change (or an application that resizes the container to the visible viewport). A plain `overflow_y_scroll()` div also ignores autoscroll requests in the pinned GPUI version. These consumer limitations are not fixed by this PR. On desktop, an input inside a `List` now scrolls into view when it gains focus (by Tab, say) or when the window is resized while it has focus. No public API changes. ## Screenshot Original author's Android example (OnePlus CPH2581, Android 16), using this change together with a gpui-mobile fork. The screenshot demonstrates that combined setup; it is not evidence that stock GPUI supports inset-only keyboard avoidance with this PR alone.  ## How to Test - New test `focused_input_asks_its_scroll_container_to_reveal_it`, inside a `List` so it runs on stock gpui-pre: focus reveals the input, scrolling away while focused is left alone, temporarily unmounting the input while it loses focus does not leave a stale reveal cache when it is remounted and focused again, and a smaller window reveals it again. It fails without the change. - `cargo test -p gpui-base --features test-support --lib input`: 371 passed (incl. the new test) - `cargo test -p gpui-kit --features test-support --test integration -- input:: input_focus::`: 210 passed - `cargo clippy -p gpui-base -p gpui-component -p gpui-kit --features gpui-kit/test-support --tests -- --deny warnings`, `cargo fmt --all -- --check`: 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. - [ ] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) Thanks for taking the time to review this. 🤖 Generated with [Claude Code](https://claude.com/claude-code). Follow-up focus-cache fix, regression coverage, and scope clarification assisted by Codex. Follow-up validation: `cargo fmt --check -p gpui-base` and `git diff --check` passed. The extended focused-input regression has not completed its first dependency build; the original author's test totals above predate this follow-up. --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
…longbridge#3628) Closes longbridge#3627 ## Description Closing or replacing a TextView previously left each paragraph's shaped text in the thread-local RETAINED_LAYOUTS cache until another insertion found at least 4096 entries. Closing the last text view could therefore retain the entire document indefinitely. Dropped InlineStates advance a global generation so each UI thread can identify pending cleanup independently. Each state also records sendable cleanup notifications for the UI owners that actually retained its layout. Its destructor notifies those owners even when the final reference is released by the background parser. Base text initialization installs one foreground cleanup task per App. Its bounded channel coalesces notifications, and the task sweeps dead entries without requiring another Inline or another frame. Live states keep their cached layouts. App teardown closes the channel before cancelling the local task, and notifications are scoped to actual owners rather than broadcast across unrelated apps. Layout handovers retain the existing sweep fallback. Cleanup metadata is excluded from InlineState's document equality. No public API changes. ## Validation - `cargo test -p gpui-base --features test-support --lib retained_layout_tests`: 7 passed. - `cargo fmt --check -p gpui-base`: passed. - `git diff --check`: passed. The regressions cover replacing a document, removing the last 40-paragraph TextView without drawing replacement text, and releasing the final state on the background executor without another layout handover. Existing shared-paragraph, inline-flow reuse, shortening, and cross-thread sweep tests remain covered. The full workspace suite and platform-specific visual checks were not rerun for this follow-up. AI-assisted: the original change and follow-up cleanup implementation and regression coverage were written with coding agents. --------- Co-authored-by: Jason Lee <[email protected]>
Adds a `pt-BR` text for every string in
`crates/component/locales/ui.yml` (120 strings), so components read in
Brazilian Portuguese when the app sets `pt-BR`: the diff view's conflict
actions and labels, calendar and date picker, inputs, dialogs, command
palette, carousel, questionnaire, speech and the rest.
Only the locale file changes; no code and no public API.
Tested by running an app (Rook Labs) on gpui-component 0.8.0 with these
texts and `rust_i18n::set_locale("pt-BR")`: the diff view's conflict
rows read "Alteração atual", "Aceitar a atual", "Aceitar a que chega",
"Aceitar as duas", and the resolved and undo labels likewise.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Sergio Balecho <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
…ge#3640) ## Description `hex_string` truncated each channel to 8 bits, so the color picker's hex field showed `7F` for an alpha of 0.5 and `FE` for a channel of 0.999. It now clamps each channel to 0–1 and rounds, so the field shows the nearest 8-bit value. No public API changes. The visible change: a color between two 8-bit steps reads as the nearer one instead of the lower one. ## How to Test - `formats_alpha_only_when_translucent` now expects `#FF000080` for an alpha of 0.5; with truncation it fails with `#FF00007F`. - On this branch, macOS, Rust 1.99.1: `cargo clippy -p gpui-base --locked -- --deny warnings`, `cargo test -p gpui-base --features test-support --lib color_picker`, `cargo fmt --check` and `typos`. ## 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. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
…ongbridge#3632) ## Description `Overscroll::packet` treats any part of a packet that the text reports as unused as the text running into an edge (`unused != 0.`). Callers work out what was used by subtracting the offset before the scroll from the offset after it: `ScrollBounce` does this with `offset.y - before.y`, and the input does the same. Far from the start, an `f32` offset cannot hold every fraction, so text that used a whole packet can report a few ten-thousandths of a pixel less. In a drag, that residue is pulled into the stretch, which leaves the edge a hair off zero. Every later packet in the same direction then goes into that stretch instead of the text. The content stops scrolling and slides with its edge clipped, even though the list is nowhere near its end. Here is how it showed in a 8,900 px list: ```text drag back off the bottom edge rest=61.74649 used=61.746094 unused=0.00039672852 next packet, dy=122 stretch 0.0000297 → 8.83, list offset frozen at -8840.754 ``` The momentum branch makes the same comparison, so a rounding residue there throws the edge in the middle of the text. A residue of up to 0.01 px now counts as rounding. `ScrollBounce`'s own phase handling already uses that tolerance (`residual.abs() > 0.01`): ```diff -if unused != 0. { pull / throw } +if unused.abs() > ROUNDING { pull / throw } // ROUNDING = 0.01 ``` This is separate from longbridge#3631, which covers a release in the middle of the text. I found both in [Tcode](https://github.com/Tryanks/tcode), whose chat timeline is a `ScrollBounce` over a `ListState`. ## How to Test `overscroll::tests::a_drag_back_off_a_far_edge_keeps_scrolling_the_text` stretches the end of 8,902.5 px of text, lets the edge go, and catches it on its way back after 1 to 59 ms. It then drags back past the edge into the text and on in the same direction. The test uses an `f32` offset, the way a scroll handle holds it. - **Before:** after the drag, the edge is stretched by 8.83 px (`the edge stays put (1 ms)`, `left: 8.828897`, `right: 0.0`). - **After:** the edge stays at 0, and the 122 px drag scrolls the text. Checks run: - `cargo test -p gpui-base --features test-support --lib`: 1439 passed, 1 failed. The failing test, `scrollable_mask::tests::horizontal_scroll_area_claims_only_steps_that_move`, fails the same way on `main` (da8b2ba) without this change. - `cargo clippy -p gpui-base --features test-support --all-targets -- --deny warnings`: clean. - `cargo fmt --check`: clean. Tried in Tcode with this patch and longbridge#3631 applied to gpui-base 0.8.0, by swiping quickly up and down on a macOS trackpad. The numbers above come from logging added to that build. Not run: the story gallery, Windows and Linux, and the text input's own use of `Overscroll`. The diagnosis, fix and test were written with Claude Code. ## 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: Jason Lee <[email protected]>
…ge#3631) ## Description When a drag ends while the finger is still moving, `Overscroll` throws the edge with the finger's speed even if nothing is stretched. `Physics::let_go` treats a zero position as outward for any velocity. So a flick in the middle of a long list pushes the content up to `max_stretch` past where it scrolled, and the content springs back over about half a second. A gesture that starts in that half second grabs the thrown edge, because `Physics::begin` keeps a displaced edge where it is. Its packets then go into the stretch instead of the list. The content slides like a picture: its edge is clipped, the background shows behind it, and no new rows come in until the finger reverses past the stretch. On a macOS trackpad this happens every time you flick and put your fingers back down to keep reading. On touch, where tracking is 0.55 and there is no stretch limit, the list seems to stick after a swipe made during a fling. The throw is meant for an edge held past its end. When the drag is released within the content, the momentum that follows already scrolls it, and throws the edge if it runs into it (the `unused != 0.` branch). With this change, the release itself carries speed only when the edge is stretched: ```diff on Ended / Cancelled while dragging - let_go(moving ? velocity * CARRY : 0) + let_go(moving && stretched ? velocity * CARRY : 0) ``` Found in [Tcode](https://github.com/Tryanks/tcode), a GPUI app on gpui-base 0.8.0, whose chat timeline is a `ScrollBounce` over a `ListState`. ## How to Test `overscroll::tests::a_flick_away_from_the_edges_leaves_the_next_drag_to_the_text` flicks six 70 px packets through 10,000 px of text and releases. It then puts the fingers back down and drags 50 px the same way. - **Before:** the release throws the edge out by 16 px (`no edge to throw`, `left: -16.023891`, `right: 0.0`). In Tcode, the drag that follows scrolls the list by 0 px. - **After:** nothing is displaced, and the drag scrolls the text by 50 px. Checks run: - `cargo test -p gpui-base --features test-support --lib`: 1439 passed, 1 failed. The failing test, `scrollable_mask::tests::horizontal_scroll_area_claims_only_steps_that_move`, fails the same way on `main` (da8b2ba) without this change. - `cargo clippy -p gpui-base --features test-support --all-targets -- --deny warnings`: clean. - `cargo fmt --check`: clean. Verified in Tcode with this patch applied to gpui-base 0.8.0, on a macOS trackpad: a flick in the middle of a long chat, followed straight away by a drag, now scrolls the chat. Not run: the story gallery, Windows and Linux (`ScrollBounce` is off by default on both), and the text input's own use of `Overscroll`. The diagnosis, fix and test were written with Claude Code. ## 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: Jason Lee <[email protected]>
…ongbridge#3635) ## Description A dialog's top edge sits at `margin_top`, a tenth of the viewport by default. That puts a short alert near the top of a tall window, and the caller cannot center it with `margin_top` because it doesn't know the surface's height. `centered()` anchors the surface at its left-center point, at the vertical middle of the viewport, instead of at its top-left corner. The positioner's existing clamp still keeps a tall dialog inside the window margin. Without `centered()`, nothing changes. ## Screenshot An `AlertDialog` with `.entrance(DialogEntrance::Fade)` in an 1180 × 728 pt window. | Before | After | | --- | --- | |  |  | ## Public API `gpui-component`: - `Dialog::centered(self) -> Self`: centers the dialog vertically in the window, whatever its height, in place of the `margin_top` offset. - `AlertDialog::centered(self) -> Self`: the same for an alert dialog. ## How to Test - `cargo test -p gpui-component --lib dialog::` passes 25 tests, including the new `centered_seats_the_alert_surface_in_the_middle`. It opens an alert in an 800 × 600 window and asserts that the surface's center is within 0.5 px of y = 300. - Checked by hand in a macOS app: five alerts with `.centered()`, in a resizable window. ## 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: mastro993 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
## Description A component library with its own scrollbar element (its own visibility modes, hover and drag handling) cannot use it on Base's editors today. `ScrollbarTheme` and `ScrollbarStyles` restyle Base's scrollbar, but the editor's viewport and content geometry exist only inside Base's layout, so the only alternative is a second scroll system over the editor. `InputBaseState::set_scrollbar_renderer` lets the consumer supply that element. When a renderer is set, the editor calls it at the existing scrollbar prepaint step and paints the returned element in place of `Scrollbar`. The callback receives an `EditorScrollbarContext`, which implements `ScrollbarHandle` (the padded viewport bounds for the track, the content size, and the offset after caret scrolling) and adds the axis and the scroll viewport. The scroll viewport is the `scroll_viewport_size` Base's own scrollbar has used since 0.8.0: the scroll range is the content size minus it, so a custom scrollbar reaches the editor's end instead of stopping short by the editor's padding. Without a renderer the editor keeps its current scrollbar. The renderer is stored on the state, not in `InputEditorStyle`, so existing style literals compile unchanged, and a style that is set again on every render (as `gpui-component`'s `Input` does) does not clear it. Like `set_editor_style`, the setter does not notify, so it can be called while rendering. ## Public API `gpui-base` - `InputBaseState::set_scrollbar_renderer(&mut self, renderer: Option<EditorScrollbarRenderer>)` (so on `InputState`, `TextareaState` and `EditorState`) — supplies the element painted in place of the editor's scrollbar; `None` keeps Base's. - `gpui_base::input::EditorScrollbarRenderer` = `Rc<dyn Fn(EditorScrollbarContext, &mut Window, &mut App) -> AnyElement>` — the renderer type. - `gpui_base::input::EditorScrollbarContext` — the editor's current-frame scrollbar geometry; implements `ScrollbarHandle`. - `EditorScrollbarContext::axis(&self) -> ScrollbarAxis` — `Vertical` with soft wrap, otherwise `Both`. - `EditorScrollbarContext::scroll_viewport_size(&self) -> Size<Pixels>` — the viewport without the editor's padding; the scroll range is the content size minus it. ## How to Test - `scrollbar_renderer_receives_the_editors_scroll_range` (`input/base/element.rs`) pads an editor 10px top and bottom, sets a renderer that records the context it receives, and scrolls to the end. The track is 20px taller than the scroll viewport, and the end offset is the content size minus the scroll viewport. Passing the padded bounds as the scroll viewport fails it (0px instead of 20px). - On this branch, macOS, Rust 1.99.1: `cargo clippy -p gpui-base -p gpui-component --locked -- --deny warnings`, `cargo test -p gpui-base --features test-support --lib`, `cargo test -p gpui-component --features test-support --lib input::`, `cargo fmt --check` and `typos`. Every test passes except `scrollable_mask::tests::horizontal_scroll_area_claims_only_steps_that_move`, which fails the same way (0px instead of 30px) on `main` at da8b2ba without this PR. - Downstream, an application scrollbar drawn through this hook on text areas and code editors: dragging its thumb to the end reaches the same offset as wheel scrolling to the end. ## 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. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
…2842) Applications displaying plain text with known URL spans currently need to encode it as Markdown/HTML or replace the text renderer. Textarea also lacks the independently managed text decoration collections available to Editor. Add `TextViewState::linked_text` and `TextViewLink` for selectable plain text with explicit UTF-8 link ranges, and `set_linked_text` to replace text and links atomically. Existing `on_link_click` callbacks handle activation. Add resting and hover underline controls to Base styles and Base/Component TextView builders, preserving existing Component TextViewStyle struct literals. Hover underlines compose with text marks and work in image-containing paragraphs; invalid ranges and stale hover state are discarded. Extend tracked text decoration collections to Textarea, including independent set, append, clear, and range inspection. Behavior lives in `gpui-base`, with the existing Component import paths retained. English and Chinese documentation cover the new usage. ## Public API ### gpui-base - `text::TextViewLink` — a plain-text link span with private fields. - `TextViewLink::new(range: Range<usize>, url: impl Into<SharedString>) -> Self` — construct a link. - `TextViewLink::range(&self) -> Range<usize>` — inspect its source byte range. - `TextViewLink::url(&self) -> &SharedString` — inspect its URL. - `TextViewState::linked_text(text: &str, links: Vec<TextViewLink>, cx: &mut Context<Self>) -> Self` — create linked plain text. - `TextViewState::set_linked_text(&mut self, text: &str, links: Vec<TextViewLink>, cx: &mut Context<Self>)` — atomically replace text and links. - `TextView::link_underline(self, underline: bool) -> Self` — override resting link underline. - `TextView::link_underline_on_hover(self, underline: bool) -> Self` — override hover underline. - `TextViewStyle::with_link_underline(self, underline: bool) -> Self` — set resting underline in Base style. - `TextViewStyle::with_link_underline_on_hover(self, underline: bool) -> Self` — set hover underline in Base style. - `TextViewStyle::is_link_underlined(&self) -> bool` — inspect resting underline policy. - `TextViewStyle::is_link_underlined_on_hover(&self) -> bool` — inspect hover underline policy. - `input::TextareaExtras` with `Default` and `InputExtras` implementations — retain Textarea annotations. - `<TextareaMode as InputModeKind>::Extras = TextareaExtras` — replaces the former unit associated type. - `TextareaState::create_decorations_collection(&mut self, decorations: Vec<TextDecoration>, cx: &mut Context<Self>) -> TextDecorationCollection` — create tracked, independently managed text decorations. ### gpui-component - `text::TextViewLink` — re-export the Base link type; the existing `TextViewState` re-export exposes its new constructor and setter. - `TextView::link_underline(self, underline: bool) -> Self` — override resting underline without replacing the component theme. - `TextView::link_underline_on_hover(self, underline: bool) -> Self` — override hover underline without replacing the component theme. - The existing `input::TextareaState` alias exposes the new decoration collection method. ## Breaking Changes The public mode trait's Textarea associated type changes. Normal TextareaState construction is unchanged; generic consumers constraining the associated type must update: ```diff - M: InputModeKind<Extras = ()> // included TextareaMode + M: InputModeKind<Extras = TextareaExtras> // TextareaMode ``` ## Validation - `cargo check -p gpui-component --offline` - `cargo fmt --check` - `git diff origin/main --check` - Added regression coverage for linked-text parsing and updates, invalid ranges, and hover underlines overlapping text marks. Tests were not run per repository configuration; no manual window verification. Implementation updates and conflict resolution were assisted by Codex. --------- Co-authored-by: Jason Lee <[email protected]>
…gbridge#3641) ## Description `write_hex_input` set `suppress_input_change` before calling `InputState::set_value`, to ignore the change event that write would send back. `set_value` emits no `InputEvent::Change` (its documentation says so), so the flag stayed set until the user's next real edit and swallowed that instead. After any programmatic value, such as `set_value`, a slider move or a palette pick, the first hex the user typed did not preview. The flag is removed. No public API changes. ## How to Test - `first_hex_edit_after_set_value_previews` sets a value, then makes a typed edit (new text and its change event) and checks that the edit previews. - On this branch, macOS, Rust 1.99.1: `cargo clippy -p gpui-base --locked -- --deny warnings`, `cargo test -p gpui-base --features test-support --lib color_picker`, `cargo fmt --check` and `typos`. ## 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. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
## Description The OtpInput component page documented an API that does not compile against v0.8.0: - It subscribed with `InputEvent`, the text-input event, but `OtpState` emits `OtpEvent`. - It discarded the `Subscription` returned by `cx.subscribe`, so the callback was unregistered immediately. - It held the value borrowed from `state.read(cx)` while calling `this.verify_*(&code, cx)`, which needs `&mut cx`. - It passed a three-argument method to `cx.listener`, which requires `(&mut T, &E, &mut Window, &mut Context<T>)`. - The PIN example used `window` without taking it as a parameter. This PR rewrites the event-handling, SMS, 2FA, PIN, and common-pattern snippets around `OtpEvent`, stores subscriptions on the view, clones the value before the mutable `cx` call, adds the four-argument listener, and uses `subscribe_in` where the callback needs `window`. It also corrects the event table (`Change` fires after every edit; `Complete` fires when the last digit is entered) and the keyboard-navigation notes (arrow keys and Delete are not handled; the cells share one focus target). Both the English and Chinese pages are updated. ## How to Test - Compiled the corrected examples in a temporary `crates/story/examples` file against the workspace `gpui-kit` v0.8.0 with: ```sh cargo check -p gpui-component-story --example zz_doc_check --no-default-features ``` The temporary file was removed before committing; only the two documentation files are included. - `typos website/component/otp-input.md website/zh-CN/component/otp-input.md` - `git diff --check` ## 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. (Documentation-only change; no story code was touched.) - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific). (Not platform-specific.)
…#3642) ## Description In an active focus trap, `advance_focus` (Tab and Shift-Tab) walks the window's tab order until focus is inside the trap, and stops early only when focus comes back to the first stop it reached. If GPUI's tab order cycles without passing that stop, it never comes back. We hit this with Shift-Tab from a modal in an application whose tab order held a stop that `focus_prev` returned as its own previous stop: the walk stayed on that stop, and the window hung on the key press. We have not isolated what makes GPUI's order do that; a handle tracked twice in one frame does not do it on its own. The search now records the stops it visits and, at the first repeat, focuses the trap's container, the fallback it already uses for a trap with no reachable stops. A full cycle behaves as before. No public API changes. ## How to Test - `focus_trap_search_ends_at_a_short_cycle_on_its_container` drives `advance_focus` with a scripted `advance` (first, second, second) that panics when it runs out, so the previous loop fails in about 0.06s instead of hanging. - The existing focus-trap tests still pass, including wrapping past 256 background stops, a trap without stops, and an inactive trap. - On this branch, macOS, Rust 1.99.1: `cargo clippy -p gpui-base --locked -- --deny warnings`, `cargo test -p gpui-base --features test-support --lib root`, `cargo fmt --check` and `typos`. ## 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. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
Toolbar sets a fixed height per size (`h_7`, `h_8`, `h_12`). Content placed with `content` that is taller than the bar, such as a two-line title, overflows and is clipped at the top and bottom. This switches the three heights to `min_h_7`, `min_h_8` and `min_h_12`. Content that fits keeps today's height (28px, 32px, 48px); taller content makes the bar grow. The Sizes section of the toolbar docs (en and zh-CN) now says the heights are minimums. No public API changes. Verified with `cargo fmt --check`, `cargo clippy -p gpui-component --all-targets -- --deny warnings -A clippy::nonminimal_bool` (the allow covers two existing `nonminimal_bool` errors in `crates/base/src/calendar.rs:132` under Rust 1.95, which fail on main too), `cargo test -p gpui-component toolbar` (6 passed) and `cargo check -p gpui-component-story`. Per AGENTS.md, no test was added just to assert the bar's height. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…3653) ## Problem `HoverCard`'s `Styled` refines the card's content, and the base `HoverCard` puts the trigger in a bare container, so a caller has no way to size the element the parent lays out. Inside a flex cell the trigger shrinks to its content, and `w_full`, `flex_1` or `min_w_0` have nowhere to go: ```rust HoverCard::new("card") .trigger(Label::new("A long name that should truncate").w_full()) .child("Card content") ``` Popover had the same problem (longbridge#3041), fixed by longbridge#3245. This applies the same fix to `HoverCard`. ## Change - `gpui_base::HoverCard` implements `Styled`, forwarding the refinement to the `Popup` root: the element laid out in the parent and measured to anchor the card. It adds no visual defaults. - `gpui_component::hover_card::HoverCard` gains `trigger_style`, which passes its refinement to that root, as `Popover::trigger_style` does. `Styled` on the component still styles the content. The test `trigger_style_is_applied_to_the_trigger_container` mirrors the Popover one: a 40px trigger with `trigger_style(w_full())` in a 200px row, anchored `TopRight`, and asserts the open card follows the container's right edge (140px unstyled, 300px styled). ## Public API ### gpui-base - `impl Styled for gpui_base::HoverCard` — styles the trigger container: the element laid out in the parent and measured to anchor the card. ### gpui-component - `pub fn trigger_style(self, style: StyleRefinement) -> Self` on `gpui_component::hover_card::HoverCard` — sets the style of the trigger container, where `w_full`, `flex_1` or `min_w_0` go for the trigger to fill its slot. ## Verification - `cargo test -p gpui-component --lib hover_card`: the new test passes, and fails at the 300px assertion when the refinement is stored but not applied. - `cargo test -p gpui-base --features test-support --lib hover_card`: 4 passed. - `cargo clippy -p gpui-base -p gpui-component --locked -- --deny warnings`, plus `--all-targets` per crate: passed, apart from a `clippy::nonminimal_bool` error in `crates/base/src/calendar.rs:132`, which this PR does not touch and which local clippy (Rust 1.95) also reports on `main`. - `cargo fmt --check`, `typos` on the changed files: passed. Tested on macOS. I did not run the Story app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…dge#3654) ## Problem `CommandState` gives the list container `p_1()` whenever there are no rows, but it renders the empty-state child only when it is not loading. While a search is in flight with no rows, the padded container has no child and draws a blank half-rem strip under the query field. ## Change Apply the container padding under the same condition that renders the empty child: `rows_count == 0 && !self.loading`. When the empty state shows, the result is unchanged. A custom `.empty()` slot that renders nothing still gets the padding. The slot returns an `AnyElement`, so the palette cannot tell that it is blank without new API. That case is out of scope here. ## Verification - `cargo fmt --check` - `cargo clippy -p gpui-component --all-targets --no-deps -- --deny warnings`. Without `--no-deps`, clippy 1.95 stops on two existing `nonminimal_bool` errors in `crates/base/src/calendar.rs:132`, which this PR does not touch. - `cargo test -p gpui-component command` (36 passed) No new test: this is a padding change, and AGENTS.md asks for no tests that only check presentation dimensions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…ongbridge#3652) Base `Toolbar` tracks a focus handle (`tab_stop(false)`) so roving focus can check containment. GPUI focuses any element with a tracked handle on mouse down, so a press on blank toolbar space or on non-focusable content, such as a title label, moved keyboard focus to the toolbar container. An application whose table or editor owns focus lost it whenever the user clicked an empty part of the toolbar. ## Change The toolbar root now calls `window.prevent_default()` on mouse down, the same mechanism native buttons use to stop the default focus transfer. GPUI dispatches bubble-phase mouse-down listeners from the innermost element outwards, so a focusable item handles the press first and still takes focus. Arrow-key roving is unchanged. This is a change in `gpui-base`, because the focus handle lives there. ## Test `toolbar::tests::behavior::clicking_blank_toolbar_space_keeps_focus_outside` focuses an element outside the toolbar and clicks blank toolbar space, then asserts that focus did not move. It then clicks a focusable item and asserts that the item took focus. The test fails without the fix ("expected outside (blank toolbar click) to be focused") and passes with it. ## Verification - `cargo test -p gpui-base --features test-support --lib toolbar`: 8 passed - `cargo test -p gpui-component --features test-support --lib toolbar`: 6 passed - `cargo fmt --check`: clean - `cargo clippy -p gpui-base --features test-support --all-targets -- --deny warnings`: nothing in `toolbar.rs`. It fails on `calendar.rs:132` (`nonminimal_bool`), which this PR does not touch and which also fails on `main` with rustc 1.95.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
There were two causes of the repeated-swipe freeze. Releasing a gesture in the middle of a list could start a spring bounce with no stretched edge. longbridge#3631 already fixes that by carrying release velocity only when the edge is stretched. This PR builds on that fix and addresses the remaining rounding case at large scroll offsets. longbridge#3632 filters out rounding remainders up to `0.01 px`. At large scroll offsets, `f32` rounding can leave a larger remainder even when the list is still far from an edge. For example, at `-1,000,000 px`, a `0.09 px` movement rounds to `0.0625 px`. `Overscroll` reads the remaining roughly `0.0275 px` as an edge, and later input stretches the content instead of moving through the list. `ScrollBounce` now reports the full delta as consumed when the requested position is within bounds. When an actual edge clamps the position, it still reports the distance moved. This removes the false remainder at its source and preserves real edge bounce and platform momentum. AI assisted with the implementation, regression and this description. --------- Co-authored-by: Jason Lee <[email protected]>
…e#3646) ## Problem A TextView selection is a band between two window points. In a table, a drag that starts in one cell and leaves the first line of its text flows row by row: it selects the rest of the row and every row below it. There is no way to select only the text of one cell. For example, in a table with the rows `Cell A | Cell B` and `Cell C | Cell D`, a drag from the start of `Cell A` to below the table copies `Cell A Cell B\nCell C Cell D`. ## Change - A drag that starts in a table cell selects only in that cell. When the pointer leaves the cell, the cursor is clamped to the cell edge, so the text from the anchor to the edge stays selected. - A drag that starts outside the table still selects through the table, as before. - Each table cell is wrapped in a private `TableCellElement`, laid out as its content. During prepaint, the cell that holds the selection anchor records itself in `GlobalState`. Prepaint runs before any text paints, so text in every view (also in other TextViews) knows the anchor cell. - Text, inline objects and custom blocks get their selection points through `TextViewState::painted_selection_points`, which applies the cell rule to the cell the text paints in. - The Copy projections from longbridge#3589 record the cell they painted in, and Copy applies the same rule. Without this, Copy re-projected the raw points and copied the whole rows. - `TextView::prepaint` now always pushes its state on the state stack, so a cell can read the anchor. The existing prepaint readers still check `max_lines` first. - Copy of a selection in one cell: in plain text, cells and rows with no selection add nothing (before, each row added a line of spaces). In Source format, a selection in only one cell is that cell's source, not a one-cell table. - The TextView page (en and zh-CN) says how a drag in a table cell behaves. No public API is added or changed. ## How to Test - `table_drag_from_a_cell_stays_in_that_cell` (new, `crates/base/src/text/text_view.rs`) drags in a real TextView: from a cell to below the table, back along a line into the previous cell, up and out of the table, from the paragraph before the table through it, and Copy before the frame that paints the last move. It checks both `selected_text()` and `TextSelection::selected_text`. It fails without the change (copies `Cell A Cell B\nCell C Cell D`), and its Copy-before-repaint case fails if only the projection part is removed. - Unit tests for the cell rule (`global_state::tests::table_cell`) and for the one-cell copy (`a_selection_in_one_table_cell_copies_only_that_cell`). - `cargo test -p gpui-base --features test-support`: 1,443 pass. `scrollable_mask::tests::horizontal_scroll_area_claims_only_steps_that_move` fails on a clean `main` too, and fails the same way with this change. - `cargo test -p gpui-kit --features test-support` passes. - `cargo fmt --check` and `cargo clippy -p gpui-base --features test-support --all-targets -- --deny warnings` pass. - Used in an application (a chat client that renders replies with TextView) on a 0.7.3 build with the same change: a drag from a cell to outside the table selects and copies only that cell. ## 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. The code and tests were written with Claude Code (AI) and reviewed by me. - [ ] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific). Not platform-specific; tested on macOS. --------- Co-authored-by: Jason Lee <[email protected]>
… a segment (longbridge#3655) ## Description Pressing a `TimeField` anywhere outside its digit segments did nothing. Only the segments listened for the mouse, so with a wide field (for example a form field set to `w_full`) most of its area was dead: a press beside `09 : 00` neither focused the field nor selected a segment. Segmented date/time inputs such as react-aria's `TimeField` (used by shadcn's date picker) focus the first segment when the field itself is pressed. - `gpui-base`: the field root now handles a left press by focusing the field and selecting its first segment. A press on a segment stops propagation after selecting that segment, so it still selects the segment it hit, not the first one. A disabled field does not handle the press, as before. - `gpui-component`: the base field now fills the styled frame (`size_full`), and the frame's horizontal padding moved onto the base field. The rendered result is the same, but a press anywhere inside the frame now reaches the field. This touches `gpui-base` because only the base state can select a segment (`select_segment` is private), and the press behavior belongs to the base field. ## Impact - Users of `TimeField`: the component itself, `DatePicker`'s time row and the `gpui-shell` retained-form `TimeField`. All three render the same component, so they all get the new behavior. Pressing a segment works as it did before. - `DatePicker` closes its popup with `on_mouse_up_out`, which runs in the capture phase and is based on pointer position. The new `stop_propagation` on segment mouse-down happens in the bubble phase, so closing the popup is not affected. The `DatePicker` frame is `flex_none` with no explicit width, so `size_full` on the inner field does not change its size. - The one new behavior a caller can observe: a `MouseDown` listener on an ancestor no longer receives presses that land on a segment. No such listener exists in this repository. - No public API changes. ## How to Test ```sh cargo test -p gpui-component --features test-support --lib time cargo test -p gpui-base --features test-support --lib time_field ``` `pressing_outside_the_segments_focuses_the_first_segment` presses the empty part of a 200px field (focused, hour selected), then the minute segment (minute selected), then the empty part again (hour selected). It fails without this change. Also checked in a downstream app (Allsum's scheduled task form): pressing the empty part of a full-width time field selects the hour. ## 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) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Jason Lee <[email protected]>
The valid Portuguese translations introduced by longbridge#3638 cause the pinned typos 1.40.0 checker to report three false positives on main ([failing Checks job](https://github.com/longbridge/gpui-kit/actions/runs/39989920902/job/120538735238)). Add three exact `pt-BR` expression exceptions, following longbridge#3608. Translations remain unchanged; no global word exemption or file exclusion is added. Validation with the official typos-cli 1.40.0 release, verified against its published SHA-256: - Full repository before: exactly three findings, exit 2; after: exit 0. - Negative controls still detect English `atual`, `Atual`, and `seperate` in source, English locale entries, and the locale file itself. - Translation bytes unchanged; `git diff --check` passes. - Independent review cleared the exact four-line patch. Rust tests were not run for this configuration-only change. Prepared with ChatGPT assistance on behalf of @huacnlee. This PR remains a draft for maintainer review.
## Description `HighlightStyle` cannot express inline code font size, borders, radius, or padding. Add one `inline_code_style(StyleRefinement)` override for typography and box presentation, covering both font-size customization and outlined code spans. Supersedes longbridge#3639. Unspecified properties inherit the resolved theme or explicit style. User overrides apply after surface-color adaptation, including on inverted surfaces. Existing `inline_code(HighlightStyle)` and `with_inline_code(HighlightStyle)` calls remain supported, and the public Component `TextViewStyle` fields are unchanged. Defaults retain the existing appearance: a 0.875 font-size ratio and no border. Shaping, wrapping, painting, and table column measurement share font-size and inset resolution. Code and prose stay separate when their font sizes match, preventing code backgrounds and padding from extending into adjacent prose. Backgrounds and borders fade with streamed text. English and Chinese documentation describe the supported properties. Line height remains paragraph-owned; margin, positioning, flex, explicit box dimensions, overflow, and interaction properties are not applied to inline code. ## Example ```rust use gpui_kit::{StyleRefinement, Styled as _, rems}; use gpui_kit::component::{ActiveTheme as _, text::TextView}; TextView::markdown("notes", "Run `cargo build` first.") .inline_code_style( StyleRefinement::default() .text_size(rems(1.)) .px(rems(0.25)) .border_1() .border_color(cx.theme().border), ) ``` ## Public API ### gpui-base - `TextViewStyle::with_inline_code_style(self, style: StyleRefinement) -> Self` — Set explicit inline code typography and box refinements. - `TextViewStyle::inline_code_style(&self) -> &StyleRefinement` — Read the explicit refinement. - `TextView::inline_code_style(self, style: StyleRefinement) -> Self` — Refine inline code over the resolved style. ### gpui-component - `text::TextView::inline_code_style(self, style: StyleRefinement) -> Self` — Apply the same override through the Component facade. ## Breaking Changes None for existing consumers. Existing HighlightStyle APIs, imports, defaults, and public Component style fields remain compatible. ## Validation - `cargo test -p gpui-base -p gpui-component --features gpui-component/test-support --lib text:: --locked` — 391 Base and 69 Component tests passed. - Tests cover equal-size code/prose separation, rem-based box measurement and wrapping at two zoom levels, style inheritance, inverted surfaces, invalid font sizes, and background/border fading. - `cargo clippy -p gpui-base -p gpui-component --features gpui-component/test-support --all-targets --locked -- --deny warnings` — passed. - `cargo fmt --all --check`, `git diff --check`, and whole-repository `typos` 1.40.0 — passed. - The typo dictionary now accepts the correct Portuguese word `atual`, resolving the unrelated CI false positive. Validation uses headless native text/layout tests. Windows/Linux rendering and performance were not checked locally. Implementation, tests, and documentation were written with AI assistance. --------- Co-authored-by: Jason Lee <[email protected]>
…longbridge#3667) Part of longbridge#3647 (items 1–3: container, header cells, body cells). ## Description `DataTable` only gave each row `Role::Row`. A screen reader found a flat list of unnamed rows: no table, no column headers, and no cell content, because GPUI only exposes text that is a `Text` with an id. This PR exposes the table's structure. - **Container:** `table-inner` is a `Grid`, or a `Table` when neither rows nor cells are selectable. It sets `aria_row_count(rows_count + 1)` and `aria_column_count(columns_count)`. The table is virtualized, so the counts cover every row, not just the rendered ones. - **Header:** the header is a `Row` at `aria_row_index(0)`. Each leaf header is a `ColumnHeader` named after its column (`aria_label(column.name)`), with its `aria_column_index`. The "Sort by …" button stays inside it. - **Body:** rows carry `aria_row_index(row_ix + 1)`. Each cell is a `Cell` with its `aria_column_index`. In cell selection mode it is a `GridCell` with `aria_selected`, which AccessKit's Windows adapter reports through `SelectionItemPattern`. - **Cell names:** `TableDelegate::cell_label` lets a delegate name its cells. It defaults to `None`, so existing delegates are unaffected. It is only called while `window.is_a11y_active()` (or under `test-support`), the same gate `InputState` uses for its value. That way a delegate's formatting costs nothing when no assistive technology is connected. - **Story:** the DataTable story implements `cell_label` with the text it already draws (`format_cell`). ## Public API ### gpui-component - `TableDelegate::cell_label(&self, row_ix: usize, col_ix: usize, cx: &App) -> Option<SharedString>`: the text a screen reader reads for a cell. Defaults to `None`. ## Screenshot UI Automation tree of `cargo run -p gpui-component-story -- datatable` (Windows 11, read with UIA's control view): | Before | After | | --- | --- | | 38 elements, 18 named. The table is 14 × `DataItem ""` plus 4 "Sort by" buttons. | 342 elements, 314 named. | ```text DataGrid "" (localized: "data grid") DataItem "" (header row) DataItem "ID" (localized: "column header") DataItem "Market" DataItem "Name" DataItem "Symbol" Button "Sort by Symbol" … DataItem "" (row) DataItem "0" DataItem "US" DataItem "Unity Software Inc." DataItem "U.US" … ``` ## How to Test - `cargo test -p gpui-kit --features test-support,component --test integration`: 419 passed. Two tests are new: - `table_exposes_its_header_and_cells_to_assistive_technology`: a `Grid` container, `ColumnHeader`s named "Name"/"Status", and a cell with the delegate's label. - `selectable_cells_report_their_selection`: `GridCell`s report `selected`. - Both fail without the `state.rs` change. - `cargo run -p gpui-component-story -- datatable` with Accessibility Insights for Windows or Narrator: the tree above. - `cargo clippy -p gpui-component -p gpui-component-story --all-targets -- -D warnings` and `cargo fmt --check` are clean. ## Not in this PR - **Row selection.** It still isn't reported on Windows. In longbridge#3647 I guessed this needed a selectable container. It's actually AccessKit: `accesskit_windows` (0.34 and 0.35) only exposes `SelectionItemPattern` for list, tree and tab items and for `GridCell`, with a `// TODO: tables (#29)` for rows. Cell selection works through `GridCell`. - **Keyboard focus** (item 4 of longbridge#3647): the table's focus still has no node. That probably interacts with longbridge#3183. - **Group headers** (`render_group_th`) and the row header column (`cell_selectable` + `row_header`) get no role yet. ## Open questions - **`Grid` vs `Table`:** I made the container a `Grid` whenever rows or cells are selectable (the default), and a `Table` otherwise. Happy to use one role for both. - **Row indices:** they count the header as row 0, the way Chromium does. Tell me if you'd prefer body rows to start at 0. - **`cell_label` naming:** fine as is, or would you prefer another name (e.g. `accessible_cell_text`)? I've only tested on Windows (UI Automation); I haven't tried VoiceOver on macOS. ## 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. - [x] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
Button.new("save").label("Save").accessibility_label("Save the current
document") currently throws even though Rust Button supports the builder
and the generated types permit it. Register the string method and replay
it into the real Button builder.
This is limited to Button. Refs longbridge#3662; other components and inherited
bridge APIs remain outside this change.
## Public API
### gpui-shell (JavaScript module `gpui-component`)
- `ButtonElement.accessibility_label(accessibility_label: string):
ButtonElement` — sets the accessible name independently of the visible
label. The descriptor now supplies the concrete generated TypeScript
declaration and validates the runtime argument.
## Validation
- Passed: `cargo fmt --all --check`, `git diff --check`, and `node
--check crates/shell/tests/types/run.mjs`.
- Added a mounted script regression comparing two buttons with the same
visible label and distinct accessible names, then checking the
explicitly named button's click callback.
- Added generated-declaration, fluent TypeScript, and missing/numeric
argument rejection checks.
- CI at `6cf4a75408a08e347e8b4825f260f95e6ab67410`: [GPUI Shell
Components
passed](https://github.com/longbridge/gpui-kit/actions/runs/40049044700/job/120737373318),
including 939 library tests (1 ignored), all 122 integration tests, the
mounted accessible-name/click regression, and generated TypeScript
contracts with runtime argument rejection checks.
- The semantic regression uses GPUI Fast's existing `GPUI_A11Y_ACTIVE=1`
test support in an isolated subprocess; non-Fast builds retain
script-call and click coverage.
- Local attempts at integration tests, Clippy, and the TypeScript runner
stopped at missing `glib-2.0` development files. CI supplies the
executed integration/type results above.
- Attempted the new regression against unchanged main production code
(`ecd2c8ff42439b73b87a238d02db10fe20376740`); the same dependency
failure prevented a behavioral negative control.
- Installing native dependencies failed with permission denied on
`/var/lib/apt/lists/partial`; no sudo is available. [Exact-head CI run
40049044700](https://github.com/longbridge/gpui-kit/actions/runs/40049044700)
is terminal: 17 jobs report success; Checks fails on the pre-existing
Portuguese `atual`/`Atual` typo false positives in unchanged
`crates/component/locales/ui.yml:646,661,676`. Shell Core (including
Clippy) passed.
- Windows Standard Runtime reports success, but its
[log](https://github.com/longbridge/gpui-kit/actions/runs/40049044700/job/120737373156)
contains three failures:
`llrt_pure_modules_execute_inside_the_shell_runtime` (incomplete deflate
stream), `every_fs_call_settles_through_a_promise` (round-trip
mismatch), and `websocket_reads_and_writes_text_and_binary_messages`
(empty binary message). All three have identical diagnostics in the
[base-main
job](https://github.com/longbridge/gpui-kit/actions/runs/39989920902/job/120538735303)
at `ecd2c8ff42439b73b87a238d02db10fe20376740`; they are outside this
Button change. The overall run is not green.
## Review and assistance
Implemented with ChatGPT assistance on behalf of @huacnlee.
Independently reviewed by a separate ChatGPT agent on behalf of
@huacnlee at commit `6cf4a75408a08e347e8b4825f260f95e6ab67410`: no
remaining source-review findings. This is an independent AI source
review, not a human review; executed CI evidence is listed above.
## Description Markdown table conversion currently pads every parsed row to the header width with default `TableCell` values. A wide header and many one-cell body rows therefore turn O(columns + rows) input into O(columns × rows) retained cells. Each synthesized cell owns a default `Paragraph` with Arc-backed state. This can exhaust an embedding application's memory when it renders attacker-influenced Markdown. Keep missing cells implicit and continue truncating excess cells to the GFM header width. The existing renderer iterates the shared column grid and reads optional cell content with `row.children.get(ix)`, so stored placeholder paragraphs are unnecessary. Add a bounded sparse-cell regression and update the ragged-row test to cover short rows, excess cells, and an explicitly empty middle cell. The code diff is confined to `crates/base/src/text/format/markdown.rs`: 48 insertions and 8 deletions. There are no public API changes. ## Before and after Measured retained `TableCell` counts from the actual production Markdown converter, using identical compact inputs: | Header columns | Body rows | Input bytes | Before | After | | ---: | ---: | ---: | ---: | ---: | | 32 | 64 | 452 | 2,080 | 96 | | 64 | 128 | 900 | 8,256 | 192 | | 128 | 256 | 1,796 | 32,896 | 384 | The new sparse-cell regression fails with the original padding and passes with the fix. No intentional OOM or large-case crash was attempted. Following previous contributions, [English report, raw logs, the temporary production-parser probe and reproduction script](https://github.com/lurenjia534/gpui-component/tree/41ca0e2caaddd78f832f818f8288dfbb18b64962/docs/evidence/sparse-markdown-table) are preserved at an immutable commit on a separate fork evidence branch. Evidence files are not included in this code PR. Exposure depends on a host rendering untrusted Markdown; this library is not itself a network service. This change removes the introduced retained parser-cell expansion. It does not establish a general document budget or bounded upstream parser, layout or rendering cost. Visual alignment and end-to-end hover interaction were not exercised. ## How to Test Validated on Linux against base `69a2077c` plus code commit `0e7a5806`: ```sh cargo test --locked --offline -p gpui-base --features test-support --lib \ text::format::markdown::tests::sparse_table_cell_count_tracks_source_cells -- --exact --nocapture cargo test --locked --offline -p gpui-base --features test-support --lib \ text::format::markdown::tests::table_rows_keep_missing_cells_implicit -- --exact --nocapture cargo clippy --locked --offline -p gpui-base --features test-support --lib --tests -- -D warnings cargo fmt --all --check git diff --check ``` Both regression tests, Clippy and formatting passed. The recorded regression invocations omitted `--locked`; Clippy used it. Omit `--offline` when dependencies are not cached. The existing Base tests require `test-support` in this checkout. The evidence branch's `docs/evidence/sparse-markdown-table/reproduce.py` temporarily restores the old sink, runs the same bounded probe before and after, confirms the expected regression failure/pass, and restores the source. Its complete reproduction run passed. ## AI Assistance AI assisted with analysis, patch review, bounded production-parser validation, English evidence, and commit/PR preparation. The submitted fix and regression tests originated in the supplied patch. Human maintainer review remains required. ## Checklist - [x] Read CONTRIBUTING and followed the repository's coding and design guidance. - [x] Reviewed the focused implementation and AI-assisted validation evidence. - [x] Targeted regressions, formatting and Base lib/tests Clippy passed. - [ ] Ran the affected native Story/example and checked visual alignment. - [ ] Performed macOS/Windows runtime verification.
…pshots (longbridge#3672) ## Description `ElementSnapshot` reads role, label, value and the state flags from an observed element's native node, but not its description or placeholder, so a UI test cannot check either on a painted element. Both reach assistive technology: Base's input editor writes `aria_placeholder` (gpui-component's `Input` passes its placeholder there), and the questionnaire and diff controls write `aria_description`. Today a test can only check them by building the element and calling `write_a11y_info` by hand, as `focus_trap.rs` and `lifecycle.rs` do, which reaches the outermost element and not one painted inside a view. GPUI's `Window::debug_a11y_tree_json()` includes both, but a test window never activates accessibility, so it returns `None`. This follows longbridge#3531, which added `disabled()` the same way. Snapshots now record `description` and `placeholder` from the node they already read `label` from. A frame with a semantic owner takes both from its editor, like its other semantic facts, so an `Input`'s placeholder is found through the component ID. We need this in an application's tests: a picker trigger with nothing chosen gives its prompt as a placeholder and no value, so a screen reader does not read the prompt as the choice, and a toggle without a visible label announces its description. ## Public API ### gpui-base - `test_support::ElementSnapshot::description(&self) -> Option<&str>` — the accessibility description, read after the label; it does not replace it. - `test_support::ElementSnapshot::placeholder(&self) -> Option<&str>` — the accessibility placeholder, the prompt shown while the element has no value. Both are also reachable through `gpui_kit::test::ElementSnapshot`. `ElementSnapshot`'s `Debug` output lists the two fields after `label`. ## How to Test - `snapshot_reads_description_and_placeholder` (`crates/kit/tests/integration/lifecycle.rs`) paints an observed `div` with a label, description and placeholder, and reads all three with no value. - `placeholder_is_read_from_the_editor_and_is_not_a_value` (`crates/kit/tests/integration/input.rs`) paints a component `Input` with a label and a placeholder, and reads the placeholder through the semantic owner with an empty value. - Negative controls: without the capture in `prepaint`, both tests fail; without the two fields in the semantic-owner snapshot, only the `Input` test fails. - The English and Chinese testing and accessibility guides list the two readers. - On this branch, macOS, Rust 1.99.1: `cargo test -p gpui-kit --features test-support,component --locked`, `cargo test -p gpui-base --features test-support --lib --locked`, `cargo clippy -p gpui-base -p gpui-kit --features gpui-kit/test-support,gpui-kit/component --all-targets --locked -- --deny warnings`, `cargo fmt --all --check` and `typos` 1.40.0. ## 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. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )