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