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, WhiteSpace, WindowContext, WrappedLine,
  5    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
247impl TextLayout {
248    fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
249        self.0.lock()
250    }
251
252    fn layout(
253        &mut self,
254        text: SharedString,
255        runs: Option<Vec<TextRun>>,
256        cx: &mut WindowContext,
257    ) -> LayoutId {
258        let text_style = cx.text_style();
259        let font_size = text_style.font_size.to_pixels(cx.rem_size());
260        let line_height = text_style
261            .line_height
262            .to_pixels(font_size.into(), cx.rem_size());
263
264        let runs = if let Some(runs) = runs {
265            runs
266        } else {
267            vec![text_style.to_run(text.len())]
268        };
269
270        let layout_id = cx.request_measured_layout(Default::default(), {
271            let element_state = self.clone();
272
273            move |known_dimensions, available_space, cx| {
274                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
275                    known_dimensions.width.or(match available_space.width {
276                        crate::AvailableSpace::Definite(x) => Some(x),
277                        _ => None,
278                    })
279                } else {
280                    None
281                };
282
283                if let Some(text_layout) = element_state.0.lock().as_ref() {
284                    if text_layout.size.is_some()
285                        && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
286                    {
287                        return text_layout.size.unwrap();
288                    }
289                }
290
291                let Some(lines) = cx
292                    .text_system()
293                    .shape_text(
294                        text.clone(),
295                        font_size,
296                        &runs,
297                        wrap_width, // Wrap if we know the width.
298                    )
299                    .log_err()
300                else {
301                    element_state.lock().replace(TextLayoutInner {
302                        lines: Default::default(),
303                        line_height,
304                        wrap_width,
305                        size: Some(Size::default()),
306                        bounds: None,
307                    });
308                    return Size::default();
309                };
310
311                let mut size: Size<Pixels> = Size::default();
312                for line in &lines {
313                    let line_size = line.size(line_height);
314                    size.height += line_size.height;
315                    size.width = size.width.max(line_size.width).ceil();
316                }
317
318                element_state.lock().replace(TextLayoutInner {
319                    lines,
320                    line_height,
321                    wrap_width,
322                    size: Some(size),
323                    bounds: None,
324                });
325
326                size
327            }
328        });
329
330        layout_id
331    }
332
333    fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
334        let mut element_state = self.lock();
335        let element_state = element_state
336            .as_mut()
337            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
338            .unwrap();
339        element_state.bounds = Some(bounds);
340    }
341
342    fn paint(&mut self, text: &str, cx: &mut WindowContext) {
343        let element_state = self.lock();
344        let element_state = element_state
345            .as_ref()
346            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
347            .unwrap();
348        let bounds = element_state
349            .bounds
350            .ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
351            .unwrap();
352
353        let line_height = element_state.line_height;
354        let mut line_origin = bounds.origin;
355        for line in &element_state.lines {
356            line.paint(line_origin, line_height, cx).log_err();
357            line_origin.y += line.size(line_height).height;
358        }
359    }
360
361    /// Get the byte index into the input of the pixel position.
362    pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
363        let element_state = self.lock();
364        let element_state = element_state
365            .as_ref()
366            .expect("measurement has not been performed");
367        let bounds = element_state
368            .bounds
369            .expect("prepaint has not been performed");
370
371        if position.y < bounds.top() {
372            return Err(0);
373        }
374
375        let line_height = element_state.line_height;
376        let mut line_origin = bounds.origin;
377        let mut line_start_ix = 0;
378        for line in &element_state.lines {
379            let line_bottom = line_origin.y + line.size(line_height).height;
380            if position.y > line_bottom {
381                line_origin.y = line_bottom;
382                line_start_ix += line.len() + 1;
383            } else {
384                let position_within_line = position - line_origin;
385                match line.index_for_position(position_within_line, line_height) {
386                    Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
387                    Err(index_within_line) => return Err(line_start_ix + index_within_line),
388                }
389            }
390        }
391
392        Err(line_start_ix.saturating_sub(1))
393    }
394
395    /// Get the pixel position for the given byte index.
396    pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
397        let element_state = self.lock();
398        let element_state = element_state
399            .as_ref()
400            .expect("measurement has not been performed");
401        let bounds = element_state
402            .bounds
403            .expect("prepaint has not been performed");
404        let line_height = element_state.line_height;
405
406        let mut line_origin = bounds.origin;
407        let mut line_start_ix = 0;
408
409        for line in &element_state.lines {
410            let line_end_ix = line_start_ix + line.len();
411            if index < line_start_ix {
412                break;
413            } else if index > line_end_ix {
414                line_origin.y += line.size(line_height).height;
415                line_start_ix = line_end_ix + 1;
416                continue;
417            } else {
418                let ix_within_line = index - line_start_ix;
419                return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
420            }
421        }
422
423        None
424    }
425
426    /// The bounds of this layout.
427    pub fn bounds(&self) -> Bounds<Pixels> {
428        self.0.lock().as_ref().unwrap().bounds.unwrap()
429    }
430
431    /// The line height for this layout.
432    pub fn line_height(&self) -> Pixels {
433        self.0.lock().as_ref().unwrap().line_height
434    }
435
436    /// The text for this layout.
437    pub fn text(&self) -> String {
438        self.0
439            .lock()
440            .as_ref()
441            .unwrap()
442            .lines
443            .iter()
444            .map(|s| s.text.to_string())
445            .collect::<Vec<_>>()
446            .join("\n")
447    }
448}
449
450/// A text element that can be interacted with.
451pub struct InteractiveText {
452    element_id: ElementId,
453    text: StyledText,
454    click_listener:
455        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
456    hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
457    tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
458    clickable_ranges: Vec<Range<usize>>,
459}
460
461struct InteractiveTextClickEvent {
462    mouse_down_index: usize,
463    mouse_up_index: usize,
464}
465
466#[doc(hidden)]
467#[derive(Default)]
468pub struct InteractiveTextState {
469    mouse_down_index: Rc<Cell<Option<usize>>>,
470    hovered_index: Rc<Cell<Option<usize>>>,
471    active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
472}
473
474/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
475impl InteractiveText {
476    /// Creates a new InteractiveText from the given text.
477    pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
478        Self {
479            element_id: id.into(),
480            text,
481            click_listener: None,
482            hover_listener: None,
483            tooltip_builder: None,
484            clickable_ranges: Vec::new(),
485        }
486    }
487
488    /// on_click is called when the user clicks on one of the given ranges, passing the index of
489    /// the clicked range.
490    pub fn on_click(
491        mut self,
492        ranges: Vec<Range<usize>>,
493        listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
494    ) -> Self {
495        self.click_listener = Some(Box::new(move |ranges, event, cx| {
496            for (range_ix, range) in ranges.iter().enumerate() {
497                if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
498                {
499                    listener(range_ix, cx);
500                }
501            }
502        }));
503        self.clickable_ranges = ranges;
504        self
505    }
506
507    /// on_hover is called when the mouse moves over a character within the text, passing the
508    /// index of the hovered character, or None if the mouse leaves the text.
509    pub fn on_hover(
510        mut self,
511        listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
512    ) -> Self {
513        self.hover_listener = Some(Box::new(listener));
514        self
515    }
516
517    /// tooltip lets you specify a tooltip for a given character index in the string.
518    pub fn tooltip(
519        mut self,
520        builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
521    ) -> Self {
522        self.tooltip_builder = Some(Rc::new(builder));
523        self
524    }
525}
526
527impl Element for InteractiveText {
528    type RequestLayoutState = ();
529    type PrepaintState = Hitbox;
530
531    fn id(&self) -> Option<ElementId> {
532        Some(self.element_id.clone())
533    }
534
535    fn request_layout(
536        &mut self,
537        _id: Option<&GlobalElementId>,
538        cx: &mut WindowContext,
539    ) -> (LayoutId, Self::RequestLayoutState) {
540        self.text.request_layout(None, cx)
541    }
542
543    fn prepaint(
544        &mut self,
545        global_id: Option<&GlobalElementId>,
546        bounds: Bounds<Pixels>,
547        state: &mut Self::RequestLayoutState,
548        cx: &mut WindowContext,
549    ) -> Hitbox {
550        cx.with_optional_element_state::<InteractiveTextState, _>(
551            global_id,
552            |interactive_state, cx| {
553                let interactive_state = interactive_state
554                    .map(|interactive_state| interactive_state.unwrap_or_default());
555
556                if let Some(interactive_state) = interactive_state.as_ref() {
557                    if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
558                    {
559                        if let Some(tooltip) = active_tooltip.tooltip.clone() {
560                            cx.set_tooltip(tooltip);
561                        }
562                    }
563                }
564
565                self.text.prepaint(None, bounds, state, cx);
566                let hitbox = cx.insert_hitbox(bounds, false);
567                (hitbox, interactive_state)
568            },
569        )
570    }
571
572    fn paint(
573        &mut self,
574        global_id: Option<&GlobalElementId>,
575        bounds: Bounds<Pixels>,
576        _: &mut Self::RequestLayoutState,
577        hitbox: &mut Hitbox,
578        cx: &mut WindowContext,
579    ) {
580        let text_layout = self.text.layout().clone();
581        cx.with_element_state::<InteractiveTextState, _>(
582            global_id.unwrap(),
583            |interactive_state, cx| {
584                let mut interactive_state = interactive_state.unwrap_or_default();
585                if let Some(click_listener) = self.click_listener.take() {
586                    let mouse_position = cx.mouse_position();
587                    if let Some(ix) = text_layout.index_for_position(mouse_position).ok() {
588                        if self
589                            .clickable_ranges
590                            .iter()
591                            .any(|range| range.contains(&ix))
592                        {
593                            cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
594                        }
595                    }
596
597                    let text_layout = text_layout.clone();
598                    let mouse_down = interactive_state.mouse_down_index.clone();
599                    if let Some(mouse_down_index) = mouse_down.get() {
600                        let hitbox = hitbox.clone();
601                        let clickable_ranges = mem::take(&mut self.clickable_ranges);
602                        cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
603                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
604                                if let Some(mouse_up_index) =
605                                    text_layout.index_for_position(event.position).ok()
606                                {
607                                    click_listener(
608                                        &clickable_ranges,
609                                        InteractiveTextClickEvent {
610                                            mouse_down_index,
611                                            mouse_up_index,
612                                        },
613                                        cx,
614                                    )
615                                }
616
617                                mouse_down.take();
618                                cx.refresh();
619                            }
620                        });
621                    } else {
622                        let hitbox = hitbox.clone();
623                        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
624                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
625                                if let Some(mouse_down_index) =
626                                    text_layout.index_for_position(event.position).ok()
627                                {
628                                    mouse_down.set(Some(mouse_down_index));
629                                    cx.refresh();
630                                }
631                            }
632                        });
633                    }
634                }
635
636                cx.on_mouse_event({
637                    let mut hover_listener = self.hover_listener.take();
638                    let hitbox = hitbox.clone();
639                    let text_layout = text_layout.clone();
640                    let hovered_index = interactive_state.hovered_index.clone();
641                    move |event: &MouseMoveEvent, phase, cx| {
642                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
643                            let current = hovered_index.get();
644                            let updated = text_layout.index_for_position(event.position).ok();
645                            if current != updated {
646                                hovered_index.set(updated);
647                                if let Some(hover_listener) = hover_listener.as_ref() {
648                                    hover_listener(updated, event.clone(), cx);
649                                }
650                                cx.refresh();
651                            }
652                        }
653                    }
654                });
655
656                if let Some(tooltip_builder) = self.tooltip_builder.clone() {
657                    let hitbox = hitbox.clone();
658                    let active_tooltip = interactive_state.active_tooltip.clone();
659                    let pending_mouse_down = interactive_state.mouse_down_index.clone();
660                    let text_layout = text_layout.clone();
661
662                    cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
663                        let position = text_layout.index_for_position(event.position).ok();
664                        let is_hovered = position.is_some()
665                            && hitbox.is_hovered(cx)
666                            && pending_mouse_down.get().is_none();
667                        if !is_hovered {
668                            active_tooltip.take();
669                            return;
670                        }
671                        let position = position.unwrap();
672
673                        if phase != DispatchPhase::Bubble {
674                            return;
675                        }
676
677                        if active_tooltip.borrow().is_none() {
678                            let task = cx.spawn({
679                                let active_tooltip = active_tooltip.clone();
680                                let tooltip_builder = tooltip_builder.clone();
681
682                                move |mut cx| async move {
683                                    cx.background_executor().timer(TOOLTIP_DELAY).await;
684                                    cx.update(|cx| {
685                                        let new_tooltip =
686                                            tooltip_builder(position, cx).map(|tooltip| {
687                                                ActiveTooltip {
688                                                    tooltip: Some(AnyTooltip {
689                                                        view: tooltip,
690                                                        mouse_position: cx.mouse_position(),
691                                                    }),
692                                                    _task: None,
693                                                }
694                                            });
695                                        *active_tooltip.borrow_mut() = new_tooltip;
696                                        cx.refresh();
697                                    })
698                                    .ok();
699                                }
700                            });
701                            *active_tooltip.borrow_mut() = Some(ActiveTooltip {
702                                tooltip: None,
703                                _task: Some(task),
704                            });
705                        }
706                    });
707
708                    let active_tooltip = interactive_state.active_tooltip.clone();
709                    cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
710                        active_tooltip.take();
711                    });
712                }
713
714                self.text.paint(None, bounds, &mut (), &mut (), cx);
715
716                ((), interactive_state)
717            },
718        );
719    }
720}
721
722impl IntoElement for InteractiveText {
723    type Element = Self;
724
725    fn into_element(self) -> Self::Element {
726        self
727    }
728}