text.rs

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