Skip to main content

gpui_authoring/elements/
text.rs

1use crate::{
2    ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
3    HighlightStyle, Hitbox, HitboxBehavior, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent,
4    MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId,
5    TruncateFrom, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
6    register_tooltip_mouse_handlers, set_tooltip_on_window,
7};
8use anyhow::Context as _;
9use gpui_util::ResultExt;
10use itertools::Itertools;
11use smallvec::SmallVec;
12use std::{
13    borrow::Cow,
14    cell::{Cell, RefCell},
15    mem,
16    ops::{Deref, DerefMut, Range},
17    rc::Rc,
18    sync::Arc,
19};
20
21/// An [`Element`] that renders text.
22///
23/// In general, [`Text`] objects should be created via the [`text!`](crate::text) macro:
24/// ```rust
25/// # use gpui::*;
26/// # fn render() -> impl IntoElement {
27/// div().child(text!("hello"))
28/// # }
29/// ```
30/// ## IDs and Accessibility
31///
32/// [`Text`] elements have an ID. This ID is primarily used to produce nodes in
33/// the accessibility tree, which allows the text to be visible to screen
34/// readers and other assistive technologies.
35///
36/// This ID is stable across frames. If the same text, with the same ID, is
37/// present in two consecutive frames, no updates are reported to the screen
38/// reader. If the text changes, but the ID stays the same, then the screen
39/// reader will be notified that a text node's content has changed. **However**,
40/// if the ID changes, then the screen reader will be notified that a node has
41/// been removed, and a new node has been added.
42///
43/// When using the [`text!`](crate::text) macro, each invocation of the macro will get a
44/// unique ID, derived from its position in the source code (filename, line, and
45/// column). For example:
46/// ```rust
47/// # use gpui::*;
48/// let x = text!("hello");
49/// let y = text!("hello");
50/// // not equal, because different `text!` invocations produced them
51/// assert_ne!(x.id(), y.id());
52///
53/// fn make_text(s: &str) -> Text { text!(s) }
54/// let x = make_text("hello");
55/// let y = make_text("hello");
56/// // equal, because the same `text!` invocation produced them
57/// assert_eq!(x.id(), y.id());
58/// ```
59/// When the contents of an invocation of [`text!`](crate::text) do not change, this
60/// distinction is less relevant (with the caveat that you still need to take
61/// care to ensure that duplicate IDs do not appear).
62///
63/// However, when a [`text!`](crate::text) invocation's argument *does* change, you should
64/// consider whether this change should be reported as a node "updating its
65/// contents", or an old node being destroyed and a new node being created.
66#[derive(Debug, Clone)]
67pub struct Text {
68    id: Option<ElementId>,
69    text: SharedString,
70}
71
72impl Text {
73    /// Create a new [`Text`] element with a specific ID.
74    ///
75    /// If you want a unique ID to be assigned automatically, use the
76    /// [`text!`](crate::text) macro. The docs for [`Text`] have more detail about
77    /// choosing IDs.
78    #[inline]
79    pub const fn new(id: ElementId, text: SharedString) -> Self {
80        Self { id: Some(id), text }
81    }
82
83    /// Create a new [`Text`] element that is inaccessible to screen readers.
84    ///
85    /// In order for text to be accessible to screen readers, it must have an ID
86    /// provided. If you want text to be accessible, either use [`text!`](crate::text) to have
87    /// an ID automatically assigned, or use [`Text::new`] to manually assign an
88    /// ID.
89    ///
90    /// This function is intended for use inside custom UI components, where
91    /// accessible properties may be set on parent containers.
92    #[inline]
93    pub const fn new_inaccessible(text: SharedString) -> Self {
94        Self { id: None, text }
95    }
96
97    /// The ID of this [`Text`] element.
98    #[inline]
99    pub const fn id(&self) -> Option<&ElementId> {
100        self.id.as_ref()
101    }
102
103    /// Produce a new [`Text`] with the given `id`.
104    pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
105        self.id = Some(id.into());
106        self
107    }
108
109    /// The text that this [`Text`] element will display.
110    #[inline]
111    pub const fn text(&self) -> &SharedString {
112        &self.text
113    }
114}
115
116impl Deref for Text {
117    type Target = SharedString;
118    fn deref(&self) -> &Self::Target {
119        &self.text
120    }
121}
122
123impl DerefMut for Text {
124    fn deref_mut(&mut self) -> &mut Self::Target {
125        &mut self.text
126    }
127}
128
129/// Trivial hash function for the location information produced by the [`text!`](crate::text)
130/// macro. Not covered by semver guarantees. Performance is not particularly
131/// significant because it's only used on small strings in const contexts.
132#[doc(hidden)]
133pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
134    const BASIS: u64 = 0xcbf29ce484222325;
135    const PRIME: u64 = 0x100000001b3;
136
137    let bytes = s.as_bytes();
138    let mut hash = BASIS;
139    let mut i = 0;
140    while i < bytes.len() {
141        hash ^= bytes[i] as u64;
142        hash = hash.wrapping_mul(PRIME);
143        i += 1;
144    }
145    hash
146}
147
148/// Create a new [`Text`] element.
149///
150/// ```rust
151/// # use gpui::*;
152/// let a = text!("hello");
153/// let b = text!(id = "farewell-message", "hello");
154///
155/// ```
156///
157/// Text created with this macro is *accessible*. The macro generates an ID
158/// based on the source location. See the docs for [`Text`] for a more in-depth
159/// explanation of the significance of the ID of a [`Text`] element.
160#[macro_export]
161macro_rules! text {
162    (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }};
163    ($text:expr) => {{
164        const ID: &'static str = concat!(file!(), "/", line!(), ":", column!());
165        const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID);
166        $crate::Text::new($crate::ElementId::Integer(HASH), $text.into())
167    }};
168}
169
170impl IntoElement for Text {
171    type Element = Self;
172    #[inline]
173    fn into_element(self) -> Self::Element {
174        self
175    }
176}
177
178impl Element for Text {
179    type RequestLayoutState = TextLayout;
180    type PrepaintState = ();
181
182    fn id(&self) -> Option<ElementId> {
183        self.id.clone()
184    }
185
186    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
187        None
188    }
189
190    fn a11y_role(&self) -> Option<accesskit::Role> {
191        if self.id.is_some() {
192            Some(accesskit::Role::Label)
193        } else {
194            None
195        }
196    }
197
198    fn write_a11y_info(&self, node: &mut accesskit::Node) {
199        node.set_value(self.text.to_string());
200    }
201
202    fn request_layout(
203        &mut self,
204        id: Option<&GlobalElementId>,
205        window: &mut Window,
206        cx: &mut App,
207    ) -> (LayoutId, Self::RequestLayoutState) {
208        <SharedString as Element>::request_layout(&mut self.text, id, window, cx)
209    }
210
211    fn prepaint(
212        &mut self,
213        id: Option<&GlobalElementId>,
214        bounds: Bounds<Pixels>,
215        request_layout: &mut Self::RequestLayoutState,
216        window: &mut Window,
217        cx: &mut App,
218    ) -> Self::PrepaintState {
219        <SharedString as Element>::prepaint(&mut self.text, id, bounds, request_layout, window, cx)
220    }
221
222    fn paint(
223        &mut self,
224        id: Option<&GlobalElementId>,
225        bounds: Bounds<Pixels>,
226        request_layout: &mut Self::RequestLayoutState,
227        prepaint: &mut Self::PrepaintState,
228        window: &mut Window,
229        cx: &mut App,
230    ) {
231        <SharedString as Element>::paint(
232            &mut self.text,
233            id,
234            bounds,
235            request_layout,
236            prepaint,
237            window,
238            cx,
239        );
240    }
241}
242
243impl Element for &'static str {
244    type RequestLayoutState = TextLayout;
245    type PrepaintState = ();
246
247    fn id(&self) -> Option<ElementId> {
248        None
249    }
250
251    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
252        None
253    }
254
255    fn request_layout(
256        &mut self,
257        _id: Option<&GlobalElementId>,
258        window: &mut Window,
259        cx: &mut App,
260    ) -> (LayoutId, Self::RequestLayoutState) {
261        let mut state = TextLayout::default();
262        let layout_id = state.layout(SharedString::from(*self), None, window, cx);
263        (layout_id, state)
264    }
265
266    fn prepaint(
267        &mut self,
268        _id: Option<&GlobalElementId>,
269        bounds: Bounds<Pixels>,
270        text_layout: &mut Self::RequestLayoutState,
271        _window: &mut Window,
272        _cx: &mut App,
273    ) {
274        text_layout.prepaint(bounds, self)
275    }
276
277    fn paint(
278        &mut self,
279        _id: Option<&GlobalElementId>,
280        _bounds: Bounds<Pixels>,
281        text_layout: &mut TextLayout,
282        _: &mut (),
283        window: &mut Window,
284        cx: &mut App,
285    ) {
286        text_layout.paint(self, window, cx)
287    }
288}
289
290impl IntoElement for &'static str {
291    type Element = Self;
292
293    fn into_element(self) -> Self::Element {
294        self
295    }
296}
297
298impl IntoElement for String {
299    type Element = SharedString;
300
301    fn into_element(self) -> Self::Element {
302        self.into()
303    }
304}
305
306impl IntoElement for Cow<'static, str> {
307    type Element = SharedString;
308
309    fn into_element(self) -> Self::Element {
310        self.into()
311    }
312}
313
314impl Element for SharedString {
315    type RequestLayoutState = TextLayout;
316    type PrepaintState = ();
317
318    fn id(&self) -> Option<ElementId> {
319        None
320    }
321
322    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
323        None
324    }
325
326    fn request_layout(
327        &mut self,
328        _id: Option<&GlobalElementId>,
329        window: &mut Window,
330        cx: &mut App,
331    ) -> (LayoutId, Self::RequestLayoutState) {
332        let mut state = TextLayout::default();
333        let layout_id = state.layout(self.clone(), None, window, cx);
334        (layout_id, state)
335    }
336
337    fn prepaint(
338        &mut self,
339        _id: Option<&GlobalElementId>,
340        bounds: Bounds<Pixels>,
341        text_layout: &mut Self::RequestLayoutState,
342        _window: &mut Window,
343        _cx: &mut App,
344    ) {
345        text_layout.prepaint(bounds, self.as_ref())
346    }
347
348    fn paint(
349        &mut self,
350        _id: Option<&GlobalElementId>,
351        _bounds: Bounds<Pixels>,
352        text_layout: &mut Self::RequestLayoutState,
353        _: &mut Self::PrepaintState,
354        window: &mut Window,
355        cx: &mut App,
356    ) {
357        text_layout.paint(self.as_ref(), window, cx)
358    }
359}
360
361impl IntoElement for SharedString {
362    type Element = Self;
363
364    fn into_element(self) -> Self::Element {
365        self
366    }
367}
368
369/// Renders text with runs of different styles.
370///
371/// Callers are responsible for setting the correct style for each run.
372/// For text with a uniform style, you can usually avoid calling this constructor
373/// and just pass text directly.
374pub struct StyledText {
375    text: SharedString,
376    runs: Option<Vec<TextRun>>,
377    delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
378    delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
379    layout: TextLayout,
380}
381
382impl StyledText {
383    /// Construct a new styled text element from the given string.
384    pub fn new(text: impl Into<SharedString>) -> Self {
385        StyledText {
386            text: text.into(),
387            runs: None,
388            delayed_highlights: None,
389            delayed_font_family_overrides: None,
390            layout: TextLayout::default(),
391        }
392    }
393
394    /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
395    pub fn layout(&self) -> &TextLayout {
396        &self.layout
397    }
398
399    /// Set the styling attributes for the given text, as well as
400    /// as any ranges of text that have had their style customized.
401    pub fn with_default_highlights(
402        mut self,
403        default_style: &TextStyle,
404        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
405    ) -> Self {
406        debug_assert!(
407            self.delayed_highlights.is_none(),
408            "Can't use `with_default_highlights` and `with_highlights`"
409        );
410        let runs = Self::compute_runs(&self.text, default_style, highlights);
411        self.with_runs(runs)
412    }
413
414    /// Set the styling attributes for the given text, as well as
415    /// as any ranges of text that have had their style customized.
416    pub fn with_highlights(
417        mut self,
418        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
419    ) -> Self {
420        debug_assert!(
421            self.runs.is_none(),
422            "Can't use `with_highlights` and `with_default_highlights`"
423        );
424        self.delayed_highlights = Some(
425            highlights
426                .into_iter()
427                .inspect(|(run, _)| {
428                    debug_assert!(self.text.is_char_boundary(run.start));
429                    debug_assert!(self.text.is_char_boundary(run.end));
430                })
431                .collect::<Vec<_>>(),
432        );
433        self
434    }
435
436    fn compute_runs(
437        text: &str,
438        default_style: &TextStyle,
439        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
440    ) -> Vec<TextRun> {
441        let mut runs = Vec::new();
442        let mut ix = 0;
443        for (range, highlight) in highlights {
444            if ix < range.start {
445                debug_assert!(text.is_char_boundary(range.start));
446                runs.push(default_style.clone().to_run(range.start - ix));
447            }
448            debug_assert!(text.is_char_boundary(range.end));
449            runs.push(
450                default_style
451                    .clone()
452                    .highlight(highlight)
453                    .to_run(range.len()),
454            );
455            ix = range.end;
456        }
457        if ix < text.len() {
458            runs.push(default_style.to_run(text.len() - ix));
459        }
460        runs
461    }
462
463    /// Override the font family for specific byte ranges of the text.
464    ///
465    /// This is resolved lazily at layout time, so the overrides are applied
466    /// on top of the inherited text style from the parent element.
467    /// Can be combined with [`with_highlights`](Self::with_highlights).
468    ///
469    /// The overrides must be sorted by range start and non-overlapping.
470    /// Each override range must fall on character boundaries.
471    pub fn with_font_family_overrides(
472        mut self,
473        overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
474    ) -> Self {
475        self.delayed_font_family_overrides = Some(
476            overrides
477                .into_iter()
478                .inspect(|(range, _)| {
479                    debug_assert!(self.text.is_char_boundary(range.start));
480                    debug_assert!(self.text.is_char_boundary(range.end));
481                })
482                .collect(),
483        );
484        self
485    }
486
487    fn apply_font_family_overrides(
488        runs: &mut [TextRun],
489        overrides: &[(Range<usize>, SharedString)],
490    ) {
491        let mut byte_offset = 0;
492        let mut override_idx = 0;
493        for run in runs.iter_mut() {
494            let run_end = byte_offset + run.len;
495            while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
496                override_idx += 1;
497            }
498            if override_idx < overrides.len() {
499                let (ref range, ref family) = overrides[override_idx];
500                if byte_offset >= range.start && run_end <= range.end {
501                    run.font.family = family.clone();
502                }
503            }
504            byte_offset = run_end;
505        }
506    }
507
508    /// Set the text runs for this piece of text.
509    pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
510        let mut text = &*self.text;
511        for run in &runs {
512            text = text.get(run.len..).unwrap_or_else(|| {
513                #[cfg(debug_assertions)]
514                panic!("invalid text run. Text: '{text}', run: {run:?}");
515                #[cfg(not(debug_assertions))]
516                panic!("invalid text run");
517            });
518        }
519        assert!(text.is_empty(), "invalid text run");
520        self.runs = Some(runs);
521        self
522    }
523}
524
525impl Element for StyledText {
526    type RequestLayoutState = ();
527    type PrepaintState = ();
528
529    fn id(&self) -> Option<ElementId> {
530        None
531    }
532
533    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
534        None
535    }
536
537    fn request_layout(
538        &mut self,
539        _id: Option<&GlobalElementId>,
540        window: &mut Window,
541        cx: &mut App,
542    ) -> (LayoutId, Self::RequestLayoutState) {
543        let font_family_overrides = self.delayed_font_family_overrides.take();
544        let mut runs = self.runs.take().or_else(|| {
545            self.delayed_highlights.take().map(|delayed_highlights| {
546                Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
547            })
548        });
549
550        if let Some(ref overrides) = font_family_overrides {
551            let runs =
552                runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
553            Self::apply_font_family_overrides(runs, overrides);
554        }
555
556        let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
557        (layout_id, ())
558    }
559
560    fn prepaint(
561        &mut self,
562        _id: Option<&GlobalElementId>,
563        bounds: Bounds<Pixels>,
564        _: &mut Self::RequestLayoutState,
565        _window: &mut Window,
566        _cx: &mut App,
567    ) {
568        self.layout.prepaint(bounds, &self.text)
569    }
570
571    fn paint(
572        &mut self,
573        _id: Option<&GlobalElementId>,
574        _bounds: Bounds<Pixels>,
575        _: &mut Self::RequestLayoutState,
576        _: &mut Self::PrepaintState,
577        window: &mut Window,
578        cx: &mut App,
579    ) {
580        self.layout.paint(&self.text, window, cx)
581    }
582}
583
584impl IntoElement for StyledText {
585    type Element = Self;
586
587    fn into_element(self) -> Self::Element {
588        self
589    }
590}
591
592/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
593#[derive(Default, Clone)]
594pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
595
596#[derive(Clone, Copy)]
597enum TextPaintPass {
598    All,
599    Background,
600    Foreground,
601}
602
603struct TextLayoutInner {
604    len: usize,
605    lines: SmallVec<[WrappedLine; 1]>,
606    line_height: Pixels,
607    wrap_width: Option<Pixels>,
608    truncate_width: Option<Pixels>,
609    size: Option<Size<Pixels>>,
610    bounds: Option<Bounds<Pixels>>,
611}
612
613impl TextLayout {
614    fn layout(
615        &self,
616        text: SharedString,
617        runs: Option<Vec<TextRun>>,
618        window: &mut Window,
619        _: &mut App,
620    ) -> LayoutId {
621        let text_style = window.text_style();
622        let font_size = text_style.font_size.to_pixels(window.rem_size());
623        let line_height = window.pixel_snap(
624            text_style
625                .line_height
626                .to_pixels(font_size.into(), window.rem_size()),
627        );
628
629        let runs = if let Some(runs) = runs {
630            runs
631        } else {
632            vec![text_style.to_run(text.len())]
633        };
634        window.request_measured_layout(Default::default(), {
635            let element_state = self.clone();
636
637            move |known_dimensions, available_space, window, cx| {
638                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
639                    known_dimensions.width.or(match available_space.width {
640                        crate::AvailableSpace::Definite(x) => Some(x),
641                        _ => None,
642                    })
643                } else {
644                    None
645                };
646
647                let (truncate_width, truncation_affix, truncate_from) =
648                    if let Some(text_overflow) = text_style.text_overflow.clone() {
649                        let width = known_dimensions.width.or(match available_space.width {
650                            crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
651                                Some(max_lines) => Some(x * max_lines),
652                                None => Some(x),
653                            },
654                            _ => None,
655                        });
656
657                        match text_overflow {
658                            TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
659                            TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
660                            TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle),
661                        }
662                    } else {
663                        (None, "".into(), TruncateFrom::End)
664                    };
665
666                // Only use cached layout if:
667                // 1. We have a cached size
668                // 2. wrap_width matches (or both are None)
669                // 3. truncate_width is None (if truncate_width is Some, we need to re-layout
670                //    because the previous layout may have been computed without truncation)
671                // 4. the cached layout was not truncated (a truncated layout answers an
672                //    unconstrained probe with the truncated size, which poisons intrinsic
673                //    sizing with whatever width some earlier measure pass happened to use)
674                if let Some(text_layout) = element_state.0.borrow().as_ref()
675                    && let Some(size) = text_layout.size
676                    && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
677                    && truncate_width.is_none()
678                    && text_layout.truncate_width.is_none()
679                {
680                    return size;
681                }
682
683                let mut line_wrapper = cx
684                    .text_system()
685                    .clone()
686                    .line_wrapper(text_style.font(), font_size);
687                let (text, runs) = if let Some(truncate_width) = truncate_width {
688                    if let Some(max_lines) = text_style.line_clamp
689                        && let Some(wrap_width) = wrap_width
690                    {
691                        line_wrapper.truncate_wrapped_line(
692                            text.clone(),
693                            wrap_width,
694                            max_lines,
695                            &truncation_affix,
696                            &runs,
697                            truncate_from,
698                        )
699                    } else if let Some(unclipped) = window
700                        .text_system()
701                        .shape_text(text.clone(), font_size, &runs, None, None)
702                        .log_err()
703                        && unclipped
704                            .iter()
705                            .all(|line| line.size(line_height).width <= truncate_width)
706                    {
707                        // The truncation decision below sums per-character advances,
708                        // which overestimates the shaped width (no kerning), truncating
709                        // text that fits exactly in its measured width. Skip truncation
710                        // whenever the honestly-shaped text fits; the shaping result
711                        // comes from the line layout cache when the same text was
712                        // already measured untruncated this frame.
713                        (text.clone(), Cow::Borrowed(&*runs))
714                    } else {
715                        line_wrapper.truncate_line(
716                            text.clone(),
717                            truncate_width,
718                            &truncation_affix,
719                            &runs,
720                            truncate_from,
721                        )
722                    }
723                } else {
724                    (text.clone(), Cow::Borrowed(&*runs))
725                };
726                let len = text.len();
727
728                let Some(lines) = window
729                    .text_system()
730                    .shape_text(
731                        text,
732                        font_size,
733                        &runs,
734                        wrap_width,            // Wrap if we know the width.
735                        text_style.line_clamp, // Limit the number of lines if line_clamp is set.
736                    )
737                    .log_err()
738                else {
739                    element_state.0.borrow_mut().replace(TextLayoutInner {
740                        lines: Default::default(),
741                        len: 0,
742                        line_height,
743                        wrap_width,
744                        truncate_width,
745                        size: Some(Size::default()),
746                        bounds: None,
747                    });
748                    return Size::default();
749                };
750
751                let mut size: Size<Pixels> = Size::default();
752                for line in &lines {
753                    let line_size = line.size(line_height);
754                    size.height += line_size.height;
755                    size.width = size.width.max(line_size.width).ceil();
756                }
757
758                element_state.0.borrow_mut().replace(TextLayoutInner {
759                    lines,
760                    len,
761                    line_height,
762                    wrap_width,
763                    truncate_width,
764                    size: Some(size),
765                    bounds: None,
766                });
767
768                size
769            }
770        })
771    }
772
773    fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
774        let mut element_state = self.0.borrow_mut();
775        let element_state = element_state
776            .as_mut()
777            .with_context(|| format!("measurement has not been performed on {text}"))
778            .unwrap();
779        element_state.bounds = Some(bounds);
780    }
781
782    fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
783        self.paint_lines(TextPaintPass::All, window, cx)
784            .with_context(|| format!("failed to paint {text}"))
785            .unwrap();
786    }
787
788    /// Paints the backgrounds of the text runs without the glyphs, so that callers can paint
789    /// content, such as highlights, between the backgrounds and the text. Follow it with
790    /// [`Self::paint_foreground`] to paint the glyphs.
791    pub fn paint_background(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
792        self.paint_lines(TextPaintPass::Background, window, cx)
793    }
794
795    /// Paints the glyphs and decorations of the text without the run backgrounds.
796    pub fn paint_foreground(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
797        self.paint_lines(TextPaintPass::Foreground, window, cx)
798    }
799
800    fn paint_lines(
801        &self,
802        pass: TextPaintPass,
803        window: &mut Window,
804        cx: &mut App,
805    ) -> anyhow::Result<()> {
806        let element_state = self.0.borrow();
807        let element_state = element_state
808            .as_ref()
809            .context("measurement has not been performed")?;
810        let bounds = element_state
811            .bounds
812            .context("prepaint has not been performed")?;
813
814        let line_height = element_state.line_height;
815        let mut line_origin = bounds.origin;
816        let text_style = window.text_style();
817        for line in &element_state.lines {
818            if matches!(pass, TextPaintPass::All | TextPaintPass::Background) {
819                line.paint_background(
820                    line_origin,
821                    line_height,
822                    text_style.text_align,
823                    Some(bounds),
824                    window,
825                    cx,
826                )
827                .log_err();
828            }
829            if matches!(pass, TextPaintPass::All | TextPaintPass::Foreground) {
830                line.paint(
831                    line_origin,
832                    line_height,
833                    text_style.text_align,
834                    Some(bounds),
835                    window,
836                    cx,
837                )
838                .log_err();
839            }
840            line_origin.y += line.size(line_height).height;
841        }
842        Ok(())
843    }
844
845    /// Get the byte index into the input of the pixel position.
846    pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
847        let element_state = self.0.borrow();
848        let element_state = element_state
849            .as_ref()
850            .expect("measurement has not been performed");
851        let bounds = element_state
852            .bounds
853            .expect("prepaint has not been performed");
854
855        if position.y < bounds.top() {
856            return Err(0);
857        }
858
859        let line_height = element_state.line_height;
860        let mut line_origin = bounds.origin;
861        let mut line_start_ix = 0;
862        for line in &element_state.lines {
863            let line_bottom = line_origin.y + line.size(line_height).height;
864            if position.y > line_bottom {
865                line_origin.y = line_bottom;
866                line_start_ix += line.len() + 1;
867            } else {
868                let position_within_line = position - line_origin;
869                match line.index_for_position(position_within_line, line_height) {
870                    Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
871                    Err(index_within_line) => return Err(line_start_ix + index_within_line),
872                }
873            }
874        }
875
876        Err(line_start_ix.saturating_sub(1))
877    }
878
879    /// Get the pixel position for the given byte index.
880    pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
881        let element_state = self.0.borrow();
882        let element_state = element_state
883            .as_ref()
884            .expect("measurement has not been performed");
885        let bounds = element_state
886            .bounds
887            .expect("prepaint has not been performed");
888        let line_height = element_state.line_height;
889
890        let mut line_origin = bounds.origin;
891        let mut line_start_ix = 0;
892
893        for line in &element_state.lines {
894            let line_end_ix = line_start_ix + line.len();
895            if index < line_start_ix {
896                break;
897            } else if index > line_end_ix {
898                line_origin.y += line.size(line_height).height;
899                line_start_ix = line_end_ix + 1;
900                continue;
901            } else {
902                let ix_within_line = index - line_start_ix;
903                return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
904            }
905        }
906
907        None
908    }
909
910    /// Retrieve the layout for the line containing the given byte index.
911    pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
912        let element_state = self.0.borrow();
913        let element_state = element_state
914            .as_ref()
915            .expect("measurement has not been performed");
916        let mut line_start_ix = 0;
917
918        for line in &element_state.lines {
919            let line_end_ix = line_start_ix + line.len();
920            if index < line_start_ix {
921                break;
922            } else if index > line_end_ix {
923                line_start_ix = line_end_ix + 1;
924                continue;
925            } else {
926                return Some(line.layout.clone());
927            }
928        }
929
930        None
931    }
932
933    /// Retrieve all line layouts in source order.
934    pub fn line_layouts(&self) -> SmallVec<[Arc<WrappedLineLayout>; 1]> {
935        self.0
936            .borrow()
937            .as_ref()
938            .expect("measurement has not been performed")
939            .lines
940            .iter()
941            .map(|line| line.layout.clone())
942            .collect()
943    }
944
945    /// The bounds of this layout.
946    pub fn bounds(&self) -> Bounds<Pixels> {
947        self.0.borrow().as_ref().unwrap().bounds.unwrap()
948    }
949
950    /// The line height for this layout.
951    pub fn line_height(&self) -> Pixels {
952        self.0.borrow().as_ref().unwrap().line_height
953    }
954
955    /// The UTF-8 length of the underlying text.
956    pub fn len(&self) -> usize {
957        self.0.borrow().as_ref().unwrap().len
958    }
959
960    /// The text for this layout.
961    pub fn text(&self) -> String {
962        self.0
963            .borrow()
964            .as_ref()
965            .unwrap()
966            .lines
967            .iter()
968            .map(|s| &s.text)
969            .join("\n")
970    }
971
972    /// The text for this layout (with soft-wraps as newlines)
973    pub fn wrapped_text(&self) -> String {
974        let mut accumulator = String::new();
975
976        for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
977            let mut seen = 0;
978            for boundary in wrapped.layout.wrap_boundaries.iter() {
979                let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
980                    [boundary.glyph_ix]
981                    .index;
982
983                accumulator.push_str(&wrapped.text[seen..index]);
984                accumulator.push('\n');
985                seen = index;
986            }
987            accumulator.push_str(&wrapped.text[seen..]);
988            accumulator.push('\n');
989        }
990        // Remove trailing newline
991        accumulator.pop();
992        accumulator
993    }
994}
995
996/// A text element that can be interacted with.
997pub struct InteractiveText {
998    element_id: ElementId,
999    text: StyledText,
1000    click_listener:
1001        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
1002    hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
1003    tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
1004    tooltip_id: Option<TooltipId>,
1005    clickable_ranges: Vec<Range<usize>>,
1006}
1007
1008struct InteractiveTextClickEvent {
1009    mouse_down_index: usize,
1010    mouse_up_index: usize,
1011}
1012
1013#[doc(hidden)]
1014#[derive(Default)]
1015pub struct InteractiveTextState {
1016    mouse_down_index: Rc<Cell<Option<usize>>>,
1017    hovered_index: Rc<Cell<Option<usize>>>,
1018    active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
1019    long_press_tooltip_active: Rc<Cell<bool>>,
1020}
1021
1022/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
1023impl InteractiveText {
1024    /// Creates a new InteractiveText from the given text.
1025    pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
1026        Self {
1027            element_id: id.into(),
1028            text,
1029            click_listener: None,
1030            hover_listener: None,
1031            tooltip_builder: None,
1032            tooltip_id: None,
1033            clickable_ranges: Vec::new(),
1034        }
1035    }
1036
1037    /// on_click is called when the user clicks on one of the given ranges, passing the index of
1038    /// the clicked range.
1039    pub fn on_click(
1040        mut self,
1041        ranges: Vec<Range<usize>>,
1042        listener: impl Fn(usize, &mut Window, &mut App) + 'static,
1043    ) -> Self {
1044        self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
1045            for (range_ix, range) in ranges.iter().enumerate() {
1046                if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
1047                {
1048                    listener(range_ix, window, cx);
1049                }
1050            }
1051        }));
1052        self.clickable_ranges = ranges;
1053        self
1054    }
1055
1056    /// on_hover is called when the mouse moves over a character within the text, passing the
1057    /// index of the hovered character, or None if the mouse leaves the text.
1058    pub fn on_hover(
1059        mut self,
1060        listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
1061    ) -> Self {
1062        self.hover_listener = Some(Box::new(listener));
1063        self
1064    }
1065
1066    /// tooltip lets you specify a tooltip for a given character index in the string.
1067    pub fn tooltip(
1068        mut self,
1069        builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
1070    ) -> Self {
1071        self.tooltip_builder = Some(Rc::new(builder));
1072        self
1073    }
1074}
1075
1076impl Element for InteractiveText {
1077    type RequestLayoutState = ();
1078    type PrepaintState = Hitbox;
1079
1080    fn id(&self) -> Option<ElementId> {
1081        Some(self.element_id.clone())
1082    }
1083
1084    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1085        None
1086    }
1087
1088    fn a11y_role(&self) -> Option<accesskit::Role> {
1089        Some(accesskit::Role::Label)
1090    }
1091
1092    fn write_a11y_info(&self, node: &mut accesskit::Node) {
1093        node.set_value(self.text.text.to_string());
1094    }
1095
1096    fn request_layout(
1097        &mut self,
1098        _id: Option<&GlobalElementId>,
1099        window: &mut Window,
1100        cx: &mut App,
1101    ) -> (LayoutId, Self::RequestLayoutState) {
1102        self.text.request_layout(None, window, cx)
1103    }
1104
1105    fn prepaint(
1106        &mut self,
1107        global_id: Option<&GlobalElementId>,
1108        bounds: Bounds<Pixels>,
1109        state: &mut Self::RequestLayoutState,
1110        window: &mut Window,
1111        cx: &mut App,
1112    ) -> Hitbox {
1113        window.with_optional_element_state::<InteractiveTextState, _>(
1114            global_id,
1115            |interactive_state, window| {
1116                let mut interactive_state = interactive_state
1117                    .map(|interactive_state| interactive_state.unwrap_or_default());
1118
1119                if let Some(interactive_state) = interactive_state.as_mut() {
1120                    if self.tooltip_builder.is_some() {
1121                        self.tooltip_id =
1122                            set_tooltip_on_window(&interactive_state.active_tooltip, window);
1123                    } else {
1124                        // If there is no longer a tooltip builder, remove the active tooltip.
1125                        interactive_state.long_press_tooltip_active.set(false);
1126                        interactive_state.active_tooltip.take();
1127                    }
1128                }
1129
1130                self.text.prepaint(None, bounds, state, window, cx);
1131                let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1132                (hitbox, interactive_state)
1133            },
1134        )
1135    }
1136
1137    fn paint(
1138        &mut self,
1139        global_id: Option<&GlobalElementId>,
1140        bounds: Bounds<Pixels>,
1141        _: &mut Self::RequestLayoutState,
1142        hitbox: &mut Hitbox,
1143        window: &mut Window,
1144        cx: &mut App,
1145    ) {
1146        let current_view = window.current_view();
1147        let text_layout = self.text.layout().clone();
1148        window.with_element_state::<InteractiveTextState, _>(
1149            global_id.unwrap(),
1150            |interactive_state, window| {
1151                let mut interactive_state = interactive_state.unwrap_or_default();
1152                if let Some(click_listener) = self.click_listener.take() {
1153                    let mouse_position = window.mouse_position();
1154                    if let Ok(ix) = text_layout.index_for_position(mouse_position)
1155                        && self
1156                            .clickable_ranges
1157                            .iter()
1158                            .any(|range| range.contains(&ix))
1159                    {
1160                        window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
1161                    }
1162
1163                    let text_layout = text_layout.clone();
1164                    let mouse_down = interactive_state.mouse_down_index.clone();
1165                    if let Some(mouse_down_index) = mouse_down.get() {
1166                        let hitbox = hitbox.clone();
1167                        let clickable_ranges = mem::take(&mut self.clickable_ranges);
1168                        window.on_mouse_event(
1169                            move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
1170                                if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1171                                    if let Ok(mouse_up_index) =
1172                                        text_layout.index_for_position(event.position)
1173                                    {
1174                                        click_listener(
1175                                            &clickable_ranges,
1176                                            InteractiveTextClickEvent {
1177                                                mouse_down_index,
1178                                                mouse_up_index,
1179                                            },
1180                                            window,
1181                                            cx,
1182                                        )
1183                                    }
1184
1185                                    mouse_down.take();
1186                                    window.refresh();
1187                                }
1188                            },
1189                        );
1190                    } else {
1191                        let hitbox = hitbox.clone();
1192                        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
1193                            if phase == DispatchPhase::Bubble
1194                                && hitbox.is_hovered(window)
1195                                && let Ok(mouse_down_index) =
1196                                    text_layout.index_for_position(event.position)
1197                            {
1198                                mouse_down.set(Some(mouse_down_index));
1199                                window.refresh();
1200                            }
1201                        });
1202                    }
1203                }
1204
1205                window.on_mouse_event({
1206                    let mut hover_listener = self.hover_listener.take();
1207                    let hitbox = hitbox.clone();
1208                    let text_layout = text_layout.clone();
1209                    let hovered_index = interactive_state.hovered_index.clone();
1210                    move |event: &MouseMoveEvent, phase, window, cx| {
1211                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1212                            let current = hovered_index.get();
1213                            let updated = text_layout.index_for_position(event.position).ok();
1214                            if current != updated {
1215                                hovered_index.set(updated);
1216                                if let Some(hover_listener) = hover_listener.as_ref() {
1217                                    hover_listener(updated, event.clone(), window, cx);
1218                                }
1219                                cx.notify(current_view);
1220                            }
1221                        }
1222                    }
1223                });
1224
1225                if let Some(tooltip_builder) = self.tooltip_builder.clone() {
1226                    let active_tooltip = interactive_state.active_tooltip.clone();
1227                    let build_tooltip = Rc::new({
1228                        let tooltip_is_hoverable = false;
1229                        let text_layout = text_layout.clone();
1230                        move |window: &mut Window, cx: &mut App| {
1231                            text_layout
1232                                .index_for_position(window.mouse_position())
1233                                .ok()
1234                                .and_then(|position| tooltip_builder(position, window, cx))
1235                                .map(|view| (view, tooltip_is_hoverable))
1236                        }
1237                    });
1238
1239                    // Use bounds instead of testing hitbox since this is called during prepaint.
1240                    let check_is_hovered_during_prepaint = Rc::new({
1241                        let source_bounds = hitbox.bounds;
1242                        let text_layout = text_layout.clone();
1243                        let pending_mouse_down = interactive_state.mouse_down_index.clone();
1244                        move |window: &Window| {
1245                            text_layout
1246                                .index_for_position(window.mouse_position())
1247                                .is_ok()
1248                                && source_bounds.contains(&window.mouse_position())
1249                                && pending_mouse_down.get().is_none()
1250                        }
1251                    });
1252
1253                    let check_is_hovered = Rc::new({
1254                        let hitbox = hitbox.clone();
1255                        let text_layout = text_layout.clone();
1256                        let pending_mouse_down = interactive_state.mouse_down_index.clone();
1257                        move |window: &Window| {
1258                            text_layout
1259                                .index_for_position(window.mouse_position())
1260                                .is_ok()
1261                                && hitbox.is_hovered(window)
1262                                && pending_mouse_down.get().is_none()
1263                        }
1264                    });
1265
1266                    register_tooltip_mouse_handlers(
1267                        &active_tooltip,
1268                        self.tooltip_id,
1269                        build_tooltip,
1270                        check_is_hovered,
1271                        check_is_hovered_during_prepaint,
1272                        interactive_state.long_press_tooltip_active.clone(),
1273                        None,
1274                        window,
1275                    );
1276                }
1277
1278                self.text.paint(None, bounds, &mut (), &mut (), window, cx);
1279
1280                ((), interactive_state)
1281            },
1282        );
1283    }
1284}
1285
1286impl IntoElement for InteractiveText {
1287    type Element = Self;
1288
1289    fn into_element(self) -> Self::Element {
1290        self
1291    }
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296    use super::*;
1297
1298    #[test]
1299    fn test_into_element_for() {
1300        use crate::{ParentElement as _, SharedString, div};
1301        use std::borrow::Cow;
1302
1303        let _ = div().child("static str");
1304        let _ = div().child("String".to_string());
1305        let _ = div().child(Cow::Borrowed("Cow"));
1306        let _ = div().child(SharedString::from("SharedString"));
1307    }
1308
1309    #[test]
1310    fn text_macro_id() {
1311        // one call to `text!` = one id
1312        fn make_text_stable_id(happy: bool) -> Text {
1313            text!(if happy { "happy" } else { "sad" })
1314        }
1315
1316        // two calls to `text!` = two ids
1317        fn make_text_unstable_id(happy: bool) -> Text {
1318            if happy { text!("happy") } else { text!("sad") }
1319        }
1320
1321        assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
1322        assert_ne!(
1323            make_text_unstable_id(false).id,
1324            make_text_unstable_id(true).id
1325        );
1326    }
1327}