Skip to main content

gpui_engine_default/
line_layout.rs

1use collections::FxHashMap;
2use gpui_engine::{
3    FontRun, LineLayout, LineLayoutIndex, PlatformTextSystem, WrapBoundary, WrappedLineLayout,
4};
5use gpui_shared_string::SharedString;
6use gpui_types::{Pixels, px};
7use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
8use smallvec::SmallVec;
9use std::{
10    borrow::Borrow,
11    hash::{Hash, Hasher},
12    ops::Range,
13    sync::{
14        Arc,
15        atomic::{AtomicUsize, Ordering},
16    },
17};
18
19use gpui_engine::LineWrapper;
20
21pub(crate) fn compute_wrap_boundaries(
22    layout: &LineLayout,
23    text: &str,
24    wrap_width: Pixels,
25    max_lines: Option<usize>,
26) -> SmallVec<[WrapBoundary; 1]> {
27    let mut boundaries = SmallVec::new();
28    let mut first_non_whitespace_ix = None;
29    let mut last_candidate_ix = None;
30    let mut last_candidate_x = px(0.);
31    let mut last_boundary = WrapBoundary {
32        run_ix: 0,
33        glyph_ix: 0,
34    };
35    let mut last_boundary_x = px(0.);
36    let mut prev_ch = '\0';
37    let mut glyphs = layout
38        .runs
39        .iter()
40        .enumerate()
41        .flat_map(move |(run_ix, run)| {
42            run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
43                let character = text[glyph.index..].chars().next().unwrap();
44                (
45                    WrapBoundary { run_ix, glyph_ix },
46                    character,
47                    glyph.position.x,
48                )
49            })
50        })
51        .peekable();
52
53    while let Some((boundary, ch, x)) = glyphs.next() {
54        if ch == '\n' {
55            continue;
56        }
57
58        // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping,
59        // but there are some differences, so we have to duplicate the code here.
60        if LineWrapper::is_word_char(ch) {
61            if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() {
62                last_candidate_ix = Some(boundary);
63                last_candidate_x = x;
64            }
65        } else {
66            if ch != ' ' && first_non_whitespace_ix.is_some() {
67                last_candidate_ix = Some(boundary);
68                last_candidate_x = x;
69            }
70        }
71
72        if ch != ' ' && first_non_whitespace_ix.is_none() {
73            first_non_whitespace_ix = Some(boundary);
74        }
75
76        let next_x = glyphs.peek().map_or(layout.width, |(_, _, x)| *x);
77        let width = next_x - last_boundary_x;
78
79        if width > wrap_width && boundary > last_boundary {
80            // When used line_clamp, we should limit the number of lines.
81            if let Some(max_lines) = max_lines
82                && boundaries.len() >= max_lines.saturating_sub(1)
83            {
84                break;
85            }
86
87            if let Some(last_candidate_ix) = last_candidate_ix.take() {
88                last_boundary = last_candidate_ix;
89                last_boundary_x = last_candidate_x;
90            } else {
91                last_boundary = boundary;
92                last_boundary_x = x;
93            }
94            boundaries.push(last_boundary);
95        }
96        prev_ch = ch;
97    }
98
99    boundaries
100}
101
102/// A per-window cache of shaped and wrapped line layouts, retaining the
103/// previous frame so reused layouts survive a frame boundary.
104pub struct LineLayoutCache {
105    previous_frame: Mutex<FrameCache>,
106    current_frame: RwLock<FrameCache>,
107    platform_text_system: Arc<dyn PlatformTextSystem>,
108    /// Advances when [`TextSystem::add_fonts`] successfully changes the font database.
109    font_generation: Arc<AtomicUsize>,
110    /// Records the generation represented by both frame caches.
111    cached_font_generation: AtomicUsize,
112}
113
114#[derive(Default)]
115struct FrameCache {
116    lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
117    wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
118    used_lines: Vec<Arc<CacheKey>>,
119    used_wrapped_lines: Vec<Arc<CacheKey>>,
120
121    // Content-addressable caches keyed by caller-provided text hash + layout params.
122    // These allow cache hits without materializing a contiguous `SharedString`.
123    //
124    // IMPORTANT: To support allocation-free lookups, we store these maps using a key type
125    // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`.
126    // On miss, we allocate once and store under an owned `HashedCacheKey`.
127    lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<LineLayout>>,
128    wrapped_lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<WrappedLineLayout>>,
129    used_lines_by_hash: Vec<Arc<HashedCacheKey>>,
130    used_wrapped_lines_by_hash: Vec<Arc<HashedCacheKey>>,
131}
132
133impl LineLayoutCache {
134    /// Creates an empty cache backed by `platform_text_system`.
135    pub fn new(
136        platform_text_system: Arc<dyn PlatformTextSystem>,
137        font_generation: Arc<AtomicUsize>,
138    ) -> Self {
139        let cached_font_generation = font_generation.load(Ordering::Acquire);
140        Self {
141            previous_frame: Mutex::default(),
142            current_frame: RwLock::default(),
143            platform_text_system,
144            font_generation,
145            cached_font_generation: AtomicUsize::new(cached_font_generation),
146        }
147    }
148
149    /// Saves the current cache position so it can be reused or truncated later.
150    pub fn layout_index(&self) -> LineLayoutIndex {
151        let font_generation = self.clear_if_font_generation_changed();
152        let frame = self.current_frame.read();
153        LineLayoutIndex {
154            font_generation,
155            lines_index: frame.used_lines.len(),
156            wrapped_lines_index: frame.used_wrapped_lines.len(),
157            lines_by_hash_index: frame.used_lines_by_hash.len(),
158            wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(),
159        }
160    }
161
162    /// Re-inserts layouts from the previous frame created before `range`.
163    pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
164        let font_generation = self.clear_if_font_generation_changed();
165        if range.start.font_generation != font_generation
166            || range.end.font_generation != font_generation
167        {
168            return;
169        }
170        let current_frame = &mut *self.current_frame.write();
171        let previous_frame = &mut *self.previous_frame.lock();
172
173        for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
174            if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
175                current_frame.lines.insert(key, line);
176            }
177            current_frame.used_lines.push(key.clone());
178        }
179
180        for key in &previous_frame.used_wrapped_lines
181            [range.start.wrapped_lines_index..range.end.wrapped_lines_index]
182        {
183            if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
184                current_frame.wrapped_lines.insert(key, line);
185            }
186            current_frame.used_wrapped_lines.push(key.clone());
187        }
188
189        for key in &previous_frame.used_lines_by_hash
190            [range.start.lines_by_hash_index..range.end.lines_by_hash_index]
191        {
192            if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) {
193                current_frame.lines_by_hash.insert(key, line);
194            }
195            current_frame.used_lines_by_hash.push(key.clone());
196        }
197
198        for key in &previous_frame.used_wrapped_lines_by_hash
199            [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index]
200        {
201            if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) {
202                current_frame.wrapped_lines_by_hash.insert(key, line);
203            }
204            current_frame.used_wrapped_lines_by_hash.push(key.clone());
205        }
206    }
207
208    /// Drops layouts created after `index`, keeping the cache consistent.
209    pub fn truncate_layouts(&self, index: LineLayoutIndex) {
210        let font_generation = self.clear_if_font_generation_changed();
211        if index.font_generation != font_generation {
212            return;
213        }
214        let current_frame = &mut *self.current_frame.write();
215        current_frame.used_lines.truncate(index.lines_index);
216        current_frame
217            .used_wrapped_lines
218            .truncate(index.wrapped_lines_index);
219        current_frame
220            .used_lines_by_hash
221            .truncate(index.lines_by_hash_index);
222        current_frame
223            .used_wrapped_lines_by_hash
224            .truncate(index.wrapped_lines_by_hash_index);
225    }
226
227    /// Ages the current frame into the previous frame and clears the current one.
228    pub fn finish_frame(&self) {
229        let _font_generation = self.clear_if_font_generation_changed();
230        let mut curr_frame = self.current_frame.write();
231        let mut prev_frame = self.previous_frame.lock();
232        std::mem::swap(&mut *prev_frame, &mut *curr_frame);
233        curr_frame.lines.clear();
234        curr_frame.wrapped_lines.clear();
235        curr_frame.used_lines.clear();
236        curr_frame.used_wrapped_lines.clear();
237
238        curr_frame.lines_by_hash.clear();
239        curr_frame.wrapped_lines_by_hash.clear();
240        curr_frame.used_lines_by_hash.clear();
241        curr_frame.used_wrapped_lines_by_hash.clear();
242    }
243
244    /// Shapes and wraps `text`, reusing a cached layout when the inputs match.
245    pub fn layout_wrapped_line<Text>(
246        &self,
247        text: Text,
248        font_size: Pixels,
249        runs: &[FontRun],
250        wrap_width: Option<Pixels>,
251        max_lines: Option<usize>,
252    ) -> Arc<WrappedLineLayout>
253    where
254        Text: AsRef<str>,
255        SharedString: From<Text>,
256    {
257        let _font_generation = self.clear_if_font_generation_changed();
258        let key = &CacheKeyRef {
259            text: text.as_ref(),
260            font_size,
261            runs,
262            wrap_width,
263            force_width: None,
264        } as &dyn AsCacheKeyRef;
265
266        let current_frame = self.current_frame.upgradable_read();
267        if let Some(layout) = current_frame.wrapped_lines.get(key) {
268            return layout.clone();
269        }
270
271        let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key);
272        if let Some((key, layout)) = previous_frame_entry {
273            let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
274            current_frame
275                .wrapped_lines
276                .insert(key.clone(), layout.clone());
277            current_frame.used_wrapped_lines.push(key);
278            layout
279        } else {
280            drop(current_frame);
281            let text = SharedString::from(text);
282            let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
283            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
284                compute_wrap_boundaries(&unwrapped_layout, text.as_ref(), wrap_width, max_lines)
285            } else {
286                SmallVec::new()
287            };
288            let layout = Arc::new(WrappedLineLayout {
289                unwrapped_layout,
290                wrap_boundaries,
291                wrap_width,
292            });
293            let key = Arc::new(CacheKey {
294                text,
295                font_size,
296                runs: SmallVec::from(runs),
297                wrap_width,
298                force_width: None,
299            });
300
301            let mut current_frame = self.current_frame.write();
302            current_frame
303                .wrapped_lines
304                .insert(key.clone(), layout.clone());
305            current_frame.used_wrapped_lines.push(key);
306
307            layout
308        }
309    }
310
311    /// Shapes a single line of `text`, reusing a cached layout when the inputs match.
312    pub fn layout_line<Text>(
313        &self,
314        text: Text,
315        font_size: Pixels,
316        runs: &[FontRun],
317        force_width: Option<Pixels>,
318    ) -> Arc<LineLayout>
319    where
320        Text: AsRef<str>,
321        SharedString: From<Text>,
322    {
323        let _font_generation = self.clear_if_font_generation_changed();
324        let key = &CacheKeyRef {
325            text: text.as_ref(),
326            font_size,
327            runs,
328            wrap_width: None,
329            force_width,
330        } as &dyn AsCacheKeyRef;
331
332        let current_frame = self.current_frame.upgradable_read();
333        if let Some(layout) = current_frame.lines.get(key) {
334            return layout.clone();
335        }
336
337        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
338        if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
339            current_frame.lines.insert(key.clone(), layout.clone());
340            current_frame.used_lines.push(key);
341            layout
342        } else {
343            let text = SharedString::from(text);
344            let mut layout = self
345                .platform_text_system
346                .layout_line(&text, font_size, runs);
347
348            if let Some(force_width) = force_width {
349                apply_force_width_to_layout(&mut layout, force_width);
350            }
351
352            let key = Arc::new(CacheKey {
353                text,
354                font_size,
355                runs: SmallVec::from(runs),
356                wrap_width: None,
357                force_width,
358            });
359            let layout = Arc::new(layout);
360            current_frame.lines.insert(key.clone(), layout.clone());
361            current_frame.used_lines.push(key);
362            layout
363        }
364    }
365
366    /// Try to retrieve a previously-shaped line layout using a caller-provided content hash.
367    ///
368    /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout
369    /// is not already cached in either the current frame or previous frame, returns `None`.
370    ///
371    /// Contract (caller enforced):
372    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
373    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
374    pub fn try_layout_line_by_hash(
375        &self,
376        text_hash: u64,
377        text_len: usize,
378        font_size: Pixels,
379        runs: &[FontRun],
380        force_width: Option<Pixels>,
381    ) -> Option<Arc<LineLayout>> {
382        let _font_generation = self.clear_if_font_generation_changed();
383        let key_ref = HashedCacheKeyRef {
384            text_hash,
385            text_len,
386            font_size,
387            runs,
388            wrap_width: None,
389            force_width,
390        };
391
392        let current_frame = self.current_frame.read();
393        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
394            HashedCacheKeyRef {
395                text_hash: key.text_hash,
396                text_len: key.text_len,
397                font_size: key.font_size,
398                runs: key.runs.as_slice(),
399                wrap_width: key.wrap_width,
400                force_width: key.force_width,
401            } == key_ref
402        }) {
403            return Some(layout.clone());
404        }
405
406        let previous_frame = self.previous_frame.lock();
407        if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| {
408            HashedCacheKeyRef {
409                text_hash: key.text_hash,
410                text_len: key.text_len,
411                font_size: key.font_size,
412                runs: key.runs.as_slice(),
413                wrap_width: key.wrap_width,
414                force_width: key.force_width,
415            } == key_ref
416        }) {
417            return Some(layout.clone());
418        }
419
420        None
421    }
422
423    /// Layout a line of text using a caller-provided content hash as the cache key.
424    ///
425    /// This enables cache hits without materializing a contiguous `SharedString` for `text`.
426    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
427    ///
428    /// Contract (caller enforced):
429    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
430    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
431    pub fn layout_line_by_hash(
432        &self,
433        text_hash: u64,
434        text_len: usize,
435        font_size: Pixels,
436        runs: &[FontRun],
437        force_width: Option<Pixels>,
438        materialize_text: impl FnOnce() -> SharedString,
439    ) -> Arc<LineLayout> {
440        let _font_generation = self.clear_if_font_generation_changed();
441        let key_ref = HashedCacheKeyRef {
442            text_hash,
443            text_len,
444            font_size,
445            runs,
446            wrap_width: None,
447            force_width,
448        };
449
450        // Fast path: already cached (no allocation).
451        let current_frame = self.current_frame.upgradable_read();
452        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
453            HashedCacheKeyRef {
454                text_hash: key.text_hash,
455                text_len: key.text_len,
456                font_size: key.font_size,
457                runs: key.runs.as_slice(),
458                wrap_width: key.wrap_width,
459                force_width: key.force_width,
460            } == key_ref
461        }) {
462            return layout.clone();
463        }
464
465        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
466
467        // Try to reuse from previous frame without allocating; do a linear scan to find a matching key.
468        // (We avoid `drain()` here because it would eagerly move all entries.)
469        let mut previous_frame = self.previous_frame.lock();
470        if let Some(existing_key) = previous_frame
471            .used_lines_by_hash
472            .iter()
473            .find(|key| {
474                HashedCacheKeyRef {
475                    text_hash: key.text_hash,
476                    text_len: key.text_len,
477                    font_size: key.font_size,
478                    runs: key.runs.as_slice(),
479                    wrap_width: key.wrap_width,
480                    force_width: key.force_width,
481                } == key_ref
482            })
483            .cloned()
484        {
485            if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) {
486                current_frame
487                    .lines_by_hash
488                    .insert(key.clone(), layout.clone());
489                current_frame.used_lines_by_hash.push(key);
490                return layout;
491            }
492        }
493
494        let text = materialize_text();
495        let mut layout = self
496            .platform_text_system
497            .layout_line(&text, font_size, runs);
498
499        if let Some(force_width) = force_width {
500            apply_force_width_to_layout(&mut layout, force_width);
501        }
502
503        let key = Arc::new(HashedCacheKey {
504            text_hash,
505            text_len,
506            font_size,
507            runs: SmallVec::from(runs),
508            wrap_width: None,
509            force_width,
510        });
511        let layout = Arc::new(layout);
512        current_frame
513            .lines_by_hash
514            .insert(key.clone(), layout.clone());
515        current_frame.used_lines_by_hash.push(key);
516        layout
517    }
518
519    fn clear_if_font_generation_changed(&self) -> usize {
520        let font_generation = self.font_generation.load(Ordering::Acquire);
521        if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
522            return font_generation;
523        }
524
525        let mut current_frame = self.current_frame.write();
526        if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
527            return font_generation;
528        }
529
530        *current_frame = FrameCache::default();
531        *self.previous_frame.lock() = FrameCache::default();
532        self.cached_font_generation
533            .store(font_generation, Ordering::Release);
534        font_generation
535    }
536}
537
538// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by
539// HarfBuzz at the same x position as their base character. The force-width
540// loop must not advance the cell counter for these zero-advance glyphs,
541// otherwise they get displaced into the next cell. We detect them by checking
542// whether shaped x has advanced by at least half a cell beyond the last base.
543fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
544    let mut glyph_pos: usize = 0;
545    // NEG_INFINITY ensures the first glyph is always classified as a base.
546    let mut last_base_shaped_x = px(f32::NEG_INFINITY);
547    let mut last_base_actual_x = px(0.);
548
549    for run in layout.runs.iter_mut() {
550        for glyph in run.glyphs.iter_mut() {
551            let shaped_x = glyph.position.x;
552
553            if shaped_x > last_base_shaped_x + force_width * 0.5 {
554                let forced_x = glyph_pos * force_width;
555                if (shaped_x - forced_x).abs() > px(1.) {
556                    glyph.position.x = forced_x;
557                }
558                last_base_shaped_x = shaped_x;
559                last_base_actual_x = glyph.position.x;
560                glyph_pos += 1;
561            } else {
562                glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
563            }
564        }
565    }
566}
567
568/// A laid out and styled line of text
569trait AsCacheKeyRef {
570    fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
571}
572
573#[derive(Clone, Debug, Eq)]
574struct CacheKey {
575    text: SharedString,
576    font_size: Pixels,
577    runs: SmallVec<[FontRun; 1]>,
578    wrap_width: Option<Pixels>,
579    force_width: Option<Pixels>,
580}
581
582#[derive(Copy, Clone, PartialEq, Eq, Hash)]
583struct CacheKeyRef<'a> {
584    text: &'a str,
585    font_size: Pixels,
586    runs: &'a [FontRun],
587    wrap_width: Option<Pixels>,
588    force_width: Option<Pixels>,
589}
590
591#[derive(Clone, Debug)]
592struct HashedCacheKey {
593    text_hash: u64,
594    text_len: usize,
595    font_size: Pixels,
596    runs: SmallVec<[FontRun; 1]>,
597    wrap_width: Option<Pixels>,
598    force_width: Option<Pixels>,
599}
600
601#[derive(Copy, Clone)]
602struct HashedCacheKeyRef<'a> {
603    text_hash: u64,
604    text_len: usize,
605    font_size: Pixels,
606    runs: &'a [FontRun],
607    wrap_width: Option<Pixels>,
608    force_width: Option<Pixels>,
609}
610
611impl PartialEq for dyn AsCacheKeyRef + '_ {
612    fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
613        self.as_cache_key_ref() == other.as_cache_key_ref()
614    }
615}
616
617impl PartialEq for HashedCacheKey {
618    fn eq(&self, other: &Self) -> bool {
619        self.text_hash == other.text_hash
620            && self.text_len == other.text_len
621            && self.font_size == other.font_size
622            && self.runs.as_slice() == other.runs.as_slice()
623            && self.wrap_width == other.wrap_width
624            && self.force_width == other.force_width
625    }
626}
627
628impl Eq for HashedCacheKey {}
629
630impl Hash for HashedCacheKey {
631    fn hash<H: Hasher>(&self, state: &mut H) {
632        self.text_hash.hash(state);
633        self.text_len.hash(state);
634        self.font_size.hash(state);
635        self.runs.as_slice().hash(state);
636        self.wrap_width.hash(state);
637        self.force_width.hash(state);
638    }
639}
640
641impl PartialEq for HashedCacheKeyRef<'_> {
642    fn eq(&self, other: &Self) -> bool {
643        self.text_hash == other.text_hash
644            && self.text_len == other.text_len
645            && self.font_size == other.font_size
646            && self.runs == other.runs
647            && self.wrap_width == other.wrap_width
648            && self.force_width == other.force_width
649    }
650}
651
652impl Eq for HashedCacheKeyRef<'_> {}
653
654impl Hash for HashedCacheKeyRef<'_> {
655    fn hash<H: Hasher>(&self, state: &mut H) {
656        self.text_hash.hash(state);
657        self.text_len.hash(state);
658        self.font_size.hash(state);
659        self.runs.hash(state);
660        self.wrap_width.hash(state);
661        self.force_width.hash(state);
662    }
663}
664
665impl Eq for dyn AsCacheKeyRef + '_ {}
666
667impl Hash for dyn AsCacheKeyRef + '_ {
668    fn hash<H: Hasher>(&self, state: &mut H) {
669        self.as_cache_key_ref().hash(state)
670    }
671}
672
673impl AsCacheKeyRef for CacheKey {
674    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
675        CacheKeyRef {
676            text: &self.text,
677            font_size: self.font_size,
678            runs: self.runs.as_slice(),
679            wrap_width: self.wrap_width,
680            force_width: self.force_width,
681        }
682    }
683}
684
685impl PartialEq for CacheKey {
686    fn eq(&self, other: &Self) -> bool {
687        self.as_cache_key_ref().eq(&other.as_cache_key_ref())
688    }
689}
690
691impl Hash for CacheKey {
692    fn hash<H: Hasher>(&self, state: &mut H) {
693        self.as_cache_key_ref().hash(state);
694    }
695}
696
697impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
698    fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
699        self.as_ref() as &dyn AsCacheKeyRef
700    }
701}
702
703impl AsCacheKeyRef for CacheKeyRef<'_> {
704    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
705        *self
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use gpui_engine::{FontId, GlyphId, ShapedGlyph, ShapedRun};
713    use gpui_types::point;
714
715    fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
716        ShapedGlyph {
717            id: GlyphId(0),
718            position: point(px(x), px(0.)),
719            index,
720            is_emoji: false,
721        }
722    }
723
724    fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
725        LineLayout {
726            font_size: px(16.),
727            width: px(100.),
728            ascent: px(12.),
729            descent: px(4.),
730            runs: vec![ShapedRun {
731                font_id: FontId(0),
732                glyphs,
733            }],
734            len: 0,
735        }
736    }
737
738    fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
739        layout.runs[0]
740            .glyphs
741            .iter()
742            .map(|g| f32::from(g.position.x))
743            .collect()
744    }
745
746    #[test]
747    fn test_force_width_latin_unchanged() {
748        let cell_width = px(8.);
749        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);
750
751        apply_force_width_to_layout(&mut layout, cell_width);
752
753        let positions = glyph_x_positions(&layout);
754        assert_eq!(positions, vec![0., 8., 16.]);
755    }
756
757    #[test]
758    fn test_force_width_combining_marks_not_advanced() {
759        let cell_width = px(8.);
760        // Simulates Thai "กี" — base consonant at x=0, combining vowel also at x=0
761        let mut layout = make_layout(vec![
762            glyph_at(0., 0), // ก (base)
763            glyph_at(0., 3), // ี (combining mark, same x)
764        ]);
765
766        apply_force_width_to_layout(&mut layout, cell_width);
767
768        let positions = glyph_x_positions(&layout);
769        assert_eq!(positions, vec![0., 0.]);
770    }
771
772    #[test]
773    fn test_force_width_base_after_combining_mark() {
774        let cell_width = px(8.);
775        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);
776
777        apply_force_width_to_layout(&mut layout, cell_width);
778
779        let positions = glyph_x_positions(&layout);
780        assert_eq!(positions, vec![0., 0., 8.]);
781    }
782
783    #[test]
784    fn test_force_width_multiple_combining_marks() {
785        let cell_width = px(8.);
786        // Simulates "ก้" — base + vowel + tone mark (two combining marks stacked)
787        let mut layout = make_layout(vec![
788            glyph_at(0., 0), // ก (base)
789            glyph_at(0., 3), // vowel (combining)
790            glyph_at(0., 6), // tone mark (combining)
791            glyph_at(8., 9), // next base
792        ]);
793
794        apply_force_width_to_layout(&mut layout, cell_width);
795
796        let positions = glyph_x_positions(&layout);
797        assert_eq!(positions, vec![0., 0., 0., 8.]);
798    }
799
800    #[test]
801    fn test_force_width_corrects_drifted_base_positions() {
802        let cell_width = px(8.);
803        // Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary
804        let mut layout = make_layout(vec![
805            glyph_at(0.5, 0),  // within 1px tolerance, kept as-is
806            glyph_at(10.2, 1), // >1px off from 8.0, corrected
807            glyph_at(19.8, 2), // >1px off from 16.0, corrected
808        ]);
809
810        apply_force_width_to_layout(&mut layout, cell_width);
811
812        let positions = glyph_x_positions(&layout);
813        assert_eq!(positions, vec![0.5, 8., 16.]);
814    }
815
816    #[test]
817    fn test_force_width_combining_mark_after_within_tolerance_base() {
818        let cell_width = px(8.);
819        // Base glyph is within 1px of grid so it keeps its shaped position.
820        // The combining mark must align to the base's actual position, not the grid slot.
821        let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);
822
823        apply_force_width_to_layout(&mut layout, cell_width);
824
825        let positions = glyph_x_positions(&layout);
826        assert_eq!(positions, vec![0.5, 0.5]);
827    }
828}