1use crate::{
2 ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
3 HighlightStyle, Hitbox, HitboxBehavior, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent,
4 MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId,
5 TruncateFrom, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
6 register_tooltip_mouse_handlers, set_tooltip_on_window,
7};
8use anyhow::Context as _;
9use gpui_util::ResultExt;
10use itertools::Itertools;
11use smallvec::SmallVec;
12use std::{
13 borrow::Cow,
14 cell::{Cell, RefCell},
15 mem,
16 ops::{Deref, DerefMut, Range},
17 rc::Rc,
18 sync::Arc,
19};
20
21#[derive(Debug, Clone)]
67pub struct Text {
68 id: Option<ElementId>,
69 text: SharedString,
70}
71
72impl Text {
73 #[inline]
79 pub const fn new(id: ElementId, text: SharedString) -> Self {
80 Self { id: Some(id), text }
81 }
82
83 #[inline]
93 pub const fn new_inaccessible(text: SharedString) -> Self {
94 Self { id: None, text }
95 }
96
97 #[inline]
99 pub const fn id(&self) -> Option<&ElementId> {
100 self.id.as_ref()
101 }
102
103 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
105 self.id = Some(id.into());
106 self
107 }
108
109 #[inline]
111 pub const fn text(&self) -> &SharedString {
112 &self.text
113 }
114}
115
116impl Deref for Text {
117 type Target = SharedString;
118 fn deref(&self) -> &Self::Target {
119 &self.text
120 }
121}
122
123impl DerefMut for Text {
124 fn deref_mut(&mut self) -> &mut Self::Target {
125 &mut self.text
126 }
127}
128
129#[doc(hidden)]
133pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
134 const BASIS: u64 = 0xcbf29ce484222325;
135 const PRIME: u64 = 0x100000001b3;
136
137 let bytes = s.as_bytes();
138 let mut hash = BASIS;
139 let mut i = 0;
140 while i < bytes.len() {
141 hash ^= bytes[i] as u64;
142 hash = hash.wrapping_mul(PRIME);
143 i += 1;
144 }
145 hash
146}
147
148#[macro_export]
161macro_rules! text {
162 (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }};
163 ($text:expr) => {{
164 const ID: &'static str = concat!(file!(), "/", line!(), ":", column!());
165 const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID);
166 $crate::Text::new($crate::ElementId::Integer(HASH), $text.into())
167 }};
168}
169
170impl IntoElement for Text {
171 type Element = Self;
172 #[inline]
173 fn into_element(self) -> Self::Element {
174 self
175 }
176}
177
178impl Element for Text {
179 type RequestLayoutState = TextLayout;
180 type PrepaintState = ();
181
182 fn id(&self) -> Option<ElementId> {
183 self.id.clone()
184 }
185
186 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
187 None
188 }
189
190 fn a11y_role(&self) -> Option<accesskit::Role> {
191 if self.id.is_some() {
192 Some(accesskit::Role::Label)
193 } else {
194 None
195 }
196 }
197
198 fn write_a11y_info(&self, node: &mut accesskit::Node) {
199 node.set_value(self.text.to_string());
200 }
201
202 fn request_layout(
203 &mut self,
204 id: Option<&GlobalElementId>,
205 window: &mut Window,
206 cx: &mut App,
207 ) -> (LayoutId, Self::RequestLayoutState) {
208 <SharedString as Element>::request_layout(&mut self.text, id, window, cx)
209 }
210
211 fn prepaint(
212 &mut self,
213 id: Option<&GlobalElementId>,
214 bounds: Bounds<Pixels>,
215 request_layout: &mut Self::RequestLayoutState,
216 window: &mut Window,
217 cx: &mut App,
218 ) -> Self::PrepaintState {
219 <SharedString as Element>::prepaint(&mut self.text, id, bounds, request_layout, window, cx)
220 }
221
222 fn paint(
223 &mut self,
224 id: Option<&GlobalElementId>,
225 bounds: Bounds<Pixels>,
226 request_layout: &mut Self::RequestLayoutState,
227 prepaint: &mut Self::PrepaintState,
228 window: &mut Window,
229 cx: &mut App,
230 ) {
231 <SharedString as Element>::paint(
232 &mut self.text,
233 id,
234 bounds,
235 request_layout,
236 prepaint,
237 window,
238 cx,
239 );
240 }
241}
242
243impl Element for &'static str {
244 type RequestLayoutState = TextLayout;
245 type PrepaintState = ();
246
247 fn id(&self) -> Option<ElementId> {
248 None
249 }
250
251 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
252 None
253 }
254
255 fn request_layout(
256 &mut self,
257 _id: Option<&GlobalElementId>,
258 window: &mut Window,
259 cx: &mut App,
260 ) -> (LayoutId, Self::RequestLayoutState) {
261 let mut state = TextLayout::default();
262 let layout_id = state.layout(SharedString::from(*self), None, window, cx);
263 (layout_id, state)
264 }
265
266 fn prepaint(
267 &mut self,
268 _id: Option<&GlobalElementId>,
269 bounds: Bounds<Pixels>,
270 text_layout: &mut Self::RequestLayoutState,
271 _window: &mut Window,
272 _cx: &mut App,
273 ) {
274 text_layout.prepaint(bounds, self)
275 }
276
277 fn paint(
278 &mut self,
279 _id: Option<&GlobalElementId>,
280 _bounds: Bounds<Pixels>,
281 text_layout: &mut TextLayout,
282 _: &mut (),
283 window: &mut Window,
284 cx: &mut App,
285 ) {
286 text_layout.paint(self, window, cx)
287 }
288}
289
290impl IntoElement for &'static str {
291 type Element = Self;
292
293 fn into_element(self) -> Self::Element {
294 self
295 }
296}
297
298impl IntoElement for String {
299 type Element = SharedString;
300
301 fn into_element(self) -> Self::Element {
302 self.into()
303 }
304}
305
306impl IntoElement for Cow<'static, str> {
307 type Element = SharedString;
308
309 fn into_element(self) -> Self::Element {
310 self.into()
311 }
312}
313
314impl Element for SharedString {
315 type RequestLayoutState = TextLayout;
316 type PrepaintState = ();
317
318 fn id(&self) -> Option<ElementId> {
319 None
320 }
321
322 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
323 None
324 }
325
326 fn request_layout(
327 &mut self,
328 _id: Option<&GlobalElementId>,
329 window: &mut Window,
330 cx: &mut App,
331 ) -> (LayoutId, Self::RequestLayoutState) {
332 let mut state = TextLayout::default();
333 let layout_id = state.layout(self.clone(), None, window, cx);
334 (layout_id, state)
335 }
336
337 fn prepaint(
338 &mut self,
339 _id: Option<&GlobalElementId>,
340 bounds: Bounds<Pixels>,
341 text_layout: &mut Self::RequestLayoutState,
342 _window: &mut Window,
343 _cx: &mut App,
344 ) {
345 text_layout.prepaint(bounds, self.as_ref())
346 }
347
348 fn paint(
349 &mut self,
350 _id: Option<&GlobalElementId>,
351 _bounds: Bounds<Pixels>,
352 text_layout: &mut Self::RequestLayoutState,
353 _: &mut Self::PrepaintState,
354 window: &mut Window,
355 cx: &mut App,
356 ) {
357 text_layout.paint(self.as_ref(), window, cx)
358 }
359}
360
361impl IntoElement for SharedString {
362 type Element = Self;
363
364 fn into_element(self) -> Self::Element {
365 self
366 }
367}
368
369pub struct StyledText {
375 text: SharedString,
376 runs: Option<Vec<TextRun>>,
377 delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
378 delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
379 layout: TextLayout,
380}
381
382impl StyledText {
383 pub fn new(text: impl Into<SharedString>) -> Self {
385 StyledText {
386 text: text.into(),
387 runs: None,
388 delayed_highlights: None,
389 delayed_font_family_overrides: None,
390 layout: TextLayout::default(),
391 }
392 }
393
394 pub fn layout(&self) -> &TextLayout {
396 &self.layout
397 }
398
399 pub fn with_default_highlights(
402 mut self,
403 default_style: &TextStyle,
404 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
405 ) -> Self {
406 debug_assert!(
407 self.delayed_highlights.is_none(),
408 "Can't use `with_default_highlights` and `with_highlights`"
409 );
410 let runs = Self::compute_runs(&self.text, default_style, highlights);
411 self.with_runs(runs)
412 }
413
414 pub fn with_highlights(
417 mut self,
418 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
419 ) -> Self {
420 debug_assert!(
421 self.runs.is_none(),
422 "Can't use `with_highlights` and `with_default_highlights`"
423 );
424 self.delayed_highlights = Some(
425 highlights
426 .into_iter()
427 .inspect(|(run, _)| {
428 debug_assert!(self.text.is_char_boundary(run.start));
429 debug_assert!(self.text.is_char_boundary(run.end));
430 })
431 .collect::<Vec<_>>(),
432 );
433 self
434 }
435
436 fn compute_runs(
437 text: &str,
438 default_style: &TextStyle,
439 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
440 ) -> Vec<TextRun> {
441 let mut runs = Vec::new();
442 let mut ix = 0;
443 for (range, highlight) in highlights {
444 if ix < range.start {
445 debug_assert!(text.is_char_boundary(range.start));
446 runs.push(default_style.clone().to_run(range.start - ix));
447 }
448 debug_assert!(text.is_char_boundary(range.end));
449 runs.push(
450 default_style
451 .clone()
452 .highlight(highlight)
453 .to_run(range.len()),
454 );
455 ix = range.end;
456 }
457 if ix < text.len() {
458 runs.push(default_style.to_run(text.len() - ix));
459 }
460 runs
461 }
462
463 pub fn with_font_family_overrides(
472 mut self,
473 overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
474 ) -> Self {
475 self.delayed_font_family_overrides = Some(
476 overrides
477 .into_iter()
478 .inspect(|(range, _)| {
479 debug_assert!(self.text.is_char_boundary(range.start));
480 debug_assert!(self.text.is_char_boundary(range.end));
481 })
482 .collect(),
483 );
484 self
485 }
486
487 fn apply_font_family_overrides(
488 runs: &mut [TextRun],
489 overrides: &[(Range<usize>, SharedString)],
490 ) {
491 let mut byte_offset = 0;
492 let mut override_idx = 0;
493 for run in runs.iter_mut() {
494 let run_end = byte_offset + run.len;
495 while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
496 override_idx += 1;
497 }
498 if override_idx < overrides.len() {
499 let (ref range, ref family) = overrides[override_idx];
500 if byte_offset >= range.start && run_end <= range.end {
501 run.font.family = family.clone();
502 }
503 }
504 byte_offset = run_end;
505 }
506 }
507
508 pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
510 let mut text = &*self.text;
511 for run in &runs {
512 text = text.get(run.len..).unwrap_or_else(|| {
513 #[cfg(debug_assertions)]
514 panic!("invalid text run. Text: '{text}', run: {run:?}");
515 #[cfg(not(debug_assertions))]
516 panic!("invalid text run");
517 });
518 }
519 assert!(text.is_empty(), "invalid text run");
520 self.runs = Some(runs);
521 self
522 }
523}
524
525impl Element for StyledText {
526 type RequestLayoutState = ();
527 type PrepaintState = ();
528
529 fn id(&self) -> Option<ElementId> {
530 None
531 }
532
533 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
534 None
535 }
536
537 fn request_layout(
538 &mut self,
539 _id: Option<&GlobalElementId>,
540 window: &mut Window,
541 cx: &mut App,
542 ) -> (LayoutId, Self::RequestLayoutState) {
543 let font_family_overrides = self.delayed_font_family_overrides.take();
544 let mut runs = self.runs.take().or_else(|| {
545 self.delayed_highlights.take().map(|delayed_highlights| {
546 Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
547 })
548 });
549
550 if let Some(ref overrides) = font_family_overrides {
551 let runs =
552 runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
553 Self::apply_font_family_overrides(runs, overrides);
554 }
555
556 let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
557 (layout_id, ())
558 }
559
560 fn prepaint(
561 &mut self,
562 _id: Option<&GlobalElementId>,
563 bounds: Bounds<Pixels>,
564 _: &mut Self::RequestLayoutState,
565 _window: &mut Window,
566 _cx: &mut App,
567 ) {
568 self.layout.prepaint(bounds, &self.text)
569 }
570
571 fn paint(
572 &mut self,
573 _id: Option<&GlobalElementId>,
574 _bounds: Bounds<Pixels>,
575 _: &mut Self::RequestLayoutState,
576 _: &mut Self::PrepaintState,
577 window: &mut Window,
578 cx: &mut App,
579 ) {
580 self.layout.paint(&self.text, window, cx)
581 }
582}
583
584impl IntoElement for StyledText {
585 type Element = Self;
586
587 fn into_element(self) -> Self::Element {
588 self
589 }
590}
591
592#[derive(Default, Clone)]
594pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
595
596struct TextLayoutInner {
597 len: usize,
598 lines: SmallVec<[WrappedLine; 1]>,
599 line_height: Pixels,
600 wrap_width: Option<Pixels>,
601 truncate_width: Option<Pixels>,
602 size: Option<Size<Pixels>>,
603 bounds: Option<Bounds<Pixels>>,
604}
605
606impl TextLayout {
607 fn layout(
608 &self,
609 text: SharedString,
610 runs: Option<Vec<TextRun>>,
611 window: &mut Window,
612 _: &mut App,
613 ) -> LayoutId {
614 let text_style = window.text_style();
615 let font_size = text_style.font_size.to_pixels(window.rem_size());
616 let line_height = window.pixel_snap(
617 text_style
618 .line_height
619 .to_pixels(font_size.into(), window.rem_size()),
620 );
621
622 let runs = if let Some(runs) = runs {
623 runs
624 } else {
625 vec![text_style.to_run(text.len())]
626 };
627 window.request_measured_layout(Default::default(), {
628 let element_state = self.clone();
629
630 move |known_dimensions, available_space, window, cx| {
631 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
632 known_dimensions.width.or(match available_space.width {
633 crate::AvailableSpace::Definite(x) => Some(x),
634 _ => None,
635 })
636 } else {
637 None
638 };
639
640 let (truncate_width, truncation_affix, truncate_from) =
641 if let Some(text_overflow) = text_style.text_overflow.clone() {
642 let width = known_dimensions.width.or(match available_space.width {
643 crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
644 Some(max_lines) => Some(x * max_lines),
645 None => Some(x),
646 },
647 _ => None,
648 });
649
650 match text_overflow {
651 TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
652 TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
653 TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle),
654 }
655 } else {
656 (None, "".into(), TruncateFrom::End)
657 };
658
659 if let Some(text_layout) = element_state.0.borrow().as_ref()
668 && let Some(size) = text_layout.size
669 && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
670 && truncate_width.is_none()
671 && text_layout.truncate_width.is_none()
672 {
673 return size;
674 }
675
676 let mut line_wrapper = cx
677 .text_system()
678 .clone()
679 .line_wrapper(text_style.font(), font_size);
680 let (text, runs) = if let Some(truncate_width) = truncate_width {
681 if let Some(max_lines) = text_style.line_clamp
682 && let Some(wrap_width) = wrap_width
683 {
684 line_wrapper.truncate_wrapped_line(
685 text.clone(),
686 wrap_width,
687 max_lines,
688 &truncation_affix,
689 &runs,
690 truncate_from,
691 )
692 } else if let Some(unclipped) = window
693 .text_system()
694 .shape_text(text.clone(), font_size, &runs, None, None)
695 .log_err()
696 && unclipped
697 .iter()
698 .all(|line| line.size(line_height).width <= truncate_width)
699 {
700 (text.clone(), Cow::Borrowed(&*runs))
707 } else {
708 line_wrapper.truncate_line(
709 text.clone(),
710 truncate_width,
711 &truncation_affix,
712 &runs,
713 truncate_from,
714 )
715 }
716 } else {
717 (text.clone(), Cow::Borrowed(&*runs))
718 };
719 let len = text.len();
720
721 let Some(lines) = window
722 .text_system()
723 .shape_text(
724 text,
725 font_size,
726 &runs,
727 wrap_width, text_style.line_clamp, )
730 .log_err()
731 else {
732 element_state.0.borrow_mut().replace(TextLayoutInner {
733 lines: Default::default(),
734 len: 0,
735 line_height,
736 wrap_width,
737 truncate_width,
738 size: Some(Size::default()),
739 bounds: None,
740 });
741 return Size::default();
742 };
743
744 let mut size: Size<Pixels> = Size::default();
745 for line in &lines {
746 let line_size = line.size(line_height);
747 size.height += line_size.height;
748 size.width = size.width.max(line_size.width).ceil();
749 }
750
751 element_state.0.borrow_mut().replace(TextLayoutInner {
752 lines,
753 len,
754 line_height,
755 wrap_width,
756 truncate_width,
757 size: Some(size),
758 bounds: None,
759 });
760
761 size
762 }
763 })
764 }
765
766 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
767 let mut element_state = self.0.borrow_mut();
768 let element_state = element_state
769 .as_mut()
770 .with_context(|| format!("measurement has not been performed on {text}"))
771 .unwrap();
772 element_state.bounds = Some(bounds);
773 }
774
775 fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
776 let element_state = self.0.borrow();
777 let element_state = element_state
778 .as_ref()
779 .with_context(|| format!("measurement has not been performed on {text}"))
780 .unwrap();
781 let bounds = element_state
782 .bounds
783 .with_context(|| format!("prepaint has not been performed on {text}"))
784 .unwrap();
785
786 let line_height = element_state.line_height;
787 let mut line_origin = bounds.origin;
788 let text_style = window.text_style();
789 for line in &element_state.lines {
790 line.paint_background(
791 line_origin,
792 line_height,
793 text_style.text_align,
794 Some(bounds),
795 window,
796 cx,
797 )
798 .log_err();
799 line.paint(
800 line_origin,
801 line_height,
802 text_style.text_align,
803 Some(bounds),
804 window,
805 cx,
806 )
807 .log_err();
808 line_origin.y += line.size(line_height).height;
809 }
810 }
811
812 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
814 let element_state = self.0.borrow();
815 let element_state = element_state
816 .as_ref()
817 .expect("measurement has not been performed");
818 let bounds = element_state
819 .bounds
820 .expect("prepaint has not been performed");
821
822 if position.y < bounds.top() {
823 return Err(0);
824 }
825
826 let line_height = element_state.line_height;
827 let mut line_origin = bounds.origin;
828 let mut line_start_ix = 0;
829 for line in &element_state.lines {
830 let line_bottom = line_origin.y + line.size(line_height).height;
831 if position.y > line_bottom {
832 line_origin.y = line_bottom;
833 line_start_ix += line.len() + 1;
834 } else {
835 let position_within_line = position - line_origin;
836 match line.index_for_position(position_within_line, line_height) {
837 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
838 Err(index_within_line) => return Err(line_start_ix + index_within_line),
839 }
840 }
841 }
842
843 Err(line_start_ix.saturating_sub(1))
844 }
845
846 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
848 let element_state = self.0.borrow();
849 let element_state = element_state
850 .as_ref()
851 .expect("measurement has not been performed");
852 let bounds = element_state
853 .bounds
854 .expect("prepaint has not been performed");
855 let line_height = element_state.line_height;
856
857 let mut line_origin = bounds.origin;
858 let mut line_start_ix = 0;
859
860 for line in &element_state.lines {
861 let line_end_ix = line_start_ix + line.len();
862 if index < line_start_ix {
863 break;
864 } else if index > line_end_ix {
865 line_origin.y += line.size(line_height).height;
866 line_start_ix = line_end_ix + 1;
867 continue;
868 } else {
869 let ix_within_line = index - line_start_ix;
870 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
871 }
872 }
873
874 None
875 }
876
877 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
879 let element_state = self.0.borrow();
880 let element_state = element_state
881 .as_ref()
882 .expect("measurement has not been performed");
883 let mut line_start_ix = 0;
884
885 for line in &element_state.lines {
886 let line_end_ix = line_start_ix + line.len();
887 if index < line_start_ix {
888 break;
889 } else if index > line_end_ix {
890 line_start_ix = line_end_ix + 1;
891 continue;
892 } else {
893 return Some(line.layout.clone());
894 }
895 }
896
897 None
898 }
899
900 pub fn line_layouts(&self) -> SmallVec<[Arc<WrappedLineLayout>; 1]> {
902 self.0
903 .borrow()
904 .as_ref()
905 .expect("measurement has not been performed")
906 .lines
907 .iter()
908 .map(|line| line.layout.clone())
909 .collect()
910 }
911
912 pub fn bounds(&self) -> Bounds<Pixels> {
914 self.0.borrow().as_ref().unwrap().bounds.unwrap()
915 }
916
917 pub fn line_height(&self) -> Pixels {
919 self.0.borrow().as_ref().unwrap().line_height
920 }
921
922 pub fn len(&self) -> usize {
924 self.0.borrow().as_ref().unwrap().len
925 }
926
927 pub fn text(&self) -> String {
929 self.0
930 .borrow()
931 .as_ref()
932 .unwrap()
933 .lines
934 .iter()
935 .map(|s| &s.text)
936 .join("\n")
937 }
938
939 pub fn wrapped_text(&self) -> String {
941 let mut accumulator = String::new();
942
943 for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
944 let mut seen = 0;
945 for boundary in wrapped.layout.wrap_boundaries.iter() {
946 let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
947 [boundary.glyph_ix]
948 .index;
949
950 accumulator.push_str(&wrapped.text[seen..index]);
951 accumulator.push('\n');
952 seen = index;
953 }
954 accumulator.push_str(&wrapped.text[seen..]);
955 accumulator.push('\n');
956 }
957 accumulator.pop();
959 accumulator
960 }
961}
962
963pub struct InteractiveText {
965 element_id: ElementId,
966 text: StyledText,
967 click_listener:
968 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
969 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
970 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
971 tooltip_id: Option<TooltipId>,
972 clickable_ranges: Vec<Range<usize>>,
973}
974
975struct InteractiveTextClickEvent {
976 mouse_down_index: usize,
977 mouse_up_index: usize,
978}
979
980#[doc(hidden)]
981#[derive(Default)]
982pub struct InteractiveTextState {
983 mouse_down_index: Rc<Cell<Option<usize>>>,
984 hovered_index: Rc<Cell<Option<usize>>>,
985 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
986 long_press_tooltip_active: Rc<Cell<bool>>,
987}
988
989impl InteractiveText {
991 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
993 Self {
994 element_id: id.into(),
995 text,
996 click_listener: None,
997 hover_listener: None,
998 tooltip_builder: None,
999 tooltip_id: None,
1000 clickable_ranges: Vec::new(),
1001 }
1002 }
1003
1004 pub fn on_click(
1007 mut self,
1008 ranges: Vec<Range<usize>>,
1009 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
1010 ) -> Self {
1011 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
1012 for (range_ix, range) in ranges.iter().enumerate() {
1013 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
1014 {
1015 listener(range_ix, window, cx);
1016 }
1017 }
1018 }));
1019 self.clickable_ranges = ranges;
1020 self
1021 }
1022
1023 pub fn on_hover(
1026 mut self,
1027 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
1028 ) -> Self {
1029 self.hover_listener = Some(Box::new(listener));
1030 self
1031 }
1032
1033 pub fn tooltip(
1035 mut self,
1036 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
1037 ) -> Self {
1038 self.tooltip_builder = Some(Rc::new(builder));
1039 self
1040 }
1041}
1042
1043impl Element for InteractiveText {
1044 type RequestLayoutState = ();
1045 type PrepaintState = Hitbox;
1046
1047 fn id(&self) -> Option<ElementId> {
1048 Some(self.element_id.clone())
1049 }
1050
1051 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1052 None
1053 }
1054
1055 fn a11y_role(&self) -> Option<accesskit::Role> {
1056 Some(accesskit::Role::Label)
1057 }
1058
1059 fn write_a11y_info(&self, node: &mut accesskit::Node) {
1060 node.set_value(self.text.text.to_string());
1061 }
1062
1063 fn request_layout(
1064 &mut self,
1065 _id: Option<&GlobalElementId>,
1066 window: &mut Window,
1067 cx: &mut App,
1068 ) -> (LayoutId, Self::RequestLayoutState) {
1069 self.text.request_layout(None, window, cx)
1070 }
1071
1072 fn prepaint(
1073 &mut self,
1074 global_id: Option<&GlobalElementId>,
1075 bounds: Bounds<Pixels>,
1076 state: &mut Self::RequestLayoutState,
1077 window: &mut Window,
1078 cx: &mut App,
1079 ) -> Hitbox {
1080 window.with_optional_element_state::<InteractiveTextState, _>(
1081 global_id,
1082 |interactive_state, window| {
1083 let mut interactive_state = interactive_state
1084 .map(|interactive_state| interactive_state.unwrap_or_default());
1085
1086 if let Some(interactive_state) = interactive_state.as_mut() {
1087 if self.tooltip_builder.is_some() {
1088 self.tooltip_id =
1089 set_tooltip_on_window(&interactive_state.active_tooltip, window);
1090 } else {
1091 interactive_state.long_press_tooltip_active.set(false);
1093 interactive_state.active_tooltip.take();
1094 }
1095 }
1096
1097 self.text.prepaint(None, bounds, state, window, cx);
1098 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1099 (hitbox, interactive_state)
1100 },
1101 )
1102 }
1103
1104 fn paint(
1105 &mut self,
1106 global_id: Option<&GlobalElementId>,
1107 bounds: Bounds<Pixels>,
1108 _: &mut Self::RequestLayoutState,
1109 hitbox: &mut Hitbox,
1110 window: &mut Window,
1111 cx: &mut App,
1112 ) {
1113 let current_view = window.current_view();
1114 let text_layout = self.text.layout().clone();
1115 window.with_element_state::<InteractiveTextState, _>(
1116 global_id.unwrap(),
1117 |interactive_state, window| {
1118 let mut interactive_state = interactive_state.unwrap_or_default();
1119 if let Some(click_listener) = self.click_listener.take() {
1120 let mouse_position = window.mouse_position();
1121 if let Ok(ix) = text_layout.index_for_position(mouse_position)
1122 && self
1123 .clickable_ranges
1124 .iter()
1125 .any(|range| range.contains(&ix))
1126 {
1127 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
1128 }
1129
1130 let text_layout = text_layout.clone();
1131 let mouse_down = interactive_state.mouse_down_index.clone();
1132 if let Some(mouse_down_index) = mouse_down.get() {
1133 let hitbox = hitbox.clone();
1134 let clickable_ranges = mem::take(&mut self.clickable_ranges);
1135 window.on_mouse_event(
1136 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
1137 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1138 if let Ok(mouse_up_index) =
1139 text_layout.index_for_position(event.position)
1140 {
1141 click_listener(
1142 &clickable_ranges,
1143 InteractiveTextClickEvent {
1144 mouse_down_index,
1145 mouse_up_index,
1146 },
1147 window,
1148 cx,
1149 )
1150 }
1151
1152 mouse_down.take();
1153 window.refresh();
1154 }
1155 },
1156 );
1157 } else {
1158 let hitbox = hitbox.clone();
1159 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
1160 if phase == DispatchPhase::Bubble
1161 && hitbox.is_hovered(window)
1162 && let Ok(mouse_down_index) =
1163 text_layout.index_for_position(event.position)
1164 {
1165 mouse_down.set(Some(mouse_down_index));
1166 window.refresh();
1167 }
1168 });
1169 }
1170 }
1171
1172 window.on_mouse_event({
1173 let mut hover_listener = self.hover_listener.take();
1174 let hitbox = hitbox.clone();
1175 let text_layout = text_layout.clone();
1176 let hovered_index = interactive_state.hovered_index.clone();
1177 move |event: &MouseMoveEvent, phase, window, cx| {
1178 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1179 let current = hovered_index.get();
1180 let updated = text_layout.index_for_position(event.position).ok();
1181 if current != updated {
1182 hovered_index.set(updated);
1183 if let Some(hover_listener) = hover_listener.as_ref() {
1184 hover_listener(updated, event.clone(), window, cx);
1185 }
1186 cx.notify(current_view);
1187 }
1188 }
1189 }
1190 });
1191
1192 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
1193 let active_tooltip = interactive_state.active_tooltip.clone();
1194 let build_tooltip = Rc::new({
1195 let tooltip_is_hoverable = false;
1196 let text_layout = text_layout.clone();
1197 move |window: &mut Window, cx: &mut App| {
1198 text_layout
1199 .index_for_position(window.mouse_position())
1200 .ok()
1201 .and_then(|position| tooltip_builder(position, window, cx))
1202 .map(|view| (view, tooltip_is_hoverable))
1203 }
1204 });
1205
1206 let check_is_hovered_during_prepaint = Rc::new({
1208 let source_bounds = hitbox.bounds;
1209 let text_layout = text_layout.clone();
1210 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1211 move |window: &Window| {
1212 text_layout
1213 .index_for_position(window.mouse_position())
1214 .is_ok()
1215 && source_bounds.contains(&window.mouse_position())
1216 && pending_mouse_down.get().is_none()
1217 }
1218 });
1219
1220 let check_is_hovered = Rc::new({
1221 let hitbox = hitbox.clone();
1222 let text_layout = text_layout.clone();
1223 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1224 move |window: &Window| {
1225 text_layout
1226 .index_for_position(window.mouse_position())
1227 .is_ok()
1228 && hitbox.is_hovered(window)
1229 && pending_mouse_down.get().is_none()
1230 }
1231 });
1232
1233 register_tooltip_mouse_handlers(
1234 &active_tooltip,
1235 self.tooltip_id,
1236 build_tooltip,
1237 check_is_hovered,
1238 check_is_hovered_during_prepaint,
1239 interactive_state.long_press_tooltip_active.clone(),
1240 None,
1241 window,
1242 );
1243 }
1244
1245 self.text.paint(None, bounds, &mut (), &mut (), window, cx);
1246
1247 ((), interactive_state)
1248 },
1249 );
1250 }
1251}
1252
1253impl IntoElement for InteractiveText {
1254 type Element = Self;
1255
1256 fn into_element(self) -> Self::Element {
1257 self
1258 }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263 use super::*;
1264
1265 #[test]
1266 fn test_into_element_for() {
1267 use crate::{ParentElement as _, SharedString, div};
1268 use std::borrow::Cow;
1269
1270 let _ = div().child("static str");
1271 let _ = div().child("String".to_string());
1272 let _ = div().child(Cow::Borrowed("Cow"));
1273 let _ = div().child(SharedString::from("SharedString"));
1274 }
1275
1276 #[test]
1277 fn text_macro_id() {
1278 fn make_text_stable_id(happy: bool) -> Text {
1280 text!(if happy { "happy" } else { "sad" })
1281 }
1282
1283 fn make_text_unstable_id(happy: bool) -> Text {
1285 if happy { text!("happy") } else { text!("sad") }
1286 }
1287
1288 assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
1289 assert_ne!(
1290 make_text_unstable_id(false).id,
1291 make_text_unstable_id(true).id
1292 );
1293 }
1294}