Skip to main content

incular_text/engine/
document.rs

1//! Documents laid out paragraph by paragraph, for editors of long text.
2
3use super::{LayoutKey, LineCaretStops, TextEngine, TextLayoutOptions, common_prefix_len};
4use crate::geometry::{ParagraphSlot, TextLineRef, TextLines};
5use crate::{TextCaretPosition, TextGeometry, TextMetrics, TextOverflow, TextStyle};
6use incular_core::Size;
7use std::sync::{Arc, OnceLock};
8
9/// Text laid out one hard-broken paragraph at a time.
10///
11/// Each paragraph keeps its own [`TextLayout`](crate::TextLayout), and the document records
12/// where each one starts. Laying out an edited version of a document reuses
13/// every paragraph before and after the edit: those after it only move, so
14/// an edit near the top of a long document costs about as much as one at
15/// its end. A [`TextLayout`](crate::TextLayout) of the same text instead stores every line in
16/// document coordinates, like Flutter's paragraph, and so rewrites every
17/// line after an edit.
18///
19/// Geometry is read through [`TextGeometry`]; it matches a [`TextLayout`](crate::TextLayout) of
20/// the same text laid out with soft wrapping and visible overflow.
21#[derive(Debug)]
22pub struct TextDocumentLayout {
23    paragraphs: Arc<[ParagraphSlot]>,
24    line_count: usize,
25    pub metrics: TextMetrics,
26    /// Caret stops in document coordinates, derived per line on first use.
27    line_carets: OnceLock<LineCaretStops>,
28}
29
30impl TextDocumentLayout {
31    fn new(paragraphs: Arc<[ParagraphSlot]>) -> Self {
32        let first = &paragraphs[0];
33        let last = &paragraphs[paragraphs.len() - 1];
34        let metrics = TextMetrics {
35            size: Size::new(
36                paragraphs.iter().fold(0f32, |width, slot| {
37                    width.max(slot.layout.metrics.size.width)
38                }),
39                last.y + last.layout.metrics.size.height,
40            ),
41            baseline: first.y + first.layout.metrics.baseline,
42            line_height: last.layout.metrics.line_height,
43        };
44        let line_count = paragraphs.iter().map(|slot| slot.layout.lines.len()).sum();
45        Self {
46            paragraphs,
47            line_count,
48            metrics,
49            line_carets: OnceLock::new(),
50        }
51    }
52
53    /// Number of hard-broken paragraphs; text without a line break has one.
54    #[must_use]
55    pub fn paragraph_count(&self) -> usize {
56        self.paragraphs.len()
57    }
58
59    #[must_use]
60    pub fn glyph_count(&self) -> usize {
61        self.paragraphs
62            .iter()
63            .map(|slot| slot.layout.glyph_count())
64            .sum()
65    }
66
67    fn slot_for_line(&self, line: usize) -> Option<&ParagraphSlot> {
68        let slot = self
69            .paragraphs
70            .partition_point(|slot| slot.first_line <= line)
71            .checked_sub(1)?;
72        Some(&self.paragraphs[slot])
73    }
74}
75
76impl TextGeometry for TextDocumentLayout {
77    fn metrics(&self) -> TextMetrics {
78        self.metrics
79    }
80
81    fn line_count(&self) -> usize {
82        self.line_count
83    }
84
85    fn get_line(&self, index: usize) -> Option<TextLineRef<'_>> {
86        if index >= self.line_count {
87            return None;
88        }
89        let slot = self.slot_for_line(index)?;
90        let line = slot.layout.lines.get(index - slot.first_line)?;
91        Some(TextLineRef::new(line, index, slot.byte, slot.y))
92    }
93
94    fn lines(&self) -> TextLines<'_> {
95        TextLines::paragraphs(&self.paragraphs)
96    }
97
98    fn lines_between(&self, top: f32, bottom: f32) -> TextLines<'_> {
99        let first = self
100            .paragraphs
101            .partition_point(|slot| slot.y <= top)
102            .saturating_sub(1);
103        let end = self
104            .paragraphs
105            .partition_point(|slot| slot.y < bottom)
106            .max(first);
107        TextLines::paragraphs(&self.paragraphs[first..end])
108    }
109
110    fn line_for_offset(&self, byte: usize) -> usize {
111        let last = self.line_count.saturating_sub(1);
112        let Some(slot) = self
113            .paragraphs
114            .partition_point(|slot| slot.byte <= byte)
115            .checked_sub(1)
116        else {
117            return last;
118        };
119        let slot = &self.paragraphs[slot];
120        let local = byte - slot.byte;
121        let lines = &slot.layout.lines;
122        match lines
123            .partition_point(|line| line.start <= local)
124            .checked_sub(1)
125        {
126            Some(line) if local < lines[line].end => slot.first_line + line,
127            _ => last,
128        }
129    }
130
131    fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition] {
132        let Some(slot) = self.slot_for_line(line).filter(|_| line < self.line_count) else {
133            return &[];
134        };
135        let lines = self.line_carets.get_or_init(|| {
136            (0..self.line_count)
137                .map(|_| OnceLock::new())
138                .collect::<Vec<_>>()
139                .into()
140        });
141        lines[line].get_or_init(|| {
142            slot.layout
143                .line_caret_positions(line - slot.first_line)
144                .iter()
145                .map(|position| TextCaretPosition {
146                    offset: position.offset + slot.byte,
147                    line,
148                    ..*position
149                })
150                .collect()
151        })
152    }
153}
154
155/// A document layout kept so laying out an edited version can carry over the
156/// paragraphs around its change.
157pub(super) struct RetainedDocument {
158    /// Empty-text key carrying the style, paragraph options and generation.
159    policy: LayoutKey<'static>,
160    text: String,
161    layout: Arc<TextDocumentLayout>,
162}
163
164/// Document layouts retained for carrying paragraphs into the next edit: a
165/// few editors can be edited alternately.
166const RETAINED_DOCUMENTS: usize = 4;
167
168impl TextEngine {
169    /// Lays out `text` paragraph by paragraph; see [`TextDocumentLayout`].
170    ///
171    /// Paragraphs are laid out with `options` and visible overflow:
172    /// `max_lines` and `overflow` apply to whole-text layouts only.
173    pub fn layout_paragraphs(
174        &mut self,
175        text: &str,
176        style: &TextStyle,
177        options: TextLayoutOptions,
178    ) -> Arc<TextDocumentLayout> {
179        self.diagnostics.layouts_requested += 1;
180        self.tick_pools();
181        let paragraph_options = TextLayoutOptions {
182            max_lines: None,
183            overflow: TextOverflow::Visible,
184            ..options
185        };
186        let policy = LayoutKey::new("", style, paragraph_options, self.generation);
187        let carried = self
188            .retained_documents
189            .iter()
190            .enumerate()
191            .filter(|(_, document)| document.policy == policy)
192            .map(|(index, document)| {
193                let (prefix, suffix) =
194                    carried_paragraphs(&document.layout.paragraphs, &document.text, text);
195                let count = document.layout.paragraphs.len();
196                (index, prefix, suffix, prefix + count - suffix)
197            })
198            .filter(|(.., carried)| *carried > 0)
199            .max_by_key(|(.., carried)| *carried);
200        if let Some((index, ..)) = carried
201            && self.retained_documents[index].text == text
202        {
203            self.diagnostics.cache_hits += 1;
204            self.diagnostics.documents_reused += 1;
205            let document = self.retained_documents.remove(index);
206            let layout = document.layout.clone();
207            self.retained_documents.insert(0, document);
208            return layout;
209        }
210        self.diagnostics.documents_composed += 1;
211        let paragraph_count = text.bytes().filter(|byte| *byte == b'\n').count() + 1;
212        self.reserve_document(paragraph_count);
213        let mut slots: Vec<ParagraphSlot> = Vec::with_capacity(paragraph_count);
214        let (mut byte_start, mut height, mut line_count) = (0usize, 0f32, 0usize);
215        let mut suffix = None;
216        if let Some((index, prefix, first_suffix, _)) = carried {
217            let document = &self.retained_documents[index];
218            let previous = document.layout.paragraphs.clone();
219            slots.extend_from_slice(&previous[..prefix]);
220            let next = &previous[prefix];
221            (byte_start, height, line_count) = (next.byte, next.y, next.first_line);
222            self.diagnostics.paragraphs_carried += prefix as u64;
223            if first_suffix < previous.len() {
224                let start = previous[first_suffix].byte + text.len() - document.text.len();
225                suffix = Some((previous, first_suffix, start));
226            }
227        }
228        loop {
229            if let Some((previous, first, start)) = &suffix
230                && *start == byte_start
231            {
232                let anchor = &previous[*first];
233                for slot in &previous[*first..] {
234                    slots.push(ParagraphSlot {
235                        byte: slot.byte - anchor.byte + byte_start,
236                        y: slot.y - anchor.y + height,
237                        first_line: slot.first_line - anchor.first_line + line_count,
238                        layout: slot.layout.clone(),
239                    });
240                }
241                self.diagnostics.paragraphs_carried += (previous.len() - *first) as u64;
242                break;
243            }
244            let paragraph_end = text[byte_start..]
245                .find('\n')
246                .map_or(text.len(), |end| byte_start + end);
247            let paragraph = &text[byte_start..paragraph_end];
248            self.diagnostics.paragraphs_considered += 1;
249            let key = LayoutKey::new(paragraph, style, paragraph_options, self.generation);
250            let layout = if let Some(existing) = self.cache.get(&key) {
251                self.diagnostics.parley_layouts_reused += 1;
252                self.diagnostics.cache_hits += 1;
253                existing
254            } else {
255                self.diagnostics.paragraphs_reshaped += 1;
256                self.diagnostics.cache_misses += 1;
257                let (built, _) = self.shape_or_reuse(paragraph, style, paragraph_options);
258                self.cache.put(key, built.clone());
259                built
260            };
261            let (y, first_line) = (height, line_count);
262            height += layout.metrics.size.height;
263            line_count += layout.lines.len();
264            slots.push(ParagraphSlot {
265                byte: byte_start,
266                y,
267                first_line,
268                layout,
269            });
270            if paragraph_end == text.len() {
271                break;
272            }
273            byte_start = paragraph_end + 1; // include the hard break
274        }
275        let layout = Arc::new(TextDocumentLayout::new(slots.into()));
276        self.retained_documents.truncate(RETAINED_DOCUMENTS - 1);
277        self.retained_documents.insert(
278            0,
279            RetainedDocument {
280                policy,
281                text: text.to_owned(),
282                layout: layout.clone(),
283            },
284        );
285        layout
286    }
287}
288
289/// Paragraphs of `previous` that `text` shares, as the number carried
290/// unchanged from the start and the index of the first one carried from the
291/// end (the paragraph count when none is).
292///
293/// Paragraph `k` and everything before it are unchanged when the common
294/// byte prefix reaches the start of paragraph `k`, so paragraphs before the
295/// one holding the first change are carried. Paragraph `j` and everything
296/// after it are unchanged when the common byte suffix covers the hard break
297/// before it, or when the whole previous text follows a new hard break. The
298/// suffix starts at or after the first recomposed paragraph in both texts,
299/// so no paragraph is carried twice.
300fn carried_paragraphs(paragraphs: &[ParagraphSlot], previous: &str, text: &str) -> (usize, usize) {
301    let prefix = common_prefix_len(previous, text);
302    let carried = paragraphs
303        .partition_point(|slot| slot.byte <= prefix)
304        .saturating_sub(1);
305    let floor = paragraphs[carried].byte;
306    let shared = common_suffix_len(previous, text);
307    if carried == 0
308        && shared == previous.len()
309        && text.len() > previous.len()
310        && text.as_bytes()[text.len() - previous.len() - 1] == b'\n'
311    {
312        return (0, 0);
313    }
314    // Lowest start whose preceding hard break is shared and which lands at
315    // or after `floor` in the new text.
316    let threshold = floor
317        .max((floor + previous.len()).saturating_sub(text.len()))
318        .max(previous.len() - shared + 1);
319    let suffix = paragraphs
320        .partition_point(|slot| slot.byte < threshold)
321        .max(carried);
322    (carried, suffix)
323}
324
325/// Length in bytes of the longest common suffix of `left` and `right`.
326fn common_suffix_len(left: &str, right: &str) -> usize {
327    let (left, right) = (left.as_bytes(), right.as_bytes());
328    let mut suffix = 0;
329    for (a, b) in left.rchunks(64).zip(right.rchunks(64)) {
330        if a == b {
331            suffix += a.len();
332        } else {
333            return suffix
334                + a.iter()
335                    .rev()
336                    .zip(b.iter().rev())
337                    .take_while(|(x, y)| x == y)
338                    .count();
339        }
340    }
341    suffix
342}