1
//! Divider (separator) widget — a thin rule line. A stateless single styled
2
//! node with no callback, a near-clone of [`crate::widgets::label::Label`].
3
//! Supports a horizontal (default) or vertical orientation.
4
//!
5
//! Key types: [`Divider`], [`DividerOrientation`].
6

            
7
use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
8
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
9
use azul_css::{
10
    props::{
11
        basic::ColorU,
12
        layout::{LayoutDisplay, LayoutHeight, LayoutAlignSelf, LayoutFlexGrow, LayoutMarginTop, LayoutMarginBottom, LayoutWidth, LayoutMarginLeft, LayoutMarginRight},
13
        property::{CssProperty, *},
14
        style::{StyleBackgroundContent, StyleBackgroundContentVec},
15
    },
16
    AzString,
17
};
18

            
19
/// Orientation of a [`Divider`].
20
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
21
#[repr(C)]
22
pub enum DividerOrientation {
23
    /// A full-width horizontal rule (1px tall) — the default.
24
    #[default]
25
    Horizontal,
26
    /// A full-height vertical rule (1px wide).
27
    Vertical,
28
}
29

            
30
/// A thin separator rule. Stateless; renders a single styled `div`.
31
#[derive(Debug, Clone, PartialEq, Eq)]
32
#[repr(C)]
33
pub struct Divider {
34
    pub orientation: DividerOrientation,
35
    pub divider_style: CssPropertyWithConditionsVec,
36
}
37

            
38
/// Default rule colour (#dddddd), matching the frame widget's border colour.
39
const DIVIDER_COLOR: ColorU = ColorU {
40
    r: 221,
41
    g: 221,
42
    b: 221,
43
    a: 255,
44
};
45
const DIVIDER_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(DIVIDER_COLOR)];
46
const DIVIDER_BG: StyleBackgroundContentVec =
47
    StyleBackgroundContentVec::from_const_slice(DIVIDER_BG_ITEMS);
48

            
49
static DIVIDER_STYLE_HORIZONTAL: &[CssPropertyWithConditions] = &[
50
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
51
    CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(1))),
52
    // Stretch across the parent's cross axis so the rule spans the full width.
53
    CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Stretch)),
54
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
55
    CssPropertyWithConditions::simple(CssProperty::const_margin_top(LayoutMarginTop::const_px(4))),
56
    CssPropertyWithConditions::simple(CssProperty::const_margin_bottom(
57
        LayoutMarginBottom::const_px(4),
58
    )),
59
    CssPropertyWithConditions::simple(CssProperty::const_background_content(DIVIDER_BG)),
60
];
61

            
62
static DIVIDER_STYLE_VERTICAL: &[CssPropertyWithConditions] = &[
63
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
64
    CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(1))),
65
    // Stretch across the parent's cross axis so the rule spans the full height.
66
    CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Stretch)),
67
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
68
    CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(4))),
69
    CssPropertyWithConditions::simple(CssProperty::const_margin_right(
70
        LayoutMarginRight::const_px(4),
71
    )),
72
    CssPropertyWithConditions::simple(CssProperty::const_background_content(DIVIDER_BG)),
73
];
74

            
75
impl Divider {
76
    /// Creates a new horizontal divider with default styling.
77
    #[inline]
78
831
    #[must_use] pub fn create() -> Self {
79
831
        Self::create_with_orientation(DividerOrientation::Horizontal)
80
831
    }
81

            
82
    /// Creates a new divider with the given orientation and default styling.
83
    #[inline]
84
1795
    #[must_use] pub fn create_with_orientation(orientation: DividerOrientation) -> Self {
85
1795
        let divider_style = match orientation {
86
            DividerOrientation::Horizontal => {
87
1307
                CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_HORIZONTAL)
88
            }
89
            DividerOrientation::Vertical => {
90
488
                CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_VERTICAL)
91
            }
92
        };
93
1795
        Self {
94
1795
            orientation,
95
1795
            divider_style,
96
1795
        }
97
1795
    }
98

            
99
    /// Sets the orientation, resetting the style to the matching default.
100
    #[inline]
101
718
    pub fn set_orientation(&mut self, orientation: DividerOrientation) {
102
718
        *self = Self::create_with_orientation(orientation);
103
718
    }
104

            
105
    /// Builder-style setter for the orientation.
106
    #[inline]
107
505
    #[must_use] pub fn with_orientation(mut self, orientation: DividerOrientation) -> Self {
108
505
        self.set_orientation(orientation);
109
505
        self
110
505
    }
111

            
112
    /// Replaces `self` with a default horizontal divider and returns the original.
113
    #[inline]
114
104
    #[must_use] pub fn swap_with_default(&mut self) -> Self {
115
104
        let mut s = Self::create();
116
104
        core::mem::swap(&mut s, self);
117
104
        s
118
104
    }
119

            
120
    /// Converts this divider into a DOM node with the `__azul-native-divider` class.
121
    #[inline]
122
517
    #[must_use] pub fn dom(self) -> Dom {
123
        static DIVIDER_CLASS: &[IdOrClass] =
124
            &[Class(AzString::from_const_str("__azul-native-divider"))];
125

            
126
517
        Dom::create_div()
127
517
            .with_ids_and_classes(IdOrClassVec::from_const_slice(DIVIDER_CLASS))
128
517
            .with_css_props(self.divider_style)
129
517
    }
130
}
131

            
132
impl Default for Divider {
133
206
    fn default() -> Self {
134
206
        Self::create()
135
206
    }
136
}
137

            
138
impl From<Divider> for Dom {
139
2
    fn from(d: Divider) -> Self {
140
2
        d.dom()
141
2
    }
142
}
143

            
144
#[cfg(test)]
145
mod autotest_generated {
146
    use std::collections::HashSet;
147

            
148
    use azul_core::dom::NodeType;
149
    use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
150

            
151
    use super::*;
152

            
153
    // ------------------------------------------------------------------
154
    // Helpers
155
    // ------------------------------------------------------------------
156

            
157
    /// Every variant of `DividerOrientation` — the complete input domain of
158
    /// `create_with_orientation`, `set_orientation` and `with_orientation`.
159
    const ALL_ORIENTATIONS: [DividerOrientation; 2] =
160
        [DividerOrientation::Horizontal, DividerOrientation::Vertical];
161

            
162
    /// The number of declarations each built-in style is expected to carry.
163
    const DECL_COUNT: usize = 7;
164

            
165
    /// The declared properties of a style vec, in declaration order.
166
    fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
167
        v.as_ref().iter().map(|p| p.property.clone()).collect()
168
    }
169

            
170
    /// The `f32` of a `PixelValue`, asserting it is an absolute `px` length. A
171
    /// rule declared in `em`/`%` would resolve against the parent font/box and
172
    /// either vanish or blow up instead of staying a hairline.
173
    fn px(pv: &PixelValue) -> f32 {
174
        assert_eq!(pv.metric, SizeMetric::Px, "divider lengths must be absolute px, got {:?}", pv.metric);
175
        pv.number.get()
176
    }
177

            
178
    fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
179
        v.as_ref().iter().find_map(|p| match &p.property {
180
            CssProperty::Height(h) => match h.get_property() {
181
                Some(LayoutHeight::Px(pv)) => Some(px(pv)),
182
                Some(other) => panic!("divider height must be a px length, got {other:?}"),
183
                None => None,
184
            },
185
            _ => None,
186
        })
187
    }
188

            
189
    fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
190
        v.as_ref().iter().find_map(|p| match &p.property {
191
            CssProperty::Width(w) => match w.get_property() {
192
                Some(LayoutWidth::Px(pv)) => Some(px(pv)),
193
                Some(other) => panic!("divider width must be a px length, got {other:?}"),
194
                None => None,
195
            },
196
            _ => None,
197
        })
198
    }
199

            
200
    /// The four margins in `(top, bottom, left, right)` order.
201
    fn margins_px(
202
        v: &CssPropertyWithConditionsVec,
203
    ) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
204
        let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
205
        (
206
            find(&|p| match p {
207
                CssProperty::MarginTop(x) => x.get_property().map(|x| px(&x.inner)),
208
                _ => None,
209
            }),
210
            find(&|p| match p {
211
                CssProperty::MarginBottom(x) => x.get_property().map(|x| px(&x.inner)),
212
                _ => None,
213
            }),
214
            find(&|p| match p {
215
                CssProperty::MarginLeft(x) => x.get_property().map(|x| px(&x.inner)),
216
                _ => None,
217
            }),
218
            find(&|p| match p {
219
                CssProperty::MarginRight(x) => x.get_property().map(|x| px(&x.inner)),
220
                _ => None,
221
            }),
222
        )
223
    }
224

            
225
    fn flex_grow(v: &CssPropertyWithConditionsVec) -> Option<f32> {
226
        v.as_ref().iter().find_map(|p| match &p.property {
227
            CssProperty::FlexGrow(f) => f.get_property().map(|f| f.inner.get()),
228
            _ => None,
229
        })
230
    }
231

            
232
    /// The single background layer of a style vec, asserting there is exactly one
233
    /// and that it is a flat colour (a gradient would not be a `Color`).
234
    fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
235
        let bg = v.as_ref().iter().find_map(|p| match &p.property {
236
            CssProperty::BackgroundContent(b) => b.get_property(),
237
            _ => None,
238
        })?;
239
        assert_eq!(bg.as_ref().len(), 1, "a divider must declare exactly one background layer");
240
        match &bg.as_ref()[0] {
241
            StyleBackgroundContent::Color(c) => Some(*c),
242
            other => panic!("divider background is not a flat colour: {other:?}"),
243
        }
244
    }
245

            
246
    /// Every `PixelValue` a style vec mentions (thickness + margins).
247
    fn all_pixel_values(v: &CssPropertyWithConditionsVec) -> Vec<PixelValue> {
248
        v.as_ref()
249
            .iter()
250
            .filter_map(|p| match &p.property {
251
                CssProperty::Height(h) => match h.get_property() {
252
                    Some(LayoutHeight::Px(pv)) => Some(*pv),
253
                    _ => None,
254
                },
255
                CssProperty::Width(w) => match w.get_property() {
256
                    Some(LayoutWidth::Px(pv)) => Some(*pv),
257
                    _ => None,
258
                },
259
                CssProperty::MarginTop(x) => x.get_property().map(|x| x.inner),
260
                CssProperty::MarginBottom(x) => x.get_property().map(|x| x.inner),
261
                CssProperty::MarginLeft(x) => x.get_property().map(|x| x.inner),
262
                CssProperty::MarginRight(x) => x.get_property().map(|x| x.inner),
263
                _ => None,
264
            })
265
            .collect()
266
    }
267

            
268
    /// True if `node` carries the CSS class `name`.
269
    fn has_class(node: &Dom, name: &str) -> bool {
270
        node.root
271
            .get_ids_and_classes()
272
            .as_ref()
273
            .iter()
274
            .any(|c| matches!(c, Class(s) if s.as_str() == name))
275
    }
276

            
277
    /// The properties of a rendered node's *inline* style, in declaration order.
278
    fn inline_properties(node: &Dom) -> Vec<CssProperty> {
279
        node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
280
    }
281

            
282
    /// `(property, number-of-conditions)` for a rendered node, in declaration order.
283
    fn inline_properties_with_condition_counts(node: &Dom) -> Vec<(CssProperty, usize)> {
284
        node.root
285
            .style
286
            .iter_inline_properties()
287
            .map(|(p, c)| (p.clone(), c.as_ref().len()))
288
            .collect()
289
    }
290

            
291
    // ------------------------------------------------------------------
292
    // DividerOrientation
293
    // ------------------------------------------------------------------
294

            
295
    #[test]
296
    fn orientation_default_is_horizontal_and_the_two_variants_are_distinct() {
297
        assert_eq!(DividerOrientation::default(), DividerOrientation::Horizontal);
298
        assert_ne!(DividerOrientation::Horizontal, DividerOrientation::Vertical);
299
        // `Copy` must not alias into something else: a copy compares equal to the
300
        // original and to itself.
301
        for o in ALL_ORIENTATIONS {
302
            let copy = o;
303
            assert_eq!(o, copy, "{o:?}: a copy diverged from the original");
304
        }
305
        assert_eq!(ALL_ORIENTATIONS.len(), 2, "a new orientation was added without updating these tests");
306
    }
307

            
308
    // ------------------------------------------------------------------
309
    // Divider::create / Divider::create_with_orientation  (constructors)
310
    // ------------------------------------------------------------------
311

            
312
    #[test]
313
    fn create_is_the_horizontal_constructor_and_the_default() {
314
        let created = Divider::create();
315
        assert_eq!(created.orientation, DividerOrientation::Horizontal);
316
        assert_eq!(created, Divider::create_with_orientation(DividerOrientation::Horizontal));
317
        assert_eq!(created, Divider::default());
318
        // `Clone` must preserve equality — the style vec is backed by a `'static`
319
        // slice, so a shallow/deep clone mix-up would show up here first.
320
        assert_eq!(created.clone(), created);
321
    }
322

            
323
    #[test]
324
    fn create_with_orientation_stores_exactly_the_orientation_it_was_given() {
325
        for o in ALL_ORIENTATIONS {
326
            let d = Divider::create_with_orientation(o);
327
            assert_eq!(d.orientation, o, "{o:?}: the orientation field does not match the argument");
328
            // Two builds of the same orientation must be indistinguishable.
329
            assert_eq!(d, Divider::create_with_orientation(o), "{o:?}: construction is not deterministic");
330
        }
331
    }
332

            
333
    #[test]
334
    fn constructed_style_vecs_have_consistent_length_and_capacity() {
335
        for o in ALL_ORIENTATIONS {
336
            let d = Divider::create_with_orientation(o);
337
            let v = &d.divider_style;
338
            assert_eq!(v.len(), v.as_ref().len(), "{o:?}: len() disagrees with the slice view");
339
            assert!(v.capacity() >= v.len(), "{o:?}: capacity {} < len {}", v.capacity(), v.len());
340
            assert!(!v.is_empty(), "{o:?}: a divider with no declarations paints nothing");
341
            assert_eq!(v.len(), DECL_COUNT, "{o:?}: unexpected number of declarations");
342
        }
343
    }
344

            
345
    // ------------------------------------------------------------------
346
    // The two built-in styles
347
    // ------------------------------------------------------------------
348

            
349
    #[test]
350
    fn horizontal_style_is_a_one_pixel_tall_rule_with_vertical_breathing_room() {
351
        let style = Divider::create().divider_style;
352

            
353
        assert_eq!(height_px(&style), Some(1.0), "a horizontal rule must be exactly 1px tall");
354
        // A declared width would pin the rule to a fixed size and defeat the
355
        // `align-self: stretch` that is supposed to span the parent.
356
        assert_eq!(width_px(&style), None, "a horizontal rule must not declare a width");
357
        assert_eq!(
358
            margins_px(&style),
359
            (Some(4.0), Some(4.0), None, None),
360
            "a horizontal rule takes 4px above/below and nothing on the sides"
361
        );
362
    }
363

            
364
    #[test]
365
    fn vertical_style_is_a_one_pixel_wide_rule_with_horizontal_breathing_room() {
366
        let style = Divider::create_with_orientation(DividerOrientation::Vertical).divider_style;
367

            
368
        assert_eq!(width_px(&style), Some(1.0), "a vertical rule must be exactly 1px wide");
369
        // The copy-paste hazard this widget is most exposed to: a `height: 1px`
370
        // left over from the horizontal style would collapse the vertical rule
371
        // into a 1x1 dot.
372
        assert_eq!(height_px(&style), None, "a vertical rule must not declare a height");
373
        assert_eq!(
374
            margins_px(&style),
375
            (None, None, Some(4.0), Some(4.0)),
376
            "a vertical rule takes 4px left/right and nothing above/below"
377
        );
378
    }
379

            
380
    #[test]
381
    fn both_orientations_share_the_colour_and_the_box_model_flags() {
382
        for o in ALL_ORIENTATIONS {
383
            let style = Divider::create_with_orientation(o).divider_style;
384
            let props = properties(&style);
385
            let has = |p: &CssProperty| props.contains(p);
386

            
387
            assert!(has(&CssProperty::const_display(LayoutDisplay::Block)), "{o:?}: not a block box");
388
            // Both halves of the "span the parent" contract: stretch on the cross
389
            // axis, no growth on the main axis.
390
            assert!(has(&CssProperty::align_self(LayoutAlignSelf::Stretch)), "{o:?}: rule does not stretch");
391
            assert!(
392
                has(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
393
                "{o:?}: rule grows on the main axis and would eat sibling space"
394
            );
395
            assert_eq!(background_color(&style), Some(DIVIDER_COLOR), "{o:?}: wrong rule colour");
396
        }
397
    }
398

            
399
    #[test]
400
    fn the_rule_colour_is_the_documented_opaque_grey() {
401
        // A translucent or non-grey rule is a visible regression, and the doc
402
        // comment pins it to #dddddd.
403
        assert_eq!(DIVIDER_COLOR, ColorU { r: 221, g: 221, b: 221, a: 255 });
404
        assert_eq!(DIVIDER_COLOR.a, 255, "a translucent rule lets the background bleed through");
405
        assert_eq!(DIVIDER_COLOR.r, DIVIDER_COLOR.g, "the rule colour is not neutral grey");
406
        assert_eq!(DIVIDER_COLOR.g, DIVIDER_COLOR.b, "the rule colour is not neutral grey");
407
        assert_eq!(DIVIDER_BG_ITEMS.len(), 1, "the rule must be a single flat layer");
408
    }
409

            
410
    #[test]
411
    fn orientation_actually_changes_the_emitted_style() {
412
        // If the two static tables were ever wired to the same slice the
413
        // `orientation` argument would silently become a no-op.
414
        let h = Divider::create_with_orientation(DividerOrientation::Horizontal).divider_style;
415
        let v = Divider::create_with_orientation(DividerOrientation::Vertical).divider_style;
416
        assert_ne!(properties(&h), properties(&v), "both orientations produce an identical style");
417
        assert_eq!(h.len(), v.len(), "the two orientations declare a different number of properties");
418

            
419
        // The axis-defining declarations must be mirror images of each other.
420
        assert!(height_px(&h).is_some() && width_px(&h).is_none());
421
        assert!(width_px(&v).is_some() && height_px(&v).is_none());
422
        assert_eq!(height_px(&h), width_px(&v), "the two rules are not the same thickness");
423
    }
424

            
425
    #[test]
426
    fn every_declaration_is_unconditional() {
427
        // A divider is stateless — a declaration gated on `:hover`/`:active`
428
        // would simply never paint.
429
        for o in ALL_ORIENTATIONS {
430
            for p in Divider::create_with_orientation(o).divider_style.as_ref() {
431
                assert!(
432
                    p.apply_if.as_ref().is_empty(),
433
                    "{o:?}: {:?} is conditional on a stateless widget",
434
                    p.property
435
                );
436
            }
437
        }
438
    }
439

            
440
    #[test]
441
    fn no_property_is_declared_twice() {
442
        // A duplicated declaration is a last-one-wins ambiguity: two heights or
443
        // two backgrounds would make one of them silently dead.
444
        for o in ALL_ORIENTATIONS {
445
            let props = properties(&Divider::create_with_orientation(o).divider_style);
446
            let mut seen = HashSet::new();
447
            for p in &props {
448
                assert!(seen.insert(core::mem::discriminant(p)), "{o:?}: duplicate declaration of {p:?}");
449
            }
450
            assert_eq!(seen.len(), props.len());
451
        }
452
    }
453

            
454
    #[test]
455
    fn every_length_is_a_finite_non_negative_absolute_px() {
456
        // Guard the `isize` -> `PixelValue` conversions: a NaN/inf/negative
457
        // length must never reach the layout solver.
458
        for o in ALL_ORIENTATIONS {
459
            let values = all_pixel_values(&Divider::create_with_orientation(o).divider_style);
460
            assert_eq!(values.len(), 3, "{o:?}: expected one thickness + two margins");
461
            for pv in values {
462
                let n = px(&pv); // also asserts SizeMetric::Px
463
                assert!(n.is_finite(), "{o:?}: non-finite length {n}");
464
                assert!(!n.is_nan(), "{o:?}: NaN length");
465
                assert!(n >= 0.0, "{o:?}: negative length {n}");
466
                assert!(n <= 64.0, "{o:?}: implausibly large length {n} for a hairline rule");
467
            }
468
        }
469
    }
470

            
471
    #[test]
472
    fn the_rule_is_thick_enough_to_be_visible_and_thin_enough_to_be_a_rule() {
473
        for o in ALL_ORIENTATIONS {
474
            let style = Divider::create_with_orientation(o).divider_style;
475
            let thickness = height_px(&style)
476
                .or_else(|| width_px(&style))
477
                .expect("a divider must declare a thickness on one axis");
478
            assert!(thickness > 0.0, "{o:?}: a 0px rule is invisible");
479
            assert!(thickness <= 4.0, "{o:?}: {thickness}px is a bar, not a rule");
480
        }
481
    }
482

            
483
    #[test]
484
    fn flex_grow_is_exactly_zero_and_not_a_rounding_artefact() {
485
        // `FloatValue` stores a fixed-point `isize`; a botched encode/decode
486
        // would show up as 0.001 or -0.0 rather than a clean 0.
487
        for o in ALL_ORIENTATIONS {
488
            let g = flex_grow(&Divider::create_with_orientation(o).divider_style)
489
                .expect("flex-grow must be declared");
490
            assert!(g.is_finite(), "{o:?}: non-finite flex-grow {g}");
491
            assert_eq!(g, 0.0, "{o:?}: flex-grow is {g}, not 0");
492
            assert!(g.is_sign_positive(), "{o:?}: flex-grow decoded as -0.0");
493
        }
494
    }
495

            
496
    #[test]
497
    fn the_fixed_point_length_encoding_round_trips() {
498
        // encode == decode for every constant this file bakes in.
499
        assert_eq!(PixelValue::const_px(1).number.get(), 1.0);
500
        assert_eq!(PixelValue::const_px(4).number.get(), 4.0);
501
        assert_eq!(LayoutFlexGrow::const_new(0).inner.get(), 0.0);
502

            
503
        // ...and the values that actually landed in the built styles are the
504
        // ones the constructors asked for.
505
        let h = properties(&Divider::create().divider_style);
506
        assert!(h.contains(&CssProperty::const_height(LayoutHeight::const_px(1))));
507
        assert!(h.contains(&CssProperty::const_margin_top(LayoutMarginTop::const_px(4))));
508
        assert!(h.contains(&CssProperty::const_margin_bottom(LayoutMarginBottom::const_px(4))));
509

            
510
        let v = properties(&Divider::create_with_orientation(DividerOrientation::Vertical).divider_style);
511
        assert!(v.contains(&CssProperty::const_width(LayoutWidth::const_px(1))));
512
        assert!(v.contains(&CssProperty::const_margin_left(LayoutMarginLeft::const_px(4))));
513
        assert!(v.contains(&CssProperty::const_margin_right(LayoutMarginRight::const_px(4))));
514
    }
515

            
516
    #[test]
517
    fn cloning_and_dropping_never_corrupts_the_shared_static_style() {
518
        // The style vec borrows a `'static` slice (`NoDestructor`). A clone that
519
        // wrongly claims ownership, or a `Drop` that frees the static, would
520
        // corrupt every future divider — so churn hard and re-check the source.
521
        for o in ALL_ORIENTATIONS {
522
            let base = Divider::create_with_orientation(o);
523
            let expected = properties(&base.divider_style);
524
            for round in 0..1000 {
525
                let c = base.clone();
526
                assert_eq!(properties(&c.divider_style), expected, "{o:?}: clone {round} diverged");
527
                drop(c);
528
            }
529
            assert_eq!(properties(&base.divider_style), expected, "{o:?}: the original was damaged");
530
            assert_eq!(
531
                properties(&Divider::create_with_orientation(o).divider_style),
532
                expected,
533
                "{o:?}: a freshly built divider disagrees after 1000 clone/drop cycles"
534
            );
535
        }
536
    }
537

            
538
    // ------------------------------------------------------------------
539
    // Divider::set_orientation
540
    // ------------------------------------------------------------------
541

            
542
    #[test]
543
    fn set_orientation_replaces_the_style_and_never_grows_it() {
544
        // A push-instead-of-replace bug would grow the vec on every flip and
545
        // leave the previous axis's `height`/`width` behind, turning the rule
546
        // into a 1x1 dot.
547
        let mut d = Divider::create();
548
        for round in 0..200 {
549
            let o = ALL_ORIENTATIONS[round % ALL_ORIENTATIONS.len()];
550
            d.set_orientation(o);
551

            
552
            assert_eq!(d.orientation, o, "round {round}: orientation field not updated");
553
            assert_eq!(d.divider_style.len(), DECL_COUNT, "round {round}: style vec changed length");
554
            assert_eq!(d, Divider::create_with_orientation(o), "round {round}: not equal to a fresh build");
555
            match o {
556
                DividerOrientation::Horizontal => {
557
                    assert_eq!(width_px(&d.divider_style), None, "round {round}: stale vertical width");
558
                    assert_eq!(height_px(&d.divider_style), Some(1.0), "round {round}");
559
                }
560
                DividerOrientation::Vertical => {
561
                    assert_eq!(height_px(&d.divider_style), None, "round {round}: stale horizontal height");
562
                    assert_eq!(width_px(&d.divider_style), Some(1.0), "round {round}");
563
                }
564
            }
565
        }
566
    }
567

            
568
    #[test]
569
    fn set_orientation_is_idempotent() {
570
        for o in ALL_ORIENTATIONS {
571
            let mut d = Divider::create_with_orientation(o);
572
            let before = d.clone();
573
            d.set_orientation(o);
574
            assert_eq!(d, before, "{o:?}: re-setting the same orientation changed the divider");
575
            d.set_orientation(o);
576
            assert_eq!(d, before, "{o:?}: the second re-set changed the divider");
577
        }
578
    }
579

            
580
    #[test]
581
    fn set_orientation_discards_a_custom_style_as_documented() {
582
        // Documented behaviour: "resetting the style to the matching default".
583
        // A caller who styled the rule loses that styling even when the
584
        // orientation does not change — assert it so the contract is explicit.
585
        let custom = CssPropertyWithConditionsVec::from_vec(vec![CssPropertyWithConditions::simple(
586
            CssProperty::const_height(LayoutHeight::const_px(42)),
587
        )]);
588
        let mut d = Divider {
589
            orientation: DividerOrientation::Horizontal,
590
            divider_style: custom,
591
        };
592
        d.set_orientation(DividerOrientation::Horizontal);
593
        assert_eq!(d, Divider::create(), "the custom style survived a same-orientation reset");
594
        assert_eq!(height_px(&d.divider_style), Some(1.0), "the 42px override was not discarded");
595
    }
596

            
597
    #[test]
598
    fn set_orientation_heals_a_hand_built_inconsistent_divider() {
599
        // Both fields are `pub`, so a caller can construct a divider whose
600
        // `orientation` contradicts its `divider_style`. The setter must
601
        // rebuild both, not just stamp the enum.
602
        let mut desynced = Divider {
603
            orientation: DividerOrientation::Vertical,
604
            divider_style: CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_HORIZONTAL),
605
        };
606
        assert_ne!(
607
            desynced,
608
            Divider::create_with_orientation(DividerOrientation::Vertical),
609
            "the desynced divider was expected to differ from a canonical one"
610
        );
611
        desynced.set_orientation(DividerOrientation::Vertical);
612
        assert_eq!(desynced, Divider::create_with_orientation(DividerOrientation::Vertical));
613
        assert_eq!(height_px(&desynced.divider_style), None, "the horizontal height survived");
614
    }
615

            
616
    // ------------------------------------------------------------------
617
    // Divider::with_orientation  (constructor / builder)
618
    // ------------------------------------------------------------------
619

            
620
    #[test]
621
    fn with_orientation_invariants_hold_for_every_orientation() {
622
        for o in ALL_ORIENTATIONS {
623
            let d = Divider::create().with_orientation(o);
624
            assert_eq!(d.orientation, o, "{o:?}: field does not match the argument");
625
            assert_eq!(d, Divider::create_with_orientation(o), "{o:?}: builder != constructor");
626
            assert_eq!(d.divider_style.len(), d.divider_style.as_ref().len());
627
            assert!(d.divider_style.capacity() >= d.divider_style.len());
628
            assert_eq!(d.divider_style.len(), DECL_COUNT);
629
        }
630
    }
631

            
632
    #[test]
633
    fn with_orientation_agrees_with_set_orientation_and_is_last_call_wins() {
634
        let chained = Divider::create()
635
            .with_orientation(DividerOrientation::Vertical)
636
            .with_orientation(DividerOrientation::Horizontal)
637
            .with_orientation(DividerOrientation::Vertical);
638

            
639
        let mut mutated = Divider::create();
640
        mutated.set_orientation(DividerOrientation::Vertical);
641
        mutated.set_orientation(DividerOrientation::Horizontal);
642
        mutated.set_orientation(DividerOrientation::Vertical);
643

            
644
        assert_eq!(chained, mutated, "the builder and the mutator must agree");
645
        assert_eq!(chained.orientation, DividerOrientation::Vertical);
646
        // In particular the intermediate horizontal `height` must be gone.
647
        assert_eq!(height_px(&chained.divider_style), None, "a stale horizontal height survived the chain");
648
        assert_eq!(width_px(&chained.divider_style), Some(1.0));
649
    }
650

            
651
    #[test]
652
    fn a_long_builder_chain_does_not_accumulate_declarations() {
653
        let mut d = Divider::create();
654
        for round in 0..500 {
655
            d = d.with_orientation(ALL_ORIENTATIONS[round % ALL_ORIENTATIONS.len()]);
656
            assert_eq!(d.divider_style.len(), DECL_COUNT, "round {round}: the style vec grew");
657
        }
658
        assert_eq!(d, Divider::create_with_orientation(DividerOrientation::Vertical));
659
    }
660

            
661
    // ------------------------------------------------------------------
662
    // Divider::swap_with_default
663
    // ------------------------------------------------------------------
664

            
665
    #[test]
666
    fn swap_with_default_returns_the_original_and_leaves_a_horizontal_default() {
667
        let mut d = Divider::create_with_orientation(DividerOrientation::Vertical);
668
        let taken = d.swap_with_default();
669

            
670
        // The returned value is the *original*, intact.
671
        assert_eq!(taken.orientation, DividerOrientation::Vertical);
672
        assert_eq!(width_px(&taken.divider_style), Some(1.0));
673
        assert_eq!(taken, Divider::create_with_orientation(DividerOrientation::Vertical));
674

            
675
        // What is left behind is a *default* — in particular the vertical
676
        // `width` must not have survived the swap.
677
        assert_eq!(d, Divider::default());
678
        assert_eq!(d.orientation, DividerOrientation::Horizontal);
679
        assert_eq!(width_px(&d.divider_style), None, "the vertical width survived the swap");
680
        assert_eq!(height_px(&d.divider_style), Some(1.0));
681
    }
682

            
683
    #[test]
684
    fn swap_with_default_is_idempotent_on_an_already_default_divider() {
685
        let mut d = Divider::default();
686
        let first = d.swap_with_default();
687
        let second = d.swap_with_default();
688
        assert_eq!(first, Divider::default());
689
        assert_eq!(second, Divider::default());
690
        assert_eq!(d, Divider::default());
691
    }
692

            
693
    #[test]
694
    fn repeated_swaps_never_corrupt_the_static_backed_style() {
695
        // `mem::swap` moves a vec that borrows a `'static` slice; 100 rounds of
696
        // swap-and-drop would surface a double free or a dangling `ptr`.
697
        let mut d = Divider::create_with_orientation(DividerOrientation::Vertical);
698
        for round in 0..100 {
699
            let taken = d.swap_with_default();
700
            if round == 0 {
701
                assert_eq!(taken.orientation, DividerOrientation::Vertical, "round 0: wrong value returned");
702
            } else {
703
                assert_eq!(taken, Divider::default(), "round {round}: the emptied slot was not a default");
704
            }
705
            assert_eq!(d, Divider::default(), "round {round}: what was left behind is not a default");
706
            assert_eq!(d.divider_style.len(), DECL_COUNT, "round {round}: the style vec changed length");
707
        }
708
    }
709

            
710
    #[test]
711
    fn swap_with_default_returns_a_custom_style_untouched() {
712
        let custom = CssPropertyWithConditionsVec::from_vec(vec![CssPropertyWithConditions::simple(
713
            CssProperty::const_width(LayoutWidth::const_px(9)),
714
        )]);
715
        let mut d = Divider {
716
            orientation: DividerOrientation::Vertical,
717
            divider_style: custom,
718
        };
719
        let taken = d.swap_with_default();
720
        assert_eq!(taken.orientation, DividerOrientation::Vertical);
721
        assert_eq!(taken.divider_style.len(), 1, "the custom style was rewritten on the way out");
722
        assert_eq!(width_px(&taken.divider_style), Some(9.0));
723
        assert_eq!(d, Divider::default());
724
    }
725

            
726
    // ------------------------------------------------------------------
727
    // Divider::dom  (round-trip: divider -> DOM)
728
    // ------------------------------------------------------------------
729

            
730
    #[test]
731
    fn dom_is_a_single_classed_div_with_no_children_or_callbacks() {
732
        for o in ALL_ORIENTATIONS {
733
            let divider = Divider::create_with_orientation(o);
734
            let expected = properties(&divider.divider_style);
735
            let dom = divider.dom();
736

            
737
            assert!(has_class(&dom, "__azul-native-divider"), "{o:?}: missing the widget class");
738
            assert_eq!(dom.root.get_node_type(), &NodeType::Div, "{o:?}: a rule must be a plain div");
739
            assert!(dom.children.as_ref().is_empty(), "{o:?}: a divider is a leaf, not a subtree");
740
            assert!(dom.root.callbacks.as_ref().is_empty(), "{o:?}: a stateless widget must not bind callbacks");
741
            assert_eq!(inline_properties(&dom), expected, "{o:?}: the rule lost its computed style");
742
            assert_eq!(
743
                dom.root.get_ids_and_classes().as_ref().len(),
744
                1,
745
                "{o:?}: expected exactly one class and no ids"
746
            );
747
        }
748
    }
749

            
750
    #[test]
751
    fn dom_renders_the_orientation_the_divider_was_last_set_to() {
752
        // `dom()` consumes the *cached* style, so a `set_orientation` that forgot
753
        // to recompute would paint the previous axis here and nowhere else.
754
        for o in ALL_ORIENTATIONS {
755
            let mut divider = Divider::create();
756
            divider.set_orientation(DividerOrientation::Vertical);
757
            divider.set_orientation(o);
758
            let expected = properties(&Divider::create_with_orientation(o).divider_style);
759
            assert_eq!(inline_properties(&divider.dom()), expected, "{o:?}: the DOM shows a stale axis");
760
        }
761
    }
762

            
763
    #[test]
764
    fn dom_of_the_two_orientations_differ_but_carry_the_same_class() {
765
        let h = Divider::create().dom();
766
        let v = Divider::create_with_orientation(DividerOrientation::Vertical).dom();
767

            
768
        assert_ne!(inline_properties(&h), inline_properties(&v), "both orientations render identically");
769
        // Both share one class, so a stylesheet cannot tell them apart by class
770
        // alone — pinned here so a future split is a deliberate change.
771
        assert!(has_class(&h, "__azul-native-divider"));
772
        assert!(has_class(&v, "__azul-native-divider"));
773
        assert_eq!(
774
            h.root.get_ids_and_classes().as_ref(),
775
            v.root.get_ids_and_classes().as_ref(),
776
            "the two orientations were expected to share their class list"
777
        );
778
    }
779

            
780
    #[test]
781
    fn the_widget_class_is_a_namespaced_ascii_css_identifier() {
782
        let dom = Divider::create().dom();
783
        let classes = dom.root.get_ids_and_classes();
784
        let name = classes
785
            .as_ref()
786
            .iter()
787
            .find_map(|c| match c {
788
                Class(s) => Some(s.as_str().to_string()),
789
                IdOrClass::Id(_) => None,
790
            })
791
            .expect("the divider must carry a class");
792

            
793
        assert_eq!(name, "__azul-native-divider");
794
        assert!(!name.is_empty(), "empty class name");
795
        assert!(name.is_ascii(), "non-ASCII class name {name:?}");
796
        assert!(name.starts_with("__azul-native-"), "unnamespaced class {name:?}");
797
        // A space, a dot or a `#` would silently split/re-target the selector.
798
        assert!(
799
            name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
800
            "class name {name:?} contains a CSS-significant character"
801
        );
802
    }
803

            
804
    #[test]
805
    fn from_divider_for_dom_is_exactly_dom() {
806
        for o in ALL_ORIENTATIONS {
807
            let divider = Divider::create_with_orientation(o);
808
            let via_into: Dom = divider.clone().into();
809
            let via_dom = divider.dom();
810
            assert_eq!(inline_properties(&via_into), inline_properties(&via_dom), "{o:?}: `From` diverges from `dom()`");
811
            assert_eq!(via_into.root.get_node_type(), via_dom.root.get_node_type(), "{o:?}: `From` built a different node");
812
            assert_eq!(
813
                via_into.root.get_ids_and_classes().as_ref(),
814
                via_dom.root.get_ids_and_classes().as_ref(),
815
                "{o:?}: `From` produced a different class list"
816
            );
817
        }
818
    }
819

            
820
    #[test]
821
    fn dom_renders_the_style_field_and_ignores_the_orientation_field() {
822
        // `dom()` only consumes `divider_style`. A hand-built divider whose
823
        // `orientation` contradicts its style therefore renders the *style* —
824
        // pinned so the divergence is documented rather than surprising.
825
        let desynced = Divider {
826
            orientation: DividerOrientation::Vertical,
827
            divider_style: CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_HORIZONTAL),
828
        };
829
        let rendered = inline_properties(&desynced.dom());
830
        assert_eq!(
831
            rendered,
832
            properties(&Divider::create().divider_style),
833
            "dom() did not render the style it was handed"
834
        );
835
    }
836

            
837
    #[test]
838
    fn dom_of_an_empty_style_is_still_a_classed_div() {
839
        // Boundary case: zero declarations. Must not panic and must keep the
840
        // class, otherwise the node becomes untargetable *and* invisible.
841
        let d = Divider {
842
            orientation: DividerOrientation::Horizontal,
843
            divider_style: CssPropertyWithConditionsVec::new(),
844
        };
845
        let dom = d.dom();
846
        assert!(has_class(&dom, "__azul-native-divider"));
847
        assert_eq!(dom.root.get_node_type(), &NodeType::Div);
848
        assert!(inline_properties(&dom).is_empty(), "properties appeared out of an empty style vec");
849
        assert!(dom.children.as_ref().is_empty());
850
    }
851

            
852
    #[test]
853
    fn dom_preserves_a_huge_custom_style_verbatim_and_in_order() {
854
        // 10k declarations through the `CssPropertyWithConditionsVec` -> `Css`
855
        // bridge: a bug that dropped, truncated or reordered entries (the very
856
        // failure that bridge's comment describes) would show up here.
857
        let big: Vec<CssPropertyWithConditions> = (0..10_000_isize)
858
            .map(|i| CssPropertyWithConditions::simple(CssProperty::const_margin_top(LayoutMarginTop::const_px(i))))
859
            .collect();
860
        let expected: Vec<CssProperty> = big.iter().map(|p| p.property.clone()).collect();
861

            
862
        let d = Divider {
863
            orientation: DividerOrientation::Horizontal,
864
            divider_style: CssPropertyWithConditionsVec::from_vec(big),
865
        };
866
        let rendered = inline_properties(&d.dom());
867
        assert_eq!(rendered.len(), 10_000, "declarations were dropped on the way into the DOM");
868
        assert_eq!(rendered, expected, "declaration order was not preserved");
869
    }
870

            
871
    #[test]
872
    fn dom_preserves_the_conditions_attached_to_each_declaration() {
873
        // The bridge promises "the original conditions intact"; a lost condition
874
        // would turn a `:hover` override into an unconditional one.
875
        let props = vec![
876
            CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
877
            CssPropertyWithConditions::on_hover(CssProperty::const_height(LayoutHeight::const_px(3))),
878
        ];
879
        let d = Divider {
880
            orientation: DividerOrientation::Horizontal,
881
            divider_style: CssPropertyWithConditionsVec::from_vec(props),
882
        };
883
        let pairs = inline_properties_with_condition_counts(&d.dom());
884
        assert_eq!(pairs.len(), 2, "a declaration was dropped");
885
        assert_eq!(pairs[0].0, CssProperty::const_display(LayoutDisplay::Block));
886
        assert_eq!(pairs[0].1, 0, "an unconditional declaration gained a condition");
887
        assert_eq!(pairs[1].0, CssProperty::const_height(LayoutHeight::const_px(3)));
888
        assert_eq!(pairs[1].1, 1, "the :hover condition was dropped");
889
    }
890

            
891
    #[test]
892
    fn building_many_doms_is_stable() {
893
        // Every divider borrows the same `'static` style table; building and
894
        // dropping many DOMs from it must not perturb later ones.
895
        let expected = properties(&Divider::create().divider_style);
896
        for round in 0..500 {
897
            let dom = Divider::create().dom();
898
            assert_eq!(inline_properties(&dom), expected, "round {round}: the shared style drifted");
899
            drop(dom);
900
        }
901
        assert_eq!(properties(&Divider::create().divider_style), expected);
902
    }
903

            
904
    // ------------------------------------------------------------------
905
    // Equality
906
    // ------------------------------------------------------------------
907

            
908
    #[test]
909
    fn equality_sees_both_fields() {
910
        assert_ne!(
911
            Divider::create_with_orientation(DividerOrientation::Horizontal),
912
            Divider::create_with_orientation(DividerOrientation::Vertical),
913
            "dividers of different orientation must not compare equal"
914
        );
915
        // Same orientation, different style => not equal.
916
        let styled = Divider {
917
            orientation: DividerOrientation::Horizontal,
918
            divider_style: CssPropertyWithConditionsVec::new(),
919
        };
920
        assert_ne!(styled, Divider::create(), "the style field must affect equality");
921
        // Same style, different orientation => not equal.
922
        let desynced = Divider {
923
            orientation: DividerOrientation::Vertical,
924
            divider_style: CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_HORIZONTAL),
925
        };
926
        assert_ne!(desynced, Divider::create(), "the orientation field must affect equality");
927
    }
928
}