Repository navigation
kit: Prepare v0.7.2 - #3458
Merged
Merged
kit: Prepare v0.7.2#3458
Conversation
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  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 |  |  | | Drag from the paragraph above into the card (#3368 case, unchanged) |  |  | 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 | | --- | --- | |  |  | ## 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:  ## 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**  **After**  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.  [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)
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 | | ---------------------------- | --------------------------- | |  |  | After, light theme:  ## 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)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Prepare v0.7.2 on
stablefrom the PRs merged intomainsince v0.7.1, leaving out every change that breaks existing consumers.stablealready carries #3371 and #3373; this adds the remaining compatible PRs withgit cherry-pick -xand bumps the workspace to 0.7.2.Included
TooltipDefaults(tooltip: Make show delay and grace period configurable withTooltipDefaults#3376)Already on
stable: #3371, #3373.Excluded (Breaking Changes)
Easing::samplenow returns values outside[0, 1]for overshooting curves.Foldervariant #3415 tab: newTabVariant::Folderbreaks exhaustive matches.TestWindowExtgains required methods.ResizeHandle::on_dragtakes&Tinstead ofRc<T>.gpui-wry→gpui-webviewrename, and the GPUI Fast requirement.#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
crates/kit/tests/collections.rsandcrates/kit/tests/rendering.rs: conflict resolutions drop the tests that belong to kit: Extend UI testing with semantic queries and Linux rendering #3402 and keep each backported PR's own test.test: Keep backported tests on the v0.7.1 test API: the menu and completion tests from menu: Focus a context menu before its first frame #3422 and input: Anchor completion/code-action popups to the current frame's caret #3412 usedfind_allandpointer_down/pointer_upfrom test: Add find_all and configurable clicks to the UI test harness #3384/kit: Extend UI testing with semantic queries and Linux rendering #3402. They now querygpui_kit::base::test_support::snapshotsand dispatch mouse events directly.kit: Prepare v0.7.2: workspace version bump;Cargo.lockonly updates workspace members.Public API
No new public API beyond what the included PRs add; each lists its items in its own
## Public APIsection (notably #3375, #3376, #3378, #3388, #3403, #3404, #3405, #3411, #3420).How to Test
cargo clippy --workspace --all-targets -- --deny warningspasses on Linux.cargo test -p gpui-kit --features "test-support component assets" --test collections --test menu --test inputpasses on Linux.renderingtest (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