Skip to content

[pull] main from feigeCode:main - #18

Open
pull[bot] wants to merge 920 commits into
nonomal:mainfrom
feigeCode:main
Open

pull[bot] wants to merge 920 commits into
nonomal:mainfrom
feigeCode:main

Conversation

@pull

@pull pull Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

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 : )

@pull pull Bot locked and limited conversation to collaborators Aug 29, 2026
@pull pull Bot added the ⤵️ pull label Aug 29, 2026
huacnlee and others added 28 commits September 27, 2026 12:38
## 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]>
Bombatomica64 and others added 30 commits October 22, 2026 21:10
…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:

![Input above the keyboard on
Android](https://raw.githubusercontent.com/Bombatomica64/Gpui-android/main/docs/demos/safe-area-keyboard.png)

## 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.

![Focused input above the keyboard on
Android](https://raw.githubusercontent.com/Bombatomica64/Gpui-android/main/docs/demos/safe-area-keyboard.png)

## 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 |
| --- | --- |
| ![Before: the alert sits near the top of the
window](https://github.com/mastro993/gpui-kit/releases/download/pr-media-dialog-centered/dialog-near-top.png)
| ![After: the alert sits in the middle of the
window](https://github.com/mastro993/gpui-kit/releases/download/pr-media-dialog-centered/dialog-centered.png)
|

## 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]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.