1use crate::{FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size, point, px};
2use collections::FxHashMap;
3use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
4use smallvec::SmallVec;
5use std::{
6 borrow::Borrow,
7 hash::{Hash, Hasher},
8 ops::Range,
9 sync::Arc,
10};
11
12use super::LineWrapper;
13
14/// A laid out and styled line of text
15#[derive(Default, Debug)]
16pub struct LineLayout {
17 /// The font size for this line
18 pub font_size: Pixels,
19 /// The width of the line
20 pub width: Pixels,
21 /// The ascent of the line
22 pub ascent: Pixels,
23 /// The descent of the line
24 pub descent: Pixels,
25 /// The shaped runs that make up this line
26 pub runs: Vec<ShapedRun>,
27 /// The length of the line in utf-8 bytes
28 pub len: usize,
29}
30
31/// A run of text that has been shaped .
32#[derive(Debug, Clone)]
33pub struct ShapedRun {
34 /// The font id for this run
35 pub font_id: FontId,
36 /// The glyphs that make up this run
37 pub glyphs: Vec<ShapedGlyph>,
38}
39
40/// A single glyph, ready to paint.
41#[derive(Clone, Debug)]
42pub struct ShapedGlyph {
43 /// The ID for this glyph, as determined by the text system.
44 pub id: GlyphId,
45
46 /// The position of this glyph in its containing line.
47 pub position: Point<Pixels>,
48
49 /// The index of this glyph in the original text.
50 pub index: usize,
51
52 /// Whether this glyph is an emoji
53 pub is_emoji: bool,
54}
55
56impl LineLayout {
57 /// The index for the character at the given x coordinate
58 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
59 if x >= self.width {
60 None
61 } else {
62 for run in self.runs.iter().rev() {
63 for glyph in run.glyphs.iter().rev() {
64 if glyph.position.x <= x {
65 return Some(glyph.index);
66 }
67 }
68 }
69 Some(0)
70 }
71 }
72
73 /// closest_index_for_x returns the character boundary closest to the given x coordinate
74 /// (e.g. to handle aligning up/down arrow keys)
75 pub fn closest_index_for_x(&self, x: Pixels) -> usize {
76 let mut prev_index = 0;
77 let mut prev_x = px(0.);
78
79 for run in self.runs.iter() {
80 for glyph in run.glyphs.iter() {
81 if glyph.position.x >= x {
82 if glyph.position.x - x < x - prev_x {
83 return glyph.index;
84 } else {
85 return prev_index;
86 }
87 }
88 prev_index = glyph.index;
89 prev_x = glyph.position.x;
90 }
91 }
92
93 if self.len == 1 {
94 if x > self.width / 2. {
95 return 1;
96 } else {
97 return 0;
98 }
99 }
100
101 self.len
102 }
103
104 /// The x position of the character at the given index
105 pub fn x_for_index(&self, index: usize) -> Pixels {
106 for run in &self.runs {
107 for glyph in &run.glyphs {
108 if glyph.index >= index {
109 return glyph.position.x;
110 }
111 }
112 }
113 self.width
114 }
115
116 /// The corresponding Font at the given index
117 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
118 for run in &self.runs {
119 for glyph in &run.glyphs {
120 if glyph.index >= index {
121 return Some(run.font_id);
122 }
123 }
124 }
125 None
126 }
127
128 fn compute_wrap_boundaries(
129 &self,
130 text: &str,
131 wrap_width: Pixels,
132 max_lines: Option<usize>,
133 ) -> SmallVec<[WrapBoundary; 1]> {
134 let mut boundaries = SmallVec::new();
135 let mut first_non_whitespace_ix = None;
136 let mut last_candidate_ix = None;
137 let mut last_candidate_x = px(0.);
138 let mut last_boundary = WrapBoundary {
139 run_ix: 0,
140 glyph_ix: 0,
141 };
142 let mut last_boundary_x = px(0.);
143 let mut prev_ch = '\0';
144 let mut glyphs = self
145 .runs
146 .iter()
147 .enumerate()
148 .flat_map(move |(run_ix, run)| {
149 run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
150 let character = text[glyph.index..].chars().next().unwrap();
151 (
152 WrapBoundary { run_ix, glyph_ix },
153 character,
154 glyph.position.x,
155 )
156 })
157 })
158 .peekable();
159
160 while let Some((boundary, ch, x)) = glyphs.next() {
161 if ch == '\n' {
162 continue;
163 }
164
165 // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping,
166 // but there are some differences, so we have to duplicate the code here.
167 if LineWrapper::is_word_char(ch) {
168 if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() {
169 last_candidate_ix = Some(boundary);
170 last_candidate_x = x;
171 }
172 } else {
173 if ch != ' ' && first_non_whitespace_ix.is_some() {
174 last_candidate_ix = Some(boundary);
175 last_candidate_x = x;
176 }
177 }
178
179 if ch != ' ' && first_non_whitespace_ix.is_none() {
180 first_non_whitespace_ix = Some(boundary);
181 }
182
183 let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
184 let width = next_x - last_boundary_x;
185
186 if width > wrap_width && boundary > last_boundary {
187 // When used line_clamp, we should limit the number of lines.
188 if let Some(max_lines) = max_lines
189 && boundaries.len() >= max_lines.saturating_sub(1)
190 {
191 break;
192 }
193
194 if let Some(last_candidate_ix) = last_candidate_ix.take() {
195 last_boundary = last_candidate_ix;
196 last_boundary_x = last_candidate_x;
197 } else {
198 last_boundary = boundary;
199 last_boundary_x = x;
200 }
201 boundaries.push(last_boundary);
202 }
203 prev_ch = ch;
204 }
205
206 boundaries
207 }
208}
209
210/// A line of text that has been wrapped to fit a given width
211#[derive(Default, Debug)]
212pub struct WrappedLineLayout {
213 /// The line layout, pre-wrapping.
214 pub unwrapped_layout: Arc<LineLayout>,
215
216 /// The boundaries at which the line was wrapped
217 pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
218
219 /// The width of the line, if it was wrapped
220 pub wrap_width: Option<Pixels>,
221}
222
223/// A boundary at which a line was wrapped
224#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
225pub struct WrapBoundary {
226 /// The index in the run just before the line was wrapped
227 pub run_ix: usize,
228 /// The index of the glyph just before the line was wrapped
229 pub glyph_ix: usize,
230}
231
232impl WrappedLineLayout {
233 /// The length of the underlying text, in utf8 bytes.
234 #[allow(clippy::len_without_is_empty)]
235 pub fn len(&self) -> usize {
236 self.unwrapped_layout.len
237 }
238
239 /// The width of this line, in pixels, whether or not it was wrapped.
240 pub fn width(&self) -> Pixels {
241 self.wrap_width
242 .unwrap_or(Pixels::MAX)
243 .min(self.unwrapped_layout.width)
244 }
245
246 /// The size of the whole wrapped text, for the given line_height.
247 /// can span multiple lines if there are multiple wrap boundaries.
248 pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
249 Size {
250 width: self.width(),
251 height: line_height * (self.wrap_boundaries.len() + 1),
252 }
253 }
254
255 /// The ascent of a line in this layout
256 pub fn ascent(&self) -> Pixels {
257 self.unwrapped_layout.ascent
258 }
259
260 /// The descent of a line in this layout
261 pub fn descent(&self) -> Pixels {
262 self.unwrapped_layout.descent
263 }
264
265 /// The wrap boundaries in this layout
266 pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
267 &self.wrap_boundaries
268 }
269
270 /// The font size of this layout
271 pub fn font_size(&self) -> Pixels {
272 self.unwrapped_layout.font_size
273 }
274
275 /// The runs in this layout, sans wrapping
276 pub fn runs(&self) -> &[ShapedRun] {
277 &self.unwrapped_layout.runs
278 }
279
280 /// The index corresponding to a given position in this layout for the given line height.
281 ///
282 /// See also [`Self::closest_index_for_position`].
283 pub fn index_for_position(
284 &self,
285 position: Point<Pixels>,
286 line_height: Pixels,
287 ) -> Result<usize, usize> {
288 self._index_for_position(position, line_height, false)
289 }
290
291 /// The closest index to a given position in this layout for the given line height.
292 ///
293 /// Closest means the character boundary closest to the given position.
294 ///
295 /// See also [`LineLayout::closest_index_for_x`].
296 pub fn closest_index_for_position(
297 &self,
298 position: Point<Pixels>,
299 line_height: Pixels,
300 ) -> Result<usize, usize> {
301 self._index_for_position(position, line_height, true)
302 }
303
304 fn _index_for_position(
305 &self,
306 mut position: Point<Pixels>,
307 line_height: Pixels,
308 closest: bool,
309 ) -> Result<usize, usize> {
310 let wrapped_line_ix = (position.y / line_height) as usize;
311
312 let wrapped_line_start_index;
313 let wrapped_line_start_x;
314 if wrapped_line_ix > 0 {
315 let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
316 return Err(0);
317 };
318 let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
319 let glyph = &run.glyphs[line_start_boundary.glyph_ix];
320 wrapped_line_start_index = glyph.index;
321 wrapped_line_start_x = glyph.position.x;
322 } else {
323 wrapped_line_start_index = 0;
324 wrapped_line_start_x = Pixels::ZERO;
325 };
326
327 let wrapped_line_end_index;
328 let wrapped_line_end_x;
329 if wrapped_line_ix < self.wrap_boundaries.len() {
330 let next_wrap_boundary_ix = wrapped_line_ix;
331 let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
332 let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
333 let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
334 wrapped_line_end_index = glyph.index;
335 wrapped_line_end_x = glyph.position.x;
336 } else {
337 wrapped_line_end_index = self.unwrapped_layout.len;
338 wrapped_line_end_x = self.unwrapped_layout.width;
339 };
340
341 let mut position_in_unwrapped_line = position;
342 position_in_unwrapped_line.x += wrapped_line_start_x;
343 if position_in_unwrapped_line.x < wrapped_line_start_x {
344 Err(wrapped_line_start_index)
345 } else if position_in_unwrapped_line.x >= wrapped_line_end_x {
346 Err(wrapped_line_end_index)
347 } else {
348 if closest {
349 Ok(self
350 .unwrapped_layout
351 .closest_index_for_x(position_in_unwrapped_line.x))
352 } else {
353 Ok(self
354 .unwrapped_layout
355 .index_for_x(position_in_unwrapped_line.x)
356 .unwrap())
357 }
358 }
359 }
360
361 /// Returns the pixel position for the given byte index.
362 pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
363 let mut line_start_ix = 0;
364 let mut line_end_indices = self
365 .wrap_boundaries
366 .iter()
367 .map(|wrap_boundary| {
368 let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
369 let glyph = &run.glyphs[wrap_boundary.glyph_ix];
370 glyph.index
371 })
372 .chain([self.len()])
373 .enumerate();
374 for (ix, line_end_ix) in line_end_indices {
375 let line_y = ix as f32 * line_height;
376 if index < line_start_ix {
377 break;
378 } else if index > line_end_ix {
379 line_start_ix = line_end_ix;
380 continue;
381 } else {
382 let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
383 let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
384 return Some(point(x, line_y));
385 }
386 }
387
388 None
389 }
390}
391
392pub(crate) struct LineLayoutCache {
393 previous_frame: Mutex<FrameCache>,
394 current_frame: RwLock<FrameCache>,
395 platform_text_system: Arc<dyn PlatformTextSystem>,
396}
397
398#[derive(Default)]
399struct FrameCache {
400 lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
401 wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
402 used_lines: Vec<Arc<CacheKey>>,
403 used_wrapped_lines: Vec<Arc<CacheKey>>,
404
405 // Content-addressable caches keyed by caller-provided text hash + layout params.
406 // These allow cache hits without materializing a contiguous `SharedString`.
407 //
408 // IMPORTANT: To support allocation-free lookups, we store these maps using a key type
409 // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`.
410 // On miss, we allocate once and store under an owned `HashedCacheKey`.
411 lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<LineLayout>>,
412 wrapped_lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<WrappedLineLayout>>,
413 used_lines_by_hash: Vec<Arc<HashedCacheKey>>,
414 used_wrapped_lines_by_hash: Vec<Arc<HashedCacheKey>>,
415}
416
417#[derive(Clone, Default)]
418pub(crate) struct LineLayoutIndex {
419 lines_index: usize,
420 wrapped_lines_index: usize,
421 lines_by_hash_index: usize,
422 wrapped_lines_by_hash_index: usize,
423}
424
425impl LineLayoutCache {
426 pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
427 Self {
428 previous_frame: Mutex::default(),
429 current_frame: RwLock::default(),
430 platform_text_system,
431 }
432 }
433
434 pub fn layout_index(&self) -> LineLayoutIndex {
435 let frame = self.current_frame.read();
436 LineLayoutIndex {
437 lines_index: frame.used_lines.len(),
438 wrapped_lines_index: frame.used_wrapped_lines.len(),
439 lines_by_hash_index: frame.used_lines_by_hash.len(),
440 wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(),
441 }
442 }
443
444 pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
445 let mut previous_frame = &mut *self.previous_frame.lock();
446 let mut current_frame = &mut *self.current_frame.write();
447
448 for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
449 if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
450 current_frame.lines.insert(key, line);
451 }
452 current_frame.used_lines.push(key.clone());
453 }
454
455 for key in &previous_frame.used_wrapped_lines
456 [range.start.wrapped_lines_index..range.end.wrapped_lines_index]
457 {
458 if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
459 current_frame.wrapped_lines.insert(key, line);
460 }
461 current_frame.used_wrapped_lines.push(key.clone());
462 }
463
464 for key in &previous_frame.used_lines_by_hash
465 [range.start.lines_by_hash_index..range.end.lines_by_hash_index]
466 {
467 if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) {
468 current_frame.lines_by_hash.insert(key, line);
469 }
470 current_frame.used_lines_by_hash.push(key.clone());
471 }
472
473 for key in &previous_frame.used_wrapped_lines_by_hash
474 [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index]
475 {
476 if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) {
477 current_frame.wrapped_lines_by_hash.insert(key, line);
478 }
479 current_frame.used_wrapped_lines_by_hash.push(key.clone());
480 }
481 }
482
483 pub fn truncate_layouts(&self, index: LineLayoutIndex) {
484 let mut current_frame = &mut *self.current_frame.write();
485 current_frame.used_lines.truncate(index.lines_index);
486 current_frame
487 .used_wrapped_lines
488 .truncate(index.wrapped_lines_index);
489 current_frame
490 .used_lines_by_hash
491 .truncate(index.lines_by_hash_index);
492 current_frame
493 .used_wrapped_lines_by_hash
494 .truncate(index.wrapped_lines_by_hash_index);
495 }
496
497 pub fn finish_frame(&self) {
498 let mut prev_frame = self.previous_frame.lock();
499 let mut curr_frame = self.current_frame.write();
500 std::mem::swap(&mut *prev_frame, &mut *curr_frame);
501 curr_frame.lines.clear();
502 curr_frame.wrapped_lines.clear();
503 curr_frame.used_lines.clear();
504 curr_frame.used_wrapped_lines.clear();
505
506 curr_frame.lines_by_hash.clear();
507 curr_frame.wrapped_lines_by_hash.clear();
508 curr_frame.used_lines_by_hash.clear();
509 curr_frame.used_wrapped_lines_by_hash.clear();
510 }
511
512 pub fn layout_wrapped_line<Text>(
513 &self,
514 text: Text,
515 font_size: Pixels,
516 runs: &[FontRun],
517 wrap_width: Option<Pixels>,
518 max_lines: Option<usize>,
519 ) -> Arc<WrappedLineLayout>
520 where
521 Text: AsRef<str>,
522 SharedString: From<Text>,
523 {
524 let key = &CacheKeyRef {
525 text: text.as_ref(),
526 font_size,
527 runs,
528 wrap_width,
529 force_width: None,
530 } as &dyn AsCacheKeyRef;
531
532 let current_frame = self.current_frame.upgradable_read();
533 if let Some(layout) = current_frame.wrapped_lines.get(key) {
534 return layout.clone();
535 }
536
537 let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key);
538 if let Some((key, layout)) = previous_frame_entry {
539 let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
540 current_frame
541 .wrapped_lines
542 .insert(key.clone(), layout.clone());
543 current_frame.used_wrapped_lines.push(key);
544 layout
545 } else {
546 drop(current_frame);
547 let text = SharedString::from(text);
548 let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
549 let wrap_boundaries = if let Some(wrap_width) = wrap_width {
550 unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines)
551 } else {
552 SmallVec::new()
553 };
554 let layout = Arc::new(WrappedLineLayout {
555 unwrapped_layout,
556 wrap_boundaries,
557 wrap_width,
558 });
559 let key = Arc::new(CacheKey {
560 text,
561 font_size,
562 runs: SmallVec::from(runs),
563 wrap_width,
564 force_width: None,
565 });
566
567 let mut current_frame = self.current_frame.write();
568 current_frame
569 .wrapped_lines
570 .insert(key.clone(), layout.clone());
571 current_frame.used_wrapped_lines.push(key);
572
573 layout
574 }
575 }
576
577 pub fn layout_line<Text>(
578 &self,
579 text: Text,
580 font_size: Pixels,
581 runs: &[FontRun],
582 force_width: Option<Pixels>,
583 ) -> Arc<LineLayout>
584 where
585 Text: AsRef<str>,
586 SharedString: From<Text>,
587 {
588 let key = &CacheKeyRef {
589 text: text.as_ref(),
590 font_size,
591 runs,
592 wrap_width: None,
593 force_width,
594 } as &dyn AsCacheKeyRef;
595
596 let current_frame = self.current_frame.upgradable_read();
597 if let Some(layout) = current_frame.lines.get(key) {
598 return layout.clone();
599 }
600
601 let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
602 if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
603 current_frame.lines.insert(key.clone(), layout.clone());
604 current_frame.used_lines.push(key);
605 layout
606 } else {
607 let text = SharedString::from(text);
608 let mut layout = self
609 .platform_text_system
610 .layout_line(&text, font_size, runs);
611
612 if let Some(force_width) = force_width {
613 apply_force_width_to_layout(&mut layout, force_width);
614 }
615
616 let key = Arc::new(CacheKey {
617 text,
618 font_size,
619 runs: SmallVec::from(runs),
620 wrap_width: None,
621 force_width,
622 });
623 let layout = Arc::new(layout);
624 current_frame.lines.insert(key.clone(), layout.clone());
625 current_frame.used_lines.push(key);
626 layout
627 }
628 }
629
630 /// Try to retrieve a previously-shaped line layout using a caller-provided content hash.
631 ///
632 /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout
633 /// is not already cached in either the current frame or previous frame, returns `None`.
634 ///
635 /// Contract (caller enforced):
636 /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
637 /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
638 pub fn try_layout_line_by_hash(
639 &self,
640 text_hash: u64,
641 text_len: usize,
642 font_size: Pixels,
643 runs: &[FontRun],
644 force_width: Option<Pixels>,
645 ) -> Option<Arc<LineLayout>> {
646 let key_ref = HashedCacheKeyRef {
647 text_hash,
648 text_len,
649 font_size,
650 runs,
651 wrap_width: None,
652 force_width,
653 };
654
655 let current_frame = self.current_frame.read();
656 if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
657 HashedCacheKeyRef {
658 text_hash: key.text_hash,
659 text_len: key.text_len,
660 font_size: key.font_size,
661 runs: key.runs.as_slice(),
662 wrap_width: key.wrap_width,
663 force_width: key.force_width,
664 } == key_ref
665 }) {
666 return Some(layout.clone());
667 }
668
669 let previous_frame = self.previous_frame.lock();
670 if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| {
671 HashedCacheKeyRef {
672 text_hash: key.text_hash,
673 text_len: key.text_len,
674 font_size: key.font_size,
675 runs: key.runs.as_slice(),
676 wrap_width: key.wrap_width,
677 force_width: key.force_width,
678 } == key_ref
679 }) {
680 return Some(layout.clone());
681 }
682
683 None
684 }
685
686 /// Layout a line of text using a caller-provided content hash as the cache key.
687 ///
688 /// This enables cache hits without materializing a contiguous `SharedString` for `text`.
689 /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
690 ///
691 /// Contract (caller enforced):
692 /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
693 /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
694 pub fn layout_line_by_hash(
695 &self,
696 text_hash: u64,
697 text_len: usize,
698 font_size: Pixels,
699 runs: &[FontRun],
700 force_width: Option<Pixels>,
701 materialize_text: impl FnOnce() -> SharedString,
702 ) -> Arc<LineLayout> {
703 let key_ref = HashedCacheKeyRef {
704 text_hash,
705 text_len,
706 font_size,
707 runs,
708 wrap_width: None,
709 force_width,
710 };
711
712 // Fast path: already cached (no allocation).
713 let current_frame = self.current_frame.upgradable_read();
714 if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
715 HashedCacheKeyRef {
716 text_hash: key.text_hash,
717 text_len: key.text_len,
718 font_size: key.font_size,
719 runs: key.runs.as_slice(),
720 wrap_width: key.wrap_width,
721 force_width: key.force_width,
722 } == key_ref
723 }) {
724 return layout.clone();
725 }
726
727 let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
728
729 // Try to reuse from previous frame without allocating; do a linear scan to find a matching key.
730 // (We avoid `drain()` here because it would eagerly move all entries.)
731 let mut previous_frame = self.previous_frame.lock();
732 if let Some(existing_key) = previous_frame
733 .used_lines_by_hash
734 .iter()
735 .find(|key| {
736 HashedCacheKeyRef {
737 text_hash: key.text_hash,
738 text_len: key.text_len,
739 font_size: key.font_size,
740 runs: key.runs.as_slice(),
741 wrap_width: key.wrap_width,
742 force_width: key.force_width,
743 } == key_ref
744 })
745 .cloned()
746 {
747 if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) {
748 current_frame
749 .lines_by_hash
750 .insert(key.clone(), layout.clone());
751 current_frame.used_lines_by_hash.push(key);
752 return layout;
753 }
754 }
755
756 let text = materialize_text();
757 let mut layout = self
758 .platform_text_system
759 .layout_line(&text, font_size, runs);
760
761 if let Some(force_width) = force_width {
762 apply_force_width_to_layout(&mut layout, force_width);
763 }
764
765 let key = Arc::new(HashedCacheKey {
766 text_hash,
767 text_len,
768 font_size,
769 runs: SmallVec::from(runs),
770 wrap_width: None,
771 force_width,
772 });
773 let layout = Arc::new(layout);
774 current_frame
775 .lines_by_hash
776 .insert(key.clone(), layout.clone());
777 current_frame.used_lines_by_hash.push(key);
778 layout
779 }
780}
781
782// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by
783// HarfBuzz at the same x position as their base character. The force-width
784// loop must not advance the cell counter for these zero-advance glyphs,
785// otherwise they get displaced into the next cell. We detect them by checking
786// whether shaped x has advanced by at least half a cell beyond the last base.
787fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
788 let mut glyph_pos: usize = 0;
789 // NEG_INFINITY ensures the first glyph is always classified as a base.
790 let mut last_base_shaped_x = px(f32::NEG_INFINITY);
791 let mut last_base_actual_x = px(0.);
792
793 for run in layout.runs.iter_mut() {
794 for glyph in run.glyphs.iter_mut() {
795 let shaped_x = glyph.position.x;
796
797 if shaped_x > last_base_shaped_x + force_width * 0.5 {
798 let forced_x = glyph_pos * force_width;
799 if (shaped_x - forced_x).abs() > px(1.) {
800 glyph.position.x = forced_x;
801 }
802 last_base_shaped_x = shaped_x;
803 last_base_actual_x = glyph.position.x;
804 glyph_pos += 1;
805 } else {
806 glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
807 }
808 }
809 }
810}
811
812/// A run of text with a single font.
813#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
814#[expect(missing_docs)]
815pub struct FontRun {
816 pub len: usize,
817 pub font_id: FontId,
818}
819
820trait AsCacheKeyRef {
821 fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
822}
823
824#[derive(Clone, Debug, Eq)]
825struct CacheKey {
826 text: SharedString,
827 font_size: Pixels,
828 runs: SmallVec<[FontRun; 1]>,
829 wrap_width: Option<Pixels>,
830 force_width: Option<Pixels>,
831}
832
833#[derive(Copy, Clone, PartialEq, Eq, Hash)]
834struct CacheKeyRef<'a> {
835 text: &'a str,
836 font_size: Pixels,
837 runs: &'a [FontRun],
838 wrap_width: Option<Pixels>,
839 force_width: Option<Pixels>,
840}
841
842#[derive(Clone, Debug)]
843struct HashedCacheKey {
844 text_hash: u64,
845 text_len: usize,
846 font_size: Pixels,
847 runs: SmallVec<[FontRun; 1]>,
848 wrap_width: Option<Pixels>,
849 force_width: Option<Pixels>,
850}
851
852#[derive(Copy, Clone)]
853struct HashedCacheKeyRef<'a> {
854 text_hash: u64,
855 text_len: usize,
856 font_size: Pixels,
857 runs: &'a [FontRun],
858 wrap_width: Option<Pixels>,
859 force_width: Option<Pixels>,
860}
861
862impl PartialEq for dyn AsCacheKeyRef + '_ {
863 fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
864 self.as_cache_key_ref() == other.as_cache_key_ref()
865 }
866}
867
868impl PartialEq for HashedCacheKey {
869 fn eq(&self, other: &Self) -> bool {
870 self.text_hash == other.text_hash
871 && self.text_len == other.text_len
872 && self.font_size == other.font_size
873 && self.runs.as_slice() == other.runs.as_slice()
874 && self.wrap_width == other.wrap_width
875 && self.force_width == other.force_width
876 }
877}
878
879impl Eq for HashedCacheKey {}
880
881impl Hash for HashedCacheKey {
882 fn hash<H: Hasher>(&self, state: &mut H) {
883 self.text_hash.hash(state);
884 self.text_len.hash(state);
885 self.font_size.hash(state);
886 self.runs.as_slice().hash(state);
887 self.wrap_width.hash(state);
888 self.force_width.hash(state);
889 }
890}
891
892impl PartialEq for HashedCacheKeyRef<'_> {
893 fn eq(&self, other: &Self) -> bool {
894 self.text_hash == other.text_hash
895 && self.text_len == other.text_len
896 && self.font_size == other.font_size
897 && self.runs == other.runs
898 && self.wrap_width == other.wrap_width
899 && self.force_width == other.force_width
900 }
901}
902
903impl Eq for HashedCacheKeyRef<'_> {}
904
905impl Hash for HashedCacheKeyRef<'_> {
906 fn hash<H: Hasher>(&self, state: &mut H) {
907 self.text_hash.hash(state);
908 self.text_len.hash(state);
909 self.font_size.hash(state);
910 self.runs.hash(state);
911 self.wrap_width.hash(state);
912 self.force_width.hash(state);
913 }
914}
915
916impl Eq for dyn AsCacheKeyRef + '_ {}
917
918impl Hash for dyn AsCacheKeyRef + '_ {
919 fn hash<H: Hasher>(&self, state: &mut H) {
920 self.as_cache_key_ref().hash(state)
921 }
922}
923
924impl AsCacheKeyRef for CacheKey {
925 fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
926 CacheKeyRef {
927 text: &self.text,
928 font_size: self.font_size,
929 runs: self.runs.as_slice(),
930 wrap_width: self.wrap_width,
931 force_width: self.force_width,
932 }
933 }
934}
935
936impl PartialEq for CacheKey {
937 fn eq(&self, other: &Self) -> bool {
938 self.as_cache_key_ref().eq(&other.as_cache_key_ref())
939 }
940}
941
942impl Hash for CacheKey {
943 fn hash<H: Hasher>(&self, state: &mut H) {
944 self.as_cache_key_ref().hash(state);
945 }
946}
947
948impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
949 fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
950 self.as_ref() as &dyn AsCacheKeyRef
951 }
952}
953
954impl AsCacheKeyRef for CacheKeyRef<'_> {
955 fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
956 *self
957 }
958}
959
960#[cfg(test)]
961mod tests {
962 use super::*;
963 use crate::GlyphId;
964
965 fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
966 ShapedGlyph {
967 id: GlyphId(0),
968 position: point(px(x), px(0.)),
969 index,
970 is_emoji: false,
971 }
972 }
973
974 fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
975 LineLayout {
976 font_size: px(16.),
977 width: px(100.),
978 ascent: px(12.),
979 descent: px(4.),
980 runs: vec![ShapedRun {
981 font_id: FontId(0),
982 glyphs,
983 }],
984 len: 0,
985 }
986 }
987
988 fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
989 layout.runs[0]
990 .glyphs
991 .iter()
992 .map(|g| f32::from(g.position.x))
993 .collect()
994 }
995
996 #[test]
997 fn test_force_width_latin_unchanged() {
998 let cell_width = px(8.);
999 let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);
1000
1001 apply_force_width_to_layout(&mut layout, cell_width);
1002
1003 let positions = glyph_x_positions(&layout);
1004 assert_eq!(positions, vec![0., 8., 16.]);
1005 }
1006
1007 #[test]
1008 fn test_force_width_combining_marks_not_advanced() {
1009 let cell_width = px(8.);
1010 // Simulates Thai "ā¸ā¸ĩ" â base consonant at x=0, combining vowel also at x=0
1011 let mut layout = make_layout(vec![
1012 glyph_at(0., 0), // ⏠(base)
1013 glyph_at(0., 3), // ā¸ĩ (combining mark, same x)
1014 ]);
1015
1016 apply_force_width_to_layout(&mut layout, cell_width);
1017
1018 let positions = glyph_x_positions(&layout);
1019 assert_eq!(positions, vec![0., 0.]);
1020 }
1021
1022 #[test]
1023 fn test_force_width_base_after_combining_mark() {
1024 let cell_width = px(8.);
1025 let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);
1026
1027 apply_force_width_to_layout(&mut layout, cell_width);
1028
1029 let positions = glyph_x_positions(&layout);
1030 assert_eq!(positions, vec![0., 0., 8.]);
1031 }
1032
1033 #[test]
1034 fn test_force_width_multiple_combining_marks() {
1035 let cell_width = px(8.);
1036 // Simulates "ā¸āš" â base + vowel + tone mark (two combining marks stacked)
1037 let mut layout = make_layout(vec![
1038 glyph_at(0., 0), // ⏠(base)
1039 glyph_at(0., 3), // vowel (combining)
1040 glyph_at(0., 6), // tone mark (combining)
1041 glyph_at(8., 9), // next base
1042 ]);
1043
1044 apply_force_width_to_layout(&mut layout, cell_width);
1045
1046 let positions = glyph_x_positions(&layout);
1047 assert_eq!(positions, vec![0., 0., 0., 8.]);
1048 }
1049
1050 #[test]
1051 fn test_force_width_corrects_drifted_base_positions() {
1052 let cell_width = px(8.);
1053 // Font metrics don't perfectly match cell grid â glyphs drift >1px from cell boundary
1054 let mut layout = make_layout(vec![
1055 glyph_at(0.5, 0), // within 1px tolerance, kept as-is
1056 glyph_at(10.2, 1), // >1px off from 8.0, corrected
1057 glyph_at(19.8, 2), // >1px off from 16.0, corrected
1058 ]);
1059
1060 apply_force_width_to_layout(&mut layout, cell_width);
1061
1062 let positions = glyph_x_positions(&layout);
1063 assert_eq!(positions, vec![0.5, 8., 16.]);
1064 }
1065
1066 #[test]
1067 fn test_force_width_combining_mark_after_within_tolerance_base() {
1068 let cell_width = px(8.);
1069 // Base glyph is within 1px of grid so it keeps its shaped position.
1070 // The combining mark must align to the base's actual position, not the grid slot.
1071 let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);
1072
1073 apply_force_width_to_layout(&mut layout, cell_width);
1074
1075 let positions = glyph_x_positions(&layout);
1076 assert_eq!(positions, vec![0.5, 0.5]);
1077 }
1078}