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