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 accesskit::{Node, Role};
  9use anyhow::Context as _;
 10use gpui_util::ResultExt;
 11use itertools::Itertools;
 12use smallvec::SmallVec;
 13use std::{
 14    borrow::Cow,
 15    cell::{Cell, RefCell},
 16    mem,
 17    ops::Range,
 18    rc::Rc,
 19    sync::Arc,
 20};
 21
 22impl Element for &'static str {
 23    type RequestLayoutState = TextLayout;
 24    type PrepaintState = ();
 25
 26    fn id(&self) -> Option<ElementId> {
 27        None
 28    }
 29
 30    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 31        None
 32    }
 33
 34    fn request_layout(
 35        &mut self,
 36        _id: Option<&GlobalElementId>,
 37        _inspector_id: Option<&InspectorElementId>,
 38        window: &mut Window,
 39        cx: &mut App,
 40    ) -> (LayoutId, Self::RequestLayoutState) {
 41        let mut state = TextLayout::default();
 42        let layout_id = state.layout(SharedString::from(*self), None, window, cx);
 43        (layout_id, state)
 44    }
 45
 46    fn prepaint(
 47        &mut self,
 48        _id: Option<&GlobalElementId>,
 49        _inspector_id: Option<&InspectorElementId>,
 50        bounds: Bounds<Pixels>,
 51        text_layout: &mut Self::RequestLayoutState,
 52        _window: &mut Window,
 53        _cx: &mut App,
 54    ) {
 55        text_layout.prepaint(bounds, self)
 56    }
 57
 58    fn paint(
 59        &mut self,
 60        _id: Option<&GlobalElementId>,
 61        _inspector_id: Option<&InspectorElementId>,
 62        _bounds: Bounds<Pixels>,
 63        text_layout: &mut TextLayout,
 64        _: &mut (),
 65        window: &mut Window,
 66        cx: &mut App,
 67    ) {
 68        text_layout.paint(self, window, cx)
 69    }
 70}
 71
 72impl IntoElement for &'static str {
 73    type Element = Self;
 74
 75    fn into_element(self) -> Self::Element {
 76        self
 77    }
 78}
 79
 80impl IntoElement for String {
 81    type Element = SharedString;
 82
 83    fn into_element(self) -> Self::Element {
 84        self.into()
 85    }
 86}
 87
 88impl IntoElement for Cow<'static, str> {
 89    type Element = SharedString;
 90
 91    fn into_element(self) -> Self::Element {
 92        self.into()
 93    }
 94}
 95
 96impl Element for SharedString {
 97    type RequestLayoutState = TextLayout;
 98    type PrepaintState = ();
 99
100    fn id(&self) -> Option<ElementId> {
101        None
102    }
103
104    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
105        None
106    }
107
108    fn request_layout(
109        &mut self,
110        _id: Option<&GlobalElementId>,
111        _inspector_id: Option<&InspectorElementId>,
112        window: &mut Window,
113        cx: &mut App,
114    ) -> (LayoutId, Self::RequestLayoutState) {
115        let mut state = TextLayout::default();
116        let layout_id = state.layout(self.clone(), None, window, cx);
117        (layout_id, state)
118    }
119
120    fn prepaint(
121        &mut self,
122        _id: Option<&GlobalElementId>,
123        _inspector_id: Option<&InspectorElementId>,
124        bounds: Bounds<Pixels>,
125        text_layout: &mut Self::RequestLayoutState,
126        _window: &mut Window,
127        _cx: &mut App,
128    ) {
129        text_layout.prepaint(bounds, self.as_ref())
130    }
131
132    fn paint(
133        &mut self,
134        _id: Option<&GlobalElementId>,
135        _inspector_id: Option<&InspectorElementId>,
136        _bounds: Bounds<Pixels>,
137        text_layout: &mut Self::RequestLayoutState,
138        _: &mut Self::PrepaintState,
139        window: &mut Window,
140        cx: &mut App,
141    ) {
142        text_layout.paint(self.as_ref(), window, cx)
143    }
144}
145
146impl IntoElement for SharedString {
147    type Element = Self;
148
149    fn into_element(self) -> Self::Element {
150        self
151    }
152}
153
154/// Renders text with runs of different styles.
155///
156/// Callers are responsible for setting the correct style for each run.
157/// For text with a uniform style, you can usually avoid calling this constructor
158/// and just pass text directly.
159pub struct StyledText {
160    text: SharedString,
161    runs: Option<Vec<TextRun>>,
162    delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
163    layout: TextLayout,
164}
165
166impl StyledText {
167    /// Construct a new styled text element from the given string.
168    pub fn new(text: impl Into<SharedString>) -> Self {
169        StyledText {
170            text: text.into(),
171            runs: None,
172            delayed_highlights: None,
173            layout: TextLayout::default(),
174        }
175    }
176
177    /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
178    pub fn layout(&self) -> &TextLayout {
179        &self.layout
180    }
181
182    /// Set the styling attributes for the given text, as well as
183    /// as any ranges of text that have had their style customized.
184    pub fn with_default_highlights(
185        mut self,
186        default_style: &TextStyle,
187        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
188    ) -> Self {
189        debug_assert!(
190            self.delayed_highlights.is_none(),
191            "Can't use `with_default_highlights` and `with_highlights`"
192        );
193        let runs = Self::compute_runs(&self.text, default_style, highlights);
194        self.with_runs(runs)
195    }
196
197    /// Set the styling attributes for the given text, as well as
198    /// as any ranges of text that have had their style customized.
199    pub fn with_highlights(
200        mut self,
201        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
202    ) -> Self {
203        debug_assert!(
204            self.runs.is_none(),
205            "Can't use `with_highlights` and `with_default_highlights`"
206        );
207        self.delayed_highlights = Some(
208            highlights
209                .into_iter()
210                .inspect(|(run, _)| {
211                    debug_assert!(self.text.is_char_boundary(run.start));
212                    debug_assert!(self.text.is_char_boundary(run.end));
213                })
214                .collect::<Vec<_>>(),
215        );
216        self
217    }
218
219    fn compute_runs(
220        text: &str,
221        default_style: &TextStyle,
222        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
223    ) -> Vec<TextRun> {
224        let mut runs = Vec::new();
225        let mut ix = 0;
226        for (range, highlight) in highlights {
227            if ix < range.start {
228                debug_assert!(text.is_char_boundary(range.start));
229                runs.push(default_style.clone().to_run(range.start - ix));
230            }
231            debug_assert!(text.is_char_boundary(range.end));
232            runs.push(
233                default_style
234                    .clone()
235                    .highlight(highlight)
236                    .to_run(range.len()),
237            );
238            ix = range.end;
239        }
240        if ix < text.len() {
241            runs.push(default_style.to_run(text.len() - ix));
242        }
243        runs
244    }
245
246    /// Set the text runs for this piece of text.
247    pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
248        let mut text = &**self.text;
249        for run in &runs {
250            text = text.get(run.len..).unwrap_or_else(|| {
251                #[cfg(debug_assertions)]
252                panic!("invalid text run. Text: '{text}', run: {run:?}");
253                #[cfg(not(debug_assertions))]
254                panic!("invalid text run");
255            });
256        }
257        assert!(text.is_empty(), "invalid text run");
258        self.runs = Some(runs);
259        self
260    }
261}
262
263impl Element for StyledText {
264    type RequestLayoutState = ();
265    type PrepaintState = ();
266
267    fn id(&self) -> Option<ElementId> {
268        None
269    }
270
271    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
272        None
273    }
274
275    fn request_layout(
276        &mut self,
277        _id: Option<&GlobalElementId>,
278        _inspector_id: Option<&InspectorElementId>,
279        window: &mut Window,
280        cx: &mut App,
281    ) -> (LayoutId, Self::RequestLayoutState) {
282        let runs = self.runs.take().or_else(|| {
283            self.delayed_highlights.take().map(|delayed_highlights| {
284                Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
285            })
286        });
287
288        let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
289        (layout_id, ())
290    }
291
292    fn prepaint(
293        &mut self,
294        _id: Option<&GlobalElementId>,
295        _inspector_id: Option<&InspectorElementId>,
296        bounds: Bounds<Pixels>,
297        _: &mut Self::RequestLayoutState,
298        _window: &mut Window,
299        _cx: &mut App,
300    ) {
301        self.layout.prepaint(bounds, &self.text)
302    }
303
304    fn paint(
305        &mut self,
306        _id: Option<&GlobalElementId>,
307        _inspector_id: Option<&InspectorElementId>,
308        _bounds: Bounds<Pixels>,
309        _: &mut Self::RequestLayoutState,
310        _: &mut Self::PrepaintState,
311        window: &mut Window,
312        cx: &mut App,
313    ) {
314        self.layout.paint(&self.text, window, cx)
315    }
316}
317
318impl IntoElement for StyledText {
319    type Element = Self;
320
321    fn into_element(self) -> Self::Element {
322        self
323    }
324}
325
326/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
327#[derive(Default, Clone)]
328pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
329
330struct TextLayoutInner {
331    len: usize,
332    lines: SmallVec<[WrappedLine; 1]>,
333    line_height: Pixels,
334    wrap_width: Option<Pixels>,
335    size: Option<Size<Pixels>>,
336    bounds: Option<Bounds<Pixels>>,
337}
338
339impl TextLayout {
340    fn layout(
341        &self,
342        text: SharedString,
343        runs: Option<Vec<TextRun>>,
344        window: &mut Window,
345        _: &mut App,
346    ) -> LayoutId {
347        let text_style = window.text_style();
348        let font_size = text_style.font_size.to_pixels(window.rem_size());
349        let line_height = text_style
350            .line_height
351            .to_pixels(font_size.into(), window.rem_size());
352
353        let runs = if let Some(runs) = runs {
354            runs
355        } else {
356            vec![text_style.to_run(text.len())]
357        };
358        window.request_measured_layout(Default::default(), {
359            let element_state = self.clone();
360
361            move |known_dimensions, available_space, window, cx| {
362                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
363                    known_dimensions.width.or(match available_space.width {
364                        crate::AvailableSpace::Definite(x) => Some(x),
365                        _ => None,
366                    })
367                } else {
368                    None
369                };
370
371                let (truncate_width, truncation_affix, truncate_from) =
372                    if let Some(text_overflow) = text_style.text_overflow.clone() {
373                        let width = known_dimensions.width.or(match available_space.width {
374                            crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
375                                Some(max_lines) => Some(x * max_lines),
376                                None => Some(x),
377                            },
378                            _ => None,
379                        });
380
381                        match text_overflow {
382                            TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
383                            TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
384                        }
385                    } else {
386                        (None, "".into(), TruncateFrom::End)
387                    };
388
389                // Only use cached layout if:
390                // 1. We have a cached size
391                // 2. wrap_width matches (or both are None)
392                // 3. truncate_width is None (if truncate_width is Some, we need to re-layout
393                //    because the previous layout may have been computed without truncation)
394                if let Some(text_layout) = element_state.0.borrow().as_ref()
395                    && let Some(size) = text_layout.size
396                    && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
397                    && truncate_width.is_none()
398                {
399                    return size;
400                }
401
402                let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
403                let (text, runs) = if let Some(truncate_width) = truncate_width {
404                    line_wrapper.truncate_line(
405                        text.clone(),
406                        truncate_width,
407                        &truncation_affix,
408                        &runs,
409                        truncate_from,
410                    )
411                } else {
412                    (text.clone(), Cow::Borrowed(&*runs))
413                };
414                let len = text.len();
415
416                let Some(lines) = window
417                    .text_system()
418                    .shape_text(
419                        text,
420                        font_size,
421                        &runs,
422                        wrap_width,            // Wrap if we know the width.
423                        text_style.line_clamp, // Limit the number of lines if line_clamp is set.
424                    )
425                    .log_err()
426                else {
427                    element_state.0.borrow_mut().replace(TextLayoutInner {
428                        lines: Default::default(),
429                        len: 0,
430                        line_height,
431                        wrap_width,
432                        size: Some(Size::default()),
433                        bounds: None,
434                    });
435                    return Size::default();
436                };
437
438                let mut size: Size<Pixels> = Size::default();
439                for line in &lines {
440                    let line_size = line.size(line_height);
441                    size.height += line_size.height;
442                    size.width = size.width.max(line_size.width).ceil();
443                }
444
445                element_state.0.borrow_mut().replace(TextLayoutInner {
446                    lines,
447                    len,
448                    line_height,
449                    wrap_width,
450                    size: Some(size),
451                    bounds: None,
452                });
453
454                size
455            }
456        })
457    }
458
459    fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
460        let mut element_state = self.0.borrow_mut();
461        let element_state = element_state
462            .as_mut()
463            .with_context(|| format!("measurement has not been performed on {text}"))
464            .unwrap();
465        element_state.bounds = Some(bounds);
466    }
467
468    fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
469        let element_state = self.0.borrow();
470        let element_state = element_state
471            .as_ref()
472            .with_context(|| format!("measurement has not been performed on {text}"))
473            .unwrap();
474        let bounds = element_state
475            .bounds
476            .with_context(|| format!("prepaint has not been performed on {text}"))
477            .unwrap();
478
479        let line_height = element_state.line_height;
480        let mut line_origin = bounds.origin;
481        let text_style = window.text_style();
482        for line in &element_state.lines {
483            line.paint_background(
484                line_origin,
485                line_height,
486                text_style.text_align,
487                Some(bounds),
488                window,
489                cx,
490            )
491            .log_err();
492            line.paint(
493                line_origin,
494                line_height,
495                text_style.text_align,
496                Some(bounds),
497                window,
498                cx,
499            )
500            .log_err();
501            line_origin.y += line.size(line_height).height;
502        }
503    }
504
505    /// Get the byte index into the input of the pixel position.
506    pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
507        let element_state = self.0.borrow();
508        let element_state = element_state
509            .as_ref()
510            .expect("measurement has not been performed");
511        let bounds = element_state
512            .bounds
513            .expect("prepaint has not been performed");
514
515        if position.y < bounds.top() {
516            return Err(0);
517        }
518
519        let line_height = element_state.line_height;
520        let mut line_origin = bounds.origin;
521        let mut line_start_ix = 0;
522        for line in &element_state.lines {
523            let line_bottom = line_origin.y + line.size(line_height).height;
524            if position.y > line_bottom {
525                line_origin.y = line_bottom;
526                line_start_ix += line.len() + 1;
527            } else {
528                let position_within_line = position - line_origin;
529                match line.index_for_position(position_within_line, line_height) {
530                    Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
531                    Err(index_within_line) => return Err(line_start_ix + index_within_line),
532                }
533            }
534        }
535
536        Err(line_start_ix.saturating_sub(1))
537    }
538
539    /// Get the pixel position for the given byte index.
540    pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
541        let element_state = self.0.borrow();
542        let element_state = element_state
543            .as_ref()
544            .expect("measurement has not been performed");
545        let bounds = element_state
546            .bounds
547            .expect("prepaint has not been performed");
548        let line_height = element_state.line_height;
549
550        let mut line_origin = bounds.origin;
551        let mut line_start_ix = 0;
552
553        for line in &element_state.lines {
554            let line_end_ix = line_start_ix + line.len();
555            if index < line_start_ix {
556                break;
557            } else if index > line_end_ix {
558                line_origin.y += line.size(line_height).height;
559                line_start_ix = line_end_ix + 1;
560                continue;
561            } else {
562                let ix_within_line = index - line_start_ix;
563                return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
564            }
565        }
566
567        None
568    }
569
570    /// Retrieve the layout for the line containing the given byte index.
571    pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
572        let element_state = self.0.borrow();
573        let element_state = element_state
574            .as_ref()
575            .expect("measurement has not been performed");
576        let bounds = element_state
577            .bounds
578            .expect("prepaint has not been performed");
579        let line_height = element_state.line_height;
580
581        let mut line_origin = bounds.origin;
582        let mut line_start_ix = 0;
583
584        for line in &element_state.lines {
585            let line_end_ix = line_start_ix + line.len();
586            if index < line_start_ix {
587                break;
588            } else if index > line_end_ix {
589                line_origin.y += line.size(line_height).height;
590                line_start_ix = line_end_ix + 1;
591                continue;
592            } else {
593                return Some(line.layout.clone());
594            }
595        }
596
597        None
598    }
599
600    /// The bounds of this layout.
601    pub fn bounds(&self) -> Bounds<Pixels> {
602        self.0.borrow().as_ref().unwrap().bounds.unwrap()
603    }
604
605    /// The line height for this layout.
606    pub fn line_height(&self) -> Pixels {
607        self.0.borrow().as_ref().unwrap().line_height
608    }
609
610    /// The UTF-8 length of the underlying text.
611    pub fn len(&self) -> usize {
612        self.0.borrow().as_ref().unwrap().len
613    }
614
615    /// The text for this layout.
616    pub fn text(&self) -> String {
617        self.0
618            .borrow()
619            .as_ref()
620            .unwrap()
621            .lines
622            .iter()
623            .map(|s| &s.text)
624            .join("\n")
625    }
626
627    /// The text for this layout (with soft-wraps as newlines)
628    pub fn wrapped_text(&self) -> String {
629        let mut accumulator = String::new();
630
631        for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
632            let mut seen = 0;
633            for boundary in wrapped.layout.wrap_boundaries.iter() {
634                let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
635                    [boundary.glyph_ix]
636                    .index;
637
638                accumulator.push_str(&wrapped.text[seen..index]);
639                accumulator.push('\n');
640                seen = index;
641            }
642            accumulator.push_str(&wrapped.text[seen..]);
643            accumulator.push('\n');
644        }
645        // Remove trailing newline
646        accumulator.pop();
647        accumulator
648    }
649}
650
651/// A text element that can be interacted with.
652pub struct InteractiveText {
653    element_id: ElementId,
654    text: StyledText,
655    click_listener:
656        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
657    hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
658    tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
659    tooltip_id: Option<TooltipId>,
660    clickable_ranges: Vec<Range<usize>>,
661}
662
663struct InteractiveTextClickEvent {
664    mouse_down_index: usize,
665    mouse_up_index: usize,
666}
667
668#[doc(hidden)]
669#[derive(Default)]
670pub struct InteractiveTextState {
671    mouse_down_index: Rc<Cell<Option<usize>>>,
672    hovered_index: Rc<Cell<Option<usize>>>,
673    active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
674}
675
676/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
677impl InteractiveText {
678    /// Creates a new InteractiveText from the given text.
679    pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
680        Self {
681            element_id: id.into(),
682            text,
683            click_listener: None,
684            hover_listener: None,
685            tooltip_builder: None,
686            tooltip_id: None,
687            clickable_ranges: Vec::new(),
688        }
689    }
690
691    /// on_click is called when the user clicks on one of the given ranges, passing the index of
692    /// the clicked range.
693    pub fn on_click(
694        mut self,
695        ranges: Vec<Range<usize>>,
696        listener: impl Fn(usize, &mut Window, &mut App) + 'static,
697    ) -> Self {
698        self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
699            for (range_ix, range) in ranges.iter().enumerate() {
700                if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
701                {
702                    listener(range_ix, window, cx);
703                }
704            }
705        }));
706        self.clickable_ranges = ranges;
707        self
708    }
709
710    /// on_hover is called when the mouse moves over a character within the text, passing the
711    /// index of the hovered character, or None if the mouse leaves the text.
712    pub fn on_hover(
713        mut self,
714        listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
715    ) -> Self {
716        self.hover_listener = Some(Box::new(listener));
717        self
718    }
719
720    /// tooltip lets you specify a tooltip for a given character index in the string.
721    pub fn tooltip(
722        mut self,
723        builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
724    ) -> Self {
725        self.tooltip_builder = Some(Rc::new(builder));
726        self
727    }
728}
729
730impl Element for InteractiveText {
731    type RequestLayoutState = ();
732    type PrepaintState = Hitbox;
733
734    fn id(&self) -> Option<ElementId> {
735        Some(self.element_id.clone())
736    }
737
738    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
739        None
740    }
741
742    fn a11y_role(&self) -> Option<Role> {
743        Some(Role::Label)
744    }
745
746    fn write_a11y_info(&self, node: &mut Node) {
747        node.set_label(self.text.text.as_ref());
748    }
749
750    fn request_layout(
751        &mut self,
752        _id: Option<&GlobalElementId>,
753        inspector_id: Option<&InspectorElementId>,
754        window: &mut Window,
755        cx: &mut App,
756    ) -> (LayoutId, Self::RequestLayoutState) {
757        self.text.request_layout(None, inspector_id, window, cx)
758    }
759
760    fn prepaint(
761        &mut self,
762        global_id: Option<&GlobalElementId>,
763        inspector_id: Option<&InspectorElementId>,
764        bounds: Bounds<Pixels>,
765        state: &mut Self::RequestLayoutState,
766        window: &mut Window,
767        cx: &mut App,
768    ) -> Hitbox {
769        window.with_optional_element_state::<InteractiveTextState, _>(
770            global_id,
771            |interactive_state, window| {
772                let mut interactive_state = interactive_state
773                    .map(|interactive_state| interactive_state.unwrap_or_default());
774
775                if let Some(interactive_state) = interactive_state.as_mut() {
776                    if self.tooltip_builder.is_some() {
777                        self.tooltip_id =
778                            set_tooltip_on_window(&interactive_state.active_tooltip, window);
779                    } else {
780                        // If there is no longer a tooltip builder, remove the active tooltip.
781                        interactive_state.active_tooltip.take();
782                    }
783                }
784
785                self.text
786                    .prepaint(None, inspector_id, bounds, state, window, cx);
787                let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
788                (hitbox, interactive_state)
789            },
790        )
791    }
792
793    fn paint(
794        &mut self,
795        global_id: Option<&GlobalElementId>,
796        inspector_id: Option<&InspectorElementId>,
797        bounds: Bounds<Pixels>,
798        _: &mut Self::RequestLayoutState,
799        hitbox: &mut Hitbox,
800        window: &mut Window,
801        cx: &mut App,
802    ) {
803        let current_view = window.current_view();
804        let text_layout = self.text.layout().clone();
805        window.with_element_state::<InteractiveTextState, _>(
806            global_id.unwrap(),
807            |interactive_state, window| {
808                let mut interactive_state = interactive_state.unwrap_or_default();
809                if let Some(click_listener) = self.click_listener.take() {
810                    let mouse_position = window.mouse_position();
811                    if let Ok(ix) = text_layout.index_for_position(mouse_position)
812                        && self
813                            .clickable_ranges
814                            .iter()
815                            .any(|range| range.contains(&ix))
816                    {
817                        window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
818                    }
819
820                    let text_layout = text_layout.clone();
821                    let mouse_down = interactive_state.mouse_down_index.clone();
822                    if let Some(mouse_down_index) = mouse_down.get() {
823                        let hitbox = hitbox.clone();
824                        let clickable_ranges = mem::take(&mut self.clickable_ranges);
825                        window.on_mouse_event(
826                            move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
827                                if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
828                                    if let Ok(mouse_up_index) =
829                                        text_layout.index_for_position(event.position)
830                                    {
831                                        click_listener(
832                                            &clickable_ranges,
833                                            InteractiveTextClickEvent {
834                                                mouse_down_index,
835                                                mouse_up_index,
836                                            },
837                                            window,
838                                            cx,
839                                        )
840                                    }
841
842                                    mouse_down.take();
843                                    window.refresh();
844                                }
845                            },
846                        );
847                    } else {
848                        let hitbox = hitbox.clone();
849                        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
850                            if phase == DispatchPhase::Bubble
851                                && hitbox.is_hovered(window)
852                                && let Ok(mouse_down_index) =
853                                    text_layout.index_for_position(event.position)
854                            {
855                                mouse_down.set(Some(mouse_down_index));
856                                window.refresh();
857                            }
858                        });
859                    }
860                }
861
862                window.on_mouse_event({
863                    let mut hover_listener = self.hover_listener.take();
864                    let hitbox = hitbox.clone();
865                    let text_layout = text_layout.clone();
866                    let hovered_index = interactive_state.hovered_index.clone();
867                    move |event: &MouseMoveEvent, phase, window, cx| {
868                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
869                            let current = hovered_index.get();
870                            let updated = text_layout.index_for_position(event.position).ok();
871                            if current != updated {
872                                hovered_index.set(updated);
873                                if let Some(hover_listener) = hover_listener.as_ref() {
874                                    hover_listener(updated, event.clone(), window, cx);
875                                }
876                                cx.notify(current_view);
877                            }
878                        }
879                    }
880                });
881
882                if let Some(tooltip_builder) = self.tooltip_builder.clone() {
883                    let active_tooltip = interactive_state.active_tooltip.clone();
884                    let build_tooltip = Rc::new({
885                        let tooltip_is_hoverable = false;
886                        let text_layout = text_layout.clone();
887                        move |window: &mut Window, cx: &mut App| {
888                            text_layout
889                                .index_for_position(window.mouse_position())
890                                .ok()
891                                .and_then(|position| tooltip_builder(position, window, cx))
892                                .map(|view| (view, tooltip_is_hoverable))
893                        }
894                    });
895
896                    // Use bounds instead of testing hitbox since this is called during prepaint.
897                    let check_is_hovered_during_prepaint = Rc::new({
898                        let source_bounds = hitbox.bounds;
899                        let text_layout = text_layout.clone();
900                        let pending_mouse_down = interactive_state.mouse_down_index.clone();
901                        move |window: &Window| {
902                            text_layout
903                                .index_for_position(window.mouse_position())
904                                .is_ok()
905                                && source_bounds.contains(&window.mouse_position())
906                                && pending_mouse_down.get().is_none()
907                        }
908                    });
909
910                    let check_is_hovered = Rc::new({
911                        let hitbox = hitbox.clone();
912                        let text_layout = text_layout.clone();
913                        let pending_mouse_down = interactive_state.mouse_down_index.clone();
914                        move |window: &Window| {
915                            text_layout
916                                .index_for_position(window.mouse_position())
917                                .is_ok()
918                                && hitbox.is_hovered(window)
919                                && pending_mouse_down.get().is_none()
920                        }
921                    });
922
923                    register_tooltip_mouse_handlers(
924                        &active_tooltip,
925                        self.tooltip_id,
926                        build_tooltip,
927                        check_is_hovered,
928                        check_is_hovered_during_prepaint,
929                        window,
930                    );
931                }
932
933                self.text
934                    .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
935
936                ((), interactive_state)
937            },
938        );
939    }
940}
941
942impl IntoElement for InteractiveText {
943    type Element = Self;
944
945    fn into_element(self) -> Self::Element {
946        self
947    }
948}
949
950#[cfg(test)]
951mod tests {
952    #[test]
953    fn test_into_element_for() {
954        use crate::{ParentElement as _, SharedString, div};
955        use std::borrow::Cow;
956
957        let _ = div().child("static str");
958        let _ = div().child("String".to_string());
959        let _ = div().child(Cow::Borrowed("Cow"));
960        let _ = div().child(SharedString::from("SharedString"));
961    }
962}