Skip to main content

gpui/text_system/
line_layout.rs

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