Skip to main content

gpui/text_system/
line_layout.rs

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