1use 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#[derive(Debug)]
22pub struct TextDocumentLayout {
23 paragraphs: Arc<[ParagraphSlot]>,
24 line_count: usize,
25 pub metrics: TextMetrics,
26 line_carets: OnceLock<LineCaretStops>,
28}
29
30impl TextDocumentLayout {
31 fn new(paragraphs: Arc<[ParagraphSlot]>) -> Self {
32 let first = ¶graphs[0];
33 let last = ¶graphs[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 #[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
155pub(super) struct RetainedDocument {
158 policy: LayoutKey<'static>,
160 text: String,
161 layout: Arc<TextDocumentLayout>,
162}
163
164const RETAINED_DOCUMENTS: usize = 4;
167
168impl TextEngine {
169 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; }
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
289fn 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 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
325fn 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}