Skip to main content

gpui/elements/
text.rs

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/// An [`Element`] that lays out and paints one text string.
23///
24/// In general, [`Text`] objects should be created via the [`text!`](crate::text!) macro:
25/// ```rust
26/// # use gpui::*;
27/// # fn render() -> impl IntoElement {
28/// div().child(text!("hello"))
29/// # }
30/// ```
31/// # IDs and accessibility
32///
33/// A [`Text`] id identifies its node in the accessibility tree. Screen readers use that node to
34/// read the text and to observe later changes.
35///
36/// Keep the id stable when the text represents the same piece of content. With the same id,
37/// AccessKit reports a text update; with a new id, it reports a removed node and a new node.
38///
39/// When using the [`text!`](crate::text!) macro, each invocation of the macro will get a
40/// unique ID, derived from its position in the source code (filename, line, and
41/// column). For example:
42/// ```rust
43/// # use gpui::*;
44/// let x = text!("hello");
45/// let y = text!("hello");
46/// // not equal, because different `text!` invocations produced them
47/// assert_ne!(x.id(), y.id());
48///
49/// fn make_text(s: &str) -> Text { text!(s) }
50/// let x = make_text("hello");
51/// let y = make_text("hello");
52/// // equal, because the same `text!` invocation produced them
53/// assert_eq!(x.id(), y.id());
54/// ```
55/// When the contents do not change, the distinction usually does not matter, but duplicate ids
56/// still produce ambiguous accessibility nodes.
57///
58/// When the argument changes, choose whether that is an update to the existing node or a new
59/// piece of content, and keep or change the id accordingly.
60#[derive(Debug, Clone)]
61pub struct Text {
62    id: Option<ElementId>,
63    text: SharedString,
64}
65
66impl Text {
67    /// Create a new [`Text`] element with a specific ID.
68    ///
69    /// If you want a unique ID to be assigned automatically, use the [`text!`](crate::text!)
70    /// macro. The docs for [`Text`] have more detail about choosing IDs.
71    #[inline]
72    pub const fn new(id: ElementId, text: SharedString) -> Self {
73        Self { id: Some(id), text }
74    }
75
76    /// Create a new [`Text`] element that is inaccessible to screen readers.
77    ///
78    /// Text without an id has no accessibility node. Use [`text!`](crate::text!) for a generated
79    /// id or [`Text::new`] to choose one.
80    ///
81    /// Use it for decorative text or text whose parent supplies the accessible representation.
82    #[inline]
83    pub const fn new_inaccessible(text: SharedString) -> Self {
84        Self { id: None, text }
85    }
86
87    /// The ID of this [`Text`] element.
88    #[inline]
89    pub const fn id(&self) -> Option<&ElementId> {
90        self.id.as_ref()
91    }
92
93    /// Produce a new [`Text`] with the given `id`.
94    pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
95        self.id = Some(id.into());
96        self
97    }
98
99    /// The text that this [`Text`] element will display.
100    #[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/// Hashes the source location that the [`text`] macro records.
120///
121/// This is an implementation detail of the macro. It is not covered by semver
122/// guarantees, so don't call it directly. Performance doesn't matter because it
123/// runs only on short strings in const contexts.
124#[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/// Creates a [`Text`] element that has an ID, so that screen readers can read it.
141///
142/// Without `id = ...`, the macro derives the ID from the file, line, and column
143/// of the call. Two calls on different lines get different IDs, and the same
144/// call produces the same ID every time. With `id = ...`, you choose the ID,
145/// which is useful when the text must keep its ID even if the code moves.
146///
147/// Use [`Text::new_inaccessible`] to skip the accessibility node.
148///
149/// # Examples
150///
151/// ```rust
152/// # use gpui::*;
153/// let a = text!("hello");
154/// let b = text!(id = "farewell-message", "hello");
155/// assert_ne!(a.id(), b.id());
156/// ```
157///
158/// See the docs for [`Text`] for how the ID affects accessibility updates.
159#[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
386/// Text that is laid out and painted with a different style for each run.
387///
388/// Use `StyledText` when part of the text needs its own color, weight, font, or
389/// decoration. Pass the text to [`StyledText::new`], then add highlights or
390/// explicit runs. Text with a single style doesn't need this type. Pass a
391/// `&str` or [`SharedString`] to the element tree directly.
392///
393/// `StyledText` wraps to the width it is given and paints each run with its own
394/// [`TextRun`]. Read the laid-out text with [`StyledText::layout`], which returns a
395/// [`TextLayout`] for hit testing and measurement.
396///
397/// # Examples
398///
399/// ```
400/// # use gpui::*;
401/// # fn render() -> impl IntoElement {
402/// StyledText::new("Warning: disk almost full").with_highlights([(
403///     0..8,
404///     HighlightStyle {
405///         color: Some(hsla(0., 0.8, 0.5, 1.)),
406///         font_weight: Some(FontWeight::BOLD),
407///         ..Default::default()
408///     },
409/// )])
410/// # }
411/// ```
412///
413/// See [text](crate::_documentation::guides::text) for an overview of text in GPUI.
414pub 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    /// Creates a styled text element for `text`, with no styling beyond the inherited text style.
424    ///
425    /// Add styling with [`StyledText::with_highlights`], [`StyledText::with_default_highlights`],
426    /// [`StyledText::with_runs`], or [`StyledText::with_font_family_overrides`].
427    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    /// Returns the layout of this element, for mapping between byte indices and pixels.
438    ///
439    /// The layout is filled in while the element is laid out and prepainted. Its query
440    /// methods panic if called before that, so call them from event handlers or after
441    /// the frame has been drawn. [`TextLayout`] is cheap to clone and shares its state.
442    pub fn layout(&self) -> &TextLayout {
443        &self.layout
444    }
445
446    /// Styles the text with `default_style`, with `highlights` applied over it.
447    ///
448    /// The runs are computed now, from `default_style`. Use this when you have a
449    /// [`TextStyle`] in hand. Otherwise use [`StyledText::with_highlights`], which
450    /// takes the inherited style at layout time.
451    ///
452    /// Each range must lie on character boundaries inside the text. The ranges must
453    /// be sorted by start and must not overlap.
454    ///
455    /// # Panics
456    ///
457    /// Panics in debug builds if a range is outside the text, or if this element
458    /// already has highlights from [`StyledText::with_highlights`].
459    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    /// Applies `highlights` over the inherited text style of the element.
473    ///
474    /// Each highlight is a byte range and a [`HighlightStyle`]. The inherited style
475    /// is read from the window when the element is laid out, so a highlight changes
476    /// only the properties it sets. Text outside every range keeps the inherited style.
477    ///
478    /// The ranges must be sorted by start, must not overlap, and must lie on
479    /// character boundaries. Debug builds check that each range is inside the text.
480    /// Release builds don't check, and unsorted ranges give wrong layouts or panic.
481    ///
482    /// # Examples
483    ///
484    /// ```
485    /// # use gpui::*;
486    /// # fn render() -> impl IntoElement {
487    /// StyledText::new("Press Enter to save").with_highlights([(
488    ///     6..11,
489    ///     HighlightStyle {
490    ///         font_weight: Some(FontWeight::BOLD),
491    ///         ..Default::default()
492    ///     },
493    /// )])
494    /// # }
495    /// ```
496    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    /// Sets the font family for byte ranges of the text.
543    ///
544    /// The family is applied at layout time, on top of the inherited style. An
545    /// override changes the family of a run only when that run lies entirely
546    /// inside the override's range. Runs are split only at highlight boundaries,
547    /// so to make an override cover part of the text, add a highlight at each
548    /// edge of the range. Can be combined with [`with_highlights`](Self::with_highlights).
549    ///
550    /// The overrides must be sorted by range start and non-overlapping.
551    /// Each override range must fall on character boundaries.
552    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    /// Sets the runs that style the text. Each run gives its byte length and its [`TextRun`].
605    ///
606    /// The run lengths must add up to the length of the text exactly, and each run
607    /// must end on a character boundary.
608    ///
609    /// # Panics
610    ///
611    /// Panics if the runs don't cover the text exactly, or if a run ends inside a
612    /// character. In debug builds the message includes the text and the run.
613    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/// The laid-out state of a text element, for mapping between byte indices and pixels.
690///
691/// Get one from [`StyledText::layout`], or from the text state that other text
692/// elements keep. Clones share the same state. The query methods read the most
693/// recent layout, so a query made after a relayout returns the new result.
694///
695/// The text elements fill this in during layout and prepaint. The query methods
696/// panic if the element hasn't been laid out yet, and the ones that need pixel
697/// positions panic if it hasn't been prepainted.
698#[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/// The part of an overflowing line that is removed and replaced by the affix.
853///
854/// The variants correspond to [`TextOverflow::Truncate`], [`TextOverflow::TruncateStart`],
855/// and [`TextOverflow::TruncateMiddle`].
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
857pub enum TruncateFrom {
858    /// Remove text from the start, keeping the end visible.
859    Start,
860    /// Remove text from the end, keeping the start visible.
861    End,
862    /// Remove text from the middle, keeping both ends visible.
863    Middle,
864}
865
866/// How a text element should truncate its text to fit, computed during layout.
867///
868/// [`TextLayout::evaluate_overflow`] creates this from the [`TextStyle`]'s
869/// `text_overflow`. Pass it to [`TextLayout::apply_truncation`] to get the
870/// shortened text.
871pub struct TextLayoutTruncation {
872    /// The width that each visual line can occupy before it is truncated, or `None` for no limit.
873    pub width: Option<Pixels>,
874    /// The text to show in place of the removed text, such as `"…"`.
875    pub affix: SharedString,
876    /// The side of the text that is removed when it doesn't fit.
877    pub source: TruncateFrom,
878}
879
880impl TextLayoutTruncation {
881    /// Creates a truncation by using the overflow as the affix, given the provided width.
882    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    /// Returns the width that text wraps at, or `None` when it doesn't wrap.
899    ///
900    /// Text does not wrap when `white_space` is [`WhiteSpace::Nowrap`]. Otherwise
901    /// the width comes from [`TextLayout::evaluate_alignment_width`].
902    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                // Wrap at every opportunity, so the min-content width is the longest unbreakable run.
912                crate::AvailableSpace::MinContent => Some(Pixels::ZERO),
913                crate::AvailableSpace::MaxContent => None,
914            }),
915        }
916    }
917
918    /// Returns the width that text aligns within, from the known or available width.
919    ///
920    /// The known width wins. Otherwise a definite available width is used. Under
921    /// min-content or max-content sizing, this returns `None`.
922    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    /// Returns how the text should be truncated, from the style's `text_overflow`.
933    ///
934    /// When the style has no `text_overflow`, the result has no width and an empty affix,
935    /// so the text is never truncated.
936    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                // Overflow is checked against each visual row's available width.
944                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    /// Shortens `text` so that it fits `truncation.width`, and returns the text to display.
985    ///
986    /// When `truncation.width` is `None`, the text and runs come back unchanged. Otherwise
987    /// the removed part is replaced by `truncation.affix`, on the side given by
988    /// `truncation.source`. Wrapping and `line_clamp` from `text_style` still apply.
989    /// The returned runs match the returned text. They borrow `runs` when nothing changes.
990    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        /// What the text's size depends on, besides the space it gets, and its layout.
1030        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                // Only use cached layout if:
1131                // 1. truncate_width is None (if truncate_width is Some, we need to re-layout
1132                //    because the previous layout may have been computed without truncation)
1133                // 2. the cached layout was not truncated (a truncated layout answers an
1134                //    unconstrained probe with the truncated size, which poisons intrinsic
1135                //    sizing with whatever width some earlier measure pass happened to use)
1136                // 3. the complete layout options match.
1137                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    /// Paints the backgrounds of the text runs without the glyphs, so callers can paint
1225    /// content such as highlights between the backgrounds and text.
1226    pub fn paint_background(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
1227        self.paint_lines(TextPaintPass::Background, window, cx)
1228    }
1229
1230    /// Paints the glyphs and decorations without the run backgrounds.
1231    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    /// Returns the UTF-8 byte index of the character at `pixel_point`, in window coordinates.
1286    ///
1287    /// Returns `Ok` with the index of the character under the point. Returns `Err`
1288    /// with `0` when the point is above the text. Points to the side of or below the
1289    /// text return `Err` with the closest index. Use this for hit testing, such as
1290    /// placing a caret on click.
1291    ///
1292    /// # Panics
1293    ///
1294    /// Panics if the element hasn't been laid out or prepainted.
1295    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    /// Returns the top-left corner of the caret for `byte_index`, in window coordinates.
1314    ///
1315    /// Returns `None` if the text has no shaped layout, or if `byte_index` is past the end
1316    /// of the text. Use this to place a caret or an overlay at a position in the text.
1317    ///
1318    /// # Panics
1319    ///
1320    /// Panics if the element hasn't been laid out or prepainted.
1321    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    /// Returns the shaped layout of the text, or `None` if `index` is past the end or the
1333    /// text has not been shaped.
1334    ///
1335    /// The layout covers the whole text, not one visual line or paragraph.
1336    /// Use [`TextLayout::line_layouts`] to get every layout in source order.
1337    ///
1338    /// # Panics
1339    ///
1340    /// Panics if the element hasn't been laid out.
1341    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    /// Returns the shaped layout of the text as a collection with one entry.
1348    ///
1349    /// This returns an empty collection if the text has not been shaped.
1350    ///
1351    /// # Panics
1352    ///
1353    /// Panics if the element hasn't been laid out.
1354    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    /// Returns the window bounds of the element, as set during prepaint.
1363    ///
1364    /// # Panics
1365    ///
1366    /// Panics if the element hasn't been prepainted.
1367    pub fn bounds(&self) -> Bounds<Pixels> {
1368        self.measured().bounds.unwrap()
1369    }
1370
1371    /// Returns the line height that the text is laid out with, in logical pixels.
1372    ///
1373    /// # Panics
1374    ///
1375    /// Panics if the element hasn't been laid out.
1376    pub fn line_height(&self) -> Pixels {
1377        self.measured().line_height
1378    }
1379
1380    /// Returns the length of the shaped text in UTF-8 bytes.
1381    ///
1382    /// This is the length after any text transform or truncation. It is `0`
1383    /// when the text has not been shaped.
1384    ///
1385    /// # Panics
1386    ///
1387    /// Panics if the element hasn't been laid out.
1388    pub fn len(&self) -> usize {
1389        self.measured().len
1390    }
1391
1392    /// Returns the text that was shaped, as a `String`.
1393    ///
1394    /// This is the text after any [`TextTransform`] and truncation. A truncated
1395    /// text includes the affix, such as an ellipsis. It returns an empty string if
1396    /// the text has not been shaped.
1397    ///
1398    /// # Panics
1399    ///
1400    /// Panics if the element hasn't been laid out.
1401    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    /// Returns the shaped text with each visual line followed by a `\n`, except the last.
1409    ///
1410    /// Soft wraps therefore become `\n`. A hard line break keeps its own `\n` as part of
1411    /// its line, so it is followed by one more `\n`. Use this to copy wrapped text as it
1412    /// appears on screen.
1413    ///
1414    /// # Panics
1415    ///
1416    /// Panics if the element hasn't been laid out.
1417    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        // Remove trailing newline
1428        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
1629/// Keeps independent prefix and suffix iterators because middle truncation moves
1630/// their cuts in opposite directions. Narrowing them with the candidate search
1631/// limits rescanning without collecting offsets into a vector. Bounds remain
1632/// inclusive so adjacent middle candidates can reuse the same cut.
1633struct 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
1721/// Returns the largest fitting candidate with its measured output, or the marker-only
1722/// candidate when none fits. The starting `keep` is only an estimate; a candidate that
1723/// doesn't fit narrows the search.
1724pub(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            // Preserve the requested marker when even the marker alone is too wide.
1751            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
1951/// A [`StyledText`] that reports clicks, hovers, and tooltips for character ranges.
1952///
1953/// Give it an ID, so that GPUI can track the mouse between frames. Then attach
1954/// the behavior you need: [`InteractiveText::on_click`] for ranges of text,
1955/// [`InteractiveText::on_hover`] for the character under the mouse, and
1956/// [`InteractiveText::tooltip`] for a tooltip per character.
1957///
1958/// # Examples
1959///
1960/// ```
1961/// # use gpui::*;
1962/// # fn render() -> impl IntoElement {
1963/// InteractiveText::new(
1964///     "terms",
1965///     StyledText::new("Read the terms or skip"),
1966/// )
1967/// .on_click(vec![9..14], |_range_index, _window, _cx| {
1968///     // Open the terms here.
1969/// })
1970/// # }
1971/// ```
1972pub 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/// The per-element state that [`InteractiveText`] keeps between frames.
1989///
1990/// This is an implementation detail. Application code doesn't use it.
1991#[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    /// Creates an interactive text element with the given ID and styled text.
2002    ///
2003    /// The ID must be unique among siblings. It keys the mouse state that
2004    /// survives between frames.
2005    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    /// Calls `listener` when the user presses and releases the mouse inside one of `ranges`.
2018    ///
2019    /// The listener receives the index in `ranges` of the matching range. The press
2020    /// and the release must both fall inside the same range. A press in one range
2021    /// and a release in another calls nothing. If the ranges overlap, the listener
2022    /// runs once for each range that contains both points.
2023    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    /// Calls `listener` as the mouse moves over the text.
2041    ///
2042    /// The listener receives the byte index of the character under the mouse, or
2043    /// `None` when the mouse is over the element but not over a character. It runs only
2044    /// when that value changes, and not when the mouse leaves the element. It also
2045    /// receives the [`MouseMoveEvent`].
2046    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    /// Sets a tooltip for each character, as chosen by `builder`.
2055    ///
2056    /// `builder` receives the byte index of the character under the mouse. It returns
2057    /// the view to show as the tooltip, or `None` for no tooltip at that character.
2058    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        // InteractiveText owns range-based selection and hit testing in its independent layout.
2097        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                        // If there is no longer a tooltip builder, remove the active tooltip.
2122                        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                    // Check hitbox geometry directly because hover state is unavailable during prepaint.
2240                    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        // one call to `text!` = one id
2484        fn make_text_stable_id(happy: bool) -> Text {
2485            text!(if happy { "happy" } else { "sad" })
2486        }
2487
2488        // two calls to `text!` = two ids
2489        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}