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(line_origin, line_height, text_style.text_align, window, cx)
396 .log_err();
397 line_origin.y += line.size(line_height).height;
398 }
399 }
400
401 /// Get the byte index into the input of the pixel position.
402 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
403 let element_state = self.lock();
404 let element_state = element_state
405 .as_ref()
406 .expect("measurement has not been performed");
407 let bounds = element_state
408 .bounds
409 .expect("prepaint has not been performed");
410
411 if position.y < bounds.top() {
412 return Err(0);
413 }
414
415 let line_height = element_state.line_height;
416 let mut line_origin = bounds.origin;
417 let mut line_start_ix = 0;
418 for line in &element_state.lines {
419 let line_bottom = line_origin.y + line.size(line_height).height;
420 if position.y > line_bottom {
421 line_origin.y = line_bottom;
422 line_start_ix += line.len() + 1;
423 } else {
424 let position_within_line = position - line_origin;
425 match line.index_for_position(position_within_line, line_height) {
426 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
427 Err(index_within_line) => return Err(line_start_ix + index_within_line),
428 }
429 }
430 }
431
432 Err(line_start_ix.saturating_sub(1))
433 }
434
435 /// Get the pixel position for the given byte index.
436 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
437 let element_state = self.lock();
438 let element_state = element_state
439 .as_ref()
440 .expect("measurement has not been performed");
441 let bounds = element_state
442 .bounds
443 .expect("prepaint has not been performed");
444 let line_height = element_state.line_height;
445
446 let mut line_origin = bounds.origin;
447 let mut line_start_ix = 0;
448
449 for line in &element_state.lines {
450 let line_end_ix = line_start_ix + line.len();
451 if index < line_start_ix {
452 break;
453 } else if index > line_end_ix {
454 line_origin.y += line.size(line_height).height;
455 line_start_ix = line_end_ix + 1;
456 continue;
457 } else {
458 let ix_within_line = index - line_start_ix;
459 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
460 }
461 }
462
463 None
464 }
465
466 /// Retrieve the layout for the line containing the given byte index.
467 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
468 let element_state = self.lock();
469 let element_state = element_state
470 .as_ref()
471 .expect("measurement has not been performed");
472 let bounds = element_state
473 .bounds
474 .expect("prepaint has not been performed");
475 let line_height = element_state.line_height;
476
477 let mut line_origin = bounds.origin;
478 let mut line_start_ix = 0;
479
480 for line in &element_state.lines {
481 let line_end_ix = line_start_ix + line.len();
482 if index < line_start_ix {
483 break;
484 } else if index > line_end_ix {
485 line_origin.y += line.size(line_height).height;
486 line_start_ix = line_end_ix + 1;
487 continue;
488 } else {
489 return Some(line.layout.clone());
490 }
491 }
492
493 None
494 }
495
496 /// The bounds of this layout.
497 pub fn bounds(&self) -> Bounds<Pixels> {
498 self.0.lock().as_ref().unwrap().bounds.unwrap()
499 }
500
501 /// The line height for this layout.
502 pub fn line_height(&self) -> Pixels {
503 self.0.lock().as_ref().unwrap().line_height
504 }
505
506 /// The text for this layout.
507 pub fn text(&self) -> String {
508 self.0
509 .lock()
510 .as_ref()
511 .unwrap()
512 .lines
513 .iter()
514 .map(|s| s.text.to_string())
515 .collect::<Vec<_>>()
516 .join("\n")
517 }
518}
519
520/// A text element that can be interacted with.
521pub struct InteractiveText {
522 element_id: ElementId,
523 text: StyledText,
524 click_listener:
525 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
526 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
527 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
528 tooltip_id: Option<TooltipId>,
529 clickable_ranges: Vec<Range<usize>>,
530}
531
532struct InteractiveTextClickEvent {
533 mouse_down_index: usize,
534 mouse_up_index: usize,
535}
536
537#[doc(hidden)]
538#[derive(Default)]
539pub struct InteractiveTextState {
540 mouse_down_index: Rc<Cell<Option<usize>>>,
541 hovered_index: Rc<Cell<Option<usize>>>,
542 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
543}
544
545/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
546impl InteractiveText {
547 /// Creates a new InteractiveText from the given text.
548 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
549 Self {
550 element_id: id.into(),
551 text,
552 click_listener: None,
553 hover_listener: None,
554 tooltip_builder: None,
555 tooltip_id: None,
556 clickable_ranges: Vec::new(),
557 }
558 }
559
560 /// on_click is called when the user clicks on one of the given ranges, passing the index of
561 /// the clicked range.
562 pub fn on_click(
563 mut self,
564 ranges: Vec<Range<usize>>,
565 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
566 ) -> Self {
567 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
568 for (range_ix, range) in ranges.iter().enumerate() {
569 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
570 {
571 listener(range_ix, window, cx);
572 }
573 }
574 }));
575 self.clickable_ranges = ranges;
576 self
577 }
578
579 /// on_hover is called when the mouse moves over a character within the text, passing the
580 /// index of the hovered character, or None if the mouse leaves the text.
581 pub fn on_hover(
582 mut self,
583 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
584 ) -> Self {
585 self.hover_listener = Some(Box::new(listener));
586 self
587 }
588
589 /// tooltip lets you specify a tooltip for a given character index in the string.
590 pub fn tooltip(
591 mut self,
592 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
593 ) -> Self {
594 self.tooltip_builder = Some(Rc::new(builder));
595 self
596 }
597}
598
599impl Element for InteractiveText {
600 type RequestLayoutState = ();
601 type PrepaintState = Hitbox;
602
603 fn id(&self) -> Option<ElementId> {
604 Some(self.element_id.clone())
605 }
606
607 fn request_layout(
608 &mut self,
609 _id: Option<&GlobalElementId>,
610 window: &mut Window,
611 cx: &mut App,
612 ) -> (LayoutId, Self::RequestLayoutState) {
613 self.text.request_layout(None, window, cx)
614 }
615
616 fn prepaint(
617 &mut self,
618 global_id: Option<&GlobalElementId>,
619 bounds: Bounds<Pixels>,
620 state: &mut Self::RequestLayoutState,
621 window: &mut Window,
622 cx: &mut App,
623 ) -> Hitbox {
624 window.with_optional_element_state::<InteractiveTextState, _>(
625 global_id,
626 |interactive_state, window| {
627 let mut interactive_state = interactive_state
628 .map(|interactive_state| interactive_state.unwrap_or_default());
629
630 if let Some(interactive_state) = interactive_state.as_mut() {
631 if self.tooltip_builder.is_some() {
632 self.tooltip_id =
633 set_tooltip_on_window(&interactive_state.active_tooltip, window);
634 } else {
635 // If there is no longer a tooltip builder, remove the active tooltip.
636 interactive_state.active_tooltip.take();
637 }
638 }
639
640 self.text.prepaint(None, bounds, state, window, cx);
641 let hitbox = window.insert_hitbox(bounds, false);
642 (hitbox, interactive_state)
643 },
644 )
645 }
646
647 fn paint(
648 &mut self,
649 global_id: Option<&GlobalElementId>,
650 bounds: Bounds<Pixels>,
651 _: &mut Self::RequestLayoutState,
652 hitbox: &mut Hitbox,
653 window: &mut Window,
654 cx: &mut App,
655 ) {
656 let text_layout = self.text.layout().clone();
657 window.with_element_state::<InteractiveTextState, _>(
658 global_id.unwrap(),
659 |interactive_state, window| {
660 let mut interactive_state = interactive_state.unwrap_or_default();
661 if let Some(click_listener) = self.click_listener.take() {
662 let mouse_position = window.mouse_position();
663 if let Ok(ix) = text_layout.index_for_position(mouse_position) {
664 if self
665 .clickable_ranges
666 .iter()
667 .any(|range| range.contains(&ix))
668 {
669 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
670 }
671 }
672
673 let text_layout = text_layout.clone();
674 let mouse_down = interactive_state.mouse_down_index.clone();
675 if let Some(mouse_down_index) = mouse_down.get() {
676 let hitbox = hitbox.clone();
677 let clickable_ranges = mem::take(&mut self.clickable_ranges);
678 window.on_mouse_event(
679 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
680 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
681 if let Ok(mouse_up_index) =
682 text_layout.index_for_position(event.position)
683 {
684 click_listener(
685 &clickable_ranges,
686 InteractiveTextClickEvent {
687 mouse_down_index,
688 mouse_up_index,
689 },
690 window,
691 cx,
692 )
693 }
694
695 mouse_down.take();
696 window.refresh();
697 }
698 },
699 );
700 } else {
701 let hitbox = hitbox.clone();
702 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
703 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
704 if let Ok(mouse_down_index) =
705 text_layout.index_for_position(event.position)
706 {
707 mouse_down.set(Some(mouse_down_index));
708 window.refresh();
709 }
710 }
711 });
712 }
713 }
714
715 window.on_mouse_event({
716 let mut hover_listener = self.hover_listener.take();
717 let hitbox = hitbox.clone();
718 let text_layout = text_layout.clone();
719 let hovered_index = interactive_state.hovered_index.clone();
720 move |event: &MouseMoveEvent, phase, window, cx| {
721 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
722 let current = hovered_index.get();
723 let updated = text_layout.index_for_position(event.position).ok();
724 if current != updated {
725 hovered_index.set(updated);
726 if let Some(hover_listener) = hover_listener.as_ref() {
727 hover_listener(updated, event.clone(), window, cx);
728 }
729 window.refresh();
730 }
731 }
732 }
733 });
734
735 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
736 let active_tooltip = interactive_state.active_tooltip.clone();
737 let pending_mouse_down = interactive_state.mouse_down_index.clone();
738 let build_tooltip = Rc::new({
739 let tooltip_is_hoverable = false;
740 let text_layout = text_layout.clone();
741 move |window: &mut Window, cx: &mut App| {
742 text_layout
743 .index_for_position(window.mouse_position())
744 .ok()
745 .and_then(|position| tooltip_builder(position, window, cx))
746 .map(|view| (view, tooltip_is_hoverable))
747 }
748 });
749 // Use bounds instead of testing hitbox since check_is_hovered is also
750 // called during prepaint.
751 let source_bounds = hitbox.bounds;
752 let check_is_hovered = Rc::new({
753 let text_layout = text_layout.clone();
754 move |window: &Window| {
755 text_layout
756 .index_for_position(window.mouse_position())
757 .is_ok()
758 && source_bounds.contains(&window.mouse_position())
759 && pending_mouse_down.get().is_none()
760 }
761 });
762 register_tooltip_mouse_handlers(
763 &active_tooltip,
764 self.tooltip_id,
765 build_tooltip,
766 check_is_hovered,
767 window,
768 );
769 }
770
771 self.text.paint(None, bounds, &mut (), &mut (), window, cx);
772
773 ((), interactive_state)
774 },
775 );
776 }
777}
778
779impl IntoElement for InteractiveText {
780 type Element = Self;
781
782 fn into_element(self) -> Self::Element {
783 self
784 }
785}