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