1
//! Frame widget — a titled border container similar to an HTML `<fieldset>`
2
//! or a Windows group box.
3
//!
4
//! Renders a header with a centered title flanked by horizontal border lines,
5
//! and a bordered content area below.
6

            
7
use azul_core::dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClassVec};
8
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
9
use azul_css::{
10
    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
11
    props::{
12
        basic::*,
13
        layout::*,
14
        property::{CssProperty, *},
15
        style::*,
16
    },
17
    *,
18
};
19

            
20
const BORDER_COLOR: ColorU = ColorU {
21
    r: 221,
22
    g: 221,
23
    b: 221,
24
    a: 255,
25
};
26

            
27
const STRING_16146701490593874959: AzString = AzString::from_const_str("system:ui");
28
const STYLE_FONT_FAMILY_8122988506401935406_ITEMS: &[StyleFontFamily] =
29
    &[StyleFontFamily::System(STRING_16146701490593874959)];
30

            
31
const CSS_MATCH_15775557796860201720_PROPERTIES: &[CssPropertyWithConditions] = &[
32
    // .__azul-native-frame .__azul-native-frame-header .__azul-native-frame-header-before div
33
    CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
34
        LayoutHeight::Px(PixelValue::const_px(8)),
35
    ))),
36
    CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
37
        LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
38
            inner: PixelValue::const_px(1),
39
        }),
40
    )),
41
    CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
42
        StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
43
            inner: BorderStyle::Solid,
44
        }),
45
    )),
46
    CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
47
        StyleBorderTopColorValue::Exact(StyleBorderTopColor {
48
            inner: BORDER_COLOR,
49
        }),
50
    )),
51
    CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
52
        LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
53
            inner: PixelValue::const_px(1),
54
        }),
55
    )),
56
    CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
57
        StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
58
            inner: BorderStyle::Solid,
59
        }),
60
    )),
61
    CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
62
        StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
63
            inner: BORDER_COLOR,
64
        }),
65
    )),
66
    // .__azul-native-frame .__azul-native-frame-header .__azul-native-frame-header-before
67
    CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
68
        LayoutWidth::Px(PixelValue::const_px(5)),
69
    ))),
70
    CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
71
        LayoutMarginTop {
72
            inner: PixelValue::const_px(6),
73
        },
74
    ))),
75
    CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
76
        LayoutFlexGrow {
77
            inner: FloatValue::const_new(1),
78
        },
79
    ))),
80
    CssPropertyWithConditions::simple(CssProperty::FlexDirection(LayoutFlexDirectionValue::Exact(
81
        LayoutFlexDirection::Column,
82
    ))),
83
];
84
const CSS_MATCH_15775557796860201720: CssPropertyWithConditionsVec =
85
    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_15775557796860201720_PROPERTIES);
86

            
87
const CSS_MATCH_16739370686243728873_PROPERTIES: &[CssPropertyWithConditions] = &[
88
    // .__azul-native-frame .__azul-native-frame-header
89
    CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
90
        StyleFontSize {
91
            inner: PixelValue::const_px(11),
92
        },
93
    ))),
94
    CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
95
        StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
96
    ))),
97
    CssPropertyWithConditions::simple(CssProperty::FlexDirection(LayoutFlexDirectionValue::Exact(
98
        LayoutFlexDirection::Row,
99
    ))),
100
    CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
101
        LayoutAlignItems::End,
102
    ))),
103
];
104
const CSS_MATCH_16739370686243728873: CssPropertyWithConditionsVec =
105
    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_16739370686243728873_PROPERTIES);
106

            
107
const CSS_MATCH_4236783900531286611_PROPERTIES: &[CssPropertyWithConditions] = &[
108
    // .__azul-native-frame .__azul-native-frame-header p
109
    CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
110
        StyleTextAlign::Center,
111
    ))),
112
    CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
113
        LayoutPaddingLeft {
114
            inner: PixelValue::const_px(3),
115
        },
116
    ))),
117
    CssPropertyWithConditions::simple(CssProperty::PaddingRight(LayoutPaddingRightValue::Exact(
118
        LayoutPaddingRight {
119
            inner: PixelValue::const_px(1),
120
        },
121
    ))),
122
    CssPropertyWithConditions::simple(CssProperty::PaddingBottom(LayoutPaddingBottomValue::Exact(
123
        LayoutPaddingBottom {
124
            inner: PixelValue::const_px(0),
125
        },
126
    ))),
127
    CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
128
        LayoutPaddingTop {
129
            inner: PixelValue::const_px(0),
130
        },
131
    ))),
132
];
133
const CSS_MATCH_4236783900531286611: CssPropertyWithConditionsVec =
134
    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_4236783900531286611_PROPERTIES);
135

            
136
const CSS_MATCH_8602559445190067154_PROPERTIES: &[CssPropertyWithConditions] = &[
137
    // .__azul-native-frame
138
    CssPropertyWithConditions::simple(CssProperty::PaddingRight(LayoutPaddingRightValue::Exact(
139
        LayoutPaddingRight {
140
            inner: PixelValue::const_px(5),
141
        },
142
    ))),
143
    CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
144
        LayoutPaddingLeft {
145
            inner: PixelValue::const_px(5),
146
        },
147
    ))),
148
    CssPropertyWithConditions::simple(CssProperty::PaddingBottom(LayoutPaddingBottomValue::Exact(
149
        LayoutPaddingBottom {
150
            inner: PixelValue::const_px(3),
151
        },
152
    ))),
153
    CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
154
        LayoutPaddingTop {
155
            inner: PixelValue::const_px(3),
156
        },
157
    ))),
158
];
159
const CSS_MATCH_8602559445190067154: CssPropertyWithConditionsVec =
160
    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_8602559445190067154_PROPERTIES);
161

            
162
const CSS_MATCH_9156589477016488419_PROPERTIES: &[CssPropertyWithConditions] = &[
163
    // .__azul-native-frame .__azul-native-frame-header .__azul-native-frame-header-after div
164
    CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
165
        LayoutHeight::Px(PixelValue::const_px(8)),
166
    ))),
167
    CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
168
        LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
169
            inner: PixelValue::const_px(1),
170
        }),
171
    )),
172
    CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
173
        StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
174
            inner: BorderStyle::Solid,
175
        }),
176
    )),
177
    CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
178
        StyleBorderTopColorValue::Exact(StyleBorderTopColor {
179
            inner: BORDER_COLOR,
180
        }),
181
    )),
182
    CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
183
        LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
184
            inner: PixelValue::const_px(1),
185
        }),
186
    )),
187
    CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
188
        StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
189
            inner: BorderStyle::Solid,
190
        }),
191
    )),
192
    CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
193
        StyleBorderRightColorValue::Exact(StyleBorderRightColor {
194
            inner: BORDER_COLOR,
195
        }),
196
    )),
197
    // .__azul-native-frame .__azul-native-frame-header .__azul-native-frame-header-after
198
    CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
199
        LayoutMarginTop {
200
            inner: PixelValue::const_px(6),
201
        },
202
    ))),
203
    CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
204
        LayoutFlexGrow {
205
            inner: FloatValue::const_new(1),
206
        },
207
    ))),
208
    CssPropertyWithConditions::simple(CssProperty::FlexDirection(LayoutFlexDirectionValue::Exact(
209
        LayoutFlexDirection::Column,
210
    ))),
211
];
212
const CSS_MATCH_9156589477016488419: CssPropertyWithConditionsVec =
213
    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_9156589477016488419_PROPERTIES);
214

            
215
const CSS_MATCH_CONTENT_AREA_PROPERTIES: &[CssPropertyWithConditions] = &[
216
    // .__azul-native-frame .__azul-native-frame-content (static properties)
217
    CssPropertyWithConditions::simple(CssProperty::PaddingRight(
218
        LayoutPaddingRightValue::Exact(LayoutPaddingRight {
219
            inner: PixelValue::const_px(5),
220
        }),
221
    )),
222
    CssPropertyWithConditions::simple(CssProperty::PaddingLeft(
223
        LayoutPaddingLeftValue::Exact(LayoutPaddingLeft {
224
            inner: PixelValue::const_px(5),
225
        }),
226
    )),
227
    CssPropertyWithConditions::simple(CssProperty::PaddingBottom(
228
        LayoutPaddingBottomValue::Exact(LayoutPaddingBottom {
229
            inner: PixelValue::const_px(5),
230
        }),
231
    )),
232
    CssPropertyWithConditions::simple(CssProperty::PaddingTop(
233
        LayoutPaddingTopValue::Exact(LayoutPaddingTop {
234
            inner: PixelValue::const_px(5),
235
        }),
236
    )),
237
    CssPropertyWithConditions::simple(CssProperty::FontSize(
238
        StyleFontSizeValue::Exact(StyleFontSize {
239
            inner: PixelValue::const_px(11),
240
        }),
241
    )),
242
    CssPropertyWithConditions::simple(CssProperty::FontFamily(
243
        StyleFontFamilyVecValue::Exact(StyleFontFamilyVec::from_const_slice(
244
            STYLE_FONT_FAMILY_8122988506401935406_ITEMS,
245
        )),
246
    )),
247
    CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
248
        LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
249
            inner: PixelValue::const_px(1),
250
        }),
251
    )),
252
    CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
253
        LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
254
            inner: PixelValue::const_px(1),
255
        }),
256
    )),
257
    CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
258
        LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
259
            inner: PixelValue::const_px(1),
260
        }),
261
    )),
262
    CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
263
        LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
264
            inner: PixelValue::const_px(1),
265
        }),
266
    )),
267
    CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
268
        StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
269
            inner: BorderStyle::Solid,
270
        }),
271
    )),
272
    CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
273
        StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
274
            inner: BorderStyle::Solid,
275
        }),
276
    )),
277
    CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
278
        StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
279
            inner: BorderStyle::Solid,
280
        }),
281
    )),
282
    CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
283
        StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
284
            inner: BorderStyle::Solid,
285
        }),
286
    )),
287
    CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
288
        StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
289
            inner: BORDER_COLOR,
290
        }),
291
    )),
292
    CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
293
        StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
294
            inner: BORDER_COLOR,
295
        }),
296
    )),
297
    CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
298
        StyleBorderRightColorValue::Exact(StyleBorderRightColor {
299
            inner: BORDER_COLOR,
300
        }),
301
    )),
302
    CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
303
        StyleBorderTopColorValue::Exact(StyleBorderTopColor {
304
            inner: BORDER_COLOR,
305
        }),
306
    )),
307
];
308

            
309
/// A titled border container widget, similar to an HTML `<fieldset>` or
310
/// a Windows group box. Displays a header with a centered title and a
311
/// bordered content area below.
312
#[derive(Debug, Clone)]
313
#[repr(C)]
314
pub struct Frame {
315
    pub title: AzString,
316
    pub flex_grow: f32,
317
    pub content: Dom,
318
}
319

            
320
impl Frame {
321
    /// Creates a new `Frame` with the given title and content DOM.
322
325
    #[must_use] pub const fn create(title: AzString, content: Dom) -> Self {
323
325
        Self {
324
325
            title,
325
325
            content,
326
325
            flex_grow: 0.0,
327
325
        }
328
325
    }
329

            
330
    /// Replaces `self` with a default frame and returns the original.
331
    #[must_use]
332
12
    pub const fn swap_with_default(&mut self) -> Self {
333
12
        let mut s = Self::create(AzString::from_const_str(""), Dom::create_div());
334
12
        core::mem::swap(&mut s, self);
335
12
        s
336
12
    }
337

            
338
    /// Sets the flex-grow factor for the content area.
339
295
    pub const fn set_flex_grow(&mut self, flex_grow: f32) {
340
295
        self.flex_grow = flex_grow;
341
295
    }
342

            
343
    /// Builder-style setter for the flex-grow factor.
344
252
    #[must_use] pub const fn with_flex_grow(mut self, flex_grow: f32) -> Self {
345
252
        self.set_flex_grow(flex_grow);
346
252
        self
347
252
    }
348

            
349
268
    #[must_use] pub fn dom(self) -> Dom {
350
268
        Dom::create_div()
351
268
            .with_css_props(CSS_MATCH_8602559445190067154)
352
268
            .with_ids_and_classes({
353
                const IDS_AND_CLASSES_14615537625743340639: &[IdOrClass] =
354
                    &[Class(AzString::from_const_str("__azul-native-frame"))];
355
268
                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_14615537625743340639)
356
            })
357
268
            .with_children(DomVec::from_vec(vec![
358
268
                Dom::create_div()
359
268
                    .with_css_props(CSS_MATCH_16739370686243728873)
360
268
                    .with_ids_and_classes({
361
                        const IDS_AND_CLASSES_17776797146874875377: &[IdOrClass] = &[Class(
362
                            AzString::from_const_str("__azul-native-frame-header"),
363
                        )];
364
268
                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_17776797146874875377)
365
                    })
366
268
                    .with_children(DomVec::from_vec(vec![
367
268
                        Dom::create_div()
368
268
                            .with_css_props(CSS_MATCH_15775557796860201720)
369
268
                            .with_ids_and_classes({
370
                                const IDS_AND_CLASSES_15264202958442287530: &[IdOrClass] =
371
                                    &[Class(AzString::from_const_str(
372
                                        "__azul-native-frame-header-before",
373
                                    ))];
374
268
                                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_15264202958442287530)
375
                            })
376
268
                            .with_children(DomVec::from_vec(vec![Dom::create_div()])),
377
268
                        Dom::create_p_with_text(self.title)
378
268
                            .with_css_props(CSS_MATCH_4236783900531286611),
379
268
                        Dom::create_div()
380
268
                            .with_css_props(CSS_MATCH_9156589477016488419)
381
268
                            .with_ids_and_classes({
382
                                const IDS_AND_CLASSES_5689091102265932280: &[IdOrClass] = &[Class(
383
                                    AzString::from_const_str("__azul-native-frame-header-after"),
384
                                )];
385
268
                                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_5689091102265932280)
386
                            })
387
268
                            .with_children(DomVec::from_vec(vec![Dom::create_div()])),
388
                    ])),
389
268
                Dom::create_div()
390
268
                    .with_css_props({
391
268
                        let mut props = vec![
392
268
                            CssPropertyWithConditions::simple(CssProperty::FlexGrow(
393
268
                                LayoutFlexGrowValue::Exact(LayoutFlexGrow {
394
268
                                    inner: FloatValue::new(self.flex_grow),
395
268
                                }),
396
268
                            )),
397
                        ];
398
268
                        props.extend_from_slice(CSS_MATCH_CONTENT_AREA_PROPERTIES);
399
268
                        CssPropertyWithConditionsVec::from_vec(props)
400
                    })
401
268
                    .with_ids_and_classes({
402
                        const IDS_AND_CLASSES_9898887665724137124: &[IdOrClass] = &[Class(
403
                            AzString::from_const_str("__azul-native-frame-content"),
404
                        )];
405
268
                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_9898887665724137124)
406
                    })
407
268
                    .with_children(vec![self.content].into()),
408
            ]))
409
268
    }
410
}
411

            
412
#[cfg(test)]
413
#[allow(
414
    clippy::float_cmp,
415
    clippy::cast_precision_loss,
416
    clippy::unreadable_literal,
417
    clippy::too_many_lines
418
)]
419
mod autotest_generated {
420
    use std::collections::HashSet;
421

            
422
    use azul_core::dom::{NodeData, NodeType};
423

            
424
    use super::*;
425

            
426
    // ------------------------------------------------------------------
427
    // Helpers
428
    // ------------------------------------------------------------------
429

            
430
    /// Titles a caller can realistically hand to a frame: empty, an interior NUL,
431
    /// stacked combining marks, a ZWJ emoji sequence, an RTL override run and a
432
    /// replacement-char/BOM/control mix. The frame never parses or normalises its
433
    /// title — every one of these has to reach the DOM byte-for-byte.
434
    const ADVERSARIAL_TEXT: [&str; 6] = [
435
        "",
436
        "a\0b",
437
        "e\u{0301}\u{0301}\u{0301}",
438
        "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}",
439
        "\u{202E}gnirts desrever\u{202C}",
440
        "\u{FFFD}\u{FEFF}\t\n",
441
    ];
442

            
443
    /// Every f32 the numeric surface of `set_flex_grow` has to survive.
444
    /// `FloatValue::new` multiplies by 1000 and casts to `isize` — NaN, the
445
    /// infinities and `f32::MAX` all hit the saturating-cast path, and anything
446
    /// below 0.001 truncates away.
447
    const ADVERSARIAL_FLOATS: [f32; 14] = [
448
        0.0,
449
        -0.0,
450
        1.0,
451
        -1.0,
452
        0.001,
453
        -0.001,
454
        f32::EPSILON,
455
        f32::MIN_POSITIVE,
456
        -f32::MIN_POSITIVE,
457
        f32::MAX,
458
        f32::MIN,
459
        f32::INFINITY,
460
        f32::NEG_INFINITY,
461
        f32::NAN,
462
    ];
463

            
464
    /// A frame with the given title over the given content.
465
    fn frame(title: &str, content: Dom) -> Frame {
466
        Frame::create(AzString::from(title), content)
467
    }
468

            
469
    /// The frame `swap_with_default` leaves behind: empty title, empty div, no growth.
470
    fn default_frame() -> Frame {
471
        Frame::create(AzString::from_const_str(""), Dom::create_div())
472
    }
473

            
474
    fn kids(dom: &Dom) -> &[Dom] {
475
        dom.children.as_ref()
476
    }
477

            
478
    /// The header row (`.__azul-native-frame-header`).
479
    fn header(dom: &Dom) -> &Dom {
480
        &kids(dom)[0]
481
    }
482

            
483
    /// The bordered content area (`.__azul-native-frame-content`).
484
    fn content_area(dom: &Dom) -> &Dom {
485
        &kids(dom)[1]
486
    }
487

            
488
    /// The left rule of the header (`.__azul-native-frame-header-before`).
489
    fn rule_before(dom: &Dom) -> &Dom {
490
        &kids(header(dom))[0]
491
    }
492

            
493
    /// The title `<p>` box, wedged between the two header rules.
494
    fn title_node(dom: &Dom) -> &Dom {
495
        &kids(header(dom))[1]
496
    }
497

            
498
    /// The right rule of the header (`.__azul-native-frame-header-after`).
499
    fn rule_after(dom: &Dom) -> &Dom {
500
        &kids(header(dom))[2]
501
    }
502

            
503
    /// The declared properties of a node's inline style, in declaration order.
504
    fn props_of(node: &NodeData) -> Vec<CssProperty> {
505
        node.style
506
            .iter_inline_properties()
507
            .map(|(p, _)| p.clone())
508
            .collect()
509
    }
510

            
511
    fn inline_props(dom: &Dom) -> Vec<CssProperty> {
512
        props_of(&dom.root)
513
    }
514

            
515
    /// The properties of a build-time constant, in declaration order.
516
    fn declared(v: &[CssPropertyWithConditions]) -> Vec<CssProperty> {
517
        v.iter().map(|p| p.property.clone()).collect()
518
    }
519

            
520
    /// The CSS classes of a node, in declaration order.
521
    fn classes(dom: &Dom) -> Vec<String> {
522
        dom.root
523
            .get_ids_and_classes()
524
            .as_ref()
525
            .iter()
526
            .filter_map(|c| match c {
527
                Class(s) => Some(s.as_str().to_string()),
528
                IdOrClass::Id(_) => None,
529
            })
530
            .collect()
531
    }
532

            
533
    /// The text of a text node, looking through the `<p>` block wrapper the
534
    /// label convention mandates (`p > text`).
535
    fn text_of(dom: &Dom) -> Option<&str> {
536
        match dom.root.get_node_type() {
537
            NodeType::Text(s) => Some(s.as_ref().as_str()),
538
            NodeType::P => match dom.children.as_ref() {
539
                [only] => match only.root.get_node_type() {
540
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
541
                    _ => None,
542
                },
543
                _ => None,
544
            },
545
            _ => None,
546
        }
547
    }
548

            
549
    /// The `flex-grow` factor as it actually lands in a node's style — i.e. *after*
550
    /// the lossy `f32 -> isize` encoding inside `FloatValue::new`.
551
    fn flex_grow_of(dom: &Dom) -> Option<f32> {
552
        dom.root
553
            .style
554
            .iter_inline_properties()
555
            .find_map(|(p, _)| match p {
556
                CssProperty::FlexGrow(v) => v.get_property().map(|f| f.inner.get()),
557
                _ => None,
558
            })
559
    }
560

            
561
    /// The `flex-grow` of the content area — the only place the caller's factor
562
    /// is allowed to surface.
563
    fn content_flex_grow(dom: &Dom) -> Option<f32> {
564
        flex_grow_of(content_area(dom))
565
    }
566

            
567
    fn count_flex_grow(dom: &Dom) -> usize {
568
        inline_props(dom)
569
            .iter()
570
            .filter(|p| matches!(p, CssProperty::FlexGrow(_)))
571
            .count()
572
    }
573

            
574
    /// The `f32` of a `PixelValue`, asserting the length is an absolute `px`. An
575
    /// `em`/`%` slipping into the frame chrome would resolve against the parent
576
    /// font/box instead of staying a fixed border, padding or rule size.
577
    fn px(pv: &PixelValue) -> f32 {
578
        assert_eq!(
579
            pv.metric,
580
            SizeMetric::Px,
581
            "frame geometry must be absolute px, got {:?}",
582
            pv.metric
583
        );
584
        pv.number.get()
585
    }
586

            
587
    /// The length a property declares, if it declares one at all.
588
    fn length_of(p: &CssProperty) -> Option<PixelValue> {
589
        match p {
590
            CssProperty::Height(v) => match v.get_property() {
591
                Some(LayoutHeight::Px(pv)) => Some(*pv),
592
                _ => None,
593
            },
594
            CssProperty::Width(v) => match v.get_property() {
595
                Some(LayoutWidth::Px(pv)) => Some(*pv),
596
                _ => None,
597
            },
598
            CssProperty::FontSize(v) => v.get_property().map(|x| x.inner),
599
            CssProperty::MarginTop(v) => v.get_property().map(|x| x.inner),
600
            CssProperty::PaddingTop(v) => v.get_property().map(|x| x.inner),
601
            CssProperty::PaddingBottom(v) => v.get_property().map(|x| x.inner),
602
            CssProperty::PaddingLeft(v) => v.get_property().map(|x| x.inner),
603
            CssProperty::PaddingRight(v) => v.get_property().map(|x| x.inner),
604
            CssProperty::BorderTopWidth(v) => v.get_property().map(|x| x.inner),
605
            CssProperty::BorderBottomWidth(v) => v.get_property().map(|x| x.inner),
606
            CssProperty::BorderLeftWidth(v) => v.get_property().map(|x| x.inner),
607
            CssProperty::BorderRightWidth(v) => v.get_property().map(|x| x.inner),
608
            _ => None,
609
        }
610
    }
611

            
612
    /// The border colour a property declares, if it declares one at all.
613
    fn border_color_of(p: &CssProperty) -> Option<ColorU> {
614
        match p {
615
            CssProperty::BorderTopColor(v) => v.get_property().map(|x| x.inner),
616
            CssProperty::BorderBottomColor(v) => v.get_property().map(|x| x.inner),
617
            CssProperty::BorderLeftColor(v) => v.get_property().map(|x| x.inner),
618
            CssProperty::BorderRightColor(v) => v.get_property().map(|x| x.inner),
619
            _ => None,
620
        }
621
    }
622

            
623
    fn walk<'a>(dom: &'a Dom, out: &mut Vec<&'a Dom>) {
624
        out.push(dom);
625
        for c in dom.children.as_ref() {
626
            walk(c, out);
627
        }
628
    }
629

            
630
    /// Every node of the tree, root first.
631
    fn all_nodes(dom: &Dom) -> Vec<&Dom> {
632
        let mut v = Vec::new();
633
        walk(dom, &mut v);
634
        v
635
    }
636

            
637
    /// The recursive descendant count. `Dom::estimated_total_children` is a
638
    /// *cached* value that, if too small, makes `convert_dom_into_compact_dom`
639
    /// under-allocate its arenas and panic on out-of-bounds writes — so it has to
640
    /// match this exactly.
641
    fn count_descendants(dom: &Dom) -> usize {
642
        dom.children
643
            .as_ref()
644
            .iter()
645
            .map(|c| 1 + count_descendants(c))
646
            .sum()
647
    }
648

            
649
    /// A chain of `depth` nested divs (depth kept modest: `Dom`'s `Drop`/`PartialEq`
650
    /// recurse).
651
    fn nested_divs(depth: usize) -> Dom {
652
        let mut d = Dom::create_div();
653
        for _ in 0..depth {
654
            d = Dom::create_div().with_child(d);
655
        }
656
        d
657
    }
658

            
659
    // ------------------------------------------------------------------
660
    // Frame::create
661
    // ------------------------------------------------------------------
662

            
663
    #[test]
664
    fn create_zeroes_flex_grow_and_stores_both_arguments_verbatim() {
665
        let f = frame("Settings", Dom::create_text_do_not_use_without_block_level_wrapper("body"));
666

            
667
        // Positive zero, not -0.0: a negative zero would flip the sign of the encoded
668
        // isize on some paths and is not what "no growth" means.
669
        assert_eq!(f.flex_grow.to_bits(), 0_u32, "flex_grow must start at +0.0");
670
        assert_eq!(f.title.as_str(), "Settings", "the title was not stored verbatim");
671
        assert_eq!(
672
            text_of(&f.content),
673
            Some("body"),
674
            "the content was not stored verbatim",
675
        );
676
    }
677

            
678
    #[test]
679
    fn create_stores_pathological_titles_byte_for_byte() {
680
        for t in ADVERSARIAL_TEXT {
681
            let f = frame(t, Dom::create_div());
682
            assert_eq!(f.title.as_str(), t, "the frame mangled or normalised its title");
683
            assert_eq!(
684
                f.title.as_str().len(),
685
                t.len(),
686
                "the byte length of the title changed (NUL truncation or re-encoding?)",
687
            );
688

            
689
            // ... and it survives the trip into the rendered DOM.
690
            let dom = f.dom();
691
            assert_eq!(
692
                text_of(title_node(&dom)),
693
                Some(t),
694
                "the title was corrupted on the way into the DOM",
695
            );
696
        }
697
    }
698

            
699
    #[test]
700
    fn create_accepts_a_very_long_title_without_truncating_it() {
701
        // 100k astral-plane chars: 400 KB of UTF-8 through the AzString/BoxOrStatic path.
702
        let huge: String = "\u{1F600}".repeat(100_000);
703
        let f = frame(&huge, Dom::create_div());
704
        assert_eq!(f.title.as_str().len(), 400_000, "the long title was truncated on store");
705

            
706
        let dom = f.dom();
707
        assert_eq!(
708
            text_of(title_node(&dom)).map(str::len),
709
            Some(400_000),
710
            "the long title was truncated on the way into the DOM",
711
        );
712
    }
713

            
714
    #[test]
715
    fn create_accepts_deeply_nested_and_very_wide_content() {
716
        let deep = frame("deep", nested_divs(64)).dom();
717
        assert_eq!(
718
            deep.estimated_total_children,
719
            count_descendants(&deep),
720
            "the cached child count desynced for deeply nested content",
721
        );
722
        assert_eq!(deep.estimated_total_children, 9 + 64, "frame chrome is 8 nodes + the content");
723

            
724
        let wide = frame(
725
            "wide",
726
            Dom::create_div().with_children(
727
                (0..2000).map(|_| Dom::create_div()).collect::<Vec<_>>().into(),
728
            ),
729
        )
730
        .dom();
731
        assert_eq!(
732
            wide.estimated_total_children,
733
            count_descendants(&wide),
734
            "the cached child count desynced for very wide content",
735
        );
736
        assert_eq!(wide.estimated_total_children, 9 + 2000);
737
    }
738

            
739
    #[test]
740
    fn frames_nest_without_desyncing_the_child_count_cache() {
741
        let inner = frame("inner", Dom::create_div()).dom();
742
        assert_eq!(inner.estimated_total_children, 9, "a bare frame is 9 nodes below the root");
743

            
744
        let outer = frame("outer", inner).dom();
745
        assert_eq!(
746
            outer.estimated_total_children,
747
            count_descendants(&outer),
748
            "nesting a frame inside a frame desynced the cached child count",
749
        );
750
        assert_eq!(outer.estimated_total_children, 18);
751
    }
752

            
753
    // ------------------------------------------------------------------
754
    // Frame::swap_with_default
755
    // ------------------------------------------------------------------
756

            
757
    #[test]
758
    fn swap_with_default_moves_every_field_out_and_leaves_a_default() {
759
        let mut f = frame("payload", Dom::create_text_do_not_use_without_block_level_wrapper("body")).with_flex_grow(2.5);
760

            
761
        let taken = f.swap_with_default();
762

            
763
        assert_eq!(taken.title.as_str(), "payload", "the title did not travel out");
764
        assert_eq!(text_of(&taken.content), Some("body"), "the content did not travel out");
765
        assert_eq!(taken.flex_grow, 2.5, "flex_grow did not travel out");
766

            
767
        assert_eq!(f.title.as_str(), "", "the swapped-in default kept a title");
768
        assert_eq!(f.content, Dom::create_div(), "the swapped-in default is not an empty div");
769
        assert_eq!(
770
            f.flex_grow.to_bits(),
771
            0_u32,
772
            "the swapped-in default has a non-zero flex_grow",
773
        );
774
    }
775

            
776
    #[test]
777
    fn repeated_swap_with_default_never_accumulates_state() {
778
        let mut f = frame("x", Dom::create_text_do_not_use_without_block_level_wrapper("y")).with_flex_grow(1.0);
779
        let first = f.swap_with_default();
780
        assert_eq!(first.title.as_str(), "x");
781

            
782
        for i in 0..8 {
783
            let taken = f.swap_with_default();
784
            assert_eq!(taken.title.as_str(), "", "swap #{i} handed back a non-default title");
785
            assert_eq!(taken.content, Dom::create_div(), "swap #{i} handed back non-default content");
786
            assert_eq!(taken.flex_grow.to_bits(), 0_u32, "swap #{i} handed back a non-zero factor");
787
            assert_eq!(f.title.as_str(), "", "swap #{i} left a non-default title behind");
788
        }
789

            
790
        // The DOM of the drained frame is still a well-formed, empty-titled frame.
791
        let dom = f.dom();
792
        assert_eq!(kids(&dom).len(), 2);
793
        assert_eq!(text_of(title_node(&dom)), Some(""));
794
        assert_eq!(content_flex_grow(&dom), Some(0.0));
795
    }
796

            
797
    #[test]
798
    fn swap_with_default_preserves_a_nan_factor_in_the_value_it_hands_back() {
799
        // The setter never sanitises, so a NaN has to survive the swap unchanged —
800
        // only the *encoding* into the DOM is allowed to collapse it (see below).
801
        let mut f = default_frame().with_flex_grow(f32::NAN);
802
        let taken = f.swap_with_default();
803
        assert!(taken.flex_grow.is_nan(), "the NaN factor was rewritten by the swap");
804
        assert_eq!(f.flex_grow.to_bits(), 0_u32, "the NaN leaked into the swapped-in default");
805
    }
806

            
807
    #[test]
808
    fn swap_with_default_leaves_a_frame_that_still_renders() {
809
        // Guards against the swapped-in default sharing/aliasing the moved-out DOM:
810
        // both halves have to render independently, in either order.
811
        let mut f = frame("original", nested_divs(8));
812
        let taken = f.swap_with_default();
813

            
814
        let left_behind = f.dom();
815
        let moved_out = taken.dom();
816

            
817
        assert_eq!(text_of(title_node(&left_behind)), Some(""));
818
        assert_eq!(text_of(title_node(&moved_out)), Some("original"));
819
        assert_eq!(left_behind.estimated_total_children, 9);
820
        assert_eq!(moved_out.estimated_total_children, 9 + 8);
821
    }
822

            
823
    // ------------------------------------------------------------------
824
    // Frame::set_flex_grow / Frame::with_flex_grow  (numeric)
825
    // ------------------------------------------------------------------
826

            
827
    #[test]
828
    fn set_flex_grow_stores_the_bit_pattern_verbatim_without_sanitising() {
829
        // The setter is a plain assignment — it must not clamp, round or NaN-scrub.
830
        // (Sanitisation happens later, at encode time; see the tests below.)
831
        for v in ADVERSARIAL_FLOATS {
832
            let mut f = default_frame();
833
            f.set_flex_grow(v);
834
            if v.is_nan() {
835
                assert!(f.flex_grow.is_nan(), "a NaN flex_grow was silently rewritten");
836
            } else {
837
                assert_eq!(
838
                    f.flex_grow.to_bits(),
839
                    v.to_bits(),
840
                    "flex_grow {v} was not stored verbatim",
841
                );
842
            }
843
        }
844
    }
845

            
846
    #[test]
847
    fn set_flex_grow_is_last_write_wins() {
848
        let mut f = default_frame();
849
        for v in ADVERSARIAL_FLOATS {
850
            f.set_flex_grow(v);
851
        }
852
        assert!(f.flex_grow.is_nan(), "the final write (NaN) did not win");
853

            
854
        f.set_flex_grow(1.5);
855
        assert_eq!(f.flex_grow, 1.5, "a later write did not overwrite the NaN");
856
        assert_eq!(count_flex_grow(content_area(&f.dom())), 1, "flex-grow was declared twice");
857
    }
858

            
859
    #[test]
860
    fn with_flex_grow_is_exactly_set_flex_grow() {
861
        for v in ADVERSARIAL_FLOATS {
862
            let mut by_setter = default_frame();
863
            by_setter.set_flex_grow(v);
864
            let by_builder = default_frame().with_flex_grow(v);
865

            
866
            assert_eq!(
867
                by_setter.flex_grow.to_bits(),
868
                by_builder.flex_grow.to_bits(),
869
                "the builder and the setter disagree for {v}",
870
            );
871
        }
872
    }
873

            
874
    #[test]
875
    fn with_flex_grow_touches_only_the_numeric_field() {
876
        let f = frame("title", Dom::create_text_do_not_use_without_block_level_wrapper("body")).with_flex_grow(f32::NAN);
877

            
878
        assert_eq!(f.title.as_str(), "title", "with_flex_grow clobbered the title");
879
        assert_eq!(text_of(&f.content), Some("body"), "with_flex_grow clobbered the content");
880
    }
881

            
882
    #[test]
883
    fn flex_grow_zero_encodes_to_positive_zero_even_from_negative_zero() {
884
        for v in [0.0_f32, -0.0_f32] {
885
            let dom = default_frame().with_flex_grow(v).dom();
886
            let got = content_flex_grow(&dom).expect("flex-grow must always be declared");
887
            assert_eq!(got.to_bits(), 0_u32, "flex_grow {v} did not encode to +0.0 (got {got})");
888
        }
889
    }
890

            
891
    #[test]
892
    fn flex_grow_round_trips_through_the_dom_at_milli_precision() {
893
        // FloatValue keeps 3 decimal places (x1000, truncating cast), so any factor that
894
        // is a whole multiple of 0.001 must come back out of the DOM unchanged.
895
        for v in [0.0_f32, 0.001, 0.5, 1.0, 2.5, 3.0, -1.5, 1000.0, 65536.0] {
896
            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom())
897
                .expect("flex-grow must always be declared");
898
            assert!(
899
                (got - v).abs() <= 0.001,
900
                "flex_grow {v} did not survive the FloatValue encoding (got {got})",
901
            );
902
        }
903
    }
904

            
905
    #[test]
906
    fn flex_grow_below_the_encoding_precision_truncates_to_zero() {
907
        // Everything under one milli-unit is quantised away — including the sign, because
908
        // the truncating cast of -0.0001 * 1000 = -0.1 lands on integer 0.
909
        for v in [
910
            f32::EPSILON,
911
            f32::MIN_POSITIVE,
912
            -f32::MIN_POSITIVE,
913
            1e-4,
914
            -1e-4,
915
            0.0005,
916
            -0.0009,
917
        ] {
918
            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom())
919
                .expect("flex-grow must always be declared");
920
            assert_eq!(
921
                got.to_bits(),
922
                0_u32,
923
                "sub-milli flex_grow {v} did not truncate to +0.0 (got {got})",
924
            );
925
        }
926

            
927
        // ... and 0.001 is genuinely the smallest factor that still registers.
928
        let smallest = content_flex_grow(&default_frame().with_flex_grow(0.001).dom()).unwrap();
929
        assert!(smallest > 0.0, "0.001 is supposed to be the smallest representable factor");
930
    }
931

            
932
    #[test]
933
    fn a_nan_flex_grow_does_not_panic_and_lands_on_zero() {
934
        let dom = default_frame().with_flex_grow(f32::NAN).dom();
935
        let got = content_flex_grow(&dom).expect("flex-grow must always be declared");
936
        assert!(!got.is_nan(), "a NaN flex-grow reached the style tree");
937
        assert_eq!(got.to_bits(), 0_u32, "NaN must encode to +0.0 (saturating cast), got {got}");
938
    }
939

            
940
    #[test]
941
    fn infinite_and_maximal_flex_grow_saturates_instead_of_overflowing() {
942
        // `FloatValue::new` does `(v * 1000.0) as isize`; f32::MAX * 1000 already overflows
943
        // to +inf, so MAX and INFINITY have to land on the same saturated bound. The point
944
        // is that nothing wraps around into a factor of the opposite sign.
945
        let max_encoded = (isize::MAX as f32) / 1000.0;
946
        let min_encoded = (isize::MIN as f32) / 1000.0;
947

            
948
        for v in [f32::INFINITY, f32::MAX] {
949
            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
950
            assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
951
            assert_eq!(got, max_encoded, "{v} did not saturate at the isize upper bound");
952
            assert!(got > 0.0, "{v} wrapped around into a non-positive factor");
953
        }
954

            
955
        for v in [f32::NEG_INFINITY, f32::MIN] {
956
            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
957
            assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
958
            assert_eq!(got, min_encoded, "{v} did not saturate at the isize lower bound");
959
            assert!(got < 0.0, "{v} wrapped around into a non-negative factor");
960
        }
961
    }
962

            
963
    #[test]
964
    fn no_flex_grow_input_can_put_a_nan_or_an_infinity_into_the_style_tree() {
965
        for v in ADVERSARIAL_FLOATS {
966
            let dom = default_frame().with_flex_grow(v).dom();
967
            let got = content_flex_grow(&dom)
968
                .unwrap_or_else(|| panic!("flex-grow disappeared for input {v}"));
969
            assert!(
970
                got.is_finite(),
971
                "input {v} produced a non-finite flex-grow ({got}) — the layout solver would \
972
                 NaN out",
973
            );
974
        }
975
    }
976

            
977
    #[test]
978
    fn flex_grow_encoding_is_monotonic() {
979
        // A sign- or rounding-bug in the x1000 cast would show up as an inversion here.
980
        let ascending = [-1000.0_f32, -1.5, -0.001, 0.0, 0.001, 1.5, 1000.0];
981
        let mut prev = f32::NEG_INFINITY;
982
        for v in ascending {
983
            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
984
            assert!(
985
                got >= prev,
986
                "encoding is not monotonic: {v} encoded to {got}, below the previous {prev}",
987
            );
988
            prev = got;
989
        }
990
    }
991

            
992
    #[test]
993
    fn the_callers_factor_only_ever_reaches_the_content_area() {
994
        // The two header rules carry a *fixed* flex-grow of 1 so they split the leftover
995
        // header width evenly; a caller's factor bleeding into them would collapse or
996
        // stretch the rules on either side of the title.
997
        for v in ADVERSARIAL_FLOATS {
998
            let dom = default_frame().with_flex_grow(v).dom();
999

            
            assert_eq!(count_flex_grow(&dom), 0, "the frame root declared a flex-grow");
            assert_eq!(count_flex_grow(header(&dom)), 0, "the header declared a flex-grow");
            assert_eq!(count_flex_grow(title_node(&dom)), 0, "the title declared a flex-grow");
            assert_eq!(flex_grow_of(rule_before(&dom)), Some(1.0), "left rule factor changed");
            assert_eq!(flex_grow_of(rule_after(&dom)), Some(1.0), "right rule factor changed");
            assert_eq!(
                count_flex_grow(content_area(&dom)),
                1,
                "the content area must declare flex-grow exactly once",
            );
        }
    }
    // ------------------------------------------------------------------
    // Frame::dom
    // ------------------------------------------------------------------
    #[test]
    fn dom_has_the_documented_shape() {
        let dom = frame("Title", Dom::create_text_do_not_use_without_block_level_wrapper("content")).dom();
        assert_eq!(kids(&dom).len(), 2, "a frame is a header plus a content area");
        assert_eq!(kids(header(&dom)).len(), 3, "the header is rule / title / rule");
        assert_eq!(kids(rule_before(&dom)).len(), 1, "the left rule wraps exactly one div");
        assert_eq!(kids(rule_after(&dom)).len(), 1, "the right rule wraps exactly one div");
        assert_eq!(
            kids(title_node(&dom)).len(),
            1,
            "the title is a <p> wrapping exactly one bare text node"
        );
        assert!(
            kids(&kids(title_node(&dom))[0]).is_empty(),
            "the text node itself must be a leaf"
        );
        assert_eq!(kids(content_area(&dom)).len(), 1, "the content area holds exactly one child");
        assert_eq!(text_of(title_node(&dom)), Some("Title"));
        assert_eq!(text_of(&kids(content_area(&dom))[0]), Some("content"));
        assert_eq!(dom.estimated_total_children, count_descendants(&dom));
    }
    #[test]
    fn an_empty_title_still_produces_a_text_node_between_the_two_rules() {
        // Dropping the node for an empty title would change the header's flex layout
        // (two rules instead of three items) and silently re-centre the rules.
        let dom = default_frame().dom();
        assert_eq!(kids(header(&dom)).len(), 3, "an empty title collapsed the header");
        assert_eq!(text_of(title_node(&dom)), Some(""), "the empty title node vanished");
    }
    #[test]
    fn dom_carries_every_class_the_stylesheet_selects_on() {
        let dom = frame("t", Dom::create_div()).dom();
        assert_eq!(classes(&dom), vec!["__azul-native-frame".to_string()]);
        assert_eq!(classes(header(&dom)), vec!["__azul-native-frame-header".to_string()]);
        assert_eq!(
            classes(rule_before(&dom)),
            vec!["__azul-native-frame-header-before".to_string()],
        );
        assert_eq!(
            classes(rule_after(&dom)),
            vec!["__azul-native-frame-header-after".to_string()],
        );
        assert_eq!(classes(content_area(&dom)), vec!["__azul-native-frame-content".to_string()]);
        // The title node and the two inner rule divs are addressed by element/descendant
        // selectors only — they carry no class of their own.
        assert!(classes(title_node(&dom)).is_empty(), "the title node grew a class");
        assert!(classes(&kids(rule_before(&dom))[0]).is_empty());
        assert!(classes(&kids(rule_after(&dom))[0]).is_empty());
    }
    #[test]
    fn dom_declares_exactly_the_build_time_constants_on_the_chrome_nodes() {
        let dom = frame("t", Dom::create_div()).dom();
        assert_eq!(inline_props(&dom), declared(CSS_MATCH_8602559445190067154.as_ref()));
        assert_eq!(inline_props(header(&dom)), declared(CSS_MATCH_16739370686243728873.as_ref()));
        assert_eq!(
            inline_props(rule_before(&dom)),
            declared(CSS_MATCH_15775557796860201720.as_ref()),
        );
        assert_eq!(
            inline_props(rule_after(&dom)),
            declared(CSS_MATCH_9156589477016488419.as_ref()),
        );
        assert_eq!(inline_props(title_node(&dom)), declared(CSS_MATCH_4236783900531286611.as_ref()));
    }
    #[test]
    fn the_content_area_declares_flex_grow_first_then_the_static_block_verbatim() {
        let dom = default_frame().with_flex_grow(2.5).dom();
        let mut expected = vec![CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::new(2.5),
            },
        ))];
        expected.extend(declared(CSS_MATCH_CONTENT_AREA_PROPERTIES));
        assert_eq!(
            inline_props(content_area(&dom)),
            expected,
            "the content-area style block drifted from FlexGrow + the static constant",
        );
    }
    #[test]
    fn no_node_declares_the_same_property_twice() {
        // A duplicate declaration means one of the two is silently dead — and which one
        // wins depends on cascade order, so the widget would style differently per node.
        let dom = frame("t", Dom::create_div()).with_flex_grow(1.0).dom();
        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
            let props = inline_props(node);
            let mut seen = HashSet::new();
            for p in &props {
                assert!(
                    seen.insert(p.get_type()),
                    "node {i} declares {:?} twice",
                    p.get_type(),
                );
            }
        }
    }
    #[test]
    fn every_declaration_is_unconditional() {
        // The frame chrome has no :hover/@media/@os variants — a stray condition would
        // make part of the border render only in one state.
        let dom = frame("t", Dom::create_div()).dom();
        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
            for (p, conditions) in node.root.style.iter_inline_properties() {
                assert!(
                    conditions.as_ref().is_empty(),
                    "node {i} gates {:?} behind a dynamic selector",
                    p.get_type(),
                );
            }
        }
    }
    #[test]
    fn all_frame_lengths_are_absolute_px() {
        let dom = frame("t", Dom::create_div()).dom();
        for node in all_nodes(&dom) {
            for p in inline_props(node) {
                if let Some(pv) = length_of(&p) {
                    let v = px(&pv); // asserts SizeMetric::Px
                    assert!(
                        v.is_finite() && (0.0..=64.0).contains(&v),
                        "{:?} = {v}px is outside the plausible frame-chrome range",
                        p.get_type(),
                    );
                }
            }
        }
    }
    #[test]
    fn every_border_in_the_frame_uses_the_one_border_colour() {
        let dom = frame("t", Dom::create_div()).dom();
        let mut seen = 0_usize;
        for node in all_nodes(&dom) {
            for p in inline_props(node) {
                if let Some(c) = border_color_of(&p) {
                    assert_eq!(c, BORDER_COLOR, "{:?} uses an off-palette colour", p.get_type());
                    seen += 1;
                }
            }
        }
        // top+left on the left rule, top+right on the right rule, all four on the content
        // area.
        assert_eq!(seen, 8, "the number of coloured borders in the frame changed");
    }
    #[test]
    fn the_content_area_is_fully_boxed_in_on_all_four_sides() {
        // A missing side would leave the group box visibly open.
        let props = inline_props(content_area(&frame("t", Dom::create_div()).dom()));
        let widths = [
            CssPropertyType::BorderTopWidth,
            CssPropertyType::BorderBottomWidth,
            CssPropertyType::BorderLeftWidth,
            CssPropertyType::BorderRightWidth,
        ];
        for want in widths {
            let found = props
                .iter()
                .find(|p| p.get_type() == want)
                .unwrap_or_else(|| panic!("the content area is missing {want:?}"));
            assert_eq!(
                length_of(found).map(|pv| px(&pv)),
                Some(1.0),
                "{want:?} is not a 1px hairline",
            );
        }
    }
    #[test]
    fn the_two_header_rules_are_symmetric_apart_from_the_left_rule_carrying_a_width() {
        // Pinned as-is: `.__azul-native-frame-header-before` declares `width: 5px` while
        // `-after` declares no width at all, so the stub of rule left of the title is
        // capped at 5px while the right-hand rule is only bounded by flex-grow. This is
        // an asymmetry in the widget's style constants, not in the code under test —
        // recorded here so a deliberate change to either side shows up as a failure.
        let dom = frame("t", Dom::create_div()).dom();
        let before: Vec<CssPropertyType> =
            inline_props(rule_before(&dom)).iter().map(CssProperty::get_type).collect();
        let after: Vec<CssPropertyType> =
            inline_props(rule_after(&dom)).iter().map(CssProperty::get_type).collect();
        assert!(before.contains(&CssPropertyType::Width), "the left rule lost its width cap");
        assert!(
            !after.contains(&CssPropertyType::Width),
            "the right rule gained a width — the known before/after asymmetry was fixed, \
             update this test",
        );
        // Mirrored side: left draws a left border, right draws a right border.
        assert!(before.contains(&CssPropertyType::BorderLeftWidth));
        assert!(after.contains(&CssPropertyType::BorderRightWidth));
        // Everything else is shared, in the same order.
        for shared in [
            CssPropertyType::Height,
            CssPropertyType::BorderTopWidth,
            CssPropertyType::BorderTopStyle,
            CssPropertyType::BorderTopColor,
            CssPropertyType::MarginTop,
            CssPropertyType::FlexGrow,
            CssPropertyType::FlexDirection,
        ] {
            assert!(before.contains(&shared), "the left rule is missing {shared:?}");
            assert!(after.contains(&shared), "the right rule is missing {shared:?}");
        }
    }
    #[test]
    fn the_inner_rule_divs_carry_no_style_of_their_own() {
        // Pinned as-is: the constants comment their `... -header-before div` properties
        // (height + borders) separately from the container's, but both blocks are applied
        // to the *container* — the inner div is an unstyled, empty leaf. Recorded so that
        // re-splitting the two selectors shows up here instead of silently changing the
        // rendered header.
        let dom = frame("t", Dom::create_div()).dom();
        for inner in [&kids(rule_before(&dom))[0], &kids(rule_after(&dom))[0]] {
            assert!(
                inline_props(inner).is_empty(),
                "the inner rule div grew inline properties",
            );
            assert!(kids(inner).is_empty(), "the inner rule div grew children");
            assert_eq!(inner.root.get_node_type(), &NodeType::Div);
        }
        // ... and the height/border pair really does sit on the containers.
        for outer in [rule_before(&dom), rule_after(&dom)] {
            let types: Vec<CssPropertyType> =
                inline_props(outer).iter().map(CssProperty::get_type).collect();
            assert!(types.contains(&CssPropertyType::Height));
            assert!(types.contains(&CssPropertyType::BorderTopWidth));
        }
    }
    #[test]
    fn the_frame_is_inert_and_attaches_no_callbacks_or_stylesheets() {
        let dom = frame("t", Dom::create_div()).with_flex_grow(1.0).dom();
        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
            assert!(
                node.root.callbacks.as_ref().is_empty(),
                "node {i} of a stateless frame carries a callback",
            );
            assert!(
                node.css.as_ref().is_empty(),
                "node {i} attached a whole stylesheet instead of inline props",
            );
        }
    }
    #[test]
    fn dom_moves_the_content_in_verbatim_including_its_own_classes_and_children() {
        let content = Dom::create_div()
            .with_ids_and_classes(IdOrClassVec::from_vec(vec![Class(AzString::from(
                "user-content",
            ))]))
            .with_children(DomVec::from_vec(vec![
                Dom::create_text_do_not_use_without_block_level_wrapper("a"),
                Dom::create_text_do_not_use_without_block_level_wrapper("b"),
            ]));
        let expected = content.clone();
        let dom = frame("t", content).dom();
        let placed = &kids(content_area(&dom))[0];
        assert_eq!(*placed, expected, "the content DOM was rewritten on the way in");
        assert_eq!(classes(placed), vec!["user-content".to_string()]);
        assert_eq!(kids(placed).len(), 2);
    }
    #[test]
    fn dom_is_deterministic_for_equal_inputs() {
        for t in ADVERSARIAL_TEXT {
            for v in ADVERSARIAL_FLOATS {
                let a = frame(t, Dom::create_text_do_not_use_without_block_level_wrapper(t)).with_flex_grow(v).dom();
                let b = frame(t, Dom::create_text_do_not_use_without_block_level_wrapper(t)).with_flex_grow(v).dom();
                assert_eq!(a, b, "two identical frames rendered differently for {t:?} / {v}");
            }
        }
    }
    #[test]
    fn distinct_factors_that_encode_differently_produce_distinct_doms() {
        // The counterpart to the determinism test: the flex factor must actually reach
        // the tree, so a change in it has to be observable in the rendered DOM.
        let a = default_frame().with_flex_grow(1.0).dom();
        let b = default_frame().with_flex_grow(2.0).dom();
        assert_ne!(a, b, "the flex factor never reached the rendered DOM");
        // ... while two factors that collapse to the same encoding are indistinguishable.
        let nan = default_frame().with_flex_grow(f32::NAN).dom();
        let zero = default_frame().with_flex_grow(0.0).dom();
        assert_eq!(nan, zero, "NaN and 0.0 stopped encoding to the same style tree");
    }
    #[test]
    fn the_title_is_styled_the_same_no_matter_what_it_contains() {
        let baseline = declared(CSS_MATCH_4236783900531286611.as_ref());
        for t in ADVERSARIAL_TEXT {
            let dom = frame(t, Dom::create_div()).dom();
            assert_eq!(
                inline_props(title_node(&dom)),
                baseline,
                "the title style changed for input {t:?}",
            );
            assert_eq!(kids(header(&dom)).len(), 3, "the header shape changed for input {t:?}");
            assert_eq!(dom.estimated_total_children, 9, "the node count changed for {t:?}");
        }
    }
}