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, Truncate, WhiteSpace, WindowContext,
5 WrappedLine, WrappedLineLayout, 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 /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
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/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
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
247const ELLIPSIS: &str = "…";
248
249impl TextLayout {
250 fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
251 self.0.lock()
252 }
253
254 fn layout(
255 &self,
256 text: SharedString,
257 runs: Option<Vec<TextRun>>,
258 cx: &mut WindowContext,
259 ) -> LayoutId {
260 let text_style = cx.text_style();
261 let font_size = text_style.font_size.to_pixels(cx.rem_size());
262 let line_height = text_style
263 .line_height
264 .to_pixels(font_size.into(), cx.rem_size());
265
266 let mut runs = if let Some(runs) = runs {
267 runs
268 } else {
269 vec![text_style.to_run(text.len())]
270 };
271
272 let layout_id = cx.request_measured_layout(Default::default(), {
273 let element_state = self.clone();
274
275 move |known_dimensions, available_space, cx| {
276 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
277 known_dimensions.width.or(match available_space.width {
278 crate::AvailableSpace::Definite(x) => Some(x),
279 _ => None,
280 })
281 } else {
282 None
283 };
284
285 let (truncate_width, ellipsis) = if let Some(truncate) = text_style.truncate {
286 let width = known_dimensions.width.or(match available_space.width {
287 crate::AvailableSpace::Definite(x) => Some(x),
288 _ => None,
289 });
290
291 match truncate {
292 Truncate::Truncate => (width, None),
293 Truncate::Ellipsis => (width, Some(ELLIPSIS)),
294 }
295 } else {
296 (None, None)
297 };
298
299 if let Some(text_layout) = element_state.0.lock().as_ref() {
300 if text_layout.size.is_some()
301 && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
302 {
303 return text_layout.size.unwrap();
304 }
305 }
306
307 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
308 let text = if let Some(truncate_width) = truncate_width {
309 line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
310 } else {
311 text.clone()
312 };
313
314 let Some(lines) = cx
315 .text_system()
316 .shape_text(
317 text, font_size, &runs, wrap_width, // Wrap if we know the width.
318 )
319 .log_err()
320 else {
321 element_state.lock().replace(TextLayoutInner {
322 lines: Default::default(),
323 line_height,
324 wrap_width,
325 size: Some(Size::default()),
326 bounds: None,
327 });
328 return Size::default();
329 };
330
331 let mut size: Size<Pixels> = Size::default();
332 for line in &lines {
333 let line_size = line.size(line_height);
334 size.height += line_size.height;
335 size.width = size.width.max(line_size.width).ceil();
336 }
337
338 element_state.lock().replace(TextLayoutInner {
339 lines,
340 line_height,
341 wrap_width,
342 size: Some(size),
343 bounds: None,
344 });
345
346 size
347 }
348 });
349
350 layout_id
351 }
352
353 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
354 let mut element_state = self.lock();
355 let element_state = element_state
356 .as_mut()
357 .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
358 .unwrap();
359 element_state.bounds = Some(bounds);
360 }
361
362 fn paint(&self, text: &str, cx: &mut WindowContext) {
363 let element_state = self.lock();
364 let element_state = element_state
365 .as_ref()
366 .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
367 .unwrap();
368 let bounds = element_state
369 .bounds
370 .ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
371 .unwrap();
372
373 let line_height = element_state.line_height;
374 let mut line_origin = bounds.origin;
375 for line in &element_state.lines {
376 line.paint(line_origin, line_height, cx).log_err();
377 line_origin.y += line.size(line_height).height;
378 }
379 }
380
381 /// Get the byte index into the input of the pixel position.
382 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
383 let element_state = self.lock();
384 let element_state = element_state
385 .as_ref()
386 .expect("measurement has not been performed");
387 let bounds = element_state
388 .bounds
389 .expect("prepaint has not been performed");
390
391 if position.y < bounds.top() {
392 return Err(0);
393 }
394
395 let line_height = element_state.line_height;
396 let mut line_origin = bounds.origin;
397 let mut line_start_ix = 0;
398 for line in &element_state.lines {
399 let line_bottom = line_origin.y + line.size(line_height).height;
400 if position.y > line_bottom {
401 line_origin.y = line_bottom;
402 line_start_ix += line.len() + 1;
403 } else {
404 let position_within_line = position - line_origin;
405 match line.index_for_position(position_within_line, line_height) {
406 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
407 Err(index_within_line) => return Err(line_start_ix + index_within_line),
408 }
409 }
410 }
411
412 Err(line_start_ix.saturating_sub(1))
413 }
414
415 /// Get the pixel position for the given byte index.
416 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
417 let element_state = self.lock();
418 let element_state = element_state
419 .as_ref()
420 .expect("measurement has not been performed");
421 let bounds = element_state
422 .bounds
423 .expect("prepaint has not been performed");
424 let line_height = element_state.line_height;
425
426 let mut line_origin = bounds.origin;
427 let mut line_start_ix = 0;
428
429 for line in &element_state.lines {
430 let line_end_ix = line_start_ix + line.len();
431 if index < line_start_ix {
432 break;
433 } else if index > line_end_ix {
434 line_origin.y += line.size(line_height).height;
435 line_start_ix = line_end_ix + 1;
436 continue;
437 } else {
438 let ix_within_line = index - line_start_ix;
439 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
440 }
441 }
442
443 None
444 }
445
446 /// Retrieve the layout for the line containing the given byte index.
447 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
448 let element_state = self.lock();
449 let element_state = element_state
450 .as_ref()
451 .expect("measurement has not been performed");
452 let bounds = element_state
453 .bounds
454 .expect("prepaint has not been performed");
455 let line_height = element_state.line_height;
456
457 let mut line_origin = bounds.origin;
458 let mut line_start_ix = 0;
459
460 for line in &element_state.lines {
461 let line_end_ix = line_start_ix + line.len();
462 if index < line_start_ix {
463 break;
464 } else if index > line_end_ix {
465 line_origin.y += line.size(line_height).height;
466 line_start_ix = line_end_ix + 1;
467 continue;
468 } else {
469 return Some(line.layout.clone());
470 }
471 }
472
473 None
474 }
475
476 /// The bounds of this layout.
477 pub fn bounds(&self) -> Bounds<Pixels> {
478 self.0.lock().as_ref().unwrap().bounds.unwrap()
479 }
480
481 /// The line height for this layout.
482 pub fn line_height(&self) -> Pixels {
483 self.0.lock().as_ref().unwrap().line_height
484 }
485
486 /// The text for this layout.
487 pub fn text(&self) -> String {
488 self.0
489 .lock()
490 .as_ref()
491 .unwrap()
492 .lines
493 .iter()
494 .map(|s| s.text.to_string())
495 .collect::<Vec<_>>()
496 .join("\n")
497 }
498}
499
500/// A text element that can be interacted with.
501pub struct InteractiveText {
502 element_id: ElementId,
503 text: StyledText,
504 click_listener:
505 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext)>>,
506 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext)>>,
507 tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext) -> Option<AnyView>>>,
508 clickable_ranges: Vec<Range<usize>>,
509}
510
511struct InteractiveTextClickEvent {
512 mouse_down_index: usize,
513 mouse_up_index: usize,
514}
515
516#[doc(hidden)]
517#[derive(Default)]
518pub struct InteractiveTextState {
519 mouse_down_index: Rc<Cell<Option<usize>>>,
520 hovered_index: Rc<Cell<Option<usize>>>,
521 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
522}
523
524/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
525impl InteractiveText {
526 /// Creates a new InteractiveText from the given text.
527 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
528 Self {
529 element_id: id.into(),
530 text,
531 click_listener: None,
532 hover_listener: None,
533 tooltip_builder: None,
534 clickable_ranges: Vec::new(),
535 }
536 }
537
538 /// on_click is called when the user clicks on one of the given ranges, passing the index of
539 /// the clicked range.
540 pub fn on_click(
541 mut self,
542 ranges: Vec<Range<usize>>,
543 listener: impl Fn(usize, &mut WindowContext) + 'static,
544 ) -> Self {
545 self.click_listener = Some(Box::new(move |ranges, event, cx| {
546 for (range_ix, range) in ranges.iter().enumerate() {
547 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
548 {
549 listener(range_ix, cx);
550 }
551 }
552 }));
553 self.clickable_ranges = ranges;
554 self
555 }
556
557 /// on_hover is called when the mouse moves over a character within the text, passing the
558 /// index of the hovered character, or None if the mouse leaves the text.
559 pub fn on_hover(
560 mut self,
561 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext) + 'static,
562 ) -> Self {
563 self.hover_listener = Some(Box::new(listener));
564 self
565 }
566
567 /// tooltip lets you specify a tooltip for a given character index in the string.
568 pub fn tooltip(
569 mut self,
570 builder: impl Fn(usize, &mut WindowContext) -> Option<AnyView> + 'static,
571 ) -> Self {
572 self.tooltip_builder = Some(Rc::new(builder));
573 self
574 }
575}
576
577impl Element for InteractiveText {
578 type RequestLayoutState = ();
579 type PrepaintState = Hitbox;
580
581 fn id(&self) -> Option<ElementId> {
582 Some(self.element_id.clone())
583 }
584
585 fn request_layout(
586 &mut self,
587 _id: Option<&GlobalElementId>,
588 cx: &mut WindowContext,
589 ) -> (LayoutId, Self::RequestLayoutState) {
590 self.text.request_layout(None, cx)
591 }
592
593 fn prepaint(
594 &mut self,
595 global_id: Option<&GlobalElementId>,
596 bounds: Bounds<Pixels>,
597 state: &mut Self::RequestLayoutState,
598 cx: &mut WindowContext,
599 ) -> Hitbox {
600 cx.with_optional_element_state::<InteractiveTextState, _>(
601 global_id,
602 |interactive_state, cx| {
603 let interactive_state = interactive_state
604 .map(|interactive_state| interactive_state.unwrap_or_default());
605
606 if let Some(interactive_state) = interactive_state.as_ref() {
607 if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
608 {
609 if let Some(tooltip) = active_tooltip.tooltip.clone() {
610 cx.set_tooltip(tooltip);
611 }
612 }
613 }
614
615 self.text.prepaint(None, bounds, state, cx);
616 let hitbox = cx.insert_hitbox(bounds, false);
617 (hitbox, interactive_state)
618 },
619 )
620 }
621
622 fn paint(
623 &mut self,
624 global_id: Option<&GlobalElementId>,
625 bounds: Bounds<Pixels>,
626 _: &mut Self::RequestLayoutState,
627 hitbox: &mut Hitbox,
628 cx: &mut WindowContext,
629 ) {
630 let text_layout = self.text.layout().clone();
631 cx.with_element_state::<InteractiveTextState, _>(
632 global_id.unwrap(),
633 |interactive_state, cx| {
634 let mut interactive_state = interactive_state.unwrap_or_default();
635 if let Some(click_listener) = self.click_listener.take() {
636 let mouse_position = cx.mouse_position();
637 if let Ok(ix) = text_layout.index_for_position(mouse_position) {
638 if self
639 .clickable_ranges
640 .iter()
641 .any(|range| range.contains(&ix))
642 {
643 cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
644 }
645 }
646
647 let text_layout = text_layout.clone();
648 let mouse_down = interactive_state.mouse_down_index.clone();
649 if let Some(mouse_down_index) = mouse_down.get() {
650 let hitbox = hitbox.clone();
651 let clickable_ranges = mem::take(&mut self.clickable_ranges);
652 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
653 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
654 if let Ok(mouse_up_index) =
655 text_layout.index_for_position(event.position)
656 {
657 click_listener(
658 &clickable_ranges,
659 InteractiveTextClickEvent {
660 mouse_down_index,
661 mouse_up_index,
662 },
663 cx,
664 )
665 }
666
667 mouse_down.take();
668 cx.refresh();
669 }
670 });
671 } else {
672 let hitbox = hitbox.clone();
673 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
674 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
675 if let Ok(mouse_down_index) =
676 text_layout.index_for_position(event.position)
677 {
678 mouse_down.set(Some(mouse_down_index));
679 cx.refresh();
680 }
681 }
682 });
683 }
684 }
685
686 cx.on_mouse_event({
687 let mut hover_listener = self.hover_listener.take();
688 let hitbox = hitbox.clone();
689 let text_layout = text_layout.clone();
690 let hovered_index = interactive_state.hovered_index.clone();
691 move |event: &MouseMoveEvent, phase, cx| {
692 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
693 let current = hovered_index.get();
694 let updated = text_layout.index_for_position(event.position).ok();
695 if current != updated {
696 hovered_index.set(updated);
697 if let Some(hover_listener) = hover_listener.as_ref() {
698 hover_listener(updated, event.clone(), cx);
699 }
700 cx.refresh();
701 }
702 }
703 }
704 });
705
706 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
707 let hitbox = hitbox.clone();
708 let source_bounds = hitbox.bounds;
709 let active_tooltip = interactive_state.active_tooltip.clone();
710 let pending_mouse_down = interactive_state.mouse_down_index.clone();
711 let text_layout = text_layout.clone();
712
713 cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
714 let position = text_layout.index_for_position(event.position).ok();
715 let is_hovered = position.is_some()
716 && hitbox.is_hovered(cx)
717 && pending_mouse_down.get().is_none();
718 if !is_hovered {
719 active_tooltip.take();
720 return;
721 }
722 let position = position.unwrap();
723
724 if phase != DispatchPhase::Bubble {
725 return;
726 }
727
728 if active_tooltip.borrow().is_none() {
729 let task = cx.spawn({
730 let active_tooltip = active_tooltip.clone();
731 let tooltip_builder = tooltip_builder.clone();
732
733 move |mut cx| async move {
734 cx.background_executor().timer(TOOLTIP_DELAY).await;
735 cx.update(|cx| {
736 let new_tooltip =
737 tooltip_builder(position, cx).map(|tooltip| {
738 ActiveTooltip {
739 tooltip: Some(AnyTooltip {
740 view: tooltip,
741 mouse_position: cx.mouse_position(),
742 hoverable: true,
743 origin_bounds: source_bounds,
744 }),
745 _task: None,
746 }
747 });
748 *active_tooltip.borrow_mut() = new_tooltip;
749 cx.refresh();
750 })
751 .ok();
752 }
753 });
754 *active_tooltip.borrow_mut() = Some(ActiveTooltip {
755 tooltip: None,
756 _task: Some(task),
757 });
758 }
759 });
760
761 let active_tooltip = interactive_state.active_tooltip.clone();
762 cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
763 active_tooltip.take();
764 });
765 }
766
767 self.text.paint(None, bounds, &mut (), &mut (), cx);
768
769 ((), interactive_state)
770 },
771 );
772 }
773}
774
775impl IntoElement for InteractiveText {
776 type Element = Self;
777
778 fn into_element(self) -> Self::Element {
779 self
780 }
781}