text.rs

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