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