Skip to main content

gpui/text_system/
line_layout.rs

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