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    fn compute_wrap_boundaries(
129        &self,
130        text: &str,
131        wrap_width: Pixels,
132        max_lines: Option<usize>,
133    ) -> SmallVec<[WrapBoundary; 1]> {
134        let mut boundaries = SmallVec::new();
135        let mut first_non_whitespace_ix = None;
136        let mut last_candidate_ix = None;
137        let mut last_candidate_x = px(0.);
138        let mut last_boundary = WrapBoundary {
139            run_ix: 0,
140            glyph_ix: 0,
141        };
142        let mut last_boundary_x = px(0.);
143        let mut prev_ch = '\0';
144        let mut glyphs = self
145            .runs
146            .iter()
147            .enumerate()
148            .flat_map(move |(run_ix, run)| {
149                run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
150                    let character = text[glyph.index..].chars().next().unwrap();
151                    (
152                        WrapBoundary { run_ix, glyph_ix },
153                        character,
154                        glyph.position.x,
155                    )
156                })
157            })
158            .peekable();
159
160        while let Some((boundary, ch, x)) = glyphs.next() {
161            if ch == '\n' {
162                continue;
163            }
164
165            // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping,
166            // but there are some differences, so we have to duplicate the code here.
167            if LineWrapper::is_word_char(ch) {
168                if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() {
169                    last_candidate_ix = Some(boundary);
170                    last_candidate_x = x;
171                }
172            } else {
173                if ch != ' ' && first_non_whitespace_ix.is_some() {
174                    last_candidate_ix = Some(boundary);
175                    last_candidate_x = x;
176                }
177            }
178
179            if ch != ' ' && first_non_whitespace_ix.is_none() {
180                first_non_whitespace_ix = Some(boundary);
181            }
182
183            let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
184            let width = next_x - last_boundary_x;
185
186            if width > wrap_width && boundary > last_boundary {
187                // When used line_clamp, we should limit the number of lines.
188                if let Some(max_lines) = max_lines
189                    && boundaries.len() >= max_lines - 1
190                {
191                    break;
192                }
193
194                if let Some(last_candidate_ix) = last_candidate_ix.take() {
195                    last_boundary = last_candidate_ix;
196                    last_boundary_x = last_candidate_x;
197                } else {
198                    last_boundary = boundary;
199                    last_boundary_x = x;
200                }
201                boundaries.push(last_boundary);
202            }
203            prev_ch = ch;
204        }
205
206        boundaries
207    }
208}
209
210/// A line of text that has been wrapped to fit a given width
211#[derive(Default, Debug)]
212pub struct WrappedLineLayout {
213    /// The line layout, pre-wrapping.
214    pub unwrapped_layout: Arc<LineLayout>,
215
216    /// The boundaries at which the line was wrapped
217    pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
218
219    /// The width of the line, if it was wrapped
220    pub wrap_width: Option<Pixels>,
221}
222
223/// A boundary at which a line was wrapped
224#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
225pub struct WrapBoundary {
226    /// The index in the run just before the line was wrapped
227    pub run_ix: usize,
228    /// The index of the glyph just before the line was wrapped
229    pub glyph_ix: usize,
230}
231
232impl WrappedLineLayout {
233    /// The length of the underlying text, in utf8 bytes.
234    #[allow(clippy::len_without_is_empty)]
235    pub fn len(&self) -> usize {
236        self.unwrapped_layout.len
237    }
238
239    /// The width of this line, in pixels, whether or not it was wrapped.
240    pub fn width(&self) -> Pixels {
241        self.wrap_width
242            .unwrap_or(Pixels::MAX)
243            .min(self.unwrapped_layout.width)
244    }
245
246    /// The size of the whole wrapped text, for the given line_height.
247    /// can span multiple lines if there are multiple wrap boundaries.
248    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
249        Size {
250            width: self.width(),
251            height: line_height * (self.wrap_boundaries.len() + 1),
252        }
253    }
254
255    /// The ascent of a line in this layout
256    pub fn ascent(&self) -> Pixels {
257        self.unwrapped_layout.ascent
258    }
259
260    /// The descent of a line in this layout
261    pub fn descent(&self) -> Pixels {
262        self.unwrapped_layout.descent
263    }
264
265    /// The wrap boundaries in this layout
266    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
267        &self.wrap_boundaries
268    }
269
270    /// The font size of this layout
271    pub fn font_size(&self) -> Pixels {
272        self.unwrapped_layout.font_size
273    }
274
275    /// The runs in this layout, sans wrapping
276    pub fn runs(&self) -> &[ShapedRun] {
277        &self.unwrapped_layout.runs
278    }
279
280    /// The index corresponding to a given position in this layout for the given line height.
281    ///
282    /// See also [`Self::closest_index_for_position`].
283    pub fn index_for_position(
284        &self,
285        position: Point<Pixels>,
286        line_height: Pixels,
287    ) -> Result<usize, usize> {
288        self._index_for_position(position, line_height, false)
289    }
290
291    /// The closest index to a given position in this layout for the given line height.
292    ///
293    /// Closest means the character boundary closest to the given position.
294    ///
295    /// See also [`LineLayout::closest_index_for_x`].
296    pub fn closest_index_for_position(
297        &self,
298        position: Point<Pixels>,
299        line_height: Pixels,
300    ) -> Result<usize, usize> {
301        self._index_for_position(position, line_height, true)
302    }
303
304    fn _index_for_position(
305        &self,
306        mut position: Point<Pixels>,
307        line_height: Pixels,
308        closest: bool,
309    ) -> Result<usize, usize> {
310        let wrapped_line_ix = (position.y / line_height) as usize;
311
312        let wrapped_line_start_index;
313        let wrapped_line_start_x;
314        if wrapped_line_ix > 0 {
315            let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
316                return Err(0);
317            };
318            let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
319            let glyph = &run.glyphs[line_start_boundary.glyph_ix];
320            wrapped_line_start_index = glyph.index;
321            wrapped_line_start_x = glyph.position.x;
322        } else {
323            wrapped_line_start_index = 0;
324            wrapped_line_start_x = Pixels::ZERO;
325        };
326
327        let wrapped_line_end_index;
328        let wrapped_line_end_x;
329        if wrapped_line_ix < self.wrap_boundaries.len() {
330            let next_wrap_boundary_ix = wrapped_line_ix;
331            let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
332            let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
333            let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
334            wrapped_line_end_index = glyph.index;
335            wrapped_line_end_x = glyph.position.x;
336        } else {
337            wrapped_line_end_index = self.unwrapped_layout.len;
338            wrapped_line_end_x = self.unwrapped_layout.width;
339        };
340
341        let mut position_in_unwrapped_line = position;
342        position_in_unwrapped_line.x += wrapped_line_start_x;
343        if position_in_unwrapped_line.x < wrapped_line_start_x {
344            Err(wrapped_line_start_index)
345        } else if position_in_unwrapped_line.x >= wrapped_line_end_x {
346            Err(wrapped_line_end_index)
347        } else {
348            if closest {
349                Ok(self
350                    .unwrapped_layout
351                    .closest_index_for_x(position_in_unwrapped_line.x))
352            } else {
353                Ok(self
354                    .unwrapped_layout
355                    .index_for_x(position_in_unwrapped_line.x)
356                    .unwrap())
357            }
358        }
359    }
360
361    /// Returns the pixel position for the given byte index.
362    pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
363        let mut line_start_ix = 0;
364        let mut line_end_indices = self
365            .wrap_boundaries
366            .iter()
367            .map(|wrap_boundary| {
368                let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
369                let glyph = &run.glyphs[wrap_boundary.glyph_ix];
370                glyph.index
371            })
372            .chain([self.len()])
373            .enumerate();
374        for (ix, line_end_ix) in line_end_indices {
375            let line_y = ix as f32 * line_height;
376            if index < line_start_ix {
377                break;
378            } else if index > line_end_ix {
379                line_start_ix = line_end_ix;
380                continue;
381            } else {
382                let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
383                let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
384                return Some(point(x, line_y));
385            }
386        }
387
388        None
389    }
390}
391
392pub(crate) struct LineLayoutCache {
393    previous_frame: Mutex<FrameCache>,
394    current_frame: RwLock<FrameCache>,
395    platform_text_system: Arc<dyn PlatformTextSystem>,
396}
397
398#[derive(Default)]
399struct FrameCache {
400    lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
401    wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
402    used_lines: Vec<Arc<CacheKey>>,
403    used_wrapped_lines: Vec<Arc<CacheKey>>,
404
405    // Content-addressable caches keyed by caller-provided text hash + layout params.
406    // These allow cache hits without materializing a contiguous `SharedString`.
407    //
408    // IMPORTANT: To support allocation-free lookups, we store these maps using a key type
409    // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`.
410    // On miss, we allocate once and store under an owned `HashedCacheKey`.
411    lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<LineLayout>>,
412    wrapped_lines_by_hash: FxHashMap<Arc<HashedCacheKey>, Arc<WrappedLineLayout>>,
413    used_lines_by_hash: Vec<Arc<HashedCacheKey>>,
414    used_wrapped_lines_by_hash: Vec<Arc<HashedCacheKey>>,
415}
416
417#[derive(Clone, Default)]
418pub(crate) struct LineLayoutIndex {
419    lines_index: usize,
420    wrapped_lines_index: usize,
421    lines_by_hash_index: usize,
422    wrapped_lines_by_hash_index: usize,
423}
424
425impl LineLayoutCache {
426    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
427        Self {
428            previous_frame: Mutex::default(),
429            current_frame: RwLock::default(),
430            platform_text_system,
431        }
432    }
433
434    pub fn layout_index(&self) -> LineLayoutIndex {
435        let frame = self.current_frame.read();
436        LineLayoutIndex {
437            lines_index: frame.used_lines.len(),
438            wrapped_lines_index: frame.used_wrapped_lines.len(),
439            lines_by_hash_index: frame.used_lines_by_hash.len(),
440            wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(),
441        }
442    }
443
444    pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
445        let mut previous_frame = &mut *self.previous_frame.lock();
446        let mut current_frame = &mut *self.current_frame.write();
447
448        for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
449            if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
450                current_frame.lines.insert(key, line);
451            }
452            current_frame.used_lines.push(key.clone());
453        }
454
455        for key in &previous_frame.used_wrapped_lines
456            [range.start.wrapped_lines_index..range.end.wrapped_lines_index]
457        {
458            if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
459                current_frame.wrapped_lines.insert(key, line);
460            }
461            current_frame.used_wrapped_lines.push(key.clone());
462        }
463
464        for key in &previous_frame.used_lines_by_hash
465            [range.start.lines_by_hash_index..range.end.lines_by_hash_index]
466        {
467            if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) {
468                current_frame.lines_by_hash.insert(key, line);
469            }
470            current_frame.used_lines_by_hash.push(key.clone());
471        }
472
473        for key in &previous_frame.used_wrapped_lines_by_hash
474            [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index]
475        {
476            if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) {
477                current_frame.wrapped_lines_by_hash.insert(key, line);
478            }
479            current_frame.used_wrapped_lines_by_hash.push(key.clone());
480        }
481    }
482
483    pub fn truncate_layouts(&self, index: LineLayoutIndex) {
484        let mut current_frame = &mut *self.current_frame.write();
485        current_frame.used_lines.truncate(index.lines_index);
486        current_frame
487            .used_wrapped_lines
488            .truncate(index.wrapped_lines_index);
489        current_frame
490            .used_lines_by_hash
491            .truncate(index.lines_by_hash_index);
492        current_frame
493            .used_wrapped_lines_by_hash
494            .truncate(index.wrapped_lines_by_hash_index);
495    }
496
497    pub fn finish_frame(&self) {
498        let mut prev_frame = self.previous_frame.lock();
499        let mut curr_frame = self.current_frame.write();
500        std::mem::swap(&mut *prev_frame, &mut *curr_frame);
501        curr_frame.lines.clear();
502        curr_frame.wrapped_lines.clear();
503        curr_frame.used_lines.clear();
504        curr_frame.used_wrapped_lines.clear();
505
506        curr_frame.lines_by_hash.clear();
507        curr_frame.wrapped_lines_by_hash.clear();
508        curr_frame.used_lines_by_hash.clear();
509        curr_frame.used_wrapped_lines_by_hash.clear();
510    }
511
512    pub fn layout_wrapped_line<Text>(
513        &self,
514        text: Text,
515        font_size: Pixels,
516        runs: &[FontRun],
517        wrap_width: Option<Pixels>,
518        max_lines: Option<usize>,
519    ) -> Arc<WrappedLineLayout>
520    where
521        Text: AsRef<str>,
522        SharedString: From<Text>,
523    {
524        let key = &CacheKeyRef {
525            text: text.as_ref(),
526            font_size,
527            runs,
528            wrap_width,
529            force_width: None,
530        } as &dyn AsCacheKeyRef;
531
532        let current_frame = self.current_frame.upgradable_read();
533        if let Some(layout) = current_frame.wrapped_lines.get(key) {
534            return layout.clone();
535        }
536
537        let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key);
538        if let Some((key, layout)) = previous_frame_entry {
539            let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
540            current_frame
541                .wrapped_lines
542                .insert(key.clone(), layout.clone());
543            current_frame.used_wrapped_lines.push(key);
544            layout
545        } else {
546            drop(current_frame);
547            let text = SharedString::from(text);
548            let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
549            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
550                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines)
551            } else {
552                SmallVec::new()
553            };
554            let layout = Arc::new(WrappedLineLayout {
555                unwrapped_layout,
556                wrap_boundaries,
557                wrap_width,
558            });
559            let key = Arc::new(CacheKey {
560                text,
561                font_size,
562                runs: SmallVec::from(runs),
563                wrap_width,
564                force_width: None,
565            });
566
567            let mut current_frame = self.current_frame.write();
568            current_frame
569                .wrapped_lines
570                .insert(key.clone(), layout.clone());
571            current_frame.used_wrapped_lines.push(key);
572
573            layout
574        }
575    }
576
577    pub fn layout_line<Text>(
578        &self,
579        text: Text,
580        font_size: Pixels,
581        runs: &[FontRun],
582        force_width: Option<Pixels>,
583    ) -> Arc<LineLayout>
584    where
585        Text: AsRef<str>,
586        SharedString: From<Text>,
587    {
588        let key = &CacheKeyRef {
589            text: text.as_ref(),
590            font_size,
591            runs,
592            wrap_width: None,
593            force_width,
594        } as &dyn AsCacheKeyRef;
595
596        let current_frame = self.current_frame.upgradable_read();
597        if let Some(layout) = current_frame.lines.get(key) {
598            return layout.clone();
599        }
600
601        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
602        if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
603            current_frame.lines.insert(key.clone(), layout.clone());
604            current_frame.used_lines.push(key);
605            layout
606        } else {
607            let text = SharedString::from(text);
608            let mut layout = self
609                .platform_text_system
610                .layout_line(&text, font_size, runs);
611
612            if let Some(force_width) = force_width {
613                let mut glyph_pos = 0;
614                for run in layout.runs.iter_mut() {
615                    for glyph in run.glyphs.iter_mut() {
616                        if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
617                            glyph.position.x = glyph_pos * force_width;
618                        }
619                        glyph_pos += 1;
620                    }
621                }
622            }
623
624            let key = Arc::new(CacheKey {
625                text,
626                font_size,
627                runs: SmallVec::from(runs),
628                wrap_width: None,
629                force_width,
630            });
631            let layout = Arc::new(layout);
632            current_frame.lines.insert(key.clone(), layout.clone());
633            current_frame.used_lines.push(key);
634            layout
635        }
636    }
637
638    /// Try to retrieve a previously-shaped line layout using a caller-provided content hash.
639    ///
640    /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout
641    /// is not already cached in either the current frame or previous frame, returns `None`.
642    ///
643    /// Contract (caller enforced):
644    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
645    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
646    pub fn try_layout_line_by_hash(
647        &self,
648        text_hash: u64,
649        text_len: usize,
650        font_size: Pixels,
651        runs: &[FontRun],
652        force_width: Option<Pixels>,
653    ) -> Option<Arc<LineLayout>> {
654        let key_ref = HashedCacheKeyRef {
655            text_hash,
656            text_len,
657            font_size,
658            runs,
659            wrap_width: None,
660            force_width,
661        };
662
663        let current_frame = self.current_frame.read();
664        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
665            HashedCacheKeyRef {
666                text_hash: key.text_hash,
667                text_len: key.text_len,
668                font_size: key.font_size,
669                runs: key.runs.as_slice(),
670                wrap_width: key.wrap_width,
671                force_width: key.force_width,
672            } == key_ref
673        }) {
674            return Some(layout.clone());
675        }
676
677        let previous_frame = self.previous_frame.lock();
678        if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| {
679            HashedCacheKeyRef {
680                text_hash: key.text_hash,
681                text_len: key.text_len,
682                font_size: key.font_size,
683                runs: key.runs.as_slice(),
684                wrap_width: key.wrap_width,
685                force_width: key.force_width,
686            } == key_ref
687        }) {
688            return Some(layout.clone());
689        }
690
691        None
692    }
693
694    /// Layout a line of text using a caller-provided content hash as the cache key.
695    ///
696    /// This enables cache hits without materializing a contiguous `SharedString` for `text`.
697    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
698    ///
699    /// Contract (caller enforced):
700    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
701    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
702    pub fn layout_line_by_hash(
703        &self,
704        text_hash: u64,
705        text_len: usize,
706        font_size: Pixels,
707        runs: &[FontRun],
708        force_width: Option<Pixels>,
709        materialize_text: impl FnOnce() -> SharedString,
710    ) -> Arc<LineLayout> {
711        let key_ref = HashedCacheKeyRef {
712            text_hash,
713            text_len,
714            font_size,
715            runs,
716            wrap_width: None,
717            force_width,
718        };
719
720        // Fast path: already cached (no allocation).
721        let current_frame = self.current_frame.upgradable_read();
722        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
723            HashedCacheKeyRef {
724                text_hash: key.text_hash,
725                text_len: key.text_len,
726                font_size: key.font_size,
727                runs: key.runs.as_slice(),
728                wrap_width: key.wrap_width,
729                force_width: key.force_width,
730            } == key_ref
731        }) {
732            return layout.clone();
733        }
734
735        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
736
737        // Try to reuse from previous frame without allocating; do a linear scan to find a matching key.
738        // (We avoid `drain()` here because it would eagerly move all entries.)
739        let mut previous_frame = self.previous_frame.lock();
740        if let Some(existing_key) = previous_frame
741            .used_lines_by_hash
742            .iter()
743            .find(|key| {
744                HashedCacheKeyRef {
745                    text_hash: key.text_hash,
746                    text_len: key.text_len,
747                    font_size: key.font_size,
748                    runs: key.runs.as_slice(),
749                    wrap_width: key.wrap_width,
750                    force_width: key.force_width,
751                } == key_ref
752            })
753            .cloned()
754        {
755            if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) {
756                current_frame
757                    .lines_by_hash
758                    .insert(key.clone(), layout.clone());
759                current_frame.used_lines_by_hash.push(key);
760                return layout;
761            }
762        }
763
764        let text = materialize_text();
765        let mut layout = self
766            .platform_text_system
767            .layout_line(&text, font_size, runs);
768
769        if let Some(force_width) = force_width {
770            let mut glyph_pos = 0;
771            for run in layout.runs.iter_mut() {
772                for glyph in run.glyphs.iter_mut() {
773                    if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
774                        glyph.position.x = glyph_pos * force_width;
775                    }
776                    glyph_pos += 1;
777                }
778            }
779        }
780
781        let key = Arc::new(HashedCacheKey {
782            text_hash,
783            text_len,
784            font_size,
785            runs: SmallVec::from(runs),
786            wrap_width: None,
787            force_width,
788        });
789        let layout = Arc::new(layout);
790        current_frame
791            .lines_by_hash
792            .insert(key.clone(), layout.clone());
793        current_frame.used_lines_by_hash.push(key);
794        layout
795    }
796}
797
798/// A run of text with a single font.
799#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
800#[expect(missing_docs)]
801pub struct FontRun {
802    pub len: usize,
803    pub font_id: FontId,
804}
805
806trait AsCacheKeyRef {
807    fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
808}
809
810#[derive(Clone, Debug, Eq)]
811struct CacheKey {
812    text: SharedString,
813    font_size: Pixels,
814    runs: SmallVec<[FontRun; 1]>,
815    wrap_width: Option<Pixels>,
816    force_width: Option<Pixels>,
817}
818
819#[derive(Copy, Clone, PartialEq, Eq, Hash)]
820struct CacheKeyRef<'a> {
821    text: &'a str,
822    font_size: Pixels,
823    runs: &'a [FontRun],
824    wrap_width: Option<Pixels>,
825    force_width: Option<Pixels>,
826}
827
828#[derive(Clone, Debug)]
829struct HashedCacheKey {
830    text_hash: u64,
831    text_len: usize,
832    font_size: Pixels,
833    runs: SmallVec<[FontRun; 1]>,
834    wrap_width: Option<Pixels>,
835    force_width: Option<Pixels>,
836}
837
838#[derive(Copy, Clone)]
839struct HashedCacheKeyRef<'a> {
840    text_hash: u64,
841    text_len: usize,
842    font_size: Pixels,
843    runs: &'a [FontRun],
844    wrap_width: Option<Pixels>,
845    force_width: Option<Pixels>,
846}
847
848impl PartialEq for dyn AsCacheKeyRef + '_ {
849    fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
850        self.as_cache_key_ref() == other.as_cache_key_ref()
851    }
852}
853
854impl PartialEq for HashedCacheKey {
855    fn eq(&self, other: &Self) -> bool {
856        self.text_hash == other.text_hash
857            && self.text_len == other.text_len
858            && self.font_size == other.font_size
859            && self.runs.as_slice() == other.runs.as_slice()
860            && self.wrap_width == other.wrap_width
861            && self.force_width == other.force_width
862    }
863}
864
865impl Eq for HashedCacheKey {}
866
867impl Hash for HashedCacheKey {
868    fn hash<H: Hasher>(&self, state: &mut H) {
869        self.text_hash.hash(state);
870        self.text_len.hash(state);
871        self.font_size.hash(state);
872        self.runs.as_slice().hash(state);
873        self.wrap_width.hash(state);
874        self.force_width.hash(state);
875    }
876}
877
878impl PartialEq for HashedCacheKeyRef<'_> {
879    fn eq(&self, other: &Self) -> bool {
880        self.text_hash == other.text_hash
881            && self.text_len == other.text_len
882            && self.font_size == other.font_size
883            && self.runs == other.runs
884            && self.wrap_width == other.wrap_width
885            && self.force_width == other.force_width
886    }
887}
888
889impl Eq for HashedCacheKeyRef<'_> {}
890
891impl Hash for HashedCacheKeyRef<'_> {
892    fn hash<H: Hasher>(&self, state: &mut H) {
893        self.text_hash.hash(state);
894        self.text_len.hash(state);
895        self.font_size.hash(state);
896        self.runs.hash(state);
897        self.wrap_width.hash(state);
898        self.force_width.hash(state);
899    }
900}
901
902impl Eq for dyn AsCacheKeyRef + '_ {}
903
904impl Hash for dyn AsCacheKeyRef + '_ {
905    fn hash<H: Hasher>(&self, state: &mut H) {
906        self.as_cache_key_ref().hash(state)
907    }
908}
909
910impl AsCacheKeyRef for CacheKey {
911    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
912        CacheKeyRef {
913            text: &self.text,
914            font_size: self.font_size,
915            runs: self.runs.as_slice(),
916            wrap_width: self.wrap_width,
917            force_width: self.force_width,
918        }
919    }
920}
921
922impl PartialEq for CacheKey {
923    fn eq(&self, other: &Self) -> bool {
924        self.as_cache_key_ref().eq(&other.as_cache_key_ref())
925    }
926}
927
928impl Hash for CacheKey {
929    fn hash<H: Hasher>(&self, state: &mut H) {
930        self.as_cache_key_ref().hash(state);
931    }
932}
933
934impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
935    fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
936        self.as_ref() as &dyn AsCacheKeyRef
937    }
938}
939
940impl AsCacheKeyRef for CacheKeyRef<'_> {
941    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
942        *self
943    }
944}