Skip to main content

incular_text/
engine.rs

1//! Parley-backed text shaping, fallback, bidi ordering, and line breaking.
2//!
3//! Incular deliberately keeps its renderer-facing glyph protocol small. Parley
4//! owns all font discovery, fallback selection and shaping; this module only
5//! translates its positioned glyph runs into `incular-rendering` commands.
6
7use super::{
8    FontFamily, FontStyle, LineHeight, TextAffinity, TextAlign, TextHeightBehavior,
9    TextLeadingDistribution, TextOverflow, TextStyle,
10};
11use icu_segmenter::GraphemeClusterSegmenter;
12pub use incular_assets::FontId;
13use incular_assets::{FontBytes, FontHandle};
14use incular_config::TextDirection;
15use incular_core::{Offset, Size};
16use incular_rendering::{GlyphPosition, GlyphRun};
17use parley::{
18    Alignment, AlignmentOptions, BaseDirection, FontContext, FontWeight as ParleyFontWeight,
19    Layout, LayoutContext, PositionedLayoutItem, StyleProperty,
20};
21mod document;
22mod pool;
23pub use document::TextDocumentLayout;
24use pool::{DEFAULT_CACHE_BYTE_BUDGET, LAYOUT_CACHE_CAPACITY, LayoutPool};
25
26use std::{
27    borrow::Cow,
28    collections::{HashMap, HashSet, hash_map::DefaultHasher},
29    hash::{Hash, Hasher},
30    sync::{Arc, OnceLock},
31};
32
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct TextMetrics {
35    pub size: Size,
36    pub baseline: f32,
37    pub line_height: f32,
38}
39
40/// A shaped cluster's selectable source coverage, in text-area coordinates
41/// with `left <= right`. Spans follow visual order, so a logical byte
42/// range can cover several disjoint spans in mixed-direction text.
43/// Consumers merge spans intersecting a selection instead of assuming
44/// the range maps to one rectangle. Zero-advance clusters are kept because
45/// they still own caret stops; selection ignores them.
46///
47/// Byte offsets are `u32`, like [`GlyphPosition::cluster`]: a line keeps
48/// one span per cluster, and `usize` offsets made spans cost more than the
49/// glyphs they cover.
50#[derive(Clone, Copy, Debug)]
51pub struct TextClusterSpan {
52    pub start: u32,
53    pub end: u32,
54    pub left: f32,
55    pub right: f32,
56    /// Whether the cluster belongs to a right-to-left run.
57    pub rtl: bool,
58}
59
60impl TextClusterSpan {
61    /// The covered source bytes.
62    #[must_use]
63    pub const fn range(&self) -> std::ops::Range<usize> {
64        self.start as usize..self.end as usize
65    }
66}
67
68impl TextLine {
69    /// Visual x-intervals covered by a half-open logical byte range on
70    /// this line. Collapsed and reversed ranges yield nothing by
71    /// policy; callers pass normalized ranges and never rely on
72    /// rejection. Caret rendering stays separate: collapsed positions
73    /// highlight nothing here and resolve through caret stops instead.
74    ///
75    /// Four granularities meet here and are not interchangeable: the
76    /// range arrives in UTF-8 bytes (clamped to char boundaries
77    /// upstream, so mid-char bytes never arrive); shaping clusters are
78    /// the coverage granularity (a range splitting a cluster expands
79    /// to the whole shaped cluster, since no visual edge exists
80    /// inside it); visual order comes from the stored spans, never
81    /// from re-sorting bytes or glyphs downstream. Touching spans
82    /// merge; genuinely disjoint spans stay separate. An empty line
83    /// covered by a nonempty range yields its caret point. Join controls
84    /// are excluded from source coverage without adding caret stops inside
85    /// the complete shaping cluster.
86    #[must_use]
87    pub fn selection_spans(&self, start: usize, end: usize) -> Vec<(f32, f32)> {
88        if start >= end {
89            return Vec::new();
90        }
91        let mut spans: Vec<(f32, f32)> = self
92            .clusters
93            .iter()
94            .filter(|span| {
95                let range = span.range();
96                span.left < span.right && range.start < end && start < range.end
97            })
98            .map(|span| (span.left, span.right))
99            .collect();
100        if spans.is_empty() && self.start == self.end && start <= self.start && self.end <= end {
101            spans.push((self.offset, self.offset));
102        }
103        spans.sort_by(|left, right| left.0.total_cmp(&right.0));
104        let mut merged: Vec<(f32, f32)> = Vec::with_capacity(spans.len());
105        for (left, right) in spans {
106            if let Some(last) = merged.last_mut()
107                && left <= last.1
108            {
109                last.1 = last.1.max(right);
110            } else {
111                merged.push((left, right));
112            }
113        }
114        merged
115    }
116}
117
118#[derive(Clone, Debug)]
119pub struct TextLine {
120    pub runs: Arc<[Arc<GlyphRun>]>,
121    pub glyphs: Arc<[GlyphPosition]>,
122    pub clusters: Arc<[TextClusterSpan]>,
123    pub width: f32,
124    /// Horizontal alignment offset in text-area coordinates. Glyph and
125    /// caret x positions already include it; `width` stays the unshifted
126    /// advance so extent checks keep working. Consumers mapping absolute
127    /// x positions must add this to `width` instead of re-deriving the
128    /// alignment.
129    pub offset: f32,
130    pub baseline: f32,
131    pub start: usize,
132    /// End of selectable/caret text on this visual line. This excludes a
133    /// trailing hard line-break which has no shaped glyph of its own.
134    pub caret_end: usize,
135    pub end: usize,
136}
137
138#[derive(Clone, Debug)]
139pub struct FontRunDebug {
140    pub range: std::ops::Range<usize>,
141    pub font: FontId,
142    pub direction: &'static str,
143}
144
145/// A visual insertion point emitted by the shaping engine.
146///
147/// A logical byte offset can have two distinct visual positions at a bidi run
148/// boundary. `affinity` keeps those positions separate, matching the concept
149/// used by Parley's cursor implementation and preventing an arrow key from
150/// jumping through a right-to-left run in logical UTF-8 order.
151#[derive(Clone, Copy, Debug, PartialEq)]
152pub struct TextCaretPosition {
153    pub offset: usize,
154    pub affinity: TextAffinity,
155    pub x: f32,
156    pub line: usize,
157}
158
159#[derive(Clone, Debug)]
160struct ClusterPosition {
161    range: std::ops::Range<usize>,
162    left: f32,
163    right: f32,
164    rtl: bool,
165    ligature_start: bool,
166    ligature_continuation: bool,
167}
168
169impl ClusterPosition {
170    fn include(&mut self, other: &Self) {
171        self.range.start = self.range.start.min(other.range.start);
172        self.range.end = self.range.end.max(other.range.end);
173        self.left = self.left.min(other.left);
174        self.right = self.right.max(other.right);
175        self.ligature_start |= other.ligature_start;
176        self.ligature_continuation |= other.ligature_continuation;
177    }
178
179    fn can_join(&self, other: &Self) -> bool {
180        self.rtl == other.rtl
181            && if self.rtl {
182                other.range.end == self.range.start
183            } else {
184                self.range.end == other.range.start
185            }
186    }
187}
188
189/// One lazily derived caret-stop list per laid-out line.
190type LineCaretStops = Arc<[OnceLock<Arc<[TextCaretPosition]>>]>;
191
192#[derive(Clone, Debug)]
193pub struct TextLayout {
194    pub lines: Arc<[TextLine]>,
195    pub metrics: TextMetrics,
196    pub font_runs: Arc<[FontRunDebug]>,
197    /// Caret stops of every line, usually derived on first use. Layouts
198    /// containing join controls retain the complete shaping boundaries
199    /// before selection coverage excludes those controls.
200    caret_positions: OnceLock<Arc<[TextCaretPosition]>>,
201    /// Per-line caret stops derived on demand when `caret_positions` was not
202    /// retained, so locating the caret in a long document derives one line
203    /// instead of every stop of every line.
204    line_carets: OnceLock<LineCaretStops>,
205    /// Whether the source had content outside the retained layout's visible
206    /// bounds. `Visible` layouts deliberately leave this false.
207    pub overflowed: bool,
208}
209
210/// Paragraph-level layout controls. They deliberately describe behavior in
211/// Incular terms and do not expose Parley builder types.
212#[derive(Clone, Copy, Debug, PartialEq)]
213pub struct TextLayoutOptions {
214    pub max_width: Option<f32>,
215    pub align: TextAlign,
216    /// Explicit paragraph base direction; `None` retains automatic detection.
217    pub text_direction: Option<TextDirection>,
218    pub soft_wrap: bool,
219    pub max_lines: Option<usize>,
220    pub overflow: TextOverflow,
221    /// Flutter's `TextHeightBehavior`: where an explicit line height's
222    /// leading goes. It has no effect without one.
223    pub height_behavior: TextHeightBehavior,
224}
225
226impl TextLayoutOptions {
227    #[must_use]
228    pub const fn new(max_width: Option<f32>, align: TextAlign) -> Self {
229        Self {
230            max_width,
231            align,
232            text_direction: None,
233            soft_wrap: true,
234            max_lines: None,
235            overflow: TextOverflow::Clip,
236            height_behavior: TextHeightBehavior {
237                apply_height_to_first_ascent: true,
238                apply_height_to_last_descent: true,
239                leading_distribution: TextLeadingDistribution::Proportional,
240            },
241        }
242    }
243
244    #[must_use]
245    pub const fn height_behavior(mut self, behavior: TextHeightBehavior) -> Self {
246        self.height_behavior = behavior;
247        self
248    }
249
250    #[must_use]
251    pub const fn soft_wrap(mut self, soft_wrap: bool) -> Self {
252        self.soft_wrap = soft_wrap;
253        self
254    }
255
256    #[must_use]
257    pub const fn text_direction(mut self, direction: TextDirection) -> Self {
258        self.text_direction = Some(direction);
259        self
260    }
261
262    #[must_use]
263    pub const fn max_lines(mut self, max_lines: Option<usize>) -> Self {
264        self.max_lines = max_lines;
265        self
266    }
267
268    #[must_use]
269    pub const fn overflow(mut self, overflow: TextOverflow) -> Self {
270        self.overflow = overflow;
271        self
272    }
273}
274
275impl Default for TextLayoutOptions {
276    fn default() -> Self {
277        Self::new(None, TextAlign::Start)
278    }
279}
280
281impl TextLayout {
282    #[must_use]
283    pub fn glyph_count(&self) -> usize {
284        self.lines.iter().map(|line| line.glyphs.len()).sum()
285    }
286
287    /// Returns the visual insertion points for one laid-out line in
288    /// left-to-right screen order.
289    #[must_use]
290    pub fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition] {
291        if let Some(caret_positions) = self.caret_positions.get() {
292            // Retained stops are grouped by ascending line.
293            let start = caret_positions.partition_point(|position| position.line < line);
294            let end = caret_positions.partition_point(|position| position.line <= line);
295            return &caret_positions[start..end];
296        }
297        if line >= self.lines.len() {
298            return &[];
299        }
300        let lines = self.line_carets.get_or_init(|| {
301            (0..self.lines.len())
302                .map(|_| OnceLock::new())
303                .collect::<Vec<_>>()
304                .into()
305        });
306        lines[line].get_or_init(|| {
307            let mut positions = Vec::new();
308            self.push_line_caret_positions(line, &mut positions);
309            positions.into()
310        })
311    }
312
313    fn derive_caret_positions(&self) -> Arc<[TextCaretPosition]> {
314        let mut positions = Vec::new();
315        for index in 0..self.lines.len() {
316            self.push_line_caret_positions(index, &mut positions);
317        }
318        positions.into()
319    }
320
321    /// Two stops per cluster (logical start and end, placed by direction),
322    /// plus the line's start and caret end when no cluster supplies them,
323    /// sorted into screen order.
324    fn push_line_caret_positions(&self, index: usize, positions: &mut Vec<TextCaretPosition>) {
325        let line = &self.lines[index];
326        let stop = |offset, affinity, x| TextCaretPosition {
327            offset,
328            affinity,
329            x,
330            line: index,
331        };
332        let first = positions.len();
333        for cluster in line.clusters.iter() {
334            let range = cluster.range();
335            let (left, right) = if cluster.rtl {
336                (range.end, range.start)
337            } else {
338                (range.start, range.end)
339            };
340            let (left_affinity, right_affinity) = if cluster.rtl {
341                (TextAffinity::Upstream, TextAffinity::Downstream)
342            } else {
343                (TextAffinity::Downstream, TextAffinity::Upstream)
344            };
345            positions.push(stop(left, left_affinity, cluster.left));
346            positions.push(stop(right, right_affinity, cluster.right));
347        }
348        let line_stops = &positions[first..];
349        if !line_stops.iter().any(|stop| stop.offset == line.start) {
350            positions.push(stop(line.start, TextAffinity::Downstream, line.offset));
351        }
352        if line.caret_end > line.start
353            && !positions[first..]
354                .iter()
355                .any(|stop| stop.offset == line.caret_end)
356        {
357            positions.push(stop(
358                line.caret_end,
359                TextAffinity::Upstream,
360                line.offset + line.width,
361            ));
362        }
363        positions[first..].sort_by(|left, right| {
364            left.x
365                .total_cmp(&right.x)
366                .then_with(|| left.offset.cmp(&right.offset))
367        });
368    }
369}
370
371/// Layout/cache counters. Legacy field names are retained for source
372/// compatibility; Parley performs fallback and coverage resolution internally.
373#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
374pub struct TextDiagnostics {
375    pub layouts_requested: u64,
376    pub cache_hits: u64,
377    pub cache_misses: u64,
378    pub font_resolution_requests: u64,
379    pub font_resolution_cache_hits: u64,
380    pub font_resolution_cache_misses: u64,
381    pub coverage_queries: u64,
382    pub fallback_runs: u64,
383    pub fallback_fonts_used: u64,
384    pub missing_clusters: u64,
385    pub shaping_runs: u64,
386    // Task 15 document-granularity instrumentation.
387    /// Paragraphs examined by the document path.
388    pub paragraphs_considered: u64,
389    /// Paragraphs that required fresh shaping (cache misses at paragraph
390    /// granularity).
391    pub paragraphs_reshaped: u64,
392    /// Parley layouts satisfied entirely from cache.
393    pub parley_layouts_reused: u64,
394    /// Multi-paragraph documents assembled from retained paragraphs.
395    pub documents_composed: u64,
396    /// Multi-paragraph documents returned whole from the cache.
397    pub documents_reused: u64,
398    /// Layouts served from a single-line layout shaped at another width.
399    pub width_reuses: u64,
400    /// Times a layout pool doubled because its working set did not fit.
401    pub cache_growths: u64,
402    /// Times a grown layout pool shrank back because its working set had
403    /// become far smaller than its capacity.
404    pub cache_shrinks: u64,
405    /// Leading paragraphs carried unchanged from an earlier composition.
406    pub paragraphs_carried: u64,
407    /// Wrapping paragraphs re-broken at a new width without reshaping.
408    pub shapes_reused: u64,
409}
410
411/// Identity of a retained layout. Lookups borrow the text, so a hit copies
412/// nothing; only a pool insert takes an owned copy (`into_owned`).
413#[derive(Hash, PartialEq, Eq)]
414struct LayoutKey<'a> {
415    text: Cow<'a, str>,
416    family: FontFamily,
417    fallbacks: Arc<[FontFamily]>,
418    size: u32,
419    weight: u16,
420    style: FontStyle,
421    line_height: Option<(u8, u32)>,
422    letter_spacing: u32,
423    font_variations: Option<Arc<str>>,
424    font_features: Option<Arc<str>>,
425    width: Option<u32>,
426    align: TextAlign,
427    text_direction: Option<TextDirection>,
428    soft_wrap: bool,
429    max_lines: Option<usize>,
430    overflow: TextOverflow,
431    height_behavior: TextHeightBehavior,
432    leading_distribution: Option<TextLeadingDistribution>,
433    generation: u64,
434}
435
436impl<'a> LayoutKey<'a> {
437    fn new(text: &'a str, style: &TextStyle, options: TextLayoutOptions, generation: u64) -> Self {
438        Self {
439            text: Cow::Borrowed(text),
440            family: style.family.clone(),
441            fallbacks: style.fallback_families.clone(),
442            size: style.size.to_bits(),
443            weight: style.weight.0,
444            style: style.style,
445            line_height: style.line_height.map(|lh| match lh {
446                crate::LineHeight::Normal => (0, 0),
447                crate::LineHeight::Multiplier(m) => (1, m.to_bits()),
448                crate::LineHeight::Absolute(a) => (2, a.to_bits()),
449            }),
450            letter_spacing: style.letter_spacing.to_bits(),
451            font_variations: style.font_variations.clone(),
452            font_features: style.font_features.clone(),
453            width: options.max_width.map(f32::to_bits),
454            align: options.align,
455            text_direction: options.text_direction,
456            soft_wrap: options.soft_wrap,
457            max_lines: options.max_lines,
458            overflow: options.overflow,
459            height_behavior: options.height_behavior,
460            leading_distribution: style.leading_distribution,
461            generation,
462        }
463    }
464
465    fn into_owned(self) -> LayoutKey<'static> {
466        LayoutKey {
467            text: Cow::Owned(self.text.into_owned()),
468            family: self.family,
469            fallbacks: self.fallbacks,
470            size: self.size,
471            weight: self.weight,
472            style: self.style,
473            line_height: self.line_height,
474            letter_spacing: self.letter_spacing,
475            font_variations: self.font_variations,
476            font_features: self.font_features,
477            width: self.width,
478            align: self.align,
479            text_direction: self.text_direction,
480            soft_wrap: self.soft_wrap,
481            max_lines: self.max_lines,
482            overflow: self.overflow,
483            height_behavior: self.height_behavior,
484            leading_distribution: self.leading_distribution,
485            generation: self.generation,
486        }
487    }
488}
489
490// Fonts every engine on this thread registers, in registration order.
491thread_local! {
492    static APPLICATION_FONTS: std::cell::RefCell<Vec<parley::fontique::Blob<u8>>> =
493        const { std::cell::RefCell::new(Vec::new()) };
494}
495
496/// Registers a TrueType/OpenType font with every [`TextEngine`] on the
497/// calling (UI) thread, including engines created earlier.
498///
499/// Families from these fonts also fill any generic family (`sans-serif`,
500/// `serif`, `monospace`, ...) the platform leaves empty. Browser targets have
501/// no system fonts, so a web application must register at least one font
502/// before text can render; there every later font joins the generic families
503/// too, as the fallback for characters the earlier ones lack. Native
504/// platforms keep their system generics and fallback.
505pub fn register_application_font(bytes: impl Into<Vec<u8>>) {
506    let blob = parley::fontique::Blob::from(bytes.into());
507    APPLICATION_FONTS.with(|fonts| fonts.borrow_mut().push(blob));
508}
509
510#[derive(Default)]
511struct MissingCharacters {
512    /// Every character reported so far; each is reported once.
513    reported: HashSet<char>,
514    pending: Vec<char>,
515    listener: Option<std::rc::Rc<dyn Fn()>>,
516}
517
518thread_local! {
519    static MISSING_CHARACTERS: std::cell::RefCell<MissingCharacters> =
520        std::cell::RefCell::new(MissingCharacters::default());
521}
522
523/// Calls `listener` on the calling (UI) thread after a layout finds
524/// characters no font could draw. The listener runs during layout: it should
525/// only schedule work, then collect the characters with
526/// [`take_missing_characters`]. Hosts without system fonts use this to load
527/// a font that covers them and [`register_application_font`] it.
528pub fn set_missing_characters_listener(listener: impl Fn() + 'static) {
529    MISSING_CHARACTERS.with_borrow_mut(|missing| {
530        missing.listener = Some(std::rc::Rc::new(listener));
531    });
532}
533
534/// Returns the characters this thread's layouts drew with a missing glyph
535/// since the last call. Each character is reported once per thread, even if
536/// no font ever covers it.
537#[must_use]
538pub fn take_missing_characters() -> Vec<char> {
539    MISSING_CHARACTERS.with_borrow_mut(|missing| std::mem::take(&mut missing.pending))
540}
541
542fn note_missing_characters(text: &str) {
543    let listener = MISSING_CHARACTERS.with_borrow_mut(|missing| {
544        let before = missing.pending.len();
545        for character in text.chars() {
546            // Layout controls and spaces have no glyph to load.
547            if character.is_whitespace() || character.is_control() {
548                continue;
549            }
550            if missing.reported.insert(character) {
551                missing.pending.push(character);
552            }
553        }
554        (missing.pending.len() > before)
555            .then(|| missing.listener.clone())
556            .flatten()
557    });
558    if let Some(listener) = listener {
559        listener();
560    }
561}
562
563/// The sole owner of font discovery and app-registered fonts. Parley owns the
564/// actual font collection and resolves scripts, bidi direction and fallback.
565pub struct TextEngine {
566    /// One `FontHandle` per distinct font blob. Handles share the font bytes
567    /// through a single `Arc`, so every cached text layout references the
568    /// same allocation instead of holding a private copy of the whole font
569    /// file (the Task 15 root cause of ~500 KB retained per shaped string
570    /// and a full-file memcpy plus full-file hash per glyph run).
571    font_handles: HashMap<(usize, usize, u32), FontHandle>,
572    font_context: FontContext,
573    layout_context: LayoutContext<()>,
574    generation: u64,
575    /// Count of thread application fonts already registered here.
576    application_fonts: usize,
577    cache: LayoutPool,
578    /// Width-independent single-line layouts keyed without their width; see
579    /// `shape_or_reuse`.
580    unwrapped: LayoutPool,
581    /// Shaped Parley layouts of paragraphs that wrapped, keyed without their
582    /// width; see `shape_raw`.
583    shaped: LayoutPool<Layout<()>>,
584    /// Approximate bytes the three pools may retain together; see
585    /// `set_cache_byte_budget`.
586    cache_byte_budget: usize,
587    /// Recent compositions, most recent first; see `carried_paragraphs`.
588    documents: Vec<ComposedDocument>,
589    /// Recent document layouts, most recent first; see `layout_paragraphs`.
590    retained_documents: Vec<document::RetainedDocument>,
591    diagnostics: TextDiagnostics,
592}
593
594/// Compositions retained for carrying paragraphs into the next edit: a few
595/// fields or editor groups can be edited alternately.
596const COMPOSED_DOCUMENTS: usize = 4;
597
598/// The finished-layout pool's half of the engine's byte budget.
599const fn cache_share(budget: usize) -> usize {
600    budget / 2
601}
602
603/// The width-independent and shaped pools' quarter each of the budget.
604const fn minor_share(budget: usize) -> usize {
605    budget / 4
606}
607
608/// Composition state at the start of one paragraph.
609#[derive(Clone, Copy)]
610struct ParagraphStart {
611    byte: usize,
612    line: usize,
613    font_run: usize,
614    width: f32,
615    height: f32,
616    line_height: f32,
617    overflowed: bool,
618}
619
620/// A composed document, kept so composing an edited version can carry over
621/// the paragraphs before its first change instead of rebasing every line,
622/// and so an unchanged document is returned whole.
623struct ComposedDocument {
624    /// Empty-text key carrying the style, paragraph options and generation.
625    policy: LayoutKey<'static>,
626    /// Requested overflow, which can clip `layout`.
627    overflow: TextOverflow,
628    text: String,
629    /// Every composed line, before any clipping.
630    lines: Arc<[TextLine]>,
631    font_runs: Arc<[FontRunDebug]>,
632    starts: Vec<ParagraphStart>,
633    layout: Arc<TextLayout>,
634}
635
636impl Default for TextEngine {
637    fn default() -> Self {
638        Self::new()
639    }
640}
641
642impl TextEngine {
643    #[must_use]
644    pub fn new() -> Self {
645        Self {
646            font_handles: HashMap::new(),
647            font_context: FontContext::new(),
648            layout_context: LayoutContext::new(),
649            generation: 1,
650            application_fonts: 0,
651            cache: LayoutPool::new(cache_share(DEFAULT_CACHE_BYTE_BUDGET)),
652            unwrapped: LayoutPool::new(minor_share(DEFAULT_CACHE_BYTE_BUDGET)),
653            shaped: LayoutPool::new(minor_share(DEFAULT_CACHE_BYTE_BUDGET)),
654            cache_byte_budget: DEFAULT_CACHE_BYTE_BUDGET,
655            documents: Vec::new(),
656            retained_documents: Vec::new(),
657            diagnostics: TextDiagnostics::default(),
658        }
659    }
660
661    /// Runs `f` with this thread's shared measuring engine.
662    ///
663    /// Application code that measures text while building or inside a
664    /// `LayoutBuilder` should use this instead of `TextEngine::new()`: a new
665    /// engine starts with an empty cache and reshapes every string on every
666    /// rebuild, while the shared engine keeps layouts across frames. A nested
667    /// call from inside `f` gets a temporary engine rather than panicking.
668    pub fn with_shared<R>(f: impl FnOnce(&mut Self) -> R) -> R {
669        thread_local! {
670            static SHARED: std::cell::RefCell<TextEngine> =
671                std::cell::RefCell::new(TextEngine::new());
672        }
673        SHARED.with(|shared| match shared.try_borrow_mut() {
674            Ok(mut engine) => f(&mut engine),
675            Err(_) => f(&mut Self::new()),
676        })
677    }
678
679    #[must_use]
680    pub const fn diagnostics(&self) -> TextDiagnostics {
681        let mut diagnostics = self.diagnostics;
682        diagnostics.cache_growths =
683            self.cache.growths + self.unwrapped.growths + self.shaped.growths;
684        diagnostics.cache_shrinks =
685            self.cache.shrinks + self.unwrapped.shrinks + self.shaped.shrinks;
686        diagnostics
687    }
688
689    /// Approximate bytes the layout pools may retain together. Defaults to
690    /// 32 MiB.
691    #[must_use]
692    pub const fn cache_byte_budget(&self) -> usize {
693        self.cache_byte_budget
694    }
695
696    /// Bounds the approximate bytes retained by the layout pools, evicting
697    /// least recently used layouts beyond it. Half goes to finished layouts
698    /// and a quarter each to width-independent lines and shaped wrapping
699    /// paragraphs. The paragraphs of a document being composed are exempt,
700    /// so a long file is never reshaped on each edit.
701    pub fn set_cache_byte_budget(&mut self, bytes: usize) {
702        self.cache_byte_budget = bytes;
703        self.cache.set_byte_budget(cache_share(bytes));
704        self.unwrapped.set_byte_budget(minor_share(bytes));
705        self.shaped.set_byte_budget(minor_share(bytes));
706    }
707
708    /// Approximate bytes the layout pools retain now. Layouts shared between
709    /// pools are counted by each, so this is an upper bound.
710    #[must_use]
711    pub const fn cache_bytes(&self) -> usize {
712        self.cache.bytes() + self.unwrapped.bytes() + self.shaped.bytes()
713    }
714
715    /// Whether fonts were registered with [`register_application_font`]
716    /// since this engine last laid out text. Retained layouts measured with
717    /// the older fonts are stale.
718    #[must_use]
719    pub fn has_pending_application_fonts(&self) -> bool {
720        APPLICATION_FONTS.with(|fonts| fonts.borrow().len() > self.application_fonts)
721    }
722
723    /// Registers thread application fonts added since this engine last laid
724    /// out text, returning whether there were any.
725    pub fn sync_application_fonts(&mut self) -> bool {
726        let pending = APPLICATION_FONTS.with(|fonts| {
727            fonts
728                .borrow()
729                .get(self.application_fonts..)
730                .map(<[_]>::to_vec)
731                .unwrap_or_default()
732        });
733        self.application_fonts += pending.len();
734        let registered = !pending.is_empty();
735        for blob in pending {
736            self.register_font_blob(blob);
737        }
738        registered
739    }
740
741    /// Counts one layout request toward the pools' decay windows.
742    fn tick_pools(&mut self) {
743        self.cache.tick();
744        self.unwrapped.tick();
745        self.shaped.tick();
746    }
747
748    #[must_use]
749    pub const fn font_database_generation(&self) -> u64 {
750        self.generation
751    }
752
753    /// Registers all faces in a TrueType/OpenType byte buffer with Parley's
754    /// Fontique collection. Invalid buffers are harmless: Fontique ignores
755    /// them, while the generation still invalidates stale cached layouts.
756    pub fn register_font_bytes(&mut self, bytes: impl Into<Vec<u8>>) {
757        self.register_font_blob(parley::fontique::Blob::from(bytes.into()));
758    }
759
760    fn register_font_blob(&mut self, blob: parley::fontique::Blob<u8>) {
761        use parley::fontique::GenericFamily;
762
763        let collection = &mut self.font_context.collection;
764        let families: Vec<_> = collection
765            .register_fonts(blob, None)
766            .into_iter()
767            .map(|(family, _)| family)
768            .collect();
769        for generic in [
770            GenericFamily::Serif,
771            GenericFamily::SansSerif,
772            GenericFamily::Monospace,
773            GenericFamily::Cursive,
774            GenericFamily::Fantasy,
775            GenericFamily::SystemUi,
776            GenericFamily::UiSerif,
777            GenericFamily::UiSansSerif,
778            GenericFamily::UiMonospace,
779            GenericFamily::UiRounded,
780            GenericFamily::Emoji,
781            GenericFamily::Math,
782            GenericFamily::FangSong,
783        ] {
784            if families.is_empty() {
785                continue;
786            }
787            if collection.generic_families(generic).next().is_none() {
788                collection.set_generic_families(generic, families.iter().copied());
789            } else if cfg!(target_arch = "wasm32") {
790                // A page has no system fallback: a later font, such as one
791                // loaded for a script the first fonts lack, is only found
792                // through the generic families.
793                collection.append_generic_families(generic, families.iter().copied());
794            }
795        }
796        self.generation = self
797            .generation
798            .checked_add(1)
799            .expect("font database generation exhausted");
800        self.cache.clear();
801        self.unwrapped.clear();
802        self.shaped.clear();
803        self.documents.clear();
804        self.retained_documents.clear();
805    }
806
807    pub fn layout(
808        &mut self,
809        text: &str,
810        style: &TextStyle,
811        max_width: Option<f32>,
812        align: TextAlign,
813    ) -> Arc<TextLayout> {
814        self.layout_with_options(text, style, TextLayoutOptions::new(max_width, align))
815    }
816
817    /// Shapes a paragraph with retained wrapping and overflow policy.
818    pub fn layout_with_options(
819        &mut self,
820        text: &str,
821        style: &TextStyle,
822        options: TextLayoutOptions,
823    ) -> Arc<TextLayout> {
824        self.diagnostics.layouts_requested += 1;
825        self.sync_application_fonts();
826        self.tick_pools();
827        // Documents with hard line breaks compose from retained per-paragraph
828        // layouts so editing one paragraph never reshapes the rest. Overflow
829        // policies keep the exact monolithic semantics and stay on the
830        // whole-string path.
831        // With soft wrapping, `Clip` can only trigger on single-line
832        // horizontal overflow, which a hard-break document never has; so this
833        // route preserves monolithic semantics exactly. Without soft wrapping,
834        // `Clip`/`Fade` against a fixed width only cut horizontal overflow to
835        // the first line, which `layout_document` applies to the composition.
836        // Trimming the first ascent or last descent applies to the whole
837        // text, not to each composed paragraph.
838        let document = text.contains('\n')
839            && options.max_lines.is_none()
840            && options.height_behavior.apply_height_to_first_ascent
841            && options.height_behavior.apply_height_to_last_descent
842            && (options.soft_wrap
843                || options.overflow == TextOverflow::Visible
844                || (options.max_width.is_some()
845                    && matches!(options.overflow, TextOverflow::Clip | TextOverflow::Fade)));
846        // Composed documents never enter the paragraph pool: each edited
847        // version is a new key holding a full copy of the text and of every
848        // line, so a pool sized for a long file's paragraphs retained
849        // hundreds of stale versions. The few recent compositions kept for
850        // prefix carry also return an unchanged document whole.
851        if document {
852            return self.layout_document(text, style, options);
853        }
854        let key = LayoutKey::new(text, style, options, self.generation);
855        if let Some(layout) = self.cache.get(&key) {
856            self.diagnostics.cache_hits += 1;
857            return layout;
858        }
859        self.diagnostics.cache_misses += 1;
860        let (layout, reused) = self.shape_or_reuse(text, style, options);
861        // `unwrapped` already serves a reused layout at every width it fits;
862        // retaining it under this width too copied each label's text on
863        // every resize.
864        if !reused {
865            self.cache.put(key, layout.clone());
866        }
867        layout
868    }
869
870    /// Shapes `text`, or returns a layout shaped for the same text at another
871    /// width when that layout is provably identical at this one.
872    ///
873    /// A single unclipped line whose alignment offset is zero is independent
874    /// of the width it was shaped against, as long as it still fits: nothing
875    /// wraps and start alignment in a left-to-right paragraph never moves it.
876    /// Window resizes and flex measurement give labels new widths every
877    /// frame; without this each such change reshaped every label.
878    ///
879    /// Also returns whether the layout was reused from another width.
880    fn shape_or_reuse(
881        &mut self,
882        text: &str,
883        style: &TextStyle,
884        options: TextLayoutOptions,
885    ) -> (Arc<TextLayout>, bool) {
886        let key = LayoutKey::new(
887            text,
888            style,
889            TextLayoutOptions {
890                max_width: None,
891                ..options
892            },
893            self.generation,
894        );
895        if let Some(layout) = self.unwrapped.get(&key)
896            && options
897                .max_width
898                .is_none_or(|width| layout.metrics.size.width <= width)
899        {
900            self.diagnostics.width_reuses += 1;
901            return (layout, true);
902        }
903        let layout = Arc::new(self.shape_and_wrap(text, style, options));
904        if width_independent(&layout, options) {
905            self.unwrapped.put(key, layout.clone());
906        }
907        (layout, false)
908    }
909
910    /// Grows the retained pool so `paragraphs` fit beside the base pool. A
911    /// cyclic scan longer than an LRU's capacity evicts every entry before its
912    /// reuse, which made each layout of a long file reshape all of it.
913    fn reserve_document(&mut self, paragraphs: usize) {
914        self.cache.pin(paragraphs);
915        self.unwrapped.pin(paragraphs);
916        let required = paragraphs.saturating_add(LAYOUT_CACHE_CAPACITY.get());
917        if self.cache.capacity() < required {
918            let capacity = required.next_power_of_two();
919            self.cache.reserve(capacity);
920            self.unwrapped.reserve(capacity);
921        }
922    }
923
924    /// Composes a multi-paragraph document from per-paragraph retained
925    /// layouts. Shaping reuse is exact (paragraph slice == cache key);
926    /// assembly copies line data with vertical and byte-range rebasing.
927    fn layout_document(
928        &mut self,
929        text: &str,
930        style: &TextStyle,
931        options: TextLayoutOptions,
932    ) -> Arc<TextLayout> {
933        let paragraph_options = TextLayoutOptions {
934            max_lines: None,
935            overflow: TextOverflow::Visible,
936            ..options
937        };
938        let policy = LayoutKey::new("", style, paragraph_options, self.generation);
939        // The router only sends `max_lines: None` here, so the policy and the
940        // overflow determine the composition of a text.
941        if let Some(index) = self.documents.iter().position(|document| {
942            document.policy == policy
943                && document.overflow == options.overflow
944                && document.text == text
945        }) {
946            self.diagnostics.cache_hits += 1;
947            self.diagnostics.documents_reused += 1;
948            let document = self.documents.remove(index);
949            let layout = document.layout.clone();
950            self.documents.insert(0, document);
951            return layout;
952        }
953        self.diagnostics.documents_composed += 1;
954        self.reserve_document(text.bytes().filter(|byte| *byte == b'\n').count() + 1);
955        let mut lines: Vec<TextLine> = Vec::new();
956        let mut font_runs = Vec::new();
957        let mut starts = Vec::new();
958        let mut state = ParagraphStart {
959            byte: 0,
960            line: 0,
961            font_run: 0,
962            width: 0.,
963            height: 0.,
964            line_height: 0.,
965            overflowed: false,
966        };
967        let mut document_carets = has_join_controls(text).then(Vec::new);
968        // Lines before the first edited paragraph have the same byte offsets
969        // and vertical position as last time, so they are carried as-is.
970        if document_carets.is_none()
971            && let Some((index, carried)) = self.carried_paragraphs(&policy, text)
972        {
973            let previous = &self.documents[index];
974            state = previous.starts[carried];
975            lines.extend_from_slice(&previous.lines[..state.line]);
976            font_runs.extend_from_slice(&previous.font_runs[..state.font_run]);
977            starts.extend_from_slice(&previous.starts[..carried]);
978            self.diagnostics.paragraphs_carried += carried as u64;
979        }
980        let mut byte_start = state.byte;
981        let mut width = state.width;
982        let mut height = state.height;
983        let mut line_height = state.line_height;
984        let mut overflowed = state.overflowed;
985        for paragraph in text[byte_start..].split('\n') {
986            self.diagnostics.paragraphs_considered += 1;
987            starts.push(ParagraphStart {
988                byte: byte_start,
989                line: lines.len(),
990                font_run: font_runs.len(),
991                width,
992                height,
993                line_height,
994                overflowed,
995            });
996            let key = LayoutKey::new(paragraph, style, paragraph_options, self.generation);
997            let paragraph_layout = if let Some(existing) = self.cache.get(&key) {
998                self.diagnostics.parley_layouts_reused += 1;
999                self.diagnostics.cache_hits += 1;
1000                existing
1001            } else {
1002                self.diagnostics.paragraphs_reshaped += 1;
1003                self.diagnostics.cache_misses += 1;
1004                let (built, _) = self.shape_or_reuse(paragraph, style, paragraph_options);
1005                self.cache.put(key, built.clone());
1006                built
1007            };
1008            if let Some(carets) = &mut document_carets {
1009                let line_start = lines.len();
1010                let paragraph_carets = paragraph_layout
1011                    .caret_positions
1012                    .get_or_init(|| paragraph_layout.derive_caret_positions());
1013                carets.extend(paragraph_carets.iter().map(|position| TextCaretPosition {
1014                    offset: position.offset + byte_start,
1015                    line: position.line + line_start,
1016                    ..*position
1017                }));
1018            }
1019            // Font-run metadata belongs to the paragraph, not each visual line.
1020            // Rebase it once even when soft wrapping creates multiple lines.
1021            for run in paragraph_layout.font_runs.iter() {
1022                let mut rebased = run.clone();
1023                rebased.range.start += byte_start;
1024                rebased.range.end += byte_start;
1025                font_runs.push(rebased);
1026            }
1027            for line in paragraph_layout.lines.iter() {
1028                // Exact-size iterators collect straight into `Arc<[T]>` with
1029                // one allocation; going through a `Vec` copied every line twice.
1030                let glyphs: Arc<[GlyphPosition]> = line
1031                    .glyphs
1032                    .iter()
1033                    .map(|glyph| {
1034                        let mut shifted = *glyph;
1035                        // Stored glyph y is negative parley space; moving the
1036                        // paragraph down by `height` subtracts from it.
1037                        shifted.offset.y -= height;
1038                        // Clusters index the full document string, so caret
1039                        // and selection mapping stay globally consistent.
1040                        shifted.cluster += byte_start as u32;
1041                        shifted
1042                    })
1043                    .collect();
1044                let shifted_runs: Arc<[Arc<GlyphRun>]> = line
1045                    .runs
1046                    .iter()
1047                    .map(|run| {
1048                        let mut shifted = (**run).clone();
1049                        shifted.origin.y += height;
1050                        Arc::new(shifted)
1051                    })
1052                    .collect();
1053                lines.push(TextLine {
1054                    runs: shifted_runs,
1055                    glyphs,
1056                    clusters: line
1057                        .clusters
1058                        .iter()
1059                        .map(|span| TextClusterSpan {
1060                            start: span.start + byte_start as u32,
1061                            end: span.end + byte_start as u32,
1062                            left: span.left,
1063                            right: span.right,
1064                            rtl: span.rtl,
1065                        })
1066                        .collect(),
1067                    width: line.width,
1068                    offset: line.offset,
1069                    baseline: line.baseline + height,
1070                    start: line.start + byte_start,
1071                    caret_end: line.caret_end + byte_start,
1072                    end: line.end + byte_start,
1073                });
1074            }
1075            width = width.max(paragraph_layout.metrics.size.width);
1076            height += paragraph_layout.metrics.size.height;
1077            line_height = paragraph_layout.metrics.line_height;
1078            overflowed |= paragraph_layout.overflowed;
1079            byte_start += paragraph.len() + 1; // include the hard break
1080        }
1081        if lines.is_empty() {
1082            lines.push(TextLine {
1083                runs: Arc::new([]),
1084                glyphs: Arc::new([]),
1085                clusters: Arc::new([]),
1086                width: 0.,
1087                offset: 0.,
1088                baseline: 0.,
1089                start: 0,
1090                caret_end: 0,
1091                end: 0,
1092            });
1093        }
1094        let baseline = lines[0].baseline;
1095        let lines: Arc<[TextLine]> = lines.into();
1096        let font_runs: Arc<[FontRunDebug]> = font_runs.into();
1097        let layout = TextLayout {
1098            lines: lines.clone(),
1099            metrics: TextMetrics {
1100                size: Size::new(width, height),
1101                baseline,
1102                line_height,
1103            },
1104            font_runs: font_runs.clone(),
1105            caret_positions: document_carets
1106                .map(|carets| OnceLock::from(Arc::from(carets)))
1107                .unwrap_or_default(),
1108            line_carets: OnceLock::new(),
1109            overflowed,
1110        };
1111        // Same horizontal-overflow policy as `shape_and_wrap` for unwrapped
1112        // text; the router only sends clipping overflows here.
1113        let horizontal_overflow = !options.soft_wrap
1114            && options.overflow != TextOverflow::Visible
1115            && options.max_width.is_some_and(|max| width > max);
1116        let layout = Arc::new(if horizontal_overflow {
1117            clipped_layout(layout, 1)
1118        } else {
1119            layout
1120        });
1121        self.documents.truncate(COMPOSED_DOCUMENTS - 1);
1122        self.documents.insert(
1123            0,
1124            ComposedDocument {
1125                policy,
1126                overflow: options.overflow,
1127                text: text.to_owned(),
1128                lines,
1129                font_runs,
1130                starts,
1131                layout: layout.clone(),
1132            },
1133        );
1134        layout
1135    }
1136
1137    /// The retained composition sharing `policy` that carries the most
1138    /// leading paragraphs into `text`, and how many it carries. Paragraph
1139    /// `k` and everything before it are unchanged when the common byte
1140    /// prefix reaches the start of paragraph `k`.
1141    fn carried_paragraphs(&self, policy: &LayoutKey<'_>, text: &str) -> Option<(usize, usize)> {
1142        self.documents
1143            .iter()
1144            .enumerate()
1145            .filter(|(_, document)| document.policy == *policy)
1146            .map(|(index, document)| {
1147                let prefix = common_prefix_len(&document.text, text);
1148                let carried = document
1149                    .starts
1150                    .partition_point(|start| start.byte <= prefix)
1151                    .saturating_sub(1);
1152                (index, carried)
1153            })
1154            .filter(|(_, carried)| *carried > 0)
1155            .max_by_key(|(_, carried)| *carried)
1156    }
1157
1158    fn font_stack(style: &TextStyle) -> parley::FontFamily<'static> {
1159        let mut names = vec![family_name(&style.family)];
1160        names.extend(style.fallback_families.iter().map(family_name));
1161        parley::FontFamily::Source(names.join(", ").into())
1162    }
1163
1164    fn shape_and_wrap(
1165        &mut self,
1166        text: &str,
1167        style: &TextStyle,
1168        options: TextLayoutOptions,
1169    ) -> TextLayout {
1170        let wrap_width = options.soft_wrap.then_some(options.max_width).flatten();
1171        let layout = self.shape_raw(
1172            text,
1173            style,
1174            wrap_width,
1175            options.max_width,
1176            options.align,
1177            options.text_direction,
1178            options.height_behavior,
1179        );
1180        let horizontal_overflow = !options.soft_wrap
1181            && options
1182                .max_width
1183                .is_some_and(|width| layout.metrics.size.width > width);
1184        let line_limit = options
1185            .max_lines
1186            .or_else(|| horizontal_overflow.then_some(1));
1187        let line_overflow = line_limit.is_some_and(|limit| layout.lines.len() > limit);
1188        if (!horizontal_overflow && !line_overflow) || options.overflow == TextOverflow::Visible {
1189            return layout;
1190        }
1191
1192        let limit = line_limit.unwrap_or(layout.lines.len());
1193        match options.overflow {
1194            TextOverflow::Visible => layout,
1195            TextOverflow::Clip | TextOverflow::Fade => clipped_layout(layout, limit),
1196            TextOverflow::Ellipsis => self.ellipsized_layout(text, style, options, limit),
1197        }
1198    }
1199
1200    fn ellipsized_layout(
1201        &mut self,
1202        text: &str,
1203        style: &TextStyle,
1204        options: TextLayoutOptions,
1205        limit: usize,
1206    ) -> TextLayout {
1207        if limit == 0 {
1208            return clipped_layout(
1209                self.shape_raw(
1210                    "",
1211                    style,
1212                    None,
1213                    options.max_width,
1214                    options.align,
1215                    options.text_direction,
1216                    options.height_behavior,
1217                ),
1218                0,
1219            );
1220        }
1221        let wrap_width = options.soft_wrap.then_some(options.max_width).flatten();
1222        let initial = self.shape_raw(
1223            text,
1224            style,
1225            wrap_width,
1226            options.max_width,
1227            options.align,
1228            options.text_direction,
1229            options.height_behavior,
1230        );
1231        let end = initial
1232            .lines
1233            .get(limit.saturating_sub(1))
1234            .map_or(0, |line| line.caret_end.min(text.len()));
1235        let mut end = grapheme_boundary_at_or_before(text, end);
1236
1237        loop {
1238            let mut candidate = text[..end].to_owned();
1239            candidate.push('\u{2026}');
1240            // The ellipsis always enters Parley as source text and is shaped
1241            // with the selected/fallback font stack, never appended as a raw
1242            // renderer glyph or a byte-truncated source suffix.
1243            let shaped = self.shape_raw(
1244                &candidate,
1245                style,
1246                wrap_width,
1247                options.max_width,
1248                options.align,
1249                options.text_direction,
1250                options.height_behavior,
1251            );
1252            let fits_width = options.max_width.is_none_or(|width| {
1253                shaped
1254                    .lines
1255                    .iter()
1256                    .all(|line| line.width <= width + f32::EPSILON)
1257            });
1258            if shaped.lines.len() <= limit && fits_width {
1259                return TextLayout {
1260                    overflowed: true,
1261                    ..shaped
1262                };
1263            }
1264            let previous = previous_grapheme_boundary(text, end);
1265            if previous == end {
1266                return TextLayout {
1267                    overflowed: true,
1268                    ..shaped
1269                };
1270            }
1271            end = previous;
1272        }
1273    }
1274
1275    /// Shapes `text` into an unbroken Parley layout.
1276    fn shape_paragraph(
1277        &mut self,
1278        text: &str,
1279        style: &TextStyle,
1280        max_width: Option<f32>,
1281        text_direction: Option<TextDirection>,
1282    ) -> Layout<()> {
1283        let mut builder =
1284            self.layout_context
1285                .ranged_builder(&mut self.font_context, text, 1.0, true);
1286        builder.set_base_direction(match text_direction {
1287            Some(TextDirection::Ltr) => BaseDirection::Ltr,
1288            Some(TextDirection::Rtl) => BaseDirection::Rtl,
1289            None => BaseDirection::Auto,
1290        });
1291        // Ranged builders retain the source text; this restores the previous
1292        // hanging-space policy without collapsing any source characters.
1293        builder.push_default(StyleProperty::WhiteSpaceCollapse(
1294            parley::WhiteSpaceCollapse::Collapse,
1295        ));
1296        if max_width.is_none() {
1297            builder.push_default(StyleProperty::TextWrapMode(parley::TextWrapMode::NoWrap));
1298        }
1299        builder.push_default(StyleProperty::FontFamily(Self::font_stack(style)));
1300        builder.push_default(StyleProperty::FontSize(style.size.max(0.0)));
1301        builder.push_default(StyleProperty::FontWeight(ParleyFontWeight::new(
1302            style.weight.0 as f32,
1303        )));
1304        builder.push_default(StyleProperty::FontStyle(match style.style {
1305            FontStyle::Normal => parley::FontStyle::Normal,
1306            FontStyle::Italic => parley::FontStyle::Italic,
1307        }));
1308        builder.push_default(StyleProperty::LetterSpacing(style.letter_spacing));
1309        if let Some(settings) = &style.font_variations {
1310            builder.push_default(StyleProperty::FontVariations(
1311                parley::FontVariations::Source(Cow::Owned(settings.to_string())),
1312            ));
1313        }
1314        if let Some(settings) = &style.font_features {
1315            builder.push_default(StyleProperty::FontFeatures(parley::FontFeatures::Source(
1316                Cow::Owned(settings.to_string()),
1317            )));
1318        }
1319        if let Some(line_height) = style.line_height
1320            && let Some(parley_lh) = line_height.to_parley(style.size)
1321        {
1322            builder.push_default(StyleProperty::LineHeight(parley_lh));
1323        }
1324
1325        builder.build(text)
1326    }
1327
1328    #[allow(clippy::too_many_arguments)]
1329    fn shape_raw(
1330        &mut self,
1331        text: &str,
1332        style: &TextStyle,
1333        max_width: Option<f32>,
1334        align_width: Option<f32>,
1335        align: TextAlign,
1336        text_direction: Option<TextDirection>,
1337        height_behavior: TextHeightBehavior,
1338    ) -> TextLayout {
1339        // Shaping does not depend on the wrap width; only line breaking and
1340        // alignment do. A wrapping paragraph keeps its shaped layout so a new
1341        // width re-breaks it instead of shaping it again.
1342        let shaping_key = LayoutKey::new(
1343            text,
1344            style,
1345            TextLayoutOptions {
1346                max_width: max_width.map(|_| 1.),
1347                align: TextAlign::Start,
1348                text_direction,
1349                soft_wrap: true,
1350                max_lines: None,
1351                overflow: TextOverflow::Visible,
1352                height_behavior: TextHeightBehavior::default(),
1353            },
1354            self.generation,
1355        );
1356        let mut layout = match self.shaped.take(&shaping_key) {
1357            Some(layout) => {
1358                self.diagnostics.shapes_reused += 1;
1359                layout
1360            }
1361            None => self.shape_paragraph(text, style, max_width, text_direction),
1362        };
1363        layout.break_all_lines(align_width.or(max_width));
1364        // Alignment resolves against the measured width even when the text
1365        // does not wrap: single-line content otherwise ignores alignment
1366        // because the wrap width is empty.
1367        layout.align(
1368            match align {
1369                TextAlign::Start => Alignment::Start,
1370                TextAlign::Center => Alignment::Center,
1371                TextAlign::End => Alignment::End,
1372                TextAlign::Justify => Alignment::Justify,
1373                TextAlign::Left => Alignment::Left,
1374                TextAlign::Right => Alignment::Right,
1375            },
1376            AlignmentOptions::default(),
1377        );
1378
1379        let mut translated = self.translate_layout(&layout, text);
1380        if matches!(
1381            style.line_height,
1382            Some(LineHeight::Multiplier(_) | LineHeight::Absolute(_))
1383        ) {
1384            apply_height_behavior(
1385                &mut translated,
1386                &layout,
1387                height_behavior,
1388                style.leading_distribution,
1389            );
1390        }
1391        if text.is_empty() {
1392            // Parley uses an internal space to obtain empty-paragraph font
1393            // metrics. That space must not become a visible advance or move
1394            // the editor's caret away from the empty aligned paragraph edge.
1395            let width = align_width.unwrap_or(0.0).max(0.0);
1396            let offset = match (align, layout.is_rtl()) {
1397                (TextAlign::Right, _)
1398                | (TextAlign::Start | TextAlign::Justify, true)
1399                | (TextAlign::End, false) => width,
1400                (TextAlign::Center, _) => width * 0.5,
1401                _ => 0.0,
1402            };
1403            for line in Arc::make_mut(&mut translated.lines) {
1404                line.runs = Arc::new([]);
1405                line.glyphs = Arc::new([]);
1406                line.clusters = Arc::new([]);
1407                line.width = 0.0;
1408                line.offset = offset;
1409            }
1410            translated.metrics.size.width = 0.0;
1411        }
1412        if layout.len() > 1 {
1413            self.shaped.put(shaping_key, layout);
1414        }
1415        translated
1416    }
1417
1418    fn font_handle_for(&mut self, font: &parley::FontData) -> FontHandle {
1419        font_handle(self, font)
1420    }
1421
1422    fn translate_layout(&mut self, layout: &Layout<()>, text: &str) -> TextLayout {
1423        let mut lines = Vec::new();
1424        let mut font_runs = Vec::new();
1425        let mut used_fonts = HashSet::new();
1426        // First glyph run decides the primary font; resolve its handle before
1427        // the mutable translation loop so borrowck stays trivial.
1428        let primary_handle = layout
1429            .lines()
1430            .flat_map(|line| line.runs())
1431            .next()
1432            .map(|run| self.font_handle_for(run.font()));
1433        let primary = primary_handle.as_ref().map(FontHandle::id);
1434
1435        for line in layout.lines() {
1436            let metrics = line.metrics();
1437            let range = line.text_range();
1438            let mut runs: Vec<Arc<GlyphRun>> = Vec::new();
1439            let mut glyphs = Vec::new();
1440            let mut caret_end = range.start;
1441            // Walk Parley's visual cluster advances once for the whole line.
1442            // This is the same cell geometry Parley uses for hit testing and
1443            // accounts for per-cluster spacing and justified advances. A
1444            // source cluster can be split across font/style glyph runs, so
1445            // merge duplicate byte ranges into one logical span and caret
1446            // pair here rather than deriving cells separately per glyph run.
1447            let mut cluster_positions: Vec<ClusterPosition> = Vec::new();
1448            let mut cluster_position_index: HashMap<(usize, usize), usize> = HashMap::new();
1449            let mut cluster_x = metrics.offset;
1450            for run in line.runs() {
1451                let rtl = run.is_rtl();
1452                for cluster in run.visual_clusters() {
1453                    let cluster_range = cluster.text_range();
1454                    // Glyph 0 is the font's "missing glyph" box.
1455                    if cluster.glyphs().any(|glyph| glyph.id == 0)
1456                        && let Some(source) = text.get(cluster_range.clone())
1457                    {
1458                        note_missing_characters(source);
1459                    }
1460                    // Invisible continuation clusters still own source bytes.
1461                    // Keep the logical line endpoint through their ranges so
1462                    // a final ligature component does not lose its caret stop.
1463                    caret_end = caret_end.max(cluster_range.end);
1464                    let edge = cluster_x + cluster.advance();
1465                    let ligature_start = cluster.is_ligature_start();
1466                    let ligature_continuation = cluster.is_ligature_continuation();
1467                    let mut cluster_glyphs = cluster.glyphs();
1468                    let Some(first_glyph) = cluster_glyphs.next() else {
1469                        // A shaping cluster can carry advance for an
1470                        // invisible continuation. Ligature components remain
1471                        // part of the start cluster's selectable range, while
1472                        // standalone join controls have no visible selection
1473                        // geometry of their own.
1474                        let is_join_control =
1475                            text.get(cluster_range.clone()).is_some_and(|component| {
1476                                !component.is_empty()
1477                                    && component.chars().all(|character| {
1478                                        matches!(character, '\u{200C}' | '\u{200D}')
1479                                    })
1480                            });
1481                        if ligature_continuation && !is_join_control {
1482                            let position = ClusterPosition {
1483                                range: cluster_range.clone(),
1484                                left: cluster_x.min(edge),
1485                                right: cluster_x.max(edge),
1486                                rtl,
1487                                ligature_start,
1488                                ligature_continuation,
1489                            };
1490                            let key = (cluster_range.start, cluster_range.end);
1491                            if let Some(index) = cluster_position_index.get(&key).copied() {
1492                                cluster_positions[index].include(&position);
1493                            } else {
1494                                cluster_position_index.insert(key, cluster_positions.len());
1495                                cluster_positions.push(position);
1496                            }
1497                        }
1498                        cluster_x = edge;
1499                        continue;
1500                    };
1501                    let glyph_advance = std::iter::once(first_glyph)
1502                        .chain(cluster_glyphs)
1503                        .map(|glyph| glyph.advance)
1504                        .sum::<f32>();
1505                    // Ligature components can share a larger glyph advance
1506                    // than their divided cluster advances; retain the glyph's
1507                    // full cell so no caret or selection edge lands inside
1508                    // that ligature. Justification and letter spacing can
1509                    // also grow the cluster cell beyond its raw glyphs.
1510                    let visible_edge = cluster_x + cluster.advance().max(glyph_advance);
1511                    let left = cluster_x.min(visible_edge);
1512                    let right = cluster_x.max(visible_edge);
1513                    let position = ClusterPosition {
1514                        range: cluster_range.clone(),
1515                        left,
1516                        right,
1517                        rtl,
1518                        ligature_start,
1519                        ligature_continuation,
1520                    };
1521                    let key = (cluster_range.start, cluster_range.end);
1522                    if let Some(index) = cluster_position_index.get(&key).copied() {
1523                        cluster_positions[index].include(&position);
1524                    } else {
1525                        cluster_position_index.insert(key, cluster_positions.len());
1526                        cluster_positions.push(position);
1527                    }
1528                    cluster_x = edge;
1529                }
1530            }
1531            for item in line.items() {
1532                let PositionedLayoutItem::GlyphRun(parley_run) = item else {
1533                    continue;
1534                };
1535                let run = parley_run.run();
1536                let font = self.font_handle_for(run.font());
1537                let font_id = font.id();
1538                if Some(font_id) != primary {
1539                    self.diagnostics.fallback_runs += 1;
1540                }
1541                used_fonts.insert(font_id);
1542                self.diagnostics.shaping_runs += 1;
1543
1544                // `PositionedLayoutItem::GlyphRun` is a line-local slice of
1545                // the underlying shaping run. Parley's renderer-facing Glyph
1546                // intentionally has no byte index, so recover each glyph's
1547                // cluster from the authoritative visual-cluster sequence
1548                // before translating it. This keeps caret movement and
1549                // selection on shaped cluster boundaries instead of assigning
1550                // every glyph to the run's first byte.
1551                //
1552                // Order and cardinality correspond structurally, not by
1553                // length coincidence: `glyphs()` and `positioned_glyphs()`
1554                // both iterate `Run::visual_clusters().flat_map(glyphs)`,
1555                // the latter only adding running x (which already carries
1556                // the line offset). The window below re-locates this
1557                // style-uniform slice within that same sequence, so the
1558                // zip pairs each positioned glyph with its own cluster
1559                // for RTL runs, ligatures, and multi-glyph clusters alike.
1560                let visible_glyphs = parley_run.glyphs().collect::<Vec<_>>();
1561                let all_clustered_glyphs = run
1562                    .visual_clusters()
1563                    .flat_map(|cluster| {
1564                        let cluster_range = cluster.text_range();
1565                        cluster
1566                            .glyphs()
1567                            .map(move |glyph| (glyph, cluster_range.clone()))
1568                    })
1569                    .collect::<Vec<_>>();
1570                let first_glyph = all_clustered_glyphs
1571                    .windows(visible_glyphs.len())
1572                    .position(|candidate| {
1573                        candidate
1574                            .iter()
1575                            .zip(&visible_glyphs)
1576                            .all(|((glyph, _), visible)| glyph == visible)
1577                    })
1578                    .unwrap_or(0);
1579                let clusters = all_clustered_glyphs
1580                    .iter()
1581                    .skip(first_glyph)
1582                    .map(|(_, range)| range.clone())
1583                    .take(visible_glyphs.len())
1584                    .collect::<Vec<_>>();
1585                let positions: Vec<_> = parley_run
1586                    .positioned_glyphs()
1587                    .zip(
1588                        clusters
1589                            .iter()
1590                            .map(|range| range.start as u32)
1591                            .chain(std::iter::repeat(run.text_range().start as u32)),
1592                    )
1593                    .map(|(glyph, cluster)| GlyphPosition {
1594                        id: glyph.id as u16,
1595                        // Incular's renderer uses `origin.y - offset.y`; Parley
1596                        // exposes glyph y from its top-left layout coordinate.
1597                        // Parley seeds positioned glyph x with the line
1598                        // alignment offset, so adding it again would translate
1599                        // aligned text twice.
1600                        offset: Offset::new(glyph.x, -glyph.y),
1601                        advance: glyph.advance,
1602                        cluster,
1603                    })
1604                    .collect();
1605                if runs.len() == 1 {
1606                    glyphs.extend(runs[0].glyphs.iter().copied());
1607                }
1608                if !runs.is_empty() {
1609                    glyphs.extend(positions.iter().copied());
1610                }
1611                runs.push(Arc::new(GlyphRun {
1612                    font,
1613                    font_size: run.font_size(),
1614                    origin: Offset::ZERO,
1615                    glyphs: positions.into(),
1616                }));
1617                font_runs.push(FontRunDebug {
1618                    range: run.text_range(),
1619                    font: font_id,
1620                    direction: if run.is_rtl() { "rtl" } else { "ltr" },
1621                });
1622            }
1623            // Every merged cluster, including zero-advance ones that only
1624            // carry caret stops; the layout derives its stops from these.
1625            let cluster_spans: Vec<_> = merge_ligature_components(cluster_positions)
1626                .iter()
1627                .map(|position| TextClusterSpan {
1628                    start: position.range.start as u32,
1629                    end: position.range.end as u32,
1630                    left: position.left,
1631                    right: position.right,
1632                    rtl: position.rtl,
1633                })
1634                .collect();
1635            // The common one-run line has exactly the same positioned glyphs
1636            // for caret handling and painting. Share the immutable allocation
1637            // instead of retaining a second copy in TextLine.
1638            let glyphs = if runs.len() == 1 {
1639                runs[0].glyphs.clone()
1640            } else {
1641                glyphs.into()
1642            };
1643            lines.push(TextLine {
1644                runs: runs.into(),
1645                glyphs,
1646                clusters: cluster_spans.into(),
1647                width: metrics.advance,
1648                offset: metrics.offset,
1649                baseline: metrics.baseline,
1650                start: range.start,
1651                caret_end,
1652                end: range.end,
1653            });
1654        }
1655
1656        if lines.is_empty() {
1657            lines.push(TextLine {
1658                runs: Arc::new([]),
1659                glyphs: Arc::new([]),
1660                clusters: Arc::new([]),
1661                width: 0.0,
1662                offset: 0.0,
1663                baseline: 0.0,
1664                start: 0,
1665                caret_end: 0,
1666                end: 0,
1667            });
1668        }
1669        self.diagnostics.fallback_fonts_used += used_fonts.len().saturating_sub(1) as u64;
1670        let baseline = lines[0].baseline;
1671        let line_height = layout
1672            .lines()
1673            .next()
1674            .map_or(0.0, |line| line.metrics().line_height);
1675        let mut translated = TextLayout {
1676            lines: lines.into(),
1677            metrics: TextMetrics {
1678                size: Size::new(layout.width(), layout.height()),
1679                baseline,
1680                line_height,
1681            },
1682            font_runs: font_runs.into(),
1683            caret_positions: OnceLock::new(),
1684            line_carets: OnceLock::new(),
1685            overflowed: false,
1686        };
1687        if has_join_controls(text) {
1688            // Carets keep Parley's complete grapheme boundaries. Selection
1689            // coverage separately excludes invisible format controls, even
1690            // when Parley attaches them to a visible grapheme.
1691            translated
1692                .caret_positions
1693                .get_or_init(|| translated.derive_caret_positions());
1694            for line in Arc::make_mut(&mut translated.lines) {
1695                let mut visible = Vec::with_capacity(line.clusters.len());
1696                for span in line.clusters.iter() {
1697                    let Some(component) = text.get(span.range()) else {
1698                        visible.push(*span);
1699                        continue;
1700                    };
1701                    let mut start = span.start;
1702                    for (offset, character) in component.char_indices() {
1703                        if matches!(character, '\u{200C}' | '\u{200D}') {
1704                            let end = span.start + offset as u32;
1705                            if start < end {
1706                                visible.push(TextClusterSpan {
1707                                    start,
1708                                    end,
1709                                    ..*span
1710                                });
1711                            }
1712                            start = end + character.len_utf8() as u32;
1713                        }
1714                    }
1715                    if start < span.end {
1716                        visible.push(TextClusterSpan { start, ..*span });
1717                    }
1718                }
1719                line.clusters = visible.into();
1720            }
1721        }
1722        translated
1723    }
1724}
1725
1726fn has_join_controls(text: &str) -> bool {
1727    text.contains('\u{200C}') || text.contains('\u{200D}')
1728}
1729
1730/// Coalesces Parley's glyphless ligature components with their rendered
1731/// cluster. Visual cluster iteration is forward for LTR runs and reversed for
1732/// RTL runs, so the component sequence starts at opposite ends of a ligature
1733/// in each direction.
1734fn merge_ligature_components(positions: Vec<ClusterPosition>) -> Vec<ClusterPosition> {
1735    let mut merged = Vec::with_capacity(positions.len());
1736    let mut pending: Option<ClusterPosition> = None;
1737    let mut pending_has_start = false;
1738
1739    for position in positions {
1740        let is_start = position.ligature_start;
1741        let is_continuation = position.ligature_continuation;
1742        if !is_start && !is_continuation {
1743            if let Some(pending) = pending.take() {
1744                merged.push(pending);
1745            }
1746            pending_has_start = false;
1747            merged.push(position);
1748            continue;
1749        }
1750        // A single source cluster can already cover the whole ligature.
1751        if is_start && is_continuation {
1752            if let Some(pending) = pending.take() {
1753                merged.push(pending);
1754            }
1755            pending_has_start = false;
1756            merged.push(position);
1757            continue;
1758        }
1759
1760        if position.rtl {
1761            if is_continuation {
1762                // In visual RTL order, continuations precede their start. A
1763                // continuation after a completed group begins the next one.
1764                if pending_has_start {
1765                    if let Some(pending) = pending.take() {
1766                        merged.push(pending);
1767                    }
1768                    pending_has_start = false;
1769                }
1770                let can_extend = pending
1771                    .as_ref()
1772                    .is_some_and(|pending| pending.can_join(&position));
1773                if can_extend {
1774                    pending
1775                        .as_mut()
1776                        .expect("checked pending ligature group")
1777                        .include(&position);
1778                } else {
1779                    if let Some(pending) = pending.take() {
1780                        merged.push(pending);
1781                    }
1782                    pending_has_start = false;
1783                    pending = Some(position);
1784                }
1785            } else {
1786                // The start completes the pending continuation group.
1787                match pending.take() {
1788                    Some(mut pending_group)
1789                        if !pending_has_start && pending_group.can_join(&position) =>
1790                    {
1791                        pending_group.include(&position);
1792                        merged.push(pending_group);
1793                    }
1794                    Some(pending_group) => {
1795                        merged.push(pending_group);
1796                        merged.push(position);
1797                    }
1798                    None => merged.push(position),
1799                }
1800                pending_has_start = false;
1801            }
1802        } else if is_start {
1803            if let Some(pending) = pending.take() {
1804                merged.push(pending);
1805            }
1806            pending = Some(position);
1807            pending_has_start = true;
1808        } else if pending.is_some() {
1809            // LTR starts precede their continuations in visual order. The
1810            // byte check prevents an excluded join control from being
1811            // bridged by a later glyphless continuation.
1812            if pending
1813                .as_ref()
1814                .is_some_and(|pending_group| pending_group.can_join(&position))
1815            {
1816                pending
1817                    .as_mut()
1818                    .expect("checked pending ligature group")
1819                    .include(&position);
1820            } else {
1821                let pending_group = pending.take().expect("pending ligature group");
1822                merged.push(pending_group);
1823                pending = Some(position);
1824                pending_has_start = false;
1825            }
1826        } else {
1827            // A run can begin in the middle of a shaped ligature if a prior
1828            // fallback/style run owns its start.
1829            pending = Some(position);
1830            pending_has_start = false;
1831        }
1832    }
1833
1834    if let Some(pending) = pending {
1835        merged.push(pending);
1836    }
1837    merged
1838}
1839
1840/// Length in bytes of the longest common prefix of `left` and `right`.
1841fn common_prefix_len(left: &str, right: &str) -> usize {
1842    let (left, right) = (left.as_bytes(), right.as_bytes());
1843    let mut prefix = 0;
1844    for (a, b) in left.chunks(64).zip(right.chunks(64)) {
1845        if a == b {
1846            prefix += a.len();
1847        } else {
1848            return prefix + a.iter().zip(b).take_while(|(x, y)| x == y).count();
1849        }
1850    }
1851    prefix
1852}
1853
1854/// Whether `layout` lays out identically at every width it fits: one
1855/// unclipped line, start-aligned in a left-to-right paragraph, unshifted.
1856fn width_independent(layout: &TextLayout, options: TextLayoutOptions) -> bool {
1857    matches!(
1858        options.align,
1859        TextAlign::Start | TextAlign::Left | TextAlign::Justify
1860    ) && options.text_direction != Some(TextDirection::Rtl)
1861        && !layout.overflowed
1862        && layout.lines.len() == 1
1863        && layout.lines[0].offset == 0.0
1864        && layout.font_runs.iter().all(|run| run.direction != "rtl")
1865}
1866
1867fn clipped_layout(mut layout: TextLayout, limit: usize) -> TextLayout {
1868    let lines = layout.lines.iter().take(limit).cloned().collect::<Vec<_>>();
1869    if let Some(carets) = layout.caret_positions.get_mut() {
1870        let retained = carets.partition_point(|position| position.line < lines.len());
1871        if retained < carets.len() {
1872            *carets = Arc::from(&carets[..retained]);
1873        }
1874    }
1875    let width = lines
1876        .iter()
1877        .fold(0.0_f32, |width, line| width.max(line.width));
1878    layout.metrics.size = Size::new(width, layout.metrics.line_height * lines.len() as f32);
1879    layout.metrics.baseline = lines.first().map_or(0.0, |line| line.baseline);
1880    layout.lines = lines.into();
1881    layout.line_carets = OnceLock::new();
1882    layout.overflowed = true;
1883    layout
1884}
1885
1886fn grapheme_boundary_at_or_before(text: &str, offset: usize) -> usize {
1887    GraphemeClusterSegmenter::new()
1888        .segment_str(text)
1889        .take_while(|boundary| *boundary <= offset)
1890        .last()
1891        .unwrap_or(0)
1892}
1893
1894fn previous_grapheme_boundary(text: &str, offset: usize) -> usize {
1895    GraphemeClusterSegmenter::new()
1896        .segment_str(text)
1897        .take_while(|boundary| *boundary < offset)
1898        .last()
1899        .unwrap_or(0)
1900}
1901
1902fn family_name(family: &FontFamily) -> String {
1903    match family {
1904        FontFamily::SansSerif => "sans-serif".into(),
1905        FontFamily::Serif => "serif".into(),
1906        FontFamily::Monospace => "monospace".into(),
1907        FontFamily::Cursive => "cursive".into(),
1908        FontFamily::SystemUi => "system-ui".into(),
1909        FontFamily::Named(name) => format!("'{name}'"),
1910    }
1911}
1912
1913/// Identity for one font blob sighting. Pointer identity is stable because
1914/// `Blob` clones share the same backing allocation, so repeated sightings of
1915/// the same face never re-copy or re-hash the bytes.
1916type FontBlobKey = (usize, usize, u32);
1917
1918fn font_blob_key(font: &parley::FontData) -> FontBlobKey {
1919    (
1920        font.data.as_ref().as_ptr() as usize,
1921        font.data.as_ref().len(),
1922        font.index,
1923    )
1924}
1925
1926/// Returns the shared handle for this font blob, building it (one full-file
1927/// hash, no copy) on first sight only.
1928fn font_handle(engine: &mut TextEngine, font: &parley::FontData) -> FontHandle {
1929    let key = font_blob_key(font);
1930    if let Some(handle) = engine.font_handles.get(&key) {
1931        return handle.clone();
1932    }
1933    let mut hasher = DefaultHasher::new();
1934    font.data.as_ref().hash(&mut hasher);
1935    hasher.write_u64(u64::from(font.index));
1936    let id = FontId(hasher.finish());
1937    // Share discovery's (typically memory-mapped) source instead of copying
1938    // the whole font file into a private heap allocation.
1939    let handle = FontHandle::with_face_index(
1940        id,
1941        FontBytes::from_shared(font.data.clone().into_raw_parts().0),
1942        font.index,
1943    );
1944    engine.font_handles.insert(key, handle.clone());
1945    handle
1946}
1947
1948/// Places an explicit line height's leading as Flutter's
1949/// `TextHeightBehavior` and `TextLeadingDistribution` do. Parley splits a
1950/// line's leading evenly above and below its content; `Proportional` splits
1951/// it in the ratio of the content's ascent to its descent instead. Without
1952/// `apply_height_to_first_ascent` the first line loses the leading above its
1953/// content, and without `apply_height_to_last_descent` the last line loses
1954/// the leading below it.
1955fn apply_height_behavior(
1956    translated: &mut TextLayout,
1957    layout: &Layout<()>,
1958    behavior: TextHeightBehavior,
1959    style_distribution: Option<TextLeadingDistribution>,
1960) {
1961    let distribution = style_distribution.unwrap_or(behavior.leading_distribution);
1962    // Each line's baseline shift, and the leading left above and below it.
1963    let placements: Vec<(f32, f32, f32)> = layout
1964        .lines()
1965        .map(|line| {
1966            let metrics = line.metrics();
1967            let ascent = metrics.baseline - metrics.content_block_min_coord;
1968            let descent = metrics.content_block_max_coord - metrics.baseline;
1969            let height = metrics.block_max_coord - metrics.block_min_coord;
1970            let baseline = match distribution {
1971                TextLeadingDistribution::Proportional if ascent + descent > 0. => {
1972                    metrics.block_min_coord + height * ascent / (ascent + descent)
1973                }
1974                _ => metrics.baseline,
1975            };
1976            (
1977                baseline - metrics.baseline,
1978                baseline - ascent - metrics.block_min_coord,
1979                metrics.block_max_coord - baseline - descent,
1980            )
1981        })
1982        .collect();
1983    let (Some(first), Some(last)) = (placements.first(), placements.last()) else {
1984        return;
1985    };
1986    let top = if behavior.apply_height_to_first_ascent {
1987        0.
1988    } else {
1989        first.1
1990    };
1991    let bottom = if behavior.apply_height_to_last_descent {
1992        0.
1993    } else {
1994        last.2
1995    };
1996    if top == 0. && bottom == 0. && placements.iter().all(|placement| placement.0 == 0.) {
1997        return;
1998    }
1999    for (line, placement) in Arc::make_mut(&mut translated.lines)
2000        .iter_mut()
2001        .zip(&placements)
2002    {
2003        let shift = placement.0 - top;
2004        if shift == 0. {
2005            continue;
2006        }
2007        line.baseline += shift;
2008        // Stored glyph y is negative layout space, as in `layout_document`.
2009        line.glyphs = line
2010            .glyphs
2011            .iter()
2012            .map(|glyph| {
2013                let mut shifted = *glyph;
2014                shifted.offset.y -= shift;
2015                shifted
2016            })
2017            .collect();
2018        line.runs = line
2019            .runs
2020            .iter()
2021            .map(|run| {
2022                let mut shifted = (**run).clone();
2023                shifted.origin.y += shift;
2024                Arc::new(shifted)
2025            })
2026            .collect();
2027    }
2028    translated.metrics.size.height = (translated.metrics.size.height - top - bottom).max(0.);
2029    translated.metrics.baseline = translated.lines[0].baseline;
2030}