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, TextTransform, 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};
20use unicode_segmentation::UnicodeSegmentation;
21
22#[derive(Debug, Clone)]
68pub struct Text {
69 id: Option<ElementId>,
70 text: SharedString,
71}
72
73impl Text {
74 #[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 inspector_id: Option<&InspectorElementId>,
206 window: &mut Window,
207 cx: &mut App,
208 ) -> (LayoutId, Self::RequestLayoutState) {
209 <SharedString as Element>::request_layout(&mut self.text, id, inspector_id, window, cx)
210 }
211
212 fn prepaint(
213 &mut self,
214 id: Option<&GlobalElementId>,
215 inspector_id: Option<&InspectorElementId>,
216 bounds: Bounds<Pixels>,
217 request_layout: &mut Self::RequestLayoutState,
218 window: &mut Window,
219 cx: &mut App,
220 ) -> Self::PrepaintState {
221 <SharedString as Element>::prepaint(
222 &mut self.text,
223 id,
224 inspector_id,
225 bounds,
226 request_layout,
227 window,
228 cx,
229 )
230 }
231
232 fn paint(
233 &mut self,
234 id: Option<&GlobalElementId>,
235 inspector_id: Option<&InspectorElementId>,
236 bounds: Bounds<Pixels>,
237 request_layout: &mut Self::RequestLayoutState,
238 prepaint: &mut Self::PrepaintState,
239 window: &mut Window,
240 cx: &mut App,
241 ) {
242 <SharedString as Element>::paint(
243 &mut self.text,
244 id,
245 inspector_id,
246 bounds,
247 request_layout,
248 prepaint,
249 window,
250 cx,
251 );
252 }
253}
254
255impl Element for &'static str {
256 type RequestLayoutState = TextLayout;
257 type PrepaintState = ();
258
259 fn id(&self) -> Option<ElementId> {
260 None
261 }
262
263 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
264 None
265 }
266
267 fn request_layout(
268 &mut self,
269 _id: Option<&GlobalElementId>,
270 _inspector_id: Option<&InspectorElementId>,
271 window: &mut Window,
272 cx: &mut App,
273 ) -> (LayoutId, Self::RequestLayoutState) {
274 let mut state = TextLayout::default();
275 let layout_id = state.layout(SharedString::from(*self), None, window, cx);
276 (layout_id, state)
277 }
278
279 fn prepaint(
280 &mut self,
281 _id: Option<&GlobalElementId>,
282 _inspector_id: Option<&InspectorElementId>,
283 bounds: Bounds<Pixels>,
284 text_layout: &mut Self::RequestLayoutState,
285 _window: &mut Window,
286 _cx: &mut App,
287 ) {
288 text_layout.prepaint(bounds, self)
289 }
290
291 fn paint(
292 &mut self,
293 _id: Option<&GlobalElementId>,
294 _inspector_id: Option<&InspectorElementId>,
295 _bounds: Bounds<Pixels>,
296 text_layout: &mut TextLayout,
297 _: &mut (),
298 window: &mut Window,
299 cx: &mut App,
300 ) {
301 text_layout.paint(self, window, cx)
302 }
303}
304
305impl IntoElement for &'static str {
306 type Element = Self;
307
308 fn into_element(self) -> Self::Element {
309 self
310 }
311}
312
313impl IntoElement for String {
314 type Element = SharedString;
315
316 fn into_element(self) -> Self::Element {
317 self.into()
318 }
319}
320
321impl IntoElement for Cow<'static, str> {
322 type Element = SharedString;
323
324 fn into_element(self) -> Self::Element {
325 self.into()
326 }
327}
328
329impl Element for SharedString {
330 type RequestLayoutState = TextLayout;
331 type PrepaintState = ();
332
333 fn id(&self) -> Option<ElementId> {
334 None
335 }
336
337 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
338 None
339 }
340
341 fn request_layout(
342 &mut self,
343 _id: Option<&GlobalElementId>,
344 _inspector_id: Option<&InspectorElementId>,
345 window: &mut Window,
346 cx: &mut App,
347 ) -> (LayoutId, Self::RequestLayoutState) {
348 let mut state = TextLayout::default();
349 let layout_id = state.layout(self.clone(), None, window, cx);
350 (layout_id, state)
351 }
352
353 fn prepaint(
354 &mut self,
355 _id: Option<&GlobalElementId>,
356 _inspector_id: Option<&InspectorElementId>,
357 bounds: Bounds<Pixels>,
358 text_layout: &mut Self::RequestLayoutState,
359 _window: &mut Window,
360 _cx: &mut App,
361 ) {
362 text_layout.prepaint(bounds, self.as_ref())
363 }
364
365 fn paint(
366 &mut self,
367 _id: Option<&GlobalElementId>,
368 _inspector_id: Option<&InspectorElementId>,
369 _bounds: Bounds<Pixels>,
370 text_layout: &mut Self::RequestLayoutState,
371 _: &mut Self::PrepaintState,
372 window: &mut Window,
373 cx: &mut App,
374 ) {
375 text_layout.paint(self.as_ref(), window, cx)
376 }
377}
378
379impl IntoElement for SharedString {
380 type Element = Self;
381
382 fn into_element(self) -> Self::Element {
383 self
384 }
385}
386
387pub struct StyledText {
393 text: SharedString,
394 runs: Option<Vec<TextRun>>,
395 delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
396 delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
397 layout: TextLayout,
398}
399
400impl StyledText {
401 pub fn new(text: impl Into<SharedString>) -> Self {
403 StyledText {
404 text: text.into(),
405 runs: None,
406 delayed_highlights: None,
407 delayed_font_family_overrides: None,
408 layout: TextLayout::default(),
409 }
410 }
411
412 pub fn layout(&self) -> &TextLayout {
414 &self.layout
415 }
416
417 pub fn with_default_highlights(
420 mut self,
421 default_style: &TextStyle,
422 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
423 ) -> Self {
424 debug_assert!(
425 self.delayed_highlights.is_none(),
426 "Can't use `with_default_highlights` and `with_highlights`"
427 );
428 let runs = Self::compute_runs(&self.text, default_style, highlights);
429 self.with_runs(runs)
430 }
431
432 pub fn with_highlights(
435 mut self,
436 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
437 ) -> Self {
438 debug_assert!(
439 self.runs.is_none(),
440 "Can't use `with_highlights` and `with_default_highlights`"
441 );
442 self.delayed_highlights = Some(
443 highlights
444 .into_iter()
445 .inspect(|(run, _)| {
446 debug_assert!(self.text.is_char_boundary(run.start));
447 debug_assert!(self.text.is_char_boundary(run.end));
448 })
449 .collect::<Vec<_>>(),
450 );
451 self
452 }
453
454 fn compute_runs(
455 text: &str,
456 default_style: &TextStyle,
457 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
458 ) -> Vec<TextRun> {
459 let mut runs = Vec::new();
460 let mut ix = 0;
461 for (range, highlight) in highlights {
462 if ix < range.start {
463 debug_assert!(text.is_char_boundary(range.start));
464 runs.push(default_style.clone().to_run(range.start - ix));
465 }
466 debug_assert!(text.is_char_boundary(range.end));
467 runs.push(
468 default_style
469 .clone()
470 .highlight(highlight)
471 .to_run(range.len()),
472 );
473 ix = range.end;
474 }
475 if ix < text.len() {
476 runs.push(default_style.to_run(text.len() - ix));
477 }
478 runs
479 }
480
481 pub fn with_font_family_overrides(
490 mut self,
491 overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
492 ) -> Self {
493 self.delayed_font_family_overrides = Some(
494 overrides
495 .into_iter()
496 .inspect(|(range, _)| {
497 debug_assert!(self.text.is_char_boundary(range.start));
498 debug_assert!(self.text.is_char_boundary(range.end));
499 })
500 .collect(),
501 );
502 self
503 }
504
505 fn apply_font_family_overrides(
506 runs: &mut [TextRun],
507 overrides: &[(Range<usize>, SharedString)],
508 ) {
509 let mut byte_offset = 0;
510 let mut override_idx = 0;
511 for run in runs.iter_mut() {
512 let run_end = byte_offset + run.len;
513 while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
514 override_idx += 1;
515 }
516 if override_idx < overrides.len() {
517 let (ref range, ref family) = overrides[override_idx];
518 if byte_offset >= range.start && run_end <= range.end {
519 run.font.family = family.clone();
520 }
521 }
522 byte_offset = run_end;
523 }
524 }
525
526 pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
528 let mut text = &*self.text;
529 for run in &runs {
530 text = text.get(run.len..).unwrap_or_else(|| {
531 #[cfg(debug_assertions)]
532 panic!("invalid text run. Text: '{text}', run: {run:?}");
533 #[cfg(not(debug_assertions))]
534 panic!("invalid text run");
535 });
536 }
537 assert!(text.is_empty(), "invalid text run");
538 self.runs = Some(runs);
539 self
540 }
541}
542
543impl Element for StyledText {
544 type RequestLayoutState = ();
545 type PrepaintState = ();
546
547 fn id(&self) -> Option<ElementId> {
548 None
549 }
550
551 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
552 None
553 }
554
555 fn request_layout(
556 &mut self,
557 _id: Option<&GlobalElementId>,
558 _inspector_id: Option<&InspectorElementId>,
559 window: &mut Window,
560 cx: &mut App,
561 ) -> (LayoutId, Self::RequestLayoutState) {
562 let font_family_overrides = self.delayed_font_family_overrides.take();
563 let mut runs = self.runs.take().or_else(|| {
564 self.delayed_highlights.take().map(|delayed_highlights| {
565 Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
566 })
567 });
568
569 if let Some(ref overrides) = font_family_overrides {
570 let runs =
571 runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
572 Self::apply_font_family_overrides(runs, overrides);
573 }
574
575 let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
576 (layout_id, ())
577 }
578
579 fn prepaint(
580 &mut self,
581 _id: Option<&GlobalElementId>,
582 _inspector_id: Option<&InspectorElementId>,
583 bounds: Bounds<Pixels>,
584 _: &mut Self::RequestLayoutState,
585 _window: &mut Window,
586 _cx: &mut App,
587 ) {
588 self.layout.prepaint(bounds, &self.text)
589 }
590
591 fn paint(
592 &mut self,
593 _id: Option<&GlobalElementId>,
594 _inspector_id: Option<&InspectorElementId>,
595 _bounds: Bounds<Pixels>,
596 _: &mut Self::RequestLayoutState,
597 _: &mut Self::PrepaintState,
598 window: &mut Window,
599 cx: &mut App,
600 ) {
601 self.layout.paint(&self.text, window, cx)
602 }
603}
604
605impl IntoElement for StyledText {
606 type Element = Self;
607
608 fn into_element(self) -> Self::Element {
609 self
610 }
611}
612
613#[derive(Default, Clone)]
615pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
616
617struct TextLayoutInner {
618 len: usize,
619 lines: SmallVec<[WrappedLine; 1]>,
620 line_height: Pixels,
621 wrap_width: Option<Pixels>,
622 truncate_width: Option<Pixels>,
623 size: Option<Size<Pixels>>,
624 bounds: Option<Bounds<Pixels>>,
625}
626
627fn apply_text_transform_preserving_byte_len(
628 text: SharedString,
629 transform: Option<TextTransform>,
630) -> SharedString {
631 let Some(transform) = transform else {
632 return text;
633 };
634 if matches!(transform, TextTransform::None) {
635 return text;
636 }
637
638 let mut output = String::with_capacity(text.len());
639 match transform {
640 TextTransform::Uppercase => {
641 for character in text.as_ref().chars() {
642 push_case_mapped_character(&mut output, character, CaseMapKind::Upper);
643 }
644 }
645 TextTransform::Lowercase => {
646 for character in text.as_ref().chars() {
647 push_case_mapped_character(&mut output, character, CaseMapKind::Lower);
648 }
649 }
650 TextTransform::Capitalize => {
651 for piece in text.as_ref().split_word_bounds() {
652 let mut seen_first_letter = false;
653 for character in piece.chars() {
654 if !seen_first_letter && character.is_alphabetic() {
655 push_case_mapped_character(&mut output, character, CaseMapKind::Upper);
656 seen_first_letter = true;
657 } else {
658 output.push(character);
659 }
660 }
661 }
662 }
663 TextTransform::None => return text,
664 }
665
666 SharedString::from(output)
667}
668
669#[derive(Copy, Clone)]
670enum CaseMapKind {
671 Upper,
672 Lower,
673}
674
675fn push_case_mapped_character(output: &mut String, character: char, kind: CaseMapKind) {
676 let mapped = match kind {
677 CaseMapKind::Upper => character.to_uppercase().collect::<String>(),
678 CaseMapKind::Lower => character.to_lowercase().collect::<String>(),
679 };
680
681 if mapped.len() == character.len_utf8() && mapped.chars().count() == 1 {
682 output.push_str(&mapped);
683 } else {
684 output.push(character);
685 }
686}
687
688#[cfg(test)]
689mod text_transform_tests {
690 use super::apply_text_transform_preserving_byte_len;
691 use crate::{SharedString, TextTransform};
692
693 #[test]
694 fn text_transforms_preserve_bytes_and_spacing() {
695 let input = SharedString::from("hello WORLD\tfoo-bar 123baz déjà vu");
696 let uppercase =
697 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase));
698 let lowercase =
699 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase));
700 let capitalize = apply_text_transform_preserving_byte_len(
701 input.clone(),
702 Some(TextTransform::Capitalize),
703 );
704
705 assert_eq!(uppercase.as_ref(), "HELLO WORLD\tFOO-BAR 123BAZ DÉJÀ VU");
706 assert_eq!(lowercase.as_ref(), "hello world\tfoo-bar 123baz déjà vu");
707 assert_eq!(capitalize.as_ref(), "Hello WORLD\tFoo-Bar 123Baz Déjà Vu");
708 assert_eq!(input.len(), uppercase.len());
709 assert_eq!(input.len(), lowercase.len());
710 assert_eq!(input.len(), capitalize.len());
711 }
712
713 #[test]
714 fn text_transforms_skip_expanding_unicode_mappings() {
715 let input = SharedString::from("straße İSTANBUL");
716 let uppercase =
717 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase));
718 let lowercase =
719 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase));
720
721 assert_eq!(uppercase.as_ref(), "STRAßE İSTANBUL");
722 assert_eq!(lowercase.as_ref(), "straße İstanbul");
723 assert_eq!(input.len(), uppercase.len());
724 assert_eq!(input.len(), lowercase.len());
725 }
726
727 #[test]
728 fn capitalize_preserves_letters_after_digit_prefix() {
729 let input = SharedString::from("123BAZ");
730 let output = apply_text_transform_preserving_byte_len(
731 input.clone(),
732 Some(TextTransform::Capitalize),
733 );
734 assert_eq!(output.as_ref(), "123BAZ");
735 assert_eq!(input.len(), output.len());
736 }
737
738 #[test]
739 fn capitalize_does_not_fold_remaining_letters() {
740 let input = SharedString::from("foo2BAR");
741 let output =
742 apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize));
743 assert_eq!(output.as_ref(), "Foo2BAR");
744 }
745
746 #[test]
747 fn capitalize_handles_apostrophe_contractions() {
748 let input = SharedString::from("don't panic");
749 let output =
750 apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize));
751 assert_eq!(output.as_ref(), "Don't Panic");
752 }
753}
754
755pub struct TextLayoutTruncation {
757 pub width: Option<Pixels>,
759 pub affix: SharedString,
761 pub source: TruncateFrom,
763}
764
765impl TextLayoutTruncation {
766 fn overflow_width(text_overflow: TextOverflow, width: Option<Pixels>) -> Self {
768 match text_overflow {
769 TextOverflow::Truncate(s) => TextLayoutTruncation {
770 width,
771 affix: s,
772 source: TruncateFrom::End,
773 },
774 TextOverflow::TruncateStart(s) => TextLayoutTruncation {
775 width,
776 affix: s,
777 source: TruncateFrom::Start,
778 },
779 TextOverflow::TruncateMiddle(s) => TextLayoutTruncation {
780 width,
781 affix: s,
782 source: TruncateFrom::Middle,
783 },
784 }
785 }
786}
787
788impl TextLayout {
789 pub fn evaluate_wrap_width(
791 white_space: &WhiteSpace,
792 known_dimensions: Size<Option<Pixels>>,
793 available_space: Size<crate::AvailableSpace>,
794 ) -> Option<Pixels> {
795 use crate::AvailableSpace::*;
796 match white_space {
797 WhiteSpace::Nowrap => None,
799 WhiteSpace::Normal => known_dimensions.width.or(match available_space.width {
801 Definite(x) => Some(x),
803 MaxContent | MinContent => None,
805 }),
806 }
807 }
808
809 pub fn evaluate_overflow(
811 text_style: &TextStyle,
812 known_dimensions: Size<Option<Pixels>>,
813 available_space: Size<crate::AvailableSpace>,
814 ) -> TextLayoutTruncation {
815 match text_style.text_overflow.clone() {
816 Some(text_overflow) => {
817 let width = known_dimensions.width.or(match available_space.width {
821 crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
822 Some(max_lines) => Some(x * max_lines),
823 None => Some(x),
824 },
825 _ => None,
826 });
827
828 TextLayoutTruncation::overflow_width(text_overflow, width)
829 }
830 None => TextLayoutTruncation {
831 width: None,
832 affix: SharedString::default(),
833 source: TruncateFrom::End,
834 },
835 }
836 }
837
838 pub fn apply_truncation<'runs>(
840 text: SharedString,
841 text_style: &TextStyle,
842 font_size: Pixels,
843 line_height: Pixels,
844 wrap_width: Option<Pixels>,
845 truncation: &TextLayoutTruncation,
846 runs: &'runs [TextRun],
847 window: &mut Window,
848 cx: &mut App,
849 ) -> (SharedString, Cow<'runs, [TextRun]>) {
850 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
851 line_wrapper.set_letter_spacing(text_style.letter_spacing);
852 if let Some(truncate_width) = truncation.width {
853 if let Some(max_lines) = text_style.line_clamp
854 && let Some(wrap_width) = wrap_width
855 {
856 line_wrapper.truncate_wrapped_line(
857 text,
858 wrap_width,
859 max_lines,
860 &truncation.affix,
861 &runs,
862 truncation.source,
863 )
864 } else if let Some(unclipped) = window
865 .text_system()
866 .shape_text(text.clone(), font_size, &runs, None, None)
867 .log_err()
868 && unclipped
869 .iter()
870 .all(|line| line.size(line_height).width <= truncate_width)
871 {
872 (text, std::borrow::Cow::Borrowed(runs))
879 } else {
880 line_wrapper.truncate_line(
881 text,
882 truncate_width,
883 &truncation.affix,
884 &runs,
885 truncation.source,
886 )
887 }
888 } else {
889 (text, std::borrow::Cow::Borrowed(runs))
890 }
891 }
892
893 fn layout(
894 &self,
895 text: SharedString,
896 runs: Option<Vec<TextRun>>,
897 window: &mut Window,
898 _: &mut App,
899 ) -> LayoutId {
900 let text_style = window.text_style();
901 let text = apply_text_transform_preserving_byte_len(text, text_style.text_transform);
902 let font_size = text_style.font_size.to_pixels(window.rem_size());
903 let line_height = window.pixel_snap(
904 text_style
905 .line_height
906 .to_pixels(font_size.into(), window.rem_size()),
907 );
908
909 let runs = if let Some(runs) = runs {
910 runs
911 } else {
912 vec![text_style.to_run(text.len())]
913 };
914 window.request_measured_layout(Default::default(), {
915 let element_state = self.clone();
916
917 move |known_dimensions, available_space, window, cx| {
918 let wrap_width = Self::evaluate_wrap_width(
919 &text_style.white_space,
920 known_dimensions,
921 available_space,
922 );
923
924 let truncation =
925 Self::evaluate_overflow(&text_style, known_dimensions, available_space);
926 let truncate_width = truncation.width;
927
928 if let Some(text_layout) = element_state.0.borrow().as_ref()
937 && let Some(size) = text_layout.size
938 && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
939 && truncate_width.is_none()
940 && text_layout.truncate_width.is_none()
941 {
942 return size;
943 }
944
945 let (text, runs) = Self::apply_truncation(
946 text.clone(),
947 &text_style,
948 font_size,
949 line_height,
950 wrap_width,
951 &truncation,
952 &runs,
953 window,
954 cx,
955 );
956 let len = text.len();
957
958 let Some(lines) = window
959 .text_system()
960 .shape_text(
961 text,
962 font_size,
963 &runs,
964 wrap_width, text_style.line_clamp, )
967 .log_err()
968 else {
969 element_state.0.borrow_mut().replace(TextLayoutInner {
970 lines: Default::default(),
971 len: 0,
972 line_height,
973 wrap_width,
974 truncate_width,
975 size: Some(Size::default()),
976 bounds: None,
977 });
978 return Size::default();
979 };
980
981 let mut size: Size<Pixels> = Size::default();
982 for line in &lines {
983 let line_size = line.size(line_height);
984 size.height += line_size.height;
985 size.width = size.width.max(line_size.width).ceil();
986 }
987
988 element_state.0.borrow_mut().replace(TextLayoutInner {
989 lines,
990 len,
991 line_height,
992 wrap_width,
993 truncate_width,
994 size: Some(size),
995 bounds: None,
996 });
997
998 size
999 }
1000 })
1001 }
1002
1003 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
1004 let mut element_state = self.0.borrow_mut();
1005 let element_state = element_state
1006 .as_mut()
1007 .with_context(|| format!("measurement has not been performed on {text}"))
1008 .unwrap();
1009 element_state.bounds = Some(bounds);
1010 }
1011
1012 fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
1013 let element_state = self.0.borrow();
1014 let element_state = element_state
1015 .as_ref()
1016 .with_context(|| format!("measurement has not been performed on {text}"))
1017 .unwrap();
1018 let bounds = element_state
1019 .bounds
1020 .with_context(|| format!("prepaint has not been performed on {text}"))
1021 .unwrap();
1022
1023 let line_height = element_state.line_height;
1024 let mut line_origin = bounds.origin;
1025 let text_style = window.text_style();
1026 for line in &element_state.lines {
1027 line.paint_background(
1028 line_origin,
1029 line_height,
1030 text_style.text_align,
1031 Some(bounds),
1032 window,
1033 cx,
1034 )
1035 .log_err();
1036 line.paint(
1037 line_origin,
1038 line_height,
1039 text_style.text_align,
1040 Some(bounds),
1041 window,
1042 cx,
1043 )
1044 .log_err();
1045 line_origin.y += line.size(line_height).height;
1046 }
1047 }
1048
1049 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
1051 let element_state = self.0.borrow();
1052 let element_state = element_state
1053 .as_ref()
1054 .expect("measurement has not been performed");
1055 let bounds = element_state
1056 .bounds
1057 .expect("prepaint has not been performed");
1058
1059 if position.y < bounds.top() {
1060 return Err(0);
1061 }
1062
1063 let line_height = element_state.line_height;
1064 let mut line_origin = bounds.origin;
1065 let mut line_start_ix = 0;
1066 for line in &element_state.lines {
1067 let line_bottom = line_origin.y + line.size(line_height).height;
1068 if position.y > line_bottom {
1069 line_origin.y = line_bottom;
1070 line_start_ix += line.len() + 1;
1071 } else {
1072 let position_within_line = position - line_origin;
1073 match line.index_for_position(position_within_line, line_height) {
1074 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
1075 Err(index_within_line) => return Err(line_start_ix + index_within_line),
1076 }
1077 }
1078 }
1079
1080 Err(line_start_ix.saturating_sub(1))
1081 }
1082
1083 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
1085 let element_state = self.0.borrow();
1086 let element_state = element_state
1087 .as_ref()
1088 .expect("measurement has not been performed");
1089 let bounds = element_state
1090 .bounds
1091 .expect("prepaint has not been performed");
1092 let line_height = element_state.line_height;
1093
1094 let mut line_origin = bounds.origin;
1095 let mut line_start_ix = 0;
1096
1097 for line in &element_state.lines {
1098 let line_end_ix = line_start_ix + line.len();
1099 if index < line_start_ix {
1100 break;
1101 } else if index > line_end_ix {
1102 line_origin.y += line.size(line_height).height;
1103 line_start_ix = line_end_ix + 1;
1104 continue;
1105 } else {
1106 let ix_within_line = index - line_start_ix;
1107 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
1108 }
1109 }
1110
1111 None
1112 }
1113
1114 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
1116 let element_state = self.0.borrow();
1117 let element_state = element_state
1118 .as_ref()
1119 .expect("measurement has not been performed");
1120 let mut line_start_ix = 0;
1121
1122 for line in &element_state.lines {
1123 let line_end_ix = line_start_ix + line.len();
1124 if index < line_start_ix {
1125 break;
1126 } else if index > line_end_ix {
1127 line_start_ix = line_end_ix + 1;
1128 continue;
1129 } else {
1130 return Some(line.layout.clone());
1131 }
1132 }
1133
1134 None
1135 }
1136
1137 pub fn line_layouts(&self) -> SmallVec<[Arc<WrappedLineLayout>; 1]> {
1139 self.0
1140 .borrow()
1141 .as_ref()
1142 .expect("measurement has not been performed")
1143 .lines
1144 .iter()
1145 .map(|line| line.layout.clone())
1146 .collect()
1147 }
1148
1149 pub fn bounds(&self) -> Bounds<Pixels> {
1151 self.0.borrow().as_ref().unwrap().bounds.unwrap()
1152 }
1153
1154 pub fn line_height(&self) -> Pixels {
1156 self.0.borrow().as_ref().unwrap().line_height
1157 }
1158
1159 pub fn len(&self) -> usize {
1161 self.0.borrow().as_ref().unwrap().len
1162 }
1163
1164 pub fn text(&self) -> String {
1166 self.0
1167 .borrow()
1168 .as_ref()
1169 .unwrap()
1170 .lines
1171 .iter()
1172 .map(|s| &s.text)
1173 .join("\n")
1174 }
1175
1176 pub fn wrapped_text(&self) -> String {
1178 let mut accumulator = String::new();
1179
1180 for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
1181 let mut seen = 0;
1182 for boundary in wrapped.layout.wrap_boundaries.iter() {
1183 let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
1184 [boundary.glyph_ix]
1185 .index;
1186
1187 accumulator.push_str(&wrapped.text[seen..index]);
1188 accumulator.push('\n');
1189 seen = index;
1190 }
1191 accumulator.push_str(&wrapped.text[seen..]);
1192 accumulator.push('\n');
1193 }
1194 accumulator.pop();
1196 accumulator
1197 }
1198}
1199
1200pub struct InteractiveText {
1202 element_id: ElementId,
1203 text: StyledText,
1204 click_listener:
1205 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
1206 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
1207 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
1208 tooltip_id: Option<TooltipId>,
1209 clickable_ranges: Vec<Range<usize>>,
1210}
1211
1212struct InteractiveTextClickEvent {
1213 mouse_down_index: usize,
1214 mouse_up_index: usize,
1215}
1216
1217#[doc(hidden)]
1218#[derive(Default)]
1219pub struct InteractiveTextState {
1220 mouse_down_index: Rc<Cell<Option<usize>>>,
1221 hovered_index: Rc<Cell<Option<usize>>>,
1222 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
1223}
1224
1225impl InteractiveText {
1227 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
1229 Self {
1230 element_id: id.into(),
1231 text,
1232 click_listener: None,
1233 hover_listener: None,
1234 tooltip_builder: None,
1235 tooltip_id: None,
1236 clickable_ranges: Vec::new(),
1237 }
1238 }
1239
1240 pub fn on_click(
1243 mut self,
1244 ranges: Vec<Range<usize>>,
1245 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
1246 ) -> Self {
1247 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
1248 for (range_ix, range) in ranges.iter().enumerate() {
1249 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
1250 {
1251 listener(range_ix, window, cx);
1252 }
1253 }
1254 }));
1255 self.clickable_ranges = ranges;
1256 self
1257 }
1258
1259 pub fn on_hover(
1262 mut self,
1263 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
1264 ) -> Self {
1265 self.hover_listener = Some(Box::new(listener));
1266 self
1267 }
1268
1269 pub fn tooltip(
1271 mut self,
1272 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
1273 ) -> Self {
1274 self.tooltip_builder = Some(Rc::new(builder));
1275 self
1276 }
1277}
1278
1279impl Element for InteractiveText {
1280 type RequestLayoutState = ();
1281 type PrepaintState = Hitbox;
1282
1283 fn id(&self) -> Option<ElementId> {
1284 Some(self.element_id.clone())
1285 }
1286
1287 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1288 None
1289 }
1290
1291 fn a11y_role(&self) -> Option<accesskit::Role> {
1292 Some(accesskit::Role::Label)
1293 }
1294
1295 fn write_a11y_info(&self, node: &mut accesskit::Node) {
1296 node.set_value(self.text.text.to_string());
1297 }
1298
1299 fn request_layout(
1300 &mut self,
1301 _id: Option<&GlobalElementId>,
1302 inspector_id: Option<&InspectorElementId>,
1303 window: &mut Window,
1304 cx: &mut App,
1305 ) -> (LayoutId, Self::RequestLayoutState) {
1306 self.text.request_layout(None, inspector_id, window, cx)
1307 }
1308
1309 fn prepaint(
1310 &mut self,
1311 global_id: Option<&GlobalElementId>,
1312 inspector_id: Option<&InspectorElementId>,
1313 bounds: Bounds<Pixels>,
1314 state: &mut Self::RequestLayoutState,
1315 window: &mut Window,
1316 cx: &mut App,
1317 ) -> Hitbox {
1318 window.with_optional_element_state::<InteractiveTextState, _>(
1319 global_id,
1320 |interactive_state, window| {
1321 let mut interactive_state = interactive_state
1322 .map(|interactive_state| interactive_state.unwrap_or_default());
1323
1324 if let Some(interactive_state) = interactive_state.as_mut() {
1325 if self.tooltip_builder.is_some() {
1326 self.tooltip_id =
1327 set_tooltip_on_window(&interactive_state.active_tooltip, window);
1328 } else {
1329 interactive_state.active_tooltip.take();
1331 }
1332 }
1333
1334 self.text
1335 .prepaint(None, inspector_id, bounds, state, window, cx);
1336 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1337 (hitbox, interactive_state)
1338 },
1339 )
1340 }
1341
1342 fn paint(
1343 &mut self,
1344 global_id: Option<&GlobalElementId>,
1345 inspector_id: Option<&InspectorElementId>,
1346 bounds: Bounds<Pixels>,
1347 _: &mut Self::RequestLayoutState,
1348 hitbox: &mut Hitbox,
1349 window: &mut Window,
1350 cx: &mut App,
1351 ) {
1352 let current_view = window.current_view();
1353 let text_layout = self.text.layout().clone();
1354 window.with_element_state::<InteractiveTextState, _>(
1355 global_id.unwrap(),
1356 |interactive_state, window| {
1357 let mut interactive_state = interactive_state.unwrap_or_default();
1358 if let Some(click_listener) = self.click_listener.take() {
1359 let mouse_position = window.mouse_position();
1360 if let Ok(ix) = text_layout.index_for_position(mouse_position)
1361 && self
1362 .clickable_ranges
1363 .iter()
1364 .any(|range| range.contains(&ix))
1365 {
1366 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
1367 }
1368
1369 let text_layout = text_layout.clone();
1370 let mouse_down = interactive_state.mouse_down_index.clone();
1371 if let Some(mouse_down_index) = mouse_down.get() {
1372 let hitbox = hitbox.clone();
1373 let clickable_ranges = mem::take(&mut self.clickable_ranges);
1374 window.on_mouse_event(
1375 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
1376 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1377 if let Ok(mouse_up_index) =
1378 text_layout.index_for_position(event.position)
1379 {
1380 click_listener(
1381 &clickable_ranges,
1382 InteractiveTextClickEvent {
1383 mouse_down_index,
1384 mouse_up_index,
1385 },
1386 window,
1387 cx,
1388 )
1389 }
1390
1391 mouse_down.take();
1392 window.refresh();
1393 }
1394 },
1395 );
1396 } else {
1397 let hitbox = hitbox.clone();
1398 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
1399 if phase == DispatchPhase::Bubble
1400 && hitbox.is_hovered(window)
1401 && let Ok(mouse_down_index) =
1402 text_layout.index_for_position(event.position)
1403 {
1404 mouse_down.set(Some(mouse_down_index));
1405 window.refresh();
1406 }
1407 });
1408 }
1409 }
1410
1411 window.on_mouse_event({
1412 let mut hover_listener = self.hover_listener.take();
1413 let hitbox = hitbox.clone();
1414 let text_layout = text_layout.clone();
1415 let hovered_index = interactive_state.hovered_index.clone();
1416 move |event: &MouseMoveEvent, phase, window, cx| {
1417 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1418 let current = hovered_index.get();
1419 let updated = text_layout.index_for_position(event.position).ok();
1420 if current != updated {
1421 hovered_index.set(updated);
1422 if let Some(hover_listener) = hover_listener.as_ref() {
1423 hover_listener(updated, event.clone(), window, cx);
1424 }
1425 cx.notify(current_view);
1426 }
1427 }
1428 }
1429 });
1430
1431 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
1432 let active_tooltip = interactive_state.active_tooltip.clone();
1433 let build_tooltip = Rc::new({
1434 let tooltip_is_hoverable = false;
1435 let text_layout = text_layout.clone();
1436 move |window: &mut Window, cx: &mut App| {
1437 text_layout
1438 .index_for_position(window.mouse_position())
1439 .ok()
1440 .and_then(|position| tooltip_builder(position, window, cx))
1441 .map(|view| (view, tooltip_is_hoverable))
1442 }
1443 });
1444
1445 let check_is_hovered_during_prepaint = Rc::new({
1447 let source_bounds = hitbox.bounds;
1448 let text_layout = text_layout.clone();
1449 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1450 move |window: &Window| {
1451 text_layout
1452 .index_for_position(window.mouse_position())
1453 .is_ok()
1454 && source_bounds.contains(&window.mouse_position())
1455 && pending_mouse_down.get().is_none()
1456 }
1457 });
1458
1459 let check_is_hovered = Rc::new({
1460 let hitbox = hitbox.clone();
1461 let text_layout = text_layout.clone();
1462 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1463 move |window: &Window| {
1464 text_layout
1465 .index_for_position(window.mouse_position())
1466 .is_ok()
1467 && hitbox.is_hovered(window)
1468 && pending_mouse_down.get().is_none()
1469 }
1470 });
1471
1472 register_tooltip_mouse_handlers(
1473 &active_tooltip,
1474 self.tooltip_id,
1475 build_tooltip,
1476 check_is_hovered,
1477 check_is_hovered_during_prepaint,
1478 None,
1479 window,
1480 );
1481 }
1482
1483 self.text
1484 .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
1485
1486 ((), interactive_state)
1487 },
1488 );
1489 }
1490}
1491
1492impl IntoElement for InteractiveText {
1493 type Element = Self;
1494
1495 fn into_element(self) -> Self::Element {
1496 self
1497 }
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502 use super::*;
1503
1504 #[test]
1505 fn test_into_element_for() {
1506 use crate::{ParentElement as _, SharedString, div};
1507 use std::borrow::Cow;
1508
1509 let _ = div().child("static str");
1510 let _ = div().child("String".to_string());
1511 let _ = div().child(Cow::Borrowed("Cow"));
1512 let _ = div().child(SharedString::from("SharedString"));
1513 }
1514
1515 #[test]
1516 fn text_macro_id() {
1517 fn make_text_stable_id(happy: bool) -> Text {
1519 text!(if happy { "happy" } else { "sad" })
1520 }
1521
1522 fn make_text_unstable_id(happy: bool) -> Text {
1524 if happy { text!("happy") } else { text!("sad") }
1525 }
1526
1527 assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
1528 assert_ne!(
1529 make_text_unstable_id(false).id,
1530 make_text_unstable_id(true).id
1531 );
1532 }
1533}