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