Skip to content

kit: Prepare v0.7.2 - #3458

Merged
huacnlee merged 37 commits into
stablefrom
release/v0.7.2
Oct 12, 2026
Merged

huacnlee merged 37 commits into
stablefrom
release/v0.7.2

Conversation

@huacnlee

Copy link
Copy Markdown
Member

Description

Prepare v0.7.2 on stable from the PRs merged into main since v0.7.1, leaving out every change that breaks existing consumers. stable already carries #3371 and #3373; this adds the remaining compatible PRs with git cherry-pick -x and bumps the workspace to 0.7.2.

Included

Already on stable: #3371, #3373.

Excluded (Breaking Changes)

#3411, #3405 and #3396 have a Breaking Changes section that reads "None" and are included. #3376 only deprecates TooltipRequest::placement, and #3397 / #3382 change behavior compatibly, so they are included too.

Adaptations

Public API

No new public API beyond what the included PRs add; each lists its items in its own ## Public API section (notably #3375, #3376, #3378, #3388, #3403, #3404, #3405, #3411, #3420).

How to Test

  • cargo clippy --workspace --all-targets -- --deny warnings passes on Linux.
  • cargo test -p gpui-kit --features "test-support component assets" --test collections --test menu --test input passes on Linux.
  • The macOS-only rendering test (text: Paint TextView selection under the glyphs #3439 adds one case, the count is updated to 11) was not run.

AI-assisted: the backport, conflict resolution and test adaptations were done with Claude Code.

🤖 Generated with Claude Code

huacnlee and others added 30 commits October 12, 2026 11:49
Add an optional `gpui-fast` feature across GPUI Kit’s existing crates:

```toml
gpui-kit = { version = "0.7", features = ["gpui-fast"] }
```

No new crates. Preserve the existing `gpui-pre-*` workspace dependencies
and default backend; optional Fast dependencies and internal conditional
aliases select the engine. Existing application imports and macros
continue to work. This is additive, with no breaking changes. Upstream
packages still compile because Cargo features are additive.

## Public API

- **gpui-kit:** `gpui-fast` selects Fast for core, platforms, and
enabled layers.
- **gpui-base, gpui-component, gpui-kit-assets, gpui-fps, gpui-wry,
gpui-shell, gpui-component-shell:** `gpui-fast` selects the matching
backend for standalone consumers.
- **gpui-component-macros:** `#[derive(IntoPlot)]` additionally
recognizes direct Fast dependencies.
- Gallery and examples expose the same feature for comparisons.

## Validation

27 focused UI/macro/assets tests and 9 documentation tests pass.
Default/Fast configurations, examples, WebView, Shell, benchmarks, and
nightly WebAssembly compile. Clippy, formatting, dependency, and
snapshot-pin checks pass. CI adds native and WebAssembly coverage.

AI-assisted with Codex.

(cherry picked from commit 7eb5209)
…efaults` (#3376)

## Description

Managed tooltips (the ones components set up through `.tooltip()`, shown
by the per-window `TooltipOverlay`) had their timing hard-coded in
`gpui-base`: a 500 ms show delay and a 300 ms grace period. Applications
could not change either.

- **Application-wide:** `gpui_base::TooltipDefaults` holds the show
delay and grace period and is installed as a global, following the
`TextViewDefaults` pattern. `TooltipOverlay` reads it on every show and
hide request, so installing new defaults also applies to windows that
are already open. Without it, timing is unchanged.
- **Per trigger:** `TooltipRequest::with_show_delay` overrides the show
delay for one request. The components with built-in tooltips expose it
as `tooltip_show_delay`. A zero delay shows the tooltip immediately
instead of going through a timer.
- The grace period stays application-wide only: it governs moving from
one trigger to another, so a per-trigger value has no clear meaning.

```rust
gpui_kit::init(cx);
TooltipDefaults::new()
    .with_show_delay(Duration::from_millis(300))
    .with_grace_period(Duration::from_millis(200))
    .install(cx);

Button::new("help")
    .tooltip("Help")
    .tooltip_show_delay(Duration::ZERO)
```

Direct GPUI `.tooltip()` calls (including `Attachment`'s tooltip) bypass
`TooltipOverlay` and are not affected; they keep using GPUI's own
`.tooltip_show_delay()`. The docs say so.

`TooltipRequest`'s setters now use the `with_` prefix required for
public data types with non-boolean fields: `placement` becomes
`with_placement` and stays as a deprecated alias.

The Tooltip story gains a "Show delay" section with immediate, default
and 1 s triggers in one row. The English and Chinese Tooltip docs
(component and Base primitive) describe the timing and both settings.

This is an AI-assisted implementation.

## Screenshot

https://github.com/user-attachments/assets/46cf6320-a488-4517-b7af-a539d55d5028

## Public API

### gpui-base

- `gpui_base::TooltipDefaults` — application-wide timing for tooltips
shown through `TooltipOverlay`. `Clone + Copy + Debug + PartialEq + Eq +
Default`; implements `Global`.
- `TooltipDefaults::new() -> Self` — the Base defaults: 500 ms show
delay, 300 ms grace period.
- `TooltipDefaults::with_show_delay(self, delay: Duration) -> Self` —
sets how long the pointer must rest on a trigger before its tooltip
shows.
- `TooltipDefaults::with_grace_period(self, period: Duration) -> Self` —
sets how long a tooltip stays after the pointer leaves; entering another
trigger within it switches immediately.
- `TooltipDefaults::show_delay(&self) -> Duration` — reads the show
delay.
- `TooltipDefaults::grace_period(&self) -> Duration` — reads the grace
period.
- `TooltipDefaults::install(self, cx: &mut App)` — installs the defaults
for the whole application.
- `TooltipDefaults::global(cx: &App) -> Self` — returns the installed
defaults, or the Base ones when none were installed.
- `TooltipRequest::with_show_delay(self, delay: Duration) -> Self` —
overrides `TooltipDefaults::show_delay` for this request.
- `TooltipRequest::with_placement(self, placement: Placement) -> Self` —
prefers a side for the tooltip; replaces `placement`.
- `TooltipRequest::placement` — now `#[deprecated]`, forwarding to
`with_placement`.

### gpui-component

- `gpui_component::tooltip::TooltipDefaults` — re-export of
`gpui_base::TooltipDefaults`.
- `Button::tooltip_show_delay(self, delay: Duration) -> Self` —
overrides the show delay of the button's tooltip.
- `Toggle::tooltip_show_delay(self, delay: Duration) -> Self` — same,
for `Toggle`.
- `Switch::tooltip_show_delay(self, delay: Duration) -> Self` — same,
for `Switch`.
- `Checkbox::tooltip_show_delay(self, delay: Duration) -> Self` — same,
for `Checkbox`.
- `Radio::tooltip_show_delay(self, delay: Duration) -> Self` — same, for
`Radio`.
- `Clipboard::tooltip_show_delay(self, delay: Duration) -> Self` — same,
for `Clipboard`.
- `InputGroupButton::tooltip_show_delay(self, delay: Duration) -> Self`
— same, for `InputGroupButton`.

## Breaking Changes

`TooltipRequest::placement` is deprecated in favor of `with_placement`.
It still compiles and behaves the same, with a deprecation warning.

```diff
- TooltipRequest::new(bounds, build).placement(Placement::Right)
+ TooltipRequest::new(bounds, build).with_placement(Placement::Right)
```

## How to Test

- `cargo test -p gpui-base --lib tooltip` — 5 passed, including the new
`show_delay_follows_defaults_and_request_override`: with a 100 ms
default the tooltip is absent at 99 ms and present at 100 ms, and a zero
`with_show_delay` shows it synchronously.
- `cargo test -p gpui-component --lib` — 603 passed
(`test_button_builder` covers `tooltip_show_delay`).
- `RUSTFLAGS="-D warnings" cargo clippy -p gpui-base -p gpui-component
--lib --tests -- --deny warnings` and `cargo clippy -p
gpui-component-story -- --deny warnings` — passed.
- `cargo fmt --check` — passed.
- `cargo run`, open **Tooltip → Show delay** on macOS: Immediate shows
at once, Default after about 0.5 s, Slow after about 1 s, and moving
between them within the grace period switches without waiting.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c0bebdc)
## Summary

Add a readonly, virtualized patch viewer for application-supplied
unified and Git diff text. `DiffFile::parse` prepares one immutable file
per change; one retained `DiffState` displays all files. Applications
own patch loading, review comments, commands and saving resolved text.

- Unified by default, optional Split, source-coordinate selection/copy,
keyboard scrolling and change navigation, folding and filename-based
syntax highlighting.
- Optional inline word or grapheme emphasis, soft wrapping, header
renderers, line/file annotations and pointer callbacks.
- Whole-file reference previews and working files with two-way or diff3
merge conflicts. Readonly conflict descriptors expose line ranges and
labels; resolution choices remain in state and the application writes
the resulting text.
- Runtime state setters preserve selection and nearby source position.
Clearing a user line selection reports `SelectionChanged(None)` without
inserting a clear into a replacement gutter gesture.
- JavaScript registration, native Story examples, regression/UI
integration fixtures and synchronized English/Chinese usage
documentation. Base remains unchanged.

## Screenshot

![Diff Story — external patch in Unified
layout](https://raw.githubusercontent.com/longbridge/gpui-kit/89137c417928339bc00aa0fca871e356c6f4cf42/docs/assets/diff-unified.png)

Native Story Gallery on Linux; external sparse hunks with an
application-owned review comment.

## Validation

Current API review fixes passed `cargo clippy -p gpui-component -p
gpui-component-story -p gpui-kit --features test-support --tests --test
diff -- --deny warnings`, `cargo fmt --all --check` and `git diff
--check`.

Regression and UI integration tests were compiled, not executed, per
repository/user configuration. Coverage includes conflict source
coordinates (including empty parts), clear-event delivery versus Shift
extension, runtime emphasis invalidation and selection/viewport
preservation. No new visual or cross-platform runtime verification was
performed for these fixes.

## Public API

All additions below are exposed through `gpui_kit::component::diff`.
Method signatures use `Self` within the named type; imported GPUI types,
`Range`, and traits have their usual meaning. Struct fields remain
private.

### gpui-component

`pub mod diff` is the public component seam. It re-exports
`DiffConflict`, `DiffConflictResolution`, `DiffFile`, `DiffFileStatus`,
`DiffLinePosition`, `DiffLineRange`, `DiffSide`, `DiffParseError`,
`DiffInlineUnit`, `DiffEvent` and `DiffState`; presentation types below
are declared directly in that module.

#### DiffSide

`pub enum DiffSide { Original, Modified }` — Identify the original or
modified source side.

#### DiffMode

`pub enum DiffMode { Split, Unified }` — Choose split or unified layout
(default Unified).

#### DiffFileStatus

`pub enum DiffFileStatus { Added, Deleted, Modified, Renamed, Copied,
Unchanged, Conflicted }` — Describe patch changes, unchanged reference
files or conflicted working files.

#### DiffInlineUnit

`pub enum DiffInlineUnit { Word, Character }` — Choose inline word or
grapheme comparison (default Word).

#### DiffHoverHighlight

`pub enum DiffHoverHighlight { None, Line, LineNumber, Both }` — Choose
pointer hover emphasis (default None).

#### DiffHunkSeparator

`pub enum DiffHunkSeparator { Metadata, LineInfo, Simple }` — Choose
hunk heading presentation (default Metadata).

#### DiffChangeIndicator

`pub enum DiffChangeIndicator { Signs, Bars, None }` — Choose change
markers (default Signs).

#### DiffConflictResolution

`pub enum DiffConflictResolution { Current, Incoming, Both }` — Choose
the current, incoming or both conflict alternatives.

#### DiffEvent

`pub enum DiffEvent { SelectionStarted(DiffLineRange),
SelectionChanged(Option<DiffLineRange>), SelectionEnded(DiffLineRange),
FileExpanded(SharedString), FileCollapsed(SharedString),
ConflictResolved(SharedString, usize) }` — Report user selection
gestures, file disclosure and conflict resolutions.

#### DiffParseError

`pub struct DiffParseError; // private fields` — Report a malformed or
unsupported patch with its patch line and diagnostic.

| Signature | Purpose |
| --- | --- |
| `DiffParseError::line(&self) -> usize` | Read line. |
| `DiffParseError::message(&self) -> &SharedString` | Read message. |

#### DiffFile

`pub struct DiffFile; // private fields` — Retain immutable file data
parsed from a patch or working-file preview.

| Signature | Purpose |
| --- | --- |
| `DiffFile::parse(patch: &str) -> Result<Vec<Self>, DiffParseError>` |
Parses every file in a unified or Git diff, in patch order, without
computing differences. |
| `DiffFile::with_language(self, language: impl Into<SharedString>) ->
Self` | Overrides the syntax language detected from the path, using a
highlighter language name such as `rust` or `javascript`. |
| `DiffFile::language(&self) -> SharedString` | The syntax language: the
override, or the one detected from the path. |
| `DiffFile::status(&self) -> DiffFileStatus` | How the file changed. |
| `DiffFile::path(&self) -> &SharedString` | The path to show for this
file: the modified path, or the original path of a deleted file. |
| `DiffFile::original_path(&self) -> Option<&SharedString>` | The
original path, or `None` for an added file. |
| `DiffFile::modified_path(&self) -> Option<&SharedString>` | The
modified path, or `None` for a deleted file. |
| `DiffFile::extended_headers(&self) -> &[SharedString]` | Git extended
header lines, such as file modes, similarity, renames and the binary
marker, as they appear in the patch. |
| `DiffFile::is_binary(&self) -> bool` | Whether the patch reports
binary contents rather than textual hunks. |
| `DiffFile::additions(&self) -> usize` | Read additions. |
| `DiffFile::deletions(&self) -> usize` | Read deletions. |
| `DiffFile::has_changes(&self) -> bool` | Whether the file changes,
including an added or deleted empty file and metadata-only changes such
as a mode change or pure rename. |
| `DiffFile::conflicts(&self) -> &[DiffConflict]` | Merge conflicts in
working-file order; slice indices identify resolutions. |
| `DiffFile::unchanged(path: impl Into<SharedString>, text: &str) ->
Self` | A file shown in full, for reference, without changes. |
| `DiffFile::parse_conflicts(path: impl Into<SharedString>, text: &str)
-> Result<Self, DiffParseError>` | Parses a working file containing Git
conflict markers, including the `\|\|\|\|\|\|\|` base section of `diff3`
style. |

#### DiffConflict

`pub struct DiffConflict; // private fields` — Inspect one parsed
conflict; all ranges are one-based with an exclusive end and may be
empty.

| Signature | Purpose |
| --- | --- |
| `DiffConflict::lines(&self) -> Range<usize>` | One-based working-file
lines, including markers, with an exclusive end. |
| `DiffConflict::current_lines(&self) -> Range<usize>` | Current-side
source lines, with an exclusive end; may be empty. |
| `DiffConflict::base_lines(&self) -> Option<Range<usize>>` | Base
source lines for diff3, with an exclusive end; may be empty. |
| `DiffConflict::incoming_lines(&self) -> Range<usize>` | Incoming-side
source lines, with an exclusive end; may be empty. |
| `DiffConflict::current_label(&self) -> &SharedString` | The label from
the opening marker. |
| `DiffConflict::base_label(&self) -> Option<&SharedString>` | The base
marker label, or None for a two-way conflict. |
| `DiffConflict::incoming_label(&self) -> &SharedString` | The label
from the closing marker. |

#### DiffLinePosition

`pub struct DiffLinePosition; // private fields` — Identify a one-based
source line by file path and side.

| Signature | Purpose |
| --- | --- |
| `DiffLinePosition::new(path: impl Into<SharedString>, side: DiffSide,
line: usize) -> Self` | Creates a position, clamping the line to at
least one. |
| `DiffLinePosition::path(&self) -> &SharedString` | The
`DiffFile::path` of the file. |
| `DiffLinePosition::side(&self) -> DiffSide` | Read side. |
| `DiffLinePosition::line(&self) -> usize` | Read line. |

#### DiffLineRange

`pub struct DiffLineRange; // private fields` — Describe an inclusive
source-line range, optionally crossing sides in patch order.

| Signature | Purpose |
| --- | --- |
| `DiffLineRange::new(path: impl Into<SharedString>, side: DiffSide,
start: usize, end: usize) -> Self` | Creates a range on one side,
clamping the lines to at least one. |
| `DiffLineRange::with_end_side(self, side: DiffSide) -> Self` | Ends
the range on `side`, keeping `end` as its line on that side. |
| `DiffLineRange::path(&self) -> &SharedString` | The `DiffFile::path`
of the file. |
| `DiffLineRange::side(&self) -> DiffSide` | The side `start` is on. |
| `DiffLineRange::start(&self) -> usize` | Read start. |
| `DiffLineRange::end_side(&self) -> DiffSide` | The side `end` is on;
the same as `Self::side` unless set. |
| `DiffLineRange::end(&self) -> usize` | Read end. |

#### DiffAnnotation

`pub struct DiffAnnotation; // private fields` — Identify
application-owned content attached to a line or file.

| Signature | Purpose |
| --- | --- |
| `DiffAnnotation::line(id: impl Into<ElementId>, position:
DiffLinePosition) -> Self` | Attaches content beneath a source line. |
| `DiffAnnotation::file(id: impl Into<ElementId>, path: impl
Into<SharedString>) -> Self` | Attaches content beneath the header of
the file at `path`. |
| `DiffAnnotation::id(&self) -> &ElementId` | Read id. |
| `DiffAnnotation::path(&self) -> &SharedString` | The `DiffFile::path`
of the annotated file. |
| `DiffAnnotation::position(&self) -> Option<&DiffLinePosition>` | The
annotated line, or `None` for a file annotation. |

#### DiffState

`pub struct DiffState; // private fields` — Retain focus, viewport,
context, selection, resolutions and prepared emphasis.

| Signature | Purpose |
| --- | --- |
| `DiffState::new(files: impl IntoIterator<Item = DiffFile>, cx: &mut
Context<Self>) -> Self` | Creates a unified viewer with three unchanged
lines around each change. |
| `DiffState::with_mode(self, mode: DiffMode) -> Self` | Sets the
initial display mode. |
| `DiffState::with_context_lines(self, lines: Option<usize>) -> Self` |
Sets the initial unchanged context around each change. |
| `DiffState::with_expansion_lines(self, lines: usize) -> Self` | Sets
how many lines an up or down disclosure reveals. |
| `DiffState::with_min_collapsed_lines(self, lines: usize) -> Self` |
Sets the shortest unchanged range that collapses; shorter ranges stay
visible. |
| `DiffState::with_inline_unit(self, unit: Option<DiffInlineUnit>) ->
Self` | Sets the unit inline changes between paired lines are emphasized
in, or `None` to emphasize whole lines only. |
| `DiffState::with_inline_max_line_length(self, length: usize) -> Self`
| Sets the longest line, in bytes, compared for inline changes. |
| `DiffState::with_syntax_max_line_length(self, length: usize) -> Self`
| Sets the longest line, in bytes, given syntax emphasis. |
| `DiffState::set_expansion_lines(&mut self, lines: usize, cx: &mut
Context<Self>)` | Changes the number of lines each disclosure reveals,
preserving selection and scrolling. |
| `DiffState::set_min_collapsed_lines(&mut self, lines: usize, cx: &mut
Context<Self>)` | Changes the shortest unchanged range that collapses,
preserving selection and scrolling. |
| `DiffState::set_inline_unit(&mut self, unit: Option<DiffInlineUnit>,
cx: &mut Context<Self>)` | Changes the inline comparison unit without
resetting selection or scrolling. |
| `DiffState::set_inline_max_line_length(&mut self, length: usize, cx:
&mut Context<Self>)` | Changes the inline comparison line limit in bytes
without resetting selection or scrolling. |
| `DiffState::set_syntax_max_line_length(&mut self, length: usize, cx:
&mut Context<Self>)` | Changes the syntax emphasis line limit in bytes
without resetting selection or scrolling. |
| `DiffState::files(&self) -> &[DiffFile]` | The files in patch order. |
| `DiffState::mode(&self) -> DiffMode` | Read mode. |
| `DiffState::context_lines(&self) -> Option<usize>` | Read context
lines. |
| `DiffState::expansion_lines(&self) -> usize` | Read expansion lines. |
| `DiffState::min_collapsed_lines(&self) -> usize` | Read min collapsed
lines. |
| `DiffState::inline_unit(&self) -> Option<DiffInlineUnit>` | Read
inline unit. |
| `DiffState::inline_max_line_length(&self) -> usize` | Read inline max
line length. |
| `DiffState::syntax_max_line_length(&self) -> usize` | Read syntax max
line length. |
| `DiffState::selected_lines(&self) -> Option<DiffLineRange>` | Read
selected lines. |
| `DiffState::is_file_collapsed(&self, path: &str) -> bool` | Whether
the file at `path` shows only its header. |
| `DiffState::set_files(&mut self, files: impl IntoIterator<Item =
DiffFile>, cx: &mut Context<Self>)` | Replaces the files and resets
expansion, selection and viewport. |
| `DiffState::set_mode(&mut self, mode: DiffMode, cx: &mut
Context<Self>)` | Changes the display mode while preserving source-line
selection and a nearby source row. |
| `DiffState::set_context_lines(&mut self, lines: Option<usize>, cx:
&mut Context<Self>)` | Sets surrounding context. |
| `DiffState::expand_unchanged(&mut self, cx: &mut Context<Self>)` |
Reveals all unchanged source while preserving the configured context
count. |
| `DiffState::collapse_unchanged(&mut self, cx: &mut Context<Self>)` |
Restores the configured context disclosures. |
| `DiffState::set_file_collapsed(&mut self, path: &str, collapsed: bool,
cx: &mut Context<Self>)` | Collapses the file at `path` to its header,
or expands it, without emitting a user event. |
| `DiffState::conflict_resolution(&self, path: &str, ix: usize) ->
Option<DiffConflictResolution>` | The resolution of conflict `ix` in the
file at `path`, if any. |
| `DiffState::resolve_conflict(&mut self, path: &str, ix: usize,
resolution: Option<DiffConflictResolution>, cx: &mut Context<Self>)` |
Resolves conflict `ix` (an index in `DiffFile::conflicts`) in the file
at `path`, or clears its resolution with `None`, without emitting a user
event. |
| `DiffState::resolved_text(&self, path: &str) -> Option<String>` | The
working file with every conflict replaced by its resolution, or `None`
while a conflict is unresolved or `path` names no conflicted file. |
| `DiffState::set_selected_lines(&mut self, range:
Option<DiffLineRange>, cx: &mut Context<Self>)` | Synchronizes
source-line selection without emitting a user event. |
| `DiffState::selected_text(&self, cx: &App) -> String` | Returns
selected source without gutters, diff markers or annotation content. |
| `DiffState::scroll_to_line(&mut self, position: DiffLinePosition, cx:
&mut Context<Self>)` | Reveals and scrolls to a source line, expanding
its file and hidden context if needed. |
| `DiffState::scroll_to_file(&mut self, path: &str, cx: &mut
Context<Self>)` | Scrolls to the header of the file at `path`, a
`DiffFile::path`. |
| `DiffState::next_change(&mut self, cx: &mut Context<Self>)` | Moves to
the next changed group after the current viewport, across files. |
| `DiffState::previous_change(&mut self, cx: &mut Context<Self>)` |
Moves to the previous changed group before the current viewport, across
files. |

#### Diff

`pub struct Diff; // private fields` — Render a themed readonly viewer
from retained state.

| Signature | Purpose |
| --- | --- |
| `Diff::new(state: &Entity<DiffState>) -> Self` | Construct
presentation for retained state. |
| `Diff::line_number(self, value: bool) -> Self` | Shows the original
and modified line-number lanes. |
| `Diff::syntax_highlight(self, value: bool) -> Self` | Emphasizes
syntax once the state has prepared it. |
| `Diff::header_visible(self, visible: bool) -> Self` | Shows a header
above each file. |
| `Diff::hover_highlight(self, highlight: DiffHoverHighlight) -> Self` |
Emphasizes the line, its number, or both under the pointer. |
| `Diff::hunk_separator(self, separator: DiffHunkSeparator) -> Self` |
Marks the start of each hunk. |
| `Diff::change_indicator(self, indicator: DiffChangeIndicator) -> Self`
| Marks changed lines beside the code. |
| `Diff::change_background(self, value: bool) -> Self` | Tints changed
lines. |
| `Diff::soft_wrap(self, value: bool) -> Self` | Wraps long lines at the
column width instead of scrolling horizontally. |
| `Diff::annotations(self, annotations: impl IntoIterator<Item =
DiffAnnotation>) -> Self` | Supplies annotations with stable identities
and targets. |
| `Diff::render_annotation<F, E>(self, render: F) -> Self where F:
Fn(&DiffAnnotation, &mut Window, &mut App) -> E + 'static, E:
IntoElement` | Renders each supplied annotation. |
| `Diff::render_header<F, E>(self, render: F) -> Self where F:
Fn(&DiffFile, &mut Window, &mut App) -> E + 'static, E: IntoElement` |
Replaces the content of each file header, keeping its shell, separator
and collapse control. |
| `Diff::render_header_prefix<F, E>(self, render: F) -> Self where F:
Fn(&DiffFile, &mut Window, &mut App) -> E + 'static, E: IntoElement` |
Inserts content before the path in the default header. |
| `Diff::render_header_title_suffix<F, E>(self, render: F) -> Self where
F: Fn(&DiffFile, &mut Window, &mut App) -> E + 'static, E: IntoElement`
| Inserts compact content immediately after the path in the default
header. |
| `Diff::render_header_suffix<F, E>(self, render: F) -> Self where F:
Fn(&DiffFile, &mut Window, &mut App) -> E + 'static, E: IntoElement` |
Inserts trailing content after the change statistics in the default
header. |
| `Diff::on_add_annotation<F>(self, handler: F) -> Self where F:
Fn(&DiffLineRange, &mut Window, &mut App) + 'static` | Shows an add
button beside the hovered line and at the end of the selected lines. |
| `Diff::on_line_click<F>(self, handler: F) -> Self where F:
Fn(&DiffLinePosition, &ClickEvent, &mut Window, &mut App) + 'static` |
Handles a click on a line's code, outside a text-selection drag. |
| `Diff::on_line_hover<F>(self, handler: F) -> Self where F:
Fn(Option<&DiffLinePosition>, &mut Window, &mut App) + 'static` |
Reports the line under the pointer, or `None` when the pointer leaves
it. |

Public trait implementations: `DiffState: EventEmitter<DiffEvent> +
Focusable`; `Diff: Styled + RenderOnce + IntoElement`; `DiffParseError:
std::fmt::Display + std::error::Error`. The extensible enums other than
`DiffSide` are `#[non_exhaustive]`. Data types implement the
Clone/Debug/equality/hash traits shown in source; `DiffMode`,
`DiffInlineUnit`, `DiffHoverHighlight`, `DiffHunkSeparator` and
`DiffChangeIndicator` also implement `Default`. Navigation action
structs are private; Copy and Select All reuse Input's action identities
without adding public re-exports.

### gpui-component-story

| Signature | Purpose |
| --- | --- |
| `pub struct DiffStory; // private fields` | Registered viewer showcase
and review example. |
| `DiffStory::view(window: &mut Window, cx: &mut App) -> Entity<Self>` |
Construct the retained showcase. |
| `pub use diff_story::DiffStory` in `stories` | Expose the registered
showcase. |
| `impl Story for DiffStory` | Register the showcase title, description
and construction. |
| `impl Render for DiffStory` | Render the showcase. |

### gpui-component-shell / JavaScript and generated TypeScript

The host registers `Diff` and `DiffState` under `gpui-component`. `Diff`
accepts no children and honors Shell layout styling. State methods are
not registered in JavaScript.

| Signature | Purpose |
| --- | --- |
| `export interface DiffState { readonly __gpuiComponentState: unique
symbol }` | Opaque retained patch state. |
| `export function DiffState(patch: string, mode?: "unified" \|
"split"): DiffState;` | Parse supplied patch text and create state. |
| `export const Diff: { new(state: DiffState): DiffElement };` |
Construct the viewer builder. |
| `DiffElement.line_number(line_number: boolean): DiffElement` | Toggle
line numbers. |
| `DiffElement.syntax_highlight(syntax_highlight: boolean): DiffElement`
| Toggle syntax emphasis. |
| `DiffElement.header_visible(header_visible: boolean): DiffElement` |
Toggle file headers. |
| `DiffElement.change_background(change_background: boolean):
DiffElement` | Toggle changed-line background. |
| `DiffElement.soft_wrap(soft_wrap: boolean): DiffElement` | Toggle
wrapping. |
| `DiffElement.hunk_separator(hunk_separator: "metadata" \| "line_info"
\| "simple"): DiffElement` | Choose hunk heading presentation. |
| `DiffElement.change_indicator(change_indicator: "signs" \| "bars" \|
"none"): DiffElement` | Choose change markers. |

Generated `DiffElement` declaration (native methods omitted here are
explicitly
unavailable, rather than inherited):

```typescript
export type DiffElement = Omit<NativeElement,
  "line_number" | "syntax_highlight" | "header_visible" | "change_background" |
  "soft_wrap" | "hunk_separator" | "change_indicator" | "disabled" | "selected" |
  "on_click" | "role" | "transition"> & {
  line_number(line_number: boolean): DiffElement;
  syntax_highlight(syntax_highlight: boolean): DiffElement;
  header_visible(header_visible: boolean): DiffElement;
  change_background(change_background: boolean): DiffElement;
  soft_wrap(soft_wrap: boolean): DiffElement;
  hunk_separator(hunk_separator: "metadata" | "line_info" | "simple"): DiffElement;
  change_indicator(change_indicator: "signs" | "bars" | "none"): DiffElement;
  disabled(unavailable: never): never;
  selected(unavailable: never): never;
  on_click(unavailable: never): never;
  role(unavailable: never): never;
  transition(unavailable: never): never;
};
```

### gpui-kit

No new root API. All Rust Diff APIs are reachable through the existing
`gpui_kit::component` re-export.

## Scope and limitations

No repository loading, full-file comparison, source editing, context
hydration or combined merge diffs. Missing patch source remains
unavailable; selection/copy cannot reconstruct a whole file. Binary
payloads are skipped and shown as metadata. Syntax context is limited to
supplied hunks; injected languages are not highlighted. Visible long
lines are chunked but not horizontally virtualized. Conflict indices
belong to the parsed file revision; replacing files clears resolutions.

## AI assistance

Implementation, Story, documentation, regression/UI integration coverage
and API review fixes were developed with Codex assistance.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9808ec1)
GPUI Kit's optional Fast backend links both GPUI Pre and GPUI Fast on
Windows. Both embed the same manifest resource, so applications fail
with `CVT1100`. Use GPUI Fast's new `windows-manifest-provided-by-host`
feature to keep Pre's identical manifest as the single embedded copy.
Kit's existing default and `default-features = false` behavior stays
unchanged.

Uses the published GPUI Fast 0.1.1 release from [GPUI Fast PR
#40](longbridge/gpui-fast#40). Cargo.lock pins
all nine GPUI Fast packages to 0.1.1 so CI builds with `--locked`.

Implementation assisted by Codex.

(cherry picked from commit c1bda59)
## Description

An app's UI tests could not find a `Progress` bar or the `TitleBar` with
`TestWindowExt::find`. They asserted neighbouring text instead, or the
tests were `#[ignore]`d. Neither control registered its element for the
test harness, although both already have stable IDs. `Progress` also
already sets its accessibility role and label.

On `main`:

- Base `Progress` holds a plain `gpui::Stateful<Div>` built with
`div().id(id)` (`crates/base/src/progress.rs:13`, `:24`). It sets
`Role::ProgressIndicator`, the label and the numeric value, but nothing
registers it. Component `Progress`
(`crates/component/src/progress/progress.rs:120`) and `ProgressCircle`
(`crates/component/src/progress/progress_circle.rs:160`) build on it, so
neither could be found.
- `TitleBar` gives `title-bar`, `window-controls` and the caption
buttons (`minimize`, `maximize`/`restore`, `close`) element IDs, but
does not call `.test_support()` on them
(`crates/component/src/title_bar.rs:209`, `:255`, `:269`, `:278`, `:330`
on `main`).

This PR adds the existing `.test_support()` registration to these
elements, as Base `Button` and other controls already do:

- `crates/base/src/progress.rs:13,24`: the root becomes
`crate::ObservedElement<gpui::Stateful<Div>>`, built with
`div().id(id).test_support()`. This is the same pattern as
`crates/base/src/button.rs:22,41`. Without the `test-support` feature,
`ObservedElement<E>` is `E`, so normal builds keep the exact type they
had before.
- `crates/component/src/title_bar.rs`: `.test_support()` on the caption
buttons (`:211`), on all three `window-controls` returns (`:257`,
`:271`, `:281`) and on `title-bar` (`:354`). On `title-bar`, the call
comes after the two `on_double_click` calls, because
`InteractiveElementExt` is implemented only for `Stateful<E>`
(`crates/base/src/event.rs:60`). A short comment there says why.

This is a change to `gpui-base`, but only to register an element that
already exists. It follows how Base `Button`, `Select`, `Accordion` and
others are registered. Nothing else changes: no roles, labels or locale
keys are added. The `TitleBar` elements still have no accessibility role
or name.

The table of observable controls in `website/docs/test.md` and
`website/zh-CN/docs/test.md` gets rows for `Progress / ProgressCircle`
and `TitleBar`.

Where caption buttons appear: on macOS and the web, `WindowControls`
renders an empty `window-controls` and no buttons. On Linux it draws
them only under client-side decorations. GPUI's test window uses
server-side decorations (the `PlatformWindow::window_decorations`
default), so in tests the buttons are found only on Windows.

This change was written with AI assistance (Claude Code).

## How to Test

Two new tests in `crates/kit/tests/components.rs`:

- `progress_and_progress_circle_are_observable` renders a `Progress` and
a loading `ProgressCircle`. It checks that `find(id)` gives
`Role::ProgressIndicator`, the accessibility label, and a visible
element.
- `title_bar_and_window_controls_are_observable` renders a `TitleBar`.
It checks that `title-bar` is found and visible and that
`window-controls` is found. On Windows it also checks that `minimize`,
`maximize` and `close` are visible inside `window-controls`. On other
platforms it checks that `close` is absent, because the title bar does
not draw caption buttons there.

Both tests failed before the fix (`missing ElementId Name("download")`
and `missing ElementId Name("title-bar")`, with no registered paths) and
pass after it.

Commands run on macOS (aarch64):

```sh
cargo test -p gpui-kit --features test-support,component --test components   # 6 passed
cargo test -p gpui-base --lib progress                                        # base Progress a11y tests
cargo test -p gpui-component --lib progress                                   # component Progress builder tests
cargo test -p gpui-component --lib title_bar
cargo fmt --all --check
cargo clippy -p gpui-base -p gpui-component -- -D warnings -A clippy::nonminimal_bool
cargo clippy -p gpui-kit --features test-support,component --tests -- -D warnings -A clippy::nonminimal_bool
```

I ran clippy with `-A clippy::nonminimal_bool` because my local clippy
(Rust 1.95) reports two `nonminimal_bool` errors at
`crates/base/src/calendar.rs:132`. This PR does not touch that file.
There are no other clippy findings.

I did not run the Windows branch of the TitleBar test (the
caption-button assertions). It compiles only on Windows, so CI is the
first run of it.

## Checklist

- [x] 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.~~ Not
applicable: the change only registers existing elements for the test
harness. It does not change layout or rendering.
- [ ] ~~Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)~~ Not applicable: no platform-specific code
changes. The test's caption-button assertions run only on Windows.

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

https://claude.ai/code/session_01PRpJSFk3zTwdgAJHehdZFm

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 4921e5b)
Fixes #3336

## Description

Opening an input's framework-rendered context menu used to hide the
selected text and focus ring. The input now retains both while its
associated menu or submenu holds keyboard focus. Submenu editing actions
also use the nearest parent menu's explicit action target, so commands
such as Select All operate on the source Textarea.

- The shared input engine associates selection visibility with a weak
popup focus handle, without retaining the popup or showing the caret
while the menu has focus.
- Input publishes its state during right-button pointer dispatch.
ContextMenu resolves ownership during deferred menu construction, after
the clicked input has taken focus. This also handles right-clicking a
previously unfocused input and preserves ownership when replacing an
open menu.
- Input, Textarea, Editor and InputGroup use the same association for
their focus appearance. Unrelated menus and focus in another input do
not retain the source's focus appearance.
- PopupMenu resolves actions and shortcut hints against the nearest
explicit action context in its parent chain. A submenu's own explicit
target takes precedence.
- English and Chinese Menu documentation describe focus appearance and
submenu action-target inheritance.

This targets framework-rendered context menus. Completion and
code-action overlays remain outside this change.

AI-assisted implementation and validation: Claude Code and OpenAI Codex.

## Public API

### `gpui-base`

Added to `gpui_base::input::InputBaseState<M>`, shared by InputState,
TextareaState and EditorState:

- `pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx:
&mut Context<Self>)`: associates a popup with selection visibility using
a weak focus handle; `None` clears it.
- `pub fn has_selection_focus(&self, window: &Window, cx: &App) ->
bool`: reports whether the input or its associated popup/submenu
currently has focus.

### `gpui-component`

- Existing `PopupMenu::action_context(self, handle: FocusHandle) ->
Self`: sets the action target; descendant submenus without their own
explicit target now use the nearest parent's target for action dispatch
and shortcut hints.

## Breaking Changes

Public signatures remain compatible. The behavior of
`PopupMenu::action_context` extends to descendant submenus that do not
specify their own target. Existing explicit submenu targets retain
precedence. Applications no longer need to repeat the parent's target on
each submenu:

```diff
 menu.action_context(target.clone())
     .submenu("More", window, cx, move |submenu, _, _| {
-        submenu.action_context(target.clone())
-            .menu("Select all", Box::new(SelectAll))
+        submenu.menu("Select all", Box::new(SelectAll))
     })
```

## Validation

Latest validation on Linux:

```bash
cargo test -p gpui-kit --features test-support --test menu --locked
cargo clippy -p gpui-component --lib --no-deps --locked -- -D warnings
cargo clippy -p gpui-kit --features test-support --test menu --no-deps --locked -- -D warnings
git diff --check
```

All 6 menu tests pass. Regression coverage includes right-clicking an
unfocused input, automatic/explicit menu ownership, menu replacement,
keyboard submenus, dismissal, clipboard Copy, typing in another input,
and clicking submenu Select All. The Select All regression verifies the
complete source selection, source keyboard focus after dismissal, and
unchanged destination content. Pointer down/up events use separate app
updates to match native event ordering.

Formatting checks pass for the affected Rust files. A native Linux
preview with the default Input menu, custom Textarea submenu and a
destination Input was launched for interactive testing. macOS and
Windows remain for CI.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 114d707)
## Description

Add a desktop Git history browser inspired by tig as a complete Diff
example. Keep commits and changed files in a resizable left sidebar,
with source as the main work area. Default to Dark appearance and Split
layout; retain syntax highlighting, wrapped source, added/deleted
backgrounds and inline emphasis.

Refine the command surfaces and containment: commit-body disclosure sits
immediately after the subject, copying sits beside the hash, Refresh
belongs to history, and compact change-navigation/settings buttons share
the metadata row. The settings icon opens a top-right anchored menu
containing layout, appearance and display choices. Commit details,
sidebar headings and the source frame have theme-token borders.
Changed-file rows have more vertical padding, file icons and aligned
status/statistics. Commit bodies scroll independently and reset on
commit changes.

The sidebar toggle lives at the left of the status bar (Cmd/Ctrl+B).
Status reports actual commit/file position and selection; shortcut hints
use Kbd resolved from Actions. There are no function-key bindings.
Startup `--commit`, `--file` and `--line` arguments reproduce a real
source change without changing repository contents or limiting
subsequent navigation.

Fix source spacing when line numbers are disabled: rendered row insets
now match the width budget, annotation anchors include the inset, and an
empty Split cell uses the source row height. Wrapped rows remain aligned
and selectable when line numbers change.

Fix Diff wheel handling so vertical wheel input scrolls rows instead of
being remapped to horizontal scrolling. Reuse the existing horizontal
scroll mask. Clearing gutter selection now emits
`SelectionChanged(None)` once without interrupting Shift extension.

Complete the additive review API: expose parsed conflict
coordinates/labels, add runtime presentation setters, and provide
renderer names matching GPUI Kit conventions. Existing renderer names
remain supported with unchanged signatures. This builds on #3378; it
does not repeat the dependency update from #3380. English and Chinese
Diff documentation are synchronized.

## Screenshot

<img width="2100" height="1290" alt="image"
src="https://github.com/user-attachments/assets/ecf65e9d-0efb-4ce7-8906-7c3ab00ec319"
/>

Reproduce with:

```sh
cargo run -p example-tig -- --commit 4890b1c --file crates/component/src/speech/waveform.rs --line 70 .
```

Earlier screenshots are retained: [initial
preview](https://raw.githubusercontent.com/longbridge/gpui-kit/diff-review-example/examples/tig/screenshots/before.png)
and [intermediate
preview](https://raw.githubusercontent.com/longbridge/gpui-kit/diff-review-example/examples/tig/screenshots/unified.png).

## Public API

### gpui-component (`gpui_kit::component::diff`)

- `DiffConflict` (`Clone`, `Debug`) — parser-owned conflict snapshot
with private fields.
- `DiffConflict::lines(&self) -> Range<usize>` — one-based, half-open
source range including markers.
- `DiffConflict::current_lines(&self) -> Range<usize>` — current-side
source range, possibly empty.
- `DiffConflict::base_lines(&self) -> Option<Range<usize>>` — optional
diff3 base source range.
- `DiffConflict::incoming_lines(&self) -> Range<usize>` — incoming-side
source range, possibly empty.
- `DiffConflict::current_label(&self) -> &SharedString` — opening marker
label.
- `DiffConflict::base_label(&self) -> Option<&SharedString>` — optional
base marker label.
- `DiffConflict::incoming_label(&self) -> &SharedString` — closing
marker label.
- `DiffFile::conflicts(&self) -> &[DiffConflict]` — source-ordered
snapshots; slice indices address conflict resolution.
- `DiffState::set_expansion_lines(&mut self, lines: usize, cx: &mut
Context<Self>)` — update incremental context expansion size.
- `DiffState::set_min_collapsed_lines(&mut self, lines: usize, cx: &mut
Context<Self>)` — update the context folding threshold while retaining
the viewport anchor.
- `DiffState::set_inline_unit(&mut self, unit: Option<DiffInlineUnit>,
cx: &mut Context<Self>)` — switch word/grapheme emphasis or disable it.
- `DiffState::set_inline_max_line_length(&mut self, length: usize, cx:
&mut Context<Self>)` — update the inline preparation limit and
invalidate earlier preparation.
- `DiffState::set_syntax_max_line_length(&mut self, length: usize, cx:
&mut Context<Self>)` — update the syntax preparation limit and
invalidate earlier preparation.
- `Diff::render_annotation<F, E>(self, render: F) -> Self` — render
application-owned annotation content.
- `Diff::render_header<F, E>(self, render: F) -> Self` — replace header
content while retaining its shell and collapse control.
- `Diff::render_header_prefix<F, E>(self, render: F) -> Self` — insert
content before the path.
- `Diff::render_header_title_suffix<F, E>(self, render: F) -> Self` —
insert content directly after the path.
- `Diff::render_header_suffix<F, E>(self, render: F) -> Self` — insert
trailing content after statistics.

For annotation renderers, `F: Fn(&DiffAnnotation, &mut Window, &mut App)
-> E + 'static`; header renderers use `&DiffFile` instead. All require
`E: IntoElement`. Existing `annotation_content`, `header`,
`header_prefix`, `header_title_suffix`, and `header_suffix` keep their
signatures as compatibility aliases.

## How to Test

```sh
cargo run -p example-tig -- .
```

Select commits with Up/Down or j/k, use [ / ] to change files, Enter to
focus source, and Escape to return to history. Use n/Shift+N to navigate
changes and the settings menu to disable wrapping before checking
vertical versus horizontal wheel gestures.

Validated locally on Linux:

- `cargo test -p example-tig --locked` — five tests, including
production-view navigation, startup file selection, default Dark
appearance, independent message scrolling and stale-result rejection.
- `cargo test -p gpui-kit --features test-support --test diff --locked`
— all 16 Diff UI tests, including wheel axes, gutter event ordering and
wrapped-source selection with hidden/toggled line numbers.
- Clippy with warnings denied for Component, Kit, Story and the example,
including test targets.
- Standard and `gpui-fast` example builds/checks; Component wasm check.
- Format and whitespace checks; repository smoke checks for history, an
empty bare repository and an invalid path.
- Native Dark Split capture of a real SpeechWaveform change showing
wrapped Rust, inline emphasis and added/deleted backgrounds; earlier
standard/minimum-size inspections are retained.

No full test suite was run. macOS and Windows runtime behavior was not
tested locally. The `typos` executable is not installed locally.

AI-assisted implementation and review.

(cherry picked from commit dc00da1)
Follows up #3373 (which fixed #3368).

## Description

Since #3373, a press and release inside a block `MarkdownPlugin` paints
the whole block as selected whenever the pointer moves even one pixel
between mouse down and mouse up. `custom_block_is_selected` accepted any
`start != end` selection with an endpoint inside the block, and the
existing test asserted `(inside, inside + 1px)` as selected.

In practice this breaks interactive block plugins such as cards with
clickable rows or charts: quick clicks routinely travel a few pixels, so
clicking two items in a row leaves the card filled with the selection
color. Because the block's visible bounds are registered as text bounds,
the endpoint counts as a text hit and the selection survives mouse up,
so the highlight stays until the next click elsewhere. A drag-distance
threshold would not be enough. During rapid clicks the pointer is
already moving toward the next target, and it easily travels more than
GPUI's 2px drag threshold.

This change makes the block atomic. The selection includes the block
once it crosses the block's edge (or on Select All), and never from a
gesture whose endpoints are both inside the block.

- Selections that start or end in the block and extend outside it still
copy it whole (`as_text()` in Plain, `to_markdown()` in Source), which
is the #3368 scenario.
- Text-only selections and click-only presses still exclude it.
- **Behavior change:** a drag that starts and ends inside the same block
no longer selects it. #3373 also supported this case (dragging entirely
inside a formula). To copy a block, drag across one of its edges.

The change is limited to `custom_block_is_selected` in
`crates/base/src/text/node.rs`, plus its geometry test.

## Screenshot

Rendered headlessly with GPUI's Metal renderer. A selectable `TextView`
holds `Text above the card.`, a block plugin card shaped like the
markdown example's `UserCard`, and `Text below the card.`. Each capture
is taken after mouse up, and the gesture is dispatched as real mouse
down/move/up events. The before column uses the parent commit, and the
after column uses this branch.

| Gesture | Before | After |
| --- | --- | --- |
| Click on the card body, pointer travels 3px between down and up |
![Before: the whole card stays filled with the selection
color](https://raw.githubusercontent.com/ylinwind/gpui-component/0734f739dbc48006711cc7e69c0f54684df0ce31/before-click.png)
| ![After: no
selection](https://raw.githubusercontent.com/ylinwind/gpui-component/0734f739dbc48006711cc7e69c0f54684df0ce31/after-click.png)
|
| Drag from the paragraph above into the card (#3368 case, unchanged) |
![Before: the card is selected with the text
above](https://raw.githubusercontent.com/ylinwind/gpui-component/0734f739dbc48006711cc7e69c0f54684df0ce31/before-drag.png)
| ![After: the card is still selected with the text
above](https://raw.githubusercontent.com/ylinwind/gpui-component/0734f739dbc48006711cc7e69c0f54684df0ce31/after-drag.png)
|

To reproduce manually:

1. Run `cargo run --locked -p example-markdown` and scroll to the
`<UserCard id="huacnlee" />` block.
2. Press on the card body (not the Follow button, which consumes mouse
down), move the pointer a few pixels, and release. Before: the card
stays filled with the selection color until the next click elsewhere.
After: nothing is selected.
3. Drag from the paragraph above into the card, and from the card into
the text below. In both cases the card is selected, and Cmd/Ctrl+C
copies it, before and after.

## How to Test

- `cargo test --locked -p gpui-base --lib text::`: all passed (337 on
the base revision). The geometry test now asserts that 1px pointer
travel and a corner-to-corner drag inside the block select nothing, in
both directions, and that the three edge-crossing cases still select.
- `cargo clippy --locked -p gpui-base --lib --tests -- -D warnings` and
`cargo fmt -p gpui-base -- --check`: passed.
- Built a downstream app (Longbridge AI chat) against this change via
`[patch]`. Its test suite passes. Manual verification in that app is
still pending; these are the cases to check:
  1. Click quickly on rows inside a block card. No selection appears.
2. Drag from text above into the card, and from the card into text
below. The card is selected, and Cmd+C copies its text.
3. Select across the whole card. The card is included in the copied
text.
  4. Select text only. The card is excluded.

## 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 <noreply@anthropic.com>
(cherry picked from commit 58d078b)
…3309)

## Problem

A themed markdown table frame is rounded — `base_text_view_style` puts
the theme `radius.md` on `style.table()` — but gpui clips children with
a **rectangular** content mask. The header row's square background quad
therefore poked out of the rounded frame as a bare right-angle corner at
the table's corners.

Visible on any theme whose header fill differs from the page background.
The demo screenshots below run `examples/markdown_table` (`cargo run -p
markdown_table`) with a visible `table_head` fill.

## Before / after

Top-left corner of a regular table, zoomed 8x (base caf830c vs this
branch):

| before | after |
| --- | --- |
|
![before](https://raw.githubusercontent.com/longbridge/gpui-kit/ihavecoke/assets-pr-3309/corner-before.png)
|
![after](https://raw.githubusercontent.com/longbridge/gpui-kit/ihavecoke/assets-pr-3309/corner-after.png)
|

## Full demo windows

### Before
<img width="900" height="732" alt="window-before"
src="https://github.com/user-attachments/assets/45bc97d9-4a02-42f3-8bf4-17291016b7b2"
/>

### After
<img width="900" height="732" alt="window-after"
src="https://github.com/user-attachments/assets/8f59648d-2ed1-4759-9cdd-4a0f7fb57824"
/>

## Fix

Round the first row's top and the last row's bottom corners to the frame
radius from `style.table()`, inset by the 1px frame border, in both the
wrap and the horizontal-scroll table layouts (`table_row_corner_radii`
in `node.rs`).

Known limit (commented in code): when the table overflows and scrolls
horizontally, the viewport edge still slices the row track square at the
clipped side — only a rounded content mask could clip that, and gpui
masks are rectangular:

![scroll
limitation](https://raw.githubusercontent.com/longbridge/gpui-kit/ihavecoke/assets-pr-3309/corner-scroll-limitation.png)

## Test

`table_row_backgrounds_follow_the_frame_corner_radius` renders a
rounded-frame table in both layouts and asserts the header background
quad carries the frame radius minus the border on its top corners and
stays square on the bottom ones. The test fails on the parent commit and
passes on this branch.

- `cargo nextest run -p gpui-base table` — 35 passed
- `cargo clippy -p gpui-base --locked -- --deny warnings` / `cargo fmt
-- --check` — clean

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9acf47c)
Closes #3392

## Description

A finger has no right button, so `ContextMenuExt` could not be opened on
a touch-only device. A long press on the trigger now opens the same menu
at the press position, using GPUI's `LongPressEvent` the same way the
plot tooltip (#3078) and touch selection (#3073) do.

- The body of the right-click handler moved into `open_menu`, which both
listeners call; the only edit is `event.position` becoming a `position`
parameter. With whitespace changes hidden in Files changed, the change
to `context_menu.rs` is small. Since the moved body is no longer nested,
`paint` now reads `shared_state` from `request_layout` (the same `Rc`
that `with_element_state` returned).
- The long-press listener is registered **before** the trigger's
children paint, so in the bubble phase it runs after theirs. An `Input`
inside the trigger claims the long press first (`prevent_default`),
keeps its word selection and edit menu, and the context menu stays shut.
- Right-click is unchanged.

A selectable `TextView` inside a context-menu trigger keeps its
long-press word selection, drag handles, and edit menu. Before claiming
a long press, the trigger queries the existing window selection geometry
through `TextSelection::is_selectable_at`; a press over blank space
still opens the object menu. The query uses the active selection scope
and existing hitboxes rather than treating the whole TextView bounds as
text.

## Public API

### gpui-base

- `TextSelection::is_selectable_at(position: Point<Pixels>, window:
&Window, cx: &mut App) -> bool`: reports whether the press hits
selectable text in the active selection scope, so a containing control
can yield to window-level touch selection without starting or changing a
selection.

Call this query during pointer dispatch with the event position;
hitboxes reflect the current event. No existing API signatures change.

## Screenshot

Physical Android phone (OnePlus CPH2581, Android 16), the lab's
context-menu demo: a long press on the dashed box, then a tap on the
first item. Kit 0.7.0 with and without this diff (backported).

**Before**

![before](https://raw.githubusercontent.com/Bombatomica64/Gpui-android/46d963305b26de6bfabaaf6c556403dbd5aa1a97/docs/demos/pr-longpress-before.gif)

**After**

![after](https://raw.githubusercontent.com/Bombatomica64/Gpui-android/46d963305b26de6bfabaaf6c556403dbd5aa1a97/docs/demos/pr-longpress-after.gif)

Before: nothing happens. After: the menu opens on the long press, and
tapping Copy fires its `on_click` (`context: Copy` in the event log).
On the same build, a long press inside an `Input` still selects the word
with handles and the Cut/Copy/Paste/Select All menu.
Builds:
[demo-pr3393](https://github.com/Bombatomica64/Gpui-android/releases/tag/demo-pr3393).

## How to Test

- `cargo test -p gpui-kit --features test-support,component --test menu
--test touch_selection`: 10 menu tests + 13 touch-selection tests passed
locally on macOS.
- The new `long_press_on_text_in_a_context_menu_trigger_selects`
regression test failed against the original PR head (empty selection
instead of `quick`) and passes with the fix; it also checks that
touch-selection handles remain available and no object context menu
opens.
- `long_press_on_blank_space_in_a_text_trigger_opens_the_menu` verifies
that blank space in the same selectable text row still opens the object
menu.
- Existing right-click, nested-menu, Input selection, Copy/Select All,
handle dragging, cached-view selection, and scrolling tests remain
covered by the two integration test targets.
- `cargo fmt --all --check` and `git diff --check`: clean.
- `cargo clippy -p gpui-base -p gpui-component -p gpui-kit --features
gpui-kit/test-support,gpui-kit/component --tests -- -D warnings`: clean.
- The physical Android recordings above demonstrate the original
long-press opening path. The TextView-priority follow-up has automated
macOS coverage; it has not been rechecked on a physical Android device.

## 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: no desktop session here; see How to Test.)
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific) (not run on desktop; the new path is
touch-only, and right-click is covered by the existing tests)

Thanks for taking the time to review this.

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

The follow-up text-selection fix and its regression tests were generated
with Codex and validated with the checks above.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 1af2d1f)
## Description

Short table columns in `TextView` often wrap a single trailing character
onto its own line (`冷启动(毫秒` + `)`, `Memo` + `ry`). The wrap layout sized
columns by UTF-8 byte length, and the scroll layout shrank every column
by the same ratio, so a column that nearly fit lost its last character.

Both layouts now size columns from measured text width. When the table
is wider than its frame, narrow columns keep their full width and only
the widest ones shrink and wrap. Widths are also rounded up to the
device pixel, and headers are measured with the `table_head` /
`table_cell` styles they render with.

**Behavior change:** column widths in the default wrap layout change for
most tables.

Not in this PR: breaks before full-width closing punctuation (`)`) and
after opening punctuation (`(`) come from GPUI's line wrapper and need a
GPUI change.

The `markdown_table` example gets a "Column fitting" section with the
same table in Chinese and English.

## Screenshot

`cargo run -p markdown_table` at 640px wide.

| Table | Before | After |
| --- | --- | --- |
| English, wrap | <img width="640" height="440"
alt="table-en-before-wrap"
src="https://github.com/user-attachments/assets/f842cb34-65ee-4190-aa49-b1b2d8c93c36"
/> | <img width="640" height="440" alt="table-en-after-wrap"
src="https://github.com/user-attachments/assets/ddf5ac5c-1373-4b76-a9db-61c1108ecfcb"
/> |
| English, scroll | <img width="640" height="440"
alt="table-en-before-adaptive"
src="https://github.com/user-attachments/assets/e0369e63-7af5-46e7-9d50-8341d0b7f3d9"
/> | <img width="640" height="440" alt="table-en-after-adaptive"
src="https://github.com/user-attachments/assets/6ea609ca-992f-4be7-aa2d-45da8ed64be6"
/> |
| Chinese, wrap | <img width="640" height="440"
alt="table-zh-before-wrap"
src="https://github.com/user-attachments/assets/316ec94e-59c5-469f-be10-d296e5ad6c0e"
/> | <img width="640" height="440" alt="table-zh-after-wrap"
src="https://github.com/user-attachments/assets/663589de-c591-4871-911f-91a8558b3d3a"
/> |
| Chinese, scroll | <img width="640" height="440"
alt="table-zh-before-adaptive"
src="https://github.com/user-attachments/assets/ac55f40e-09a3-476f-ac00-65d8f816a73b"
/> | <img width="640" height="440" alt="table-zh-after-adaptive"
src="https://github.com/user-attachments/assets/d0d6ab7a-72b4-4813-aecc-6d4700aa7c71"
/> |

## How to Test

- `cargo test -p gpui-base --lib`: passed. The new
`table_keeps_narrow_columns_on_one_line_while_wide_ones_wrap` fails on
the parent commit.
- `cargo run -p markdown_table`, scroll to "6. Column fitting", switch
layouts and narrow the 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.
- [x] 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 <noreply@anthropic.com>
(cherry picked from commit 3cc2d0d)
Fixes #3334

## Description

With Normal groups, Settings placed its horizontal page padding outside
`gpui::list`. The controls kept their expected inset, but outward focus
rings crossed the list's clip bounds: Switch and Checkbox lost the right
edge, and a full-width Input lost both edges.

Move the existing `.px_4()` into a full-width wrapper around each list
item. The wrapper owns the page inset separately from `SettingGroup`'s
own horizontal padding. Keep the existing group rendering and vertical
spacing.

This changes only `crates/component/src/setting/page.rs`: 7 insertions
and 5 deletions. The list still clips content that scrolls outside its
viewport.

AI-assisted implementation and validation with OpenAI Codex, submitted
after contributor approval.

## Screenshot

The same native Settings Story before and after the fix. Crops are
enlarged 4× using nearest-neighbor scaling; the original screenshots are
linked below.

![Before and after: complete focus rings for Normal Switch, Checkbox and
Input, including a narrow dark
window](https://raw.githubusercontent.com/lurenjia534/gpui-component/bb9c5fce7b0222943882063b7ab0921868a98eae/comparison-normal.png)

[Full-size screenshots and capture
metadata](https://github.com/lurenjia534/gpui-component/tree/bb9c5fce7b0222943882063b7ab0921868a98eae)

## How to Test

```bash
cargo run --locked -p gpui-component-story -- Settings
```

1. Set Group Variant to Normal and focus Dark Mode, Auto Switch Theme,
and Other → CLI Path.
2. Compare Fill and Outline, then check Normal in a narrow dark window.
3. Focus a Switch and scroll it outside the content viewport; its ring
must remain clipped at the viewport boundary.

Native visual verification on Linux/X11 with Xvfb, Mesa Lavapipe, and
display scale 1 compared baseline `dc00da18` against `426d7c54`:

| Scenario | Result |
| --- | --- |
| Normal: Switch, Checkbox, Input; 1440 × 1000, light | Missing
focus-ring edges restored; other pixels identical |
| Normal: Switch; 1000 × 800, dark | Missing right focus-ring edge
restored; other pixels identical |
| Fill and Outline | Before/after screenshots pixel-identical |
| Focused Switch scrolled outside the viewport | Before/after
screenshots pixel-identical |

Checks passed:

```bash
cargo check --locked -p gpui-component --lib
cargo fmt -p gpui-component -- --check
cargo clippy --locked -p gpui-component --lib -- --deny warnings
cargo build --locked -p gpui-component-story
git diff --check
```

Both baseline and patched Story binaries were built and launched for the
comparisons. macOS, Windows, interface zoom, and fractional display
scaling were not exercised.

## Checklist

- [x] Read the contributing, design, and coding guides; kept the PR
focused.
- [x] Reviewed the focused diff and the AI-assisted before/after
verification.
- [x] Built and launched the affected Settings Story on Linux.
- [ ] Cross-platform performance verification.

(cherry picked from commit 952202f)
…l offset (#3400)

## Problem

#3309 rounds the table's first and last rows, which only lines up while
the content sits at its unscrolled position. Once a wide table scrolls
horizontally, gpui's rectangular content mask slices the clipped side
square into the frame's rounded corner notches — the known limit noted
in #3309.

## Fix

Add `RoundedFrameCover`, a sibling overlay (like `ScrollableMask`)
pinned to the viewport: it fills each corner notch — inside the frame's
rect corner, outside the border arc — with a backdrop color, so the
frame reads as a rounded clip at every scroll offset. The frame's own
border paints after its children, so it stays crisp on top of both the
scrolled content and the covers; nothing is repainted.

`horizontal_scroll_area` installs the cover from the caller style's
corner radii, so markdown tables get it automatically. The element is
exported from gpui-base — `RoundedFrameCover::uniform(radius)`,
`.backdrop(color)`, `.frame_border(width)` — so application frames with
their own rounded scroll viewports can reuse it; `frame_border` grows
the notches out to the border box when the cover is a child of the
bordered element itself.

### Before
<img width="308" height="217" alt="image"
src="https://github.com/user-attachments/assets/780173b3-7225-407b-bd79-6d2ed679a784"
/>

### After
<img width="276" height="273" alt="image"
src="https://github.com/user-attachments/assets/3f75bc6b-2be8-4d60-98b7-36c353b9541d"
/>

## Test

- `corner_notch_paths_stay_inside_their_corner_squares` pins the notch
geometry: each path tessellates to an r-sized square anchored at its
frame corner.
- `cargo nextest run -p gpui-base scrollable_mask` — 17 passed.
- `cargo clippy -p gpui-base --locked -- --deny warnings` / `cargo fmt
-- --check` — clean.
- Verified end-to-end in a consuming application via a local patch:
markdown tables and an application-owned scrollable card keep rounded
corners through horizontal scrolling.

(cherry picked from commit 72ec173)
## Description

`Dialog` exposes `margin_top` to move the surface down from the default
top offset (a tenth of the viewport height), but `AlertDialog` offers no
way to change it. Every alert therefore sits pinned near the top of the
window, even a one-line confirm in a tall window where a mid-window
placement would read better, and there is no way for an app to line an
alert up with where its `Dialog`s rest.

This adds a `margin_top` setter on `AlertDialog` that passes through to
the underlying `Dialog`, mirroring the other pass-through setters
(`keyboard`, `close_button`).

Hit while building [SkillStar](https://github.com/xxww0098/SkillStar), a
GPUI Kit app whose confirmation dialogs wanted a mid-window placement:
the natural component for a confirm is `AlertDialog`, but without
`margin_top` every confirm floated near the top edge.

## Public API

- `gpui_component::dialog::AlertDialog::margin_top(margin_top: impl
Into<Pixels>) -> Self` — overrides the default top offset (a tenth of
the viewport height) of the alert surface.

## How to Test

`cargo test -p gpui-component --lib dialog`

The new `margin_top_moves_the_alert_surface_off_the_default_offset` test
opens a confirm alert with `margin_top(100px)` in a 600px-tall window
and asserts the painted surface rests at y≈100 instead of the default
60. (The alert chrome carries fractional text metrics — the surface is
128.5px tall here — so the painted origin is asserted within a
subpixel.)

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

Co-authored-by: xxww0098 <shefkie1957@gmail.com>
(cherry picked from commit 3751aef)
…3403)

Multi-cursor editing exists internally (Alt-click, the
AddCursorAbove/Below actions), but applications had no public API to
place a cursor at an arbitrary byte range. That makes features like
"select all occurrences" (VS Code's Cmd+Shift+L) or additive "select
next occurrence" impossible to build on top of the editor.

This change adds a single public method,
`InputBaseState::add_selection`, the programmatic equivalent of
Alt-clicking another cursor into the text.

Behavior mirrors the existing internal paths:
- The range is clipped to the text and expanded to character boundaries,
like `set_selected_range`.
- A range already covered by a selection adds nothing (Alt-click is
idempotent the same way).
- On a single-line input there is no second cursor, so the call falls
back to replacing the selection.
- The view scrolls to the new selection, matching `set_selected_range`
via `move_to`.

## Public API

### gpui-base
- `InputBaseState::add_selection(&mut self, range: Range<usize>, cx:
&mut Context<Self>)` — Adds `range` as an additional selection while
keeping existing ones, enabling programmatic multi-cursor placement
(e.g. select-all-occurrences).

---
This PR is 100% AI-generated, reviewed and validated by the contributor:
`cargo fmt --check`, `cargo clippy -p gpui-base --all-targets -- --deny
warnings`, and the input test suite (329 passed, including the new
`test_add_selection`) are all green.

---------

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit c7b5b48)
## Problem

Scrolling a long chat session in Allsum used 35–45 % process CPU here
(50–60 % in the original report), most of it laying out and shaping
message rows that had only moved. Part of the cause was in gpui-kit:
selectable text, the text selection layer, `Root` and inputs **wrote**
entities and globals on every frame they drew, even when nothing about
the selection or focus changed.

Under GPUI Fast's retained views a write is a change, so every view that
read any of these (every message row with a `TextView`) was built again
on the next frame, and the transcript list could not stay on its scroll
layer.

Per-frame writes found:

| Where | Write every frame |
|---|---|
| `TextSelectionHandle::register` | `WindowSelectionState` updated with
the participant's geometry |
| `TextView` prepaint / paint | `GlobalState::global_mut` to push/pop
the text view stack, `begin_selection_frame` |
| `update_runs` / `set_hit_test_runs` | participant entity updated with
its painted runs |
| `TextSelectionLayer` | registry global set again; touch frame shifted;
finish-frame flag updated |
| selection publishing | `set_snapshot` on every participant, changed or
not |
| scroll / mouse-move handlers | state updated on every event, selecting
or not |
| `Root` / `InputState` | focus registry re-synced |

## Changes

- With no selection, gesture or touch selection active, a participant
records its geometry without updating the state (`register_quietly`,
`finish_frame_quietly`).
- The text view stack and selection document order are interior state of
`GlobalState` (`RefCell`/`Cell`), read through `GlobalState::global`.
Painted runs and copy text are interior state of the participant.
- Snapshots are published only to participants whose snapshot differs.
The registry, active scope, focus sync and the mouse-move and scroll
handlers write only when something changed.
- Touch handles are recorded per participant and replaced when that
participant paints them again. Before, they expired with frames the
selection layer drew; once the layer's view is retained it may not draw,
so stale handle bounds made a tap on the text ignored.
- Test fix: `a_flow_is_laid_out_again_at_another_width` changed the
view's width without `cx.notify()`. It passed only because the per-frame
writes rebuilt the view anyway.

## Before / after

Allsum release build (macOS, Apple silicon), long session scrolled by
the wheel for 30 s, process CPU, together with longbridge/gpui-fast#44:

| Scroll | before | after |
|---|---|---|
| slow | 35.3–36.4 % | **31.2–33.4 %** |
| fast | 43.0–43.6 % | **37.0–37.7 %** |

Main thread over 12 s: `List::prepaint` 1374 → 451 ms,
`StateInner::layout_items` 1218 → 84 ms, `compute_layout` 879 → 180 ms.
With only the gpui-fast change, the list still fell off its layer,
because these writes kept rebuilding the rows.

## Testing

- New test `redrawing_selectable_text_leaves_its_readers_retained` (GPUI
Fast): a view reading the selection state beside selectable text is not
rendered again when the text redraws. It fails without this change (13
renders instead of 5).
- `cargo test -p gpui-base --lib`: 1317 passed. `cargo test -p
gpui-component --lib`: 649 passed.
- `cargo test -p gpui-base --lib --features gpui-fast`: the same 14
tests fail as on `main`, and no new ones. Before the touch-handle
change, `long_press_release_keeps_handles_which_drag_the_selection`
failed here.
- `cargo clippy -p gpui-base -p gpui-component --tests -- --deny
warnings` (with and without `gpui-fast`) and `cargo fmt --check` are
clean.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ab7b54f)
Following your review of #3385, the sort icon stays the control for
changing a column's sort, and a header click still selects the column.
On `main` that control is invisible to assistive technology and to the
kit's UI test harness. `render_sort_icon` draws `div().id(("icon-sort",
col_ix))` with an `on_click`, but with no role, no label and no
`test_support()`. Screen readers get no sort button, and
`TestWindowExt::find(("icon-sort", 0))` panics with a missing
`ElementId`. So applications that test sorting call
`TableDelegate::perform_sort` directly instead of clicking.

This PR gives the icon `test_support()`, `Role::Button` and a localized
label that names the column
(`crates/component/src/table/state.rs:1615-1618`). The new
`Table.SortBy` key is "Sort by %{column}", with zh-CN, zh-HK and zh-TW
entries (`crates/component/locales/ui.yml:378-383`). It follows the Diff
line-number button, which is also a plain clickable div with
`Role::Button` and a `t!(..)` label. The column name is in the label, so
a screen-reader user can tell the sort buttons apart.

The interaction is unchanged. A related observation, left as it is: a
click on the icon also bubbles to the header's `on_click`, so it sorts
the column and selects it. The new test records that current behaviour.
If you'd rather the icon click stop propagation, that's a one-line
follow-up.

The current sort direction is not exposed. gpui-pre 0.3.8 has no
sort-direction setter on `div`, and AccessKit's `SortDirection` would
belong on a column-header role, which the header does not have yet.

This change was written with AI assistance (Claude Code).

None. No public API changes; the sort icon gains a role and a label.

A new UI test,
`table_sort_icon_is_a_labelled_button_that_cycles_the_sort`, in
`crates/kit/tests/collections.rs`. It finds `("icon-sort", 0)` and
checks the role `Button` and the label "Sort by Name", and checks that a
column without a sort has no icon. It then clicks the icon three times
and checks that `perform_sort` saw descending, ascending, then default.
On `main` it fails with `missing ElementId NamedInteger("icon-sort",
0)`.

```bash
cargo test -p gpui-kit --features "test-support component" --test collections   # 7 passed
cargo test -p gpui-component --lib table                                        # 9 passed
cargo fmt --all --check
cargo clippy -p gpui-component -p gpui-kit --features "gpui-kit/test-support gpui-kit/component" --all-targets -- -D warnings -A clippy::nonminimal_bool
typos
```

The `-A clippy::nonminimal_bool` is only there because clippy on newer
Rust flags `crates/base/src/calendar.rs:132`, which fails the same way
on `main` and is not touched here.

- [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.~~
Covered by the UI test above.
- [ ] ~~Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)~~ Not applicable: the change is not
platform-specific.

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

https://claude.ai/code/session_015GWcK7TACWQXrgZuXXFwjP

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 636e088)
This is the visible-range half of #3385, split out on its own. The
header-click sort half is dropped, as you asked in #3385, and the sort
interaction is unchanged.

On `main`, `update_visible_range_if_need` returns early for any range of
one row or fewer, because `uniform_list` also calls the render callback
with `ix..ix+1` to measure a row. As a result:

- a table with one row never reports `0..1`;
- a table whose rows drop to zero never reports anything, because the
empty branch does not render the list, so `visible_rows_changed` keeps
the stale range;
- with `stripe`, the filler rows are part of the list, so the reported
end can pass `rows_count` (a 3-row striped table reports `0..8`).

An app that shows "Showing a–b of n" under a table has to clamp the
range itself, and still shows a wrong range on first load.

The fix, in `crates/component/src/table/state.rs`:

- `update_visible_range_if_need` takes the item count (`:1336`). The
one-item guard applies only when there is more than one item (`:1343`).
- The reported end is clamped to the item count. If that leaves an empty
range while there are items (a stale range scrolled past a shrunken
list), nothing is reported; the list scrolls back on the next frame and
reports the real range (`:1347-1353`).
- The empty branch reports `0..0` (`:2472-2473`). The existing equality
check means this fires once, and not at all for a table that starts
empty.
- The column axis had the same guard, and its virtual list measures with
`ix..ix+1` too. The column call now passes the number of scrollable
columns (`:2157`) and gets the same treatment. No test covers the column
path.

The `visible_rows_changed` doc comment now says the range never ends
past `rows_count` and that a table whose rows drop to none reports
`0..0`.

Remaining edge: when there is more than one row but the viewport is
shorter than one row, the real range is one row long and is still
skipped, because it cannot be told apart from the measuring call.

This change was written with AI assistance (Claude Code).

No public signature changes. `TableDelegate::visible_rows_changed` now
also fires with `0..1` for a one-row table and `0..0` when the rows drop
to none, and its end never passes `rows_count`.
`visible_columns_changed` likewise fires for a single scrollable column.

Two UI tests in `crates/kit/tests/collections.rs`, using the
`TableProbe` fixture (with the names from your cleanup in #3385) that
records `visible_rows_changed` calls:

- `table_reports_one_row_and_empty_visible_ranges`: a 1-row table
reports `0..1`; after the row count drops to 0 and a frame renders, it
reports `0..0`.
- `table_visible_range_stops_at_the_last_row_under_stripe_filler`: a
3-row striped table never reports a range ending past 3.

Both fail on `main` (no `0..1` report; `[0..3, 0..8]`) and pass with the
change.

```bash
cargo test -p gpui-kit --features "test-support component" --test collections   # 8 passed
cargo test -p gpui-component --lib table                                        # 9 passed
cargo fmt --all --check
cargo clippy -p gpui-component -p gpui-kit --features "gpui-kit/test-support gpui-kit/component" --all-targets -- -D warnings -A clippy::nonminimal_bool
```

The `-A clippy::nonminimal_bool` is only there because clippy on newer
Rust flags `crates/base/src/calendar.rs:132`, which fails the same way
on `main` and is not touched here.

- [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.~~
Covered by the UI tests above.
- [ ] ~~Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)~~ Not applicable: the change is not
platform-specific.

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

https://claude.ai/code/session_015GWcK7TACWQXrgZuXXFwjP

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 29399be)
## Description

Dialogs always slide from the window top to their resting position while
fading in. When a dialog rests low in a tall window, it crosses much of
the window before settling.

Add a shared `DialogEntrance` API so applications can choose the
entrance for both `Dialog` and `AlertDialog`:

```rust
dialog.entrance(DialogEntrance::FadeSlide)
alert.entrance(DialogEntrance::Fade)
```

| Entrance | Behavior |
| --- | --- |
| `SlideDown` (default) | Existing slide from the window top with its
original timing and easing. |
| `Fade` | Fade in at the final position without movement. |
| `FadeSlide` | Fade in with short downward travel from above the
resolved resting position. |
| `None` | Display the surface and backdrop immediately. |

`Fade` and `FadeSlide` use the theme's normal duration and enter easing.
`FadeSlide` uses the theme's short motion distance and applies travel
after window-boundary positioning, so a dialog clamped upwards still
moves relative to its actual resting position. Travel is shortened when
there is insufficient space above the surface, and pointer hit testing
follows the moving surface.

Reduced motion immediately displays the final state for every option. A
zero theme duration also skips the fade entrances. Surface opacity,
shadows, and the backdrop use the same entrance timing. Closing remains
immediate.

The Dialog story exposes all four choices under Options → Entrance.
Dialog and AlertDialog documentation is updated in English and Chinese.

## Public API

### gpui-component

- `#[non_exhaustive] pub enum dialog::DialogEntrance { SlideDown, Fade,
FadeSlide, None }` — Select the shared entrance presentation. Implements
`Clone`, `Copy`, `Debug`, `Default`, `PartialEq`, `Eq`, `Serialize`, and
`Deserialize`.
- `DialogEntrance::SlideDown` — Default; preserve the existing slide and
fade entrance.
- `DialogEntrance::Fade` — Fade at the final position without movement.
- `DialogEntrance::FadeSlide` — Fade with bounded, theme-defined
downward travel.
- `DialogEntrance::None` — Immediately show the surface and backdrop.
- `Dialog::entrance(self, entrance: DialogEntrance) -> Self` — Choose
the dialog entrance.
- `AlertDialog::entrance(self, entrance: DialogEntrance) -> Self` —
Apply the same entrance policy to an alert dialog.

## Breaking Changes

None for existing consumers. The API is additive and `SlideDown`
preserves the existing default behavior.

## Validation

- `cargo test -p gpui-component --lib dialog -- --nocapture` — 17
passed, covering resolved-boundary travel, intermediate positions,
top-edge limits, pointer hit testing, builder defaults, AlertDialog
forwarding, reduced motion, immediate keyboard dismissal, and zero theme
duration.
- `cargo check -p gpui-component-story` — passed.
- `cargo clippy -p gpui-component -p gpui-component-story --all-targets
-- --deny warnings` — passed.
- `cargo fmt --all --check` and `git diff --check` — passed.

Implementation, tests, and documentation were generated with AI
assistance. The story was compile-checked; its animations were not
manually compared across platforms.

---------

Co-authored-by: xxww0098 <shefkie1957@gmail.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 6baad8b)
## Description

Creating input entities inside an `open_dialog` builder resets editing
state when the dialog layer rebuilds. Focusing content unconditionally
in that builder can also steal focus from other controls or nested
dialogs. These contracts were unclear while building
[SkillStar](https://github.com/xxww0098/SkillStar).

Document the builder contract for `open_dialog`, `open_alert_dialog`,
`open_sheet`, and `open_sheet_at`:

- Builders are render closures that may run again when their layer
rebuilds. View caching means they are not guaranteed to run on every
displayed frame.
- Create persistent entities before opening and capture their handles to
preserve editing state.
- `open_dialog` initially focuses the dialog shell. Set initial content
focus only on the first builder invocation, using a separate one-time
guard for each opening.

The API docs include an input example with a `Cell` guard, and the
English and Chinese Dialog pages explain the same lifecycle and focus
rules.

Clarify the existing footer contract: plain `Dialog` does not generate
buttons from `button_props`; those props install action callbacks.
Applications compose their own `DialogFooter`. `AlertDialog` generates a
default footer when no custom footer is supplied. Correct the private
header setter's copied footer description as well.

Documentation only; no public signatures or runtime behavior change.

## Validation

- `cargo fmt --all --check` and `git diff --check` — passed.
- `cargo doc -p gpui-component --no-deps` — passed; warnings remain at
unmodified documentation locations.
- Runtime tests were not rerun for this documentation-only revision.

The documentation revision was generated with AI assistance.

---------

Co-authored-by: xxww0098 <shefkie1957@gmail.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 296fe2f)
Closing a tab steals keyboard focus into nowhere: `DockArea` drops the
panel, but the keystroke that follows lands on no view at all. If the
closed panel held focus, the panel its group displays in its place
should take it over.

This change makes `DockArea::remove_panel` check, before removing,
whether the panel being closed contains the focused view. If it did, the
panel the group now shows in its place (via `active_panel`) is focused.
Closing a tab that never had focus changes nothing.

Two tests cover both sides:

- `dock_hands_the_focus_to_the_tab_that_replaces_a_closed_one` — closing
the focused tab moves focus to the newly visible tab.
- `dock_leaves_the_focus_alone_when_a_tab_without_it_closes` — closing a
background tab leaves focus where it was.

No public API changes; behavior-only fix.

---
This PR is 100% AI-generated: originally drafted with Claude on
2026-10-04, rebased onto current main, reviewed and validated by the
contributor: `cargo fmt --check`, `cargo clippy -p gpui-base
--all-targets -- --deny warnings`, and the dock test suite (4 passed,
including the two new focus tests) are all green.

---------

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 59cba65)
A `DropdownButton`'s menu trigger shows only a caret, so it has no
visible label for a screen reader to announce. The action half is named
by the inner `Button`, but the trigger's accessible name fell back to
nothing.

This change adds `DropdownButton::menu_accessibility_label`, which sets
the name announced for the menu trigger. Without it, the trigger gets a
localized default ("More options") via a new
`DropdownButton.more_options` locale key, so the caret-only trigger
always has a name. The default is translated for en, zh-CN, zh-HK and
zh-TW, and the storybook docs mention the new builder.

Test `dropdown_button_names_its_menu_trigger` asserts both the explicit
label and the localized default.

## Public API

### gpui-component
- `DropdownButton::menu_accessibility_label(impl Into<SharedString>) ->
Self` — Sets the screen-reader name for the menu trigger; defaults to
the localized "More options".

---
This PR is 100% AI-generated: originally drafted with Claude on
2026-10-04, rebased onto current main (the test was ported to the
renamed `#[gpui_kit::test]` macro), reviewed and validated by the
contributor: `cargo fmt --check`, `cargo clippy -p gpui-component
--all-targets -- --deny warnings`, and the components test suite (7
passed, including dropdown_button_names_its_menu_trigger) are all green.

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1e41f17)
## Description

Follow-up to #3407. Two more places update an entity even though nothing
in it changed. Under GPUI Fast, a view that read that entity is then
counted as changed and built again, even when it is retained.

- **`TextViewState`**: a markdown text that is parsed synchronously when
it is set still receives an acknowledgement (`baseline_ack`) from the
background parser. `commit_parsed_update` discarded it, but only after
`weak_self.update`, which already marked the state as updated. The
receive task now checks first, with `read_with` and the new
`TextViewState::commits`, and skips results that commit nothing.
`commit_parsed_update` uses the same check.
- **`ResizablePanelGroup` / `ResizablePanel`**: both report their bounds
in `on_prepaint` on every frame they are drawn, and updated
`ResizableState` every time, even when the bounds were the same. Every
view that read the state was rebuilt on every frame. They now return
early when nothing would change: same group bounds, or
`ResizableState::panel_size_changes` (crate-private) is false.

Behaviour is unchanged: the skipped updates were no-ops.

## Before / After

Measured by the new tests (`gpui-fast` feature), counting renders of a
view that reads the state:

| Case | Before | After |
| --- | --- | --- |
| Reader of a `TextViewState` after the parser acknowledges a
synchronous parse | 2 renders | 1 render |
| Reader of a `ResizableState` while its panels are redrawn at the same
bounds (3 extra frames) | 11 renders | 5 renders |

## Tests

-
`text::state::tests::parse_ack::acknowledging_a_parse_leaves_its_readers_retained`
-
`resizable::tests::redrawing_panels_where_they_were_leaves_their_readers_retained`

Both fail without the fix and pass with it. `cargo fmt --check` and
`cargo clippy -p gpui-base -- --deny warnings` (with and without
`gpui-fast`) are clean. `cargo test -p gpui-base --lib` passes, 1323
tests. With `--features gpui-fast` the same 14 tests fail on `main` as
on this branch, so this change neither causes nor fixes them.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2ef9968)
## Description

Editor completions previously opened only as a side effect of typing.
Applications could not request suggestions from a command, menu, or
their own shortcut without editing text.

Add the Editor-only `ShowCompletions` action and `show_completions` API.
There is no default shortcut: hosts choose bindings that fit their
platform and avoid input-method shortcuts such as Ctrl+Space. Hosts can
scope bindings to `Input && mode == editor`; existing `Input` bindings
remain valid.

```rust
window.dispatch_action(Box::new(ShowCompletions), cx);
editor.update(cx, |state, cx| state.show_completions(window, cx));
```

The action addresses the focused Editor; the method focuses the
specified Editor. Suggestions refine against the identifier prefix
before the caret, and an empty prefix still requests suggestions. Prefix
scanning walks backward through the rope instead of copying the whole
preceding document. Manual requests use LSP `INVOKED` with no trigger
character.

Disabled/readonly editors, missing providers, and IME preedit issue no
request. An already-open completion menu keeps its selected item. A new
request cancels the previous overlay request and inline suggestion,
closes code actions, and uses the existing completion lifecycle.
Responses are checked against the current document revision, caret,
focus, editability, and composition state before being applied.

English and Chinese Editor documentation describe the command, API,
host-owned shortcuts, and lifecycle rules.

## Public API

### gpui-base

- `input::ShowCompletions` — Editor action requesting suggestions
without typing; no default keybinding.
- `InputBaseState<EditorMode>::show_completions(&mut self, window: &mut
Window, cx: &mut Context<Self>)` — Request manual completions and focus
the Editor, respecting editability, IME, and an existing completion
menu.

### gpui-component

- `input::ShowCompletions` — Re-export the Base action for applications
using `gpui_kit::component::input`.

## Breaking Changes

None for existing consumers. The public API is additive and existing
`Input` bindings remain supported.

## Validation

- `cargo test -p gpui-kit --features 'test-support component' --test
input` — 179 passed. Covers explicit and empty-prefix requests,
host-configured Editor-only shortcuts, no Ctrl+Space default, menu
selection preservation, IME preedit/commit,
readonly/disabled/missing-provider guards, request replacement,
Escape/caret cancellation, inline suggestion/debounce cancellation,
code-action replacement, and API focus from an application command.
- `cargo clippy -p gpui-base -p gpui-kit --features
'gpui-kit/test-support gpui-kit/component' --all-targets -- --deny
warnings` — passed.
- `cargo fmt --all --check` and `git diff --check` — passed.

The implementation, tests, and documentation revision were generated
with AI assistance. IME coverage uses GPUI's simulated input-handler
protocol; it does not drive native candidate windows or verify OS
shortcut interception.

---------

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 32529dc)
Right-clicking a cell draws its outline, but the outline was never
cleared: clicking another cell selected it while the right-click outline
stayed painted, and clicking outside the table left the outline behind
too.

This change clears the right-clicked cell state when a cell is selected
(`set_selected_cell`), and treats a right-clicked cell like a
right-clicked row in the table element, so a click outside the table
dismisses the outline as well. A new `TableState::right_clicked_cell()`
getter exposes the state for tests and callers.

Test `table_forgets_a_right_clicked_cell_on_the_next_click` covers both
cases: selecting another cell clears the outline, and clicking outside
the table clears it.

- `TableState::right_clicked_cell(&self) -> Option<(usize, usize)>` —
Returns the right-clicked cell as `(row_ix, col_ix)`, if any.

---
This PR is 100% AI-generated: originally drafted with Claude on
2026-10-04, rebased onto current main (the test was ported onto
upstream's extended `collections.rs`), reviewed and validated by the
contributor: `cargo fmt --check`, `cargo clippy -p gpui-component
--all-targets -- --deny warnings`, and the collections test suite (8
passed, including table_forgets_a_right_clicked_cell_on_the_next_click)
are all green.

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 288767c)
## Description

When a `TabBar` overflows, clicking a partially hidden tab selects it
but leaves it clipped, so there is no way to keep moving through the
strip except by scrolling manually.

Clicking a tab (or picking it from the overflow menu) now scrolls it
into view with part of the neighboring tab showing, animated with the
same spring as the selection indicator. This also works when
`track_scroll` is not used.

As agreed in #3021, only a click does this: changing the selection, the
tabs or the layout still leaves the offset where the user put it, and a
wheel or trackpad scroll during the animation takes over immediately.
Reduced motion skips the animation.

## Screenshot

| Before | After |
| ------ | ----- |
|
https://github.com/user-attachments/assets/5e189ca9-9a28-446f-ba70-dc229400c5f4
|
https://github.com/user-attachments/assets/1ca955cb-f3fe-4cd3-97c9-b50848d70735
|

## How to Test

Run `cargo run -- tabs`, add tabs in **Dynamic Tabs** until the bar
overflows, then click the clipped tabs at either edge.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 02daace)
## Description

`DataTable` used one height for header rows and body rows, so a two-line
header forced every record row to grow with it. `header_row_height` sets
the height of each header row on its own. Body rows keep
`Size::table_row_height`. When `header_row_height` is unset, header rows
still use that body height, so existing tables look the same.

Page Up and Page Down used to subtract one body row as a stand-in for
the header. They now subtract the full header band (header row height
times the number of header rows) before dividing by the body row height.
With a single header row at the body height, that is the same count as
before.

## Screenshot

No visual change when `header_row_height` is unset. The Data Table story
adds a Header Row Height menu (Default / 40px / 48px) to try it.

## Public API

### gpui-component

- `gpui_component::table::DataTable::header_row_height(self, height:
impl Into<gpui::Pixels>) -> Self` — sets each header row's height
independently of the body row size. Column-group rows use the same
height. When unset, header rows keep `Size::table_row_height()`.

## How to Test

- `cargo check -p gpui-component --locked`
- `cargo check -p gpui-component-story --locked` (passed here)
- `cargo run` and open the Data Table story. Scroll and use Page Up /
Page Down. With Header Row Height on Default, the header and scrollbar
match the previous build. Group Headers is on by default, so Page Up /
Page Down now step one row less than before, because both header rows
are subtracted. Switch Header Row Height to 40px or 48px to see taller
header rows over unchanged body rows.
- To try the new height:
`DataTable::new(&state).with_size(Size::Size(px(20.))).header_row_height(px(34.))`.
Header rows are 34px and body rows stay 20px. The vertical scrollbar
starts below the header band.

## 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 <cursoragent@cursor.com>
Co-authored-by: Floyd Wang <gassnake999@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 37cad32)
Closes #3364

## Description

With accessibility active, opening a `ContextMenu` aborts a debug build
with `set_focus called more than once in a single frame`.

GPUI registers the focused element as the frame's accessibility focus
while prepainting it, and allows one per frame. `ContextMenu` moved
focus to its menu inside the menu's own prepaint. The menu is deferred
and prepaints last, so the element that held focus before (for example
the right-clicked input) had already registered, and the menu registered
a second time.

- `ContextMenu` now focuses the menu in the deferred callback that
builds it, before its first frame is drawn, after the input selection
ownership from #3382 is resolved. `DeferredMenu::build_menu` no longer
touches focus.
- The old prepaint focus ran on every frame and pulled focus back
whenever it left the menu. That also covered a case it was never written
for: entering a submenu with the keyboard and then pointing at another
parent item closes the submenu while it still holds focus, leaving
Escape and the arrow keys with nowhere to go. `PopupMenu` now takes
focus back itself when a hover closes a focused submenu, which also
applies to `DropdownMenu` and `AppMenuBar`.

Behavior change: while a context menu is open, focus that the
application moves elsewhere is no longer pulled back to the menu on the
next frame.

## How to Test

```bash
cargo test -p gpui-component --lib menu::
cargo test -p gpui-kit --features test-support --test menu
```

- `menu_takes_focus_before_its_first_frame` checks that focus has left
the previous element when the frame that draws the menu begins
rendering. It fails on `main`.
- `hovering_away_from_a_keyboard_entered_submenu_refocuses_its_parent`
enters a submenu with the keyboard, hovers another parent item, and
checks that Escape still dismisses the menu. It passes on `main`, fails
with only the `ContextMenu` change, and passes with this PR.

The headless test platform cannot activate the accessibility tree, so
the tests cover the focus ordering that causes the panic rather than the
panic itself. This has not been run against a screen reader on a native
debug build.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9c4b175)
## Description

Completed partial selections in a managed `TextViewState` should survive
presentation changes when the source is unchanged. This PR retains the
logical selection range and Copy payload across style,
syntax-highlighter, inherited-font, zoom, and width changes by default.
It adds no configuration API.

Current styles and wrapping still apply. The retained range is not
reconstructed by projecting an old pointer rectangle onto the new
layout, and later outer-layout origin changes do not become new
selection gestures. A new drag or explicit clear updates selection
normally. Source replacement and Markdown parser-configuration
replacement still invalidate selection.

The owning view must retain both the state and its painted selection
participant. This behavior does not pin rows unmounted by an outer
virtual list. Changes are confined to SDK selection policy; GPUI and
platform input are not patched. The changes were developed with AI
assistance.

Motivation: [Kagi issue
#1091](TomiXRM/kagi#1091). This PR addresses
selection behavior and does not claim to complete the broader
conversation-performance work.

## Public API

No public APIs are added, removed, or changed relative to the base
branch. The opt-in builder proposed in earlier revisions has been
removed; presentation-only changes now preserve completed selections by
default.

## Validation

Validated locally after commit `48cc5389a`:

- 331 Base and 69 Component tests selected by `text::`: all 400 passed.
- 7 Kit text-selection integration tests: all passed. Fixtures use
`gpui_kit::open_window` through the shared test helper.
- `cargo fmt --all --check`: passed.
- Clippy for Base, Component, Kit, and the text-selection integration
target with `--deny warnings`: passed.
- `git diff --check`: passed.

Coverage includes completed partial selection and Copy across color,
width-only, font, zoom, and origin changes; cross-block and
cross-renderer reflow; new drags replacing retained ranges; explicit
clear; source replacement; and parser-configuration replacement.
Syntax-highlighter changes apply new presentation while preserving the
selection. The existing Component regression now verifies default
retention after same-size style changes.

The Base README and English/Chinese TextView documentation describe the
default behavior and mounted-participant requirement. Native
GUI/performance evaluation has not been performed for this update.
Results from CI on earlier revisions do not validate the latest head.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 0bbd987)
henrikogaard and others added 7 commits October 12, 2026 11:49
Closes #3423

## Description

Inline `<mark>` tags inside a Markdown paragraph were dropped.
CommonMark hands each raw tag over as its own HTML sibling, and
`inline_html_mark` only knew
`strong`/`b`/`em`/`i`/`u`/`s`/`del`/`strike`, so `<mark>` never paired
and the text rendered without a highlight. `color` and
`style="background-color: …"` were ignored.

This pairs `<mark>` like the other inline formatting tags, passes the
opening tag's attributes through, and reuses the block HTML color rules
(`color` first, then `style="background-color"`, then the default
`#fef08a`).

## Screenshot

From [Rísta](https://github.com/henrikogaard/rista), which uses
`TextView` markdown preview. The left pane is the source editor; the
right pane is the preview. The note has plain `==highlighted==` (emitted
as bare `<mark>`) and six colored highlights (emitted as `<mark
color="#rrggbbaa">`).

| Before | After |
| ---------------------------- | --------------------------- |
| ![Unpatched gpui-kit 0.7, dark
theme](https://raw.githubusercontent.com/henrikogaard/gpui-kit/cursor/pr-screenshots-0bc0/pr-screenshots/before_highlights.png)
| ![This branch, dark
theme](https://raw.githubusercontent.com/henrikogaard/gpui-kit/cursor/pr-screenshots-0bc0/pr-screenshots/after_highlights.png)
|

After, light theme:

![This branch, light
theme](https://raw.githubusercontent.com/henrikogaard/gpui-kit/cursor/pr-screenshots-0bc0/pr-screenshots/after_light_highlights.png)

## How to Test

Unit tests:

```bash
cargo test -p gpui-base --lib -- text::format::markdown text::format::html
cargo fmt -p gpui-base -- --check
cargo clippy -p gpui-base --lib --tests -- --deny warnings
```

All three passed on this branch (73 format tests).

Rísta check: open a note with `==highlighted==` and colored `==🔴text==`
highlights. The preview should show the default yellow mark and the
colored washes, in both dark and light themes.

## 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 Agent <cursoragent@cursor.com>
Co-authored-by: Henrik Øgård <henrikogaard@users.noreply.github.com>
Co-authored-by: Floyd Wang <gassnake999@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ed27c62)
…ret (#3412)

A completion popup opened by a keystroke stays where the caret *was*:
the menu resolves its position in `render()` from the input's previous
frame paint layout, but the caret's new position is only known after the
current frame's prepaint.

This change resolves the popup position during the current frame instead
of requesting a follow-up frame:

- `TextElement::prepaint` stashes fresh caret geometry
(`PrepaintCaretGeometry`: cursor bounds, line height, input bounds,
clamped scroll offset) in the state. Only the positioning geometry is
stashed; the existing paint-time state updates are untouched.
- Popups live inside `deferred()`, so their prepaint runs after the
input's prepaint. A new `CaretAnchoredPopup` element resolves the anchor
there via the shared `caret_popup_anchor()` and builds the popup content
from it — position, hitboxes, accessibility bounds, and width/layout
decisions (`max_width`, vertical vs horizontal docs) are all consistent
with the resolved position.
- Both the completion and code-action menus use the shared anchor; their
duplicated `origin()` methods are removed.

No extra frame is requested for any input, ordinary or otherwise.

Two regression tests in `crates/kit/tests/input/completions.rs`:

- `completion_popup_follows_caret_on_first_frame` — types a character,
renders exactly one frame, and asserts the popup moved right with the
caret and sits at the freshly laid-out caret position (not just moved).
- `ordinary_input_schedules_no_followup_frame` — types in a plain input,
settles, and asserts `simulate_next_frame` schedules nothing.

## Public API

### gpui-base
- `PrepaintCaretGeometry` (doc-hidden) — caret positioning geometry from
the input's latest prepaint: cursor bounds, line height, input bounds,
and the clamped scroll offset.
- `InputBaseState::prepaint_caret_geometry(&self) ->
Option<PrepaintCaretGeometry>` (doc-hidden) — reads the stashed geometry
for caret-anchored popups.

No public API changes; behavior-only fix otherwise.

---
This PR is 100% AI-generated: originally drafted with Claude on
2026-10-04, reworked per review feedback, rebased onto current main,
reviewed and validated by the contributor: `cargo fmt --check`, `cargo
clippy -p gpui-base -p gpui-component --all-targets -- --deny warnings`,
and the input test suite (168 passed, including the two new regression
tests) are all green.

---------

Co-authored-by: Sola <sola-ryu@users.noreply.github.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
Co-authored-by: Endless <endless@longbridge.com>
(cherry picked from commit 75fc4ed)
)

Closes #3427

## Description

A custom context menu builder set with `on_context_menu` ran inside
`cx.defer_in`, which executes the callback inside the input's `update`.
A builder that reads the input (cursor, selection, text) to decide its
items then panicked with "cannot read ... InputBaseState ... while it is
already being updated".

`handle_right_click_menu` now defers with `window.defer` and a weak
handle to the input, so the handler runs with the input released. The
focus bookkeeping that used to run in the same lease (reusing an
already-focused popup, capturing the menu's focus for
`set_selection_focus`) runs right after the handler in `input.update`,
unchanged. If the input is gone by the time the deferred call runs,
nothing happens, as before.

Both deferrals push the same deferred effect, so the handler still runs
at the same point in the effect cycle. Textarea and Editor go through
the same function.

The change was written with an AI coding assistant; I reviewed and
tested it.

## How to Test

- New test `context_menu_handler_can_read_its_input`
(`crates/base/src/input/base/state.rs`): the handler reads the input's
cursor. On `main` it panics with the lease error from the issue; with
this change it reads the expected cursor.
- `cargo test -p gpui-base --lib input` (335 passed), including the
existing `context_menu_handler_is_deferred_and_respects_disabled` that
covers the focus and popup-reuse paths.
- `cargo test -p gpui-kit --features test-support,component --test menu
--test input --test input_focus` (12, 179, 4 passed).
- `cargo clippy -p gpui-base -p gpui-component --all-targets -- --deny
warnings` and `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; covered by the input and menu tests above.)
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific) (not platform-specific)

(cherry picked from commit a25598e)
Closes #3429

## Description

`TextareaState::set_auto_grow(min, max)` replaced the layout mode with a
fresh `LayoutMode::auto_grow`, which starts at `rows: min_rows`. The
rows were counted again only on the next edit, wrap width change or
token layout rebuild, so a textarea that already held text collapsed to
`min_rows` after the call and stayed there until the user typed.

This counts the rows right after replacing the mode
(`self.mode.update_auto_grow(&self.display_map)`), so the textarea keeps
fitting its text, clamped to the new limits. One line in
`set_auto_grow`; nothing else changes.

We hit it in an app that adapts `max_rows` of a commit message textarea
to the room its panel has (window resize, font size change): every
adjustment shrank a long message to one row.

## Screenshot

No visual change beyond the bug itself. A textarea with `auto_grow(1,
10)` and 8 lines of text:

| Step | Before | After |
| --- | --- | --- |
| first render | 8 rows (178 px) | 8 rows |
| after `set_auto_grow(1, 12)` | 1 row (38 px) | 8 rows |

The heights were measured headlessly with the macOS text system before
the fix; the rows after it come from the regression test below.

## How to Test

- New regression test
`input::state::tests::test_set_auto_grow_keeps_rows_fitted`: eight
lines, `auto_grow(1, 10)` → 8 rows; `set_auto_grow(1, 12)` → still 8
(was 1 before the fix); `set_auto_grow(1, 4)` → 4.
- `cargo test -p gpui-base --lib --locked`: 1325 passed.
- `cargo test -p gpui-shell --lib --locked -- textarea` (the shell's
`set_auto_grow` binding): passed.
- `cargo clippy -p gpui-base --all-targets --locked -- --deny warnings`
and `cargo fmt --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.
- [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 <noreply@anthropic.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
(cherry picked from commit 1ccf011)
Closes #3438

`Inline::paint` painted the text before the selection quad, so the quad
covered the selected glyphs. An opaque selection color hid the text
completely; a translucent one only tinted it, which is why the default
theme masks the problem.

This paints the text in two passes around the selection: run backgrounds
first, then the selection, then the glyphs, using GPUI's
`TextLayout::paint_background` and `paint_foreground`. The selection is
now
a background under the glyphs, as `TextViewStyle::with_selection`
documents, and it still shows on runs that carry their own background,
such
as `<mark>` highlights. It matches how range highlights are already
painted
in the same function and how `Input` paints its selection. The selection
range only needs the text layout, which is available before painting, so
nothing else changes.

This touches `gpui-base` because the paint order lives there.

If the inline state lock is poisoned, `paint` still paints the glyphs
before
it returns early. Before this change the text was painted ahead of the
lock,
so a poisoned lock must not hide it now.

`crates/kit/tests/rendering.rs` gains
`selected_text_stays_over_an_opaque_selection`: it selects all text
under an
opaque selection color and checks that both selection and glyph pixels
are
painted. It fails on the previous paint order and passes with this
change.

Opaque selection color, all text selected next to the same content
unselected (right). Windows 11.

<img width="536" height="223" alt="repro3"
src="https://github.com/user-attachments/assets/1b4f1761-50b5-4f93-8391-a3a259366e56"
/>

<img width="536" height="223" alt="fixed-opaque"
src="https://github.com/user-attachments/assets/1dad3f3b-4a49-45f0-92b4-c37aa5de276c"
/>

In any example with a `TextView`, set an opaque selection color and
select
text, including a fenced code block:

```rust
Theme::global_mut(cx).selection = hsla(0.6, 0.9, 0.5, 1.);
Theme::sync_base(cx);
```

Before: the selected text is hidden by a solid block. After: the text
stays
readable on top of it.

Built-in themes with an opaque selection color show it in the Story
Gallery without code changes: Aurora Light, macOS Classic Dark, Kibble.

Also ran `cargo test -p gpui-base --lib text::` (340 passed) and
`cargo fmt --check`. The rendering test runs on macOS and Linux:

```bash
cargo test -p gpui-kit --features "test-support component assets" --test rendering
```

AI-assisted: the change was written with Claude Code and reviewed by me.

- [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: Jason Lee <huacnlee@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4e2a00e)
Rewrite the menu and completion tests that used find_all and pointer_down/pointer_up, which came with #3384 and #3402 and are not part of this release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@huacnlee
huacnlee merged commit c619370 into stable Oct 12, 2026
16 checks passed
@huacnlee
huacnlee deleted the release/v0.7.2 branch October 12, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.