Skip to main content

gpui/text_system/
line_layout.rs

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