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