Skip to main content

azul_core/
ui_solver.rs

1//! Small geometry types used by the layout solver and text shaping pipeline.
2//!
3//! Default font / text constants live in [`azul_css::defaults`].
4
5use crate::geom::{LogicalPosition, LogicalSize};
6
7/// Resolved top/right/bottom/left offsets in logical pixels (used for
8/// margins, padding, and borders after CSS resolution).
9#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
10#[repr(C)]
11pub struct ResolvedOffsets {
12    pub top: f32,
13    pub left: f32,
14    pub right: f32,
15    pub bottom: f32,
16}
17
18impl ResolvedOffsets {
19    #[must_use] pub const fn zero() -> Self {
20        Self {
21            top: 0.0,
22            left: 0.0,
23            right: 0.0,
24            bottom: 0.0,
25        }
26    }
27    #[must_use]
28    pub fn total_vertical(&self) -> f32 {
29        self.top + self.bottom
30    }
31    #[must_use]
32    pub fn total_horizontal(&self) -> f32 {
33        self.left + self.right
34    }
35}
36
37/// Index into a font's glyph table.
38type GlyphIndex = u32;
39
40/// A single positioned glyph with its index, screen position, and size.
41#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd)]
42pub struct GlyphInstance {
43    pub index: GlyphIndex,
44    pub point: LogicalPosition,
45    pub size: LogicalSize,
46}
47
48impl GlyphInstance {
49    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
50        self.point.scale_for_dpi(scale_factor);
51        self.size.scale_for_dpi(scale_factor);
52    }
53}
54
55#[cfg(test)]
56mod autotest_generated {
57    use super::*;
58
59    fn offsets(top: f32, left: f32, right: f32, bottom: f32) -> ResolvedOffsets {
60        ResolvedOffsets {
61            top,
62            left,
63            right,
64            bottom,
65        }
66    }
67
68    fn glyph(index: u32, x: f32, y: f32, w: f32, h: f32) -> GlyphInstance {
69        GlyphInstance {
70            index,
71            point: LogicalPosition::new(x, y),
72            size: LogicalSize::new(w, h),
73        }
74    }
75
76    // --- ResolvedOffsets::zero (constructor) ---
77
78    #[test]
79    fn zero_is_all_zeroes_and_usable_in_const_context() {
80        const Z: ResolvedOffsets = ResolvedOffsets::zero();
81        assert_eq!(Z.top, 0.0);
82        assert_eq!(Z.left, 0.0);
83        assert_eq!(Z.right, 0.0);
84        assert_eq!(Z.bottom, 0.0);
85        // Positive zero, not -0.0: sign leaks through `f32::signum` in layout math.
86        assert!(Z.top.is_sign_positive());
87        assert!(Z.left.is_sign_positive());
88        assert!(Z.right.is_sign_positive());
89        assert!(Z.bottom.is_sign_positive());
90    }
91
92    #[test]
93    fn zero_matches_default_and_is_neutral_for_totals() {
94        assert_eq!(ResolvedOffsets::zero(), ResolvedOffsets::default());
95        assert_eq!(ResolvedOffsets::zero().total_vertical(), 0.0);
96        assert_eq!(ResolvedOffsets::zero().total_horizontal(), 0.0);
97    }
98
99    // --- ResolvedOffsets::total_vertical / total_horizontal (getters) ---
100
101    #[test]
102    fn totals_sum_only_their_own_axis() {
103        let o = offsets(1.0, 20.0, 300.0, 4000.0);
104        assert_eq!(o.total_vertical(), 4001.0); // top + bottom
105        assert_eq!(o.total_horizontal(), 320.0); // left + right
106        // Perturbing one axis must not move the other.
107        let o2 = offsets(1.0, -20.0, -300.0, 4000.0);
108        assert_eq!(o2.total_vertical(), o.total_vertical());
109        assert_eq!(o2.total_horizontal(), -320.0);
110    }
111
112    #[test]
113    fn totals_handle_negative_and_cancelling_offsets() {
114        let o = offsets(-5.0, -2.5, 2.5, 5.0);
115        assert_eq!(o.total_vertical(), 0.0);
116        assert_eq!(o.total_horizontal(), 0.0);
117    }
118
119    #[test]
120    fn totals_saturate_to_infinity_instead_of_wrapping() {
121        let o = offsets(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
122        assert!(o.total_vertical().is_infinite() && o.total_vertical().is_sign_positive());
123        assert!(o.total_horizontal().is_infinite() && o.total_horizontal().is_sign_positive());
124
125        let o = offsets(f32::MIN, f32::MIN, f32::MIN, f32::MIN);
126        assert!(o.total_vertical().is_infinite() && o.total_vertical().is_sign_negative());
127        assert!(o.total_horizontal().is_infinite() && o.total_horizontal().is_sign_negative());
128    }
129
130    #[test]
131    fn totals_of_opposing_infinities_are_nan_not_a_panic() {
132        let o = offsets(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
133        assert!(o.total_vertical().is_nan());
134        assert!(o.total_horizontal().is_nan());
135    }
136
137    #[test]
138    fn totals_propagate_nan() {
139        let o = offsets(f32::NAN, 1.0, 2.0, 10.0);
140        assert!(o.total_vertical().is_nan());
141        assert_eq!(o.total_horizontal(), 3.0); // unaffected axis stays finite
142
143        let o = offsets(1.0, f32::NAN, 2.0, 10.0);
144        assert_eq!(o.total_vertical(), 11.0);
145        assert!(o.total_horizontal().is_nan());
146    }
147
148    #[test]
149    fn totals_on_subnormals_do_not_flush_to_a_wrong_value() {
150        let o = offsets(f32::MIN_POSITIVE, f32::MIN_POSITIVE, 0.0, 0.0);
151        assert_eq!(o.total_vertical(), f32::MIN_POSITIVE);
152        assert_eq!(o.total_horizontal(), f32::MIN_POSITIVE);
153    }
154
155    #[test]
156    fn totals_are_pure_getters() {
157        let o = offsets(3.0, 7.0, 11.0, 13.0);
158        let before = o;
159        let _ = o.total_vertical();
160        let _ = o.total_horizontal();
161        assert_eq!(o, before);
162        // Repeated calls are deterministic.
163        let (v1, v2) = (o.total_vertical(), o.total_vertical());
164        let (h1, h2) = (o.total_horizontal(), o.total_horizontal());
165        assert_eq!(v1, v2);
166        assert_eq!(h1, h2);
167    }
168
169    // --- GlyphInstance::scale_for_dpi (numeric) ---
170
171    #[test]
172    fn scale_for_dpi_by_one_is_identity_and_never_touches_the_glyph_index() {
173        let mut g = glyph(u32::MAX, 1.5, -2.5, 3.5, 4.5);
174        g.scale_for_dpi(1.0);
175        assert_eq!(g.index, u32::MAX);
176        assert_eq!(g.point.x, 1.5);
177        assert_eq!(g.point.y, -2.5);
178        assert_eq!(g.size.width, 3.5);
179        assert_eq!(g.size.height, 4.5);
180    }
181
182    #[test]
183    fn scale_for_dpi_by_zero_collapses_to_zero_and_preserves_sign() {
184        let mut g = glyph(7, 10.0, -10.0, 20.0, -20.0);
185        g.scale_for_dpi(0.0);
186        assert_eq!(g.index, 7);
187        assert_eq!(g.point.x, 0.0);
188        assert_eq!(g.point.y, 0.0);
189        assert!(g.point.x.is_sign_positive());
190        assert!(g.point.y.is_sign_negative()); // -10.0 * 0.0 == -0.0
191        assert_eq!(g.size.width, 0.0);
192        assert_eq!(g.size.height, 0.0);
193    }
194
195    #[test]
196    fn scale_for_dpi_negative_factor_mirrors_deterministically() {
197        let mut g = glyph(1, 2.0, -4.0, 8.0, -16.0);
198        g.scale_for_dpi(-2.0);
199        assert_eq!(g.point.x, -4.0);
200        assert_eq!(g.point.y, 8.0);
201        assert_eq!(g.size.width, -16.0);
202        assert_eq!(g.size.height, 32.0);
203    }
204
205    #[test]
206    fn scale_for_dpi_round_trips_for_exact_binary_factors() {
207        let original = glyph(42, 12.0, -6.5, 100.0, 0.25);
208        let mut g = original;
209        g.scale_for_dpi(4.0);
210        g.scale_for_dpi(0.25);
211        assert_eq!(g.index, original.index);
212        assert_eq!(g.point.x, original.point.x);
213        assert_eq!(g.point.y, original.point.y);
214        assert_eq!(g.size.width, original.size.width);
215        assert_eq!(g.size.height, original.size.height);
216    }
217
218    #[test]
219    fn scale_for_dpi_overflows_to_infinity_rather_than_wrapping() {
220        let mut g = glyph(0, f32::MAX, -f32::MAX, f32::MAX, f32::MAX);
221        g.scale_for_dpi(2.0);
222        assert!(g.point.x.is_infinite() && g.point.x.is_sign_positive());
223        assert!(g.point.y.is_infinite() && g.point.y.is_sign_negative());
224        assert!(g.size.width.is_infinite());
225        assert!(g.size.height.is_infinite());
226    }
227
228    #[test]
229    fn scale_for_dpi_underflows_to_zero_rather_than_panicking() {
230        let mut g = glyph(0, f32::MIN_POSITIVE, f32::MIN_POSITIVE, f32::MIN_POSITIVE, 1.0);
231        g.scale_for_dpi(f32::MIN_POSITIVE);
232        assert_eq!(g.point.x, 0.0);
233        assert_eq!(g.point.y, 0.0);
234        assert_eq!(g.size.width, 0.0);
235        assert_eq!(g.size.height, f32::MIN_POSITIVE);
236    }
237
238    #[test]
239    fn scale_for_dpi_with_nan_factor_poisons_all_coordinates_without_panicking() {
240        let mut g = glyph(3, 1.0, 2.0, 3.0, 4.0);
241        g.scale_for_dpi(f32::NAN);
242        assert_eq!(g.index, 3);
243        assert!(g.point.x.is_nan());
244        assert!(g.point.y.is_nan());
245        assert!(g.size.width.is_nan());
246        assert!(g.size.height.is_nan());
247    }
248
249    #[test]
250    fn scale_for_dpi_with_infinite_factor_is_defined_at_zero_and_nonzero_coords() {
251        // finite non-zero * inf == inf (sign-correct)
252        let mut g = glyph(0, 1.0, -1.0, 2.0, -2.0);
253        g.scale_for_dpi(f32::INFINITY);
254        assert!(g.point.x.is_infinite() && g.point.x.is_sign_positive());
255        assert!(g.point.y.is_infinite() && g.point.y.is_sign_negative());
256        assert!(g.size.width.is_infinite() && g.size.width.is_sign_positive());
257        assert!(g.size.height.is_infinite() && g.size.height.is_sign_negative());
258
259        // 0 * inf == NaN, per IEEE-754 — must not panic, must not silently become 0.
260        let mut g = glyph(0, 0.0, 0.0, 0.0, 0.0);
261        g.scale_for_dpi(f32::INFINITY);
262        assert!(g.point.x.is_nan());
263        assert!(g.size.width.is_nan());
264
265        // -inf flips signs.
266        let mut g = glyph(0, 1.0, 1.0, 1.0, 1.0);
267        g.scale_for_dpi(f32::NEG_INFINITY);
268        assert!(g.point.x.is_infinite() && g.point.x.is_sign_negative());
269        assert!(g.size.height.is_infinite() && g.size.height.is_sign_negative());
270    }
271
272    #[test]
273    fn scale_for_dpi_on_default_glyph_is_a_no_op_for_finite_factors() {
274        let mut g = GlyphInstance::default();
275        g.scale_for_dpi(1_000_000.0);
276        assert_eq!(g.index, 0);
277        assert_eq!(g.point.x, 0.0);
278        assert_eq!(g.point.y, 0.0);
279        assert_eq!(g.size.width, 0.0);
280        assert_eq!(g.size.height, 0.0);
281    }
282
283    #[test]
284    fn glyph_equality_stays_reflexive_after_a_nan_scale() {
285        // `LogicalPosition`/`LogicalSize` quantize NaN to a single sentinel, so
286        // `GlyphInstance: Eq` must remain reflexive even with poisoned coords.
287        let mut g = glyph(9, 1.0, 2.0, 3.0, 4.0);
288        g.scale_for_dpi(f32::NAN);
289        let same = g;
290        assert_eq!(g, same);
291        // A NaN glyph must not compare equal to the origin glyph.
292        assert_ne!(g, glyph(9, 0.0, 0.0, 0.0, 0.0));
293    }
294}