text.rs

  1use crate::{
  2    ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
  3    HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
  4    Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
  5    WrappedLine, TOOLTIP_DELAY,
  6};
  7use anyhow::anyhow;
  8use parking_lot::{Mutex, MutexGuard};
  9use smallvec::SmallVec;
 10use std::{
 11    cell::{Cell, RefCell},
 12    mem,
 13    ops::Range,
 14    rc::Rc,
 15    sync::Arc,
 16};
 17use util::ResultExt;
 18
 19impl Element for &'static str {
 20    type RequestLayoutState = TextLayout;
 21    type PrepaintState = ();
 22
 23    fn id(&self) -> Option<ElementId> {
 24        None
 25    }
 26
 27    fn request_layout(
 28        &mut self,
 29        _id: Option<&GlobalElementId>,
 30        cx: &mut WindowContext,
 31    ) -> (LayoutId, Self::RequestLayoutState) {
 32        let mut state = TextLayout::default();
 33        let layout_id = state.layout(SharedString::from(*self), None, cx);
 34        (layout_id, state)
 35    }
 36
 37    fn prepaint(
 38        &mut self,
 39        _id: Option<&GlobalElementId>,
 40        bounds: Bounds<Pixels>,
 41        text_layout: &mut Self::RequestLayoutState,
 42        _cx: &mut WindowContext,
 43    ) {
 44        text_layout.prepaint(bounds, self)
 45    }
 46
 47    fn paint(
 48        &mut self,
 49        _id: Option<&GlobalElementId>,
 50        _bounds: Bounds<Pixels>,
 51        text_layout: &mut TextLayout,
 52        _: &mut (),
 53        cx: &mut WindowContext,
 54    ) {
 55        text_layout.paint(self, cx)
 56    }
 57}
 58
 59impl IntoElement for &'static str {
 60    type Element = Self;
 61
 62    fn into_element(self) -> Self::Element {
 63        self
 64    }
 65}
 66
 67impl IntoElement for String {
 68    type Element = SharedString;
 69
 70    fn into_element(self) -> Self::Element {
 71        self.into()
 72    }
 73}
 74
 75impl Element for SharedString {
 76    type RequestLayoutState = TextLayout;
 77    type PrepaintState = ();
 78
 79    fn id(&self) -> Option<ElementId> {
 80        None
 81    }
 82
 83    fn request_layout(
 84        &mut self,
 85
 86        _id: Option<&GlobalElementId>,
 87
 88        cx: &mut WindowContext,
 89    ) -> (LayoutId, Self::RequestLayoutState) {
 90        let mut state = TextLayout::default();
 91        let layout_id = state.layout(self.clone(), None, cx);
 92        (layout_id, state)
 93    }
 94
 95    fn prepaint(
 96        &mut self,
 97        _id: Option<&GlobalElementId>,
 98        bounds: Bounds<Pixels>,
 99        text_layout: &mut Self::RequestLayoutState,
100        _cx: &mut WindowContext,
101    ) {
102        text_layout.prepaint(bounds, self.as_ref())
103    }
104
105    fn paint(
106        &mut self,
107        _id: Option<&GlobalElementId>,
108        _bounds: Bounds<Pixels>,
109        text_layout: &mut Self::RequestLayoutState,
110        _: &mut Self::PrepaintState,
111        cx: &mut WindowContext,
112    ) {
113        text_layout.paint(self.as_ref(), cx)
114    }
115}
116
117impl IntoElement for SharedString {
118    type Element = Self;
119
120    fn into_element(self) -> Self::Element {
121        self
122    }
123}
124
125/// Renders text with runs of different styles.
126///
127/// Callers are responsible for setting the correct style for each run.
128/// For text with a uniform style, you can usually avoid calling this constructor
129/// and just pass text directly.
130pub struct StyledText {
131    text: SharedString,
132    runs: Option<Vec<TextRun>>,
133    layout: TextLayout,
134}
135
136impl StyledText {
137    /// Construct a new styled text element from the given string.
138    pub fn new(text: impl Into<SharedString>) -> Self {
139        StyledText {
140            text: text.into(),
141            runs: None,
142            layout: TextLayout::default(),
143        }
144    }
145
146    /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
147    pub fn layout(&self) -> &TextLayout {
148        &self.layout
149    }
150
151    /// Set the styling attributes for the given text, as well as
152    /// as any ranges of text that have had their style customized.
153    pub fn with_highlights(
154        mut self,
155        default_style: &TextStyle,
156        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
157    ) -> Self {
158        let mut runs = Vec::new();
159        let mut ix = 0;
160        for (range, highlight) in highlights {
161            if ix < range.start {
162                runs.push(default_style.clone().to_run(range.start - ix));
163            }
164            runs.push(
165                default_style
166                    .clone()
167                    .highlight(highlight)
168                    .to_run(range.len()),
169            );
170            ix = range.end;
171        }
172        if ix < self.text.len() {
173            runs.push(default_style.to_run(self.text.len() - ix));
174        }
175        self.runs = Some(runs);
176        self
177    }
178
179    /// Set the text runs for this piece of text.
180    pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
181        self.runs = Some(runs);
182        self
183    }
184}
185
186impl Element for StyledText {
187    type RequestLayoutState = ();
188    type PrepaintState = ();
189
190    fn id(&self) -> Option<ElementId> {
191        None
192    }
193
194    fn request_layout(
195        &mut self,
196
197        _id: Option<&GlobalElementId>,
198
199        cx: &mut WindowContext,
200    ) -> (LayoutId, Self::RequestLayoutState) {
201        let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
202        (layout_id, ())
203    }
204
205    fn prepaint(
206        &mut self,
207        _id: Option<&GlobalElementId>,
208        bounds: Bounds<Pixels>,
209        _: &mut Self::RequestLayoutState,
210        _cx: &mut WindowContext,
211    ) {
212        self.layout.prepaint(bounds, &self.text)
213    }
214
215    fn paint(
216        &mut self,
217        _id: Option<&GlobalElementId>,
218        _bounds: Bounds<Pixels>,
219        _: &mut Self::RequestLayoutState,
220        _: &mut Self::PrepaintState,
221        cx: &mut WindowContext,
222    ) {
223        self.layout.paint(&self.text, cx)
224    }
225}
226
227impl IntoElement for StyledText {
228    type Element = Self;
229
230    fn into_element(self) -> Self::Element {
231        self
232    }
233}
234
235/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
236#[derive(Default, Clone)]
237pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
238
239struct TextLayoutInner {
240    lines: SmallVec<[WrappedLine; 1]>,
241    line_height: Pixels,
242    wrap_width: Option<Pixels>,
243    size: Option<Size<Pixels>>,
244    bounds: Option<Bounds<Pixels>>,
245}
246
247const ELLIPSIS: &str = "";
248
249impl TextLayout {
250    fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
251        self.0.lock()
252    }
253
254    fn layout(
255        &self,
256        text: SharedString,
257        runs: Option<Vec<TextRun>>,
258        cx: &mut WindowContext,
259    ) -> LayoutId {
260        let text_style = cx.text_style();
261        let font_size = text_style.font_size.to_pixels(cx.rem_size());
262        let line_height = text_style
263            .line_height
264            .to_pixels(font_size.into(), cx.rem_size());
265
266        let runs = if let Some(runs) = runs {
267            runs
268        } else {
269            vec![text_style.to_run(text.len())]
270        };
271
272        let layout_id = cx.request_measured_layout(Default::default(), {
273            let element_state = self.clone();
274
275            move |known_dimensions, available_space, cx| {
276                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
277                    known_dimensions.width.or(match available_space.width {
278                        crate::AvailableSpace::Definite(x) => Some(x),
279                        _ => None,
280                    })
281                } else {
282                    None
283                };
284
285                let (truncate_width, ellipsis) = if let Some(truncate) = text_style.truncate {
286                    let width = known_dimensions.width.or(match available_space.width {
287                        crate::AvailableSpace::Definite(x) => Some(x),
288                        _ => None,
289                    });
290
291                    match truncate {
292                        Truncate::Truncate => (width, None),
293                        Truncate::Ellipsis => (width, Some(ELLIPSIS)),
294                    }
295                } else {
296                    (None, None)
297                };
298
299                if let Some(text_layout) = element_state.0.lock().as_ref() {
300                    if text_layout.size.is_some()
301                        && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
302                    {
303                        return text_layout.size.unwrap();
304                    }
305                }
306
307                let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
308                let text = if let Some(truncate_width) = truncate_width {
309                    line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
310                } else {
311                    text.clone()
312                };
313
314                let Some(lines) = cx
315                    .text_system()
316                    .shape_text(
317                        text, font_size, &runs, wrap_width, // Wrap if we know the width.
318                    )
319                    .log_err()
320                else {
321                    element_state.lock().replace(TextLayoutInner {
322                        lines: Default::default(),
323                        line_height,
324                        wrap_width,
325                        size: Some(Size::default()),
326                        bounds: None,
327                    });
328                    return Size::default();
329                };
330
331                let mut size: Size<Pixels> = Size::default();
332                for line in &lines {
333                    let line_size = line.size(line_height);
334                    size.height += line_size.height;
335                    size.width = size.width.max(line_size.width).ceil();
336                }
337
338                element_state.lock().replace(TextLayoutInner {
339                    lines,
340                    line_height,
341                    wrap_width,
342                    size: Some(size),
343                    bounds: None,
344                });
345
346                size
347            }
348        });
349
350        layout_id
351    }
352
353    fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
354        let mut element_state = self.lock();
355        let element_state = element_state
356            .as_mut()
357            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
358            .unwrap();
359        element_state.bounds = Some(bounds);
360    }
361
362    fn paint(&self, text: &str, cx: &mut WindowContext) {
363        let element_state = self.lock();
364        let element_state = element_state
365            .as_ref()
366            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
367            .unwrap();
368        let bounds = element_state
369            .bounds
370            .ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
371            .unwrap();
372
373        let line_height = element_state.line_height;
374        let mut line_origin = bounds.origin;
375        for line in &element_state.lines {
376            line.paint(line_origin, line_height, cx).log_err();
377            line_origin.y += line.size(line_height).height;
378        }
379    }
380
381    /// Get the byte index into the input of the pixel position.
382    pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
383        let element_state = self.lock();
384        let element_state = element_state
385            .as_ref()
386            .expect("measurement has not been performed");
387        let bounds = element_state
388            .bounds
389            .expect("prepaint has not been performed");
390
391        if position.y < bounds.top() {
392            return Err(0);
393        }
394
395        let line_height = element_state.line_height;
396        let mut line_origin = bounds.origin;
397        let mut line_start_ix = 0;
398        for line in &element_state.lines {
399            let line_bottom = line_origin.y + line.size(line_height).height;
400            if position.y > line_bottom {
401                line_origin.y = line_bottom;
402                line_start_ix += line.len() + 1;
403            } else {
404                let position_within_line = position - line_origin;
405                match line.index_for_position(position_within_line, line_height) {
406                    Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
407                    Err(index_within_line) => return Err(line_start_ix + index_within_line),
408                }
409            }
410        }
411
412        Err(line_start_ix.saturating_sub(1))
413    }
414
415    /// Get the pixel position for the given byte index.
416    pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
417        let element_state = self.lock();
418        let element_state = element_state
419            .as_ref()
420            .expect("measurement has not been performed");
421        let bounds = element_state
422            .bounds
423            .expect("prepaint has not been performed");
424        let line_height = element_state.line_height;
425
426        let mut line_origin = bounds.origin;
427        let mut line_start_ix = 0;
428
429        for line in &element_state.lines {
430            let line_end_ix = line_start_ix + line.len();
431            if index < line_start_ix {
432                break;
433            } else if index > line_end_ix {
434                line_origin.y += line.size(line_height).height;
435                line_start_ix = line_end_ix + 1;
436                continue;
437            } else {
438                let ix_within_line = index - line_start_ix;
439                return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
440            }
441        }
442
443        None
444    }
445
446    /// The bounds of this layout.
447    pub fn bounds(&self) -> Bounds<Pixels> {
448        self.0.lock().as_ref().unwrap().bounds.unwrap()
449    }
450
451    /// The line height for this layout.
452    pub fn line_height(&self) -> Pixels {
453        self.0.lock().as_ref().unwrap().line_height
454    }
455
456    /// The text for this layout.
457    pub fn text(&self) -> String {
458        self.0
459            .lock()
460            .as_ref()
461            .unwrap()
462            .lines
463            .iter()
464            .map(|s| s.text.to_string())
465            .collect::<Vec<_>>()
466            .join("\n")
467    }
468}
469
470/// A text element that can be interacted with.
471pub struct InteractiveText {
472    element_id: ElementId,
473    text: StyledText,
474    click_listener:
475        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
476    hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
477    tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
478    clickable_ranges: Vec<Range<usize>>,
479}
480
481struct InteractiveTextClickEvent {
482    mouse_down_index: usize,
483    mouse_up_index: usize,
484}
485
486#[doc(hidden)]
487#[derive(Default)]
488pub struct InteractiveTextState {
489    mouse_down_index: Rc<Cell<Option<usize>>>,
490    hovered_index: Rc<Cell<Option<usize>>>,
491    active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
492}
493
494/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
495impl InteractiveText {
496    /// Creates a new InteractiveText from the given text.
497    pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
498        Self {
499            element_id: id.into(),
500            text,
501            click_listener: None,
502            hover_listener: None,
503            tooltip_builder: None,
504            clickable_ranges: Vec::new(),
505        }
506    }
507
508    /// on_click is called when the user clicks on one of the given ranges, passing the index of
509    /// the clicked range.
510    pub fn on_click(
511        mut self,
512        ranges: Vec<Range<usize>>,
513        listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
514    ) -> Self {
515        self.click_listener = Some(Box::new(move |ranges, event, cx| {
516            for (range_ix, range) in ranges.iter().enumerate() {
517                if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
518                {
519                    listener(range_ix, cx);
520                }
521            }
522        }));
523        self.clickable_ranges = ranges;
524        self
525    }
526
527    /// on_hover is called when the mouse moves over a character within the text, passing the
528    /// index of the hovered character, or None if the mouse leaves the text.
529    pub fn on_hover(
530        mut self,
531        listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
532    ) -> Self {
533        self.hover_listener = Some(Box::new(listener));
534        self
535    }
536
537    /// tooltip lets you specify a tooltip for a given character index in the string.
538    pub fn tooltip(
539        mut self,
540        builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
541    ) -> Self {
542        self.tooltip_builder = Some(Rc::new(builder));
543        self
544    }
545}
546
547impl Element for InteractiveText {
548    type RequestLayoutState = ();
549    type PrepaintState = Hitbox;
550
551    fn id(&self) -> Option<ElementId> {
552        Some(self.element_id.clone())
553    }
554
555    fn request_layout(
556        &mut self,
557        _id: Option<&GlobalElementId>,
558        cx: &mut WindowContext,
559    ) -> (LayoutId, Self::RequestLayoutState) {
560        self.text.request_layout(None, cx)
561    }
562
563    fn prepaint(
564        &mut self,
565        global_id: Option<&GlobalElementId>,
566        bounds: Bounds<Pixels>,
567        state: &mut Self::RequestLayoutState,
568        cx: &mut WindowContext,
569    ) -> Hitbox {
570        cx.with_optional_element_state::<InteractiveTextState, _>(
571            global_id,
572            |interactive_state, cx| {
573                let interactive_state = interactive_state
574                    .map(|interactive_state| interactive_state.unwrap_or_default());
575
576                if let Some(interactive_state) = interactive_state.as_ref() {
577                    if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
578                    {
579                        if let Some(tooltip) = active_tooltip.tooltip.clone() {
580                            cx.set_tooltip(tooltip);
581                        }
582                    }
583                }
584
585                self.text.prepaint(None, bounds, state, cx);
586                let hitbox = cx.insert_hitbox(bounds, false);
587                (hitbox, interactive_state)
588            },
589        )
590    }
591
592    fn paint(
593        &mut self,
594        global_id: Option<&GlobalElementId>,
595        bounds: Bounds<Pixels>,
596        _: &mut Self::RequestLayoutState,
597        hitbox: &mut Hitbox,
598        cx: &mut WindowContext,
599    ) {
600        let text_layout = self.text.layout().clone();
601        cx.with_element_state::<InteractiveTextState, _>(
602            global_id.unwrap(),
603            |interactive_state, cx| {
604                let mut interactive_state = interactive_state.unwrap_or_default();
605                if let Some(click_listener) = self.click_listener.take() {
606                    let mouse_position = cx.mouse_position();
607                    if let Ok(ix) = text_layout.index_for_position(mouse_position) {
608                        if self
609                            .clickable_ranges
610                            .iter()
611                            .any(|range| range.contains(&ix))
612                        {
613                            cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
614                        }
615                    }
616
617                    let text_layout = text_layout.clone();
618                    let mouse_down = interactive_state.mouse_down_index.clone();
619                    if let Some(mouse_down_index) = mouse_down.get() {
620                        let hitbox = hitbox.clone();
621                        let clickable_ranges = mem::take(&mut self.clickable_ranges);
622                        cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
623                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
624                                if let Ok(mouse_up_index) =
625                                    text_layout.index_for_position(event.position)
626                                {
627                                    click_listener(
628                                        &clickable_ranges,
629                                        InteractiveTextClickEvent {
630                                            mouse_down_index,
631                                            mouse_up_index,
632                                        },
633                                        cx,
634                                    )
635                                }
636
637                                mouse_down.take();
638                                cx.refresh();
639                            }
640                        });
641                    } else {
642                        let hitbox = hitbox.clone();
643                        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
644                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
645                                if let Ok(mouse_down_index) =
646                                    text_layout.index_for_position(event.position)
647                                {
648                                    mouse_down.set(Some(mouse_down_index));
649                                    cx.refresh();
650                                }
651                            }
652                        });
653                    }
654                }
655
656                cx.on_mouse_event({
657                    let mut hover_listener = self.hover_listener.take();
658                    let hitbox = hitbox.clone();
659                    let text_layout = text_layout.clone();
660                    let hovered_index = interactive_state.hovered_index.clone();
661                    move |event: &MouseMoveEvent, phase, cx| {
662                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
663                            let current = hovered_index.get();
664                            let updated = text_layout.index_for_position(event.position).ok();
665                            if current != updated {
666                                hovered_index.set(updated);
667                                if let Some(hover_listener) = hover_listener.as_ref() {
668                                    hover_listener(updated, event.clone(), cx);
669                                }
670                                cx.refresh();
671                            }
672                        }
673                    }
674                });
675
676                if let Some(tooltip_builder) = self.tooltip_builder.clone() {
677                    let hitbox = hitbox.clone();
678                    let active_tooltip = interactive_state.active_tooltip.clone();
679                    let pending_mouse_down = interactive_state.mouse_down_index.clone();
680                    let text_layout = text_layout.clone();
681
682                    cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
683                        let position = text_layout.index_for_position(event.position).ok();
684                        let is_hovered = position.is_some()
685                            && hitbox.is_hovered(cx)
686                            && pending_mouse_down.get().is_none();
687                        if !is_hovered {
688                            active_tooltip.take();
689                            return;
690                        }
691                        let position = position.unwrap();
692
693                        if phase != DispatchPhase::Bubble {
694                            return;
695                        }
696
697                        if active_tooltip.borrow().is_none() {
698                            let task = cx.spawn({
699                                let active_tooltip = active_tooltip.clone();
700                                let tooltip_builder = tooltip_builder.clone();
701
702                                move |mut cx| async move {
703                                    cx.background_executor().timer(TOOLTIP_DELAY).await;
704                                    cx.update(|cx| {
705                                        let new_tooltip =
706                                            tooltip_builder(position, cx).map(|tooltip| {
707                                                ActiveTooltip {
708                                                    tooltip: Some(AnyTooltip {
709                                                        view: tooltip,
710                                                        mouse_position: cx.mouse_position(),
711                                                    }),
712                                                    _task: None,
713                                                }
714                                            });
715                                        *active_tooltip.borrow_mut() = new_tooltip;
716                                        cx.refresh();
717                                    })
718                                    .ok();
719                                }
720                            });
721                            *active_tooltip.borrow_mut() = Some(ActiveTooltip {
722                                tooltip: None,
723                                _task: Some(task),
724                            });
725                        }
726                    });
727
728                    let active_tooltip = interactive_state.active_tooltip.clone();
729                    cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
730                        active_tooltip.take();
731                    });
732                }
733
734                self.text.paint(None, bounds, &mut (), &mut (), cx);
735
736                ((), interactive_state)
737            },
738        );
739    }
740}
741
742impl IntoElement for InteractiveText {
743    type Element = Self;
744
745    fn into_element(self) -> Self::Element {
746        self
747    }
748}