Skip to main content

ferroui_base/media/
glyph_drawing_options.rs

1/// Options that influence how a single glyph is drawn, independent of variable-font
2/// axis configuration: which CPAL palette to use for color glyphs, and which bitmap
3/// strike size to prefer for bitmap-based glyphs.
4///
5/// All properties are optional. `None` means "use the font's default" (palette 0
6/// for color glyphs; no bitmap strike preference). Concerns specific to variable fonts
7/// (axis coordinates, named instances) live on `FontVariationSettings`.
8///
9/// A record with init-only properties: a value is built from [`GlyphDrawingOptions::new`]
10/// with [`GlyphDrawingOptions::with_palette_index`] and
11/// [`GlyphDrawingOptions::with_pixel_size`], which validate as the property initialisers do.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub struct GlyphDrawingOptions {
14    palette_index: Option<i32>,
15    pixel_size: Option<i32>,
16}
17
18impl GlyphDrawingOptions {
19    /// The instance representing "use the font's defaults".
20    pub const DEFAULT: GlyphDrawingOptions = GlyphDrawingOptions { palette_index: None, pixel_size: None };
21
22    /// Creates options that use the font's defaults.
23    pub const fn new() -> GlyphDrawingOptions {
24        Self::DEFAULT
25    }
26
27    /// Gets the optional `CPAL` palette index used to resolve colors for
28    /// COLR v0 / COLR v1 glyphs.
29    ///
30    /// When `None`, the font's default palette (palette 0) is used.
31    pub fn palette_index(&self) -> Option<i32> {
32        self.palette_index
33    }
34
35    /// Returns a copy of the options with the given palette index.
36    ///
37    /// # Panics
38    ///
39    /// When the value is negative (`ArgumentOutOfRangeException`).
40    pub fn with_palette_index(mut self, value: Option<i32>) -> GlyphDrawingOptions {
41        if let Some(v) = value {
42            if v < 0 {
43                panic!("PaletteIndex must be non-negative. (Parameter 'value')\nActual value was {v}.");
44            }
45        }
46
47        self.palette_index = value;
48        self
49    }
50
51    /// Gets the optional pixel size used to select a bitmap strike from `sbix`,
52    /// `CBDT` or `EBDT` tables.
53    ///
54    /// When `None`, no bitmap strike is selected and the font's outline (or
55    /// color-layer) representation is used instead.
56    pub fn pixel_size(&self) -> Option<i32> {
57        self.pixel_size
58    }
59
60    /// Returns a copy of the options with the given pixel size.
61    ///
62    /// # Panics
63    ///
64    /// When the value is less than 1 (`ArgumentOutOfRangeException`).
65    pub fn with_pixel_size(mut self, value: Option<i32>) -> GlyphDrawingOptions {
66        if let Some(v) = value {
67            if v < 1 {
68                panic!("PixelSize must be at least 1. (Parameter 'value')\nActual value was {v}.");
69            }
70        }
71
72        self.pixel_size = value;
73        self
74    }
75}
76
77// Port of `GlyphDrawingOptionsTests`. The record is a class upstream and a `Copy` value here,
78// so the assertions on the identity of instances (`Assert.Same`, `Assert.NotSame`) have no
79// counterpart: the tests that have them keep their other assertions.
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::collections::hash_map::DefaultHasher;
84    use std::hash::{Hash, Hasher};
85    use std::panic::catch_unwind;
86
87    fn hash_code(options: &GlyphDrawingOptions) -> u64 {
88        let mut hasher = DefaultHasher::new();
89        options.hash(&mut hasher);
90        hasher.finish()
91    }
92
93    #[test]
94    fn default_has_null_palette_index_and_null_pixel_size() {
95        let options = GlyphDrawingOptions::DEFAULT;
96
97        assert_eq!(None, options.palette_index());
98        assert_eq!(None, options.pixel_size());
99    }
100
101    // A value has no identity: the default is the same value every time.
102    #[test]
103    fn default_is_a_singleton() {
104        assert_eq!(GlyphDrawingOptions::DEFAULT, GlyphDrawingOptions::DEFAULT);
105    }
106
107    #[test]
108    fn parameterless_constructor_produces_an_instance_equal_to_default() {
109        // The record's equality contract should treat a freshly-constructed
110        // instance with no overrides as equal to the Default singleton. This
111        // keeps callers from having to compare against Default by reference.
112        let fresh = GlyphDrawingOptions::new();
113
114        assert_eq!(GlyphDrawingOptions::DEFAULT, fresh);
115        assert_eq!(hash_code(&GlyphDrawingOptions::DEFAULT), hash_code(&fresh));
116    }
117
118    #[test]
119    fn palette_index_accepts_null() {
120        let options = GlyphDrawingOptions::new().with_palette_index(None);
121
122        assert_eq!(None, options.palette_index());
123    }
124
125    #[test]
126    fn palette_index_accepts_non_negative_values() {
127        for value in [0, 1, 99] {
128            let options = GlyphDrawingOptions::new().with_palette_index(Some(value));
129
130            assert_eq!(Some(value), options.palette_index());
131        }
132    }
133
134    #[test]
135    fn palette_index_rejects_negative_values() {
136        for value in [-1, i32::MIN] {
137            assert!(catch_unwind(|| GlyphDrawingOptions::new().with_palette_index(Some(value))).is_err());
138        }
139    }
140
141    #[test]
142    fn pixel_size_accepts_null() {
143        let options = GlyphDrawingOptions::new().with_pixel_size(None);
144
145        assert_eq!(None, options.pixel_size());
146    }
147
148    #[test]
149    fn pixel_size_accepts_positive_values() {
150        for value in [1, 16, 256] {
151            let options = GlyphDrawingOptions::new().with_pixel_size(Some(value));
152
153            assert_eq!(Some(value), options.pixel_size());
154        }
155    }
156
157    #[test]
158    fn pixel_size_rejects_values_below_one() {
159        // A pixel size of zero is meaningless for a bitmap strike; reject it at
160        // construction time rather than letting it propagate to renderer code.
161        for value in [0, -1, i32::MIN] {
162            assert!(catch_unwind(|| GlyphDrawingOptions::new().with_pixel_size(Some(value))).is_err());
163        }
164    }
165
166    #[test]
167    fn equality_is_structural_across_two_records_with_same_values() {
168        let a = GlyphDrawingOptions::new().with_palette_index(Some(1)).with_pixel_size(Some(16));
169        let b = GlyphDrawingOptions::new().with_palette_index(Some(1)).with_pixel_size(Some(16));
170
171        assert_eq!(a, b);
172        assert_eq!(hash_code(&a), hash_code(&b));
173    }
174
175    #[test]
176    fn equality_distinguishes_different_palette_index() {
177        let a = GlyphDrawingOptions::new().with_palette_index(Some(0));
178        let b = GlyphDrawingOptions::new().with_palette_index(Some(1));
179
180        assert_ne!(a, b);
181    }
182
183    #[test]
184    fn equality_distinguishes_different_pixel_size() {
185        let a = GlyphDrawingOptions::new().with_pixel_size(Some(16));
186        let b = GlyphDrawingOptions::new().with_pixel_size(Some(32));
187
188        assert_ne!(a, b);
189    }
190
191    #[test]
192    fn with_expression_produces_a_modified_copy() {
193        // `record` participation is part of the public contract; ensure callers
194        // can derive a tweaked instance.
195        let original = GlyphDrawingOptions::new().with_palette_index(Some(1)).with_pixel_size(Some(16));
196        let tweaked = original.with_palette_index(Some(2));
197
198        assert_eq!(Some(1), original.palette_index());
199        assert_eq!(Some(2), tweaked.palette_index());
200        assert_eq!(Some(16), tweaked.pixel_size());
201        assert_ne!(original, tweaked);
202    }
203
204    #[test]
205    fn with_expression_validates_new_values() {
206        let original = GlyphDrawingOptions::new().with_palette_index(Some(1));
207
208        assert!(catch_unwind(|| original.with_palette_index(Some(-5))).is_err());
209    }
210}