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