Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion crates/base/src/input/base/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
};

use super::{
InputBaseState, RangeDecorationStyle, TextDecoration,
InputBaseState, PrepaintCaretGeometry, RangeDecorationStyle, TextDecoration,
layout::{LastLayout, WhitespaceIndicators},
mode::LayoutMode,
};
Expand Down Expand Up @@ -2862,6 +2862,34 @@ impl<M: InputModeKind> Element for TextElement<M> {
.find(|info| info.is_active)
.map(|info| info.bounds);

// Stash the fresh caret geometry for caret-anchored popups. Their
// deferred prepaint runs after this prepaint but before paint, so
// they anchor to the current frame's caret instead of the previous
// frame's. Only the positioning geometry is stashed here; the rest
// of the paint-time state updates stay where they are.
let caret_geometry = last_layout.cursor_bounds.map(|cursor_bounds| {
let mut scroll_offset = cursor_scroll_offset;
// Mirror update_scroll_offset's clamping with the fresh values,
// so the stash matches what paint will publish.
let state = self.state.read(cx);
let safe_y_range = (-scroll_size.height + input_bounds.size.height).min(px(0.))..px(0.);
scroll_offset.y = if state.is_single_line() {
px(0.)
} else {
scroll_offset.y.clamp(safe_y_range.start, safe_y_range.end)
};
scroll_offset.x = clamp_horizontal_scroll_offset(
scroll_offset.x,
scroll_size.width,
input_bounds.size.width,
last_layout.text_align,
);
PrepaintCaretGeometry::new(cursor_bounds, line_height, input_bounds, scroll_offset)
});
self.state.update(cx, |state, _| {
state.prepaint_caret_geometry = caret_geometry;
});

let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx);
let selection_paths = self.layout_selections(&last_layout, &mut bounds, window, cx);
let hover_highlight_path = self.layout_hover_highlight(&last_layout, &mut bounds, cx);
Expand Down
68 changes: 68 additions & 0 deletions crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,58 @@ struct PasteTarget {
selections: Vec<CursorSelection>,
}

/// Caret positioning geometry from the input's latest prepaint.
///
/// Unlike `last_layout` (published at paint time), this is stashed during
/// prepaint so caret-anchored popups can position themselves from the
/// current frame's geometry in their deferred prepaint, instead of the
/// previous frame's paint.
#[derive(Clone, Copy, Debug)]
#[doc(hidden)]
pub struct PrepaintCaretGeometry {
cursor_bounds: Bounds<Pixels>,
line_height: Pixels,
input_bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
}

impl PrepaintCaretGeometry {
pub(super) fn new(
cursor_bounds: Bounds<Pixels>,
line_height: Pixels,
input_bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
) -> Self {
Self {
cursor_bounds,
line_height,
input_bounds,
scroll_offset,
}
}

/// The active caret's bounds from the fresh prepaint layout.
pub fn cursor_bounds(&self) -> Bounds<Pixels> {
self.cursor_bounds
}

/// Line height from the fresh prepaint layout.
pub fn line_height(&self) -> Pixels {
self.line_height
}

/// The input container bounds from the fresh prepaint.
pub fn input_bounds(&self) -> Bounds<Pixels> {
self.input_bounds
}

/// The scroll offset that will be in effect after paint, clamped like
/// `InputBaseState::update_scroll_offset` clamps it.
pub fn scroll_offset(&self) -> Point<Pixels> {
self.scroll_offset
}
}

/// The shared text-editing engine behind [`crate::input::InputState`],
/// [`crate::input::TextareaState`] and [`crate::input::EditorState`].
///
Expand Down Expand Up @@ -397,6 +449,12 @@ pub struct InputBaseState<M: InputModeKind> {
/// The marked range is the temporary insert text on IME typing.
pub(super) ime_marked_range: Option<CursorSelection>,
pub(super) last_layout: Option<LastLayout>,
/// Fresh caret geometry stashed during prepaint for caret-anchored popups.
///
/// Popups are positioned in their deferred prepaint, which runs after the
/// input's prepaint but before its paint, so they read this instead of
/// the previous frame's `last_layout`.
pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
pub(super) last_cursor: Option<usize>,
/// The input container bounds
pub(super) input_bounds: Bounds<Pixels>,
Expand Down Expand Up @@ -570,6 +628,15 @@ impl<M: InputModeKind> InputBaseState<M> {
Some((layout.cursor_bounds?, layout.line_height))
}

/// Caret geometry from the latest prepaint, for caret-anchored popups.
///
/// Popups positioned in their deferred prepaint read this to anchor to
/// the current frame's caret instead of the previous frame's paint.
#[doc(hidden)]
pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
self.prepaint_caret_geometry
}

pub fn input_bounds(&self) -> Bounds<Pixels> {
self.input_bounds
}
Expand Down Expand Up @@ -780,6 +847,7 @@ impl<M: InputModeKind> InputBaseState<M> {
number_max: None,
mode: LayoutMode::default(),
last_layout: None,
prepaint_caret_geometry: None,
last_bounds: None,
last_selected_range: None,
column_select_start: None,
Expand Down
12 changes: 6 additions & 6 deletions crates/component/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ pub use gpui_base::input::{
IndentInline, InputEdit, InputEvent, InputHighlighter, InputHighlighterFactory, InputState,
Lsp, MaskPattern, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
MoveToEnd, MoveToEndOfLine, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveToStartOfLine,
MoveUp, Outdent, OutdentInline, Paste, Point, RangeDecoration, RangeDecorationCollection,
RangeDecorationStyle, Redo, Replace, Rope, RopeExt, RopeLines, Search, SelectAll, SelectToEnd,
SelectToEndOfLine, SelectToNextWordEnd, SelectToPreviousWordStart, SelectToStart,
SelectToStartOfLine, Selection, ShowCharacterPalette, ShowCompletions, ShowDocumentHandler,
TabSize, TextDecoration, TextDecorationCollection, TextareaState, ToggleCodeActions, Undo,
WrappingIndent,
MoveUp, Outdent, OutdentInline, Paste, Point, PrepaintCaretGeometry, RangeDecoration,
RangeDecorationCollection, RangeDecorationStyle, Redo, Replace, Rope, RopeExt, RopeLines,
Search, SelectAll, SelectToEnd, SelectToEndOfLine, SelectToNextWordEnd,
SelectToPreviousWordStart, SelectToStart, SelectToStartOfLine, Selection, ShowCharacterPalette,
ShowCompletions, ShowDocumentHandler, TabSize, TextDecoration, TextDecorationCollection,
TextareaState, ToggleCodeActions, Undo, WrappingIndent,
};
pub use gpui_base::input::{EditorMode, InputMode, InputModeKind, TextareaMode};
#[doc(hidden)]
Expand Down
132 changes: 132 additions & 0 deletions crates/component/src/input/popovers/caret_anchored.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Caret-anchored popups positioned from the input's current-frame geometry.
//!
//! The input stashes fresh caret geometry in its prepaint
//! ([`PrepaintCaretGeometry`]). Popups live inside `deferred()`, so their
//! prepaint runs after the input's prepaint but before its paint; resolving
//! the anchor there puts the popup at the current frame's caret instead of
//! the previous frame's.

use gpui::{
AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, Pixels, Point, Size, Style, WeakEntity, Window, px,
};

use crate::input::{EditorState, PrepaintCaretGeometry};

/// Computes the caret-anchored popup origin from fresh prepaint geometry.
///
/// Shared by the completion and code-action menus so both anchor to the
/// same point: just below the caret, nudged left. The result is in window
/// coordinates, matching where the caret itself is painted.
pub(crate) fn caret_popup_anchor(geometry: PrepaintCaretGeometry) -> Point<Pixels> {
Point::new(
geometry.cursor_bounds().origin.x - px(4.),
geometry.cursor_bounds().origin.y
+ geometry.scroll_offset().y
+ geometry.line_height()
+ px(4.),
)
}

/// A popup positioned at the caret from the input's current-frame prepaint.
///
/// The content is built in prepaint (not render) from the resolved anchor,
/// so position, hitboxes, accessibility bounds, and width/layout decisions
/// are all consistent with the current frame's caret. Always use inside
/// `deferred()` so the prepaint runs after the input's prepaint.
pub(crate) struct CaretAnchoredPopup {
editor: WeakEntity<EditorState>,
build: Box<dyn Fn(Point<Pixels>, &mut Window, &mut App) -> AnyElement>,
content: Option<AnyElement>,
}

impl CaretAnchoredPopup {
pub(crate) fn new(
editor: WeakEntity<EditorState>,
build: impl Fn(Point<Pixels>, &mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
Self {
editor,
build: Box::new(build),
content: None,
}
}
}

impl Element for CaretAnchoredPopup {
type RequestLayoutState = ();
type PrepaintState = ();

fn id(&self) -> Option<ElementId> {
None
}

fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}

fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
// The real layout happens in prepaint, once the anchor is resolved.
let layout_id = window.request_layout(Style::default(), [], cx);
(layout_id, ())
}

fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
let Some(editor) = self.editor.upgrade() else {
return;
};
let Some(geometry) = editor.read(cx).prepaint_caret_geometry() else {
return;
};
let anchor = caret_popup_anchor(geometry);
let mut content = (self.build)(anchor, window, cx);
let viewport = window.viewport_size();
content.layout_as_root(
Size {
width: AvailableSpace::Definite(viewport.width),
height: AvailableSpace::Definite(viewport.height),
},
window,
cx,
);
content.prepaint_at(anchor, window, cx);
self.content = Some(content);
}

fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
if let Some(content) = self.content.as_mut() {
content.paint(window, cx);
}
}
}

impl IntoElement for CaretAnchoredPopup {
type Element = Self;

fn into_element(self) -> Self::Element {
self
}
}
Loading
Loading