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