1use azul_core::{
9 callbacks::Update,
10 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec},
11 refany::RefAny,
12};
13use azul_css::css::BoxOrStatic;
14use azul_css::{
15 dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
16 props::{
17 basic::{ColorU, PixelValueNoPercent, PixelValue, FloatValue},
18 layout::{LayoutDisplay, LayoutFlexDirection, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutFlexGrow},
19 property::{CssProperty, StyleBoxShadowValue, LayoutFlexGrowValue},
20 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBoxShadow, BoxShadowClipMode, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius},
21 },
22 impl_option_inner, AzString,
23};
24
25use crate::callbacks::CallbackInfo;
26
27const CARD_BORDER_COLOR: ColorU = ColorU {
29 r: 222,
30 g: 226,
31 b: 230,
32 a: 255,
33};
34const CARD_BG_COLOR: ColorU = ColorU {
36 r: 255,
37 g: 255,
38 b: 255,
39 a: 255,
40};
41const CARD_SHADOW_COLOR: ColorU = ColorU {
43 r: 0,
44 g: 0,
45 b: 0,
46 a: 38,
47};
48
49const CARD_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(CARD_BG_COLOR)];
50const CARD_BG: StyleBackgroundContentVec =
51 StyleBackgroundContentVec::from_const_slice(CARD_BG_ITEMS);
52
53static CARD_SHADOW: StyleBoxShadow = StyleBoxShadow {
55 offset_x: PixelValueNoPercent {
56 inner: PixelValue::const_px(0),
57 },
58 offset_y: PixelValueNoPercent {
59 inner: PixelValue::const_px(2),
60 },
61 blur_radius: PixelValueNoPercent {
62 inner: PixelValue::const_px(6),
63 },
64 spread_radius: PixelValueNoPercent {
65 inner: PixelValue::const_px(0),
66 },
67 clip_mode: BoxShadowClipMode::Outset,
68 color: CARD_SHADOW_COLOR,
69};
70
71const CARD_STYLE: &[CssPropertyWithConditions] = &[
72 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
73 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
74 LayoutFlexDirection::Column,
75 )),
76 CssPropertyWithConditions::simple(CssProperty::const_background_content(CARD_BG)),
77 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
79 12,
80 ))),
81 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
82 LayoutPaddingBottom::const_px(12),
83 )),
84 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
85 12,
86 ))),
87 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
88 LayoutPaddingRight::const_px(12),
89 )),
90 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
92 LayoutBorderTopWidth::const_px(1),
93 )),
94 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
95 LayoutBorderBottomWidth::const_px(1),
96 )),
97 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
98 LayoutBorderLeftWidth::const_px(1),
99 )),
100 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
101 LayoutBorderRightWidth::const_px(1),
102 )),
103 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
104 inner: BorderStyle::Solid,
105 })),
106 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
107 StyleBorderBottomStyle {
108 inner: BorderStyle::Solid,
109 },
110 )),
111 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
112 inner: BorderStyle::Solid,
113 })),
114 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
115 StyleBorderRightStyle {
116 inner: BorderStyle::Solid,
117 },
118 )),
119 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
120 inner: CARD_BORDER_COLOR,
121 })),
122 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
123 StyleBorderBottomColor {
124 inner: CARD_BORDER_COLOR,
125 },
126 )),
127 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
128 inner: CARD_BORDER_COLOR,
129 })),
130 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
131 StyleBorderRightColor {
132 inner: CARD_BORDER_COLOR,
133 },
134 )),
135 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
137 StyleBorderTopLeftRadius::const_px(8),
138 )),
139 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
140 StyleBorderTopRightRadius::const_px(8),
141 )),
142 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
143 StyleBorderBottomLeftRadius::const_px(8),
144 )),
145 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
146 StyleBorderBottomRightRadius::const_px(8),
147 )),
148 CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(
150 BoxOrStatic::Static(&raw const CARD_SHADOW),
151 ))),
152 CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(StyleBoxShadowValue::Exact(
153 BoxOrStatic::Static(&raw const CARD_SHADOW),
154 ))),
155 CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(
156 BoxOrStatic::Static(&raw const CARD_SHADOW),
157 ))),
158 CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(
159 BoxOrStatic::Static(&raw const CARD_SHADOW),
160 ))),
161];
162
163#[derive(Debug, Clone, PartialEq)]
166#[repr(C)]
167pub struct Card {
168 pub content: Dom,
170 pub flex_grow: f32,
172 pub on_click: OptionCardOnClick,
174}
175
176pub type CardOnClickCallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
178impl_widget_callback!(
179 CardOnClick,
180 OptionCardOnClick,
181 CardOnClickCallback,
182 CardOnClickCallbackType
183);
184
185azul_core::impl_managed_callback! {
187 wrapper: CardOnClickCallback,
188 info_ty: CallbackInfo,
189 return_ty: Update,
190 default_ret: Update::DoNothing,
191 invoker_static: CARD_ON_CLICK_INVOKER,
192 invoker_ty: AzCardOnClickCallbackInvoker,
193 thunk_fn: az_card_on_click_callback_thunk,
194 setter_fn: AzApp_setCardOnClickCallbackInvoker,
195 from_handle_fn: AzCardOnClickCallback_createFromHostHandle,
196}
197
198impl Card {
199 #[must_use] pub const fn create(content: Dom) -> Self {
201 Self {
202 content,
203 flex_grow: 0.0,
204 on_click: OptionCardOnClick::None,
205 }
206 }
207
208 #[must_use] pub const fn swap_with_default(&mut self) -> Self {
210 let mut s = Self::create(Dom::create_div());
211 core::mem::swap(&mut s, self);
212 s
213 }
214
215 pub fn set_content(&mut self, content: Dom) {
217 self.content = content;
218 }
219
220 #[must_use] pub fn with_content(mut self, content: Dom) -> Self {
222 self.set_content(content);
223 self
224 }
225
226 pub const fn set_flex_grow(&mut self, flex_grow: f32) {
228 self.flex_grow = flex_grow;
229 }
230
231 #[must_use] pub const fn with_flex_grow(mut self, flex_grow: f32) -> Self {
233 self.set_flex_grow(flex_grow);
234 self
235 }
236
237 pub fn set_on_click<C: Into<CardOnClickCallback>>(&mut self, data: RefAny, on_click: C) {
239 self.on_click = Some(CardOnClick {
240 refany: data,
241 callback: on_click.into(),
242 })
243 .into();
244 }
245
246 #[must_use] pub fn with_on_click<C: Into<CardOnClickCallback>>(
248 mut self,
249 data: RefAny,
250 on_click: C,
251 ) -> Self {
252 self.set_on_click(data, on_click);
253 self
254 }
255
256 #[must_use] pub fn dom(self) -> Dom {
257 use azul_core::{
258 callbacks::{CoreCallback, CoreCallbackData},
259 dom::{EventFilter, HoverEventFilter},
260 };
261
262 static CARD_CLASS: &[IdOrClass] =
263 &[Class(AzString::from_const_str("__azul-native-card"))];
264
265 let callbacks = match self.on_click.into_option() {
268 Some(CardOnClick {
269 refany: data,
270 callback,
271 }) => vec![CoreCallbackData {
272 event: EventFilter::Hover(HoverEventFilter::MouseUp),
273 callback: CoreCallback {
274 cb: callback.cb as *const () as usize,
275 ctx: callback.ctx,
276 },
277 refany: data,
278 }],
279 None => Vec::new(),
280 };
281
282 let mut props = vec![CssPropertyWithConditions::simple(CssProperty::FlexGrow(
284 LayoutFlexGrowValue::Exact(LayoutFlexGrow {
285 inner: FloatValue::new(self.flex_grow),
286 }),
287 ))];
288 props.extend_from_slice(CARD_STYLE);
289
290 Dom::create_div()
291 .with_ids_and_classes(IdOrClassVec::from_const_slice(CARD_CLASS))
292 .with_css_props(CssPropertyWithConditionsVec::from_vec(props))
293 .with_callbacks(callbacks.into())
294 .with_children(vec![self.content].into())
295 }
296}
297
298impl Default for Card {
299 fn default() -> Self {
300 Self::create(Dom::create_div())
301 }
302}
303
304impl From<Card> for Dom {
305 fn from(c: Card) -> Self {
306 c.dom()
307 }
308}
309
310#[cfg(test)]
311#[allow(
312 clippy::float_cmp,
313 clippy::cast_precision_loss,
314 clippy::unreadable_literal,
315 clippy::too_many_lines
316)]
317mod autotest_generated {
318 use azul_core::dom::{EventFilter, HoverEventFilter, NodeType};
319 use azul_css::props::basic::length::SizeMetric;
320
321 use super::*;
322 use crate::callbacks::Callback;
323
324 extern "C" fn click_a(_data: RefAny, _info: CallbackInfo) -> Update {
329 Update::DoNothing
330 }
331
332 extern "C" fn click_b(_data: RefAny, _info: CallbackInfo) -> Update {
333 Update::RefreshDom
334 }
335
336 const ADVERSARIAL_TEXT: [&str; 6] = [
341 "",
342 "a\0b",
343 "e\u{0301}\u{0301}\u{0301}",
344 "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}",
345 "\u{202E}gnirts desrever\u{202C}",
346 "\u{FFFD}\u{FEFF}\t\n",
347 ];
348
349 const ADVERSARIAL_FLOATS: [f32; 14] = [
353 0.0,
354 -0.0,
355 1.0,
356 -1.0,
357 0.001,
358 -0.001,
359 f32::EPSILON,
360 f32::MIN_POSITIVE,
361 -f32::MIN_POSITIVE,
362 f32::MAX,
363 f32::MIN,
364 f32::INFINITY,
365 f32::NEG_INFINITY,
366 f32::NAN,
367 ];
368
369 fn inline_props(dom: &Dom) -> Vec<CssProperty> {
371 dom.root
372 .style
373 .iter_inline_properties()
374 .map(|(p, _)| p.clone())
375 .collect()
376 }
377
378 fn classes(dom: &Dom) -> Vec<String> {
380 dom.root
381 .get_ids_and_classes()
382 .as_ref()
383 .iter()
384 .filter_map(|c| match c {
385 IdOrClass::Class(s) => Some(s.as_str().to_string()),
386 IdOrClass::Id(_) => None,
387 })
388 .collect()
389 }
390
391 fn dom_flex_grow(dom: &Dom) -> Option<f32> {
394 dom.root
395 .style
396 .iter_inline_properties()
397 .find_map(|(p, _)| match p {
398 CssProperty::FlexGrow(v) => v.get_property().map(|f| f.inner.get()),
399 _ => None,
400 })
401 }
402
403 fn px(pv: &PixelValue) -> f32 {
407 assert_eq!(
408 pv.metric,
409 SizeMetric::Px,
410 "card geometry must be absolute px, got {:?}",
411 pv.metric
412 );
413 pv.number.get()
414 }
415
416 fn count_descendants(dom: &Dom) -> usize {
420 dom.children
421 .as_ref()
422 .iter()
423 .map(|c| 1 + count_descendants(c))
424 .sum()
425 }
426
427 fn nested_divs(depth: usize) -> Dom {
429 let mut d = Dom::create_div();
430 for _ in 0..depth {
431 d = Dom::create_div().with_child(d);
432 }
433 d
434 }
435
436 fn text_of(dom: &Dom) -> Option<&str> {
437 match dom.root.get_node_type() {
438 NodeType::Text(s) => Some(s.as_ref().as_str()),
439 _ => None,
440 }
441 }
442
443 #[test]
448 fn create_zeroes_the_numeric_field_and_leaves_the_callback_unset() {
449 let c = Card::create(Dom::create_div());
450
451 assert_eq!(c.flex_grow.to_bits(), 0_u32, "flex_grow must start at +0.0");
454 assert!(c.on_click.is_none(), "a fresh card must have no callback");
455 assert_eq!(c.content, Dom::create_div(), "the content was not stored verbatim");
456 }
457
458 #[test]
459 fn create_with_a_div_is_exactly_the_default_card() {
460 assert_eq!(
461 Card::create(Dom::create_div()),
462 Card::default(),
463 "Default and create(div) drifted apart",
464 );
465 }
466
467 #[test]
468 fn create_stores_pathological_content_verbatim() {
469 for t in ADVERSARIAL_TEXT {
470 let c = Card::create(Dom::create_text(t));
471 assert_eq!(
472 text_of(&c.content),
473 Some(t),
474 "the card mangled or normalised its text content",
475 );
476
477 let dom = c.dom();
479 let child = &dom.children.as_ref()[0];
480 assert_eq!(text_of(child), Some(t), "the text was corrupted on the way into the DOM");
481 }
482 }
483
484 #[test]
485 fn create_accepts_deeply_nested_and_very_wide_content() {
486 let deep = Card::create(nested_divs(64)).dom();
487 assert_eq!(
488 deep.estimated_total_children,
489 count_descendants(&deep),
490 "the cached child count desynced for deeply nested content",
491 );
492
493 let wide = Card::create(
494 Dom::create_div().with_children((0..2000).map(|_| Dom::create_div()).collect::<Vec<_>>().into()),
495 )
496 .dom();
497 assert_eq!(
498 wide.estimated_total_children,
499 count_descendants(&wide),
500 "the cached child count desynced for very wide content",
501 );
502 assert_eq!(wide.estimated_total_children, 2001, "card div + 2000 grandchildren expected");
503 }
504
505 #[test]
510 fn swap_with_default_moves_every_field_out_and_leaves_a_default() {
511 let mut c = Card::create(Dom::create_text("payload")).with_flex_grow(2.5);
512 c.set_on_click(RefAny::new(7u32), click_a as CardOnClickCallbackType);
513
514 let taken = c.swap_with_default();
515
516 assert_eq!(text_of(&taken.content), Some("payload"), "the content did not travel out");
517 assert_eq!(taken.flex_grow, 2.5, "flex_grow did not travel out");
518 assert!(taken.on_click.is_some(), "the callback did not travel out");
519
520 assert_eq!(c, Card::default(), "what was left behind is not a default card");
521 assert!(c.on_click.is_none(), "the swapped-in default still carries a callback");
522 assert_eq!(c.flex_grow.to_bits(), 0_u32, "the swapped-in default has a non-zero flex_grow");
523 }
524
525 #[test]
526 fn repeated_swap_with_default_never_accumulates_state() {
527 let mut c = Card::create(Dom::create_text("x")).with_flex_grow(1.0);
528 let _first = c.swap_with_default();
529
530 for i in 0..8 {
531 let taken = c.swap_with_default();
532 assert_eq!(taken, Card::default(), "swap #{i} handed back something other than a default");
533 assert_eq!(c, Card::default(), "swap #{i} left something other than a default behind");
534 }
535
536 let dom = c.dom();
538 assert_eq!(dom.children.as_ref().len(), 1);
539 assert_eq!(dom_flex_grow(&dom), Some(0.0));
540 }
541
542 #[test]
547 fn set_content_replaces_rather_than_appends() {
548 let mut c = Card::create(Dom::create_text("first"));
549 c.set_content(Dom::create_text("second"));
550 c.set_content(Dom::create_text("third"));
551
552 assert_eq!(text_of(&c.content), Some("third"), "the last content did not win");
553
554 let dom = c.dom();
555 assert_eq!(
556 dom.children.as_ref().len(),
557 1,
558 "re-setting the content appended a child instead of replacing it",
559 );
560 assert_eq!(text_of(&dom.children.as_ref()[0]), Some("third"));
561 }
562
563 #[test]
564 fn with_content_touches_only_the_content_field() {
565 let base = Card::create(Dom::create_text("old")).with_flex_grow(3.0);
566 let c = base.with_content(Dom::create_text("new"));
567
568 assert_eq!(text_of(&c.content), Some("new"));
569 assert_eq!(c.flex_grow, 3.0, "with_content clobbered flex_grow");
570 assert!(c.on_click.is_none(), "with_content invented a callback");
571 }
572
573 #[test]
574 fn with_content_preserves_an_already_installed_callback() {
575 let c = Card::default()
576 .with_on_click(RefAny::new(1u8), click_a as CardOnClickCallbackType)
577 .with_content(Dom::create_text("late content"));
578
579 assert!(c.on_click.is_some(), "with_content dropped the callback");
580 let dom = c.dom();
581 assert_eq!(dom.root.callbacks.as_ref().len(), 1, "the callback was lost on the way into the DOM");
582 assert_eq!(text_of(&dom.children.as_ref()[0]), Some("late content"));
583 }
584
585 #[test]
590 fn set_flex_grow_stores_the_bit_pattern_verbatim_without_sanitising() {
591 for v in ADVERSARIAL_FLOATS {
594 let mut c = Card::default();
595 c.set_flex_grow(v);
596 if v.is_nan() {
597 assert!(c.flex_grow.is_nan(), "a NaN flex_grow was silently rewritten");
598 } else {
599 assert_eq!(c.flex_grow.to_bits(), v.to_bits(), "flex_grow {v} was not stored verbatim");
600 }
601 }
602 }
603
604 #[test]
605 fn with_flex_grow_is_exactly_set_flex_grow() {
606 for v in ADVERSARIAL_FLOATS {
607 let mut by_setter = Card::default();
608 by_setter.set_flex_grow(v);
609 let by_builder = Card::default().with_flex_grow(v);
610
611 assert_eq!(
612 by_setter.flex_grow.to_bits(),
613 by_builder.flex_grow.to_bits(),
614 "the builder and the setter disagree for {v}",
615 );
616 }
617 }
618
619 #[test]
620 fn with_flex_grow_touches_only_the_numeric_field() {
621 let c = Card::create(Dom::create_text("body"))
622 .with_on_click(RefAny::new(1u8), click_a as CardOnClickCallbackType)
623 .with_flex_grow(f32::NAN);
624
625 assert_eq!(text_of(&c.content), Some("body"), "with_flex_grow clobbered the content");
626 assert!(c.on_click.is_some(), "with_flex_grow dropped the callback");
627 }
628
629 #[test]
630 fn a_nan_flex_grow_breaks_the_derived_equality_of_the_card() {
631 let c = Card::default().with_flex_grow(f32::NAN);
635 assert_ne!(c, c.clone(), "NaN equality semantics changed");
636
637 for v in ADVERSARIAL_FLOATS.iter().copied().filter(|v| !v.is_nan()) {
639 let c = Card::default().with_flex_grow(v);
640 assert_eq!(c, c.clone(), "a card with flex_grow {v} is not equal to its own clone");
641 }
642 }
643
644 #[test]
645 fn flex_grow_zero_encodes_to_positive_zero_even_from_negative_zero() {
646 for v in [0.0_f32, -0.0_f32] {
647 let dom = Card::default().with_flex_grow(v).dom();
648 let got = dom_flex_grow(&dom).expect("flex-grow must always be declared");
649 assert_eq!(got.to_bits(), 0_u32, "flex_grow {v} did not encode to +0.0 (got {got})");
650 }
651 }
652
653 #[test]
654 fn flex_grow_round_trips_through_the_dom_at_milli_precision() {
655 for v in [0.0_f32, 0.001, 0.5, 1.0, 2.5, 3.0, -1.5, 1000.0, 65536.0] {
658 let got = dom_flex_grow(&Card::default().with_flex_grow(v).dom())
659 .expect("flex-grow must always be declared");
660 assert!(
661 (got - v).abs() <= 0.001,
662 "flex_grow {v} did not survive the FloatValue encoding (got {got})",
663 );
664 }
665 }
666
667 #[test]
668 fn flex_grow_below_the_encoding_precision_truncates_to_zero() {
669 for v in [f32::EPSILON, f32::MIN_POSITIVE, -f32::MIN_POSITIVE, 1e-4, -1e-4, 0.0005, -0.0009] {
672 let got = dom_flex_grow(&Card::default().with_flex_grow(v).dom())
673 .expect("flex-grow must always be declared");
674 assert_eq!(got.to_bits(), 0_u32, "sub-milli flex_grow {v} did not truncate to +0.0 (got {got})");
675 }
676
677 let smallest = dom_flex_grow(&Card::default().with_flex_grow(0.001).dom()).unwrap();
679 assert!(smallest > 0.0, "0.001 is supposed to be the smallest representable factor");
680 }
681
682 #[test]
683 fn a_nan_flex_grow_does_not_panic_and_lands_on_zero() {
684 let dom = Card::default().with_flex_grow(f32::NAN).dom();
685 let got = dom_flex_grow(&dom).expect("flex-grow must always be declared");
686 assert!(!got.is_nan(), "a NaN flex-grow reached the style tree");
687 assert_eq!(got.to_bits(), 0_u32, "NaN must encode to +0.0 (saturating cast), got {got}");
688 }
689
690 #[test]
691 fn infinite_and_maximal_flex_grow_saturates_instead_of_overflowing() {
692 let max_encoded = (isize::MAX as f32) / 1000.0;
696 let min_encoded = (isize::MIN as f32) / 1000.0;
697
698 for v in [f32::INFINITY, f32::MAX] {
699 let got = dom_flex_grow(&Card::default().with_flex_grow(v).dom()).unwrap();
700 assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
701 assert_eq!(got, max_encoded, "{v} did not saturate at the isize upper bound");
702 assert!(got > 0.0, "{v} wrapped around into a non-positive factor");
703 }
704
705 for v in [f32::NEG_INFINITY, f32::MIN] {
706 let got = dom_flex_grow(&Card::default().with_flex_grow(v).dom()).unwrap();
707 assert!(got.is_finite(), "an infinite flex-grow reached the style tree for {v}");
708 assert_eq!(got, min_encoded, "{v} did not saturate at the isize lower bound");
709 assert!(got < 0.0, "{v} wrapped around into a non-negative factor");
710 }
711 }
712
713 #[test]
714 fn no_flex_grow_input_can_put_a_nan_or_an_infinity_into_the_style_tree() {
715 for v in ADVERSARIAL_FLOATS {
716 let dom = Card::default().with_flex_grow(v).dom();
717 let got = dom_flex_grow(&dom).unwrap_or_else(|| panic!("flex-grow disappeared for input {v}"));
718 assert!(
719 got.is_finite(),
720 "input {v} produced a non-finite flex-grow ({got}) — the layout solver would NaN out",
721 );
722 }
723 }
724
725 #[test]
726 fn flex_grow_encoding_is_monotonic() {
727 let ascending = [-1000.0_f32, -1.5, -0.001, 0.0, 0.001, 1.5, 1000.0];
729 let mut prev = f32::NEG_INFINITY;
730 for v in ascending {
731 let got = dom_flex_grow(&Card::default().with_flex_grow(v).dom()).unwrap();
732 assert!(got >= prev, "encoding is not monotonic: {v} encoded to {got}, below the previous {prev}");
733 prev = got;
734 }
735 }
736
737 #[test]
742 fn set_on_click_replaces_the_previous_callback_instead_of_appending() {
743 let mut c = Card::default();
744 assert!(c.on_click.is_none());
745
746 c.set_on_click(RefAny::new(1u32), click_a as CardOnClickCallbackType);
747 assert!(c.on_click.is_some(), "set_on_click did not store the callback");
748
749 c.set_on_click(RefAny::new(2u32), click_b as CardOnClickCallbackType);
750 let stored = c.on_click.as_ref().expect("callback must still be present");
751 assert_eq!(
752 stored.callback.cb as *const () as usize,
753 click_b as CardOnClickCallbackType as *const () as usize,
754 "the second set_on_click did not replace the first",
755 );
756
757 let dom = c.dom();
758 assert_eq!(
759 dom.root.callbacks.as_ref().len(),
760 1,
761 "a re-set callback was appended instead of replaced — the card would fire twice",
762 );
763 }
764
765 #[test]
766 fn with_on_click_round_trips_the_function_pointer_and_the_payload_into_the_dom() {
767 let cb: CardOnClickCallbackType = click_a;
768 let expected_ptr = cb as *const () as usize;
769
770 let dom = Card::create(Dom::create_text("clickable"))
771 .with_on_click(RefAny::new(0xDEAD_BEEF_u32), cb)
772 .dom();
773
774 let callbacks = dom.root.callbacks.as_ref();
775 assert_eq!(callbacks.len(), 1, "exactly one click callback is expected");
776 assert_eq!(
777 callbacks[0].event,
778 EventFilter::Hover(HoverEventFilter::MouseUp),
779 "the card must fire on mouse-up, not on any other filter",
780 );
781 assert_eq!(
782 callbacks[0].callback.cb, expected_ptr,
783 "the fn pointer was corrupted on the way into the DOM",
784 );
785
786 let mut data = callbacks[0].refany.clone();
789 assert_eq!(
790 *data.downcast_ref::<u32>().expect("payload changed type"),
791 0xDEAD_BEEF,
792 "payload was corrupted",
793 );
794 assert!(data.downcast_ref::<u64>().is_none(), "a wrong-type downcast reinterpreted the payload");
795 }
796
797 #[test]
798 fn with_on_click_accepts_a_generic_callback_without_mangling_the_pointer() {
799 let generic = Callback {
802 cb: click_a,
803 ctx: azul_core::refany::OptionRefAny::None,
804 };
805 let raw: CardOnClickCallbackType = click_a;
806 let expected_ptr = raw as *const () as usize;
807
808 let dom = Card::default().with_on_click(RefAny::new(1u8), generic).dom();
809 let callbacks = dom.root.callbacks.as_ref();
810 assert_eq!(callbacks.len(), 1);
811 assert_eq!(
812 callbacks[0].callback.cb, expected_ptr,
813 "the Callback -> CardOnClickCallback transmute mangled the pointer",
814 );
815 assert!(callbacks[0].callback.ctx.is_none(), "a native callback must carry no FFI context");
816 }
817
818 #[test]
819 fn a_card_without_a_callback_registers_no_callbacks() {
820 let dom = Card::create(Dom::create_text("inert")).with_flex_grow(1.0).dom();
821 assert!(
822 dom.root.callbacks.as_ref().is_empty(),
823 "a callback appeared on a card that was never given one",
824 );
825 }
826
827 #[test]
828 fn set_on_click_does_not_disturb_the_other_fields() {
829 let mut c = Card::create(Dom::create_text("body")).with_flex_grow(2.0);
830 c.set_on_click(RefAny::new("payload".to_string()), click_a as CardOnClickCallbackType);
831
832 assert_eq!(text_of(&c.content), Some("body"), "set_on_click clobbered the content");
833 assert_eq!(c.flex_grow, 2.0, "set_on_click clobbered flex_grow");
834
835 let dom = c.dom();
836 assert_eq!(dom_flex_grow(&dom), Some(2.0));
837 let mut data = dom.root.callbacks.as_ref()[0].refany.clone();
838 let payload = data.downcast_ref::<String>().expect("payload changed type");
839 assert_eq!(payload.as_str(), "payload", "the RefAny payload was corrupted");
840 }
841
842 #[test]
847 fn dom_builds_a_single_card_div_holding_the_content_as_its_only_child() {
848 let dom = Card::create(Dom::create_text("body")).dom();
849
850 assert_eq!(*dom.root.get_node_type(), NodeType::Div, "the card container must be a div");
851 assert_eq!(
852 classes(&dom),
853 vec!["__azul-native-card".to_string()],
854 "the card class is what the UA stylesheet and tests key off",
855 );
856 assert_eq!(dom.children.as_ref().len(), 1, "the card must wrap exactly one child");
857 assert_eq!(text_of(&dom.children.as_ref()[0]), Some("body"));
858 assert_eq!(
859 dom.estimated_total_children,
860 count_descendants(&dom),
861 "the cached child count is inconsistent — compact-DOM conversion would over/under-allocate",
862 );
863 }
864
865 #[test]
866 fn dom_prepends_the_flex_grow_declaration_to_the_static_card_style() {
867 let dom = Card::default().with_flex_grow(4.0).dom();
868 let props = inline_props(&dom);
869
870 assert_eq!(
871 props.len(),
872 1 + CARD_STYLE.len(),
873 "the card's inline style must be exactly flex-grow + the static card style",
874 );
875 assert!(
876 matches!(props[0], CssProperty::FlexGrow(_)),
877 "flex-grow must come first, so nothing in the static style can shadow it",
878 );
879 assert_eq!(
880 props.iter().filter(|p| matches!(p, CssProperty::FlexGrow(_))).count(),
881 1,
882 "flex-grow was declared more than once",
883 );
884 }
885
886 #[test]
887 fn dom_carries_the_static_card_geometry() {
888 let props = inline_props(&Card::default().dom());
889
890 let mut paddings = Vec::new();
891 let mut border_widths = Vec::new();
892 let mut radii = Vec::new();
893 let mut border_colors = Vec::new();
894 let mut border_styles = Vec::new();
895 let mut display = None;
896 let mut direction = None;
897 let mut background = None;
898
899 for p in &props {
900 match p {
901 CssProperty::PaddingTop(v) => paddings.push(v.get_property().map(|x| px(&x.inner))),
902 CssProperty::PaddingBottom(v) => paddings.push(v.get_property().map(|x| px(&x.inner))),
903 CssProperty::PaddingLeft(v) => paddings.push(v.get_property().map(|x| px(&x.inner))),
904 CssProperty::PaddingRight(v) => paddings.push(v.get_property().map(|x| px(&x.inner))),
905 CssProperty::BorderTopWidth(v) => border_widths.push(v.get_property().map(|x| px(&x.inner))),
906 CssProperty::BorderBottomWidth(v) => border_widths.push(v.get_property().map(|x| px(&x.inner))),
907 CssProperty::BorderLeftWidth(v) => border_widths.push(v.get_property().map(|x| px(&x.inner))),
908 CssProperty::BorderRightWidth(v) => border_widths.push(v.get_property().map(|x| px(&x.inner))),
909 CssProperty::BorderTopLeftRadius(v) => radii.push(v.get_property().map(|x| px(&x.inner))),
910 CssProperty::BorderTopRightRadius(v) => radii.push(v.get_property().map(|x| px(&x.inner))),
911 CssProperty::BorderBottomLeftRadius(v) => radii.push(v.get_property().map(|x| px(&x.inner))),
912 CssProperty::BorderBottomRightRadius(v) => radii.push(v.get_property().map(|x| px(&x.inner))),
913 CssProperty::BorderTopColor(v) => border_colors.push(v.get_property().map(|x| x.inner)),
914 CssProperty::BorderBottomColor(v) => border_colors.push(v.get_property().map(|x| x.inner)),
915 CssProperty::BorderLeftColor(v) => border_colors.push(v.get_property().map(|x| x.inner)),
916 CssProperty::BorderRightColor(v) => border_colors.push(v.get_property().map(|x| x.inner)),
917 CssProperty::BorderTopStyle(v) => border_styles.push(v.get_property().map(|x| x.inner)),
918 CssProperty::BorderBottomStyle(v) => border_styles.push(v.get_property().map(|x| x.inner)),
919 CssProperty::BorderLeftStyle(v) => border_styles.push(v.get_property().map(|x| x.inner)),
920 CssProperty::BorderRightStyle(v) => border_styles.push(v.get_property().map(|x| x.inner)),
921 CssProperty::Display(v) => display = v.get_property().copied(),
922 CssProperty::FlexDirection(v) => direction = v.get_property().copied(),
923 CssProperty::BackgroundContent(v) => {
924 background = v.get_property().map(|bg| bg.as_ref().to_vec());
925 }
926 _ => {}
927 }
928 }
929
930 assert_eq!(paddings, vec![Some(12.0); 4], "all four paddings must be 12px");
931 assert_eq!(border_widths, vec![Some(1.0); 4], "all four borders must be 1px");
932 assert_eq!(radii, vec![Some(8.0); 4], "all four corners must be 8px");
933 assert_eq!(border_colors, vec![Some(CARD_BORDER_COLOR); 4], "all four border colours must match");
934 assert_eq!(border_styles, vec![Some(BorderStyle::Solid); 4], "all four border styles must be solid");
935 assert_eq!(display, Some(LayoutDisplay::Flex), "the card container must be a flex box");
936 assert_eq!(direction, Some(LayoutFlexDirection::Column), "the card must stack its content in a column");
937
938 let bg = background.expect("the card must declare a background");
939 assert_eq!(bg.len(), 1, "exactly one background layer expected");
940 match &bg[0] {
941 StyleBackgroundContent::Color(c) => assert_eq!(*c, CARD_BG_COLOR, "the card background is not white"),
942 other => panic!("the card background must be a flat colour, got {other:?}"),
943 }
944 }
945
946 #[test]
947 fn dom_box_shadows_dereference_the_shared_static_descriptor() {
948 let dom = Card::default().dom();
949
950 let mut shadows = 0_usize;
951 for (p, _) in dom.root.style.iter_inline_properties() {
952 let value = match p {
953 CssProperty::BoxShadowTop(v)
954 | CssProperty::BoxShadowBottom(v)
955 | CssProperty::BoxShadowLeft(v)
956 | CssProperty::BoxShadowRight(v) => v.get_property(),
957 _ => None,
958 };
959 let Some(boxed) = value else { continue };
960
961 let s: &StyleBoxShadow = boxed.as_ref();
964 assert_eq!(px(&s.offset_x.inner), 0.0);
965 assert_eq!(px(&s.offset_y.inner), 2.0);
966 assert_eq!(px(&s.blur_radius.inner), 6.0);
967 assert_eq!(px(&s.spread_radius.inner), 0.0);
968 assert_eq!(s.clip_mode, BoxShadowClipMode::Outset);
969 assert_eq!(s.color, CARD_SHADOW_COLOR);
970 shadows += 1;
971 }
972 assert_eq!(shadows, 4, "the card must declare a shadow on all four edges");
973 }
974
975 #[test]
976 fn dropping_card_doms_never_frees_the_static_shadow() {
977 let survivor = Card::default().dom();
981 for _ in 0..64 {
982 drop(Card::create(Dom::create_text("throwaway")).with_flex_grow(1.0).dom());
983 }
984
985 assert_eq!(CARD_SHADOW.offset_y.inner.number.get(), 2.0, "the static shadow was mutated or freed");
986 assert_eq!(CARD_SHADOW.color, CARD_SHADOW_COLOR);
987
988 let shadows = survivor
989 .root
990 .style
991 .iter_inline_properties()
992 .filter_map(|(p, _)| match p {
993 CssProperty::BoxShadowTop(v)
994 | CssProperty::BoxShadowBottom(v)
995 | CssProperty::BoxShadowLeft(v)
996 | CssProperty::BoxShadowRight(v) => v.get_property(),
997 _ => None,
998 })
999 .map(|b| px(&b.as_ref().blur_radius.inner))
1000 .collect::<Vec<_>>();
1001 assert_eq!(shadows, vec![6.0; 4], "a surviving card's shadows were corrupted by other cards' drops");
1002 }
1003
1004 #[test]
1005 fn dom_is_deterministic_and_never_mutates_the_shared_static_style() {
1006 let baseline = inline_props(&Card::default().dom());
1007
1008 for i in 0..32 {
1009 let props = inline_props(&Card::default().dom());
1010 assert_eq!(props.len(), baseline.len(), "build #{i} produced a different number of properties");
1011 assert_eq!(props, baseline, "build #{i} diverged — the static CARD_STYLE was mutated");
1012 }
1013 }
1014
1015 #[test]
1016 fn from_card_for_dom_is_exactly_dom() {
1017 let c = Card::create(Dom::create_text("body")).with_flex_grow(1.5);
1018 assert_eq!(Dom::from(c.clone()), c.dom(), "the From impl diverged from Card::dom");
1019 }
1020
1021 #[test]
1022 fn cards_nest_without_desyncing_the_child_counts() {
1023 let inner = Card::create(Dom::create_text("inner")).with_flex_grow(1.0).dom();
1024 let outer = Card::create(inner).with_flex_grow(2.0).dom();
1025
1026 assert_eq!(
1027 outer.estimated_total_children,
1028 count_descendants(&outer),
1029 "nesting cards desynced the cached child count",
1030 );
1031 assert_eq!(dom_flex_grow(&outer), Some(2.0), "the outer card's flex-grow was overwritten");
1032 assert_eq!(
1033 dom_flex_grow(&outer.children.as_ref()[0]),
1034 Some(1.0),
1035 "the inner card's flex-grow was overwritten",
1036 );
1037 assert_eq!(classes(&outer.children.as_ref()[0]), vec!["__azul-native-card".to_string()]);
1038 }
1039}