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.saturating_sub(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                apply_force_width_to_layout(&mut layout, force_width);
 614            }
 615
 616            let key = Arc::new(CacheKey {
 617                text,
 618                font_size,
 619                runs: SmallVec::from(runs),
 620                wrap_width: None,
 621                force_width,
 622            });
 623            let layout = Arc::new(layout);
 624            current_frame.lines.insert(key.clone(), layout.clone());
 625            current_frame.used_lines.push(key);
 626            layout
 627        }
 628    }
 629
 630    /// Try to retrieve a previously-shaped line layout using a caller-provided content hash.
 631    ///
 632    /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout
 633    /// is not already cached in either the current frame or previous frame, returns `None`.
 634    ///
 635    /// Contract (caller enforced):
 636    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
 637    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
 638    pub fn try_layout_line_by_hash(
 639        &self,
 640        text_hash: u64,
 641        text_len: usize,
 642        font_size: Pixels,
 643        runs: &[FontRun],
 644        force_width: Option<Pixels>,
 645    ) -> Option<Arc<LineLayout>> {
 646        let key_ref = HashedCacheKeyRef {
 647            text_hash,
 648            text_len,
 649            font_size,
 650            runs,
 651            wrap_width: None,
 652            force_width,
 653        };
 654
 655        let current_frame = self.current_frame.read();
 656        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
 657            HashedCacheKeyRef {
 658                text_hash: key.text_hash,
 659                text_len: key.text_len,
 660                font_size: key.font_size,
 661                runs: key.runs.as_slice(),
 662                wrap_width: key.wrap_width,
 663                force_width: key.force_width,
 664            } == key_ref
 665        }) {
 666            return Some(layout.clone());
 667        }
 668
 669        let previous_frame = self.previous_frame.lock();
 670        if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| {
 671            HashedCacheKeyRef {
 672                text_hash: key.text_hash,
 673                text_len: key.text_len,
 674                font_size: key.font_size,
 675                runs: key.runs.as_slice(),
 676                wrap_width: key.wrap_width,
 677                force_width: key.force_width,
 678            } == key_ref
 679        }) {
 680            return Some(layout.clone());
 681        }
 682
 683        None
 684    }
 685
 686    /// Layout a line of text using a caller-provided content hash as the cache key.
 687    ///
 688    /// This enables cache hits without materializing a contiguous `SharedString` for `text`.
 689    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
 690    ///
 691    /// Contract (caller enforced):
 692    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
 693    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
 694    pub fn layout_line_by_hash(
 695        &self,
 696        text_hash: u64,
 697        text_len: usize,
 698        font_size: Pixels,
 699        runs: &[FontRun],
 700        force_width: Option<Pixels>,
 701        materialize_text: impl FnOnce() -> SharedString,
 702    ) -> Arc<LineLayout> {
 703        let key_ref = HashedCacheKeyRef {
 704            text_hash,
 705            text_len,
 706            font_size,
 707            runs,
 708            wrap_width: None,
 709            force_width,
 710        };
 711
 712        // Fast path: already cached (no allocation).
 713        let current_frame = self.current_frame.upgradable_read();
 714        if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| {
 715            HashedCacheKeyRef {
 716                text_hash: key.text_hash,
 717                text_len: key.text_len,
 718                font_size: key.font_size,
 719                runs: key.runs.as_slice(),
 720                wrap_width: key.wrap_width,
 721                force_width: key.force_width,
 722            } == key_ref
 723        }) {
 724            return layout.clone();
 725        }
 726
 727        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
 728
 729        // Try to reuse from previous frame without allocating; do a linear scan to find a matching key.
 730        // (We avoid `drain()` here because it would eagerly move all entries.)
 731        let mut previous_frame = self.previous_frame.lock();
 732        if let Some(existing_key) = previous_frame
 733            .used_lines_by_hash
 734            .iter()
 735            .find(|key| {
 736                HashedCacheKeyRef {
 737                    text_hash: key.text_hash,
 738                    text_len: key.text_len,
 739                    font_size: key.font_size,
 740                    runs: key.runs.as_slice(),
 741                    wrap_width: key.wrap_width,
 742                    force_width: key.force_width,
 743                } == key_ref
 744            })
 745            .cloned()
 746        {
 747            if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) {
 748                current_frame
 749                    .lines_by_hash
 750                    .insert(key.clone(), layout.clone());
 751                current_frame.used_lines_by_hash.push(key);
 752                return layout;
 753            }
 754        }
 755
 756        let text = materialize_text();
 757        let mut layout = self
 758            .platform_text_system
 759            .layout_line(&text, font_size, runs);
 760
 761        if let Some(force_width) = force_width {
 762            apply_force_width_to_layout(&mut layout, force_width);
 763        }
 764
 765        let key = Arc::new(HashedCacheKey {
 766            text_hash,
 767            text_len,
 768            font_size,
 769            runs: SmallVec::from(runs),
 770            wrap_width: None,
 771            force_width,
 772        });
 773        let layout = Arc::new(layout);
 774        current_frame
 775            .lines_by_hash
 776            .insert(key.clone(), layout.clone());
 777        current_frame.used_lines_by_hash.push(key);
 778        layout
 779    }
 780}
 781
 782// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by
 783// HarfBuzz at the same x position as their base character. The force-width
 784// loop must not advance the cell counter for these zero-advance glyphs,
 785// otherwise they get displaced into the next cell. We detect them by checking
 786// whether shaped x has advanced by at least half a cell beyond the last base.
 787fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
 788    let mut glyph_pos: usize = 0;
 789    // NEG_INFINITY ensures the first glyph is always classified as a base.
 790    let mut last_base_shaped_x = px(f32::NEG_INFINITY);
 791    let mut last_base_actual_x = px(0.);
 792
 793    for run in layout.runs.iter_mut() {
 794        for glyph in run.glyphs.iter_mut() {
 795            let shaped_x = glyph.position.x;
 796
 797            if shaped_x > last_base_shaped_x + force_width * 0.5 {
 798                let forced_x = glyph_pos * force_width;
 799                if (shaped_x - forced_x).abs() > px(1.) {
 800                    glyph.position.x = forced_x;
 801                }
 802                last_base_shaped_x = shaped_x;
 803                last_base_actual_x = glyph.position.x;
 804                glyph_pos += 1;
 805            } else {
 806                glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
 807            }
 808        }
 809    }
 810}
 811
 812/// A run of text with a single font.
 813#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 814#[expect(missing_docs)]
 815pub struct FontRun {
 816    pub len: usize,
 817    pub font_id: FontId,
 818}
 819
 820trait AsCacheKeyRef {
 821    fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
 822}
 823
 824#[derive(Clone, Debug, Eq)]
 825struct CacheKey {
 826    text: SharedString,
 827    font_size: Pixels,
 828    runs: SmallVec<[FontRun; 1]>,
 829    wrap_width: Option<Pixels>,
 830    force_width: Option<Pixels>,
 831}
 832
 833#[derive(Copy, Clone, PartialEq, Eq, Hash)]
 834struct CacheKeyRef<'a> {
 835    text: &'a str,
 836    font_size: Pixels,
 837    runs: &'a [FontRun],
 838    wrap_width: Option<Pixels>,
 839    force_width: Option<Pixels>,
 840}
 841
 842#[derive(Clone, Debug)]
 843struct HashedCacheKey {
 844    text_hash: u64,
 845    text_len: usize,
 846    font_size: Pixels,
 847    runs: SmallVec<[FontRun; 1]>,
 848    wrap_width: Option<Pixels>,
 849    force_width: Option<Pixels>,
 850}
 851
 852#[derive(Copy, Clone)]
 853struct HashedCacheKeyRef<'a> {
 854    text_hash: u64,
 855    text_len: usize,
 856    font_size: Pixels,
 857    runs: &'a [FontRun],
 858    wrap_width: Option<Pixels>,
 859    force_width: Option<Pixels>,
 860}
 861
 862impl PartialEq for dyn AsCacheKeyRef + '_ {
 863    fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
 864        self.as_cache_key_ref() == other.as_cache_key_ref()
 865    }
 866}
 867
 868impl PartialEq for HashedCacheKey {
 869    fn eq(&self, other: &Self) -> bool {
 870        self.text_hash == other.text_hash
 871            && self.text_len == other.text_len
 872            && self.font_size == other.font_size
 873            && self.runs.as_slice() == other.runs.as_slice()
 874            && self.wrap_width == other.wrap_width
 875            && self.force_width == other.force_width
 876    }
 877}
 878
 879impl Eq for HashedCacheKey {}
 880
 881impl Hash for HashedCacheKey {
 882    fn hash<H: Hasher>(&self, state: &mut H) {
 883        self.text_hash.hash(state);
 884        self.text_len.hash(state);
 885        self.font_size.hash(state);
 886        self.runs.as_slice().hash(state);
 887        self.wrap_width.hash(state);
 888        self.force_width.hash(state);
 889    }
 890}
 891
 892impl PartialEq for HashedCacheKeyRef<'_> {
 893    fn eq(&self, other: &Self) -> bool {
 894        self.text_hash == other.text_hash
 895            && self.text_len == other.text_len
 896            && self.font_size == other.font_size
 897            && self.runs == other.runs
 898            && self.wrap_width == other.wrap_width
 899            && self.force_width == other.force_width
 900    }
 901}
 902
 903impl Eq for HashedCacheKeyRef<'_> {}
 904
 905impl Hash for HashedCacheKeyRef<'_> {
 906    fn hash<H: Hasher>(&self, state: &mut H) {
 907        self.text_hash.hash(state);
 908        self.text_len.hash(state);
 909        self.font_size.hash(state);
 910        self.runs.hash(state);
 911        self.wrap_width.hash(state);
 912        self.force_width.hash(state);
 913    }
 914}
 915
 916impl Eq for dyn AsCacheKeyRef + '_ {}
 917
 918impl Hash for dyn AsCacheKeyRef + '_ {
 919    fn hash<H: Hasher>(&self, state: &mut H) {
 920        self.as_cache_key_ref().hash(state)
 921    }
 922}
 923
 924impl AsCacheKeyRef for CacheKey {
 925    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
 926        CacheKeyRef {
 927            text: &self.text,
 928            font_size: self.font_size,
 929            runs: self.runs.as_slice(),
 930            wrap_width: self.wrap_width,
 931            force_width: self.force_width,
 932        }
 933    }
 934}
 935
 936impl PartialEq for CacheKey {
 937    fn eq(&self, other: &Self) -> bool {
 938        self.as_cache_key_ref().eq(&other.as_cache_key_ref())
 939    }
 940}
 941
 942impl Hash for CacheKey {
 943    fn hash<H: Hasher>(&self, state: &mut H) {
 944        self.as_cache_key_ref().hash(state);
 945    }
 946}
 947
 948impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
 949    fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
 950        self.as_ref() as &dyn AsCacheKeyRef
 951    }
 952}
 953
 954impl AsCacheKeyRef for CacheKeyRef<'_> {
 955    fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
 956        *self
 957    }
 958}
 959
 960#[cfg(test)]
 961mod tests {
 962    use super::*;
 963    use crate::GlyphId;
 964
 965    fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
 966        ShapedGlyph {
 967            id: GlyphId(0),
 968            position: point(px(x), px(0.)),
 969            index,
 970            is_emoji: false,
 971        }
 972    }
 973
 974    fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
 975        LineLayout {
 976            font_size: px(16.),
 977            width: px(100.),
 978            ascent: px(12.),
 979            descent: px(4.),
 980            runs: vec![ShapedRun {
 981                font_id: FontId(0),
 982                glyphs,
 983            }],
 984            len: 0,
 985        }
 986    }
 987
 988    fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
 989        layout.runs[0]
 990            .glyphs
 991            .iter()
 992            .map(|g| f32::from(g.position.x))
 993            .collect()
 994    }
 995
 996    #[test]
 997    fn test_force_width_latin_unchanged() {
 998        let cell_width = px(8.);
 999        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);
1000
1001        apply_force_width_to_layout(&mut layout, cell_width);
1002
1003        let positions = glyph_x_positions(&layout);
1004        assert_eq!(positions, vec![0., 8., 16.]);
1005    }
1006
1007    #[test]
1008    fn test_force_width_combining_marks_not_advanced() {
1009        let cell_width = px(8.);
1010        // Simulates Thai "⏁ā¸ĩ" — base consonant at x=0, combining vowel also at x=0
1011        let mut layout = make_layout(vec![
1012            glyph_at(0., 0), // ⏁ (base)
1013            glyph_at(0., 3), // ā¸ĩ (combining mark, same x)
1014        ]);
1015
1016        apply_force_width_to_layout(&mut layout, cell_width);
1017
1018        let positions = glyph_x_positions(&layout);
1019        assert_eq!(positions, vec![0., 0.]);
1020    }
1021
1022    #[test]
1023    fn test_force_width_base_after_combining_mark() {
1024        let cell_width = px(8.);
1025        let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);
1026
1027        apply_force_width_to_layout(&mut layout, cell_width);
1028
1029        let positions = glyph_x_positions(&layout);
1030        assert_eq!(positions, vec![0., 0., 8.]);
1031    }
1032
1033    #[test]
1034    fn test_force_width_multiple_combining_marks() {
1035        let cell_width = px(8.);
1036        // Simulates "ā¸āš‰" — base + vowel + tone mark (two combining marks stacked)
1037        let mut layout = make_layout(vec![
1038            glyph_at(0., 0), // ⏁ (base)
1039            glyph_at(0., 3), // vowel (combining)
1040            glyph_at(0., 6), // tone mark (combining)
1041            glyph_at(8., 9), // next base
1042        ]);
1043
1044        apply_force_width_to_layout(&mut layout, cell_width);
1045
1046        let positions = glyph_x_positions(&layout);
1047        assert_eq!(positions, vec![0., 0., 0., 8.]);
1048    }
1049
1050    #[test]
1051    fn test_force_width_corrects_drifted_base_positions() {
1052        let cell_width = px(8.);
1053        // Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary
1054        let mut layout = make_layout(vec![
1055            glyph_at(0.5, 0),  // within 1px tolerance, kept as-is
1056            glyph_at(10.2, 1), // >1px off from 8.0, corrected
1057            glyph_at(19.8, 2), // >1px off from 16.0, corrected
1058        ]);
1059
1060        apply_force_width_to_layout(&mut layout, cell_width);
1061
1062        let positions = glyph_x_positions(&layout);
1063        assert_eq!(positions, vec![0.5, 8., 16.]);
1064    }
1065
1066    #[test]
1067    fn test_force_width_combining_mark_after_within_tolerance_base() {
1068        let cell_width = px(8.);
1069        // Base glyph is within 1px of grid so it keeps its shaped position.
1070        // The combining mark must align to the base's actual position, not the grid slot.
1071        let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);
1072
1073        apply_force_width_to_layout(&mut layout, cell_width);
1074
1075        let positions = glyph_x_positions(&layout);
1076        assert_eq!(positions, vec![0.5, 0.5]);
1077    }
1078}