Skip to main content

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}