Skip to main content

gpui/elements/
text.rs

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