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