Skip to main content

oxideav_scribe/
style.rs

1//! Font request style — italic knob the shaper honours when emitting
2//! glyph paths.
3//!
4//! - **Italic** is synthesised from `post.italicAngle` (a horizontal
5//!   shear applied at outline-flatten time when the requested style is
6//!   italic but the font itself is upright). See
7//!   [`synthetic_italic_shear`].
8//!
9//! In every case, callers that have a real Italic / Bold variant of
10//! the font available should prefer loading those as separate
11//! [`crate::Face`]s — synthetic styles are the fallback for fonts
12//! that ship only one cut.
13//!
14//! ## Why not just a `bool`?
15//!
16//! Because we want to remember the user's requested weight and italic
17//! flag through the shaping pipeline so future synthesis rounds can
18//! hook in without a public-API break. `Style` is `Copy` so it travels
19//! through arguments cheaply.
20//!
21//! Default is `Style { italic: false, weight: 400 }` — upright Regular.
22
23/// Font selection / synthesis request. Carried through the shaper.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Style {
26    /// Caller wants italic. If the underlying face is already italic
27    /// (`Face::italic_angle()` is non-zero), no synthesis is applied —
28    /// the font already provides the slant. If the face is upright,
29    /// the consumer applies a horizontal shear of
30    /// `tan(-DEFAULT_SYNTHETIC_ITALIC_DEG)` (`~12°` forward slant) at
31    /// outline-flatten time.
32    pub italic: bool,
33    /// OpenType `usWeightClass` value (100..=1000). Consumed by
34    /// downstream rasterizers wanting to synthesise bold; the shaper
35    /// itself doesn't apply it (true bold should always come from a
36    /// real Bold face when one is available).
37    pub weight: u16,
38}
39
40impl Style {
41    /// Upright, regular weight (400).
42    pub const REGULAR: Style = Style {
43        italic: false,
44        weight: 400,
45    };
46
47    /// Upright, regular weight — same as `Style::REGULAR`. Convenience
48    /// for symmetry with `italic()`.
49    pub fn regular() -> Self {
50        Self::REGULAR
51    }
52
53    /// Italic, regular weight (400).
54    pub fn italic() -> Self {
55        Self {
56            italic: true,
57            weight: 400,
58        }
59    }
60
61    /// Builder: set the italic flag.
62    #[must_use]
63    pub fn with_italic(mut self, italic: bool) -> Self {
64        self.italic = italic;
65        self
66    }
67
68    /// Builder: set the weight (clamped to 1..=1000 to keep the cache
69    /// key well-defined; OpenType allows 100..=1000 in 100 increments
70    /// but variable fonts can land any integer in between).
71    #[must_use]
72    pub fn with_weight(mut self, weight: u16) -> Self {
73        self.weight = weight.clamp(1, 1000);
74        self
75    }
76}
77
78impl Default for Style {
79    fn default() -> Self {
80        Self::REGULAR
81    }
82}
83
84/// Synthetic-italic shear angle in degrees. `tan(12°) ≈ 0.213`, which
85/// matches the slant the major desktop renderers (Quartz, GDI+,
86/// FreeType) apply when the font lacks an italic variant. Mirrors
87/// historical Type-1 Oblique fonts which ship with `italicAngle = -12`.
88pub const DEFAULT_SYNTHETIC_ITALIC_DEG: f32 = 12.0;
89
90/// Threshold (in degrees) under which the font's own `italicAngle` is
91/// considered "upright". Some upright faces ship a tiny non-zero value
92/// for visual centring; if we sheared on top of that we'd look weird.
93pub const ITALIC_ANGLE_EPSILON_DEG: f32 = 0.5;
94
95/// Compute the horizontal shear (`x' = x + shear * y` in TT Y-up
96/// coordinates) that synthesises italic for the requested style on a
97/// face whose own `italic_angle` is `face_italic_deg` (TT/post
98/// convention: negative for forward slant, 0 for upright).
99///
100/// Returns `0.0` when no synthesis is needed: either the request is
101/// upright, or the face is already italic.
102pub fn synthetic_italic_shear(style: Style, face_italic_deg: f32) -> f32 {
103    if !style.italic {
104        return 0.0;
105    }
106    if face_italic_deg.abs() > ITALIC_ANGLE_EPSILON_DEG {
107        // Font is already slanted; honour it instead of double-shearing.
108        return 0.0;
109    }
110    DEFAULT_SYNTHETIC_ITALIC_DEG.to_radians().tan()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn default_is_regular_upright() {
119        assert_eq!(Style::default(), Style::REGULAR);
120        assert!(!Style::default().italic);
121        assert_eq!(Style::default().weight, 400);
122    }
123
124    #[test]
125    fn italic_builder_sets_flag() {
126        let s = Style::italic();
127        assert!(s.italic);
128        assert_eq!(s.weight, 400);
129    }
130
131    #[test]
132    fn weight_is_clamped() {
133        assert_eq!(Style::REGULAR.with_weight(0).weight, 1);
134        assert_eq!(Style::REGULAR.with_weight(2_000).weight, 1000);
135        assert_eq!(Style::REGULAR.with_weight(700).weight, 700);
136    }
137
138    #[test]
139    fn upright_request_yields_zero_shear() {
140        // Upright request on upright face: 0.
141        assert_eq!(synthetic_italic_shear(Style::REGULAR, 0.0), 0.0);
142        // Upright request on italic face: still 0 (we never un-italicise).
143        assert_eq!(synthetic_italic_shear(Style::REGULAR, -12.0), 0.0);
144    }
145
146    #[test]
147    fn italic_request_on_upright_face_yields_default_shear() {
148        let shear = synthetic_italic_shear(Style::italic(), 0.0);
149        let expected = DEFAULT_SYNTHETIC_ITALIC_DEG.to_radians().tan();
150        assert!(
151            (shear - expected).abs() < 1e-6,
152            "shear = {shear}, expected = {expected}"
153        );
154        // Also ~0.213 (tan 12 deg).
155        assert!(shear > 0.20 && shear < 0.22, "shear = {shear}");
156    }
157
158    #[test]
159    fn italic_request_on_italic_face_yields_zero() {
160        // Face already at -12 deg → no synthesis.
161        assert_eq!(synthetic_italic_shear(Style::italic(), -12.0), 0.0);
162        // Same for positive backslant.
163        assert_eq!(synthetic_italic_shear(Style::italic(), 8.0), 0.0);
164    }
165
166    #[test]
167    fn epsilon_band_still_synthesises() {
168        // Tiny font angle (0.3 deg) is treated as upright.
169        let shear = synthetic_italic_shear(Style::italic(), 0.3);
170        assert!(shear > 0.20);
171    }
172}