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