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