Skip to main content

oxitext_layout/engine/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::linebreak::{LineBreak, LineBreaker};
6use oxitext_core::{
7    FontVerticalMetrics, LayoutConstraints, OxiTextError, PositionedGlyph, ShapedGlyph, ShapedRun,
8    TextAlignment,
9};
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use std::sync::Arc;
13
14use super::functions::{
15    advance_for_glyph, apply_hanging_punctuation, apply_truncation, build_ranges_from_kp_breaks,
16    compute_alignment, count_internal_ws_gaps, find_cluster_for_positioned_glyph,
17};
18
19/// Controls which line-breaking algorithm the layout engine uses.
20///
21/// The default is [`BreakingStrategy::Greedy`], which runs in O(n) and matches
22/// the behaviour of browsers' `white-space: normal` wrapping.
23/// [`BreakingStrategy::KnuthPlass`] minimises total paragraph demerits (see
24/// [`crate::knuth_plass`]) and typically produces more even line lengths.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub enum BreakingStrategy {
27    /// Greedy (first-fit) algorithm — O(n), default.
28    #[default]
29    Greedy,
30    /// Knuth-Plass optimal algorithm — minimises total paragraph demerits.
31    ///
32    /// Falls back to greedy when `max_width` is 0 or no feasible solution
33    /// exists.
34    KnuthPlass,
35}
36/// The structured result of a layout pass.
37#[derive(Debug, Clone)]
38pub struct LayoutResult {
39    /// All positioned glyphs in logical (reading) order.
40    pub glyphs: Vec<PositionedGlyph>,
41    /// Line records indexing into [`Self::glyphs`].
42    pub lines: Vec<Line>,
43    /// Aggregate metrics.
44    pub metrics: ParagraphMetrics,
45    /// Decoration rectangles computed from the layout (underlines, overlines,
46    /// strikethroughs). Empty unless decorations are requested via
47    /// [`crate::options::LayoutOptions::decoration`].
48    pub decorations: Vec<oxitext_core::DecorationRect>,
49    /// Positioned inline objects (images, widgets) placed during layout.
50    pub inline_objects: Vec<oxitext_core::PositionedInlineObject>,
51}
52impl LayoutResult {
53    /// Find the glyph nearest to pixel coordinates `(x, y)` for hit-testing
54    /// and cursor placement during text selection.
55    ///
56    /// Returns `Some((line_index, glyph_index_within_line, cluster_byte_offset))`
57    /// where:
58    /// - `line_index` is the index into [`LayoutResult::lines`],
59    /// - `glyph_index_within_line` is the 0-based position within that line's
60    ///   glyph range (i.e. `0` is the first glyph of the line),
61    /// - `cluster_byte_offset` is [`PositionedGlyph::cluster`] — the UTF-8
62    ///   byte offset of the glyph's source character.
63    ///
64    /// If `(x, y)` falls outside all lines the nearest line is chosen. If it
65    /// falls outside all glyphs on the chosen line the nearest endpoint glyph
66    /// is returned.
67    ///
68    /// Returns `None` only when `self.lines` is empty.
69    pub fn hit_test(&self, x: f32, y: f32) -> Option<(usize, usize, u32)> {
70        if self.lines.is_empty() {
71            return None;
72        }
73        let line_idx = {
74            let mut best = 0usize;
75            let mut best_dist = f32::MAX;
76            'line_search: for (li, line) in self.lines.iter().enumerate() {
77                let top = line.metrics.baseline_y - line.metrics.ascent;
78                let bottom = line.metrics.baseline_y + line.metrics.descent;
79                if y >= top && y <= bottom {
80                    best = li;
81                    break 'line_search;
82                }
83                let mid = (top + bottom) * 0.5;
84                let dist = (y - mid).abs();
85                if dist < best_dist {
86                    best_dist = dist;
87                    best = li;
88                }
89            }
90            best
91        };
92        let line = &self.lines[line_idx];
93        let gs = line.glyph_start;
94        let ge = line.glyph_end;
95        if gs >= ge {
96            return Some((line_idx, 0, 0));
97        }
98        let mut best_gi = gs;
99        let mut best_dist = f32::MAX;
100        'glyph_search: for gi in gs..ge {
101            let g = &self.glyphs[gi];
102            let left = g.pos.0;
103            let right = g.pos.0 + g.advance_x;
104            if x >= left && x <= right {
105                best_gi = gi;
106                break 'glyph_search;
107            }
108            let mid = (left + right) * 0.5;
109            let dist = (x - mid).abs();
110            if dist < best_dist {
111                best_dist = dist;
112                best_gi = gi;
113            }
114        }
115        let glyph_idx_in_line = best_gi - gs;
116        let cluster = self.glyphs[best_gi].cluster;
117        Some((line_idx, glyph_idx_in_line, cluster))
118    }
119
120    /// Return the unique `(glyph_id, font_size)` pairs present in this layout,
121    /// suitable for pre-warming an SDF atlas or batching rasterisation.
122    ///
123    /// Each `(gid, px_size)` pair appears exactly once regardless of how many
124    /// times that glyph occurs in the layout.  The order of the returned entries
125    /// is stable: pairs are emitted in the order their first occurrence is
126    /// encountered when iterating [`Self::glyphs`] from index 0.
127    ///
128    /// The `font_size_bits` value stored internally as a `u32` key is obtained
129    /// via `font_size.to_bits()` so that equal IEEE-754 floats are always
130    /// treated as equal keys even in a `HashSet`.
131    ///
132    /// # Rasteriser usage
133    ///
134    /// ```rust,ignore
135    /// for (glyph_id, px_size) in layout.unique_glyphs_for_atlas() {
136    ///     atlas.pre_warm(glyph_id, px_size);
137    /// }
138    /// ```
139    pub fn unique_glyphs_for_atlas(&self) -> Vec<(u16, f32)> {
140        use std::collections::HashSet;
141        let mut seen: HashSet<(u16, u32)> = HashSet::new();
142        let mut result = Vec::new();
143        for g in &self.glyphs {
144            let key = (g.gid, g.font_size.to_bits());
145            if seen.insert(key) {
146                result.push((g.gid, g.font_size));
147            }
148        }
149        result
150    }
151
152    /// Return per-glyph `(glyph_id, x, y, font_size)` tuples ready for direct
153    /// handoff to a rasteriser.
154    ///
155    /// Positions are in pixel coordinates with the origin at the top-left of
156    /// the text block (matching [`PositionedGlyph::pos`]).  The returned
157    /// `Vec` preserves logical (reading) order and contains exactly one entry
158    /// per glyph in [`Self::glyphs`].
159    ///
160    /// # Rasteriser usage
161    ///
162    /// ```rust,ignore
163    /// for (gid, x, y, px_size) in layout.rasterization_inputs() {
164    ///     rasterizer.draw(gid, x, y, px_size);
165    /// }
166    /// ```
167    pub fn rasterization_inputs(&self) -> Vec<(u16, f32, f32, f32)> {
168        self.glyphs
169            .iter()
170            .map(|g| (g.gid, g.pos.0, g.pos.1, g.font_size))
171            .collect()
172    }
173
174    /// Returns the set of glyphs that should be pre-loaded into an SDF atlas
175    /// before rendering.  Each entry is `(glyph_id, px_size)`.
176    ///
177    /// This is an alias for [`Self::unique_glyphs_for_atlas`] with an
178    /// SDF-oriented name for use at the oxitext-sdf integration boundary.
179    ///
180    /// # Usage with oxitext-sdf
181    ///
182    /// ```rust,ignore
183    /// let layout = engine.layout(text, runs, &constraints, alignment, None)?;
184    /// for (glyph_id, px_size) in layout.sdf_glyph_set() {
185    ///     if let Ok(Some(tile)) =
186    ///         oxitext_sdf::glyph_to_sdf_tile(font_data, glyph_id, px_size, 64, 4.0)
187    ///     {
188    ///         atlas.pack_tile(tile);
189    ///     }
190    /// }
191    /// ```
192    pub fn sdf_glyph_set(&self) -> Vec<(u16, f32)> {
193        self.unique_glyphs_for_atlas()
194    }
195}
196/// Resolved vertical line metrics derived from font metrics or a size fallback.
197#[derive(Debug, Clone, Copy)]
198struct VerticalLineModel {
199    ascent: f32,
200    descent: f32,
201    leading: f32,
202    line_height: f32,
203}
204impl VerticalLineModel {
205    /// Build a vertical model from optional font metrics and a font size.
206    ///
207    /// When `metrics` is `Some`, the design-unit ascender/descender/line-gap
208    /// are scaled to pixels by `font_size / units_per_em`. Otherwise a
209    /// reasonable fallback of `0.8 / 0.2 / 0.4 × font_size` is used (the same
210    /// proportions the legacy [`crate::SimpleLayouter`] assumed).
211    fn from_metrics(metrics: Option<&FontVerticalMetrics>, font_size: f32) -> Self {
212        match metrics {
213            Some(m) => {
214                let ascent = m.ascent_px(font_size);
215                let descent = m.descent_px(font_size);
216                let leading = m.line_gap_px(font_size);
217                Self {
218                    ascent,
219                    descent,
220                    leading,
221                    line_height: ascent + descent + leading,
222                }
223            }
224            None => {
225                let ascent = font_size * 0.8;
226                let descent = font_size * 0.2;
227                let leading = font_size * 0.4;
228                Self {
229                    ascent,
230                    descent,
231                    leading,
232                    line_height: ascent + descent + leading,
233                }
234            }
235        }
236    }
237}
238/// Word-aware, alignment-capable layout engine.
239///
240/// Carries two optional caches to improve throughput in GUI loops and other
241/// scenarios where the same (or similarly-sized) text is laid out repeatedly:
242///
243/// - **`scratch`** — a reusable [`PositionedGlyph`] buffer.  On every
244///   [`Self::layout_with_strategy`] call the buffer is cleared (keeping its
245///   allocated capacity) and refilled, so the heap allocation survives across
246///   calls.
247/// - **`break_cache_text` / `break_cache_ops`** — the last source string and
248///   its precomputed UAX #14 break opportunities.  When the caller re-lays
249///   out the same text (e.g. after a window resize) the expensive
250///   [`crate::linebreak::LineBreaker`] pass is skipped.
251/// - **`dirty_ranges`** — byte offset ranges in the source text that have
252///   changed since the last layout pass.  When non-empty, the next layout
253///   call will re-break all affected lines.  Cleared automatically by
254///   [`Self::layout_if_dirty`] after a successful relayout.
255#[derive(Debug, Default)]
256pub struct LayoutEngine {
257    /// Reusable scratch buffer for positioned glyphs (capacity survives calls).
258    scratch: Vec<PositionedGlyph>,
259    /// Source text of the last break-opportunity computation.
260    pub(crate) break_cache_text: String,
261    /// Break opportunities from the last computation: `(byte_offset, kind)`.
262    pub(crate) break_cache_ops: Vec<(usize, crate::linebreak::LineBreak)>,
263    /// Dirty ranges (byte offsets in the source text) that have changed since
264    /// the last layout pass.  If non-empty, the next layout call will re-break
265    /// all lines.  In a future optimisation only lines overlapping dirty ranges
266    /// would be re-broken; for now the full paragraph is always re-laid out.
267    dirty_ranges: Vec<std::ops::Range<usize>>,
268}
269impl LayoutEngine {
270    /// Creates a new layout engine.
271    pub fn new() -> Self {
272        Self {
273            scratch: Vec::new(),
274            break_cache_text: String::new(),
275            break_cache_ops: Vec::new(),
276            dirty_ranges: Vec::new(),
277        }
278    }
279
280    /// Mark a byte range of the source text as modified (content changed,
281    /// inserted, or deleted).  The next layout call will re-layout lines
282    /// affected by this range.
283    ///
284    /// Multiple overlapping or disjoint ranges can be accumulated before
285    /// triggering a layout pass.  All dirty markers are cleared automatically
286    /// by [`Self::layout_if_dirty`] after a successful relayout.
287    pub fn mark_dirty(&mut self, range: std::ops::Range<usize>) {
288        self.dirty_ranges.push(range);
289    }
290
291    /// Clear all dirty markers.
292    ///
293    /// Called automatically by [`Self::layout_if_dirty`] after a layout pass.
294    /// You can also call this manually to discard pending dirty state without
295    /// triggering a relayout (e.g. after discarding the associated text edit).
296    pub fn clear_dirty(&mut self) {
297        self.dirty_ranges.clear();
298    }
299
300    /// Returns `true` if any text range has been marked dirty since the last
301    /// [`Self::clear_dirty`] or [`Self::layout_if_dirty`] call.
302    pub fn has_dirty(&self) -> bool {
303        !self.dirty_ranges.is_empty()
304    }
305
306    /// Relayout only if dirty; otherwise return the cached layout result.
307    ///
308    /// - `cached`: the previous [`LayoutResult`] to return unchanged when no
309    ///   dirty ranges are pending and a cached result is available.
310    /// - `layout_fn`: a closure that produces a fresh [`LayoutResult`] when a
311    ///   relayout is needed.  The closure receives `&mut LayoutEngine` so it
312    ///   can call any of the layout methods directly.
313    ///
314    /// After a relayout `layout_fn` is invoked, all dirty markers are cleared
315    /// automatically.  If the engine is clean *and* `cached` is `None`, the
316    /// closure is still called (there is nothing to return otherwise).
317    pub fn layout_if_dirty<F>(&mut self, cached: Option<LayoutResult>, layout_fn: F) -> LayoutResult
318    where
319        F: FnOnce(&mut LayoutEngine) -> LayoutResult,
320    {
321        if self.dirty_ranges.is_empty() {
322            if let Some(prev) = cached {
323                return prev;
324            }
325        }
326        let result = layout_fn(self);
327        self.clear_dirty();
328        result
329    }
330    /// Lays out `runs` over `source_text`, wrapping at line-break opportunities.
331    ///
332    /// When the `icu` feature is enabled the layout uses CLDR-compliant line
333    /// breaking via [`Self::layout_cldr`] (better quality for CJK, Thai, and
334    /// other complex scripts).  Without the `icu` feature this falls back to
335    /// UAX #14 line breaking via the greedy (first-fit) algorithm.
336    ///
337    /// To explicitly request UAX #14 line breaking regardless of the `icu`
338    /// feature, use [`Self::layout_uax14`].
339    ///
340    /// - `source_text` must be the exact string the runs were shaped from, so
341    ///   that [`ShapedGlyph::cluster`] byte offsets index into it.
342    /// - `constraints.max_width` of `0.0` disables wrapping (single line per
343    ///   mandatory break).
344    /// - `alignment` controls horizontal placement within `max_width`.
345    /// - `font_metrics`, when supplied, drives accurate line height; otherwise
346    ///   a size-proportional fallback is used.
347    ///
348    /// # Errors
349    /// Currently infallible for well-formed input; returns `Err` only for
350    /// forward compatibility.
351    pub fn layout(
352        &mut self,
353        source_text: &str,
354        runs: &[ShapedRun],
355        constraints: &LayoutConstraints,
356        alignment: TextAlignment,
357        font_metrics: Option<&FontVerticalMetrics>,
358    ) -> Result<LayoutResult, OxiTextError> {
359        #[cfg(feature = "icu")]
360        {
361            // When ICU is available, use CLDR-compliant line breaking for
362            // better quality segmentation across complex scripts.
363            self.layout_cldr(source_text, runs, constraints, alignment, font_metrics)
364        }
365        #[cfg(not(feature = "icu"))]
366        {
367            // Fall back to UAX #14 unicode-linebreak (greedy algorithm).
368            self.layout_with_strategy(
369                source_text,
370                runs,
371                constraints,
372                alignment,
373                font_metrics,
374                BreakingStrategy::Greedy,
375            )
376        }
377    }
378
379    /// Lays out `runs` using UAX #14 (`unicode-linebreak`) line breaking,
380    /// regardless of whether the `icu` feature is compiled in.
381    ///
382    /// This is the explicit opt-out from CLDR line breaking.  Use this when
383    /// you need a consistent UAX #14 code path independent of feature flags,
384    /// for example in tests that compare break positions.
385    ///
386    /// Uses the greedy (first-fit) algorithm.  For Knuth-Plass optimal breaking
387    /// call [`LayoutEngine::layout_with_strategy`] directly with
388    /// [`BreakingStrategy::KnuthPlass`].
389    ///
390    /// # Errors
391    /// Currently infallible for well-formed input; returns `Err` only for
392    /// forward compatibility.
393    pub fn layout_uax14(
394        &mut self,
395        source_text: &str,
396        runs: &[ShapedRun],
397        constraints: &LayoutConstraints,
398        alignment: TextAlignment,
399        font_metrics: Option<&FontVerticalMetrics>,
400    ) -> Result<LayoutResult, OxiTextError> {
401        self.layout_with_strategy(
402            source_text,
403            runs,
404            constraints,
405            alignment,
406            font_metrics,
407            BreakingStrategy::Greedy,
408        )
409    }
410    /// Lays out `runs` over `source_text` using the specified breaking
411    /// strategy.
412    ///
413    /// This is the full-featured entry point.  [`LayoutEngine::layout`] is a
414    /// convenience wrapper that always uses [`BreakingStrategy::Greedy`].
415    ///
416    /// When `strategy` is [`BreakingStrategy::KnuthPlass`] and
417    /// `constraints.max_width > 0`, the algorithm calls
418    /// [`crate::knuth_plass::optimal_breaks`] to compute globally optimal
419    /// break positions before positioning glyphs.  If the KP solver finds no
420    /// feasible solution it automatically falls back to the greedy algorithm.
421    ///
422    /// # Errors
423    /// Currently infallible for well-formed input; returns `Err` only for
424    /// forward compatibility.
425    pub fn layout_with_strategy(
426        &mut self,
427        source_text: &str,
428        runs: &[ShapedRun],
429        constraints: &LayoutConstraints,
430        alignment: TextAlignment,
431        font_metrics: Option<&FontVerticalMetrics>,
432        strategy: BreakingStrategy,
433    ) -> Result<LayoutResult, OxiTextError> {
434        self.layout_impl(
435            source_text,
436            runs,
437            constraints,
438            alignment,
439            font_metrics,
440            strategy,
441            None,
442        )
443    }
444    /// Lays out `runs` using externally-supplied break point byte offsets.
445    ///
446    /// Identical to [`LayoutEngine::layout_with_strategy`] (greedy algorithm)
447    /// except that instead of computing UAX #14 break opportunities internally,
448    /// this method treats every offset in `break_points` as an
449    /// [`crate::linebreak::LineBreak::Allowed`] opportunity.  This allows
450    /// callers — e.g. the facade or ICU-backed pipeline — to inject their own
451    /// (CLDR-compliant) break points without re-running the built-in linebreaker.
452    ///
453    /// # Arguments
454    /// - `source_text` — the source string the runs were shaped from.
455    /// - `runs` — shaped glyph runs.
456    /// - `constraints` — layout constraints (max width, font size).
457    /// - `alignment` — horizontal text alignment.
458    /// - `font_metrics` — optional font vertical metrics.
459    /// - `break_points` — slice of UTF-8 byte offsets where line breaks are
460    ///   permitted.  The slice need not be sorted (it will be searched with
461    ///   binary search after sorting internally).
462    ///
463    /// # Errors
464    /// Currently infallible for well-formed input.
465    pub fn layout_with_break_points(
466        &mut self,
467        source_text: &str,
468        runs: &[ShapedRun],
469        constraints: &LayoutConstraints,
470        alignment: TextAlignment,
471        font_metrics: Option<&FontVerticalMetrics>,
472        break_points: &[usize],
473    ) -> Result<LayoutResult, OxiTextError> {
474        self.layout_impl(
475            source_text,
476            runs,
477            constraints,
478            alignment,
479            font_metrics,
480            BreakingStrategy::Greedy,
481            Some(break_points),
482        )
483    }
484    /// CLDR-compliant layout using [`oxitext_icu::IcuSegmenter`] for line breaking.
485    ///
486    /// When the `icu` feature is enabled, this method creates an
487    /// [`oxitext_icu::IcuSegmenter`], queries CLDR line-break opportunities for
488    /// `source_text`, and delegates to [`Self::layout_with_break_points`].
489    ///
490    /// This provides CLDR-compliant line breaking as a drop-in replacement for
491    /// the UAX #14 unicode-linebreak path used by [`Self::layout`].
492    ///
493    /// # Errors
494    /// Currently infallible for well-formed input.
495    #[cfg(feature = "icu")]
496    pub fn layout_cldr(
497        &mut self,
498        source_text: &str,
499        runs: &[ShapedRun],
500        constraints: &LayoutConstraints,
501        alignment: TextAlignment,
502        font_metrics: Option<&FontVerticalMetrics>,
503    ) -> Result<LayoutResult, OxiTextError> {
504        let seg = oxitext_icu::IcuSegmenter::new();
505        let icu_breaks = seg.line_break_opportunities(source_text);
506
507        // Build the combined break list: ICU Allowed breaks merged with
508        // Mandatory breaks at every hard-break character (`\n`, `\r\n`).
509        // We pre-seed break_cache_ops so that layout_impl sees the correct
510        // LineBreak::Mandatory entries for hard newlines — ICU returns
511        // line-break *opportunities* as Allowed only.
512        let mut ops: Vec<(usize, LineBreak)> = icu_breaks
513            .iter()
514            .map(|&off| (off, LineBreak::Allowed))
515            .collect();
516
517        for (i, c) in source_text.char_indices() {
518            if c == '\n' {
519                // Byte offset of the character *after* the newline is the break point.
520                let after_newline = i + c.len_utf8();
521                ops.push((after_newline, LineBreak::Mandatory));
522            }
523        }
524
525        // Also merge soft-hyphen (U+00AD) opportunities for parity with the
526        // non-ICU path.
527        let soft = crate::hyphenation::soft_hyphen_breaks(source_text);
528        for off in soft {
529            ops.push((off, LineBreak::Allowed));
530        }
531
532        // Sort by offset, then deduplicate: Mandatory wins over Allowed at the
533        // same offset.
534        ops.sort_unstable_by_key(|(off, _)| *off);
535        ops.dedup_by(|later, earlier| {
536            if later.0 == earlier.0 {
537                if later.1 == LineBreak::Mandatory {
538                    earlier.1 = LineBreak::Mandatory;
539                }
540                true // remove `later` (keep `earlier`, now possibly upgraded)
541            } else {
542                false
543            }
544        });
545
546        // Pre-seed the cache so layout_impl reuses our ops.
547        self.break_cache_text = source_text.to_owned();
548        self.break_cache_ops = ops;
549
550        // Delegate directly to layout_with_strategy (bypassing layout() to avoid
551        // re-entering layout_cldr when the icu feature is enabled).  The break
552        // cache is already populated, so no LineBreaker pass will be triggered.
553        self.layout_with_strategy(
554            source_text,
555            runs,
556            constraints,
557            alignment,
558            font_metrics,
559            BreakingStrategy::Greedy,
560        )
561    }
562    /// Internal layout implementation shared by all horizontal layout paths.
563    ///
564    /// `external_breaks`, when `Some`, bypasses the UAX #14 `LineBreaker` and
565    /// treats every provided byte offset as an [`LineBreak::Allowed`]
566    /// opportunity. When `None`, break opportunities are computed (and cached)
567    /// by the built-in [`LineBreaker`].
568    #[allow(clippy::too_many_arguments)]
569    fn layout_impl(
570        &mut self,
571        source_text: &str,
572        runs: &[ShapedRun],
573        constraints: &LayoutConstraints,
574        alignment: TextAlignment,
575        font_metrics: Option<&FontVerticalMetrics>,
576        strategy: BreakingStrategy,
577        external_breaks: Option<&[usize]>,
578    ) -> Result<LayoutResult, OxiTextError> {
579        let model = VerticalLineModel::from_metrics(font_metrics, constraints.font_size);
580        let bidi_levels: Option<Vec<unicode_bidi::Level>> =
581            if crate::reorder::needs_bidi(source_text) {
582                Some(
583                    crate::bidi::BidiParagraph::new(source_text, None)
584                        .levels()
585                        .to_vec(),
586                )
587            } else {
588                None
589            };
590        let ext_sorted: Option<Vec<usize>> = external_breaks.map(|bp| {
591            let mut v = bp.to_vec();
592            v.sort_unstable();
593            v
594        });
595        if ext_sorted.is_none() && source_text != self.break_cache_text {
596            let breaker = LineBreaker::new(source_text);
597            let mut ops = breaker.breaks().to_vec();
598
599            // Merge soft-hyphen break opportunities (U+00AD).
600            // `soft_hyphen_breaks` returns "after" offsets matching the
601            // same convention used by unicode-linebreak and LineBreaker.
602            let soft = crate::hyphenation::soft_hyphen_breaks(source_text);
603            for off in soft {
604                ops.push((off, LineBreak::Allowed));
605            }
606
607            // Sort by offset; deduplicate with Mandatory winning over Allowed
608            // at the same position.
609            ops.sort_unstable_by_key(|(off, _)| *off);
610            ops.dedup_by(|later, earlier| {
611                if later.0 == earlier.0 {
612                    if later.1 == LineBreak::Mandatory {
613                        earlier.1 = LineBreak::Mandatory;
614                    }
615                    true
616                } else {
617                    false
618                }
619            });
620
621            self.break_cache_ops = ops;
622            self.break_cache_text = source_text.to_owned();
623        }
624        struct FlatGlyph<'a> {
625            g: &'a ShapedGlyph,
626            font: &'a Arc<[u8]>,
627        }
628        let mut flat: Vec<FlatGlyph<'_>> = Vec::new();
629        for run in runs {
630            for g in &run.glyphs {
631                flat.push(FlatGlyph {
632                    g,
633                    font: &run.font_data,
634                });
635            }
636        }
637        let wrap = constraints.max_width > 0.0;
638        let max_w = constraints.max_width;
639        let mut line_ranges: Vec<(usize, usize)> = Vec::new();
640        let mut overflow = false;
641        let use_kp = strategy == BreakingStrategy::KnuthPlass && wrap && ext_sorted.is_none();
642        let mut kp_succeeded = false;
643        if use_kp {
644            let breaks = &self.break_cache_ops;
645            let flat_advances: Vec<f32> = flat.iter().map(|fg| fg.g.x_advance).collect();
646            let flat_is_ws: Vec<bool> = flat.iter().map(|fg| fg.g.is_whitespace).collect();
647            let mut byte_to_glyph_idx: std::collections::HashMap<usize, usize> =
648                std::collections::HashMap::new();
649            for (i, fg) in flat.iter().enumerate() {
650                byte_to_glyph_idx.entry(fg.g.cluster as usize).or_insert(i);
651            }
652            let break_opps: Vec<(usize, LineBreak)> = breaks
653                .iter()
654                .filter_map(|(off, kind)| byte_to_glyph_idx.get(off).map(|&gi| (gi, kind.clone())))
655                .collect();
656            let kp_breaks =
657                crate::knuth_plass::optimal_breaks(&flat_advances, &flat_is_ws, &break_opps, max_w);
658            if !kp_breaks.is_empty() || flat.is_empty() {
659                build_ranges_from_kp_breaks(&kp_breaks, flat.len(), &mut line_ranges);
660                kp_succeeded = true;
661            }
662        }
663        if !kp_succeeded {
664            // Helper: return the Unicode code-point that ends immediately
665            // before byte offset `off` (i.e. the last char of `source_text[..off]`).
666            let char_preceding = |off: usize| -> Option<char> {
667                if off == 0 {
668                    return None;
669                }
670                for back in 1..=4usize {
671                    if back > off {
672                        break;
673                    }
674                    let start = off - back;
675                    if source_text.is_char_boundary(start) {
676                        return source_text[start..off].chars().next_back();
677                    }
678                }
679                None
680            };
681            let break_at_fn = |off: usize| -> Option<LineBreak> {
682                if let Some(ref sorted) = ext_sorted {
683                    // Mandatory breaks: newline characters always force a new line.
684                    // The external break list does not carry mandatory/allowed
685                    // distinction, so we infer it from the preceding character.
686                    if matches!(
687                        char_preceding(off),
688                        Some('\n')
689                            | Some('\r')
690                            | Some('\u{000C}')
691                            | Some('\u{0085}')
692                            | Some('\u{2028}')
693                            | Some('\u{2029}')
694                    ) {
695                        return Some(LineBreak::Mandatory);
696                    }
697                    if sorted.binary_search(&off).is_ok() {
698                        return Some(LineBreak::Allowed);
699                    }
700                    return None;
701                }
702                self.break_cache_ops
703                    .iter()
704                    .find(|(pos, _)| *pos == off)
705                    .map(|(_, kind)| kind.clone())
706            };
707            let char_at =
708                |byte_off: usize| -> Option<char> { source_text.get(byte_off..)?.chars().next() };
709            let mut line_start = 0usize;
710            let mut cursor = 0.0f32;
711            let mut last_safe_break: Option<usize> = None;
712            let mut width_at_break = 0.0f32;
713            let mut i = 0usize;
714            while i < flat.len() {
715                let adv = flat[i].g.x_advance;
716                let cluster_off = flat[i].g.cluster as usize;
717                if i > line_start {
718                    let current_char = char_at(cluster_off);
719                    let preceding_char = char_preceding(cluster_off);
720                    let zwj_precedes = preceding_char == Some('\u{200D}');
721                    let is_zwnj = current_char == Some('\u{200C}');
722                    let effective_break: Option<LineBreak> = if zwj_precedes {
723                        None
724                    } else if is_zwnj {
725                        Some(LineBreak::Allowed)
726                    } else {
727                        break_at_fn(cluster_off)
728                    };
729                    if let Some(kind) = effective_break {
730                        if kind == LineBreak::Mandatory {
731                            line_ranges.push((line_start, i));
732                            line_start = i;
733                            cursor = 0.0;
734                            last_safe_break = None;
735                            width_at_break = 0.0;
736                            continue;
737                        } else {
738                            last_safe_break = Some(i);
739                            width_at_break = cursor;
740                        }
741                    }
742                }
743                if wrap && cursor + adv > max_w && i > line_start {
744                    if let Some(brk) = last_safe_break {
745                        if brk > line_start {
746                            line_ranges.push((line_start, brk));
747                            line_start = brk;
748                            cursor -= width_at_break;
749                            last_safe_break = None;
750                            width_at_break = 0.0;
751                            continue;
752                        }
753                    }
754                    overflow = true;
755                    line_ranges.push((line_start, i));
756                    line_start = i;
757                    cursor = 0.0;
758                    last_safe_break = None;
759                    width_at_break = 0.0;
760                    continue;
761                }
762                cursor += adv;
763                i += 1;
764            }
765            if line_start < flat.len() {
766                line_ranges.push((line_start, flat.len()));
767            } else if line_ranges.is_empty() {
768                line_ranges.push((0, 0));
769            }
770        }
771        if line_ranges.is_empty() {
772            line_ranges.push((0, 0));
773        }
774        self.scratch.clear();
775        /// Per-line alignment metadata collected during Phase 1.
776        struct LineAlignMeta {
777            glyph_start: usize,
778            glyph_end: usize,
779            x_offset: f32,
780            trimmed_width: f32,
781            baseline_y: f32,
782        }
783        let last_line_idx = line_ranges.len().saturating_sub(1);
784        let mut line_metas: Vec<LineAlignMeta> = Vec::with_capacity(line_ranges.len());
785        let mut total_width = 0.0f32;
786        let mut baseline_y = model.ascent;
787        let is_justify = alignment == TextAlignment::Justify;
788        for (li, &(start, end)) in line_ranges.iter().enumerate() {
789            let mut trimmed_width = 0.0f32;
790            {
791                let mut running = 0.0f32;
792                for fg in &flat[start..end] {
793                    running += fg.g.x_advance;
794                    if !fg.g.is_whitespace {
795                        trimmed_width = running;
796                    }
797                }
798            }
799            let ws_gaps = count_internal_ws_gaps(flat[start..end].iter().map(|fg| fg.g));
800            let (x_offset, justify_extra) = compute_alignment(
801                alignment,
802                trimmed_width,
803                max_w,
804                wrap,
805                li == last_line_idx,
806                ws_gaps,
807            );
808            let glyph_start = self.scratch.len();
809            let pen_start = if is_justify { x_offset } else { 0.0 };
810            let mut pen = pen_start;
811            match &bidi_levels {
812                Some(levels) => {
813                    let line_levels: Vec<unicode_bidi::Level> = flat[start..end]
814                        .iter()
815                        .map(|fg| {
816                            let idx = fg.g.cluster as usize;
817                            levels
818                                .get(idx)
819                                .copied()
820                                .unwrap_or_else(unicode_bidi::Level::ltr)
821                        })
822                        .collect();
823                    let visual_order = crate::reorder::line_visual_order(&line_levels);
824                    for vi in &visual_order {
825                        let fg = &flat[start + vi];
826                        let adv = fg.g.x_advance
827                            + if justify_extra > 0.0 && fg.g.is_whitespace {
828                                justify_extra
829                            } else {
830                                0.0
831                            };
832                        self.scratch.push(PositionedGlyph {
833                            gid: fg.g.gid,
834                            font_data: Arc::clone(fg.font),
835                            pos: (pen + fg.g.x_offset, baseline_y + fg.g.y_offset),
836                            font_size: constraints.font_size,
837                            advance_x: adv,
838                            cluster: fg.g.cluster,
839                        });
840                        pen += adv;
841                    }
842                }
843                None => {
844                    for fg in &flat[start..end] {
845                        let adv = fg.g.x_advance
846                            + if justify_extra > 0.0 && fg.g.is_whitespace {
847                                justify_extra
848                            } else {
849                                0.0
850                            };
851                        self.scratch.push(PositionedGlyph {
852                            gid: fg.g.gid,
853                            font_data: Arc::clone(fg.font),
854                            pos: (pen + fg.g.x_offset, baseline_y + fg.g.y_offset),
855                            font_size: constraints.font_size,
856                            advance_x: adv,
857                            cluster: fg.g.cluster,
858                        });
859                        pen += adv;
860                    }
861                }
862            }
863            total_width = total_width.max(trimmed_width);
864            line_metas.push(LineAlignMeta {
865                glyph_start,
866                glyph_end: self.scratch.len(),
867                x_offset: if is_justify { 0.0 } else { x_offset },
868                trimmed_width,
869                baseline_y,
870            });
871            baseline_y += model.line_height;
872        }
873        if !is_justify {
874            let glyphs_slice = self.scratch.as_mut_slice();
875            let mut per_line: Vec<(f32, &mut [PositionedGlyph])> =
876                Vec::with_capacity(line_metas.len());
877            let mut remaining: &mut [PositionedGlyph] = glyphs_slice;
878            let mut consumed = 0usize;
879            for meta in &line_metas {
880                let line_len = meta.glyph_end - meta.glyph_start;
881                let (line_slice, rest) = remaining.split_at_mut(line_len);
882                per_line.push((meta.x_offset, line_slice));
883                remaining = rest;
884                consumed += line_len;
885            }
886            let _ = consumed;
887            #[cfg(not(target_arch = "wasm32"))]
888            per_line.par_iter_mut().for_each(|(x_off, line_glyphs)| {
889                if *x_off != 0.0 {
890                    for g in line_glyphs.iter_mut() {
891                        g.pos.0 += *x_off;
892                    }
893                }
894            });
895            #[cfg(target_arch = "wasm32")]
896            for (x_off, line_glyphs) in per_line.iter_mut() {
897                if *x_off != 0.0 {
898                    for g in line_glyphs.iter_mut() {
899                        g.pos.0 += *x_off;
900                    }
901                }
902            }
903        }
904        let mut lines: Vec<Line> = Vec::with_capacity(line_metas.len());
905        for meta in &line_metas {
906            lines.push(Line {
907                glyph_start: meta.glyph_start,
908                glyph_end: meta.glyph_end,
909                metrics: LineMetrics {
910                    ascent: model.ascent,
911                    descent: model.descent,
912                    leading: model.leading,
913                    baseline_y: meta.baseline_y,
914                    width: meta.trimmed_width,
915                },
916            });
917        }
918        let total_height = if lines.is_empty() {
919            0.0
920        } else {
921            model.line_height * lines.len() as f32
922        };
923        let mut glyphs: Vec<PositionedGlyph> = Vec::with_capacity(self.scratch.len());
924        glyphs.append(&mut self.scratch);
925        Ok(LayoutResult {
926            glyphs,
927            lines,
928            metrics: ParagraphMetrics {
929                total_height,
930                total_width,
931                line_count: line_ranges.len(),
932                overflow,
933                truncated: false,
934            },
935            decorations: Vec::new(),
936            inline_objects: Vec::new(),
937        })
938    }
939    /// Lays out `runs` in vertical top-to-bottom flow.
940    ///
941    /// Each glyph advances the cursor downward by its vertical advance (falling
942    /// back to `font_size` when no `vmtx` data is available).  When
943    /// `max_column_height > 0.0`, the text wraps into additional columns once
944    /// the current column's height would be exceeded; each column advances the
945    /// `x` origin by `font_size * 1.2`.
946    ///
947    /// A "line" in this context is one vertical *column* of glyphs.  The
948    /// returned [`Line`] structs therefore index into the column-by-column
949    /// glyph list, and [`ParagraphMetrics::line_count`] equals the number of
950    /// columns used.
951    ///
952    /// Note: bidi reordering is **not** applied in vertical mode; vertical CJK
953    /// text is always read top-to-bottom in column order.
954    ///
955    /// # Errors
956    /// Currently infallible for well-formed input; returns `Err` only for
957    /// forward compatibility.
958    pub fn layout_vertical(
959        &mut self,
960        _source_text: &str,
961        runs: &[ShapedRun],
962        max_column_height: f32,
963        font_size: f32,
964        _font_metrics: Option<&FontVerticalMetrics>,
965    ) -> Result<LayoutResult, OxiTextError> {
966        struct FlatGlyph<'a> {
967            g: &'a ShapedGlyph,
968            font: &'a Arc<[u8]>,
969        }
970        let mut flat: Vec<FlatGlyph<'_>> = Vec::new();
971        for run in runs {
972            for g in &run.glyphs {
973                flat.push(FlatGlyph {
974                    g,
975                    font: &run.font_data,
976                });
977            }
978        }
979        let column_width = font_size * 1.2;
980        let mut glyphs: Vec<PositionedGlyph> = Vec::with_capacity(flat.len());
981        let mut lines: Vec<Line> = Vec::new();
982        let mut column_x = 0.0f32;
983        let mut cursor_y = 0.0f32;
984        let mut col_glyph_start = 0usize;
985        let mut max_y_in_column = 0.0f32;
986        let mut max_total_y = 0.0f32;
987        // Cache parsed ttf_parser::Face instances keyed by byte-slice pointer so
988        // each unique font face is parsed exactly once across the entire glyph loop
989        // instead of once per glyph.
990        let mut face_cache = crate::vertical::ParsedFaceCache::new();
991        for fg in &flat {
992            let v_adv = face_cache.vmtx_advance_or_default(fg.font.as_ref(), fg.g.gid, font_size);
993            if max_column_height > 0.0 && cursor_y + v_adv > max_column_height && cursor_y > 0.0 {
994                let metrics = LineMetrics {
995                    ascent: font_size * 0.8,
996                    descent: font_size * 0.2,
997                    leading: 0.0,
998                    baseline_y: column_x,
999                    width: max_y_in_column,
1000                };
1001                lines.push(Line {
1002                    glyph_start: col_glyph_start,
1003                    glyph_end: glyphs.len(),
1004                    metrics,
1005                });
1006                max_total_y = max_total_y.max(cursor_y);
1007                column_x += column_width;
1008                cursor_y = 0.0;
1009                max_y_in_column = 0.0;
1010                col_glyph_start = glyphs.len();
1011            }
1012            glyphs.push(PositionedGlyph {
1013                gid: fg.g.gid,
1014                font_data: Arc::clone(fg.font),
1015                pos: (column_x + fg.g.x_offset, cursor_y + fg.g.y_offset),
1016                font_size,
1017                advance_x: fg.g.x_advance,
1018                cluster: fg.g.cluster,
1019            });
1020            cursor_y += v_adv;
1021            max_y_in_column = max_y_in_column.max(cursor_y);
1022        }
1023        {
1024            let metrics = LineMetrics {
1025                ascent: font_size * 0.8,
1026                descent: font_size * 0.2,
1027                leading: 0.0,
1028                baseline_y: column_x,
1029                width: max_y_in_column,
1030            };
1031            lines.push(Line {
1032                glyph_start: col_glyph_start,
1033                glyph_end: glyphs.len(),
1034                metrics,
1035            });
1036            max_total_y = max_total_y.max(cursor_y);
1037        }
1038        if lines.is_empty() {
1039            lines.push(Line {
1040                glyph_start: 0,
1041                glyph_end: 0,
1042                metrics: LineMetrics {
1043                    ascent: font_size * 0.8,
1044                    descent: font_size * 0.2,
1045                    leading: 0.0,
1046                    baseline_y: 0.0,
1047                    width: 0.0,
1048                },
1049            });
1050        }
1051        let num_columns = lines.len();
1052        let total_width = num_columns as f32 * column_width;
1053        let total_height = max_total_y;
1054        Ok(LayoutResult {
1055            glyphs,
1056            lines,
1057            metrics: ParagraphMetrics {
1058                total_height,
1059                total_width,
1060                line_count: num_columns,
1061                overflow: false,
1062                truncated: false,
1063            },
1064            decorations: Vec::new(),
1065            inline_objects: Vec::new(),
1066        })
1067    }
1068    /// Lays out multiple paragraphs stacked vertically.
1069    ///
1070    /// Each paragraph is laid out independently using [`LayoutEngine::layout`]
1071    /// (greedy algorithm).  The y-positions of each paragraph's glyphs and line
1072    /// baselines are offset by the accumulated height of all previous paragraphs
1073    /// plus `para_spacing` between them.
1074    ///
1075    /// The returned [`LayoutResult`] has all glyphs and lines merged into a
1076    /// single flat list.  [`ParagraphMetrics`] reflects the combined extent.
1077    ///
1078    /// # Errors
1079    /// Propagates any error returned by the inner [`LayoutEngine::layout`]
1080    /// calls.
1081    pub fn layout_paragraphs(
1082        &mut self,
1083        paragraphs: &[&str],
1084        shaped_runs_per_paragraph: &[&[ShapedRun]],
1085        constraints: &LayoutConstraints,
1086        para_spacing: f32,
1087        options: &crate::options::LayoutOptions,
1088        font_metrics: Option<&FontVerticalMetrics>,
1089    ) -> Result<LayoutResult, OxiTextError> {
1090        let alignment = options.alignment;
1091        let mut combined_glyphs: Vec<PositionedGlyph> = Vec::new();
1092        let mut combined_lines: Vec<Line> = Vec::new();
1093        let mut cursor_y = 0.0f32;
1094        let mut total_width = 0.0f32;
1095        let mut overflow = false;
1096        let mut para_count = 0usize;
1097        let n = paragraphs.len().min(shaped_runs_per_paragraph.len());
1098        for idx in 0..n {
1099            let text = paragraphs[idx];
1100            let runs = shaped_runs_per_paragraph[idx];
1101            let result = self.layout(text, runs, constraints, alignment, font_metrics)?;
1102            let glyph_offset = combined_glyphs.len();
1103            for g in &result.glyphs {
1104                combined_glyphs.push(PositionedGlyph {
1105                    gid: g.gid,
1106                    font_data: std::sync::Arc::clone(&g.font_data),
1107                    pos: (g.pos.0, g.pos.1 + cursor_y),
1108                    font_size: g.font_size,
1109                    advance_x: g.advance_x,
1110                    cluster: g.cluster,
1111                });
1112            }
1113            for line in &result.lines {
1114                combined_lines.push(Line {
1115                    glyph_start: line.glyph_start + glyph_offset,
1116                    glyph_end: line.glyph_end + glyph_offset,
1117                    metrics: LineMetrics {
1118                        ascent: line.metrics.ascent,
1119                        descent: line.metrics.descent,
1120                        leading: line.metrics.leading,
1121                        baseline_y: line.metrics.baseline_y + cursor_y,
1122                        width: line.metrics.width,
1123                    },
1124                });
1125            }
1126            total_width = total_width.max(result.metrics.total_width);
1127            overflow |= result.metrics.overflow;
1128            para_count += result.metrics.line_count;
1129            cursor_y += result.metrics.total_height;
1130            if idx + 1 < n {
1131                cursor_y += para_spacing;
1132            }
1133        }
1134        let total_height = cursor_y;
1135        Ok(LayoutResult {
1136            glyphs: combined_glyphs,
1137            lines: combined_lines,
1138            metrics: ParagraphMetrics {
1139                total_height,
1140                total_width,
1141                line_count: para_count,
1142                overflow,
1143                truncated: false,
1144            },
1145            decorations: Vec::new(),
1146            inline_objects: Vec::new(),
1147        })
1148    }
1149    /// Lays out a single text block using comprehensive [`crate::options::LayoutOptions`].
1150    ///
1151    /// This is a unified entry point that dispatches to the appropriate layout
1152    /// path based on [`crate::options::LayoutOptions::flow_direction`] and applies optional
1153    /// post-processing (truncation).
1154    ///
1155    /// Tab stop handling for `\t` characters: when a glyph's cluster character
1156    /// is `\t`, the cursor advances to the next tab stop instead of using the
1157    /// glyph's natural advance.  The positioned glyph's x is placed at the
1158    /// pre-tab cursor position (the whitespace gap itself is empty).
1159    ///
1160    /// # Errors
1161    /// Propagates any error returned by the inner layout calls.
1162    pub fn layout_with_options(
1163        &mut self,
1164        source_text: &str,
1165        shaped_runs: &[ShapedRun],
1166        max_width: f32,
1167        options: &crate::options::LayoutOptions,
1168        font_metrics: Option<&FontVerticalMetrics>,
1169        font_size: f32,
1170    ) -> Result<LayoutResult, OxiTextError> {
1171        use oxitext_core::FlowDirection;
1172        let constraints = LayoutConstraints {
1173            max_width,
1174            font_size,
1175        };
1176        let mut result = match options.flow_direction {
1177            FlowDirection::Vertical => {
1178                self.layout_vertical(source_text, shaped_runs, max_width, font_size, font_metrics)?
1179            }
1180            FlowDirection::Horizontal => self.layout(
1181                source_text,
1182                shaped_runs,
1183                &constraints,
1184                options.alignment,
1185                font_metrics,
1186            )?,
1187        };
1188        let tab_stops = &options.tab_stops;
1189        if !source_text.is_empty() {
1190            for line in &result.lines {
1191                let gs = line.glyph_start;
1192                let ge = line.glyph_end;
1193                if gs >= ge {
1194                    continue;
1195                }
1196                let mut pen = result.glyphs[gs].pos.0;
1197                for gi in gs..ge {
1198                    let cluster = result.glyphs[gi].pos;
1199                    let char_at_cluster: Option<char> = {
1200                        let cluster_off = find_cluster_for_positioned_glyph(
1201                            gi - gs,
1202                            shaped_runs,
1203                            line.glyph_start,
1204                        );
1205                        cluster_off
1206                            .and_then(|off| source_text.get(off..))
1207                            .and_then(|s| s.chars().next())
1208                    };
1209                    if char_at_cluster == Some('\t') {
1210                        let snap = tab_stops.next_stop(pen);
1211                        result.glyphs[gi].pos = (pen, cluster.1);
1212                        pen = snap;
1213                    } else {
1214                        let next_x = if gi + 1 < ge {
1215                            result.glyphs[gi + 1].pos.0
1216                        } else {
1217                            let adv = advance_for_glyph(gi - gs, shaped_runs, line.glyph_start);
1218                            cluster.0 + adv
1219                        };
1220                        pen = next_x;
1221                    }
1222                }
1223            }
1224        }
1225        if let Some(trunc) = &options.truncation {
1226            result = apply_truncation(result, trunc);
1227        }
1228        if options.hanging_punctuation {
1229            apply_hanging_punctuation(&mut result, source_text);
1230        }
1231        if let Some(decoration) = options.decoration {
1232            result.decorations = super::functions::compute_decoration_rects(
1233                &result.lines,
1234                &result.glyphs,
1235                decoration,
1236            );
1237        }
1238        // Append inline objects at the end of the last line.
1239        if !options.inline_objects.is_empty() {
1240            let last_line_y = result
1241                .lines
1242                .last()
1243                .map(|l| l.metrics.baseline_y)
1244                .unwrap_or(0.0);
1245            let mut cursor_x = result
1246                .glyphs
1247                .last()
1248                .map(|g| g.pos.0 + g.advance_x)
1249                .unwrap_or(0.0);
1250            let last_line_idx = result.lines.len().saturating_sub(1);
1251            for obj in &options.inline_objects {
1252                result
1253                    .inline_objects
1254                    .push(oxitext_core::PositionedInlineObject {
1255                        object: obj.clone(),
1256                        x: cursor_x,
1257                        y: last_line_y,
1258                        line: last_line_idx,
1259                    });
1260                cursor_x += obj.advance;
1261            }
1262        }
1263        Ok(result)
1264    }
1265}
1266/// Aggregate metrics for a whole laid-out block of text.
1267#[derive(Debug, Clone, Copy, PartialEq)]
1268pub struct ParagraphMetrics {
1269    /// Total height of all lines stacked vertically, in pixels.
1270    pub total_height: f32,
1271    /// Width of the widest line, in pixels.
1272    pub total_width: f32,
1273    /// Number of lines produced.
1274    pub line_count: usize,
1275    /// `true` if any line's natural width exceeded `max_width` and could not
1276    /// be broken (e.g. a single unbreakable token wider than the column).
1277    pub overflow: bool,
1278    /// `true` if the last line was truncated with an ellipsis because it
1279    /// exceeded `TruncationMode::max_width`.
1280    pub truncated: bool,
1281}
1282/// Vertical metrics for a single laid-out line, in pixels.
1283#[derive(Debug, Clone, Copy, PartialEq)]
1284pub struct LineMetrics {
1285    /// Distance from the line's top to its baseline (positive).
1286    pub ascent: f32,
1287    /// Distance from the baseline to the line's bottom (positive).
1288    pub descent: f32,
1289    /// Extra leading distributed below the line.
1290    pub leading: f32,
1291    /// Absolute Y coordinate of the baseline from the layout origin.
1292    pub baseline_y: f32,
1293    /// Total advance width of the line's glyphs (before alignment), in pixels.
1294    pub width: f32,
1295}
1296impl LineMetrics {
1297    /// Total height consumed by the line (ascent + descent + leading).
1298    pub fn height(&self) -> f32 {
1299        self.ascent + self.descent + self.leading
1300    }
1301}
1302/// A single laid-out line: a contiguous slice of positioned glyphs plus its
1303/// metrics.
1304#[derive(Debug, Clone)]
1305pub struct Line {
1306    /// Index of the first glyph of this line in [`LayoutResult::glyphs`].
1307    pub glyph_start: usize,
1308    /// Index past the last glyph of this line in [`LayoutResult::glyphs`].
1309    pub glyph_end: usize,
1310    /// Vertical and width metrics for the line.
1311    pub metrics: LineMetrics,
1312}
1313impl Line {
1314    /// Number of glyphs in the line.
1315    pub fn len(&self) -> usize {
1316        self.glyph_end - self.glyph_start
1317    }
1318    /// Returns `true` if the line has no glyphs.
1319    pub fn is_empty(&self) -> bool {
1320        self.glyph_start == self.glyph_end
1321    }
1322}