Skip to main content

gpui_base/input/base/
token_presentation.rs

1//! Presentation callbacks and measured geometry. None of this enters document history.
2use super::{InlineToken, InlineTokenSpan, InputBaseState, InputModeKind, rope_ext::RopeExt as _};
3use gpui::{AnyElement, App, Bounds, ClickEvent, Font, IntoElement, Pixels, Window};
4use std::{collections::HashMap, ops::Range, rc::Rc};
5
6/// Read-only context for a single inline renderer. Width is the full available row.
7#[derive(Clone)]
8pub struct InlineTokenContext {
9    span: InlineTokenSpan,
10    selected: bool,
11    disabled: bool,
12    readonly: bool,
13    line_height: Pixels,
14    available_width: Pixels,
15}
16impl InlineTokenContext {
17    pub fn token(&self) -> &InlineToken {
18        self.span.token()
19    }
20    pub fn range(&self) -> Range<usize> {
21        self.span.range()
22    }
23    pub fn is_selected(&self) -> bool {
24        self.selected
25    }
26    pub fn is_disabled(&self) -> bool {
27        self.disabled
28    }
29    pub fn is_readonly(&self) -> bool {
30        self.readonly
31    }
32    pub fn line_height(&self) -> Pixels {
33        self.line_height
34    }
35    pub fn available_width(&self) -> Pixels {
36        self.available_width
37    }
38}
39
40/// Current token snapshot delivered after releasing the editor's update borrow.
41#[derive(Clone)]
42pub struct InlineTokenClickEvent {
43    span: InlineTokenSpan,
44    bounds: Bounds<Pixels>,
45    event: ClickEvent,
46}
47impl InlineTokenClickEvent {
48    pub fn token(&self) -> &InlineToken {
49        self.span.token()
50    }
51    pub fn range(&self) -> Range<usize> {
52        self.span.range()
53    }
54    pub fn bounds(&self) -> Bounds<Pixels> {
55        self.bounds
56    }
57    /// The click that opened the token; keyboard activation reports a
58    /// keyboard click.
59    pub fn click(&self) -> &ClickEvent {
60        &self.event
61    }
62}
63
64/// Hover snapshot for one atomic inline token. Hover never selects or edits;
65/// it reports pointer presence so the application can show a tooltip or run
66/// custom logic.
67#[derive(Clone)]
68pub struct InlineTokenHoverEvent {
69    span: InlineTokenSpan,
70    bounds: Bounds<Pixels>,
71    hovered: bool,
72    range_utf16: (usize, usize),
73}
74impl InlineTokenHoverEvent {
75    pub fn token(&self) -> &InlineToken {
76        self.span.token()
77    }
78    pub fn range(&self) -> Range<usize> {
79        self.span.range()
80    }
81    pub fn bounds(&self) -> Bounds<Pixels> {
82        self.bounds
83    }
84    /// Whether the pointer entered (`true`) or left (`false`) the token.
85    pub fn is_hovered(&self) -> bool {
86        self.hovered
87    }
88    /// The token range in UTF-16 code units, captured when the event was
89    /// built. Exits delivered after the text changed report these entry
90    /// coordinates, since the byte range can no longer be converted against
91    /// the current text.
92    pub fn range_utf16(&self) -> (usize, usize) {
93        self.range_utf16
94    }
95}
96
97/// The retained hover presence: the entered span, its placed bounds, and its
98/// UTF-16 range as of entry. JavaScript string offsets shift with later edits,
99/// so an exit delivered after a change cannot recompute them from the current
100/// text and reuses these instead.
101#[derive(Clone)]
102pub(super) struct HoverSnapshot {
103    pub(super) span: InlineTokenSpan,
104    pub(super) bounds: Bounds<Pixels>,
105    pub(super) range_utf16: (usize, usize),
106}
107
108/// A renderer installed by a styled control. Not part of the supported API.
109#[doc(hidden)]
110pub type InlineTokenRenderer = Rc<dyn Fn(&InlineTokenContext, &mut Window, &mut App) -> AnyElement>;
111/// A click listener installed by a styled control. Not part of the supported API.
112#[doc(hidden)]
113pub type InlineTokenClickListener = Rc<dyn Fn(&InlineTokenClickEvent, &mut Window, &mut App)>;
114/// A hover listener installed by a styled control. Not part of the supported API.
115#[doc(hidden)]
116pub type InlineTokenHoverListener = Rc<dyn Fn(&InlineTokenHoverEvent, &mut Window, &mut App)>;
117
118/// Presentation shared by Base and styled controls. It owns no content.
119#[derive(Clone, Default)]
120pub(crate) struct InlineTokenPresentation {
121    renderer: Option<InlineTokenRenderer>,
122    listener: Option<InlineTokenClickListener>,
123    hover_listener: Option<InlineTokenHoverListener>,
124    secret: bool,
125}
126impl InlineTokenPresentation {
127    pub(crate) fn token<R: IntoElement>(
128        mut self,
129        render: impl Fn(&InlineTokenContext, &mut Window, &mut App) -> R + 'static,
130    ) -> Self {
131        self.renderer = Some(Rc::new(move |token, window, cx| {
132            render(token, window, cx).into_any_element()
133        }));
134        self
135    }
136    pub(crate) fn on_token_click(
137        mut self,
138        listener: impl Fn(&InlineTokenClickEvent, &mut Window, &mut App) + 'static,
139    ) -> Self {
140        self.listener = Some(Rc::new(listener));
141        self
142    }
143    pub(crate) fn on_token_hover(
144        mut self,
145        listener: impl Fn(&InlineTokenHoverEvent, &mut Window, &mut App) + 'static,
146    ) -> Self {
147        self.hover_listener = Some(Rc::new(listener));
148        self
149    }
150    pub(super) fn has_hover_listener(&self) -> bool {
151        self.hover_listener.is_some()
152    }
153    pub(super) fn has_listener(&self) -> bool {
154        self.listener.is_some()
155    }
156    pub(super) fn render(
157        &self,
158        token: &InlineTokenContext,
159        window: &mut Window,
160        cx: &mut App,
161    ) -> AnyElement {
162        if let Some(render) = &self.renderer {
163            render(token, window, cx)
164        } else {
165            gpui::div()
166                .child(token.token().label().clone())
167                .into_any_element()
168        }
169    }
170}
171use gpui::ParentElement as _;
172
173#[derive(Default)]
174pub(super) struct TokenLayoutCache {
175    pub(super) key: Option<(Font, Pixels, Pixels, Pixels, bool)>,
176    pub(super) revision: u64,
177    pub(super) unwrapped_width: Pixels,
178    pub(super) metrics: Rc<[(Range<usize>, Pixels)]>,
179    pub(super) widths: HashMap<InlineToken, Pixels>,
180}
181
182impl<M: InputModeKind> InputBaseState<M> {
183    /// Inject presentation from a view without editing or notifying the document.
184    pub(crate) fn set_token_presentation(&mut self, presentation: InlineTokenPresentation) {
185        self.token_presentation = presentation;
186    }
187    /// Install a styled control's renderer, click listener and secrecy without
188    /// editing or notifying the document. Not part of the supported API.
189    #[doc(hidden)]
190    pub fn install_token_presentation(
191        &mut self,
192        renderer: Option<InlineTokenRenderer>,
193        listener: Option<InlineTokenClickListener>,
194        secret: bool,
195    ) {
196        self.token_presentation = InlineTokenPresentation {
197            renderer,
198            listener,
199            hover_listener: self.token_presentation.hover_listener.clone(),
200            secret,
201        };
202    }
203    /// Install a styled control's token hover listener without editing or
204    /// notifying the document. Kept separate so the original installer keeps
205    /// compiling for existing consumers. Not part of the supported API.
206    #[doc(hidden)]
207    pub fn install_token_hover_presentation(
208        &mut self,
209        hover_listener: Option<InlineTokenHoverListener>,
210    ) {
211        self.token_presentation.hover_listener = hover_listener;
212    }
213    pub(super) fn tokens_visible(&self) -> bool {
214        !self.masked
215            && !self.token_presentation.secret
216            && self.mask_pattern.is_none()
217            && !self.token_spans().is_empty()
218    }
219    pub(super) fn token_context(
220        &self,
221        span: &InlineTokenSpan,
222        line_height: Pixels,
223        width: Pixels,
224    ) -> InlineTokenContext {
225        let range = span.range();
226        let selection = self.selected_range();
227        InlineTokenContext {
228            span: span.clone(),
229            selected: selection.start < range.end && range.start < selection.end,
230            disabled: self.disabled,
231            readonly: self.readonly,
232            line_height,
233            available_width: width,
234        }
235    }
236    /// The token starting at `start`, paired with the listener that opens it.
237    pub(super) fn token_activation(
238        &self,
239        start: usize,
240        bounds: Bounds<Pixels>,
241        event: ClickEvent,
242    ) -> Option<(InlineTokenClickListener, InlineTokenClickEvent)> {
243        if self.disabled || !self.tokens_visible() {
244            return None;
245        }
246        let span = self
247            .token_spans()
248            .iter()
249            .find(|span| span.range().start == start)?
250            .clone();
251        Some((
252            self.token_presentation.listener.clone()?,
253            InlineTokenClickEvent {
254                span,
255                bounds,
256                event,
257            },
258        ))
259    }
260    /// The token starting at `start`, paired with the hover listener.
261    /// Disabled tokens never enter; existing hover still receives its exit.
262    /// Readonly tokens report hover.
263    /// Records and clears the retained hover snapshot used for exit
264    /// reconciliation. An exit only clears the snapshot when it belongs to the
265    /// exiting token: GPUI dispatches a newly entered token before the exit of
266    /// the token the pointer left, so an older exit must not drop the newer
267    /// token's snapshot.
268    pub(super) fn token_hover(
269        &mut self,
270        start: usize,
271        bounds: Bounds<Pixels>,
272        hovered: bool,
273        expected_token: Option<&InlineToken>,
274    ) -> Option<(InlineTokenHoverListener, InlineTokenHoverEvent)> {
275        let listener = self.token_presentation.hover_listener.clone()?;
276        let matches = |snapshot: &HoverSnapshot| {
277            snapshot.span.range().start == start
278                && expected_token.is_none_or(|token| snapshot.span.token() == token)
279        };
280        let snapshot = if hovered {
281            if self.disabled || !self.tokens_visible() {
282                return None;
283            }
284            let span = self
285                .token_spans()
286                .iter()
287                .find(|span| {
288                    span.range().start == start
289                        && expected_token.is_none_or(|token| span.token() == token)
290                })?
291                .clone();
292            if self
293                .hovered_token
294                .as_ref()
295                .is_some_and(|snapshot| snapshot.span == span)
296            {
297                return None;
298            }
299            let range = span.range();
300            let snapshot = HoverSnapshot {
301                span,
302                bounds,
303                range_utf16: (
304                    self.text.offset_to_offset_utf16(range.start),
305                    self.text.offset_to_offset_utf16(range.end),
306                ),
307            };
308            if let Some(previous) = self.hovered_token.replace(snapshot.clone()) {
309                self.pending_token_hover_exits.push(previous);
310            }
311            snapshot
312        } else if self.hovered_token.as_ref().is_some_and(matches) {
313            self.hovered_token.take()?
314        } else {
315            let index = self.pending_token_hover_exits.iter().position(matches)?;
316            self.pending_token_hover_exits.remove(index)
317        };
318        Some((
319            listener,
320            InlineTokenHoverEvent {
321                span: snapshot.span,
322                bounds: snapshot.bounds,
323                hovered,
324                range_utf16: snapshot.range_utf16,
325            },
326        ))
327    }
328    /// Deliver exits from entry snapshots, even after edits or callback reentry
329    /// remove the token. Keep predecessors until their mouse exit is dispatched.
330    pub(super) fn reconcile_token_hover(
331        &mut self,
332    ) -> Option<(InlineTokenHoverListener, InlineTokenHoverEvent)> {
333        let is_placed = |snapshot: &HoverSnapshot| {
334            !self.disabled
335                && self.tokens_visible()
336                && self.token_presentation.has_hover_listener()
337                && self.token_bounds.contains_key(&snapshot.span.range().start)
338                && self.token_spans().contains(&snapshot.span)
339        };
340        let snapshot = if let Some(index) = self
341            .pending_token_hover_exits
342            .iter()
343            .position(|snapshot| !is_placed(snapshot))
344        {
345            self.pending_token_hover_exits.remove(index)
346        } else if self
347            .hovered_token
348            .as_ref()
349            .is_some_and(|snapshot| !is_placed(snapshot))
350        {
351            self.hovered_token.take()?
352        } else {
353            return None;
354        };
355        Some((
356            self.token_presentation.hover_listener.clone()?,
357            InlineTokenHoverEvent {
358                span: snapshot.span,
359                bounds: snapshot.bounds,
360                hovered: false,
361                range_utf16: snapshot.range_utf16,
362            },
363        ))
364    }
365    pub(super) fn token_is_secret(&self) -> bool {
366        self.token_presentation.secret
367    }
368}