Skip to main content

azul_layout/widgets/
divider.rs

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
7use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
8use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
9use 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)]
22pub 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)]
33pub 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.
39const DIVIDER_COLOR: ColorU = ColorU {
40    r: 221,
41    g: 221,
42    b: 221,
43    a: 255,
44};
45const DIVIDER_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(DIVIDER_COLOR)];
46const DIVIDER_BG: StyleBackgroundContentVec =
47    StyleBackgroundContentVec::from_const_slice(DIVIDER_BG_ITEMS);
48
49static 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
62static 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
75impl Divider {
76    /// Creates a new horizontal divider with default styling.
77    #[inline]
78    #[must_use] pub fn create() -> Self {
79        Self::create_with_orientation(DividerOrientation::Horizontal)
80    }
81
82    /// Creates a new divider with the given orientation and default styling.
83    #[inline]
84    #[must_use] pub fn create_with_orientation(orientation: DividerOrientation) -> Self {
85        let divider_style = match orientation {
86            DividerOrientation::Horizontal => {
87                CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_HORIZONTAL)
88            }
89            DividerOrientation::Vertical => {
90                CssPropertyWithConditionsVec::from_const_slice(DIVIDER_STYLE_VERTICAL)
91            }
92        };
93        Self {
94            orientation,
95            divider_style,
96        }
97    }
98
99    /// Sets the orientation, resetting the style to the matching default.
100    #[inline]
101    pub fn set_orientation(&mut self, orientation: DividerOrientation) {
102        *self = Self::create_with_orientation(orientation);
103    }
104
105    /// Builder-style setter for the orientation.
106    #[inline]
107    #[must_use] pub fn with_orientation(mut self, orientation: DividerOrientation) -> Self {
108        self.set_orientation(orientation);
109        self
110    }
111
112    /// Replaces `self` with a default horizontal divider and returns the original.
113    #[inline]
114    #[must_use] pub fn swap_with_default(&mut self) -> Self {
115        let mut s = Self::create();
116        core::mem::swap(&mut s, self);
117        s
118    }
119
120    /// Converts this divider into a DOM node with the `__azul-native-divider` class.
121    #[inline]
122    #[must_use] pub fn dom(self) -> Dom {
123        static DIVIDER_CLASS: &[IdOrClass] =
124            &[Class(AzString::from_const_str("__azul-native-divider"))];
125
126        Dom::create_div()
127            .with_ids_and_classes(IdOrClassVec::from_const_slice(DIVIDER_CLASS))
128            .with_css_props(self.divider_style)
129    }
130}
131
132impl Default for Divider {
133    fn default() -> Self {
134        Self::create()
135    }
136}
137
138impl From<Divider> for Dom {
139    fn from(d: Divider) -> Self {
140        d.dom()
141    }
142}
143
144#[cfg(test)]
145mod 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, IdOrClass::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                IdOrClass::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}