1use 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
21#[repr(C)]
22pub enum DividerOrientation {
23 #[default]
25 Horizontal,
26 Vertical,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32#[repr(C)]
33pub struct Divider {
34 pub orientation: DividerOrientation,
35 pub divider_style: CssPropertyWithConditionsVec,
36}
37
38const 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 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 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 #[inline]
78 #[must_use] pub fn create() -> Self {
79 Self::create_with_orientation(DividerOrientation::Horizontal)
80 }
81
82 #[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 #[inline]
101 pub fn set_orientation(&mut self, orientation: DividerOrientation) {
102 *self = Self::create_with_orientation(orientation);
103 }
104
105 #[inline]
107 #[must_use] pub fn with_orientation(mut self, orientation: DividerOrientation) -> Self {
108 self.set_orientation(orientation);
109 self
110 }
111
112 #[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 #[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 const ALL_ORIENTATIONS: [DividerOrientation; 2] =
160 [DividerOrientation::Horizontal, DividerOrientation::Vertical];
161
162 const DECL_COUNT: usize = 7;
164
165 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
167 v.as_ref().iter().map(|p| p.property.clone()).collect()
168 }
169
170 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 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 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 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 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 fn inline_properties(node: &Dom) -> Vec<CssProperty> {
279 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
280 }
281
282 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 #[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 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 #[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 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 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 #[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 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 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 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 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 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 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 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 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 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); 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 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 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 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 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 #[test]
543 fn set_orientation_replaces_the_style_and_never_grows_it() {
544 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 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 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 #[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 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 #[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 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 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 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 #[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(÷r.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 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(÷r.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 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 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 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 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 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 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 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 #[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 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 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}