Skip to main content

azul_layout/widgets/
frame.rs

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
7use 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
9use azul_css::{
10    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
11    props::{
12        basic::*,
13        layout::*,
14        property::{CssProperty, *},
15        style::*,
16    },
17    *,
18};
19
20const BORDER_COLOR: ColorU = ColorU {
21    r: 221,
22    g: 221,
23    b: 221,
24    a: 255,
25};
26
27const STRING_16146701490593874959: AzString = AzString::from_const_str("system:ui");
28const STYLE_FONT_FAMILY_8122988506401935406_ITEMS: &[StyleFontFamily] =
29    &[StyleFontFamily::System(STRING_16146701490593874959)];
30
31const 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];
84const CSS_MATCH_15775557796860201720: CssPropertyWithConditionsVec =
85    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_15775557796860201720_PROPERTIES);
86
87const 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];
104const CSS_MATCH_16739370686243728873: CssPropertyWithConditionsVec =
105    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_16739370686243728873_PROPERTIES);
106
107const 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];
133const CSS_MATCH_4236783900531286611: CssPropertyWithConditionsVec =
134    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_4236783900531286611_PROPERTIES);
135
136const 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];
159const CSS_MATCH_8602559445190067154: CssPropertyWithConditionsVec =
160    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_8602559445190067154_PROPERTIES);
161
162const 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];
212const CSS_MATCH_9156589477016488419: CssPropertyWithConditionsVec =
213    CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_9156589477016488419_PROPERTIES);
214
215const 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)]
314pub struct Frame {
315    pub title: AzString,
316    pub flex_grow: f32,
317    pub content: Dom,
318}
319
320impl Frame {
321    /// Creates a new `Frame` with the given title and content DOM.
322    #[must_use] pub const fn create(title: AzString, content: Dom) -> Self {
323        Self {
324            title,
325            content,
326            flex_grow: 0.0,
327        }
328    }
329
330    /// Replaces `self` with a default frame and returns the original.
331    #[must_use]
332    pub const fn swap_with_default(&mut self) -> Self {
333        let mut s = Self::create(AzString::from_const_str(""), Dom::create_div());
334        core::mem::swap(&mut s, self);
335        s
336    }
337
338    /// Sets the flex-grow factor for the content area.
339    pub const fn set_flex_grow(&mut self, flex_grow: f32) {
340        self.flex_grow = flex_grow;
341    }
342
343    /// Builder-style setter for the flex-grow factor.
344    #[must_use] pub const fn with_flex_grow(mut self, flex_grow: f32) -> Self {
345        self.set_flex_grow(flex_grow);
346        self
347    }
348
349    #[must_use] pub fn dom(self) -> Dom {
350        Dom::create_div()
351            .with_css_props(CSS_MATCH_8602559445190067154)
352            .with_ids_and_classes({
353                const IDS_AND_CLASSES_14615537625743340639: &[IdOrClass] =
354                    &[Class(AzString::from_const_str("__azul-native-frame"))];
355                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_14615537625743340639)
356            })
357            .with_children(DomVec::from_vec(vec![
358                Dom::create_div()
359                    .with_css_props(CSS_MATCH_16739370686243728873)
360                    .with_ids_and_classes({
361                        const IDS_AND_CLASSES_17776797146874875377: &[IdOrClass] = &[Class(
362                            AzString::from_const_str("__azul-native-frame-header"),
363                        )];
364                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_17776797146874875377)
365                    })
366                    .with_children(DomVec::from_vec(vec![
367                        Dom::create_div()
368                            .with_css_props(CSS_MATCH_15775557796860201720)
369                            .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                                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_15264202958442287530)
375                            })
376                            .with_children(DomVec::from_vec(vec![Dom::create_div()])),
377                        Dom::create_text(self.title).with_css_props(CSS_MATCH_4236783900531286611),
378                        Dom::create_div()
379                            .with_css_props(CSS_MATCH_9156589477016488419)
380                            .with_ids_and_classes({
381                                const IDS_AND_CLASSES_5689091102265932280: &[IdOrClass] = &[Class(
382                                    AzString::from_const_str("__azul-native-frame-header-after"),
383                                )];
384                                IdOrClassVec::from_const_slice(IDS_AND_CLASSES_5689091102265932280)
385                            })
386                            .with_children(DomVec::from_vec(vec![Dom::create_div()])),
387                    ])),
388                Dom::create_div()
389                    .with_css_props({
390                        let mut props = vec![
391                            CssPropertyWithConditions::simple(CssProperty::FlexGrow(
392                                LayoutFlexGrowValue::Exact(LayoutFlexGrow {
393                                    inner: FloatValue::new(self.flex_grow),
394                                }),
395                            )),
396                        ];
397                        props.extend_from_slice(CSS_MATCH_CONTENT_AREA_PROPERTIES);
398                        CssPropertyWithConditionsVec::from_vec(props)
399                    })
400                    .with_ids_and_classes({
401                        const IDS_AND_CLASSES_9898887665724137124: &[IdOrClass] = &[Class(
402                            AzString::from_const_str("__azul-native-frame-content"),
403                        )];
404                        IdOrClassVec::from_const_slice(IDS_AND_CLASSES_9898887665724137124)
405                    })
406                    .with_children(vec![self.content].into()),
407            ]))
408    }
409}
410
411#[cfg(test)]
412#[allow(
413    clippy::float_cmp,
414    clippy::cast_precision_loss,
415    clippy::unreadable_literal,
416    clippy::too_many_lines
417)]
418mod autotest_generated {
419    use std::collections::HashSet;
420
421    use azul_core::dom::{NodeData, NodeType};
422
423    use super::*;
424
425    // ------------------------------------------------------------------
426    // Helpers
427    // ------------------------------------------------------------------
428
429    /// Titles a caller can realistically hand to a frame: empty, an interior NUL,
430    /// stacked combining marks, a ZWJ emoji sequence, an RTL override run and a
431    /// replacement-char/BOM/control mix. The frame never parses or normalises its
432    /// title — every one of these has to reach the DOM byte-for-byte.
433    const ADVERSARIAL_TEXT: [&str; 6] = [
434        "",
435        "a\0b",
436        "e\u{0301}\u{0301}\u{0301}",
437        "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}",
438        "\u{202E}gnirts desrever\u{202C}",
439        "\u{FFFD}\u{FEFF}\t\n",
440    ];
441
442    /// Every f32 the numeric surface of `set_flex_grow` has to survive.
443    /// `FloatValue::new` multiplies by 1000 and casts to `isize` — NaN, the
444    /// infinities and `f32::MAX` all hit the saturating-cast path, and anything
445    /// below 0.001 truncates away.
446    const ADVERSARIAL_FLOATS: [f32; 14] = [
447        0.0,
448        -0.0,
449        1.0,
450        -1.0,
451        0.001,
452        -0.001,
453        f32::EPSILON,
454        f32::MIN_POSITIVE,
455        -f32::MIN_POSITIVE,
456        f32::MAX,
457        f32::MIN,
458        f32::INFINITY,
459        f32::NEG_INFINITY,
460        f32::NAN,
461    ];
462
463    /// A frame with the given title over the given content.
464    fn frame(title: &str, content: Dom) -> Frame {
465        Frame::create(AzString::from(title), content)
466    }
467
468    /// The frame `swap_with_default` leaves behind: empty title, empty div, no growth.
469    fn default_frame() -> Frame {
470        Frame::create(AzString::from_const_str(""), Dom::create_div())
471    }
472
473    fn kids(dom: &Dom) -> &[Dom] {
474        dom.children.as_ref()
475    }
476
477    /// The header row (`.__azul-native-frame-header`).
478    fn header(dom: &Dom) -> &Dom {
479        &kids(dom)[0]
480    }
481
482    /// The bordered content area (`.__azul-native-frame-content`).
483    fn content_area(dom: &Dom) -> &Dom {
484        &kids(dom)[1]
485    }
486
487    /// The left rule of the header (`.__azul-native-frame-header-before`).
488    fn rule_before(dom: &Dom) -> &Dom {
489        &kids(header(dom))[0]
490    }
491
492    /// The title text node, wedged between the two header rules.
493    fn title_node(dom: &Dom) -> &Dom {
494        &kids(header(dom))[1]
495    }
496
497    /// The right rule of the header (`.__azul-native-frame-header-after`).
498    fn rule_after(dom: &Dom) -> &Dom {
499        &kids(header(dom))[2]
500    }
501
502    /// The declared properties of a node's inline style, in declaration order.
503    fn props_of(node: &NodeData) -> Vec<CssProperty> {
504        node.style
505            .iter_inline_properties()
506            .map(|(p, _)| p.clone())
507            .collect()
508    }
509
510    fn inline_props(dom: &Dom) -> Vec<CssProperty> {
511        props_of(&dom.root)
512    }
513
514    /// The properties of a build-time constant, in declaration order.
515    fn declared(v: &[CssPropertyWithConditions]) -> Vec<CssProperty> {
516        v.iter().map(|p| p.property.clone()).collect()
517    }
518
519    /// The CSS classes of a node, in declaration order.
520    fn classes(dom: &Dom) -> Vec<String> {
521        dom.root
522            .get_ids_and_classes()
523            .as_ref()
524            .iter()
525            .filter_map(|c| match c {
526                IdOrClass::Class(s) => Some(s.as_str().to_string()),
527                IdOrClass::Id(_) => None,
528            })
529            .collect()
530    }
531
532    fn text_of(dom: &Dom) -> Option<&str> {
533        match dom.root.get_node_type() {
534            NodeType::Text(s) => Some(s.as_ref().as_str()),
535            _ => None,
536        }
537    }
538
539    /// The `flex-grow` factor as it actually lands in a node's style — i.e. *after*
540    /// the lossy `f32 -> isize` encoding inside `FloatValue::new`.
541    fn flex_grow_of(dom: &Dom) -> Option<f32> {
542        dom.root
543            .style
544            .iter_inline_properties()
545            .find_map(|(p, _)| match p {
546                CssProperty::FlexGrow(v) => v.get_property().map(|f| f.inner.get()),
547                _ => None,
548            })
549    }
550
551    /// The `flex-grow` of the content area — the only place the caller's factor
552    /// is allowed to surface.
553    fn content_flex_grow(dom: &Dom) -> Option<f32> {
554        flex_grow_of(content_area(dom))
555    }
556
557    fn count_flex_grow(dom: &Dom) -> usize {
558        inline_props(dom)
559            .iter()
560            .filter(|p| matches!(p, CssProperty::FlexGrow(_)))
561            .count()
562    }
563
564    /// The `f32` of a `PixelValue`, asserting the length is an absolute `px`. An
565    /// `em`/`%` slipping into the frame chrome would resolve against the parent
566    /// font/box instead of staying a fixed border, padding or rule size.
567    fn px(pv: &PixelValue) -> f32 {
568        assert_eq!(
569            pv.metric,
570            SizeMetric::Px,
571            "frame geometry must be absolute px, got {:?}",
572            pv.metric
573        );
574        pv.number.get()
575    }
576
577    /// The length a property declares, if it declares one at all.
578    fn length_of(p: &CssProperty) -> Option<PixelValue> {
579        match p {
580            CssProperty::Height(v) => match v.get_property() {
581                Some(LayoutHeight::Px(pv)) => Some(*pv),
582                _ => None,
583            },
584            CssProperty::Width(v) => match v.get_property() {
585                Some(LayoutWidth::Px(pv)) => Some(*pv),
586                _ => None,
587            },
588            CssProperty::FontSize(v) => v.get_property().map(|x| x.inner),
589            CssProperty::MarginTop(v) => v.get_property().map(|x| x.inner),
590            CssProperty::PaddingTop(v) => v.get_property().map(|x| x.inner),
591            CssProperty::PaddingBottom(v) => v.get_property().map(|x| x.inner),
592            CssProperty::PaddingLeft(v) => v.get_property().map(|x| x.inner),
593            CssProperty::PaddingRight(v) => v.get_property().map(|x| x.inner),
594            CssProperty::BorderTopWidth(v) => v.get_property().map(|x| x.inner),
595            CssProperty::BorderBottomWidth(v) => v.get_property().map(|x| x.inner),
596            CssProperty::BorderLeftWidth(v) => v.get_property().map(|x| x.inner),
597            CssProperty::BorderRightWidth(v) => v.get_property().map(|x| x.inner),
598            _ => None,
599        }
600    }
601
602    /// The border colour a property declares, if it declares one at all.
603    fn border_color_of(p: &CssProperty) -> Option<ColorU> {
604        match p {
605            CssProperty::BorderTopColor(v) => v.get_property().map(|x| x.inner),
606            CssProperty::BorderBottomColor(v) => v.get_property().map(|x| x.inner),
607            CssProperty::BorderLeftColor(v) => v.get_property().map(|x| x.inner),
608            CssProperty::BorderRightColor(v) => v.get_property().map(|x| x.inner),
609            _ => None,
610        }
611    }
612
613    fn walk<'a>(dom: &'a Dom, out: &mut Vec<&'a Dom>) {
614        out.push(dom);
615        for c in dom.children.as_ref() {
616            walk(c, out);
617        }
618    }
619
620    /// Every node of the tree, root first.
621    fn all_nodes(dom: &Dom) -> Vec<&Dom> {
622        let mut v = Vec::new();
623        walk(dom, &mut v);
624        v
625    }
626
627    /// The recursive descendant count. `Dom::estimated_total_children` is a
628    /// *cached* value that, if too small, makes `convert_dom_into_compact_dom`
629    /// under-allocate its arenas and panic on out-of-bounds writes — so it has to
630    /// match this exactly.
631    fn count_descendants(dom: &Dom) -> usize {
632        dom.children
633            .as_ref()
634            .iter()
635            .map(|c| 1 + count_descendants(c))
636            .sum()
637    }
638
639    /// A chain of `depth` nested divs (depth kept modest: `Dom`'s `Drop`/`PartialEq`
640    /// recurse).
641    fn nested_divs(depth: usize) -> Dom {
642        let mut d = Dom::create_div();
643        for _ in 0..depth {
644            d = Dom::create_div().with_child(d);
645        }
646        d
647    }
648
649    // ------------------------------------------------------------------
650    // Frame::create
651    // ------------------------------------------------------------------
652
653    #[test]
654    fn create_zeroes_flex_grow_and_stores_both_arguments_verbatim() {
655        let f = frame("Settings", Dom::create_text("body"));
656
657        // Positive zero, not -0.0: a negative zero would flip the sign of the encoded
658        // isize on some paths and is not what "no growth" means.
659        assert_eq!(f.flex_grow.to_bits(), 0_u32, "flex_grow must start at +0.0");
660        assert_eq!(f.title.as_str(), "Settings", "the title was not stored verbatim");
661        assert_eq!(
662            text_of(&f.content),
663            Some("body"),
664            "the content was not stored verbatim",
665        );
666    }
667
668    #[test]
669    fn create_stores_pathological_titles_byte_for_byte() {
670        for t in ADVERSARIAL_TEXT {
671            let f = frame(t, Dom::create_div());
672            assert_eq!(f.title.as_str(), t, "the frame mangled or normalised its title");
673            assert_eq!(
674                f.title.as_str().len(),
675                t.len(),
676                "the byte length of the title changed (NUL truncation or re-encoding?)",
677            );
678
679            // ... and it survives the trip into the rendered DOM.
680            let dom = f.dom();
681            assert_eq!(
682                text_of(title_node(&dom)),
683                Some(t),
684                "the title was corrupted on the way into the DOM",
685            );
686        }
687    }
688
689    #[test]
690    fn create_accepts_a_very_long_title_without_truncating_it() {
691        // 100k astral-plane chars: 400 KB of UTF-8 through the AzString/BoxOrStatic path.
692        let huge: String = "\u{1F600}".repeat(100_000);
693        let f = frame(&huge, Dom::create_div());
694        assert_eq!(f.title.as_str().len(), 400_000, "the long title was truncated on store");
695
696        let dom = f.dom();
697        assert_eq!(
698            text_of(title_node(&dom)).map(str::len),
699            Some(400_000),
700            "the long title was truncated on the way into the DOM",
701        );
702    }
703
704    #[test]
705    fn create_accepts_deeply_nested_and_very_wide_content() {
706        let deep = frame("deep", nested_divs(64)).dom();
707        assert_eq!(
708            deep.estimated_total_children,
709            count_descendants(&deep),
710            "the cached child count desynced for deeply nested content",
711        );
712        assert_eq!(deep.estimated_total_children, 8 + 64, "frame chrome is 8 nodes + the content");
713
714        let wide = frame(
715            "wide",
716            Dom::create_div().with_children(
717                (0..2000).map(|_| Dom::create_div()).collect::<Vec<_>>().into(),
718            ),
719        )
720        .dom();
721        assert_eq!(
722            wide.estimated_total_children,
723            count_descendants(&wide),
724            "the cached child count desynced for very wide content",
725        );
726        assert_eq!(wide.estimated_total_children, 8 + 2000);
727    }
728
729    #[test]
730    fn frames_nest_without_desyncing_the_child_count_cache() {
731        let inner = frame("inner", Dom::create_div()).dom();
732        assert_eq!(inner.estimated_total_children, 8, "a bare frame is 8 nodes below the root");
733
734        let outer = frame("outer", inner).dom();
735        assert_eq!(
736            outer.estimated_total_children,
737            count_descendants(&outer),
738            "nesting a frame inside a frame desynced the cached child count",
739        );
740        assert_eq!(outer.estimated_total_children, 16);
741    }
742
743    // ------------------------------------------------------------------
744    // Frame::swap_with_default
745    // ------------------------------------------------------------------
746
747    #[test]
748    fn swap_with_default_moves_every_field_out_and_leaves_a_default() {
749        let mut f = frame("payload", Dom::create_text("body")).with_flex_grow(2.5);
750
751        let taken = f.swap_with_default();
752
753        assert_eq!(taken.title.as_str(), "payload", "the title did not travel out");
754        assert_eq!(text_of(&taken.content), Some("body"), "the content did not travel out");
755        assert_eq!(taken.flex_grow, 2.5, "flex_grow did not travel out");
756
757        assert_eq!(f.title.as_str(), "", "the swapped-in default kept a title");
758        assert_eq!(f.content, Dom::create_div(), "the swapped-in default is not an empty div");
759        assert_eq!(
760            f.flex_grow.to_bits(),
761            0_u32,
762            "the swapped-in default has a non-zero flex_grow",
763        );
764    }
765
766    #[test]
767    fn repeated_swap_with_default_never_accumulates_state() {
768        let mut f = frame("x", Dom::create_text("y")).with_flex_grow(1.0);
769        let first = f.swap_with_default();
770        assert_eq!(first.title.as_str(), "x");
771
772        for i in 0..8 {
773            let taken = f.swap_with_default();
774            assert_eq!(taken.title.as_str(), "", "swap #{i} handed back a non-default title");
775            assert_eq!(taken.content, Dom::create_div(), "swap #{i} handed back non-default content");
776            assert_eq!(taken.flex_grow.to_bits(), 0_u32, "swap #{i} handed back a non-zero factor");
777            assert_eq!(f.title.as_str(), "", "swap #{i} left a non-default title behind");
778        }
779
780        // The DOM of the drained frame is still a well-formed, empty-titled frame.
781        let dom = f.dom();
782        assert_eq!(kids(&dom).len(), 2);
783        assert_eq!(text_of(title_node(&dom)), Some(""));
784        assert_eq!(content_flex_grow(&dom), Some(0.0));
785    }
786
787    #[test]
788    fn swap_with_default_preserves_a_nan_factor_in_the_value_it_hands_back() {
789        // The setter never sanitises, so a NaN has to survive the swap unchanged —
790        // only the *encoding* into the DOM is allowed to collapse it (see below).
791        let mut f = default_frame().with_flex_grow(f32::NAN);
792        let taken = f.swap_with_default();
793        assert!(taken.flex_grow.is_nan(), "the NaN factor was rewritten by the swap");
794        assert_eq!(f.flex_grow.to_bits(), 0_u32, "the NaN leaked into the swapped-in default");
795    }
796
797    #[test]
798    fn swap_with_default_leaves_a_frame_that_still_renders() {
799        // Guards against the swapped-in default sharing/aliasing the moved-out DOM:
800        // both halves have to render independently, in either order.
801        let mut f = frame("original", nested_divs(8));
802        let taken = f.swap_with_default();
803
804        let left_behind = f.dom();
805        let moved_out = taken.dom();
806
807        assert_eq!(text_of(title_node(&left_behind)), Some(""));
808        assert_eq!(text_of(title_node(&moved_out)), Some("original"));
809        assert_eq!(left_behind.estimated_total_children, 8);
810        assert_eq!(moved_out.estimated_total_children, 8 + 8);
811    }
812
813    // ------------------------------------------------------------------
814    // Frame::set_flex_grow / Frame::with_flex_grow  (numeric)
815    // ------------------------------------------------------------------
816
817    #[test]
818    fn set_flex_grow_stores_the_bit_pattern_verbatim_without_sanitising() {
819        // The setter is a plain assignment — it must not clamp, round or NaN-scrub.
820        // (Sanitisation happens later, at encode time; see the tests below.)
821        for v in ADVERSARIAL_FLOATS {
822            let mut f = default_frame();
823            f.set_flex_grow(v);
824            if v.is_nan() {
825                assert!(f.flex_grow.is_nan(), "a NaN flex_grow was silently rewritten");
826            } else {
827                assert_eq!(
828                    f.flex_grow.to_bits(),
829                    v.to_bits(),
830                    "flex_grow {v} was not stored verbatim",
831                );
832            }
833        }
834    }
835
836    #[test]
837    fn set_flex_grow_is_last_write_wins() {
838        let mut f = default_frame();
839        for v in ADVERSARIAL_FLOATS {
840            f.set_flex_grow(v);
841        }
842        assert!(f.flex_grow.is_nan(), "the final write (NaN) did not win");
843
844        f.set_flex_grow(1.5);
845        assert_eq!(f.flex_grow, 1.5, "a later write did not overwrite the NaN");
846        assert_eq!(count_flex_grow(content_area(&f.dom())), 1, "flex-grow was declared twice");
847    }
848
849    #[test]
850    fn with_flex_grow_is_exactly_set_flex_grow() {
851        for v in ADVERSARIAL_FLOATS {
852            let mut by_setter = default_frame();
853            by_setter.set_flex_grow(v);
854            let by_builder = default_frame().with_flex_grow(v);
855
856            assert_eq!(
857                by_setter.flex_grow.to_bits(),
858                by_builder.flex_grow.to_bits(),
859                "the builder and the setter disagree for {v}",
860            );
861        }
862    }
863
864    #[test]
865    fn with_flex_grow_touches_only_the_numeric_field() {
866        let f = frame("title", Dom::create_text("body")).with_flex_grow(f32::NAN);
867
868        assert_eq!(f.title.as_str(), "title", "with_flex_grow clobbered the title");
869        assert_eq!(text_of(&f.content), Some("body"), "with_flex_grow clobbered the content");
870    }
871
872    #[test]
873    fn flex_grow_zero_encodes_to_positive_zero_even_from_negative_zero() {
874        for v in [0.0_f32, -0.0_f32] {
875            let dom = default_frame().with_flex_grow(v).dom();
876            let got = content_flex_grow(&dom).expect("flex-grow must always be declared");
877            assert_eq!(got.to_bits(), 0_u32, "flex_grow {v} did not encode to +0.0 (got {got})");
878        }
879    }
880
881    #[test]
882    fn flex_grow_round_trips_through_the_dom_at_milli_precision() {
883        // FloatValue keeps 3 decimal places (x1000, truncating cast), so any factor that
884        // is a whole multiple of 0.001 must come back out of the DOM unchanged.
885        for v in [0.0_f32, 0.001, 0.5, 1.0, 2.5, 3.0, -1.5, 1000.0, 65536.0] {
886            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom())
887                .expect("flex-grow must always be declared");
888            assert!(
889                (got - v).abs() <= 0.001,
890                "flex_grow {v} did not survive the FloatValue encoding (got {got})",
891            );
892        }
893    }
894
895    #[test]
896    fn flex_grow_below_the_encoding_precision_truncates_to_zero() {
897        // Everything under one milli-unit is quantised away — including the sign, because
898        // the truncating cast of -0.0001 * 1000 = -0.1 lands on integer 0.
899        for v in [
900            f32::EPSILON,
901            f32::MIN_POSITIVE,
902            -f32::MIN_POSITIVE,
903            1e-4,
904            -1e-4,
905            0.0005,
906            -0.0009,
907        ] {
908            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom())
909                .expect("flex-grow must always be declared");
910            assert_eq!(
911                got.to_bits(),
912                0_u32,
913                "sub-milli flex_grow {v} did not truncate to +0.0 (got {got})",
914            );
915        }
916
917        // ... and 0.001 is genuinely the smallest factor that still registers.
918        let smallest = content_flex_grow(&default_frame().with_flex_grow(0.001).dom()).unwrap();
919        assert!(smallest > 0.0, "0.001 is supposed to be the smallest representable factor");
920    }
921
922    #[test]
923    fn a_nan_flex_grow_does_not_panic_and_lands_on_zero() {
924        let dom = default_frame().with_flex_grow(f32::NAN).dom();
925        let got = content_flex_grow(&dom).expect("flex-grow must always be declared");
926        assert!(!got.is_nan(), "a NaN flex-grow reached the style tree");
927        assert_eq!(got.to_bits(), 0_u32, "NaN must encode to +0.0 (saturating cast), got {got}");
928    }
929
930    #[test]
931    fn infinite_and_maximal_flex_grow_saturates_instead_of_overflowing() {
932        // `FloatValue::new` does `(v * 1000.0) as isize`; f32::MAX * 1000 already overflows
933        // to +inf, so MAX and INFINITY have to land on the same saturated bound. The point
934        // is that nothing wraps around into a factor of the opposite sign.
935        let max_encoded = (isize::MAX as f32) / 1000.0;
936        let min_encoded = (isize::MIN as f32) / 1000.0;
937
938        for v in [f32::INFINITY, f32::MAX] {
939            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
940            assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
941            assert_eq!(got, max_encoded, "{v} did not saturate at the isize upper bound");
942            assert!(got > 0.0, "{v} wrapped around into a non-positive factor");
943        }
944
945        for v in [f32::NEG_INFINITY, f32::MIN] {
946            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
947            assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
948            assert_eq!(got, min_encoded, "{v} did not saturate at the isize lower bound");
949            assert!(got < 0.0, "{v} wrapped around into a non-negative factor");
950        }
951    }
952
953    #[test]
954    fn no_flex_grow_input_can_put_a_nan_or_an_infinity_into_the_style_tree() {
955        for v in ADVERSARIAL_FLOATS {
956            let dom = default_frame().with_flex_grow(v).dom();
957            let got = content_flex_grow(&dom)
958                .unwrap_or_else(|| panic!("flex-grow disappeared for input {v}"));
959            assert!(
960                got.is_finite(),
961                "input {v} produced a non-finite flex-grow ({got}) — the layout solver would \
962                 NaN out",
963            );
964        }
965    }
966
967    #[test]
968    fn flex_grow_encoding_is_monotonic() {
969        // A sign- or rounding-bug in the x1000 cast would show up as an inversion here.
970        let ascending = [-1000.0_f32, -1.5, -0.001, 0.0, 0.001, 1.5, 1000.0];
971        let mut prev = f32::NEG_INFINITY;
972        for v in ascending {
973            let got = content_flex_grow(&default_frame().with_flex_grow(v).dom()).unwrap();
974            assert!(
975                got >= prev,
976                "encoding is not monotonic: {v} encoded to {got}, below the previous {prev}",
977            );
978            prev = got;
979        }
980    }
981
982    #[test]
983    fn the_callers_factor_only_ever_reaches_the_content_area() {
984        // The two header rules carry a *fixed* flex-grow of 1 so they split the leftover
985        // header width evenly; a caller's factor bleeding into them would collapse or
986        // stretch the rules on either side of the title.
987        for v in ADVERSARIAL_FLOATS {
988            let dom = default_frame().with_flex_grow(v).dom();
989
990            assert_eq!(count_flex_grow(&dom), 0, "the frame root declared a flex-grow");
991            assert_eq!(count_flex_grow(header(&dom)), 0, "the header declared a flex-grow");
992            assert_eq!(count_flex_grow(title_node(&dom)), 0, "the title declared a flex-grow");
993            assert_eq!(flex_grow_of(rule_before(&dom)), Some(1.0), "left rule factor changed");
994            assert_eq!(flex_grow_of(rule_after(&dom)), Some(1.0), "right rule factor changed");
995            assert_eq!(
996                count_flex_grow(content_area(&dom)),
997                1,
998                "the content area must declare flex-grow exactly once",
999            );
1000        }
1001    }
1002
1003    // ------------------------------------------------------------------
1004    // Frame::dom
1005    // ------------------------------------------------------------------
1006
1007    #[test]
1008    fn dom_has_the_documented_shape() {
1009        let dom = frame("Title", Dom::create_text("content")).dom();
1010
1011        assert_eq!(kids(&dom).len(), 2, "a frame is a header plus a content area");
1012        assert_eq!(kids(header(&dom)).len(), 3, "the header is rule / title / rule");
1013        assert_eq!(kids(rule_before(&dom)).len(), 1, "the left rule wraps exactly one div");
1014        assert_eq!(kids(rule_after(&dom)).len(), 1, "the right rule wraps exactly one div");
1015        assert!(kids(title_node(&dom)).is_empty(), "the title node must be a leaf");
1016        assert_eq!(kids(content_area(&dom)).len(), 1, "the content area holds exactly one child");
1017
1018        assert_eq!(text_of(title_node(&dom)), Some("Title"));
1019        assert_eq!(text_of(&kids(content_area(&dom))[0]), Some("content"));
1020        assert_eq!(dom.estimated_total_children, count_descendants(&dom));
1021    }
1022
1023    #[test]
1024    fn an_empty_title_still_produces_a_text_node_between_the_two_rules() {
1025        // Dropping the node for an empty title would change the header's flex layout
1026        // (two rules instead of three items) and silently re-centre the rules.
1027        let dom = default_frame().dom();
1028        assert_eq!(kids(header(&dom)).len(), 3, "an empty title collapsed the header");
1029        assert_eq!(text_of(title_node(&dom)), Some(""), "the empty title node vanished");
1030    }
1031
1032    #[test]
1033    fn dom_carries_every_class_the_stylesheet_selects_on() {
1034        let dom = frame("t", Dom::create_div()).dom();
1035
1036        assert_eq!(classes(&dom), vec!["__azul-native-frame".to_string()]);
1037        assert_eq!(classes(header(&dom)), vec!["__azul-native-frame-header".to_string()]);
1038        assert_eq!(
1039            classes(rule_before(&dom)),
1040            vec!["__azul-native-frame-header-before".to_string()],
1041        );
1042        assert_eq!(
1043            classes(rule_after(&dom)),
1044            vec!["__azul-native-frame-header-after".to_string()],
1045        );
1046        assert_eq!(classes(content_area(&dom)), vec!["__azul-native-frame-content".to_string()]);
1047
1048        // The title node and the two inner rule divs are addressed by element/descendant
1049        // selectors only — they carry no class of their own.
1050        assert!(classes(title_node(&dom)).is_empty(), "the title node grew a class");
1051        assert!(classes(&kids(rule_before(&dom))[0]).is_empty());
1052        assert!(classes(&kids(rule_after(&dom))[0]).is_empty());
1053    }
1054
1055    #[test]
1056    fn dom_declares_exactly_the_build_time_constants_on_the_chrome_nodes() {
1057        let dom = frame("t", Dom::create_div()).dom();
1058
1059        assert_eq!(inline_props(&dom), declared(CSS_MATCH_8602559445190067154.as_ref()));
1060        assert_eq!(inline_props(header(&dom)), declared(CSS_MATCH_16739370686243728873.as_ref()));
1061        assert_eq!(
1062            inline_props(rule_before(&dom)),
1063            declared(CSS_MATCH_15775557796860201720.as_ref()),
1064        );
1065        assert_eq!(
1066            inline_props(rule_after(&dom)),
1067            declared(CSS_MATCH_9156589477016488419.as_ref()),
1068        );
1069        assert_eq!(inline_props(title_node(&dom)), declared(CSS_MATCH_4236783900531286611.as_ref()));
1070    }
1071
1072    #[test]
1073    fn the_content_area_declares_flex_grow_first_then_the_static_block_verbatim() {
1074        let dom = default_frame().with_flex_grow(2.5).dom();
1075
1076        let mut expected = vec![CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1077            LayoutFlexGrow {
1078                inner: FloatValue::new(2.5),
1079            },
1080        ))];
1081        expected.extend(declared(CSS_MATCH_CONTENT_AREA_PROPERTIES));
1082
1083        assert_eq!(
1084            inline_props(content_area(&dom)),
1085            expected,
1086            "the content-area style block drifted from FlexGrow + the static constant",
1087        );
1088    }
1089
1090    #[test]
1091    fn no_node_declares_the_same_property_twice() {
1092        // A duplicate declaration means one of the two is silently dead — and which one
1093        // wins depends on cascade order, so the widget would style differently per node.
1094        let dom = frame("t", Dom::create_div()).with_flex_grow(1.0).dom();
1095        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
1096            let props = inline_props(node);
1097            let mut seen = HashSet::new();
1098            for p in &props {
1099                assert!(
1100                    seen.insert(p.get_type()),
1101                    "node {i} declares {:?} twice",
1102                    p.get_type(),
1103                );
1104            }
1105        }
1106    }
1107
1108    #[test]
1109    fn every_declaration_is_unconditional() {
1110        // The frame chrome has no :hover/@media/@os variants — a stray condition would
1111        // make part of the border render only in one state.
1112        let dom = frame("t", Dom::create_div()).dom();
1113        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
1114            for (p, conditions) in node.root.style.iter_inline_properties() {
1115                assert!(
1116                    conditions.as_ref().is_empty(),
1117                    "node {i} gates {:?} behind a dynamic selector",
1118                    p.get_type(),
1119                );
1120            }
1121        }
1122    }
1123
1124    #[test]
1125    fn all_frame_lengths_are_absolute_px() {
1126        let dom = frame("t", Dom::create_div()).dom();
1127        for node in all_nodes(&dom) {
1128            for p in inline_props(node) {
1129                if let Some(pv) = length_of(&p) {
1130                    let v = px(&pv); // asserts SizeMetric::Px
1131                    assert!(
1132                        v.is_finite() && (0.0..=64.0).contains(&v),
1133                        "{:?} = {v}px is outside the plausible frame-chrome range",
1134                        p.get_type(),
1135                    );
1136                }
1137            }
1138        }
1139    }
1140
1141    #[test]
1142    fn every_border_in_the_frame_uses_the_one_border_colour() {
1143        let dom = frame("t", Dom::create_div()).dom();
1144        let mut seen = 0_usize;
1145        for node in all_nodes(&dom) {
1146            for p in inline_props(node) {
1147                if let Some(c) = border_color_of(&p) {
1148                    assert_eq!(c, BORDER_COLOR, "{:?} uses an off-palette colour", p.get_type());
1149                    seen += 1;
1150                }
1151            }
1152        }
1153        // top+left on the left rule, top+right on the right rule, all four on the content
1154        // area.
1155        assert_eq!(seen, 8, "the number of coloured borders in the frame changed");
1156    }
1157
1158    #[test]
1159    fn the_content_area_is_fully_boxed_in_on_all_four_sides() {
1160        // A missing side would leave the group box visibly open.
1161        let props = inline_props(content_area(&frame("t", Dom::create_div()).dom()));
1162        let widths = [
1163            CssPropertyType::BorderTopWidth,
1164            CssPropertyType::BorderBottomWidth,
1165            CssPropertyType::BorderLeftWidth,
1166            CssPropertyType::BorderRightWidth,
1167        ];
1168        for want in widths {
1169            let found = props
1170                .iter()
1171                .find(|p| p.get_type() == want)
1172                .unwrap_or_else(|| panic!("the content area is missing {want:?}"));
1173            assert_eq!(
1174                length_of(found).map(|pv| px(&pv)),
1175                Some(1.0),
1176                "{want:?} is not a 1px hairline",
1177            );
1178        }
1179    }
1180
1181    #[test]
1182    fn the_two_header_rules_are_symmetric_apart_from_the_left_rule_carrying_a_width() {
1183        // Pinned as-is: `.__azul-native-frame-header-before` declares `width: 5px` while
1184        // `-after` declares no width at all, so the stub of rule left of the title is
1185        // capped at 5px while the right-hand rule is only bounded by flex-grow. This is
1186        // an asymmetry in the widget's style constants, not in the code under test —
1187        // recorded here so a deliberate change to either side shows up as a failure.
1188        let dom = frame("t", Dom::create_div()).dom();
1189        let before: Vec<CssPropertyType> =
1190            inline_props(rule_before(&dom)).iter().map(CssProperty::get_type).collect();
1191        let after: Vec<CssPropertyType> =
1192            inline_props(rule_after(&dom)).iter().map(CssProperty::get_type).collect();
1193
1194        assert!(before.contains(&CssPropertyType::Width), "the left rule lost its width cap");
1195        assert!(
1196            !after.contains(&CssPropertyType::Width),
1197            "the right rule gained a width — the known before/after asymmetry was fixed, \
1198             update this test",
1199        );
1200
1201        // Mirrored side: left draws a left border, right draws a right border.
1202        assert!(before.contains(&CssPropertyType::BorderLeftWidth));
1203        assert!(after.contains(&CssPropertyType::BorderRightWidth));
1204
1205        // Everything else is shared, in the same order.
1206        for shared in [
1207            CssPropertyType::Height,
1208            CssPropertyType::BorderTopWidth,
1209            CssPropertyType::BorderTopStyle,
1210            CssPropertyType::BorderTopColor,
1211            CssPropertyType::MarginTop,
1212            CssPropertyType::FlexGrow,
1213            CssPropertyType::FlexDirection,
1214        ] {
1215            assert!(before.contains(&shared), "the left rule is missing {shared:?}");
1216            assert!(after.contains(&shared), "the right rule is missing {shared:?}");
1217        }
1218    }
1219
1220    #[test]
1221    fn the_inner_rule_divs_carry_no_style_of_their_own() {
1222        // Pinned as-is: the constants comment their `... -header-before div` properties
1223        // (height + borders) separately from the container's, but both blocks are applied
1224        // to the *container* — the inner div is an unstyled, empty leaf. Recorded so that
1225        // re-splitting the two selectors shows up here instead of silently changing the
1226        // rendered header.
1227        let dom = frame("t", Dom::create_div()).dom();
1228        for inner in [&kids(rule_before(&dom))[0], &kids(rule_after(&dom))[0]] {
1229            assert!(
1230                inline_props(inner).is_empty(),
1231                "the inner rule div grew inline properties",
1232            );
1233            assert!(kids(inner).is_empty(), "the inner rule div grew children");
1234            assert_eq!(inner.root.get_node_type(), &NodeType::Div);
1235        }
1236
1237        // ... and the height/border pair really does sit on the containers.
1238        for outer in [rule_before(&dom), rule_after(&dom)] {
1239            let types: Vec<CssPropertyType> =
1240                inline_props(outer).iter().map(CssProperty::get_type).collect();
1241            assert!(types.contains(&CssPropertyType::Height));
1242            assert!(types.contains(&CssPropertyType::BorderTopWidth));
1243        }
1244    }
1245
1246    #[test]
1247    fn the_frame_is_inert_and_attaches_no_callbacks_or_stylesheets() {
1248        let dom = frame("t", Dom::create_div()).with_flex_grow(1.0).dom();
1249        for (i, node) in all_nodes(&dom).into_iter().enumerate() {
1250            assert!(
1251                node.root.callbacks.as_ref().is_empty(),
1252                "node {i} of a stateless frame carries a callback",
1253            );
1254            assert!(
1255                node.css.as_ref().is_empty(),
1256                "node {i} attached a whole stylesheet instead of inline props",
1257            );
1258        }
1259    }
1260
1261    #[test]
1262    fn dom_moves_the_content_in_verbatim_including_its_own_classes_and_children() {
1263        let content = Dom::create_div()
1264            .with_ids_and_classes(IdOrClassVec::from_vec(vec![Class(AzString::from(
1265                "user-content",
1266            ))]))
1267            .with_children(DomVec::from_vec(vec![
1268                Dom::create_text("a"),
1269                Dom::create_text("b"),
1270            ]));
1271        let expected = content.clone();
1272
1273        let dom = frame("t", content).dom();
1274        let placed = &kids(content_area(&dom))[0];
1275
1276        assert_eq!(*placed, expected, "the content DOM was rewritten on the way in");
1277        assert_eq!(classes(placed), vec!["user-content".to_string()]);
1278        assert_eq!(kids(placed).len(), 2);
1279    }
1280
1281    #[test]
1282    fn dom_is_deterministic_for_equal_inputs() {
1283        for t in ADVERSARIAL_TEXT {
1284            for v in ADVERSARIAL_FLOATS {
1285                let a = frame(t, Dom::create_text(t)).with_flex_grow(v).dom();
1286                let b = frame(t, Dom::create_text(t)).with_flex_grow(v).dom();
1287                assert_eq!(a, b, "two identical frames rendered differently for {t:?} / {v}");
1288            }
1289        }
1290    }
1291
1292    #[test]
1293    fn distinct_factors_that_encode_differently_produce_distinct_doms() {
1294        // The counterpart to the determinism test: the flex factor must actually reach
1295        // the tree, so a change in it has to be observable in the rendered DOM.
1296        let a = default_frame().with_flex_grow(1.0).dom();
1297        let b = default_frame().with_flex_grow(2.0).dom();
1298        assert_ne!(a, b, "the flex factor never reached the rendered DOM");
1299
1300        // ... while two factors that collapse to the same encoding are indistinguishable.
1301        let nan = default_frame().with_flex_grow(f32::NAN).dom();
1302        let zero = default_frame().with_flex_grow(0.0).dom();
1303        assert_eq!(nan, zero, "NaN and 0.0 stopped encoding to the same style tree");
1304    }
1305
1306    #[test]
1307    fn the_title_is_styled_the_same_no_matter_what_it_contains() {
1308        let baseline = declared(CSS_MATCH_4236783900531286611.as_ref());
1309        for t in ADVERSARIAL_TEXT {
1310            let dom = frame(t, Dom::create_div()).dom();
1311            assert_eq!(
1312                inline_props(title_node(&dom)),
1313                baseline,
1314                "the title style changed for input {t:?}",
1315            );
1316            assert_eq!(kids(header(&dom)).len(), 3, "the header shape changed for input {t:?}");
1317            assert_eq!(dom.estimated_total_children, 8, "the node count changed for {t:?}");
1318        }
1319    }
1320}