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}