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

            
5
use 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)]
11
pub struct ResolvedOffsets {
12
    pub top: f32,
13
    pub left: f32,
14
    pub right: f32,
15
    pub bottom: f32,
16
}
17

            
18
impl ResolvedOffsets {
19
3
    #[must_use] pub const fn zero() -> Self {
20
3
        Self {
21
3
            top: 0.0,
22
3
            left: 0.0,
23
3
            right: 0.0,
24
3
            bottom: 0.0,
25
3
        }
26
3
    }
27
    #[must_use]
28
16
    pub fn total_vertical(&self) -> f32 {
29
16
        self.top + self.bottom
30
16
    }
31
    #[must_use]
32
15
    pub fn total_horizontal(&self) -> f32 {
33
15
        self.left + self.right
34
15
    }
35
}
36

            
37
/// Index into a font's glyph table.
38
type GlyphIndex = u32;
39

            
40
/// A single positioned glyph with its index, screen position, and size.
41
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd)]
42
pub struct GlyphInstance {
43
    pub index: GlyphIndex,
44
    pub point: LogicalPosition,
45
    pub size: LogicalSize,
46
}
47

            
48
impl GlyphInstance {
49
13
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
50
13
        self.point.scale_for_dpi(scale_factor);
51
13
        self.size.scale_for_dpi(scale_factor);
52
13
    }
53
}
54

            
55
#[cfg(test)]
56
mod 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
}