text.rs

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