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