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                Ok(self
419                    .unwrapped_layout
420                    .index_for_x(position_in_unwrapped_line.x)
421                    .unwrap())
422            }
423        }
424    }
425
426    /// Returns the pixel position for the given byte index.
427    pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
428        let mut line_start_ix = 0;
429        let mut line_end_indices = self
430            .wrap_boundaries
431            .iter()
432            .map(|wrap_boundary| {
433                let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
434                let glyph = &run.glyphs[wrap_boundary.glyph_ix];
435                glyph.index
436            })
437            .chain([self.len()])
438            .enumerate();
439        for (ix, line_end_ix) in line_end_indices {
440            let line_y = ix as f32 * line_height;
441            if index < line_start_ix {
442                break;
443            } else if index > line_end_ix {
444                line_start_ix = line_end_ix;
445                continue;
446            } else {
447                let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
448                let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
449                return Some(point(x, line_y));
450            }
451        }
452
453        None
454    }
455}
456
457pub(crate) struct LineLayoutCache {
458    previous_frame: Mutex<FrameCache>,
459    current_frame: RwLock<FrameCache>,
460    platform_text_system: Arc<dyn PlatformTextSystem>,
461    /// Advances when [`TextSystem::add_fonts`] successfully changes the font database.
462    font_generation: Arc<AtomicUsize>,
463    /// Records the generation represented by both frame caches.
464    cached_font_generation: AtomicUsize,
465}
466
467#[derive(Default)]
468struct FrameCache {
469    lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
470    wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
471    used_lines: Vec<Arc<CacheKey>>,
472    used_wrapped_lines: Vec<Arc<CacheKey>>,
473
474    // Content-addressable caches keyed by caller-provided text hash + layout params.
475    // These allow cache hits without materializing a contiguous `SharedString`.
476    //
477    // IMPORTANT: To support allocation-free lookups, we store these maps using a key type
478    // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`.
479    // On miss, we allocate once and store under an owned `HashedCacheKey`.
480    lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<LineLayout>>,
481    wrapped_lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<WrappedLineLayout>>,
482    used_lines_by_hash: Vec<Arc<HashedCacheKey>>,
483    used_wrapped_lines_by_hash: Vec<Arc<HashedCacheKey>>,
484}
485
486#[derive(Clone, Default)]
487pub(crate) struct LineLayoutIndex {
488    font_generation: usize,
489    lines_index: usize,
490    wrapped_lines_index: usize,
491    lines_by_hash_index: usize,
492    wrapped_lines_by_hash_index: usize,
493}
494
495impl LineLayoutCache {
496    pub fn new(
497        platform_text_system: Arc<dyn PlatformTextSystem>,
498        font_generation: Arc<AtomicUsize>,
499    ) -> Self {
500        let cached_font_generation = font_generation.load(Ordering::Acquire);
501        Self {
502            previous_frame: Mutex::default(),
503            current_frame: RwLock::default(),
504            platform_text_system,
505            font_generation,
506            cached_font_generation: AtomicUsize::new(cached_font_generation),
507        }
508    }
509
510    pub fn layout_index(&self) -> LineLayoutIndex {
511        let font_generation = self.clear_if_font_generation_changed();
512        let frame = self.current_frame.read();
513        LineLayoutIndex {
514            font_generation,
515            lines_index: frame.used_lines.len(),
516            wrapped_lines_index: frame.used_wrapped_lines.len(),
517            lines_by_hash_index: frame.used_lines_by_hash.len(),
518            wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(),
519        }
520    }
521
522    pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
523        let font_generation = self.clear_if_font_generation_changed();
524        if range.start.font_generation != font_generation
525            || range.end.font_generation != font_generation
526        {
527            return;
528        }
529        let mut current_frame = &mut *self.current_frame.write();
530        let mut previous_frame = &mut *self.previous_frame.lock();
531
532        for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
533            if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
534                current_frame.lines.insert(key, line);
535            }
536            current_frame.used_lines.push(key.clone());
537        }
538
539        for key in &previous_frame.used_wrapped_lines
540            [range.start.wrapped_lines_index..range.end.wrapped_lines_index]
541        {
542            if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
543                current_frame.wrapped_lines.insert(key, line);
544            }
545            current_frame.used_wrapped_lines.push(key.clone());
546        }
547
548        for key in &previous_frame.used_lines_by_hash
549            [range.start.lines_by_hash_index..range.end.lines_by_hash_index]
550        {
551            if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) {
552                current_frame.lines_by_hash.insert(key, line);
553            }
554            current_frame.used_lines_by_hash.push(key.clone());
555        }
556
557        for key in &previous_frame.used_wrapped_lines_by_hash
558            [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index]
559        {
560            if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) {
561                current_frame.wrapped_lines_by_hash.insert(key, line);
562            }
563            current_frame.used_wrapped_lines_by_hash.push(key.clone());
564        }
565    }
566
567    pub fn truncate_layouts(&self, index: LineLayoutIndex) {
568        let font_generation = self.clear_if_font_generation_changed();
569        if index.font_generation != font_generation {
570            return;
571        }
572        let mut current_frame = &mut *self.current_frame.write();
573        current_frame.used_lines.truncate(index.lines_index);
574        current_frame
575            .used_wrapped_lines
576            .truncate(index.wrapped_lines_index);
577        current_frame
578            .used_lines_by_hash
579            .truncate(index.lines_by_hash_index);
580        current_frame
581            .used_wrapped_lines_by_hash
582            .truncate(index.wrapped_lines_by_hash_index);
583    }
584
585    pub fn finish_frame(&self) {
586        let _font_generation = self.clear_if_font_generation_changed();
587        let mut curr_frame = self.current_frame.write();
588        let mut prev_frame = self.previous_frame.lock();
589        std::mem::swap(&mut *prev_frame, &mut *curr_frame);
590        curr_frame.lines.clear();
591        curr_frame.wrapped_lines.clear();
592        curr_frame.used_lines.clear();
593        curr_frame.used_wrapped_lines.clear();
594
595        curr_frame.lines_by_hash.clear();
596        curr_frame.wrapped_lines_by_hash.clear();
597        curr_frame.used_lines_by_hash.clear();
598        curr_frame.used_wrapped_lines_by_hash.clear();
599    }
600
601    pub fn layout_wrapped_line<Text>(
602        &self,
603        text: Text,
604        font_size: Pixels,
605        runs: &[FontRun],
606        wrap_width: Option<Pixels>,
607        max_lines: Option<usize>,
608    ) -> Arc<WrappedLineLayout>
609    where
610        Text: AsRef<str>,
611        SharedString: From<Text>,
612    {
613        let _font_generation = self.clear_if_font_generation_changed();
614        let key = &CacheKeyRef {
615            text: text.as_ref(),
616            font_size,
617            runs,
618            wrap_width,
619            force_width: None,
620        } as &dyn AsCacheKeyRef;
621
622        let current_frame = self.current_frame.upgradable_read();
623        if let Some(layout) = current_frame.wrapped_lines.get(key) {
624            return layout.clone();
625        }
626
627        let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key);
628        if let Some((key, layout)) = previous_frame_entry {
629            let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
630            current_frame
631                .wrapped_lines
632                .insert(key.clone(), layout.clone());
633            current_frame.used_wrapped_lines.push(key);
634            layout
635        } else {
636            drop(current_frame);
637            let text = SharedString::from(text);
638            let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
639            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
640                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines)
641            } else {
642                SmallVec::new()
643            };
644            let layout = Arc::new(WrappedLineLayout {
645                unwrapped_layout,
646                wrap_boundaries,
647                wrap_width,
648            });
649            let key = Arc::new(CacheKey {
650                text,
651                font_size,
652                runs: SmallVec::from(runs),
653                wrap_width,
654                force_width: None,
655            });
656
657            let mut current_frame = self.current_frame.write();
658            current_frame
659                .wrapped_lines
660                .insert(key.clone(), layout.clone());
661            current_frame.used_wrapped_lines.push(key);
662
663            layout
664        }
665    }
666
667    pub fn layout_line<Text>(
668        &self,
669        text: Text,
670        font_size: Pixels,
671        runs: &[FontRun],
672        force_width: Option<Pixels>,
673    ) -> Arc<LineLayout>
674    where
675        Text: AsRef<str>,
676        SharedString: From<Text>,
677    {
678        let _font_generation = self.clear_if_font_generation_changed();
679        let key = &CacheKeyRef {
680            text: text.as_ref(),
681            font_size,
682            runs,
683            wrap_width: None,
684            force_width,
685        } as &dyn AsCacheKeyRef;
686
687        let current_frame = self.current_frame.upgradable_read();
688        if let Some(layout) = current_frame.lines.get(key) {
689            return layout.clone();
690        }
691
692        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
693        if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
694            current_frame.lines.insert(key.clone(), layout.clone());
695            current_frame.used_lines.push(key);
696            layout
697        } else {
698            let text = SharedString::from(text);
699            let mut layout = self
700                .platform_text_system
701                .layout_line(&text, font_size, runs);
702
703            if let Some(force_width) = force_width {
704                apply_force_width_to_layout(&mut layout, force_width);
705            }
706
707            let key = Arc::new(CacheKey {
708                text,
709                font_size,
710                runs: SmallVec::from(runs),
711                wrap_width: None,
712                force_width,
713            });
714            let layout = Arc::new(layout);
715            current_frame.lines.insert(key.clone(), layout.clone());
716            current_frame.used_lines.push(key);
717            layout
718        }
719    }
720
721    /// Try to retrieve a previously-shaped line layout using a caller-provided content hash.
722    ///
723    /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout
724    /// is not already cached in either the current frame or previous frame, returns `None`.
725    ///
726    /// Contract (caller enforced):
727    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
728    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
729    pub fn try_layout_line_by_hash(
730        &self,
731        text_hash: u64,
732        text_len: usize,
733        font_size: Pixels,
734        runs: &[FontRun],
735        force_width: Option<Pixels>,
736    ) -> Option<Arc<LineLayout>> {
737        let _font_generation = self.clear_if_font_generation_changed();
738        let key_ref = HashedCacheKeyRef {
739            text_hash,
740            text_len,
741            font_size,
742            runs,
743            wrap_width: None,
744            force_width,
745        };
746
747        let current_frame = self.current_frame.read();
748        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
749            HashedCacheKeyRef {
750                text_hash: key.text_hash,
751                text_len: key.text_len,
752                font_size: key.font_size,
753                runs: key.runs.as_slice(),
754                wrap_width: key.wrap_width,
755                force_width: key.force_width,
756            } == key_ref
757        }) {
758            return Some(layout.clone());
759        }
760
761        let previous_frame = self.previous_frame.lock();
762        if let Some((_, layout)) = previous_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        None
776    }
777
778    /// Layout a line of text using a caller-provided content hash as the cache key.
779    ///
780    /// This enables cache hits without materializing a contiguous `SharedString` for `text`.
781    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
782    ///
783    /// Contract (caller enforced):
784    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
785    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
786    pub fn layout_line_by_hash(
787        &self,
788        text_hash: u64,
789        text_len: usize,
790        font_size: Pixels,
791        runs: &[FontRun],
792        force_width: Option<Pixels>,
793        materialize_text: impl FnOnce() -> SharedString,
794    ) -> Arc<LineLayout> {
795        let _font_generation = self.clear_if_font_generation_changed();
796        let key_ref = HashedCacheKeyRef {
797            text_hash,
798            text_len,
799            font_size,
800            runs,
801            wrap_width: None,
802            force_width,
803        };
804
805        // Fast path: already cached (no allocation).
806        let current_frame = self.current_frame.upgradable_read();
807        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
808            HashedCacheKeyRef {
809                text_hash: key.text_hash,
810                text_len: key.text_len,
811                font_size: key.font_size,
812                runs: key.runs.as_slice(),
813                wrap_width: key.wrap_width,
814                force_width: key.force_width,
815            } == key_ref
816        }) {
817            return layout.clone();
818        }
819
820        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
821
822        // Try to reuse from previous frame without allocating; do a linear scan to find a matching key.
823        // (We avoid `drain()` here because it would eagerly move all entries.)
824        let mut previous_frame = self.previous_frame.lock();
825        if let Some(existing_key) = previous_frame
826            .used_lines_by_hash
827            .iter()
828            .find(|key| {
829                HashedCacheKeyRef {
830                    text_hash: key.text_hash,
831                    text_len: key.text_len,
832                    font_size: key.font_size,
833                    runs: key.runs.as_slice(),
834                    wrap_width: key.wrap_width,
835                    force_width: key.force_width,
836                } == key_ref
837            })
838            .cloned()
839        {
840            if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) {
841                current_frame
842                    .lines_by_hash
843                    .insert(key.clone(), layout.clone());
844                current_frame.used_lines_by_hash.push(key);
845                return layout;
846            }
847        }
848
849        let text = materialize_text();
850        let mut layout = self
851            .platform_text_system
852            .layout_line(&text, font_size, runs);
853
854        if let Some(force_width) = force_width {
855            apply_force_width_to_layout(&mut layout, force_width);
856        }
857
858        let key = Arc::new(HashedCacheKey {
859            text_hash,
860            text_len,
861            font_size,
862            runs: SmallVec::from(runs),
863            wrap_width: None,
864            force_width,
865        });
866        let layout = Arc::new(layout);
867        current_frame
868            .lines_by_hash
869            .insert(key.clone(), layout.clone());
870        current_frame.used_lines_by_hash.push(key);
871        layout
872    }
873
874    fn clear_if_font_generation_changed(&self) -> usize {
875        let font_generation = self.font_generation.load(Ordering::Acquire);
876        if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
877            return font_generation;
878        }
879
880        let mut current_frame = self.current_frame.write();
881        if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
882            return font_generation;
883        }
884
885        *current_frame = FrameCache::default();
886        *self.previous_frame.lock() = FrameCache::default();
887        self.cached_font_generation
888            .store(font_generation, Ordering::Release);
889        font_generation
890    }
891}
892
893// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by
894// HarfBuzz at the same x position as their base character. The force-width
895// loop must not advance the cell counter for these zero-advance glyphs,
896// otherwise they get displaced into the next cell. We detect them by checking
897// whether shaped x has advanced by at least half a cell beyond the last base.
898fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
899    let mut glyph_pos: usize = 0;
900    // NEG_INFINITY ensures the first glyph is always classified as a base.
901    let mut last_base_shaped_x = px(f32::NEG_INFINITY);
902    let mut last_base_actual_x = px(0.);
903
904    for run in layout.runs.iter_mut() {
905        for glyph in run.glyphs.iter_mut() {
906            let shaped_x = glyph.position.x;
907
908            if shaped_x > last_base_shaped_x + force_width * 0.5 {
909                let forced_x = glyph_pos * force_width;
910                if (shaped_x - forced_x).abs() > px(1.) {
911                    glyph.position.x = forced_x;
912                }
913                last_base_shaped_x = shaped_x;
914                last_base_actual_x = glyph.position.x;
915                glyph_pos += 1;
916            } else {
917                glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
918            }
919        }
920    }
921}
922
923/// A run of text with a single font.
924#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
925#[expect(missing_docs)]
926pub struct FontRun {
927    pub len: usize,
928    pub font_id: FontId,
929}
930
931trait AsCacheKeyRef {
932    fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
933}
934
935#[derive(Clone, Debug, Eq)]
936struct CacheKey {
937    text: SharedString,
938    font_size: Pixels,
939    runs: SmallVec<[FontRun; 1]>,
940    wrap_width: Option<Pixels>,
941    force_width: Option<Pixels>,
942}
943
944#[derive(Copy, Clone, PartialEq, Eq, Hash)]
945struct CacheKeyRef<'a> {
946    text: &'a str,
947    font_size: Pixels,
948    runs: &'a [FontRun],
949    wrap_width: Option<Pixels>,
950    force_width: Option<Pixels>,
951}
952
953#[derive(Clone, Debug)]
954struct HashedCacheKey {
955    text_hash: u64,
956    text_len: usize,
957    font_size: Pixels,
958    runs: SmallVec<[FontRun; 1]>,
959    wrap_width: Option<Pixels>,
960    force_width: Option<Pixels>,
961}
962
963#[derive(Copy, Clone)]
964struct HashedCacheKeyRef<'a> {
965    text_hash: u64,
966    text_len: usize,
967    font_size: Pixels,
968    runs: &'a [FontRun],
969    wrap_width: Option<Pixels>,
970    force_width: Option<Pixels>,
971}
972
973impl PartialEq for dyn AsCacheKeyRef + '_ {
974    fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
975        self.as_cache_key_ref() == other.as_cache_key_ref()
976    }
977}
978
979impl PartialEq for HashedCacheKey {
980    fn eq(&self, other: &Self) -> bool {
981        self.text_hash == other.text_hash
982            && self.text_len == other.text_len
983            && self.font_size == other.font_size
984            && self.runs.as_slice() == other.runs.as_slice()
985            && self.wrap_width == other.wrap_width
986            && self.force_width == other.force_width
987    }
988}
989
990impl Eq for HashedCacheKey {}
991
992impl Hash for HashedCacheKey {
993    fn hash<H: Hasher>(&self, state: &mut H) {
994        self.text_hash.hash(state);
995        self.text_len.hash(state);
996        self.font_size.hash(state);
997        self.runs.as_slice().hash(state);
998        self.wrap_width.hash(state);
999        self.force_width.hash(state);
1000    }
1001}
1002
1003impl PartialEq for HashedCacheKeyRef<'_> {
1004    fn eq(&self, other: &Self) -> bool {
1005        self.text_hash == other.text_hash
1006            && self.text_len == other.text_len
1007            && self.font_size == other.font_size
1008            && self.runs == other.runs
1009            && self.wrap_width == other.wrap_width
1010            && self.force_width == other.force_width
1011    }
1012}
1013
1014impl Eq for HashedCacheKeyRef<'_> {}
1015
1016impl Hash for HashedCacheKeyRef<'_> {
1017    fn hash<H: Hasher>(&self, state: &mut H) {
1018        self.text_hash.hash(state);
1019        self.text_len.hash(state);
1020        self.font_size.hash(state);
1021        self.runs.hash(state);
1022        self.wrap_width.hash(state);
1023        self.force_width.hash(state);
1024    }
1025}
1026
1027impl Eq for dyn AsCacheKeyRef + '_ {}
1028
1029impl Hash for dyn AsCacheKeyRef + '_ {
1030    fn hash<H: Hasher>(&self, state: &mut H) {
1031        self.as_cache_key_ref().hash(state)
1032    }
1033}
1034
1035impl AsCacheKeyRef for CacheKey {
1036    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
1037        CacheKeyRef {
1038            text: &self.text,
1039            font_size: self.font_size,
1040            runs: self.runs.as_slice(),
1041            wrap_width: self.wrap_width,
1042            force_width: self.force_width,
1043        }
1044    }
1045}
1046
1047impl PartialEq for CacheKey {
1048    fn eq(&self, other: &Self) -> bool {
1049        self.as_cache_key_ref().eq(&other.as_cache_key_ref())
1050    }
1051}
1052
1053impl Hash for CacheKey {
1054    fn hash<H: Hasher>(&self, state: &mut H) {
1055        self.as_cache_key_ref().hash(state);
1056    }
1057}
1058
1059impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
1060    fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
1061        self.as_ref() as &dyn AsCacheKeyRef
1062    }
1063}
1064
1065impl AsCacheKeyRef for CacheKeyRef<'_> {
1066    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
1067        *self
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use super::*;
1074    use crate::GlyphId;
1075
1076    fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
1077        ShapedGlyph {
1078            id: GlyphId(0),
1079            position: point(px(x), px(0.)),
1080            index,
1081            is_emoji: false,
1082        }
1083    }
1084
1085    fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
1086        LineLayout {
1087            font_size: px(16.),
1088            width: px(100.),
1089            ascent: px(12.),
1090            descent: px(4.),
1091            runs: vec![ShapedRun {
1092                font_id: FontId(0),
1093                glyphs,
1094            }],
1095            len: 0,
1096        }
1097    }
1098
1099    fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
1100        layout.runs[0]
1101            .glyphs
1102            .iter()
1103            .map(|g| f32::from(g.position.x))
1104            .collect()
1105    }
1106
1107    #[test]
1108    fn test_force_width_latin_unchanged() {
1109        let cell_width = px(8.);
1110        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);
1111
1112        apply_force_width_to_layout(&mut layout, cell_width);
1113
1114        let positions = glyph_x_positions(&layout);
1115        assert_eq!(positions, vec![0., 8., 16.]);
1116    }
1117
1118    #[test]
1119    fn test_force_width_combining_marks_not_advanced() {
1120        let cell_width = px(8.);
1121        // Simulates Thai "กี" — base consonant at x=0, combining vowel also at x=0
1122        let mut layout = make_layout(vec![
1123            glyph_at(0., 0), // ก (base)
1124            glyph_at(0., 3), // ี (combining mark, same x)
1125        ]);
1126
1127        apply_force_width_to_layout(&mut layout, cell_width);
1128
1129        let positions = glyph_x_positions(&layout);
1130        assert_eq!(positions, vec![0., 0.]);
1131    }
1132
1133    #[test]
1134    fn test_force_width_base_after_combining_mark() {
1135        let cell_width = px(8.);
1136        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);
1137
1138        apply_force_width_to_layout(&mut layout, cell_width);
1139
1140        let positions = glyph_x_positions(&layout);
1141        assert_eq!(positions, vec![0., 0., 8.]);
1142    }
1143
1144    #[test]
1145    fn test_force_width_multiple_combining_marks() {
1146        let cell_width = px(8.);
1147        // Simulates "ก้" — base + vowel + tone mark (two combining marks stacked)
1148        let mut layout = make_layout(vec![
1149            glyph_at(0., 0), // ก (base)
1150            glyph_at(0., 3), // vowel (combining)
1151            glyph_at(0., 6), // tone mark (combining)
1152            glyph_at(8., 9), // next base
1153        ]);
1154
1155        apply_force_width_to_layout(&mut layout, cell_width);
1156
1157        let positions = glyph_x_positions(&layout);
1158        assert_eq!(positions, vec![0., 0., 0., 8.]);
1159    }
1160
1161    #[test]
1162    fn test_force_width_corrects_drifted_base_positions() {
1163        let cell_width = px(8.);
1164        // Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary
1165        let mut layout = make_layout(vec![
1166            glyph_at(0.5, 0),  // within 1px tolerance, kept as-is
1167            glyph_at(10.2, 1), // >1px off from 8.0, corrected
1168            glyph_at(19.8, 2), // >1px off from 16.0, corrected
1169        ]);
1170
1171        apply_force_width_to_layout(&mut layout, cell_width);
1172
1173        let positions = glyph_x_positions(&layout);
1174        assert_eq!(positions, vec![0.5, 8., 16.]);
1175    }
1176
1177    #[test]
1178    fn test_force_width_combining_mark_after_within_tolerance_base() {
1179        let cell_width = px(8.);
1180        // Base glyph is within 1px of grid so it keeps its shaped position.
1181        // The combining mark must align to the base's actual position, not the grid slot.
1182        let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);
1183
1184        apply_force_width_to_layout(&mut layout, cell_width);
1185
1186        let positions = glyph_x_positions(&layout);
1187        assert_eq!(positions, vec![0.5, 0.5]);
1188    }
1189}