text.rs

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