1mod line;
2#[cfg(test)]
3mod line_wrapper_tests;
4
5pub use line::*;
6
7use crate::{
8 FontId, FontRun, LineLayout, LineLayoutIndex, Pixels, SharedString, TextRun, TextSystem,
9};
10use anyhow::Result;
11use derive_more::Deref;
12use smallvec::SmallVec;
13use std::cmp;
14use std::ops::Range;
15use std::sync::Arc;
16
17#[derive(Deref)]
19pub struct WindowTextSystem {
20 #[deref]
21 text_system: Arc<dyn TextSystem>,
22}
23
24impl WindowTextSystem {
25 pub fn new(text_system: Arc<dyn TextSystem>) -> Self {
27 Self { text_system }
28 }
29
30 pub(crate) fn layout_index(&self) -> LineLayoutIndex {
31 self.text_system.layout_index()
32 }
33
34 pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
35 self.text_system.reuse_layouts(index)
36 }
37
38 pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
39 self.text_system.truncate_layouts(index)
40 }
41
42 pub fn shape_line(
49 &self,
50 text: SharedString,
51 font_size: Pixels,
52 runs: &[TextRun],
53 force_width: Option<Pixels>,
54 ) -> ShapedLine {
55 debug_assert!(
56 text.find('\n').is_none(),
57 "text argument should not contain newlines"
58 );
59
60 let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
61 for run in runs {
62 if let Some(last_run) = decoration_runs.last_mut()
63 && last_run.color == run.color
64 && last_run.underline == run.underline
65 && last_run.strikethrough == run.strikethrough
66 && last_run.background_color == run.background_color
67 {
68 last_run.len += run.len as u32;
69 continue;
70 }
71 decoration_runs.push(DecorationRun {
72 len: run.len as u32,
73 color: run.color,
74 background_color: run.background_color,
75 underline: run.underline,
76 strikethrough: run.strikethrough,
77 });
78 }
79
80 let layout = self.layout_line(&text, font_size, runs, force_width);
81
82 ShapedLine {
83 layout,
84 text,
85 decoration_runs,
86 }
87 }
88
89 pub fn shape_line_by_hash(
100 &self,
101 text_hash: u64,
102 text_len: usize,
103 font_size: Pixels,
104 runs: &[TextRun],
105 force_width: Option<Pixels>,
106 materialize_text: impl FnOnce() -> SharedString + 'static,
107 ) -> ShapedLine {
108 let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
109 for run in runs {
110 if let Some(last_run) = decoration_runs.last_mut()
111 && last_run.color == run.color
112 && last_run.underline == run.underline
113 && last_run.strikethrough == run.strikethrough
114 && last_run.background_color == run.background_color
115 {
116 last_run.len += run.len as u32;
117 continue;
118 }
119 decoration_runs.push(DecorationRun {
120 len: run.len as u32,
121 color: run.color,
122 background_color: run.background_color,
123 underline: run.underline,
124 strikethrough: run.strikethrough,
125 });
126 }
127
128 let mut used_force_width = force_width;
129 let layout = self.layout_line_by_hash(
130 text_hash,
131 text_len,
132 font_size,
133 runs,
134 used_force_width,
135 || {
136 let text = materialize_text();
137 debug_assert!(
138 text.find('\n').is_none(),
139 "text argument should not contain newlines"
140 );
141 text
142 },
143 );
144
145 let text: SharedString = SharedString::new_static("");
149
150 ShapedLine {
151 layout,
152 text,
153 decoration_runs,
154 }
155 }
156
157 pub fn shape_text(
161 &self,
162 text: SharedString,
163 font_size: Pixels,
164 runs: &[TextRun],
165 wrap_width: Option<Pixels>,
166 line_clamp: Option<usize>,
167 ) -> Result<SmallVec<[WrappedLine; 1]>> {
168 let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
169 let mut font_runs = self.take_font_runs();
170
171 let mut lines = SmallVec::new();
172 let mut max_wrap_lines = line_clamp;
173 let mut wrapped_lines = 0;
174
175 let mut process_line = |line_text: SharedString, line_start, line_end| {
176 font_runs.clear();
177
178 let mut decoration_runs = <Vec<DecorationRun>>::with_capacity(32);
179 let mut run_start = line_start;
180 while run_start < line_end {
181 let Some(run) = runs.peek_mut() else {
182 log::warn!("`TextRun`s do not cover the entire to be shaped text");
183 break;
184 };
185
186 let run_len_within_line = cmp::min(line_end - run_start, run.len);
187
188 let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
189 && last_run.color == run.color
190 && last_run.underline == run.underline
191 && last_run.strikethrough == run.strikethrough
192 && last_run.background_color == run.background_color
193 {
194 last_run.len += run_len_within_line as u32;
195 false
196 } else {
197 decoration_runs.push(DecorationRun {
198 len: run_len_within_line as u32,
199 color: run.color,
200 background_color: run.background_color,
201 underline: run.underline,
202 strikethrough: run.strikethrough,
203 });
204 true
205 };
206
207 let font_id = self.resolve_font(&run.font);
208 if let Some(font_run) = font_runs.last_mut()
209 && font_id == font_run.font_id
210 && !decoration_changed
211 {
212 font_run.len += run_len_within_line;
213 } else {
214 font_runs.push(FontRun {
215 len: run_len_within_line,
216 font_id,
217 });
218 }
219
220 run.len -= run_len_within_line;
222 if run.len == 0 {
223 runs.next();
224 }
225 run_start += run_len_within_line;
226 }
227
228 let layout = self.text_system.layout_wrapped_line(
229 &line_text,
230 font_size,
231 &font_runs,
232 wrap_width,
233 max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)),
234 );
235 wrapped_lines += layout.wrap_boundaries.len();
236
237 lines.push(WrappedLine {
238 layout,
239 decoration_runs,
240 text: line_text,
241 });
242
243 if let Some(run) = runs.peek_mut() {
245 run.len -= 1;
246 if run.len == 0 {
247 runs.next();
248 }
249 }
250 };
251
252 let mut split_lines = text.split('\n');
253
254 if let Some(first_line) = split_lines.next()
256 && let Some(second_line) = split_lines.next()
257 {
258 let mut line_start = 0;
259 process_line(
260 SharedString::new(first_line),
261 line_start,
262 line_start + first_line.len(),
263 );
264 line_start += first_line.len() + '\n'.len_utf8();
265 process_line(
266 SharedString::new(second_line),
267 line_start,
268 line_start + second_line.len(),
269 );
270 for line_text in split_lines {
271 line_start += line_text.len() + '\n'.len_utf8();
272 process_line(
273 SharedString::new(line_text),
274 line_start,
275 line_start + line_text.len(),
276 );
277 }
278 } else {
279 let end = text.len();
280 process_line(text, 0, end);
281 }
282
283 self.recycle_font_runs(font_runs);
284
285 Ok(lines)
286 }
287
288 pub(crate) fn finish_frame(&self) {
289 self.text_system.finish_frame()
290 }
291
292 pub fn layout_line(
297 &self,
298 text: &str,
299 font_size: Pixels,
300 runs: &[TextRun],
301 force_width: Option<Pixels>,
302 ) -> Arc<LineLayout> {
303 let mut last_run = None::<&TextRun>;
304 let mut font_runs = self.take_font_runs();
305 font_runs.clear();
306
307 for run in runs.iter() {
308 let decoration_changed = if let Some(last_run) = last_run
309 && last_run.color == run.color
310 && last_run.underline == run.underline
311 && last_run.strikethrough == run.strikethrough
312 {
315 false
316 } else {
317 last_run = Some(run);
318 true
319 };
320
321 let font_id = self.resolve_font(&run.font);
322 if let Some(font_run) = font_runs.last_mut()
323 && font_id == font_run.font_id
324 && !decoration_changed
325 {
326 font_run.len += run.len;
327 } else {
328 font_runs.push(FontRun {
329 len: run.len,
330 font_id,
331 });
332 }
333 }
334
335 let layout = self
336 .text_system
337 .layout_line(text, font_size, &font_runs, force_width);
338
339 self.recycle_font_runs(font_runs);
340
341 layout
342 }
343
344 pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
346 let mut buffer = [0; 4];
347 let buffer: &_ = ch.encode_utf8(&mut buffer);
348 self.text_system
349 .layout_line(
350 buffer,
351 font_size,
352 &[FontRun {
353 len: buffer.len(),
354 font_id,
355 }],
356 None,
357 )
358 .width
359 }
360
361 pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
363 self.layout_width(font_id, font_size, 'm')
364 }
365
366 pub fn try_layout_line_by_hash(
375 &self,
376 text_hash: u64,
377 text_len: usize,
378 font_size: Pixels,
379 runs: &[TextRun],
380 force_width: Option<Pixels>,
381 ) -> Option<Arc<LineLayout>> {
382 let mut last_run = None::<&TextRun>;
383 let mut font_runs = self.take_font_runs();
384 font_runs.clear();
385
386 for run in runs.iter() {
387 let decoration_changed = if let Some(last_run) = last_run
388 && last_run.color == run.color
389 && last_run.underline == run.underline
390 && last_run.strikethrough == run.strikethrough
391 {
394 false
395 } else {
396 last_run = Some(run);
397 true
398 };
399
400 let font_id = self.resolve_font(&run.font);
401 if let Some(font_run) = font_runs.last_mut()
402 && font_id == font_run.font_id
403 && !decoration_changed
404 {
405 font_run.len += run.len;
406 } else {
407 font_runs.push(FontRun {
408 len: run.len,
409 font_id,
410 });
411 }
412 }
413
414 let layout = self.text_system.try_layout_line_by_hash(
415 text_hash,
416 text_len,
417 font_size,
418 &font_runs,
419 force_width,
420 );
421
422 self.recycle_font_runs(font_runs);
423
424 layout
425 }
426
427 pub fn layout_line_by_hash(
436 &self,
437 text_hash: u64,
438 text_len: usize,
439 font_size: Pixels,
440 runs: &[TextRun],
441 force_width: Option<Pixels>,
442 materialize_text: impl FnOnce() -> SharedString + 'static,
443 ) -> Arc<LineLayout> {
444 let mut last_run = None::<&TextRun>;
445 let mut font_runs = self.take_font_runs();
446 font_runs.clear();
447
448 for run in runs.iter() {
449 let decoration_changed = if let Some(last_run) = last_run
450 && last_run.color == run.color
451 && last_run.underline == run.underline
452 && last_run.strikethrough == run.strikethrough
453 {
456 false
457 } else {
458 last_run = Some(run);
459 true
460 };
461
462 let font_id = self.resolve_font(&run.font);
463 if let Some(font_run) = font_runs.last_mut()
464 && font_id == font_run.font_id
465 && !decoration_changed
466 {
467 font_run.len += run.len;
468 } else {
469 font_runs.push(FontRun {
470 len: run.len,
471 font_id,
472 });
473 }
474 }
475
476 let layout = self.text_system.layout_line_by_hash(
477 text_hash,
478 text_len,
479 font_size,
480 &font_runs,
481 force_width,
482 Box::new(materialize_text),
483 );
484
485 self.recycle_font_runs(font_runs);
486
487 layout
488 }
489}