1use crate::{
2 ActiveTooltip, AnyView, App, AppContext, Bounds, DispatchPhase, Element, ElementId,
3 GlobalElementId, HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
4 LayoutId, LineLayout, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParagraphDirection, Pixels,
5 Point, ShapedText, ShapedTextLayout, SharedString, Size, TextAlign, TextLayoutOptions,
6 TextOverflow, TextRangeExt, TextRun, TextStyle, TextTransform, TooltipId, UnicodeBidi,
7 WhiteSpace, Window, px, register_tooltip_mouse_handlers, set_tooltip_on_window,
8};
9use anyhow::Context as _;
10use gpui_util::ResultExt;
11use smallvec::SmallVec;
12use std::{
13 borrow::Cow,
14 cell::{Cell, Ref, RefCell},
15 mem,
16 ops::{Deref, DerefMut, Range},
17 rc::Rc,
18 sync::Arc,
19};
20use unicode_segmentation::UnicodeSegmentation;
21
22#[derive(Debug, Clone)]
61pub struct Text {
62 id: Option<ElementId>,
63 text: SharedString,
64}
65
66impl Text {
67 #[inline]
72 pub const fn new(id: ElementId, text: SharedString) -> Self {
73 Self { id: Some(id), text }
74 }
75
76 #[inline]
83 pub const fn new_inaccessible(text: SharedString) -> Self {
84 Self { id: None, text }
85 }
86
87 #[inline]
89 pub const fn id(&self) -> Option<&ElementId> {
90 self.id.as_ref()
91 }
92
93 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
95 self.id = Some(id.into());
96 self
97 }
98
99 #[inline]
101 pub const fn text(&self) -> &SharedString {
102 &self.text
103 }
104}
105
106impl Deref for Text {
107 type Target = SharedString;
108 fn deref(&self) -> &Self::Target {
109 &self.text
110 }
111}
112
113impl DerefMut for Text {
114 fn deref_mut(&mut self) -> &mut Self::Target {
115 &mut self.text
116 }
117}
118
119#[doc(hidden)]
125pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
126 const BASIS: u64 = 0xcbf29ce484222325;
127 const PRIME: u64 = 0x100000001b3;
128
129 let bytes = s.as_bytes();
130 let mut hash = BASIS;
131 let mut i = 0;
132 while i < bytes.len() {
133 hash ^= bytes[i] as u64;
134 hash = hash.wrapping_mul(PRIME);
135 i += 1;
136 }
137 hash
138}
139
140#[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, window)
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(), window)
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 {
415 text: SharedString,
416 runs: Option<Vec<TextRun>>,
417 delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
418 delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
419 layout: TextLayout,
420}
421
422impl StyledText {
423 pub fn new(text: impl Into<SharedString>) -> Self {
428 StyledText {
429 text: text.into(),
430 runs: None,
431 delayed_highlights: None,
432 delayed_font_family_overrides: None,
433 layout: TextLayout::default(),
434 }
435 }
436
437 pub fn layout(&self) -> &TextLayout {
443 &self.layout
444 }
445
446 pub fn with_default_highlights(
460 mut self,
461 default_style: &TextStyle,
462 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
463 ) -> Self {
464 debug_assert!(
465 self.delayed_highlights.is_none(),
466 "Can't use `with_default_highlights` and `with_highlights`"
467 );
468 let runs = Self::compute_runs(&self.text, default_style, highlights);
469 self.with_runs(runs)
470 }
471
472 pub fn with_highlights(
497 mut self,
498 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
499 ) -> Self {
500 debug_assert!(
501 self.runs.is_none(),
502 "Can't use `with_highlights` and `with_default_highlights`"
503 );
504 self.delayed_highlights = Some(
505 highlights
506 .into_iter()
507 .inspect(|(run, _)| {
508 debug_assert!(self.text.contains_range(run));
509 })
510 .collect::<Vec<_>>(),
511 );
512 self
513 }
514
515 fn compute_runs(
516 text: &str,
517 default_style: &TextStyle,
518 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
519 ) -> Vec<TextRun> {
520 let mut runs = Vec::new();
521 let mut ix = 0;
522 for (range, highlight) in highlights {
523 debug_assert!(text.contains_range(&range));
524
525 if ix < range.start {
526 runs.push(default_style.clone().to_run(range.start - ix));
527 }
528 runs.push(
529 default_style
530 .clone()
531 .highlight(highlight)
532 .to_run(range.len()),
533 );
534 ix = range.end;
535 }
536 if ix < text.len() {
537 runs.push(default_style.to_run(text.len() - ix));
538 }
539 runs
540 }
541
542 pub fn with_font_family_overrides(
553 mut self,
554 overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
555 ) -> Self {
556 self.delayed_font_family_overrides = Some(
557 overrides
558 .into_iter()
559 .inspect(|(range, _)| {
560 debug_assert!(self.text.contains_range(range));
561 })
562 .collect(),
563 );
564 self
565 }
566
567 fn apply_font_family_overrides(
568 runs: &mut [TextRun],
569 overrides: &[(Range<usize>, SharedString)],
570 ) {
571 let mut byte_offset = 0;
572 let mut override_idx = 0;
573 for run in runs.iter_mut() {
574 let run_end = byte_offset + run.len;
575 while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
576 override_idx += 1;
577 }
578 if override_idx < overrides.len() {
579 let (ref range, ref family) = overrides[override_idx];
580 if byte_offset >= range.start && run_end <= range.end {
581 run.font.family = family.clone();
582 }
583 }
584 byte_offset = run_end;
585 }
586 }
587
588 fn take_runs(&mut self, default_style: &TextStyle) -> Vec<TextRun> {
589 let font_family_overrides = self.delayed_font_family_overrides.take();
590 let mut runs = self.runs.take().or_else(|| {
591 self.delayed_highlights.take().map(|delayed_highlights| {
592 Self::compute_runs(&self.text, default_style, delayed_highlights)
593 })
594 });
595
596 if let Some(ref overrides) = font_family_overrides {
597 let runs = runs.get_or_insert_with(|| vec![default_style.to_run(self.text.len())]);
598 Self::apply_font_family_overrides(runs, overrides);
599 }
600
601 runs.unwrap_or_else(|| vec![default_style.to_run(self.text.len())])
602 }
603
604 pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
614 let mut text = &*self.text;
615 for run in &runs {
616 text = text.get(run.len..).unwrap_or_else(|| {
617 #[cfg(debug_assertions)]
618 panic!("invalid text run. Text: '{text}', run: {run:?}");
619 #[cfg(not(debug_assertions))]
620 panic!("invalid text run");
621 });
622 }
623 assert!(text.is_empty(), "invalid text run");
624 self.runs = Some(runs);
625 self
626 }
627}
628
629impl Element for StyledText {
630 type RequestLayoutState = ();
631 type PrepaintState = ();
632
633 fn id(&self) -> Option<ElementId> {
634 None
635 }
636
637 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
638 None
639 }
640
641 fn request_layout(
642 &mut self,
643 _id: Option<&GlobalElementId>,
644 _inspector_id: Option<&InspectorElementId>,
645 window: &mut Window,
646 cx: &mut App,
647 ) -> (LayoutId, Self::RequestLayoutState) {
648 let runs = self.take_runs(&window.text_style());
649 let layout_id = self
650 .layout
651 .layout(self.text.clone(), Some(runs), window, cx);
652 (layout_id, ())
653 }
654
655 fn prepaint(
656 &mut self,
657 _id: Option<&GlobalElementId>,
658 _inspector_id: Option<&InspectorElementId>,
659 bounds: Bounds<Pixels>,
660 _: &mut Self::RequestLayoutState,
661 window: &mut Window,
662 _cx: &mut App,
663 ) {
664 self.layout.prepaint(bounds, &self.text, window)
665 }
666
667 fn paint(
668 &mut self,
669 _id: Option<&GlobalElementId>,
670 _inspector_id: Option<&InspectorElementId>,
671 _bounds: Bounds<Pixels>,
672 _: &mut Self::RequestLayoutState,
673 _: &mut Self::PrepaintState,
674 window: &mut Window,
675 cx: &mut App,
676 ) {
677 self.layout.paint(&self.text, window, cx)
678 }
679}
680
681impl IntoElement for StyledText {
682 type Element = Self;
683
684 fn into_element(self) -> Self::Element {
685 self
686 }
687}
688
689#[derive(Default, Clone)]
699pub struct TextLayout(Rc<TextLayoutState>);
700
701#[derive(Default)]
702struct TextLayoutState {
703 layout_id: Cell<Option<LayoutId>>,
704 layout: RefCell<Option<TextLayoutInner>>,
705}
706
707#[derive(Clone, Copy)]
708enum TextPaintPass {
709 All,
710 Background,
711 Foreground,
712}
713
714struct TextLayoutInner {
715 len: usize,
716 document: Option<ShapedText>,
717 line_height: Pixels,
718 truncate_width: Option<Pixels>,
719 options: TextLayoutOptions,
720 size: Size<Pixels>,
721 bounds: Option<Bounds<Pixels>>,
722}
723
724fn apply_text_transform_preserving_byte_len(
725 text: SharedString,
726 transform: Option<TextTransform>,
727) -> SharedString {
728 let Some(transform) = transform else {
729 return text;
730 };
731 if matches!(transform, TextTransform::None) {
732 return text;
733 }
734
735 let mut output = String::with_capacity(text.len());
736 match transform {
737 TextTransform::Uppercase => {
738 for character in text.as_ref().chars() {
739 push_case_mapped_character(&mut output, character, CaseMapKind::Upper);
740 }
741 }
742 TextTransform::Lowercase => {
743 for character in text.as_ref().chars() {
744 push_case_mapped_character(&mut output, character, CaseMapKind::Lower);
745 }
746 }
747 TextTransform::Capitalize => {
748 for piece in text.as_ref().split_word_bounds() {
749 let mut seen_first_letter = false;
750 for character in piece.chars() {
751 if !seen_first_letter && character.is_alphabetic() {
752 push_case_mapped_character(&mut output, character, CaseMapKind::Upper);
753 seen_first_letter = true;
754 } else {
755 output.push(character);
756 }
757 }
758 }
759 }
760 TextTransform::None => return text,
761 }
762
763 SharedString::from(output)
764}
765
766#[derive(Copy, Clone)]
767enum CaseMapKind {
768 Upper,
769 Lower,
770}
771
772fn push_case_mapped_character(output: &mut String, character: char, kind: CaseMapKind) {
773 let mapped = match kind {
774 CaseMapKind::Upper => character.to_uppercase().collect::<String>(),
775 CaseMapKind::Lower => character.to_lowercase().collect::<String>(),
776 };
777
778 if mapped.len() == character.len_utf8() && mapped.chars().count() == 1 {
779 output.push_str(&mapped);
780 } else {
781 output.push(character);
782 }
783}
784
785#[cfg(test)]
786mod text_transform_tests {
787 use super::apply_text_transform_preserving_byte_len;
788 use crate::{SharedString, TextTransform};
789
790 #[test]
791 fn text_transforms_preserve_bytes_and_spacing() {
792 let input = SharedString::from("hello WORLD\tfoo-bar 123baz déjà vu");
793 let uppercase =
794 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase));
795 let lowercase =
796 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase));
797 let capitalize = apply_text_transform_preserving_byte_len(
798 input.clone(),
799 Some(TextTransform::Capitalize),
800 );
801
802 assert_eq!(uppercase.as_ref(), "HELLO WORLD\tFOO-BAR 123BAZ DÉJÀ VU");
803 assert_eq!(lowercase.as_ref(), "hello world\tfoo-bar 123baz déjà vu");
804 assert_eq!(capitalize.as_ref(), "Hello WORLD\tFoo-Bar 123Baz Déjà Vu");
805 assert_eq!(input.len(), uppercase.len());
806 assert_eq!(input.len(), lowercase.len());
807 assert_eq!(input.len(), capitalize.len());
808 }
809
810 #[test]
811 fn text_transforms_skip_expanding_unicode_mappings() {
812 let input = SharedString::from("straße İSTANBUL");
813 let uppercase =
814 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Uppercase));
815 let lowercase =
816 apply_text_transform_preserving_byte_len(input.clone(), Some(TextTransform::Lowercase));
817
818 assert_eq!(uppercase.as_ref(), "STRAßE İSTANBUL");
819 assert_eq!(lowercase.as_ref(), "straße İstanbul");
820 assert_eq!(input.len(), uppercase.len());
821 assert_eq!(input.len(), lowercase.len());
822 }
823
824 #[test]
825 fn capitalize_preserves_letters_after_digit_prefix() {
826 let input = SharedString::from("123BAZ");
827 let output = apply_text_transform_preserving_byte_len(
828 input.clone(),
829 Some(TextTransform::Capitalize),
830 );
831 assert_eq!(output.as_ref(), "123BAZ");
832 assert_eq!(input.len(), output.len());
833 }
834
835 #[test]
836 fn capitalize_does_not_fold_remaining_letters() {
837 let input = SharedString::from("foo2BAR");
838 let output =
839 apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize));
840 assert_eq!(output.as_ref(), "Foo2BAR");
841 }
842
843 #[test]
844 fn capitalize_handles_apostrophe_contractions() {
845 let input = SharedString::from("don't panic");
846 let output =
847 apply_text_transform_preserving_byte_len(input, Some(TextTransform::Capitalize));
848 assert_eq!(output.as_ref(), "Don't Panic");
849 }
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
857pub enum TruncateFrom {
858 Start,
860 End,
862 Middle,
864}
865
866pub struct TextLayoutTruncation {
872 pub width: Option<Pixels>,
874 pub affix: SharedString,
876 pub source: TruncateFrom,
878}
879
880impl TextLayoutTruncation {
881 fn overflow_width(text_overflow: TextOverflow, width: Option<Pixels>) -> Self {
883 let (affix, source) = match text_overflow {
884 TextOverflow::Truncate(affix) => (affix, TruncateFrom::End),
885 TextOverflow::TruncateStart(affix) => (affix, TruncateFrom::Start),
886 TextOverflow::TruncateMiddle(affix) => (affix, TruncateFrom::Middle),
887 };
888
889 Self {
890 width,
891 affix,
892 source,
893 }
894 }
895}
896
897impl TextLayout {
898 pub fn evaluate_wrap_width(
903 white_space: &WhiteSpace,
904 known_dimensions: Size<Option<Pixels>>,
905 available_space: Size<crate::AvailableSpace>,
906 ) -> Option<Pixels> {
907 match white_space {
908 WhiteSpace::Nowrap => None,
909 WhiteSpace::Normal => known_dimensions.width.or(match available_space.width {
910 crate::AvailableSpace::Definite(width) => Some(width),
911 crate::AvailableSpace::MinContent => Some(Pixels::ZERO),
913 crate::AvailableSpace::MaxContent => None,
914 }),
915 }
916 }
917
918 pub fn evaluate_alignment_width(
923 known_dimensions: Size<Option<Pixels>>,
924 available_space: Size<crate::AvailableSpace>,
925 ) -> Option<Pixels> {
926 known_dimensions.width.or(match available_space.width {
927 crate::AvailableSpace::Definite(width) => Some(width),
928 crate::AvailableSpace::MinContent | crate::AvailableSpace::MaxContent => None,
929 })
930 }
931
932 pub fn evaluate_overflow(
937 text_style: &TextStyle,
938 known_dimensions: Size<Option<Pixels>>,
939 available_space: Size<crate::AvailableSpace>,
940 ) -> TextLayoutTruncation {
941 match text_style.text_overflow.clone() {
942 Some(text_overflow) => {
943 let width = Self::evaluate_alignment_width(known_dimensions, available_space);
945
946 TextLayoutTruncation::overflow_width(text_overflow, width)
947 }
948 None => TextLayoutTruncation {
949 width: None,
950 affix: SharedString::default(),
951 source: TruncateFrom::End,
952 },
953 }
954 }
955
956 pub(crate) fn layout_options(
957 text_style: &TextStyle,
958 known_dimensions: Size<Option<Pixels>>,
959 available_space: Size<crate::AvailableSpace>,
960 resolved_direction: crate::ResolvedDirection,
961 unicode_bidi: UnicodeBidi,
962 ) -> (TextLayoutOptions, TextLayoutTruncation) {
963 let alignment_width = Self::evaluate_alignment_width(known_dimensions, available_space);
964 let direction = match unicode_bidi {
965 UnicodeBidi::Plaintext => ParagraphDirection::Auto,
966 _ => resolved_direction.into(),
967 };
968 let options = TextLayoutOptions {
969 wrap_width: match text_style.white_space {
970 WhiteSpace::Normal => alignment_width,
971 WhiteSpace::Nowrap => None,
972 },
973 line_clamp: text_style.line_clamp,
974 alignment_width,
975 text_align: text_style.text_align,
976 direction,
977 unicode_bidi,
978 };
979 let truncation = Self::evaluate_overflow(text_style, known_dimensions, available_space);
980
981 (options, truncation)
982 }
983
984 pub fn apply_truncation<'runs>(
991 text: SharedString,
992 text_style: &TextStyle,
993 font_size: Pixels,
994 line_height: Pixels,
995 wrap_width: Option<Pixels>,
996 truncation: &TextLayoutTruncation,
997 runs: &'runs [TextRun],
998 paragraph_direction: ParagraphDirection,
999 unicode_bidi: UnicodeBidi,
1000 window: &mut Window,
1001 cx: &mut App,
1002 ) -> (SharedString, Cow<'runs, [TextRun]>) {
1003 let _ = (line_height, cx);
1004 let Some(truncate_width) = truncation.width else {
1005 return (text, Cow::Borrowed(runs));
1006 };
1007 truncate_to_shaped_layout(
1008 text,
1009 font_size,
1010 wrap_width,
1011 truncate_width,
1012 text_style.line_clamp,
1013 &truncation.affix,
1014 runs,
1015 truncation.source,
1016 paragraph_direction,
1017 unicode_bidi,
1018 window,
1019 )
1020 }
1021
1022 fn layout(
1023 &self,
1024 text: SharedString,
1025 runs: Option<Vec<TextRun>>,
1026 window: &mut Window,
1027 _: &mut App,
1028 ) -> LayoutId {
1029 struct TextKey {
1031 text: SharedString,
1032 runs: Arc<[TextRun]>,
1033 text_style: TextStyle,
1034 font_size: Pixels,
1035 line_height: Pixels,
1036 font_generation: u64,
1037 state: TextLayout,
1038 }
1039
1040 impl crate::taffy::MeasureKey for TextKey {
1041 fn same(&self, previous: &dyn crate::taffy::MeasureKey) -> bool {
1042 previous
1043 .as_any()
1044 .downcast_ref::<Self>()
1045 .is_some_and(|previous| {
1046 self.text == previous.text
1047 && self.runs == previous.runs
1048 && self.text_style == previous.text_style
1049 && self.font_size == previous.font_size
1050 && self.line_height == previous.line_height
1051 && self.font_generation == previous.font_generation
1052 })
1053 }
1054
1055 fn inherit(&self, previous: &dyn crate::taffy::MeasureKey) -> bool {
1056 let Some(previous) = previous.as_any().downcast_ref::<Self>() else {
1057 return false;
1058 };
1059 let layout =
1060 previous
1061 .state
1062 .0
1063 .layout
1064 .borrow()
1065 .as_ref()
1066 .map(|layout| TextLayoutInner {
1067 document: layout.document.as_ref().map(|document| ShapedText {
1068 layout: document.layout.clone(),
1069 text: document.text.clone(),
1070 }),
1071 bounds: None,
1072 ..*layout
1073 });
1074 let measured = layout.is_some();
1075 *self.state.0.layout.borrow_mut() = layout;
1076 measured
1077 }
1078
1079 fn as_any(&self) -> &dyn std::any::Any {
1080 self
1081 }
1082 }
1083
1084 let text_style = window.text_style();
1085 let text = apply_text_transform_preserving_byte_len(text, text_style.text_transform);
1086 let font_size = text_style.font_size.to_pixels(window.rem_size());
1087 let line_height = window.pixel_snap(
1088 text_style
1089 .line_height
1090 .to_pixels(font_size.into(), window.rem_size()),
1091 );
1092
1093 let runs = if let Some(runs) = runs {
1094 runs
1095 } else {
1096 vec![text_style.to_run(text.len())]
1097 };
1098
1099 let runs: Arc<[TextRun]> = runs.into();
1100 let content = crate::InlineContent::Text {
1101 text: text.clone(),
1102 runs: runs.clone(),
1103 font_size,
1104 line_height,
1105 };
1106
1107 let key = TextKey {
1108 text: text.clone(),
1109 runs: runs.clone(),
1110 text_style: text_style.clone(),
1111 font_size,
1112 line_height,
1113 font_generation: window.text_system().font_generation(),
1114 state: self.clone(),
1115 };
1116 let layout_id = window.request_keyed_measured_layout(Default::default(), key, {
1117 let element_state = self.clone();
1118
1119 move |known_dimensions, available_space, window, cx| {
1120 let unicode_bidi = window.resolved_unicode_bidi();
1121 let (options, truncation) = Self::layout_options(
1122 &text_style,
1123 known_dimensions,
1124 available_space,
1125 window.resolved_direction(),
1126 unicode_bidi,
1127 );
1128 let truncate_width = truncation.width;
1129
1130 if let Some(text_layout) = element_state.0.layout.borrow().as_ref()
1138 && truncate_width.is_none()
1139 && text_layout.truncate_width.is_none()
1140 && text_layout.options == options
1141 {
1142 return text_layout.size;
1143 }
1144
1145 let (text, runs) = Self::apply_truncation(
1146 text.clone(),
1147 &text_style,
1148 font_size,
1149 line_height,
1150 options.wrap_width,
1151 &truncation,
1152 &runs,
1153 options.direction,
1154 options.unicode_bidi,
1155 window,
1156 cx,
1157 );
1158 let document = window
1159 .text_system()
1160 .shape_text_with_options(text, font_size, &runs, options)
1161 .log_err();
1162
1163 let size = document
1164 .as_ref()
1165 .map_or_else(Size::default, |document| document.size(line_height));
1166
1167 let len = document
1168 .as_ref()
1169 .map(|document| document.len())
1170 .unwrap_or_default();
1171
1172 element_state
1173 .0
1174 .layout
1175 .borrow_mut()
1176 .replace(TextLayoutInner {
1177 document,
1178 len,
1179 line_height,
1180 truncate_width,
1181 options,
1182 size,
1183 bounds: None,
1184 });
1185
1186 size
1187 }
1188 });
1189
1190 window.publish_inline_content(layout_id, content);
1191 self.0.layout_id.set(Some(layout_id));
1192 layout_id
1193 }
1194
1195 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str, window: &mut Window) {
1196 if window.current_inline_fragments.is_some() {
1197 return;
1198 }
1199
1200 let bounds = self
1201 .0
1202 .layout_id
1203 .get()
1204 .map(|layout_id| window.parent_relative_layout_bounds(layout_id))
1205 .unwrap_or(bounds);
1206 let mut element_state = self.0.layout.borrow_mut();
1207 let element_state = element_state
1208 .as_mut()
1209 .with_context(|| format!("measurement has not been performed on {text}"))
1210 .unwrap();
1211 element_state.bounds = Some(bounds);
1212 }
1213
1214 fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
1215 if window.current_inline_fragments.is_some() {
1216 return;
1217 }
1218
1219 self.paint_lines(TextPaintPass::All, window, cx)
1220 .with_context(|| format!("failed to paint {text}"))
1221 .unwrap();
1222 }
1223
1224 pub fn paint_background(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
1227 self.paint_lines(TextPaintPass::Background, window, cx)
1228 }
1229
1230 pub fn paint_foreground(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
1232 self.paint_lines(TextPaintPass::Foreground, window, cx)
1233 }
1234
1235 fn paint_lines(
1236 &self,
1237 pass: TextPaintPass,
1238 window: &mut Window,
1239 cx: &mut App,
1240 ) -> anyhow::Result<()> {
1241 let element_state = self.0.layout.borrow();
1242 let element_state = element_state
1243 .as_ref()
1244 .context("measurement has not been performed")?;
1245 let bounds = element_state
1246 .bounds
1247 .context("prepaint has not been performed")?;
1248
1249 let line_height = element_state.line_height;
1250 if let Some(document) = &element_state.document {
1251 if matches!(pass, TextPaintPass::All | TextPaintPass::Background) {
1252 document
1253 .paint_background(
1254 bounds.origin,
1255 line_height,
1256 TextAlign::Left,
1257 None,
1258 window,
1259 cx,
1260 )
1261 .log_err();
1262 }
1263 if matches!(pass, TextPaintPass::All | TextPaintPass::Foreground) {
1264 document
1265 .paint(
1266 bounds.origin,
1267 line_height,
1268 TextAlign::Left,
1269 None,
1270 window,
1271 cx,
1272 )
1273 .log_err();
1274 }
1275 }
1276 Ok(())
1277 }
1278
1279 fn measured(&self) -> Ref<'_, TextLayoutInner> {
1280 Ref::map(self.0.layout.borrow(), |layout| {
1281 layout.as_ref().expect("measurement has not been performed")
1282 })
1283 }
1284
1285 pub fn index_for_position(&self, pixel_point: Point<Pixels>) -> Result<usize, usize> {
1296 let element_state = self.measured();
1297 let bounds = element_state
1298 .bounds
1299 .expect("prepaint has not been performed");
1300
1301 if pixel_point.y < bounds.top() {
1302 return Err(0);
1303 }
1304
1305 let line_height = element_state.line_height;
1306 let Some(document) = &element_state.document else {
1307 return Err(0);
1308 };
1309
1310 document.byte_index_for_pixel_point(pixel_point - bounds.origin, line_height)
1311 }
1312
1313 pub fn position_for_index(&self, byte_index: usize) -> Option<Point<Pixels>> {
1322 let element_state = self.measured();
1323 let bounds = element_state
1324 .bounds
1325 .expect("prepaint has not been performed");
1326 let line_height = element_state.line_height;
1327
1328 let document = element_state.document.as_ref()?;
1329 Some(bounds.origin + document.visual_position_for_byte_index(byte_index, line_height)?)
1330 }
1331
1332 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<ShapedTextLayout>> {
1342 let element_state = self.measured();
1343 let document = element_state.document.as_ref()?;
1344 (index <= document.len()).then(|| document.layout.clone())
1345 }
1346
1347 pub fn line_layouts(&self) -> SmallVec<[Arc<ShapedTextLayout>; 1]> {
1355 self.measured()
1356 .document
1357 .iter()
1358 .map(|document| document.layout.clone())
1359 .collect()
1360 }
1361
1362 pub fn bounds(&self) -> Bounds<Pixels> {
1368 self.measured().bounds.unwrap()
1369 }
1370
1371 pub fn line_height(&self) -> Pixels {
1377 self.measured().line_height
1378 }
1379
1380 pub fn len(&self) -> usize {
1389 self.measured().len
1390 }
1391
1392 pub fn text(&self) -> String {
1402 self.measured()
1403 .document
1404 .as_ref()
1405 .map_or_else(String::new, |document| document.text.to_string())
1406 }
1407
1408 pub fn wrapped_text(&self) -> String {
1418 let mut accumulator = String::new();
1419 let element_state = self.measured();
1420
1421 if let Some(document) = &element_state.document {
1422 for visual_line in document.layout.visual_lines() {
1423 accumulator.push_str(&document.text[visual_line.text_range.clone()]);
1424 accumulator.push('\n');
1425 }
1426 }
1427 accumulator.pop();
1429 accumulator
1430 }
1431}
1432
1433fn truncate_to_shaped_layout<'a>(
1434 text: SharedString,
1435 font_size: Pixels,
1436 wrap_width: Option<Pixels>,
1437 truncate_width: Pixels,
1438 max_lines: Option<usize>,
1439 affix: &str,
1440 runs: &'a [TextRun],
1441 direction: TruncateFrom,
1442 paragraph_direction: ParagraphDirection,
1443 unicode_bidi: UnicodeBidi,
1444 window: &mut Window,
1445) -> (SharedString, Cow<'a, [TextRun]>) {
1446 let options = TextLayoutOptions {
1447 wrap_width,
1448 direction: paragraph_direction,
1449 unicode_bidi,
1450 text_align: TextAlign::Left,
1451 ..Default::default()
1452 };
1453 let Ok(document) =
1454 window
1455 .text_system()
1456 .shape_text_with_options(text.clone(), font_size, runs, options)
1457 else {
1458 return (text, Cow::Borrowed(runs));
1459 };
1460 let width = wrap_width.unwrap_or(truncate_width);
1461 let fits = text_layout_fits(&document.layout.layout, width, max_lines);
1462
1463 if fits {
1464 return (text, Cow::Borrowed(runs));
1465 }
1466
1467 let mut boundaries = text
1468 .grapheme_indices(true)
1469 .map(|(index, _)| index)
1470 .collect::<Vec<_>>();
1471 boundaries.push(text.len());
1472 let grapheme_count = boundaries.len().saturating_sub(1);
1473 let grapheme_ranges = boundaries
1474 .windows(2)
1475 .map(|boundary| boundary[0]..boundary[1])
1476 .collect::<Vec<_>>();
1477 let grapheme_widths = document
1478 .platform_layout
1479 .inline_geometry_for_ranges(&grapheme_ranges)
1480 .into_iter()
1481 .map(|regions| {
1482 regions
1483 .into_iter()
1484 .map(|geometry| geometry.bounds.size.width)
1485 .sum::<Pixels>()
1486 })
1487 .collect::<Vec<_>>();
1488 let mut prefix_widths = Vec::with_capacity(grapheme_widths.len() + 1);
1489 prefix_widths.push(Pixels::ZERO);
1490 for width in &grapheme_widths {
1491 prefix_widths.push(prefix_widths.last().copied().unwrap_or_default() + *width);
1492 }
1493
1494 let affix_width = if affix.is_empty() {
1495 Pixels::ZERO
1496 } else {
1497 let candidate = make_truncation_candidate(
1498 &text,
1499 &TruncationBoundaries::new(boundaries.iter().copied(), grapheme_count),
1500 0,
1501 affix,
1502 runs,
1503 direction,
1504 );
1505
1506 window
1507 .text_system()
1508 .shape_text_with_options(
1509 SharedString::from(affix),
1510 font_size,
1511 &candidate.runs,
1512 TextLayoutOptions {
1513 wrap_width: None,
1514 ..options
1515 },
1516 )
1517 .map_or(Pixels::ZERO, |layout| layout.width())
1518 };
1519 let available_width = (width - affix_width).max(Pixels::ZERO);
1520
1521 let keep = if direction == TruncateFrom::End
1522 && let (Some(wrap_width), Some(max_lines)) = (wrap_width, max_lines)
1523 {
1524 let last_line_idx = max_lines.max(1).saturating_sub(1);
1525 let line_start = document
1526 .visual_lines()
1527 .get(last_line_idx)
1528 .map_or(0, |line| line.text_range.start);
1529 let fixed_count = grapheme_ranges.partition_point(|range| range.end <= line_start);
1530 fixed_count
1531 + grapheme_widths[fixed_count..]
1532 .iter()
1533 .scan(Pixels::ZERO, |used, advance| {
1534 *used += *advance;
1535 Some(*used <= (wrap_width - affix_width).max(Pixels::ZERO))
1536 })
1537 .take_while(|fits| *fits)
1538 .count()
1539 } else {
1540 (0..grapheme_count)
1541 .take_while(|keep| {
1542 let candidate_count = keep + 1;
1543 let width: Pixels = match direction {
1544 TruncateFrom::End => prefix_widths[candidate_count],
1545 TruncateFrom::Start => {
1546 prefix_widths[grapheme_count]
1547 - prefix_widths[grapheme_count - candidate_count]
1548 }
1549 TruncateFrom::Middle => {
1550 let front_count = candidate_count.saturating_mul(2).div_ceil(3);
1551 let back_count = candidate_count - front_count;
1552 prefix_widths[front_count] + prefix_widths[grapheme_count]
1553 - prefix_widths[grapheme_count - back_count]
1554 }
1555 };
1556
1557 width <= available_width
1558 })
1559 .count()
1560 };
1561
1562 let (candidate, ()) = truncate_with_measured_candidates(
1563 &text,
1564 boundaries.iter().copied(),
1565 Some(keep),
1566 affix,
1567 runs,
1568 direction,
1569 |candidate| {
1570 let fits = window
1571 .text_system()
1572 .shape_text_with_options(
1573 candidate.text.clone(),
1574 font_size,
1575 &candidate.runs,
1576 options,
1577 )
1578 .is_ok_and(|document| text_layout_fits(&document.layout.layout, width, max_lines));
1579
1580 ((), fits)
1581 },
1582 );
1583
1584 (candidate.text, Cow::Owned(candidate.runs))
1585}
1586
1587pub(crate) fn text_layout_fits(
1588 layout: &LineLayout,
1589 width: Pixels,
1590 max_lines: Option<usize>,
1591) -> bool {
1592 max_lines.is_none_or(|count| layout.platform_layout.line_count() <= count.max(1))
1593 && layout.platform_layout.size().width <= width + px(0.01)
1594}
1595
1596pub(crate) struct TruncationCandidate {
1597 pub text: SharedString,
1598 pub runs: Vec<TextRun>,
1599 retained: SmallVec<[(Range<usize>, usize); 2]>,
1600 affix_range: Range<usize>,
1601 affix_source: usize,
1602}
1603
1604impl TruncationCandidate {
1605 pub(crate) fn display_ranges(&self, source: &Range<usize>) -> SmallVec<[Range<usize>; 3]> {
1606 let mut ranges: SmallVec<[Range<usize>; 3]> = SmallVec::new();
1607
1608 for (retained, display_start) in &self.retained {
1609 let start = source.start.max(retained.start);
1610 let end = source.end.min(retained.end);
1611
1612 if start < end {
1613 ranges.push(
1614 start - retained.start + display_start..end - retained.start + display_start,
1615 );
1616 }
1617 }
1618
1619 if source.contains(&self.affix_source) && !self.affix_range.is_empty() {
1620 ranges.push(self.affix_range.clone());
1621 }
1622
1623 ranges.sort_by_key(|range| range.start);
1624
1625 ranges
1626 }
1627}
1628
1629struct TruncationBoundaries<Boundaries> {
1634 grapheme_count: usize,
1635 front: BoundaryWindow<Boundaries>,
1636 back: BoundaryWindow<Boundaries>,
1637}
1638
1639impl<Boundaries: DoubleEndedIterator<Item = usize> + Clone> TruncationBoundaries<Boundaries> {
1640 fn new(boundaries: Boundaries, grapheme_count: usize) -> Self {
1641 Self {
1642 grapheme_count,
1643 front: BoundaryWindow {
1644 boundaries: boundaries.clone(),
1645 first: 0,
1646 last: grapheme_count,
1647 },
1648 back: BoundaryWindow {
1649 boundaries,
1650 first: 0,
1651 last: grapheme_count,
1652 },
1653 }
1654 }
1655
1656 fn narrow(&mut self, keep: usize, direction: TruncateFrom, fits: bool) {
1657 let (front_count, back_count) = match direction {
1658 TruncateFrom::End => (keep, 0),
1659 TruncateFrom::Start => (0, keep),
1660 TruncateFrom::Middle => {
1661 let front_count = keep.saturating_mul(2).div_ceil(3);
1662
1663 (front_count, keep - front_count)
1664 }
1665 };
1666
1667 if direction != TruncateFrom::Start {
1668 if fits {
1669 self.front.retain_after(front_count);
1670 } else {
1671 self.front.retain_before(front_count);
1672 }
1673 }
1674
1675 if direction != TruncateFrom::End {
1676 let back_index = self.grapheme_count - back_count;
1677
1678 if fits {
1679 self.back.retain_before(back_index);
1680 } else {
1681 self.back.retain_after(back_index);
1682 }
1683 }
1684 }
1685}
1686
1687struct BoundaryWindow<Boundaries> {
1688 boundaries: Boundaries,
1689 first: usize,
1690 last: usize,
1691}
1692
1693impl<Boundaries: DoubleEndedIterator<Item = usize> + Clone> BoundaryWindow<Boundaries> {
1694 fn get(&self, index: usize) -> usize {
1695 let from_start = index - self.first;
1696 let from_end = self.last - index;
1697
1698 if from_start <= from_end {
1699 self.boundaries.clone().nth(from_start)
1700 } else {
1701 self.boundaries.clone().nth_back(from_end)
1702 }
1703 .expect("grapheme boundary is present")
1704 }
1705
1706 fn retain_after(&mut self, index: usize) {
1707 if index > self.first {
1708 let _boundary = self.boundaries.nth(index - self.first - 1);
1709 self.first = index;
1710 }
1711 }
1712
1713 fn retain_before(&mut self, index: usize) {
1714 if index < self.last {
1715 let _boundary = self.boundaries.nth_back(self.last - index - 1);
1716 self.last = index;
1717 }
1718 }
1719}
1720
1721pub(crate) fn truncate_with_measured_candidates<Layout>(
1725 text: &str,
1726 boundaries: impl DoubleEndedIterator<Item = usize> + Clone,
1727 initial_keep: Option<usize>,
1728 affix: &str,
1729 runs: &[TextRun],
1730 direction: TruncateFrom,
1731 mut measure: impl FnMut(&TruncationCandidate) -> (Layout, bool),
1732) -> (TruncationCandidate, Layout) {
1733 let boundary_count = boundaries.clone().count();
1734 let grapheme_count = boundary_count.saturating_sub(1);
1735 let mut boundaries = TruncationBoundaries::new(boundaries, grapheme_count);
1736
1737 let mut lower = 0;
1738 let mut upper = boundary_count.saturating_sub(2);
1739 let mut keep = initial_keep.unwrap_or(boundary_count / 2).min(upper);
1740 let mut best = None;
1741
1742 loop {
1743 let candidate = make_truncation_candidate(text, &boundaries, keep, affix, runs, direction);
1744 let (layout, fits) = measure(&candidate);
1745
1746 if fits {
1747 best = Some((candidate, layout));
1748 lower = keep + 1;
1749 } else if keep == 0 {
1750 return best.unwrap_or((candidate, layout));
1752 } else {
1753 upper = keep - 1;
1754 }
1755
1756 if lower > upper {
1757 return best.expect("a fitting candidate was measured");
1758 }
1759
1760 boundaries.narrow(keep, direction, fits);
1761 keep = lower + (upper - lower) / 2;
1762 }
1763}
1764
1765fn make_truncation_candidate(
1766 text: &str,
1767 boundaries: &TruncationBoundaries<impl DoubleEndedIterator<Item = usize> + Clone>,
1768 keep: usize,
1769 affix: &str,
1770 runs: &[TextRun],
1771 direction: TruncateFrom,
1772) -> TruncationCandidate {
1773 let grapheme_count = boundaries.grapheme_count;
1774 let keep = keep.min(grapheme_count);
1775 let (front_end, back_start, affix_source) = match direction {
1776 TruncateFrom::End => {
1777 let prefix = text[..boundaries.front.get(keep)]
1778 .trim_end_matches(|ch: char| ch.is_whitespace() || ch.is_ascii_punctuation());
1779 let end = prefix.len();
1780
1781 (end, text.len(), end.min(text.len().saturating_sub(1)))
1782 }
1783 TruncateFrom::Start => {
1784 let start = boundaries.back.get(grapheme_count - keep);
1785
1786 (0, start, start.saturating_sub(1))
1787 }
1788 TruncateFrom::Middle => {
1789 let front_count = keep.saturating_mul(2).div_ceil(3);
1790 let back_count = keep - front_count;
1791 let end = boundaries.front.get(front_count);
1792
1793 (
1794 end,
1795 boundaries.back.get(grapheme_count - back_count),
1796 end.saturating_sub(1),
1797 )
1798 }
1799 };
1800
1801 let mut candidate = TruncationCandidate {
1802 text: format!("{}{affix}{}", &text[..front_end], &text[back_start..]).into(),
1803 runs: Vec::new(),
1804 retained: SmallVec::from_buf([
1805 (0..front_end, 0),
1806 (back_start..text.len(), front_end + affix.len()),
1807 ]),
1808 affix_range: front_end..front_end + affix.len(),
1809 affix_source,
1810 };
1811 let mut offset = 0;
1812 let mut display_runs = Vec::new();
1813
1814 for run in runs {
1815 let source = offset..offset + run.len;
1816 offset = source.end;
1817
1818 for range in candidate.display_ranges(&source) {
1819 display_runs.push((
1820 range.start,
1821 TextRun {
1822 len: range.len(),
1823 ..run.clone()
1824 },
1825 ));
1826 }
1827 }
1828
1829 display_runs.sort_by_key(|(start, _run)| *start);
1830 candidate.runs = display_runs.into_iter().map(|(_start, run)| run).collect();
1831
1832 candidate
1833}
1834
1835#[cfg(test)]
1836mod truncation_tests {
1837 use super::*;
1838
1839 #[test]
1840 fn truncation_candidates_keep_complete_graphemes_and_cover_output_with_runs() {
1841 const FAMILY: &str = "👩👩👧👦";
1842 let text = format!("Ae\u{301}{FAMILY}🦀");
1843 let split = "Ae\u{301}".len();
1844 let runs = [
1845 TextRun {
1846 len: split,
1847 ..Default::default()
1848 },
1849 TextRun {
1850 len: text.len() - split,
1851 ..Default::default()
1852 },
1853 ];
1854 let boundaries = text
1855 .grapheme_indices(true)
1856 .map(|(index, _grapheme)| index)
1857 .chain(std::iter::once(text.len()));
1858 let grapheme_count = boundaries.clone().count().saturating_sub(1);
1859 let boundaries = TruncationBoundaries::new(boundaries, grapheme_count);
1860
1861 for (direction, keep, expected) in [
1862 (TruncateFrom::Start, 0, "…".to_owned()),
1863 (TruncateFrom::End, 0, "…".to_owned()),
1864 (TruncateFrom::Middle, 0, "…".to_owned()),
1865 (TruncateFrom::Start, 1, "…🦀".to_owned()),
1866 (TruncateFrom::Middle, 1, "A…".to_owned()),
1867 (TruncateFrom::Start, 2, format!("…{FAMILY}🦀")),
1868 (TruncateFrom::End, 2, "Ae\u{301}…".to_owned()),
1869 (TruncateFrom::Middle, 3, "Ae\u{301}…🦀".to_owned()),
1870 (TruncateFrom::Start, usize::MAX, format!("…{text}")),
1871 (TruncateFrom::End, usize::MAX, format!("{text}…")),
1872 (
1873 TruncateFrom::Middle,
1874 usize::MAX,
1875 format!("Ae\u{301}{FAMILY}…🦀"),
1876 ),
1877 ] {
1878 let candidate =
1879 make_truncation_candidate(&text, &boundaries, keep, "…", &runs, direction);
1880 assert_eq!(candidate.text.as_ref(), expected, "{direction:?}");
1881 assert_eq!(
1882 candidate.runs.iter().map(|run| run.len).sum::<usize>(),
1883 candidate.text.len(),
1884 "style runs must cover {:?} after {direction:?} truncation",
1885 candidate.text
1886 );
1887 }
1888 }
1889
1890 #[test]
1891 fn measured_truncation_keeps_the_selected_output_and_marker_fallback() {
1892 let text = "Ae\u{301}👩👩👧👦🦀";
1893
1894 for (text, direction, expected) in [
1895 ("", TruncateFrom::Start, "…"),
1896 ("", TruncateFrom::End, "…"),
1897 ("", TruncateFrom::Middle, "…"),
1898 ("🦀", TruncateFrom::Start, "…"),
1899 ("🦀", TruncateFrom::End, "…"),
1900 ("🦀", TruncateFrom::Middle, "…"),
1901 (text, TruncateFrom::Start, "…🦀"),
1902 (text, TruncateFrom::End, "A…"),
1903 (text, TruncateFrom::Middle, "A…"),
1904 ] {
1905 let runs = [TextRun {
1906 len: text.len(),
1907 ..Default::default()
1908 }];
1909 let boundaries = text
1910 .grapheme_indices(true)
1911 .map(|(index, _grapheme)| index)
1912 .chain(std::iter::once(text.len()));
1913
1914 for initial_keep in [None, Some(0), Some(usize::MAX)] {
1915 let (candidate, measured_text) = truncate_with_measured_candidates(
1916 text,
1917 boundaries.clone(),
1918 initial_keep,
1919 "…",
1920 &runs,
1921 direction,
1922 |candidate| {
1923 let fits = candidate.text.graphemes(true).count() <= 2;
1924
1925 (candidate.text.clone(), fits)
1926 },
1927 );
1928 assert_eq!(
1929 candidate.text.as_ref(),
1930 expected,
1931 "{text:?} {direction:?} {initial_keep:?}"
1932 );
1933 assert_eq!(measured_text, candidate.text);
1934 }
1935
1936 let (candidate, measured_text) = truncate_with_measured_candidates(
1937 text,
1938 boundaries,
1939 None,
1940 "…",
1941 &runs,
1942 direction,
1943 |candidate| (candidate.text.clone(), false),
1944 );
1945 assert_eq!(candidate.text.as_ref(), "…");
1946 assert_eq!(measured_text, candidate.text);
1947 }
1948 }
1949}
1950
1951pub struct InteractiveText {
1973 element_id: ElementId,
1974 text: StyledText,
1975 click_listener:
1976 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
1977 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
1978 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
1979 tooltip_id: Option<TooltipId>,
1980 clickable_ranges: Vec<Range<usize>>,
1981}
1982
1983struct InteractiveTextClickEvent {
1984 mouse_down_index: usize,
1985 mouse_up_index: usize,
1986}
1987
1988#[doc(hidden)]
1992#[derive(Default)]
1993pub struct InteractiveTextState {
1994 mouse_down_index: Rc<Cell<Option<usize>>>,
1995 hovered_index: Rc<Cell<Option<usize>>>,
1996 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
1997 long_press_tooltip_active: Rc<Cell<bool>>,
1998}
1999
2000impl InteractiveText {
2001 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
2006 Self {
2007 element_id: id.into(),
2008 text,
2009 click_listener: None,
2010 hover_listener: None,
2011 tooltip_builder: None,
2012 tooltip_id: None,
2013 clickable_ranges: Vec::new(),
2014 }
2015 }
2016
2017 pub fn on_click(
2024 mut self,
2025 ranges: Vec<Range<usize>>,
2026 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
2027 ) -> Self {
2028 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
2029 for (range_ix, range) in ranges.iter().enumerate() {
2030 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
2031 {
2032 listener(range_ix, window, cx);
2033 }
2034 }
2035 }));
2036 self.clickable_ranges = ranges;
2037 self
2038 }
2039
2040 pub fn on_hover(
2047 mut self,
2048 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
2049 ) -> Self {
2050 self.hover_listener = Some(Box::new(listener));
2051 self
2052 }
2053
2054 pub fn tooltip(
2059 mut self,
2060 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
2061 ) -> Self {
2062 self.tooltip_builder = Some(Rc::new(builder));
2063 self
2064 }
2065}
2066
2067impl Element for InteractiveText {
2068 type RequestLayoutState = ();
2069 type PrepaintState = Hitbox;
2070
2071 fn id(&self) -> Option<ElementId> {
2072 Some(self.element_id.clone())
2073 }
2074
2075 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
2076 None
2077 }
2078
2079 fn a11y_role(&self) -> Option<accesskit::Role> {
2080 Some(accesskit::Role::Label)
2081 }
2082
2083 fn write_a11y_info(&self, node: &mut accesskit::Node) {
2084 node.set_value(self.text.text.to_string());
2085 }
2086
2087 fn request_layout(
2088 &mut self,
2089 _id: Option<&GlobalElementId>,
2090 inspector_id: Option<&InspectorElementId>,
2091 window: &mut Window,
2092 cx: &mut App,
2093 ) -> (LayoutId, Self::RequestLayoutState) {
2094 let result = self.text.request_layout(None, inspector_id, window, cx);
2095
2096 window.publish_inline_content(result.0, crate::InlineContent::Atomic);
2098 result
2099 }
2100
2101 fn prepaint(
2102 &mut self,
2103 global_id: Option<&GlobalElementId>,
2104 inspector_id: Option<&InspectorElementId>,
2105 bounds: Bounds<Pixels>,
2106 state: &mut Self::RequestLayoutState,
2107 window: &mut Window,
2108 cx: &mut App,
2109 ) -> Hitbox {
2110 window.with_optional_element_state::<InteractiveTextState, _>(
2111 global_id,
2112 |interactive_state, window| {
2113 let mut interactive_state = interactive_state
2114 .map(|interactive_state| interactive_state.unwrap_or_default());
2115
2116 if let Some(interactive_state) = interactive_state.as_mut() {
2117 if self.tooltip_builder.is_some() {
2118 self.tooltip_id =
2119 set_tooltip_on_window(&interactive_state.active_tooltip, window);
2120 } else {
2121 interactive_state.long_press_tooltip_active.set(false);
2123 interactive_state.active_tooltip.take();
2124 }
2125 }
2126
2127 self.text
2128 .prepaint(None, inspector_id, bounds, state, window, cx);
2129 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2130 (hitbox, interactive_state)
2131 },
2132 )
2133 }
2134
2135 fn paint(
2136 &mut self,
2137 global_id: Option<&GlobalElementId>,
2138 inspector_id: Option<&InspectorElementId>,
2139 bounds: Bounds<Pixels>,
2140 _: &mut Self::RequestLayoutState,
2141 hitbox: &mut Hitbox,
2142 window: &mut Window,
2143 cx: &mut App,
2144 ) {
2145 let current_view = window.current_view();
2146 let text_layout = self.text.layout().clone();
2147 window.with_element_state::<InteractiveTextState, _>(
2148 global_id.unwrap(),
2149 |interactive_state, window| {
2150 let mut interactive_state = interactive_state.unwrap_or_default();
2151 if let Some(click_listener) = self.click_listener.take() {
2152 let mouse_position = window.mouse_position();
2153 if let Ok(ix) = text_layout.index_for_position(mouse_position)
2154 && self
2155 .clickable_ranges
2156 .iter()
2157 .any(|range| range.contains(&ix))
2158 {
2159 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
2160 }
2161
2162 let text_layout = text_layout.clone();
2163 let mouse_down = interactive_state.mouse_down_index.clone();
2164 let current_view = window.current_view();
2165 if let Some(mouse_down_index) = mouse_down.get() {
2166 let hitbox = hitbox.clone();
2167 let clickable_ranges = mem::take(&mut self.clickable_ranges);
2168 window.on_mouse_event(
2169 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
2170 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
2171 if let Ok(mouse_up_index) =
2172 text_layout.index_for_position(event.position)
2173 {
2174 click_listener(
2175 &clickable_ranges,
2176 InteractiveTextClickEvent {
2177 mouse_down_index,
2178 mouse_up_index,
2179 },
2180 window,
2181 cx,
2182 )
2183 }
2184
2185 mouse_down.take();
2186 cx.notify(current_view);
2187 }
2188 },
2189 );
2190 } else {
2191 let hitbox = hitbox.clone();
2192 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
2193 if phase == DispatchPhase::Bubble
2194 && hitbox.is_hovered(window)
2195 && let Ok(mouse_down_index) =
2196 text_layout.index_for_position(event.position)
2197 {
2198 mouse_down.set(Some(mouse_down_index));
2199 cx.notify(current_view);
2200 }
2201 });
2202 }
2203 }
2204
2205 window.on_mouse_event({
2206 let mut hover_listener = self.hover_listener.take();
2207 let hitbox = hitbox.clone();
2208 let text_layout = text_layout.clone();
2209 let hovered_index = interactive_state.hovered_index.clone();
2210 move |event: &MouseMoveEvent, phase, window, cx| {
2211 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
2212 let current = hovered_index.get();
2213 let updated = text_layout.index_for_position(event.position).ok();
2214 if current != updated {
2215 hovered_index.set(updated);
2216 if let Some(hover_listener) = hover_listener.as_ref() {
2217 hover_listener(updated, event.clone(), window, cx);
2218 }
2219 cx.notify(current_view);
2220 }
2221 }
2222 }
2223 });
2224
2225 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
2226 let active_tooltip = interactive_state.active_tooltip.clone();
2227 let build_tooltip = Rc::new({
2228 let tooltip_is_hoverable = false;
2229 let text_layout = text_layout.clone();
2230 move |window: &mut Window, cx: &mut App| {
2231 text_layout
2232 .index_for_position(window.mouse_position())
2233 .ok()
2234 .and_then(|position| tooltip_builder(position, window, cx))
2235 .map(|view| (view, tooltip_is_hoverable))
2236 }
2237 });
2238
2239 let check_is_hovered_during_prepaint = Rc::new({
2241 let source_hitbox = hitbox.clone();
2242 let text_layout = text_layout.clone();
2243 let pending_mouse_down = interactive_state.mouse_down_index.clone();
2244 move |window: &Window| {
2245 text_layout
2246 .index_for_position(window.mouse_position())
2247 .is_ok()
2248 && source_hitbox.contains(&window.mouse_position())
2249 && pending_mouse_down.get().is_none()
2250 }
2251 });
2252
2253 let check_is_hovered = Rc::new({
2254 let hitbox = hitbox.clone();
2255 let text_layout = text_layout.clone();
2256 let pending_mouse_down = interactive_state.mouse_down_index.clone();
2257 move |window: &Window| {
2258 text_layout
2259 .index_for_position(window.mouse_position())
2260 .is_ok()
2261 && hitbox.is_hovered(window)
2262 && pending_mouse_down.get().is_none()
2263 }
2264 });
2265
2266 register_tooltip_mouse_handlers(
2267 &active_tooltip,
2268 self.tooltip_id,
2269 build_tooltip,
2270 check_is_hovered,
2271 check_is_hovered_during_prepaint,
2272 interactive_state.long_press_tooltip_active.clone(),
2273 None,
2274 window,
2275 );
2276 }
2277
2278 self.text
2279 .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
2280
2281 ((), interactive_state)
2282 },
2283 );
2284 }
2285}
2286
2287impl IntoElement for InteractiveText {
2288 type Element = Self;
2289
2290 fn into_element(self) -> Self::Element {
2291 self
2292 }
2293}
2294
2295#[cfg(test)]
2296mod tests {
2297 use super::*;
2298 use crate::{
2299 AvailableSpace, Context, Hsla, Render, ScaledPixels, TestApp, TestAppContext, div, hsla,
2300 prelude::*, size,
2301 };
2302 use std::collections::HashSet;
2303
2304 const CONTAINER_COLOR: Hsla = hsla(0.72, 0.45, 0.32, 1.0);
2305 const TEXT_BACKGROUND_COLOR: Hsla = hsla(0.37, 0.65, 0.42, 1.0);
2306
2307 #[test]
2308 fn wrap_width_of_a_min_content_probe_breaks_at_every_opportunity() {
2309 let space = |width| Size {
2310 width,
2311 height: AvailableSpace::MaxContent,
2312 };
2313 let unknown = Size::default();
2314 let wrap_width = |white_space, known: Size<Option<Pixels>>, space| {
2315 TextLayout::evaluate_wrap_width(&white_space, known, space)
2316 };
2317
2318 assert_eq!(
2319 wrap_width(
2320 WhiteSpace::Normal,
2321 unknown,
2322 space(AvailableSpace::MinContent)
2323 ),
2324 Some(Pixels::ZERO)
2325 );
2326 assert_eq!(
2327 wrap_width(
2328 WhiteSpace::Normal,
2329 unknown,
2330 space(AvailableSpace::MaxContent)
2331 ),
2332 None
2333 );
2334 assert_eq!(
2335 wrap_width(
2336 WhiteSpace::Normal,
2337 unknown,
2338 space(AvailableSpace::Definite(px(80.)))
2339 ),
2340 Some(px(80.))
2341 );
2342 let known = Size {
2343 width: Some(px(50.)),
2344 height: None,
2345 };
2346 assert_eq!(
2347 wrap_width(WhiteSpace::Normal, known, space(AvailableSpace::MinContent)),
2348 Some(px(50.))
2349 );
2350 assert_eq!(
2351 wrap_width(
2352 WhiteSpace::Nowrap,
2353 unknown,
2354 space(AvailableSpace::MinContent)
2355 ),
2356 None
2357 );
2358 }
2359
2360 #[gpui::test]
2361 fn text_measurement_replaces_state_when_wrap_width_changes(cx: &mut TestAppContext) {
2362 let cx = cx.add_empty_window();
2363
2364 cx.draw(Point::default(), size(px(240.), px(100.)), |window, cx| {
2365 let layout = TextLayout::default();
2366 let text = SharedString::from("Measure café over multiple words");
2367
2368 for (width, cached) in [(240., false), (240., true), (80., false)] {
2369 let previous_bounds = layout
2370 .0
2371 .layout
2372 .borrow()
2373 .as_ref()
2374 .and_then(|state| state.bounds);
2375
2376 let previous_document = layout
2377 .0
2378 .layout
2379 .borrow()
2380 .as_ref()
2381 .and_then(|state| state.document.as_ref())
2382 .map(|document| document.layout.clone());
2383
2384 let layout_id = layout.layout(text.clone(), None, window, cx);
2385
2386 window.compute_layout(
2387 layout_id,
2388 size(
2389 AvailableSpace::Definite(px(width)),
2390 AvailableSpace::MaxContent,
2391 ),
2392 cx,
2393 );
2394
2395 let bounds = window.layout_bounds(layout_id);
2396
2397 {
2398 let state = layout.0.layout.borrow();
2399 let state = state.as_ref().unwrap();
2400 let document = state.document.as_ref().unwrap();
2401
2402 assert_eq!(document.text, text);
2403 assert_eq!(state.len, text.len());
2404 assert_eq!(state.size, bounds.size);
2405 assert_eq!(state.size, document.size(state.line_height));
2406 assert_eq!(state.options.wrap_width, Some(px(width)));
2407 assert_eq!(state.truncate_width, None);
2408 assert_eq!(state.bounds, if cached { previous_bounds } else { None });
2409
2410 if let Some(previous_document) = previous_document {
2411 assert_eq!(Arc::ptr_eq(&document.layout, &previous_document), cached);
2412 }
2413 }
2414
2415 layout.prepaint(bounds, &text, window);
2416
2417 assert_eq!(layout.bounds(), bounds);
2418 }
2419
2420 div()
2421 });
2422 }
2423
2424 struct CenteredTextView {
2425 extent: f32,
2426 }
2427
2428 impl Render for CenteredTextView {
2429 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2430 div()
2431 .flex()
2432 .items_center()
2433 .justify_center()
2434 .w(px(320.0 + self.extent))
2435 .h(px(160.0 + self.extent))
2436 .child(
2437 div()
2438 .flex()
2439 .items_center()
2440 .justify_center()
2441 .w(px(118.0))
2442 .h(px(31.0))
2443 .bg(CONTAINER_COLOR)
2444 .text_size(px(14.0))
2445 .child(StyledText::new("x").with_highlights([(
2446 0..1,
2447 HighlightStyle {
2448 background_color: Some(TEXT_BACKGROUND_COLOR),
2449 ..Default::default()
2450 },
2451 )])),
2452 )
2453 }
2454 }
2455
2456 fn only_quad(window: &Window, color: Hsla) -> Bounds<ScaledPixels> {
2457 let color = color.into();
2458 let mut bounds = window
2459 .rendered_frame
2460 .scene
2461 .quads
2462 .iter()
2463 .filter(|quad| quad.background.solid == color)
2464 .map(|quad| quad.bounds);
2465 let result = bounds.next().expect("expected a rendered quad");
2466 assert!(bounds.next().is_none(), "expected only one rendered quad");
2467 result
2468 }
2469
2470 #[test]
2471 fn test_into_element_for() {
2472 use crate::{ParentElement as _, SharedString, div};
2473 use std::borrow::Cow;
2474
2475 let _ = div().child("static str");
2476 let _ = div().child("String".to_string());
2477 let _ = div().child(Cow::Borrowed("Cow"));
2478 let _ = div().child(SharedString::from("SharedString"));
2479 }
2480
2481 #[test]
2482 fn text_macro_id() {
2483 fn make_text_stable_id(happy: bool) -> Text {
2485 text!(if happy { "happy" } else { "sad" })
2486 }
2487
2488 fn make_text_unstable_id(happy: bool) -> Text {
2490 if happy { text!("happy") } else { text!("sad") }
2491 }
2492
2493 assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
2494 assert_ne!(
2495 make_text_unstable_id(false).id,
2496 make_text_unstable_id(true).id
2497 );
2498 }
2499
2500 #[test]
2501 fn accessible_text_keeps_its_unicode_value_and_stable_identity() {
2502 let first = Text::new("status".into(), "Ready العربية 👩🏽💻".into());
2503 let second = Text::new("status".into(), "Done 日本語 ✅".into());
2504 assert_eq!(first.id(), second.id());
2505 assert_eq!(first.a11y_role(), Some(accesskit::Role::Label));
2506
2507 let mut node = accesskit::Node::new(accesskit::Role::Label);
2508 second.write_a11y_info(&mut node);
2509 assert_eq!(node.value(), Some("Done 日本語 ✅"));
2510
2511 let hidden = Text::new_inaccessible("decorative 👀".into());
2512 assert_eq!(hidden.a11y_role(), None);
2513 }
2514
2515 #[test]
2516 fn centered_text_keeps_its_device_pixel_offset_when_its_parent_moves() {
2517 for scale_factor in [1.0, 1.5] {
2518 let mut app = TestApp::new();
2519 let mut test_window = app.open_window(|window, _| {
2520 window.set_scale_factor(scale_factor);
2521 CenteredTextView { extent: 0.0 }
2522 });
2523 test_window.draw();
2524
2525 let (initial_container, initial_background) = test_window.update(|_, window, _| {
2526 (
2527 only_quad(window, CONTAINER_COLOR),
2528 only_quad(window, TEXT_BACKGROUND_COLOR),
2529 )
2530 });
2531 let expected_offset = initial_background.origin - initial_container.origin;
2532 let mut container_origins = HashSet::from([(
2533 initial_container.origin.x.as_f32() as i32,
2534 initial_container.origin.y.as_f32() as i32,
2535 )]);
2536
2537 for step in 1..=32 {
2538 test_window.update(|view, _, cx| {
2539 view.extent = step as f32;
2540 cx.notify();
2541 });
2542 test_window.draw();
2543
2544 let (container, background) = test_window.update(|_, window, _| {
2545 (
2546 only_quad(window, CONTAINER_COLOR),
2547 only_quad(window, TEXT_BACKGROUND_COLOR),
2548 )
2549 });
2550 assert_eq!(
2551 background.origin - container.origin,
2552 expected_offset,
2553 "text moved within its parent at scale {scale_factor}, step {step}"
2554 );
2555 container_origins.insert((
2556 container.origin.x.as_f32() as i32,
2557 container.origin.y.as_f32() as i32,
2558 ));
2559 }
2560
2561 assert!(
2562 container_origins.len() > 8,
2563 "fixture did not cross enough device pixels at scale {scale_factor}"
2564 );
2565 }
2566 }
2567}