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