1
//! Label widget for displaying static text with platform-specific default styling.
2

            
3
use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
4
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
5
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
6
use azul_css::{
7
    props::{
8
        basic::*,
9
        layout::*,
10
        property::{CssProperty, *},
11
        style::*,
12
    },
13
    *,
14
};
15

            
16
/// A static text label widget with platform-appropriate default styling.
17
#[derive(Debug, Clone)]
18
#[repr(C)]
19
pub struct Label {
20
    pub string: AzString,
21
    pub label_style: CssPropertyWithConditionsVec,
22
}
23

            
24
const SANS_SERIF_STR: &str = "system:ui";
25
const SANS_SERIF: AzString = AzString::from_const_str(SANS_SERIF_STR);
26
const SANS_SERIF_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SANS_SERIF)];
27
const SANS_SERIF_FAMILY: StyleFontFamilyVec =
28
    StyleFontFamilyVec::from_const_slice(SANS_SERIF_FAMILIES);
29

            
30
/// Standard label text color (#4C4C4C), matching platform UI defaults.
31
const COLOR_4C4C4C: ColorU = ColorU {
32
    r: 76,
33
    g: 76,
34
    b: 76,
35
    a: 255,
36
};
37

            
38
static 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

            
56
static 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

            
74
/// No default styling on unsupported platforms (e.g. WASM, FreeBSD);
75
/// callers should provide explicit styles via `label_style`.
76
static LABEL_STYLE_OTHER: &[CssPropertyWithConditions] = &[];
77

            
78
impl Label {
79
    /// Creates a new label with the given text and platform-specific default styling.
80
    #[inline]
81
1270
    #[must_use] pub fn create(string: AzString) -> Self {
82
1270
        Self {
83
1270
            string,
84
1270
            #[cfg(target_os = "windows")]
85
1270
            label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_DEFAULT),
86
1270
            #[cfg(target_os = "linux")]
87
1270
            label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_DEFAULT),
88
1270
            #[cfg(target_os = "macos")]
89
1270
            label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_MAC),
90
1270
            #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
91
1270
            label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_OTHER),
92
1270
        }
93
1270
    }
94

            
95
    /// Replaces `self` with an empty default label, returning the original.
96
    #[inline]
97
    #[must_use]
98
205
    pub fn swap_with_default(&mut self) -> Self {
99
205
        let mut s = Self::create(AzString::from_const_str(""));
100
205
        core::mem::swap(&mut s, self);
101
205
        s
102
205
    }
103

            
104
    /// Converts this label into a `<p>` block carrying the
105
    /// `__azul-native-label` class and wrapping a bare text node.
106
    ///
107
    /// The `<p>` is the styled box: a `NodeType::Text` node is always
108
    /// inline-level and owns no rect, so every box-model property here would
109
    /// be inert on a raw text node.
110
    #[inline]
111
1034
    #[must_use] pub fn dom(self) -> Dom {
112
        static LABEL_CLASS: &[IdOrClass] =
113
            &[Class(AzString::from_const_str("__azul-native-label"))];
114

            
115
1034
        Dom::create_p_with_text(self.string)
116
1034
            .with_ids_and_classes(IdOrClassVec::from_const_slice(LABEL_CLASS))
117
1034
            .with_css_props(self.label_style)
118
1034
    }
119
}
120

            
121
impl From<Label> for Dom {
122
3
    fn from(l: Label) -> Self {
123
3
        l.dom()
124
3
    }
125
}
126

            
127
#[cfg(test)]
128
mod autotest_generated {
129
    use std::collections::HashSet;
130

            
131
    use azul_core::dom::NodeType;
132
    use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
133

            
134
    use super::*;
135

            
136
    // ------------------------------------------------------------------
137
    // Helpers
138
    // ------------------------------------------------------------------
139

            
140
    /// The number of declarations each *populated* platform table carries.
141
    const DECL_COUNT: usize = 9;
142

            
143
    /// The class `dom()` stamps onto the label `<p>`.
144
    const LABEL_CLASS_NAME: &str = "__azul-native-label";
145

            
146
    /// The style table `Label::create` is expected to pick on *this* target.
147
    ///
148
    /// Uses `cfg!` rather than `#[cfg]` so every branch still type-checks on
149
    /// every platform — a table that stopped compiling on macOS would otherwise
150
    /// only be caught by a macOS CI run.
151
    fn expected_table() -> &'static [CssPropertyWithConditions] {
152
        if cfg!(target_os = "macos") {
153
            LABEL_STYLE_MAC
154
        } else if cfg!(any(target_os = "windows", target_os = "linux")) {
155
            LABEL_STYLE_DEFAULT
156
        } else {
157
            LABEL_STYLE_OTHER
158
        }
159
    }
160

            
161
    /// The font size `Label::create` bakes in on this target — `None` on the
162
    /// platforms that are documented to get no default styling at all.
163
    fn expected_font_size() -> Option<f32> {
164
        if cfg!(target_os = "macos") {
165
            Some(12.0)
166
        } else if cfg!(any(target_os = "windows", target_os = "linux")) {
167
            Some(13.0)
168
        } else {
169
            None
170
        }
171
    }
172

            
173
    /// True when this target is one of the platforms that gets a real table.
174
    fn target_is_styled() -> bool {
175
        !expected_table().is_empty()
176
    }
177

            
178
    /// The declared properties of a style vec, in declaration order.
179
    fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
180
        v.as_ref().iter().map(|p| p.property.clone()).collect()
181
    }
182

            
183
    /// The `f32` of a `PixelValue`, asserting it is an absolute `px` length. A
184
    /// font size declared in `em`/`%` would resolve against the *parent* font
185
    /// and make the "platform default" scale with whatever it is dropped into.
186
    fn px(pv: &PixelValue) -> f32 {
187
        assert_eq!(pv.metric, SizeMetric::Px, "label lengths must be absolute px, got {:?}", pv.metric);
188
        pv.number.get()
189
    }
190

            
191
    fn font_size_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
192
        v.as_ref().iter().find_map(|p| match &p.property {
193
            CssProperty::FontSize(f) => f.get_property().map(|f| px(&f.inner)),
194
            _ => None,
195
        })
196
    }
197

            
198
    fn text_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
199
        v.as_ref().iter().find_map(|p| match &p.property {
200
            CssProperty::TextColor(c) => c.get_property().map(|c| c.inner),
201
            _ => None,
202
        })
203
    }
204

            
205
    fn flex_grow(v: &CssPropertyWithConditionsVec) -> Option<f32> {
206
        v.as_ref().iter().find_map(|p| match &p.property {
207
            CssProperty::FlexGrow(f) => f.get_property().map(|f| f.inner.get()),
208
            _ => None,
209
        })
210
    }
211

            
212
    fn font_families(v: &CssPropertyWithConditionsVec) -> Option<Vec<StyleFontFamily>> {
213
        v.as_ref().iter().find_map(|p| match &p.property {
214
            CssProperty::FontFamily(f) => f.get_property().map(|f| f.as_ref().to_vec()),
215
            _ => None,
216
        })
217
    }
218

            
219
    /// True if `node` carries the CSS class `name`.
220
    fn has_class(node: &Dom, name: &str) -> bool {
221
        node.root
222
            .get_ids_and_classes()
223
            .as_ref()
224
            .iter()
225
            .any(|c| matches!(c, Class(s) if s.as_str() == name))
226
    }
227

            
228
    /// The properties of a rendered node's *inline* style, in declaration order.
229
    fn inline_properties(node: &Dom) -> Vec<CssProperty> {
230
        node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
231
    }
232

            
233
    /// The text carried by a text node, looking through the `<p>` block
234
    /// wrapper the label convention mandates (`p > text`).
235
    fn text_of(node: &Dom) -> Option<&str> {
236
        match node.root.get_node_type() {
237
            NodeType::Text(s) => Some(s.as_ref().as_str()),
238
            NodeType::P => match node.children.as_ref() {
239
                [only] => match only.root.get_node_type() {
240
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
241
                    _ => None,
242
                },
243
                _ => None,
244
            },
245
            _ => None,
246
        }
247
    }
248

            
249
    /// `Label` derives neither `PartialEq` nor `Default`, so compare field-wise.
250
    fn assert_same_label(a: &Label, b: &Label, ctx: &str) {
251
        assert_eq!(a.string, b.string, "{ctx}: the string differs");
252
        assert_eq!(a.label_style, b.label_style, "{ctx}: the style differs");
253
    }
254

            
255
    /// Inputs a label must survive verbatim. Empty, whitespace-only, embedded
256
    /// NUL, combining marks, ZWJ sequences, bidi overrides, strings that look
257
    /// like this widget's own sentinels, and one very large allocation.
258
    fn adversarial_strings() -> Vec<String> {
259
        let mut v: Vec<String> = [
260
            "",
261
            "Label",
262
            " ",
263
            "\t\n\r",
264
            "e\u{0301}",                                   // e + combining acute
265
            "\u{212B}",                                    // ANGSTROM SIGN (NFC-folds to U+00C5)
266
            "\u{C5}",                                      // the folded form — must stay distinct
267
            "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}", // ZWJ family emoji
268
            "\u{5E9}\u{5DC}\u{5D5}\u{5DD}",                // RTL Hebrew
269
            "\0",                                          // a single NUL
270
            "a\0b",                                        // embedded NUL
271
            "\u{FFFD}\u{202E}\u{200B}",                    // replacement, RTL override, ZWSP
272
            "system:ui",                                   // the font sentinel this file bakes in
273
            "__azul-native-label",                         // this widget's own class name
274
            "}\n#pwned { color: red; }",                   // CSS-injection shaped
275
            "<div>&amp;</div>",                            // markup shaped
276
            "-9223372036854775808",                        // i64::MIN
277
        ]
278
        .iter()
279
        .map(|s| (*s).to_string())
280
        .collect();
281
        v.push("x".repeat(200_000));
282
        v.push("\0".repeat(1_000));
283
        v
284
    }
285

            
286
    // ------------------------------------------------------------------
287
    // Label::create  (constructor)
288
    // ------------------------------------------------------------------
289

            
290
    #[test]
291
    fn create_stores_every_adversarial_string_verbatim() {
292
        // The string travels through `AzString` (an FFI `U8Vec`), so a
293
        // C-string-style NUL truncation or a lossy UTF-8 round-trip would show
294
        // up as a shortened or mangled label here first.
295
        for s in adversarial_strings() {
296
            let label = Label::create(AzString::from(s.clone()));
297
            assert_eq!(label.string.as_str(), s.as_str(), "the string was rewritten");
298
            assert_eq!(label.string.len(), s.len(), "byte length changed (NUL truncation?)");
299
            assert_eq!(
300
                label.string.chars().count(),
301
                s.chars().count(),
302
                "char count changed (lossy UTF-8 round-trip?)"
303
            );
304
        }
305
    }
306

            
307
    #[test]
308
    fn create_never_normalises_or_folds_the_string() {
309
        // U+212B and U+00C5 render identically but are different code points;
310
        // a normalisation pass hidden in the string plumbing would silently
311
        // merge them and break byte-exact round-trips through FFI.
312
        let angstrom = Label::create(AzString::from("\u{212B}".to_string()));
313
        let a_ring = Label::create(AzString::from("\u{C5}".to_string()));
314
        assert_ne!(angstrom.string, a_ring.string, "the two forms were normalised into one");
315
        assert_eq!(angstrom.string.len(), 3, "U+212B must stay a 3-byte sequence");
316
        assert_eq!(a_ring.string.len(), 2, "U+00C5 must stay a 2-byte sequence");
317
    }
318

            
319
    #[test]
320
    fn create_uses_the_platform_table_for_this_target() {
321
        let label = Label::create(AzString::from_const_str("hello"));
322
        assert_eq!(
323
            properties(&label.label_style),
324
            expected_table().iter().map(|p| p.property.clone()).collect::<Vec<_>>(),
325
            "the constructor picked the wrong platform style table"
326
        );
327
        assert_eq!(font_size_px(&label.label_style), expected_font_size());
328
    }
329

            
330
    #[test]
331
    fn create_is_deterministic_and_independent_of_the_string() {
332
        // The style must not depend on the text: two labels built from wildly
333
        // different strings must carry byte-identical styling.
334
        let a = Label::create(AzString::from_const_str(""));
335
        let b = Label::create(AzString::from("x".repeat(100_000)));
336
        assert_eq!(a.label_style, b.label_style, "the style varies with the label text");
337
        assert_same_label(&a, &Label::create(AzString::from_const_str("")), "repeat construction");
338
    }
339

            
340
    #[test]
341
    fn constructed_style_vec_has_consistent_length_and_capacity() {
342
        let v = Label::create(AzString::from_const_str("hi")).label_style;
343
        assert_eq!(v.len(), v.as_ref().len(), "len() disagrees with the slice view");
344
        assert!(v.capacity() >= v.len(), "capacity {} < len {}", v.capacity(), v.len());
345
        if target_is_styled() {
346
            assert_eq!(v.len(), DECL_COUNT, "unexpected number of declarations");
347
        } else {
348
            assert!(v.is_empty(), "unsupported platforms are documented to get no styling");
349
        }
350
    }
351

            
352
    #[test]
353
    fn cloning_and_dropping_never_corrupts_the_shared_static_style() {
354
        // `label_style` borrows a `'static` slice (`NoDestructor`) while
355
        // `string` may be heap-owned. A clone that wrongly claims ownership of
356
        // the static, or a `Drop` that frees it, would corrupt every future
357
        // label — so churn hard and re-check both the original and a fresh build.
358
        let base = Label::create(AzString::from("churn \u{1F600}".to_string()));
359
        let expected_props = properties(&base.label_style);
360
        for round in 0..1000 {
361
            let c = base.clone();
362
            assert_eq!(c.string.as_str(), "churn \u{1F600}", "clone {round}: the string diverged");
363
            assert_eq!(properties(&c.label_style), expected_props, "clone {round}: the style diverged");
364
            drop(c);
365
        }
366
        assert_eq!(base.string.as_str(), "churn \u{1F600}", "the original string was damaged");
367
        assert_eq!(properties(&base.label_style), expected_props, "the original style was damaged");
368
        assert_eq!(
369
            properties(&Label::create(AzString::from_const_str("x")).label_style),
370
            expected_props,
371
            "a freshly built label disagrees after 1000 clone/drop cycles"
372
        );
373
    }
374

            
375
    #[test]
376
    fn a_const_str_label_survives_clone_churn_of_its_static_backing() {
377
        // `AzString::from_const_str` is `NoDestructor`-backed; `clone_self`
378
        // copies it. A shallow clone plus a `Drop` that freed the `'static`
379
        // would be a use-after-free the very next read.
380
        let base = Label::create(AzString::from_const_str(LABEL_CLASS_NAME));
381
        for round in 0..1000 {
382
            let c = base.clone();
383
            assert_eq!(c.string.as_str(), LABEL_CLASS_NAME, "clone {round}: static backing corrupted");
384
            drop(c);
385
        }
386
        assert_eq!(base.string.as_str(), LABEL_CLASS_NAME);
387
    }
388

            
389
    // ------------------------------------------------------------------
390
    // The platform style tables
391
    // ------------------------------------------------------------------
392

            
393
    #[test]
394
    fn the_two_populated_tables_differ_only_in_font_size() {
395
        // They are hand-duplicated blocks: a fix applied to one and not the
396
        // other is the failure mode this file is most exposed to.
397
        assert_eq!(LABEL_STYLE_DEFAULT.len(), DECL_COUNT);
398
        assert_eq!(LABEL_STYLE_MAC.len(), DECL_COUNT, "the two tables have diverged in length");
399
        for (i, (d, m)) in LABEL_STYLE_DEFAULT.iter().zip(LABEL_STYLE_MAC.iter()).enumerate() {
400
            assert_eq!(
401
                core::mem::discriminant(&d.property),
402
                core::mem::discriminant(&m.property),
403
                "declaration {i} is a different property on the two platforms"
404
            );
405
            if matches!(d.property, CssProperty::FontSize(_)) {
406
                assert_ne!(d.property, m.property, "the two tables were expected to differ in font size");
407
            } else {
408
                assert_eq!(d.property, m.property, "declaration {i} drifted between the two tables");
409
            }
410
        }
411
    }
412

            
413
    #[test]
414
    fn label_style_other_is_empty_as_documented() {
415
        // The doc comment promises "no default styling on unsupported
416
        // platforms"; a stray declaration here would style WASM/FreeBSD
417
        // differently from everything the other two tables were tuned against.
418
        assert!(LABEL_STYLE_OTHER.is_empty(), "the fallback table is no longer empty");
419
    }
420

            
421
    #[test]
422
    fn every_declaration_is_unconditional() {
423
        // A label is stateless — a declaration gated on `:hover`/`:active`
424
        // would simply never paint.
425
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
426
            for p in table {
427
                assert!(
428
                    p.apply_if.as_ref().is_empty(),
429
                    "{name}: {:?} is conditional on a stateless widget",
430
                    p.property
431
                );
432
            }
433
        }
434
    }
435

            
436
    #[test]
437
    fn no_property_is_declared_twice() {
438
        // A duplicated declaration is a last-one-wins ambiguity: two font sizes
439
        // would make one of them silently dead.
440
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
441
            let mut seen = HashSet::new();
442
            for p in table {
443
                assert!(
444
                    seen.insert(core::mem::discriminant(&p.property)),
445
                    "{name}: duplicate declaration of {:?}",
446
                    p.property
447
                );
448
            }
449
            assert_eq!(seen.len(), table.len());
450
        }
451
    }
452

            
453
    #[test]
454
    fn the_text_colour_is_the_documented_opaque_grey() {
455
        // The doc comment pins it to #4C4C4C; a translucent label would let the
456
        // background bleed through the glyphs.
457
        assert_eq!(COLOR_4C4C4C, ColorU { r: 76, g: 76, b: 76, a: 255 });
458
        assert_eq!(COLOR_4C4C4C.a, 255, "a translucent label lets the background bleed through");
459
        assert_eq!(COLOR_4C4C4C.r, COLOR_4C4C4C.g, "the label colour is not neutral grey");
460
        assert_eq!(COLOR_4C4C4C.g, COLOR_4C4C4C.b, "the label colour is not neutral grey");
461

            
462
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
463
            let v = CssPropertyWithConditionsVec::from_const_slice(table);
464
            assert_eq!(text_color(&v), Some(COLOR_4C4C4C), "{name}: wrong text colour");
465
        }
466
    }
467

            
468
    #[test]
469
    fn every_font_size_is_a_finite_positive_absolute_px() {
470
        // Guard the `isize` -> `PixelValue` fixed-point conversion: a NaN, an
471
        // infinity, a zero or a negative size must never reach the shaper.
472
        for (name, table, want) in [("default", LABEL_STYLE_DEFAULT, 13.0), ("mac", LABEL_STYLE_MAC, 12.0)] {
473
            let v = CssPropertyWithConditionsVec::from_const_slice(table);
474
            let size = font_size_px(&v).expect("a label must declare a font size");
475
            assert!(size.is_finite(), "{name}: non-finite font size {size}");
476
            assert!(!size.is_nan(), "{name}: NaN font size");
477
            assert!(size > 0.0, "{name}: a {size}px label is invisible");
478
            assert!(size <= 128.0, "{name}: {size}px is implausible for a UI label");
479
            assert_eq!(size, want, "{name}: unexpected font size");
480
        }
481
    }
482

            
483
    #[test]
484
    fn the_fixed_point_length_encoding_round_trips() {
485
        // encode == decode for every numeric constant this file bakes in.
486
        assert_eq!(PixelValue::const_px(13).number.get(), 13.0);
487
        assert_eq!(PixelValue::const_px(12).number.get(), 12.0);
488
        assert_eq!(StyleFontSize::const_px(13).inner.number.get(), 13.0);
489
        assert_eq!(StyleFontSize::const_px(12).inner.number.get(), 12.0);
490
        assert_eq!(LayoutFlexGrow::const_new(1).inner.get(), 1.0);
491

            
492
        // ...and the values that actually landed in the tables are the ones the
493
        // declarations asked for.
494
        let d: Vec<CssProperty> = LABEL_STYLE_DEFAULT.iter().map(|p| p.property.clone()).collect();
495
        assert!(d.contains(&CssProperty::const_font_size(StyleFontSize::const_px(13))));
496
        let m: Vec<CssProperty> = LABEL_STYLE_MAC.iter().map(|p| p.property.clone()).collect();
497
        assert!(m.contains(&CssProperty::const_font_size(StyleFontSize::const_px(12))));
498
    }
499

            
500
    #[test]
501
    fn flex_grow_is_exactly_one_and_not_a_rounding_artefact() {
502
        // `FloatValue` stores a fixed-point `isize`; a botched encode/decode
503
        // would show up as 0.999 or -0.0 rather than a clean 1.
504
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
505
            let v = CssPropertyWithConditionsVec::from_const_slice(table);
506
            let g = flex_grow(&v).expect("flex-grow must be declared");
507
            assert!(g.is_finite(), "{name}: non-finite flex-grow {g}");
508
            assert_eq!(g, 1.0, "{name}: flex-grow is {g}, not 1");
509
            assert!(g.is_sign_positive(), "{name}: flex-grow decoded as -0.0");
510
        }
511
    }
512

            
513
    #[test]
514
    fn the_font_family_is_a_single_system_ui_entry() {
515
        // `SANS_SERIF` / `SANS_SERIF_FAMILY` are `const`, so each mention
516
        // materialises a fresh value — bind them once and read them back.
517
        let sentinel: AzString = SANS_SERIF;
518
        let family_vec: StyleFontFamilyVec = SANS_SERIF_FAMILY;
519

            
520
        assert_eq!(SANS_SERIF_STR, "system:ui");
521
        assert_eq!(sentinel.as_str(), SANS_SERIF_STR, "the const AzString lost its backing str");
522
        assert_eq!(sentinel.as_str().len(), SANS_SERIF_STR.len());
523
        assert_eq!(SANS_SERIF_FAMILIES.len(), 1, "the fallback chain changed length");
524
        assert_eq!(family_vec.as_ref().len(), 1, "the const vec disagrees with its slice");
525

            
526
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
527
            let v = CssPropertyWithConditionsVec::from_const_slice(table);
528
            let fams = font_families(&v).expect("a label must declare a font family");
529
            assert_eq!(fams.len(), 1, "{name}: unexpected fallback chain length");
530
            match &fams[0] {
531
                // Pinned as-is: this file spells the UI font as a *named*
532
                // family whose name happens to be the `system:ui` sentinel,
533
                // not as `StyleFontFamily::SystemType(SystemFontType::Ui)`.
534
                StyleFontFamily::System(s) => assert_eq!(s.as_str(), "system:ui", "{name}: wrong family name"),
535
                other => panic!("{name}: unexpected font family variant {other:?}"),
536
            }
537
        }
538
    }
539

            
540
    #[test]
541
    fn the_centering_declarations_are_mutually_consistent() {
542
        // A label centres its text both ways; each half of that contract lives
543
        // in a different declaration, so assert them together.
544
        for (name, table) in [("default", LABEL_STYLE_DEFAULT), ("mac", LABEL_STYLE_MAC)] {
545
            let props: Vec<CssProperty> = table.iter().map(|p| p.property.clone()).collect();
546
            let has = |p: &CssProperty| props.contains(p);
547
            assert!(has(&CssProperty::const_display(LayoutDisplay::Flex)), "{name}: not a flex box");
548
            assert!(
549
                has(&CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
550
                "{name}: not a column"
551
            );
552
            assert!(
553
                has(&CssProperty::const_justify_content(LayoutJustifyContent::Center)),
554
                "{name}: not centred on the main axis"
555
            );
556
            assert!(
557
                has(&CssProperty::const_align_items(LayoutAlignItems::Center)),
558
                "{name}: not centred on the cross axis"
559
            );
560
            assert!(
561
                has(&CssProperty::const_text_align(StyleTextAlign::Center)),
562
                "{name}: the text itself is not centred"
563
            );
564
        }
565
    }
566

            
567
    // ------------------------------------------------------------------
568
    // Label::swap_with_default
569
    // ------------------------------------------------------------------
570

            
571
    #[test]
572
    fn swap_with_default_returns_the_original_and_leaves_an_empty_label() {
573
        let mut label = Label::create(AzString::from("original \u{1F600}".to_string()));
574
        let expected_style = label.label_style.clone();
575
        let taken = label.swap_with_default();
576

            
577
        // The returned value is the *original*, intact.
578
        assert_eq!(taken.string.as_str(), "original \u{1F600}", "the wrong value was returned");
579
        assert_eq!(taken.label_style, expected_style, "the returned label lost its style");
580

            
581
        // What is left behind is a freshly created empty label — not a hollowed
582
        // out husk with a dangling string or an empty style vec.
583
        assert_eq!(label.string.as_str(), "", "what was left behind is not empty");
584
        assert!(label.string.is_empty());
585
        assert_same_label(&label, &Label::create(AzString::from_const_str("")), "left-behind label");
586
    }
587

            
588
    #[test]
589
    fn swap_with_default_is_idempotent_on_an_already_empty_label() {
590
        let mut label = Label::create(AzString::from_const_str(""));
591
        let first = label.swap_with_default();
592
        let second = label.swap_with_default();
593
        assert_eq!(first.string.as_str(), "");
594
        assert_eq!(second.string.as_str(), "");
595
        assert_same_label(&label, &Label::create(AzString::from_const_str("")), "after two swaps");
596
    }
597

            
598
    #[test]
599
    fn repeated_swaps_never_corrupt_the_static_backed_style() {
600
        // `mem::swap` moves a vec that borrows a `'static` slice; 200 rounds of
601
        // swap-and-drop would surface a double free or a dangling `ptr`.
602
        let mut label = Label::create(AzString::from("swap me".to_string()));
603
        for round in 0..200 {
604
            let taken = label.swap_with_default();
605
            if round == 0 {
606
                assert_eq!(taken.string.as_str(), "swap me", "round 0: wrong value returned");
607
            } else {
608
                assert_eq!(taken.string.as_str(), "", "round {round}: the emptied slot was not empty");
609
            }
610
            assert_eq!(properties(&taken.label_style), properties(&label.label_style), "round {round}");
611
            assert_eq!(label.string.as_str(), "", "round {round}: what was left behind is not empty");
612
            drop(taken);
613
        }
614
        assert_same_label(&label, &Label::create(AzString::from_const_str("")), "after 200 swaps");
615
    }
616

            
617
    #[test]
618
    fn swap_with_default_returns_a_custom_style_untouched() {
619
        // Both fields are `pub`, so a caller can hand-build a label. The swap
620
        // must hand that style back rather than rewriting it on the way out.
621
        let custom = CssPropertyWithConditionsVec::from_vec(vec![CssPropertyWithConditions::simple(
622
            CssProperty::const_font_size(StyleFontSize::const_px(42)),
623
        )]);
624
        let mut label = Label {
625
            string: AzString::from_const_str("custom"),
626
            label_style: custom,
627
        };
628
        let taken = label.swap_with_default();
629
        assert_eq!(taken.string.as_str(), "custom");
630
        assert_eq!(taken.label_style.len(), 1, "the custom style was rewritten on the way out");
631
        assert_eq!(font_size_px(&taken.label_style), Some(42.0));
632
        // ...and the slot is refilled with the platform default, not the custom one.
633
        assert_eq!(font_size_px(&label.label_style), expected_font_size(), "the 42px override survived");
634
    }
635

            
636
    #[test]
637
    fn swap_with_default_hands_back_a_huge_string_without_truncating_it() {
638
        let huge = "\u{1F600}".repeat(50_000);
639
        let mut label = Label::create(AzString::from(huge.clone()));
640
        let taken = label.swap_with_default();
641
        assert_eq!(taken.string.len(), huge.len(), "the huge string was truncated on the way out");
642
        assert_eq!(taken.string.as_str(), huge.as_str());
643
        assert!(label.string.is_empty());
644
    }
645

            
646
    // ------------------------------------------------------------------
647
    // Label::dom  (round-trip: label -> DOM)
648
    // ------------------------------------------------------------------
649

            
650
    #[test]
651
    fn dom_is_a_single_classed_text_node_carrying_the_computed_style() {
652
        let label = Label::create(AzString::from_const_str("Hello"));
653
        let expected = properties(&label.label_style);
654
        let dom = label.dom();
655

            
656
        assert_eq!(text_of(&dom), Some("Hello"), "the label is not a text node, or was mangled");
657
        assert!(has_class(&dom, LABEL_CLASS_NAME), "missing the widget class");
658
        assert_eq!(
659
            dom.root.get_ids_and_classes().as_ref().len(),
660
            1,
661
            "expected exactly one class and no ids"
662
        );
663
        assert_eq!(
664
            dom.children.as_ref().len(),
665
            1,
666
            "a label is a <p> wrapping exactly one bare text node"
667
        );
668
        assert!(
669
            dom.children.as_ref()[0].children.as_ref().is_empty(),
670
            "the text node itself is a leaf, not a subtree"
671
        );
672
        assert_eq!(dom.estimated_total_children, 1, "the <p> owns exactly its text node");
673
        assert!(dom.css.as_ref().is_empty(), "a label must not attach a scoped stylesheet");
674
        assert!(dom.root.callbacks.as_ref().is_empty(), "a static widget must not bind callbacks");
675
        assert_eq!(inline_properties(&dom), expected, "the label lost its computed style");
676
    }
677

            
678
    #[test]
679
    fn dom_preserves_adversarial_labels_verbatim() {
680
        for s in adversarial_strings() {
681
            let dom = Label::create(AzString::from(s.clone())).dom();
682
            let t = text_of(&dom).expect("expected a text node");
683
            assert_eq!(t, s.as_str(), "the label changed on its way into the DOM");
684
            assert_eq!(t.len(), s.len(), "byte length changed (NUL truncation?)");
685
            assert!(has_class(&dom, LABEL_CLASS_NAME), "the class was lost for {s:?}");
686
            // The text must never leak into the style, and the style must never
687
            // vary with the text.
688
            assert_eq!(inline_properties(&dom).len(), expected_table().len(), "style varies with the text");
689
        }
690
    }
691

            
692
    #[test]
693
    fn dom_of_an_empty_label_is_still_a_classed_text_node() {
694
        // The empty label is what `swap_with_default` leaves behind, so it is
695
        // the one input guaranteed to be rendered somewhere.
696
        let dom = Label::create(AzString::from_const_str("")).dom();
697
        assert_eq!(text_of(&dom), Some(""), "the empty label is not a text node");
698
        assert!(has_class(&dom, LABEL_CLASS_NAME));
699
        assert_eq!(inline_properties(&dom).len(), expected_table().len());
700
    }
701

            
702
    #[test]
703
    fn dom_renders_the_style_field_verbatim_even_when_hand_built() {
704
        // `dom()` consumes `label_style` as-is. A hand-built label must be
705
        // rendered with exactly the style it was given — no re-derivation.
706
        let custom = CssPropertyWithConditionsVec::from_vec(vec![
707
            CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(99))),
708
            CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
709
        ]);
710
        let dom = Label {
711
            string: AzString::from_const_str("hand built"),
712
            label_style: custom.clone(),
713
        }
714
        .dom();
715
        assert_eq!(inline_properties(&dom), properties(&custom), "the custom style was rewritten");
716
        assert!(has_class(&dom, LABEL_CLASS_NAME), "a hand-built label lost the widget class");
717
        assert_eq!(text_of(&dom), Some("hand built"));
718
    }
719

            
720
    #[test]
721
    fn dom_of_a_style_less_label_carries_no_inline_declarations() {
722
        // The `LABEL_STYLE_OTHER` shape: an empty style vec must produce an
723
        // empty inline style, not a phantom rule block.
724
        let dom = Label {
725
            string: AzString::from_const_str("bare"),
726
            label_style: CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE_OTHER),
727
        }
728
        .dom();
729
        assert!(inline_properties(&dom).is_empty(), "an empty style vec produced declarations");
730
        assert!(has_class(&dom, LABEL_CLASS_NAME));
731
        assert_eq!(text_of(&dom), Some("bare"));
732
    }
733

            
734
    #[test]
735
    fn the_widget_class_is_a_namespaced_ascii_css_identifier() {
736
        let dom = Label::create(AzString::from_const_str("x")).dom();
737
        let classes = dom.root.get_ids_and_classes();
738
        let name = classes
739
            .as_ref()
740
            .iter()
741
            .find_map(|c| match c {
742
                Class(s) => Some(s.as_str().to_string()),
743
                IdOrClass::Id(_) => None,
744
            })
745
            .expect("the label must carry a class");
746

            
747
        assert_eq!(name, LABEL_CLASS_NAME);
748
        assert!(!name.is_empty(), "empty class name");
749
        assert!(name.is_ascii(), "non-ASCII class name {name:?}");
750
        assert!(name.starts_with("__azul-native-"), "unnamespaced class {name:?}");
751
        // A space, a dot or a `#` would silently split/re-target the selector.
752
        assert!(
753
            name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
754
            "class name {name:?} contains a CSS-significant character"
755
        );
756
    }
757

            
758
    #[test]
759
    fn a_label_whose_text_is_the_class_name_does_not_gain_a_second_class() {
760
        // `dom()` sets ids-and-classes *after* the text node is built; a
761
        // confusion between the two would show up as an extra attribute here.
762
        let dom = Label::create(AzString::from_const_str(LABEL_CLASS_NAME)).dom();
763
        assert_eq!(dom.root.get_ids_and_classes().as_ref().len(), 1, "the text leaked into the class list");
764
        assert_eq!(text_of(&dom), Some(LABEL_CLASS_NAME));
765
    }
766

            
767
    #[test]
768
    fn from_label_for_dom_is_exactly_dom() {
769
        for s in ["", "ok", "\u{1F600}\0"] {
770
            let label = Label::create(AzString::from(s.to_string()));
771
            let via_into: Dom = label.clone().into();
772
            let via_dom = label.dom();
773
            assert_eq!(via_into, via_dom, "{s:?}: `From` diverges from `dom()`");
774
        }
775
    }
776

            
777
    #[test]
778
    fn building_many_doms_never_corrupts_the_shared_static_class_list() {
779
        // `dom()` hands a `'static`-backed `IdOrClassVec` to each node it
780
        // builds; 1000 build-and-drop rounds would surface a double free.
781
        for round in 0..1000 {
782
            let dom = Label::create(AzString::from_const_str("churn")).dom();
783
            assert!(has_class(&dom, LABEL_CLASS_NAME), "round {round}: the static class list was corrupted");
784
            drop(dom);
785
        }
786
        let dom = Label::create(AzString::from_const_str("churn")).dom();
787
        assert!(has_class(&dom, LABEL_CLASS_NAME), "the static class list did not survive the churn");
788
        assert_eq!(inline_properties(&dom).len(), expected_table().len());
789
    }
790
}