text.rs

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