Skip to main content

gpui_base/text/
stream_fade.rs

1//! Fades streamed text in: the rendered characters an update adds start
2//! transparent and reach full color over [`TextViewMotion::stream_fade`],
3//! as one chunk, or word by word (character by character for CJK) when
4//! [`TextViewMotion::stream_fade_stagger`] separates them. An update too
5//! large to light up within one fade goes back to fading as one chunk -- see
6//! [`TextViewMotion::stagger_step`].
7//!
8//! The tracker compares rendered text, not source, so `**bo` completing into
9//! bold `bold` fades the changed glyphs rather than mapping source bytes.
10
11#[cfg(not(target_family = "wasm"))]
12use std::time::Instant;
13use std::{ops::Range, sync::Arc, time::Duration};
14#[cfg(target_family = "wasm")]
15use web_time::Instant;
16
17use gpui::{ElementId, SharedString};
18
19use super::{
20    document::ParsedDocument,
21    node::{BlockNode, InlineNode, Paragraph},
22};
23use crate::motion::{Easing, Timing};
24
25/// Motion policy of a text view. Base plays it; every duration defaults to
26/// zero, so an unstyled view adopts new content at once.
27#[derive(Clone, Debug)]
28pub struct TextViewMotion {
29    stream_fade: Duration,
30    stream_fade_stagger: Duration,
31    stream_fade_easing: Easing,
32}
33
34impl Default for TextViewMotion {
35    fn default() -> Self {
36        Self {
37            stream_fade: Duration::ZERO,
38            stream_fade_stagger: Duration::ZERO,
39            stream_fade_easing: Easing::default(),
40        }
41    }
42}
43
44impl TextViewMotion {
45    /// How long the text an update appends takes to reach full color.
46    pub fn with_stream_fade(mut self, duration: Duration) -> Self {
47        self.stream_fade = duration;
48        self
49    }
50
51    /// How much later each further word of one update starts fading than
52    /// the word before it; zero fades the update as one chunk. A long update
53    /// is compressed so its last word starts within one [`Self::stream_fade`].
54    pub fn with_stream_fade_stagger(mut self, stagger: Duration) -> Self {
55        self.stream_fade_stagger = stagger;
56        self
57    }
58
59    /// The curve the appended text fades in along.
60    pub fn with_stream_fade_easing(mut self, easing: Easing) -> Self {
61        self.stream_fade_easing = easing;
62        self
63    }
64
65    pub fn stream_fade(&self) -> Duration {
66        self.stream_fade
67    }
68
69    pub fn stream_fade_stagger(&self) -> Duration {
70        self.stream_fade_stagger
71    }
72
73    pub fn stream_fade_easing(&self) -> &Easing {
74        &self.stream_fade_easing
75    }
76
77    /// The start offset between consecutive words of an update `words` long: the stagger as
78    /// asked, or nothing.
79    ///
80    /// Staggering only reads as words arriving one after another while the whole update lights
81    /// up well within one [`Self::stream_fade`]. Once the last word would start later than
82    /// that, what is left is a sweep drawn across text that appeared at once -- and an update
83    /// that large (`stream_fade / stream_fade_stagger` words and up) was not typed anyway.
84    /// Squeezing the step to fit only makes the sweep faster, so drop it instead and let the
85    /// update fade as one chunk.
86    fn stagger_step(&self, words: usize) -> Duration {
87        if words < 2 {
88            return Duration::ZERO;
89        }
90        let span = self.stream_fade_stagger.as_nanos() * (words as u128 - 1);
91        if span > self.stream_fade.as_nanos() {
92            return Duration::ZERO;
93        }
94        self.stream_fade_stagger
95    }
96}
97
98/// Identifies one run of rendered text across re-parses: the source start of
99/// the block that owns it, plus the cell ordinal inside a table.
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub(crate) struct TextLeafKey {
102    block_start: usize,
103    ordinal: usize,
104}
105
106/// The key as an element id, for a leaf that needs element state or an
107/// accessibility identity: unique per leaf, and allocation-free, as it is
108/// built every frame.
109impl From<TextLeafKey> for ElementId {
110    fn from(key: TextLeafKey) -> Self {
111        let mut bytes = [0; 20];
112        bytes[..8].copy_from_slice(&(key.block_start as u64).to_le_bytes());
113        bytes[8..16].copy_from_slice(&(key.ordinal as u64).to_le_bytes());
114        ElementId::OpaqueId(bytes)
115    }
116}
117
118impl TextLeafKey {
119    pub(crate) fn block(start: usize) -> Self {
120        Self {
121            block_start: start,
122            ordinal: 0,
123        }
124    }
125
126    pub(crate) fn table_cell(table_start: usize, ordinal: usize) -> Self {
127        Self {
128            block_start: table_start,
129            ordinal: ordinal + 1,
130        }
131    }
132}
133
134/// Rendered byte ranges with the [`gpui::HighlightStyle::fade_out`] factor
135/// each one paints with this frame: `1.0` transparent, `0.0` opaque.
136pub(crate) type FadeRanges = Vec<(Range<usize>, f32)>;
137
138/// One frame's fade factors, resolved once per render so node rendering only
139/// looks up its leaf.
140#[derive(Debug, Default)]
141pub(crate) struct StreamFadeFrame {
142    leaves: Vec<(TextLeafKey, FadeRanges)>,
143}
144
145impl StreamFadeFrame {
146    pub(crate) fn fades(&self, key: TextLeafKey) -> Option<&[(Range<usize>, f32)]> {
147        self.leaves
148            .iter()
149            .find(|(leaf, _)| *leaf == key)
150            .map(|(_, fades)| fades.as_slice())
151    }
152}
153
154struct FadeSegment {
155    range: Range<usize>,
156    started_at: Instant,
157}
158
159#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
160enum PendingUpdate {
161    #[default]
162    None,
163    /// Every update since the last parse extended the text, which was
164    /// `origin` bytes long before the first of them.
165    Extend {
166        origin: usize,
167    },
168    Replace,
169}
170
171/// Tracks which rendered text arrived recently and samples its fade.
172#[derive(Default)]
173pub(super) struct StreamFadeTracker {
174    motion: TextViewMotion,
175    pending: PendingUpdate,
176    segments: Vec<(TextLeafKey, Vec<FadeSegment>)>,
177}
178
179impl StreamFadeTracker {
180    pub(super) fn set_motion(&mut self, motion: TextViewMotion) {
181        if motion.stream_fade.is_zero() {
182            self.segments.clear();
183            self.pending = PendingUpdate::None;
184        }
185        self.motion = motion;
186    }
187
188    pub(super) fn is_enabled(&self) -> bool {
189        !self.motion.stream_fade.is_zero()
190    }
191
192    /// The next update extends the current text, which is `len` bytes long.
193    pub(super) fn note_extend(&mut self, len: usize) {
194        if !self.is_enabled() {
195            return;
196        }
197        self.pending = match self.pending {
198            PendingUpdate::None => PendingUpdate::Extend { origin: len },
199            PendingUpdate::Extend { origin } => PendingUpdate::Extend {
200                origin: origin.min(len),
201            },
202            PendingUpdate::Replace => PendingUpdate::Replace,
203        };
204    }
205
206    /// The next update replaces the current text.
207    pub(super) fn note_replace(&mut self) {
208        if self.is_enabled() {
209            self.pending = PendingUpdate::Replace;
210        }
211    }
212
213    /// Forgets the noted update after its parse failed.
214    pub(super) fn discard_pending(&mut self) {
215        self.pending = PendingUpdate::None;
216    }
217
218    /// Records what `new` renders that `old` did not, for the updates noted
219    /// since the last call, as segments that start fading at `now`.
220    ///
221    /// Only blocks the extension reaches are compared, walking both documents
222    /// from the tail, so a chunk landing at the end of a long document costs
223    /// one paragraph, not the document.
224    pub(super) fn record(&mut self, old: &ParsedDocument, new: &ParsedDocument, now: Instant) {
225        let pending = std::mem::take(&mut self.pending);
226        if !self.is_enabled() {
227            self.segments.clear();
228            return;
229        }
230        let origin = match pending {
231            PendingUpdate::None => return,
232            PendingUpdate::Replace => {
233                self.segments.clear();
234                return;
235            }
236            PendingUpdate::Extend { origin } => origin,
237        };
238
239        let mut affected = Vec::new();
240        for block in new.blocks.iter().rev() {
241            if block.span().is_some_and(|span| span.end <= origin) {
242                break;
243            }
244            text_leaves(block, &mut affected);
245        }
246        let Some(first_start) = affected.iter().map(|(key, _)| key.block_start).min() else {
247            return;
248        };
249
250        let mut previous = Vec::new();
251        for block in old.blocks.iter().rev() {
252            if block.span().is_some_and(|span| span.end < first_start) {
253                break;
254            }
255            text_leaves(block, &mut previous);
256        }
257
258        for (key, leaf) in affected {
259            let len = leaf.len();
260            let prefix = previous
261                .iter()
262                .find(|(previous_key, _)| *previous_key == key)
263                .map_or(0, |(_, old_leaf)| leaf.common_prefix_len(old_leaf));
264            let segments = match self.segments.iter().position(|(k, _)| *k == key) {
265                Some(ix) => &mut self.segments[ix].1,
266                None => {
267                    self.segments.push((key, Vec::new()));
268                    &mut self.segments.last_mut().expect("just pushed").1
269                }
270            };
271            // Text past the divergence is repainted, so its earlier fade no
272            // longer describes what is on screen.
273            segments.retain_mut(|segment| {
274                segment.range.end = segment.range.end.min(prefix);
275                segment.range.start < segment.range.end
276            });
277            if prefix >= len {
278                continue;
279            }
280            if self.motion.stream_fade_stagger.is_zero() {
281                segments.push(FadeSegment {
282                    range: prefix..len,
283                    started_at: now,
284                });
285                continue;
286            }
287            let words = fade_units(leaf.chunks(), prefix, len);
288            let step = self.motion.stagger_step(words.len());
289            for (ix, range) in words.into_iter().enumerate() {
290                segments.push(FadeSegment {
291                    range,
292                    started_at: now + step * ix as u32,
293                });
294            }
295        }
296        self.segments.retain(|(_, segments)| !segments.is_empty());
297    }
298
299    /// Samples every unfinished segment at `now`, dropping the finished ones.
300    /// `None` means nothing is fading, so no frame needs to follow.
301    pub(super) fn frame(
302        &mut self,
303        now: Instant,
304        reduce_motion: bool,
305    ) -> Option<Arc<StreamFadeFrame>> {
306        if self.segments.is_empty() {
307            return None;
308        }
309        if reduce_motion || !self.is_enabled() {
310            self.segments.clear();
311            return None;
312        }
313        let timing =
314            Timing::new(self.motion.stream_fade).ease(self.motion.stream_fade_easing.clone());
315        let mut leaves = Vec::with_capacity(self.segments.len());
316        self.segments.retain_mut(|(key, segments)| {
317            let mut fades = Vec::with_capacity(segments.len());
318            segments.retain(|segment| {
319                let sample = timing.sample(now.saturating_duration_since(segment.started_at));
320                if sample.finished {
321                    return false;
322                }
323                let fade_out = (1.0 - sample.directed_progress).clamp(0.0, 1.0);
324                fades.push((segment.range.clone(), fade_out));
325                true
326            });
327            if fades.is_empty() {
328                return false;
329            }
330            leaves.push((*key, fades));
331            true
332        });
333        (!leaves.is_empty()).then(|| Arc::new(StreamFadeFrame { leaves }))
334    }
335}
336
337/// A block's rendered text, in the byte space its highlights use.
338enum TextLeaf<'a> {
339    Paragraph(&'a Paragraph),
340    Code(SharedString),
341}
342
343enum Chunks<'a> {
344    Paragraph(std::slice::Iter<'a, InlineNode>),
345    Code(Option<&'a str>),
346}
347
348impl<'a> Iterator for Chunks<'a> {
349    type Item = &'a str;
350
351    fn next(&mut self) -> Option<&'a str> {
352        match self {
353            Self::Paragraph(nodes) => nodes.next().map(|node| node.text.as_ref()),
354            Self::Code(code) => code.take(),
355        }
356    }
357}
358
359impl TextLeaf<'_> {
360    fn chunks(&self) -> Chunks<'_> {
361        match self {
362            Self::Paragraph(paragraph) => Chunks::Paragraph(paragraph.children.iter()),
363            Self::Code(code) => Chunks::Code(Some(code.as_ref())),
364        }
365    }
366
367    fn len(&self) -> usize {
368        self.chunks().map(str::len).sum()
369    }
370
371    /// The length of the rendered text `self` shares with `old`, on a char
372    /// boundary of `self`.
373    fn common_prefix_len(&self, old: &Self) -> usize {
374        let prefix = common_prefix_len(self.chunks(), old.chunks());
375        floor_char_boundary(self.chunks(), prefix)
376    }
377}
378
379fn text_leaves<'a>(block: &'a BlockNode, out: &mut Vec<(TextLeafKey, TextLeaf<'a>)>) {
380    match block {
381        BlockNode::Paragraph(paragraph) => {
382            if let Some(span) = paragraph.span {
383                out.push((
384                    TextLeafKey::block(span.start),
385                    TextLeaf::Paragraph(paragraph),
386                ));
387            }
388        }
389        BlockNode::Heading {
390            children,
391            span: Some(span),
392            ..
393        } => out.push((
394            TextLeafKey::block(span.start),
395            TextLeaf::Paragraph(children),
396        )),
397        BlockNode::CodeBlock(code_block) => {
398            if let Some(span) = code_block.span {
399                out.push((
400                    TextLeafKey::block(span.start),
401                    TextLeaf::Code(code_block.code()),
402                ));
403            }
404        }
405        BlockNode::Table(table) => {
406            if let Some(span) = table.span {
407                let cells = table.children.iter().flat_map(|row| row.children.iter());
408                for (ordinal, cell) in cells.enumerate() {
409                    out.push((
410                        TextLeafKey::table_cell(span.start, ordinal),
411                        TextLeaf::Paragraph(&cell.children),
412                    ));
413                }
414            }
415        }
416        BlockNode::Root { children, .. }
417        | BlockNode::Blockquote { children, .. }
418        | BlockNode::List { children, .. }
419        | BlockNode::ListItem { children, .. } => {
420            for child in children {
421                text_leaves(child, out);
422            }
423        }
424        _ => {}
425    }
426}
427
428/// Compares chunk by chunk with slice equality, descending to bytes only at
429/// the first chunk pair that differs.
430fn common_prefix_len<'a>(
431    mut a: impl Iterator<Item = &'a str>,
432    mut b: impl Iterator<Item = &'a str>,
433) -> usize {
434    let (mut a_rest, mut b_rest): (&[u8], &[u8]) = (&[], &[]);
435    let mut len = 0;
436    loop {
437        if a_rest.is_empty() {
438            match a.next() {
439                Some(chunk) => a_rest = chunk.as_bytes(),
440                None => return len,
441            }
442            continue;
443        }
444        if b_rest.is_empty() {
445            match b.next() {
446                Some(chunk) => b_rest = chunk.as_bytes(),
447                None => return len,
448            }
449            continue;
450        }
451        let step = a_rest.len().min(b_rest.len());
452        if a_rest[..step] != b_rest[..step] {
453            return len
454                + a_rest
455                    .iter()
456                    .zip(b_rest)
457                    .take_while(|(x, y)| x == y)
458                    .count();
459        }
460        len += step;
461        a_rest = &a_rest[step..];
462        b_rest = &b_rest[step..];
463    }
464}
465
466/// Splits `start..end` of the rendered text into the units that fade one
467/// after another: a word together with the whitespace after it, or one CJK
468/// character, since CJK text has no spaces to reveal it by.
469fn fade_units<'a>(
470    chunks: impl Iterator<Item = &'a str>,
471    start: usize,
472    end: usize,
473) -> Vec<Range<usize>> {
474    let mut units = Vec::new();
475    let mut unit_start = start;
476    let mut unit_has_glyph = false;
477    let mut previous: Option<char> = None;
478    let mut offset = 0;
479    for chunk in chunks {
480        if offset + chunk.len() <= start {
481            offset += chunk.len();
482            previous = chunk.chars().next_back();
483            continue;
484        }
485        for (ix, c) in chunk.char_indices() {
486            let position = offset + ix;
487            if position >= end {
488                break;
489            }
490            if position >= start {
491                let starts_unit = unit_has_glyph
492                    && !c.is_whitespace()
493                    && (is_cjk(c) || previous.is_some_and(|p| p.is_whitespace() || is_cjk(p)));
494                if starts_unit && position > unit_start {
495                    units.push(unit_start..position);
496                    unit_start = position;
497                    unit_has_glyph = false;
498                }
499                unit_has_glyph |= !c.is_whitespace();
500            }
501            previous = Some(c);
502        }
503        offset += chunk.len();
504        if offset >= end {
505            break;
506        }
507    }
508    if unit_start < end {
509        units.push(unit_start..end);
510    }
511    units
512}
513
514fn is_cjk(c: char) -> bool {
515    matches!(
516        u32::from(c),
517        0x3040..=0x30FF // Hiragana, Katakana
518            | 0x3400..=0x4DBF // CJK Unified Ideographs Extension A
519            | 0x4E00..=0x9FFF // CJK Unified Ideographs
520            | 0xAC00..=0xD7AF // Hangul syllables
521            | 0xF900..=0xFAFF // CJK Compatibility Ideographs
522            | 0x20000..=0x2FA1F // CJK Unified Ideographs Extensions B and later
523    )
524}
525
526fn floor_char_boundary<'a>(chunks: impl Iterator<Item = &'a str>, offset: usize) -> usize {
527    let mut start = 0;
528    for chunk in chunks {
529        let end = start + chunk.len();
530        if offset < end {
531            let mut local = offset - start;
532            while !chunk.is_char_boundary(local) {
533                local -= 1;
534            }
535            return start + local;
536        }
537        start = end;
538    }
539    offset
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn common_prefix_spans_chunk_boundaries() {
548        assert_eq!(
549            common_prefix_len(["ab", "cd"].into_iter(), ["abc", "d"].into_iter()),
550            4
551        );
552        assert_eq!(
553            common_prefix_len(["ab", "cd"].into_iter(), ["abc", "x"].into_iter()),
554            3
555        );
556        assert_eq!(
557            common_prefix_len(["", "ab"].into_iter(), ["a", "", "b", "c"].into_iter()),
558            2
559        );
560        assert_eq!(common_prefix_len(["ab"].into_iter(), [].into_iter()), 0);
561    }
562
563    #[test]
564    fn fade_units_are_words_with_their_trailing_space() {
565        let text = ["hello", " one two", "  three"];
566        assert_eq!(
567            fade_units(text.into_iter(), 5, 20),
568            vec![5..10, 10..15, 15..20]
569        );
570        // A unit that is only whitespace joins the word after it.
571        assert_eq!(fade_units(["a  b"].into_iter(), 1, 4), vec![1..4]);
572        assert_eq!(
573            fade_units(["abc"].into_iter(), 3, 3),
574            Vec::<Range<usize>>::new()
575        );
576    }
577
578    #[test]
579    fn fade_units_split_cjk_by_character() {
580        assert_eq!(
581            fade_units(["你好,世界 ok"].into_iter(), 0, 18),
582            vec![0..3, 3..6, 6..9, 9..12, 12..16, 16..18]
583        );
584        // Latin before CJK starts a unit at the script change.
585        assert_eq!(fade_units(["ab中"].into_iter(), 0, 5), vec![0..2, 2..5]);
586    }
587
588    #[test]
589    fn stagger_holds_while_the_update_lights_up_within_one_fade() {
590        let motion = TextViewMotion::default()
591            .with_stream_fade(Duration::from_millis(600))
592            .with_stream_fade_stagger(Duration::from_millis(100));
593        assert_eq!(motion.stagger_step(1), Duration::ZERO);
594        assert_eq!(motion.stagger_step(3), Duration::from_millis(100));
595        // The 7th word starts at 600 ms, exactly one fade in -- still the stagger as asked.
596        assert_eq!(motion.stagger_step(7), Duration::from_millis(100));
597    }
598
599    #[test]
600    fn an_update_too_large_to_light_up_in_one_fade_fades_as_one_chunk() {
601        let motion = TextViewMotion::default()
602            .with_stream_fade(Duration::from_millis(600))
603            .with_stream_fade_stagger(Duration::from_millis(100));
604        // An 8th word would start past the fade: that is a sweep, not typing.
605        assert_eq!(motion.stagger_step(8), Duration::ZERO);
606        assert_eq!(motion.stagger_step(200), Duration::ZERO);
607        // Without a stagger nothing changes -- every update was already one chunk.
608        let plain = TextViewMotion::default().with_stream_fade(Duration::from_millis(600));
609        assert_eq!(plain.stagger_step(200), Duration::ZERO);
610    }
611
612    #[test]
613    fn prefix_never_splits_a_character() {
614        // "中" and "串" share their first UTF-8 byte.
615        let new = "a中";
616        let old = "a串";
617        let prefix = common_prefix_len([new].into_iter(), [old].into_iter());
618        assert!(prefix > 1 && !new.is_char_boundary(prefix));
619        assert_eq!(floor_char_boundary([new].into_iter(), prefix), 1);
620        assert_eq!(floor_char_boundary(["a", "中"].into_iter(), 4), 4);
621    }
622}