zenith_layout/engine.rs
1//! Zenith-owned text-layout types and the `TextLayoutEngine` trait.
2//!
3//! No third-party shaping or font types appear here. All shaping engines
4//! implement `TextLayoutEngine` and hide their dependencies behind it.
5
6use zenith_core::{FontProvider, FontStyle};
7
8use crate::error::LayoutError;
9
10/// Base writing direction for a shaping request.
11///
12/// Controls the rustybuzz buffer direction so glyph advances and complex-script
13/// joining (e.g. Arabic) are correct. The DEFAULT is [`TextDirection::Ltr`], so
14/// a request that omits the field shapes exactly as before (byte-identical).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum TextDirection {
17 /// Left-to-right (the default).
18 #[default]
19 Ltr,
20 /// Right-to-left (Arabic, Hebrew, …). The shaper reorders glyphs to visual
21 /// order and applies RTL-correct joining.
22 Rtl,
23}
24
25/// A request to shape a run of text into positioned glyphs.
26#[derive(Debug, Clone, PartialEq)]
27pub struct ShapeRequest<'a> {
28 /// The text to shape.
29 pub text: &'a str,
30 /// Priority-ordered font family preferences.
31 pub families: &'a [String],
32 /// Font weight (e.g. 400 = regular, 700 = bold).
33 pub weight: u16,
34 /// Font style variant.
35 pub style: FontStyle,
36 /// Requested font size in pixels.
37 pub font_size: f32,
38 /// Base writing direction. Defaults to [`TextDirection::Ltr`].
39 pub direction: TextDirection,
40}
41
42/// One positioned glyph, baseline-relative, measured from the run origin in pixels.
43///
44/// Positive x is rightward; positive y is downward (0 = on the baseline).
45#[derive(Debug, Clone, PartialEq)]
46pub struct PositionedGlyph {
47 /// Glyph identifier within the resolved font face.
48 pub glyph_id: u16,
49 /// Horizontal offset from the run origin, in pixels.
50 pub x: f32,
51 /// Vertical offset from the baseline, in pixels (positive = below baseline).
52 pub y: f32,
53 /// Source Unicode text this glyph maps back to, for text extraction
54 /// (PDF ToUnicode). The first glyph of a shaping cluster carries the whole
55 /// cluster's source substring (so a ligature maps to all its chars); later
56 /// glyphs of the same cluster carry an empty string. Empty means "no source
57 /// text" — extraction emits nothing for this glyph.
58 pub text: String,
59}
60
61/// A shaped run of text in a single resolved font.
62///
63/// All values are in pixels. No third-party types appear in any field.
64#[derive(Debug, Clone, PartialEq)]
65pub struct ZenithGlyphRun {
66 /// Stable id of the resolved font face (matches `FontData::id`).
67 ///
68 /// The renderer re-resolves font bytes via `FontProvider::by_id`.
69 pub font_id: String,
70 /// Font size at which the run was shaped, in pixels.
71 pub font_size: f32,
72 /// Ascent in pixels, positive above the baseline.
73 ///
74 /// Baseline placement: `box_top + ascent`.
75 pub ascent: f32,
76 /// Descent magnitude in pixels (positive value; baseline to bottom of descenders).
77 pub descent: f32,
78 /// Recommended line height in pixels: `ascent + descent + line_gap`.
79 pub line_height: f32,
80 /// Total pen advance across the run in pixels.
81 pub advance_width: f32,
82 /// Positioned glyphs, baseline-relative, in run order.
83 pub glyphs: Vec<PositionedGlyph>,
84}
85
86/// Result of fallback shaping: the shaped runs plus any characters that NO
87/// registered face (primary or fallback) could supply a glyph for.
88pub struct FallbackResult {
89 /// Shaped glyph runs, one per contiguous sub-run that resolved to a single
90 /// face, in visual order.
91 pub runs: Vec<ZenithGlyphRun>,
92 /// Characters (deduped, sorted by codepoint) for which no registered face
93 /// had a glyph. Excludes default-ignorable code points (joiners, variation
94 /// selectors, control characters, whitespace, etc.).
95 pub missing_chars: Vec<char>,
96}
97
98/// Trait implemented by every shaping engine.
99///
100/// Engines are free to resolve fonts, call native shapers, and accumulate any
101/// internal state, but they must not expose third-party types through this trait.
102pub trait TextLayoutEngine {
103 /// Shape `req.text` into a `ZenithGlyphRun` using fonts from `provider`.
104 ///
105 /// # Errors
106 ///
107 /// Returns `LayoutError` if no font can be resolved, if the font bytes are
108 /// malformed, if `units_per_em` is zero, or if any other shaping step fails.
109 fn shape(
110 &self,
111 req: &ShapeRequest<'_>,
112 provider: &dyn FontProvider,
113 ) -> Result<ZenithGlyphRun, LayoutError>;
114
115 /// Shape `req.text` with per-glyph font fallback, returning a
116 /// [`FallbackResult`] with one [`ZenithGlyphRun`] per contiguous sub-run
117 /// that resolved to a single face, plus any characters that no registered
118 /// face could cover.
119 ///
120 /// The primary face (resolved from `req.families`/`weight`/`style`) shapes
121 /// every character it covers; characters the primary lacks are itemized to
122 /// the first face in `provider.all_faces()` (deterministic order) that
123 /// covers them, falling back to the primary (rendering `.notdef`) when no
124 /// registered face covers a character. Whitespace and punctuation the
125 /// primary covers stay with the primary, so mixed-script runs do not
126 /// fragment on shared characters.
127 ///
128 /// When every character is covered by the primary face this returns exactly
129 /// one run, identical to [`Self::shape`], with an empty `missing_chars`.
130 ///
131 /// # Errors
132 ///
133 /// Returns `LayoutError` under the same conditions as [`Self::shape`]
134 /// (no resolvable primary font, malformed bytes, zero `units_per_em`).
135 fn shape_with_fallback(
136 &self,
137 req: &ShapeRequest<'_>,
138 provider: &dyn FontProvider,
139 ) -> Result<FallbackResult, LayoutError>;
140}