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