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