1use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
10use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
11use azul_css::{
12 props::{
13 basic::{color::ColorU, StyleFontSize},
14 layout::{LayoutDisplay, LayoutFlexDirection, LayoutJustifyContent, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
15 property::{CssProperty, *},
16 style::{StyleBackgroundContentVec, StyleBackgroundContent, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextAlign, StyleTextColor},
17 },
18 AzString,
19};
20
21#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
23#[repr(C)]
24pub enum BadgeKind {
25 #[default]
27 Default,
28 Primary,
30 Success,
32 Danger,
34 Warning,
36 Info,
38}
39
40impl BadgeKind {
41 #[allow(clippy::trivially_copy_pass_by_ref)] const fn colors(&self) -> (ColorU, ColorU) {
44 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
45 const DARK: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
46 match self {
47 Self::Default => (ColorU { r: 108, g: 117, b: 125, a: 255 }, WHITE),
48 Self::Primary => (ColorU { r: 13, g: 110, b: 253, a: 255 }, WHITE),
49 Self::Success => (ColorU { r: 25, g: 135, b: 84, a: 255 }, WHITE),
50 Self::Danger => (ColorU { r: 220, g: 53, b: 69, a: 255 }, WHITE),
51 Self::Warning => (ColorU { r: 255, g: 193, b: 7, a: 255 }, DARK),
52 Self::Info => (ColorU { r: 13, g: 202, b: 240, a: 255 }, DARK),
53 }
54 }
55
56 #[must_use] pub const fn class_name(&self) -> &'static str {
58 match self {
59 Self::Default => "__azul-badge-default",
60 Self::Primary => "__azul-badge-primary",
61 Self::Success => "__azul-badge-success",
62 Self::Danger => "__azul-badge-danger",
63 Self::Warning => "__azul-badge-warning",
64 Self::Info => "__azul-badge-info",
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
72#[repr(C)]
73pub struct Badge {
74 pub string: AzString,
76 pub kind: BadgeKind,
78 pub badge_style: CssPropertyWithConditionsVec,
80}
81
82fn build_badge_style(kind: BadgeKind) -> CssPropertyWithConditionsVec {
86 let (bg, text) = kind.colors();
87 let bg_vec =
88 StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
89 CssPropertyWithConditionsVec::from_vec(alloc::vec![
90 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
91 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
92 LayoutFlexDirection::Row,
93 )),
94 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
95 LayoutJustifyContent::Center,
96 )),
97 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
98 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
100 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
101 0,
102 ))),
103 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
105 2,
106 ))),
107 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
108 LayoutPaddingBottom::const_px(2),
109 )),
110 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
111 LayoutPaddingLeft::const_px(8),
112 )),
113 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
114 LayoutPaddingRight::const_px(8),
115 )),
116 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
118 StyleBorderTopLeftRadius::const_px(10),
119 )),
120 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
121 StyleBorderTopRightRadius::const_px(10),
122 )),
123 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
124 StyleBorderBottomLeftRadius::const_px(10),
125 )),
126 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
127 StyleBorderBottomRightRadius::const_px(10),
128 )),
129 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(12))),
130 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
131 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
132 inner: text,
133 })),
134 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
135 ])
136}
137
138impl Badge {
139 #[inline]
141 #[must_use] pub fn create(string: AzString) -> Self {
142 Self::with_kind(string, BadgeKind::Default)
143 }
144
145 #[inline]
147 #[must_use] pub fn with_kind(string: AzString, kind: BadgeKind) -> Self {
148 Self {
149 string,
150 kind,
151 badge_style: build_badge_style(kind),
152 }
153 }
154
155 #[inline]
157 pub fn set_kind(&mut self, kind: BadgeKind) {
158 self.kind = kind;
159 self.badge_style = build_badge_style(kind);
160 }
161
162 #[inline]
164 #[must_use] pub fn with_badge_kind(mut self, kind: BadgeKind) -> Self {
165 self.set_kind(kind);
166 self
167 }
168
169 #[inline]
171 #[must_use] pub fn swap_with_default(&mut self) -> Self {
172 let mut s = Self::create(AzString::from_const_str(""));
173 core::mem::swap(&mut s, self);
174 s
175 }
176
177 #[inline]
179 #[must_use] pub fn dom(self) -> Dom {
180 static BADGE_CLASS: &[IdOrClass] =
181 &[Class(AzString::from_const_str("__azul-native-badge"))];
182
183 Dom::create_text(self.string)
184 .with_ids_and_classes(IdOrClassVec::from_const_slice(BADGE_CLASS))
185 .with_css_props(self.badge_style)
186 }
187}
188
189impl Default for Badge {
190 fn default() -> Self {
191 Self::create(AzString::from_const_str(""))
192 }
193}
194
195impl From<Badge> for Dom {
196 fn from(b: Badge) -> Self {
197 b.dom()
198 }
199}
200
201#[cfg(test)]
202mod autotest_generated {
203 use std::collections::HashSet;
204
205 use azul_core::dom::NodeType;
206 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
207
208 use super::*;
209
210 const ALL_KINDS: [BadgeKind; 6] = [
217 BadgeKind::Default,
218 BadgeKind::Primary,
219 BadgeKind::Success,
220 BadgeKind::Danger,
221 BadgeKind::Warning,
222 BadgeKind::Info,
223 ];
224
225 const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
226 const DARK: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
227
228 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
230 v.as_ref().iter().map(|p| p.property.clone()).collect()
231 }
232
233 fn px(pv: &PixelValue) -> f32 {
237 assert_eq!(pv.metric, SizeMetric::Px, "badge geometry must be absolute px, got {:?}", pv.metric);
238 pv.number.get()
239 }
240
241 fn padding_px(v: &CssPropertyWithConditionsVec) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
243 let find = |f: &dyn Fn(&CssProperty) -> Option<f32>| v.as_ref().iter().find_map(|p| f(&p.property));
244 (
245 find(&|p| match p {
246 CssProperty::PaddingTop(x) => x.get_property().map(|x| px(&x.inner)),
247 _ => None,
248 }),
249 find(&|p| match p {
250 CssProperty::PaddingBottom(x) => x.get_property().map(|x| px(&x.inner)),
251 _ => None,
252 }),
253 find(&|p| match p {
254 CssProperty::PaddingLeft(x) => x.get_property().map(|x| px(&x.inner)),
255 _ => None,
256 }),
257 find(&|p| match p {
258 CssProperty::PaddingRight(x) => x.get_property().map(|x| px(&x.inner)),
259 _ => None,
260 }),
261 )
262 }
263
264 fn radii_px(v: &CssPropertyWithConditionsVec) -> Vec<f32> {
266 v.as_ref()
267 .iter()
268 .filter_map(|p| match &p.property {
269 CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
270 CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
271 CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| px(&r.inner)),
272 CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| px(&r.inner)),
273 _ => None,
274 })
275 .collect()
276 }
277
278 fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
279 v.as_ref().iter().find_map(|p| match &p.property {
280 CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
281 _ => None,
282 })
283 }
284
285 fn text_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
286 v.as_ref().iter().find_map(|p| match &p.property {
287 CssProperty::TextColor(c) => c.get_property().map(|c| c.inner),
288 _ => None,
289 })
290 }
291
292 fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
295 let bg = v.as_ref().iter().find_map(|p| match &p.property {
296 CssProperty::BackgroundContent(b) => b.get_property(),
297 _ => None,
298 })?;
299 assert_eq!(bg.as_ref().len(), 1, "a badge must declare exactly one background layer");
300 match &bg.as_ref()[0] {
301 StyleBackgroundContent::Color(c) => Some(*c),
302 other => panic!("badge background is not a flat colour: {other:?}"),
303 }
304 }
305
306 fn all_pixel_values(v: &CssPropertyWithConditionsVec) -> Vec<PixelValue> {
308 v.as_ref()
309 .iter()
310 .filter_map(|p| match &p.property {
311 CssProperty::PaddingTop(x) => x.get_property().map(|x| x.inner),
312 CssProperty::PaddingBottom(x) => x.get_property().map(|x| x.inner),
313 CssProperty::PaddingLeft(x) => x.get_property().map(|x| x.inner),
314 CssProperty::PaddingRight(x) => x.get_property().map(|x| x.inner),
315 CssProperty::BorderTopLeftRadius(r) => r.get_property().map(|r| r.inner),
316 CssProperty::BorderTopRightRadius(r) => r.get_property().map(|r| r.inner),
317 CssProperty::BorderBottomLeftRadius(r) => r.get_property().map(|r| r.inner),
318 CssProperty::BorderBottomRightRadius(r) => r.get_property().map(|r| r.inner),
319 CssProperty::FontSize(f) => f.get_property().map(|f| f.inner),
320 _ => None,
321 })
322 .collect()
323 }
324
325 fn luma(c: ColorU) -> f32 {
329 0.2126 * f32::from(c.r) + 0.7152 * f32::from(c.g) + 0.0722 * f32::from(c.b)
330 }
331
332 fn has_class(node: &Dom, name: &str) -> bool {
334 node.root
335 .get_ids_and_classes()
336 .as_ref()
337 .iter()
338 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == name))
339 }
340
341 fn inline_properties(node: &Dom) -> Vec<CssProperty> {
343 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
344 }
345
346 fn adversarial_strings() -> Vec<String> {
350 let mut v: Vec<String> = [
351 "",
352 "9",
353 "99+",
354 " ",
355 "e\u{0301}", "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}", "\u{5E9}\u{5DC}\u{5D5}\u{5DD}", "\0", "a\0b", "\u{FFFD}\u{202E}\u{200B}", "-9223372036854775808", ]
363 .iter()
364 .map(|s| (*s).to_string())
365 .collect();
366 v.push("x".repeat(100_000));
367 v
368 }
369
370 #[test]
375 fn colors_returns_the_documented_constants_for_every_kind() {
376 let expected = [
377 (BadgeKind::Default, ColorU { r: 108, g: 117, b: 125, a: 255 }, WHITE),
378 (BadgeKind::Primary, ColorU { r: 13, g: 110, b: 253, a: 255 }, WHITE),
379 (BadgeKind::Success, ColorU { r: 25, g: 135, b: 84, a: 255 }, WHITE),
380 (BadgeKind::Danger, ColorU { r: 220, g: 53, b: 69, a: 255 }, WHITE),
381 (BadgeKind::Warning, ColorU { r: 255, g: 193, b: 7, a: 255 }, DARK),
382 (BadgeKind::Info, ColorU { r: 13, g: 202, b: 240, a: 255 }, DARK),
383 ];
384 for (kind, bg, text) in expected {
385 assert_eq!(kind.colors(), (bg, text), "{kind:?}: wrong (background, text) pair");
386 }
387 for kind in ALL_KINDS {
390 let (_, text) = kind.colors();
391 let dark_text = matches!(kind, BadgeKind::Warning | BadgeKind::Info);
392 assert_eq!(text == DARK, dark_text, "{kind:?}: text colour contradicts the documented variant");
393 }
394 }
395
396 #[test]
397 fn colors_are_fully_opaque_on_every_kind() {
398 for kind in ALL_KINDS {
401 let (bg, text) = kind.colors();
402 assert_eq!(bg.a, 255, "{kind:?}: translucent background {bg:?}");
403 assert_eq!(text.a, 255, "{kind:?}: translucent text colour {text:?}");
404 }
405 }
406
407 #[test]
408 fn colors_give_every_kind_a_distinguishable_background() {
409 let mut seen = HashSet::new();
411 for kind in ALL_KINDS {
412 let (bg, _) = kind.colors();
413 assert!(seen.insert((bg.r, bg.g, bg.b, bg.a)), "{kind:?}: duplicate background colour {bg:?}");
414 }
415 assert_eq!(seen.len(), ALL_KINDS.len());
416 }
417
418 #[test]
419 fn colors_pick_the_more_readable_of_the_two_text_colours() {
420 for kind in ALL_KINDS {
425 let (bg, text) = kind.colors();
426 let other = if text == WHITE { DARK } else { WHITE };
427
428 let chosen = (luma(bg) - luma(text)).abs();
429 let rejected = (luma(bg) - luma(other)).abs();
430 assert!(
431 chosen > rejected,
432 "{kind:?}: text {text:?} (Δluma {chosen:.1}) is less readable on {bg:?} than {other:?} (Δluma {rejected:.1})"
433 );
434 assert!(chosen >= 60.0, "{kind:?}: text/background brightness gap {chosen:.1} is too low to read");
435
436 let light_bg = luma(bg) >= 128.0;
438 assert_eq!(text == DARK, light_bg, "{kind:?}: bg luma {:.1} but text is {text:?}", luma(bg));
439 }
440 }
441
442 #[test]
443 fn colors_is_pure_and_the_default_kind_is_grey() {
444 assert_eq!(BadgeKind::default(), BadgeKind::Default);
445 assert_eq!(BadgeKind::default().colors(), BadgeKind::Default.colors());
446 for kind in ALL_KINDS {
449 let copy = kind;
450 assert_eq!(kind.colors(), kind.colors(), "{kind:?}: colors() is not pure");
451 assert_eq!(kind.colors(), copy.colors(), "{kind:?}: a copy disagrees with the original");
452 }
453 }
454
455 #[test]
460 fn class_name_returns_the_documented_string_for_every_kind() {
461 assert_eq!(BadgeKind::Default.class_name(), "__azul-badge-default");
462 assert_eq!(BadgeKind::Primary.class_name(), "__azul-badge-primary");
463 assert_eq!(BadgeKind::Success.class_name(), "__azul-badge-success");
464 assert_eq!(BadgeKind::Danger.class_name(), "__azul-badge-danger");
465 assert_eq!(BadgeKind::Warning.class_name(), "__azul-badge-warning");
466 assert_eq!(BadgeKind::Info.class_name(), "__azul-badge-info");
467 assert_eq!(BadgeKind::default().class_name(), "__azul-badge-default");
468 }
469
470 #[test]
471 fn class_name_is_unique_per_kind_and_a_usable_css_identifier() {
472 let mut seen = HashSet::new();
473 for kind in ALL_KINDS {
474 let name = kind.class_name();
475 assert!(seen.insert(name), "{kind:?}: class name {name:?} collides with another kind");
476 assert!(!name.is_empty(), "{kind:?}: empty class name");
477 assert!(name.starts_with("__azul-badge-"), "{kind:?}: unnamespaced class {name:?}");
478 assert!(name.is_ascii(), "{kind:?}: non-ASCII class name {name:?}");
479 assert!(
481 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
482 "{kind:?}: class name {name:?} contains a CSS-significant character"
483 );
484 assert_eq!(name.as_ptr(), kind.class_name().as_ptr(), "{kind:?}: class_name() is not a stable constant");
486 }
487 assert_eq!(seen.len(), ALL_KINDS.len());
488 }
489
490 #[test]
495 fn build_badge_style_emits_the_documented_pill_geometry() {
496 for kind in ALL_KINDS {
497 let style = build_badge_style(kind);
498 assert_eq!(padding_px(&style), (Some(2.0), Some(2.0), Some(8.0), Some(8.0)), "{kind:?}: padding is not 2px 8px");
499 assert_eq!(radii_px(&style), vec![10.0, 10.0, 10.0, 10.0], "{kind:?}: all four corners must carry a 10px radius");
500 assert_eq!(font_size_px(&style), Some(12.0), "{kind:?}: wrong font size");
501 }
502 }
503
504 #[test]
505 fn build_badge_style_radius_actually_rounds_the_pill_to_a_semicircle() {
506 for kind in ALL_KINDS {
510 let style = build_badge_style(kind);
511 let (top, bottom, ..) = padding_px(&style);
512 let height = font_size_px(&style).expect("a font size must be declared")
513 + top.expect("padding-top")
514 + bottom.expect("padding-bottom");
515 for r in radii_px(&style) {
516 assert!(r * 2.0 >= height, "{kind:?}: radius {r} does not reach half of the {height}px pill height");
517 }
518 }
519 }
520
521 #[test]
522 fn build_badge_style_hugs_its_content_and_centres_the_text() {
523 for kind in ALL_KINDS {
524 let props = properties(&build_badge_style(kind));
525 let has = |p: &CssProperty| props.contains(p);
526
527 assert!(has(&CssProperty::const_display(LayoutDisplay::Flex)), "{kind:?}: not a flex box");
528 assert!(has(&CssProperty::const_flex_direction(LayoutFlexDirection::Row)), "{kind:?}: wrong flex direction");
529 assert!(has(&CssProperty::const_justify_content(LayoutJustifyContent::Center)), "{kind:?}: text not centred");
530 assert!(has(&CssProperty::const_align_items(LayoutAlignItems::Center)), "{kind:?}: text not centred");
531 assert!(has(&CssProperty::const_text_align(StyleTextAlign::Center)), "{kind:?}: text not centred");
532 assert!(has(&CssProperty::align_self(LayoutAlignSelf::Start)), "{kind:?}: badge stretches on the cross axis");
535 assert!(
536 has(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
537 "{kind:?}: badge grows on the main axis"
538 );
539 }
540 }
541
542 #[test]
543 fn build_badge_style_colours_track_the_kind() {
544 for kind in ALL_KINDS {
545 let style = build_badge_style(kind);
546 let (bg, text) = kind.colors();
547 assert_eq!(background_color(&style), Some(bg), "{kind:?}: emitted background != colors().0");
548 assert_eq!(text_color(&style), Some(text), "{kind:?}: emitted text colour != colors().1");
549 }
550 }
551
552 #[test]
553 fn build_badge_style_declares_every_property_at_most_once() {
554 for kind in ALL_KINDS {
557 let props = properties(&build_badge_style(kind));
558 let mut seen = HashSet::new();
559 for p in &props {
560 assert!(seen.insert(core::mem::discriminant(p)), "{kind:?}: duplicate declaration of {p:?}");
561 }
562 assert_eq!(seen.len(), props.len());
563 }
564 }
565
566 #[test]
567 fn build_badge_style_properties_are_all_unconditional() {
568 for kind in ALL_KINDS {
571 for p in build_badge_style(kind).as_ref() {
572 assert!(
573 p.apply_if.as_ref().is_empty(),
574 "{kind:?}: {:?} is conditional on a stateless widget",
575 p.property
576 );
577 }
578 }
579 }
580
581 #[test]
582 fn build_badge_style_is_deterministic_and_kind_dependent() {
583 let len = build_badge_style(BadgeKind::Default).as_ref().len();
584 for kind in ALL_KINDS {
585 assert_eq!(
586 properties(&build_badge_style(kind)),
587 properties(&build_badge_style(kind)),
588 "{kind:?}: two builds of the same kind disagree"
589 );
590 assert_eq!(
591 build_badge_style(kind).as_ref().len(),
592 len,
593 "{kind:?}: emits a different number of declarations than Default"
594 );
595 }
596 for (i, a) in ALL_KINDS.iter().enumerate() {
598 for b in &ALL_KINDS[i + 1..] {
599 assert_ne!(
600 properties(&build_badge_style(*a)),
601 properties(&build_badge_style(*b)),
602 "{a:?} and {b:?} produce an identical style"
603 );
604 }
605 }
606 }
607
608 #[test]
609 fn build_badge_style_emits_only_finite_non_negative_px_lengths() {
610 for kind in ALL_KINDS {
613 let values = all_pixel_values(&build_badge_style(kind));
614 assert_eq!(values.len(), 9, "{kind:?}: expected 4 paddings + 4 radii + 1 font size");
615 for pv in values {
616 let n = px(&pv); assert!(n.is_finite(), "{kind:?}: non-finite length {n}");
618 assert!(n >= 0.0, "{kind:?}: negative length {n}");
619 assert!(n <= 128.0, "{kind:?}: implausibly large length {n} for a badge");
620 }
621 }
622 }
623
624 #[test]
629 fn create_defaults_to_grey_and_keeps_the_text_verbatim() {
630 for s in adversarial_strings() {
631 let b = Badge::create(AzString::from(s.clone()));
632 assert_eq!(b.string.as_str(), s.as_str(), "the label was not preserved verbatim");
633 assert_eq!(b.string.len(), s.len(), "byte length changed (NUL truncation?)");
634 assert_eq!(b.kind, BadgeKind::Default, "create() must use the grey default kind");
635 assert_eq!(properties(&b.badge_style), properties(&build_badge_style(BadgeKind::Default)));
636 }
637 }
638
639 #[test]
640 fn with_kind_stores_both_arguments_and_the_matching_style() {
641 for kind in ALL_KINDS {
642 for s in adversarial_strings() {
643 let b = Badge::with_kind(AzString::from(s.clone()), kind);
644 assert_eq!(b.string.as_str(), s.as_str(), "{kind:?}: label not preserved");
645 assert_eq!(b.string.len(), s.len(), "{kind:?}: byte length changed");
646 assert_eq!(b.kind, kind, "{kind:?}: kind field does not match the argument");
647 assert_eq!(properties(&b.badge_style), properties(&build_badge_style(kind)));
649 assert_eq!(background_color(&b.badge_style), Some(kind.colors().0));
650 }
651 }
652 }
653
654 #[test]
655 fn create_is_with_kind_default() {
656 for s in ["", "99+", "\u{1F600}"] {
657 assert_eq!(
658 Badge::create(AzString::from_const_str(s)),
659 Badge::with_kind(AzString::from_const_str(s), BadgeKind::Default)
660 );
661 }
662 }
663
664 #[test]
665 fn default_badge_is_an_empty_grey_badge_and_equality_sees_every_field() {
666 let d = Badge::default();
667 assert_eq!(d, Badge::create(AzString::from_const_str("")));
668 assert_eq!(d.string.as_str(), "");
669 assert_eq!(d.kind, BadgeKind::Default);
670 assert_eq!(d.clone(), d, "Clone must preserve equality");
671
672 assert_ne!(d, Badge::create(AzString::from_const_str("9")), "the label must affect equality");
673 assert_ne!(
674 Badge::with_kind(AzString::from_const_str("9"), BadgeKind::Danger),
675 Badge::with_kind(AzString::from_const_str("9"), BadgeKind::Success),
676 "badges of different kinds must not compare equal"
677 );
678 }
679
680 #[test]
685 fn set_kind_recomputes_the_style_without_growing_it() {
686 let mut b = Badge::create(AzString::from_const_str("99+"));
690 let expected_len = build_badge_style(BadgeKind::Default).as_ref().len();
691
692 for round in 0..50 {
693 let kind = ALL_KINDS[round % ALL_KINDS.len()];
694 b.set_kind(kind);
695
696 assert_eq!(b.kind, kind, "round {round}: kind field not updated");
697 assert_eq!(
698 b.badge_style.as_ref().len(),
699 expected_len,
700 "round {round}: style vec changed length — stale declarations?"
701 );
702 assert_eq!(
703 properties(&b.badge_style),
704 properties(&build_badge_style(kind)),
705 "round {round}: style does not match a freshly built one"
706 );
707 assert_eq!(background_color(&b.badge_style), Some(kind.colors().0), "round {round}: stale background");
708 assert_eq!(b.string.as_str(), "99+", "round {round}: set_kind ate the label");
709 }
710 }
711
712 #[test]
713 fn with_badge_kind_agrees_with_set_kind_and_is_last_call_wins() {
714 let chained = Badge::create(AzString::from_const_str("9"))
715 .with_badge_kind(BadgeKind::Danger)
716 .with_badge_kind(BadgeKind::Warning)
717 .with_badge_kind(BadgeKind::Info);
718
719 let mut mutated = Badge::create(AzString::from_const_str("9"));
720 mutated.set_kind(BadgeKind::Danger);
721 mutated.set_kind(BadgeKind::Warning);
722 mutated.set_kind(BadgeKind::Info);
723
724 assert_eq!(chained, mutated, "the builder and the mutator must agree");
725 assert_eq!(chained.kind, BadgeKind::Info);
726 assert_eq!(chained.string.as_str(), "9");
727 assert_eq!(properties(&chained.badge_style), properties(&build_badge_style(BadgeKind::Info)));
728 assert_eq!(background_color(&chained.badge_style), Some(BadgeKind::Info.colors().0));
730 }
731
732 #[test]
733 fn setting_the_same_kind_twice_is_idempotent() {
734 for kind in ALL_KINDS {
735 let once = Badge::with_kind(AzString::from_const_str("x"), kind);
736 let twice = once.clone().with_badge_kind(kind);
737 assert_eq!(once, twice, "{kind:?}: re-setting the same kind changed the badge");
738 }
739 }
740
741 #[test]
746 fn swap_with_default_returns_the_original_and_leaves_a_default_behind() {
747 let mut b = Badge::with_kind(AzString::from_const_str("99+"), BadgeKind::Danger);
748 let taken = b.swap_with_default();
749
750 assert_eq!(taken.string.as_str(), "99+");
752 assert_eq!(taken.kind, BadgeKind::Danger);
753 assert_eq!(properties(&taken.badge_style), properties(&build_badge_style(BadgeKind::Danger)));
754
755 assert_eq!(b, Badge::default());
758 assert_eq!(b.string.as_str(), "");
759 assert_eq!(b.kind, BadgeKind::Default);
760 assert_eq!(background_color(&b.badge_style), Some(BadgeKind::Default.colors().0), "the red survived the swap");
761 }
762
763 #[test]
764 fn swap_with_default_is_idempotent_on_an_already_default_badge() {
765 let mut b = Badge::default();
766 let first = b.swap_with_default();
767 let second = b.swap_with_default();
768 assert_eq!(first, Badge::default());
769 assert_eq!(second, Badge::default());
770 assert_eq!(b, Badge::default());
771 }
772
773 #[test]
774 fn swap_with_default_survives_a_huge_label_and_repeated_swaps() {
775 let long = "x".repeat(100_000);
776 let mut b = Badge::with_kind(AzString::from(long.clone()), BadgeKind::Success);
777 for round in 0..10 {
778 let taken = b.swap_with_default();
779 if round == 0 {
780 assert_eq!(taken.string.len(), long.len(), "the long label was truncated");
781 assert_eq!(taken.kind, BadgeKind::Success);
782 } else {
783 assert_eq!(taken, Badge::default(), "round {round}: the emptied badge is not a default");
784 }
785 assert_eq!(b, Badge::default(), "round {round}: what was left behind is not a default");
786 }
787 }
788
789 #[test]
794 fn dom_is_a_single_classed_text_node_carrying_the_computed_style() {
795 for kind in ALL_KINDS {
796 let badge = Badge::with_kind(AzString::from_const_str("99+"), kind);
797 let expected = properties(&badge.badge_style);
798 let dom = badge.dom();
799
800 assert!(has_class(&dom, "__azul-native-badge"), "{kind:?}: missing the widget class");
801 assert!(dom.children.as_ref().is_empty(), "{kind:?}: a badge is a single text node, not a subtree");
802 assert_eq!(inline_properties(&dom), expected, "{kind:?}: the pill lost its computed style");
803
804 match dom.root.get_node_type() {
805 NodeType::Text(s) => assert_eq!(s.as_ref().as_str(), "99+", "{kind:?}: the label was mangled"),
806 other => panic!("{kind:?}: expected a text node, got {other:?}"),
807 }
808 }
809 }
810
811 #[test]
812 fn dom_renders_the_kind_the_badge_was_last_set_to() {
813 for kind in ALL_KINDS {
816 let mut badge = Badge::create(AzString::from_const_str("9"));
817 badge.set_kind(BadgeKind::Danger);
818 badge.set_kind(kind);
819 let expected = properties(&build_badge_style(kind));
820 assert_eq!(inline_properties(&badge.dom()), expected, "{kind:?}: the DOM does not show the current kind");
821 }
822 }
823
824 #[test]
825 fn dom_preserves_adversarial_labels_verbatim() {
826 for s in adversarial_strings() {
827 let dom = Badge::create(AzString::from(s.clone())).dom();
828 match dom.root.get_node_type() {
829 NodeType::Text(t) => {
830 assert_eq!(t.as_ref().as_str(), s.as_str(), "the label changed on its way into the DOM");
831 assert_eq!(t.as_ref().len(), s.len(), "byte length changed (NUL truncation?)");
832 }
833 other => panic!("expected a text node, got {other:?}"),
834 }
835 assert!(has_class(&dom, "__azul-native-badge"));
836 }
837 }
838
839 #[test]
840 fn from_badge_for_dom_is_exactly_dom() {
841 for kind in ALL_KINDS {
842 let badge = Badge::with_kind(AzString::from_const_str("ok"), kind);
843 let via_into: Dom = badge.clone().into();
844 let via_dom = badge.dom();
845 assert_eq!(inline_properties(&via_into), inline_properties(&via_dom), "{kind:?}: `From` diverges from `dom()`");
846 assert_eq!(via_into.root.get_node_type(), via_dom.root.get_node_type(), "{kind:?}: `From` built a different node");
847 }
848 }
849}