text.rs

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