1use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
4use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
5#[allow(clippy::wildcard_imports)] use azul_css::{
7 props::{
8 basic::*,
9 layout::*,
10 property::{CssProperty, *},
11 style::*,
12 },
13 *,
14};
15
16#[derive(Debug, Clone)]
18#[repr(C)]
19pub struct Label {
20 pub string: AzString,
21 pub label_style: CssPropertyWithConditionsVec,
22}
23
24const SANS_SERIF_STR: &str = "system:ui";
25const SANS_SERIF: AzString = AzString::from_const_str(SANS_SERIF_STR);
26const SANS_SERIF_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SANS_SERIF)];
27const SANS_SERIF_FAMILY: StyleFontFamilyVec =
28 StyleFontFamilyVec::from_const_slice(SANS_SERIF_FAMILIES);
29
30const COLOR_4C4C4C: ColorU = ColorU {
32 r: 76,
33 g: 76,
34 b: 76,
35 a: 255,
36};
37
38static LABEL_STYLE_DEFAULT: &[CssPropertyWithConditions] = &[
39 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
40 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
41 LayoutFlexDirection::Column,
42 )),
43 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
44 LayoutJustifyContent::Center,
45 )),
46 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
47 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
48 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
49 inner: COLOR_4C4C4C,
50 })),
51 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
52 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
53 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
54];
55
56static LABEL_STYLE_MAC: &[CssPropertyWithConditions] = &[
57 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
58 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
59 LayoutFlexDirection::Column,
60 )),
61 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
62 LayoutJustifyContent::Center,
63 )),
64 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
65 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
66 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
67 inner: COLOR_4C4C4C,
68 })),
69 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(12))),
70 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
71 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
72];
73
74static LABEL_STYLE_OTHER: &[CssPropertyWithConditions] = &[];
77
78impl Label {
79 #[inline]
81 #[must_use] pub fn create(string: AzString) -> Self {
82 Self {
83 string,
84 #[cfg(target_os = "windows")]
85 label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_DEFAULT),
86 #[cfg(target_os = "linux")]
87 label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_DEFAULT),
88 #[cfg(target_os = "macos")]
89 label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_MAC),
90 #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
91 label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_OTHER),
92 }
93 }
94
95 #[inline]
97 #[must_use]
98 pub fn swap_with_default(&mut self) -> Self {
99 let mut s = Self::create(AzString::from_const_str(""));
100 core::mem::swap(&mut s, self);
101 s
102 }
103
104 #[inline]
106 #[must_use] pub fn dom(self) -> Dom {
107 static LABEL_CLASS: &[IdOrClass] =
108 &[Class(AzString::from_const_str("__azul-native-label"))];
109
110 Dom::create_text(self.string)
111 .with_ids_and_classes(IdOrClassVec::from_const_slice(LABEL_CLASS))
112 .with_css_props(self.label_style)
113 }
114}
115
116impl From<Label> for Dom {
117 fn from(l: Label) -> Self {
118 l.dom()
119 }
120}
121
122#[cfg(test)]
123mod autotest_generated {
124 use std::collections::HashSet;
125
126 use azul_core::dom::NodeType;
127 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
128
129 use super::*;
130
131 const DECL_COUNT: usize = 9;
137
138 const LABEL_CLASS_NAME: &str = "__azul-native-label";
140
141 fn expected_table() -> &'static [CssPropertyWithConditions] {
147 if cfg!(target_os = "macos") {
148 LABEL_STYLE_MAC
149 } else if cfg!(any(target_os = "windows", target_os = "linux")) {
150 LABEL_STYLE_DEFAULT
151 } else {
152 LABEL_STYLE_OTHER
153 }
154 }
155
156 fn expected_font_size() -> Option<f32> {
159 if cfg!(target_os = "macos") {
160 Some(12.0)
161 } else if cfg!(any(target_os = "windows", target_os = "linux")) {
162 Some(13.0)
163 } else {
164 None
165 }
166 }
167
168 fn target_is_styled() -> bool {
170 !expected_table().is_empty()
171 }
172
173 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
175 v.as_ref().iter().map(|p| p.property.clone()).collect()
176 }
177
178 fn px(pv: &PixelValue) -> f32 {
182 assert_eq!(pv.metric, SizeMetric::Px, "label lengths must be absolute px, got {:?}", pv.metric);
183 pv.number.get()
184 }
185
186 fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
187 v.as_ref().iter().find_map(|p| match &p.property {
188 CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
189 _ => None,
190 })
191 }
192
193 fn text_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
194 v.as_ref().iter().find_map(|p| match &p.property {
195 CssProperty::TextColor(c) => c.get_property().map(|c| c.inner),
196 _ => None,
197 })
198 }
199
200 fn flex_grow(v: &CssPropertyWithConditionsVec) -> Option<f32> {
201 v.as_ref().iter().find_map(|p| match &p.property {
202 CssProperty::FlexGrow(f) => f.get_property().map(|f| f.inner.get()),
203 _ => None,
204 })
205 }
206
207 fn font_families(v: &CssPropertyWithConditionsVec) -> Option<Vec<StyleFontFamily>> {
208 v.as_ref().iter().find_map(|p| match &p.property {
209 CssProperty::FontFamily(f) => f.get_property().map(|f| f.as_ref().to_vec()),
210 _ => None,
211 })
212 }
213
214 fn has_class(node: &Dom, name: &str) -> bool {
216 node.root
217 .get_ids_and_classes()
218 .as_ref()
219 .iter()
220 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == name))
221 }
222
223 fn inline_properties(node: &Dom) -> Vec<CssProperty> {
225 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
226 }
227
228 fn text_of(node: &Dom) -> Option<&str> {
230 match node.root.get_node_type() {
231 NodeType::Text(s) => Some(s.as_ref().as_str()),
232 _ => None,
233 }
234 }
235
236 fn assert_same_label(a: &Label, b: &Label, ctx: &str) {
238 assert_eq!(a.string, b.string, "{ctx}: the string differs");
239 assert_eq!(a.label_style, b.label_style, "{ctx}: the style differs");
240 }
241
242 fn adversarial_strings() -> Vec<String> {
246 let mut v: Vec<String> = [
247 "",
248 "Label",
249 " ",
250 "\t\n\r",
251 "e\u{0301}", "\u{212B}", "\u{C5}", "\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}", "system:ui", "__azul-native-label", "}\n#pwned { color: red; }", "<div>&</div>", "-9223372036854775808", ]
265 .iter()
266 .map(|s| (*s).to_string())
267 .collect();
268 v.push("x".repeat(200_000));
269 v.push("\0".repeat(1_000));
270 v
271 }
272
273 #[test]
278 fn create_stores_every_adversarial_string_verbatim() {
279 for s in adversarial_strings() {
283 let label = Label::create(AzString::from(s.clone()));
284 assert_eq!(label.string.as_str(), s.as_str(), "the string was rewritten");
285 assert_eq!(label.string.len(), s.len(), "byte length changed (NUL truncation?)");
286 assert_eq!(
287 label.string.chars().count(),
288 s.chars().count(),
289 "char count changed (lossy UTF-8 round-trip?)"
290 );
291 }
292 }
293
294 #[test]
295 fn create_never_normalises_or_folds_the_string() {
296 let angstrom = Label::create(AzString::from("\u{212B}".to_string()));
300 let a_ring = Label::create(AzString::from("\u{C5}".to_string()));
301 assert_ne!(angstrom.string, a_ring.string, "the two forms were normalised into one");
302 assert_eq!(angstrom.string.len(), 3, "U+212B must stay a 3-byte sequence");
303 assert_eq!(a_ring.string.len(), 2, "U+00C5 must stay a 2-byte sequence");
304 }
305
306 #[test]
307 fn create_uses_the_platform_table_for_this_target() {
308 let label = Label::create(AzString::from_const_str("hello"));
309 assert_eq!(
310 properties(&label.label_style),
311 expected_table().iter().map(|p| p.property.clone()).collect::<Vec<_>>(),
312 "the constructor picked the wrong platform style table"
313 );
314 assert_eq!(font_size_px(&label.label_style), expected_font_size());
315 }
316
317 #[test]
318 fn create_is_deterministic_and_independent_of_the_string() {
319 let a = Label::create(AzString::from_const_str(""));
322 let b = Label::create(AzString::from("x".repeat(100_000)));
323 assert_eq!(a.label_style, b.label_style, "the style varies with the label text");
324 assert_same_label(&a, &Label::create(AzString::from_const_str("")), "repeat construction");
325 }
326
327 #[test]
328 fn constructed_style_vec_has_consistent_length_and_capacity() {
329 let v = Label::create(AzString::from_const_str("hi")).label_style;
330 assert_eq!(v.len(), v.as_ref().len(), "len() disagrees with the slice view");
331 assert!(v.capacity() >= v.len(), "capacity {} < len {}", v.capacity(), v.len());
332 if target_is_styled() {
333 assert_eq!(v.len(), DECL_COUNT, "unexpected number of declarations");
334 } else {
335 assert!(v.is_empty(), "unsupported platforms are documented to get no styling");
336 }
337 }
338
339 #[test]
340 fn cloning_and_dropping_never_corrupts_the_shared_static_style() {
341 let base = Label::create(AzString::from("churn \u{1F600}".to_string()));
346 let expected_props = properties(&base.label_style);
347 for round in 0..1000 {
348 let c = base.clone();
349 assert_eq!(c.string.as_str(), "churn \u{1F600}", "clone {round}: the string diverged");
350 assert_eq!(properties(&c.label_style), expected_props, "clone {round}: the style diverged");
351 drop(c);
352 }
353 assert_eq!(base.string.as_str(), "churn \u{1F600}", "the original string was damaged");
354 assert_eq!(properties(&base.label_style), expected_props, "the original style was damaged");
355 assert_eq!(
356 properties(&Label::create(AzString::from_const_str("x")).label_style),
357 expected_props,
358 "a freshly built label disagrees after 1000 clone/drop cycles"
359 );
360 }
361
362 #[test]
363 fn a_const_str_label_survives_clone_churn_of_its_static_backing() {
364 let base = Label::create(AzString::from_const_str(LABEL_CLASS_NAME));
368 for round in 0..1000 {
369 let c = base.clone();
370 assert_eq!(c.string.as_str(), LABEL_CLASS_NAME, "clone {round}: static backing corrupted");
371 drop(c);
372 }
373 assert_eq!(base.string.as_str(), LABEL_CLASS_NAME);
374 }
375
376 #[test]
381 fn the_two_populated_tables_differ_only_in_font_size() {
382 assert_eq!(LABEL_STYLE_DEFAULT.len(), DECL_COUNT);
385 assert_eq!(LABEL_STYLE_MAC.len(), DECL_COUNT, "the two tables have diverged in length");
386 for (i, (d, m)) in LABEL_STYLE_DEFAULT.iter().zip(LABEL_STYLE_MAC.iter()).enumerate() {
387 assert_eq!(
388 core::mem::discriminant(&d.property),
389 core::mem::discriminant(&m.property),
390 "declaration {i} is a different property on the two platforms"
391 );
392 if matches!(d.property, CssProperty::FontSize(_)) {
393 assert_ne!(d.property, m.property, "the two tables were expected to differ in font size");
394 } else {
395 assert_eq!(d.property, m.property, "declaration {i} drifted between the two tables");
396 }
397 }
398 }
399
400 #[test]
401 fn label_style_other_is_empty_as_documented() {
402 assert!(LABEL_STYLE_OTHER.is_empty(), "the fallback table is no longer empty");
406 }
407
408 #[test]
409 fn every_declaration_is_unconditional() {
410 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
413 for p in table {
414 assert!(
415 p.apply_if.as_ref().is_empty(),
416 "{name}: {:?} is conditional on a stateless widget",
417 p.property
418 );
419 }
420 }
421 }
422
423 #[test]
424 fn no_property_is_declared_twice() {
425 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
428 let mut seen = HashSet::new();
429 for p in table {
430 assert!(
431 seen.insert(core::mem::discriminant(&p.property)),
432 "{name}: duplicate declaration of {:?}",
433 p.property
434 );
435 }
436 assert_eq!(seen.len(), table.len());
437 }
438 }
439
440 #[test]
441 fn the_text_colour_is_the_documented_opaque_grey() {
442 assert_eq!(COLOR_4C4C4C, ColorU { r: 76, g: 76, b: 76, a: 255 });
445 assert_eq!(COLOR_4C4C4C.a, 255, "a translucent label lets the background bleed through");
446 assert_eq!(COLOR_4C4C4C.r, COLOR_4C4C4C.g, "the label colour is not neutral grey");
447 assert_eq!(COLOR_4C4C4C.g, COLOR_4C4C4C.b, "the label colour is not neutral grey");
448
449 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
450 let v = CssPropertyWithConditionsVec::from_const_slice(table);
451 assert_eq!(text_color(&v), Some(COLOR_4C4C4C), "{name}: wrong text colour");
452 }
453 }
454
455 #[test]
456 fn every_font_size_is_a_finite_positive_absolute_px() {
457 for (name, table, want) in [("default", LABEL_STYLE_DEFAULT, 13.0), ("mac", LABEL_STYLE_MAC, 12.0)] {
460 let v = CssPropertyWithConditionsVec::from_const_slice(table);
461 let size = font_size_px(&v).expect("a label must declare a font size");
462 assert!(size.is_finite(), "{name}: non-finite font size {size}");
463 assert!(!size.is_nan(), "{name}: NaN font size");
464 assert!(size > 0.0, "{name}: a {size}px label is invisible");
465 assert!(size <= 128.0, "{name}: {size}px is implausible for a UI label");
466 assert_eq!(size, want, "{name}: unexpected font size");
467 }
468 }
469
470 #[test]
471 fn the_fixed_point_length_encoding_round_trips() {
472 assert_eq!(PixelValue::const_px(13).number.get(), 13.0);
474 assert_eq!(PixelValue::const_px(12).number.get(), 12.0);
475 assert_eq!(StyleFontSize::const_px(13).inner.number.get(), 13.0);
476 assert_eq!(StyleFontSize::const_px(12).inner.number.get(), 12.0);
477 assert_eq!(LayoutFlexGrow::const_new(1).inner.get(), 1.0);
478
479 let d: Vec<CssProperty> = LABEL_STYLE_DEFAULT.iter().map(|p| p.property.clone()).collect();
482 assert!(d.contains(&CssProperty::const_font_size(StyleFontSize::const_px(13))));
483 let m: Vec<CssProperty> = LABEL_STYLE_MAC.iter().map(|p| p.property.clone()).collect();
484 assert!(m.contains(&CssProperty::const_font_size(StyleFontSize::const_px(12))));
485 }
486
487 #[test]
488 fn flex_grow_is_exactly_one_and_not_a_rounding_artefact() {
489 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
492 let v = CssPropertyWithConditionsVec::from_const_slice(table);
493 let g = flex_grow(&v).expect("flex-grow must be declared");
494 assert!(g.is_finite(), "{name}: non-finite flex-grow {g}");
495 assert_eq!(g, 1.0, "{name}: flex-grow is {g}, not 1");
496 assert!(g.is_sign_positive(), "{name}: flex-grow decoded as -0.0");
497 }
498 }
499
500 #[test]
501 fn the_font_family_is_a_single_system_ui_entry() {
502 let sentinel: AzString = SANS_SERIF;
505 let family_vec: StyleFontFamilyVec = SANS_SERIF_FAMILY;
506
507 assert_eq!(SANS_SERIF_STR, "system:ui");
508 assert_eq!(sentinel.as_str(), SANS_SERIF_STR, "the const AzString lost its backing str");
509 assert_eq!(sentinel.as_str().len(), SANS_SERIF_STR.len());
510 assert_eq!(SANS_SERIF_FAMILIES.len(), 1, "the fallback chain changed length");
511 assert_eq!(family_vec.as_ref().len(), 1, "the const vec disagrees with its slice");
512
513 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
514 let v = CssPropertyWithConditionsVec::from_const_slice(table);
515 let fams = font_families(&v).expect("a label must declare a font family");
516 assert_eq!(fams.len(), 1, "{name}: unexpected fallback chain length");
517 match &fams[0] {
518 StyleFontFamily::System(s) => assert_eq!(s.as_str(), "system:ui", "{name}: wrong family name"),
522 other => panic!("{name}: unexpected font family variant {other:?}"),
523 }
524 }
525 }
526
527 #[test]
528 fn the_centering_declarations_are_mutually_consistent() {
529 for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
532 let props: Vec<CssProperty> = table.iter().map(|p| p.property.clone()).collect();
533 let has = |p: &CssProperty| props.contains(p);
534 assert!(has(&CssProperty::const_display(LayoutDisplay::Flex)), "{name}: not a flex box");
535 assert!(
536 has(&CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
537 "{name}: not a column"
538 );
539 assert!(
540 has(&CssProperty::const_justify_content(LayoutJustifyContent::Center)),
541 "{name}: not centred on the main axis"
542 );
543 assert!(
544 has(&CssProperty::const_align_items(LayoutAlignItems::Center)),
545 "{name}: not centred on the cross axis"
546 );
547 assert!(
548 has(&CssProperty::const_text_align(StyleTextAlign::Center)),
549 "{name}: the text itself is not centred"
550 );
551 }
552 }
553
554 #[test]
559 fn swap_with_default_returns_the_original_and_leaves_an_empty_label() {
560 let mut label = Label::create(AzString::from("original \u{1F600}".to_string()));
561 let expected_style = label.label_style.clone();
562 let taken = label.swap_with_default();
563
564 assert_eq!(taken.string.as_str(), "original \u{1F600}", "the wrong value was returned");
566 assert_eq!(taken.label_style, expected_style, "the returned label lost its style");
567
568 assert_eq!(label.string.as_str(), "", "what was left behind is not empty");
571 assert!(label.string.is_empty());
572 assert_same_label(&label, &Label::create(AzString::from_const_str("")), "left-behind label");
573 }
574
575 #[test]
576 fn swap_with_default_is_idempotent_on_an_already_empty_label() {
577 let mut label = Label::create(AzString::from_const_str(""));
578 let first = label.swap_with_default();
579 let second = label.swap_with_default();
580 assert_eq!(first.string.as_str(), "");
581 assert_eq!(second.string.as_str(), "");
582 assert_same_label(&label, &Label::create(AzString::from_const_str("")), "after two swaps");
583 }
584
585 #[test]
586 fn repeated_swaps_never_corrupt_the_static_backed_style() {
587 let mut label = Label::create(AzString::from("swap me".to_string()));
590 for round in 0..200 {
591 let taken = label.swap_with_default();
592 if round == 0 {
593 assert_eq!(taken.string.as_str(), "swap me", "round 0: wrong value returned");
594 } else {
595 assert_eq!(taken.string.as_str(), "", "round {round}: the emptied slot was not empty");
596 }
597 assert_eq!(properties(&taken.label_style), properties(&label.label_style), "round {round}");
598 assert_eq!(label.string.as_str(), "", "round {round}: what was left behind is not empty");
599 drop(taken);
600 }
601 assert_same_label(&label, &Label::create(AzString::from_const_str("")), "after 200 swaps");
602 }
603
604 #[test]
605 fn swap_with_default_returns_a_custom_style_untouched() {
606 let custom = CssPropertyWithConditionsVec::from_vec(vec![CssPropertyWithConditions::simple(
609 CssProperty::const_font_size(StyleFontSize::const_px(42)),
610 )]);
611 let mut label = Label {
612 string: AzString::from_const_str("custom"),
613 label_style: custom,
614 };
615 let taken = label.swap_with_default();
616 assert_eq!(taken.string.as_str(), "custom");
617 assert_eq!(taken.label_style.len(), 1, "the custom style was rewritten on the way out");
618 assert_eq!(font_size_px(&taken.label_style), Some(42.0));
619 assert_eq!(font_size_px(&label.label_style), expected_font_size(), "the 42px override survived");
621 }
622
623 #[test]
624 fn swap_with_default_hands_back_a_huge_string_without_truncating_it() {
625 let huge = "\u{1F600}".repeat(50_000);
626 let mut label = Label::create(AzString::from(huge.clone()));
627 let taken = label.swap_with_default();
628 assert_eq!(taken.string.len(), huge.len(), "the huge string was truncated on the way out");
629 assert_eq!(taken.string.as_str(), huge.as_str());
630 assert!(label.string.is_empty());
631 }
632
633 #[test]
638 fn dom_is_a_single_classed_text_node_carrying_the_computed_style() {
639 let label = Label::create(AzString::from_const_str("Hello"));
640 let expected = properties(&label.label_style);
641 let dom = label.dom();
642
643 assert_eq!(text_of(&dom), Some("Hello"), "the label is not a text node, or was mangled");
644 assert!(has_class(&dom, LABEL_CLASS_NAME), "missing the widget class");
645 assert_eq!(
646 dom.root.get_ids_and_classes().as_ref().len(),
647 1,
648 "expected exactly one class and no ids"
649 );
650 assert!(dom.children.as_ref().is_empty(), "a label is a leaf, not a subtree");
651 assert_eq!(dom.estimated_total_children, 0, "a leaf must not claim descendants");
652 assert!(dom.css.as_ref().is_empty(), "a label must not attach a scoped stylesheet");
653 assert!(dom.root.callbacks.as_ref().is_empty(), "a static widget must not bind callbacks");
654 assert_eq!(inline_properties(&dom), expected, "the label lost its computed style");
655 }
656
657 #[test]
658 fn dom_preserves_adversarial_labels_verbatim() {
659 for s in adversarial_strings() {
660 let dom = Label::create(AzString::from(s.clone())).dom();
661 let t = text_of(&dom).expect("expected a text node");
662 assert_eq!(t, s.as_str(), "the label changed on its way into the DOM");
663 assert_eq!(t.len(), s.len(), "byte length changed (NUL truncation?)");
664 assert!(has_class(&dom, LABEL_CLASS_NAME), "the class was lost for {s:?}");
665 assert_eq!(inline_properties(&dom).len(), expected_table().len(), "style varies with the text");
668 }
669 }
670
671 #[test]
672 fn dom_of_an_empty_label_is_still_a_classed_text_node() {
673 let dom = Label::create(AzString::from_const_str("")).dom();
676 assert_eq!(text_of(&dom), Some(""), "the empty label is not a text node");
677 assert!(has_class(&dom, LABEL_CLASS_NAME));
678 assert_eq!(inline_properties(&dom).len(), expected_table().len());
679 }
680
681 #[test]
682 fn dom_renders_the_style_field_verbatim_even_when_hand_built() {
683 let custom = CssPropertyWithConditionsVec::from_vec(vec![
686 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(99))),
687 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
688 ]);
689 let dom = Label {
690 string: AzString::from_const_str("hand built"),
691 label_style: custom.clone(),
692 }
693 .dom();
694 assert_eq!(inline_properties(&dom), properties(&custom), "the custom style was rewritten");
695 assert!(has_class(&dom, LABEL_CLASS_NAME), "a hand-built label lost the widget class");
696 assert_eq!(text_of(&dom), Some("hand built"));
697 }
698
699 #[test]
700 fn dom_of_a_style_less_label_carries_no_inline_declarations() {
701 let dom = Label {
704 string: AzString::from_const_str("bare"),
705 label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_OTHER),
706 }
707 .dom();
708 assert!(inline_properties(&dom).is_empty(), "an empty style vec produced declarations");
709 assert!(has_class(&dom, LABEL_CLASS_NAME));
710 assert_eq!(text_of(&dom), Some("bare"));
711 }
712
713 #[test]
714 fn the_widget_class_is_a_namespaced_ascii_css_identifier() {
715 let dom = Label::create(AzString::from_const_str("x")).dom();
716 let classes = dom.root.get_ids_and_classes();
717 let name = classes
718 .as_ref()
719 .iter()
720 .find_map(|c| match c {
721 IdOrClass::Class(s) => Some(s.as_str().to_string()),
722 IdOrClass::Id(_) => None,
723 })
724 .expect("the label must carry a class");
725
726 assert_eq!(name, LABEL_CLASS_NAME);
727 assert!(!name.is_empty(), "empty class name");
728 assert!(name.is_ascii(), "non-ASCII class name {name:?}");
729 assert!(name.starts_with("__azul-native-"), "unnamespaced class {name:?}");
730 assert!(
732 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
733 "class name {name:?} contains a CSS-significant character"
734 );
735 }
736
737 #[test]
738 fn a_label_whose_text_is_the_class_name_does_not_gain_a_second_class() {
739 let dom = Label::create(AzString::from_const_str(LABEL_CLASS_NAME)).dom();
742 assert_eq!(dom.root.get_ids_and_classes().as_ref().len(), 1, "the text leaked into the class list");
743 assert_eq!(text_of(&dom), Some(LABEL_CLASS_NAME));
744 }
745
746 #[test]
747 fn from_label_for_dom_is_exactly_dom() {
748 for s in ["", "ok", "\u{1F600}\0"] {
749 let label = Label::create(AzString::from(s.to_string()));
750 let via_into: Dom = label.clone().into();
751 let via_dom = label.dom();
752 assert_eq!(via_into, via_dom, "{s:?}: `From` diverges from `dom()`");
753 }
754 }
755
756 #[test]
757 fn building_many_doms_never_corrupts_the_shared_static_class_list() {
758 for round in 0..1000 {
761 let dom = Label::create(AzString::from_const_str("churn")).dom();
762 assert!(has_class(&dom, LABEL_CLASS_NAME), "round {round}: the static class list was corrupted");
763 drop(dom);
764 }
765 let dom = Label::create(AzString::from_const_str("churn")).dom();
766 assert!(has_class(&dom, LABEL_CLASS_NAME), "the static class list did not survive the churn");
767 assert_eq!(inline_properties(&dom).len(), expected_table().len());
768 }
769}