Skip to main content

gpui/text_system/
line_layout.rs

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