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, 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 /// The bounds of this layout.
447 pub fn bounds(&self) -> Bounds<Pixels> {
448 self.0.lock().as_ref().unwrap().bounds.unwrap()
449 }
450
451 /// The line height for this layout.
452 pub fn line_height(&self) -> Pixels {
453 self.0.lock().as_ref().unwrap().line_height
454 }
455
456 /// The text for this layout.
457 pub fn text(&self) -> String {
458 self.0
459 .lock()
460 .as_ref()
461 .unwrap()
462 .lines
463 .iter()
464 .map(|s| s.text.to_string())
465 .collect::<Vec<_>>()
466 .join("\n")
467 }
468}
469
470/// A text element that can be interacted with.
471pub struct InteractiveText {
472 element_id: ElementId,
473 text: StyledText,
474 click_listener:
475 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
476 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
477 tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
478 clickable_ranges: Vec<Range<usize>>,
479}
480
481struct InteractiveTextClickEvent {
482 mouse_down_index: usize,
483 mouse_up_index: usize,
484}
485
486#[doc(hidden)]
487#[derive(Default)]
488pub struct InteractiveTextState {
489 mouse_down_index: Rc<Cell<Option<usize>>>,
490 hovered_index: Rc<Cell<Option<usize>>>,
491 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
492}
493
494/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
495impl InteractiveText {
496 /// Creates a new InteractiveText from the given text.
497 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
498 Self {
499 element_id: id.into(),
500 text,
501 click_listener: None,
502 hover_listener: None,
503 tooltip_builder: None,
504 clickable_ranges: Vec::new(),
505 }
506 }
507
508 /// on_click is called when the user clicks on one of the given ranges, passing the index of
509 /// the clicked range.
510 pub fn on_click(
511 mut self,
512 ranges: Vec<Range<usize>>,
513 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
514 ) -> Self {
515 self.click_listener = Some(Box::new(move |ranges, event, cx| {
516 for (range_ix, range) in ranges.iter().enumerate() {
517 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
518 {
519 listener(range_ix, cx);
520 }
521 }
522 }));
523 self.clickable_ranges = ranges;
524 self
525 }
526
527 /// on_hover is called when the mouse moves over a character within the text, passing the
528 /// index of the hovered character, or None if the mouse leaves the text.
529 pub fn on_hover(
530 mut self,
531 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
532 ) -> Self {
533 self.hover_listener = Some(Box::new(listener));
534 self
535 }
536
537 /// tooltip lets you specify a tooltip for a given character index in the string.
538 pub fn tooltip(
539 mut self,
540 builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
541 ) -> Self {
542 self.tooltip_builder = Some(Rc::new(builder));
543 self
544 }
545}
546
547impl Element for InteractiveText {
548 type RequestLayoutState = ();
549 type PrepaintState = Hitbox;
550
551 fn id(&self) -> Option<ElementId> {
552 Some(self.element_id.clone())
553 }
554
555 fn request_layout(
556 &mut self,
557 _id: Option<&GlobalElementId>,
558 cx: &mut WindowContext,
559 ) -> (LayoutId, Self::RequestLayoutState) {
560 self.text.request_layout(None, cx)
561 }
562
563 fn prepaint(
564 &mut self,
565 global_id: Option<&GlobalElementId>,
566 bounds: Bounds<Pixels>,
567 state: &mut Self::RequestLayoutState,
568 cx: &mut WindowContext,
569 ) -> Hitbox {
570 cx.with_optional_element_state::<InteractiveTextState, _>(
571 global_id,
572 |interactive_state, cx| {
573 let interactive_state = interactive_state
574 .map(|interactive_state| interactive_state.unwrap_or_default());
575
576 if let Some(interactive_state) = interactive_state.as_ref() {
577 if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
578 {
579 if let Some(tooltip) = active_tooltip.tooltip.clone() {
580 cx.set_tooltip(tooltip);
581 }
582 }
583 }
584
585 self.text.prepaint(None, bounds, state, cx);
586 let hitbox = cx.insert_hitbox(bounds, false);
587 (hitbox, interactive_state)
588 },
589 )
590 }
591
592 fn paint(
593 &mut self,
594 global_id: Option<&GlobalElementId>,
595 bounds: Bounds<Pixels>,
596 _: &mut Self::RequestLayoutState,
597 hitbox: &mut Hitbox,
598 cx: &mut WindowContext,
599 ) {
600 let text_layout = self.text.layout().clone();
601 cx.with_element_state::<InteractiveTextState, _>(
602 global_id.unwrap(),
603 |interactive_state, cx| {
604 let mut interactive_state = interactive_state.unwrap_or_default();
605 if let Some(click_listener) = self.click_listener.take() {
606 let mouse_position = cx.mouse_position();
607 if let Ok(ix) = text_layout.index_for_position(mouse_position) {
608 if self
609 .clickable_ranges
610 .iter()
611 .any(|range| range.contains(&ix))
612 {
613 cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
614 }
615 }
616
617 let text_layout = text_layout.clone();
618 let mouse_down = interactive_state.mouse_down_index.clone();
619 if let Some(mouse_down_index) = mouse_down.get() {
620 let hitbox = hitbox.clone();
621 let clickable_ranges = mem::take(&mut self.clickable_ranges);
622 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
623 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
624 if let Ok(mouse_up_index) =
625 text_layout.index_for_position(event.position)
626 {
627 click_listener(
628 &clickable_ranges,
629 InteractiveTextClickEvent {
630 mouse_down_index,
631 mouse_up_index,
632 },
633 cx,
634 )
635 }
636
637 mouse_down.take();
638 cx.refresh();
639 }
640 });
641 } else {
642 let hitbox = hitbox.clone();
643 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
644 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
645 if let Ok(mouse_down_index) =
646 text_layout.index_for_position(event.position)
647 {
648 mouse_down.set(Some(mouse_down_index));
649 cx.refresh();
650 }
651 }
652 });
653 }
654 }
655
656 cx.on_mouse_event({
657 let mut hover_listener = self.hover_listener.take();
658 let hitbox = hitbox.clone();
659 let text_layout = text_layout.clone();
660 let hovered_index = interactive_state.hovered_index.clone();
661 move |event: &MouseMoveEvent, phase, cx| {
662 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
663 let current = hovered_index.get();
664 let updated = text_layout.index_for_position(event.position).ok();
665 if current != updated {
666 hovered_index.set(updated);
667 if let Some(hover_listener) = hover_listener.as_ref() {
668 hover_listener(updated, event.clone(), cx);
669 }
670 cx.refresh();
671 }
672 }
673 }
674 });
675
676 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
677 let hitbox = hitbox.clone();
678 let active_tooltip = interactive_state.active_tooltip.clone();
679 let pending_mouse_down = interactive_state.mouse_down_index.clone();
680 let text_layout = text_layout.clone();
681
682 cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
683 let position = text_layout.index_for_position(event.position).ok();
684 let is_hovered = position.is_some()
685 && hitbox.is_hovered(cx)
686 && pending_mouse_down.get().is_none();
687 if !is_hovered {
688 active_tooltip.take();
689 return;
690 }
691 let position = position.unwrap();
692
693 if phase != DispatchPhase::Bubble {
694 return;
695 }
696
697 if active_tooltip.borrow().is_none() {
698 let task = cx.spawn({
699 let active_tooltip = active_tooltip.clone();
700 let tooltip_builder = tooltip_builder.clone();
701
702 move |mut cx| async move {
703 cx.background_executor().timer(TOOLTIP_DELAY).await;
704 cx.update(|cx| {
705 let new_tooltip =
706 tooltip_builder(position, cx).map(|tooltip| {
707 ActiveTooltip {
708 tooltip: Some(AnyTooltip {
709 view: tooltip,
710 mouse_position: cx.mouse_position(),
711 }),
712 _task: None,
713 }
714 });
715 *active_tooltip.borrow_mut() = new_tooltip;
716 cx.refresh();
717 })
718 .ok();
719 }
720 });
721 *active_tooltip.borrow_mut() = Some(ActiveTooltip {
722 tooltip: None,
723 _task: Some(task),
724 });
725 }
726 });
727
728 let active_tooltip = interactive_state.active_tooltip.clone();
729 cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
730 active_tooltip.take();
731 });
732 }
733
734 self.text.paint(None, bounds, &mut (), &mut (), cx);
735
736 ((), interactive_state)
737 },
738 );
739 }
740}
741
742impl IntoElement for InteractiveText {
743 type Element = Self;
744
745 fn into_element(self) -> Self::Element {
746 self
747 }
748}