Skip to content

text: expose selection and range geometry for TextView, Editor and Diff #3669

Open

Description

@suxiaoshao

Description

An application placing a floating toolbar beside selected text, or a clickable marker beside a saved range, needs visible range rectangles without changing selection.

In v0.8.0:

  • TextView exposes selected text and source ranges, but selection_points is crate-private.
  • Editor's range_to_bounds returns an endpoint bounding box, not visible wrapped-line fragments, and requires both endpoints to have layout.
  • Diff's selection handle is crate-private; line selection does not expose geometry.

Environment

  • GPUI: gpui-pre 0.3.9
  • GPUI Component: gpui-kit 0.8.0
  • Platform: macOS 27 (arm64)

Steps to Reproduce

Select several wrapped lines, then scroll the selection's first endpoint out of view. Inside an existing view callback:

let state = text_view.read(cx); // Entity<TextViewState>
let source_range = state.selected_source_range();
let text = state.selected_text();
// Content is available, but no public query returns the visible
// selection rectangles for positioning a toolbar.

let state = editor.read(cx); // Entity<EditorState>
let bounds = state.range_to_bounds(&state.selected_range());
// An endpoint box cannot describe the remaining visible fragments.

Expected

Query visible rectangles for the current selection or an explicit range. A toolbar can follow wrapping and scrolling; a source marker can remain anchored after selection is cleared. Mouse and keyboard selections use the same API.

Actual

The public APIs provide content, source positions or endpoint bounds, but not clipped visual-line geometry. Pointer coordinates do not describe keyboard selections. An application would otherwise need to reconstruct component layout.

Suggested change

Expose a shared read-only geometry value, with private fields and accessors. Illustrative API:

impl SelectionGeometry {
    pub fn line_bounds(&self) -> &[Bounds<Pixels>];
    pub fn first_visible_line(&self) -> Option<Bounds<Pixels>>;
    pub fn viewport(&self) -> Bounds<Pixels>;
}
// State methods on TextView, Editor and Diff:
// selection_geometry(...) -> Option<SelectionGeometry>
// range_geometry(range, ...) -> Result<Option<SelectionGeometry>, ...>

Use window coordinates and visible fragments only, preserving disjoint fragments and Diff side identity. TextView ranges can use rendered offsets, Editor buffer byte offsets, and Diff source coordinates. Document the frame phase in which both selection and geometry are committed; return unavailable for stale ranges or missing layout. This should not move selection, scroll, or focus. TextView/Editor support can precede Diff.

Activity

  1. suxiaoshao commented on Oct 24, 2026

    @suxiaoshao
    ContributorAuthor

    A related timing gap, in the same area as geometry:

    In TextView, inline elements update their selected range during paint, while selected_text() and the source-range getters read the document. A getter called from a MouseUp handler, or from a defer scheduled there, can therefore still see the projection from the previous paint. The selection only becomes reliable after the next paint.

    #3589 (included in v0.8.0) makes the Copy path refresh the projection synchronously. It does not give other callers a defined point at which the current selection is committed and safe to read.

    When geometry is exposed, please also define when selection and geometry are committed, for example a selection-changed/committed notification or a documented post-paint query point. A consumer that places a toolbar beside the selection then doesn't have to guess the frame order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions