1
//! Alert / banner widget — a coloured inline message box conveying an
2
//! informational, success, warning or danger status. A container (a near-clone
3
//! of [`crate::widgets::card::Card`] / [`crate::widgets::frame::Frame`]) holding
4
//! a message string, with an optional dismissible "x" close affordance.
5
//!
6
//! When made dismissible (`with_dismissible(true)` or `set_on_dismiss`), the
7
//! alert mirrors the stateful pattern of [`crate::widgets::check_box::CheckBox`]:
8
//! it carries an [`AlertStateWrapper`] (`{ visible } + on_dismiss`) in a
9
//! [`RefAny`] attached to the close button. Clicking the close button flips
10
//! `visible` to `false`, invokes the optional user `on_dismiss`, and hides the
11
//! whole alert by setting `display: none` on the container via
12
//! `set_css_property` (mirroring check_box's live restyle). A non-dismissible
13
//! alert renders no close button and carries no live callback — it is then just
14
//! a stateless styled container.
15
//!
16
//! Key types: [`Alert`], [`AlertKind`], [`AlertState`], [`AlertOnDismiss`].
17

            
18
use azul_core::{
19
    callbacks::{CoreCallbackData, Update},
20
    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
21
    refany::RefAny,
22
};
23
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
24
use azul_css::{
25
    props::{
26
        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
27
        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutMarginLeft},
28
        property::{CssProperty, *},
29
        style::{StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleCursor, StyleUserSelect},
30
    },
31
    impl_option_inner, AzString,
32
};
33

            
34
use crate::callbacks::{Callback, CallbackInfo};
35

            
36
static ALERT_CONTAINER_CLASS: &[IdOrClass] =
37
    &[Class(AzString::from_const_str("__azul-native-alert"))];
38
static ALERT_MESSAGE_CLASS: &[IdOrClass] =
39
    &[Class(AzString::from_const_str("__azul-native-alert-message"))];
40
static ALERT_CLOSE_CLASS: &[IdOrClass] =
41
    &[Class(AzString::from_const_str("__azul-native-alert-close"))];
42

            
43
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
44
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
45
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
46
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
47

            
48
/// Callback function type invoked when a dismissible alert's close button is clicked.
49
pub type AlertOnDismissCallbackType = extern "C" fn(RefAny, CallbackInfo, AlertState) -> Update;
50
impl_widget_callback!(
51
    AlertOnDismiss,
52
    OptionAlertOnDismiss,
53
    AlertOnDismissCallback,
54
    AlertOnDismissCallbackType
55
);
56

            
57
azul_core::impl_managed_callback! {
58
    wrapper:        AlertOnDismissCallback,
59
    info_ty:        CallbackInfo,
60
    return_ty:      Update,
61
    default_ret:    Update::DoNothing,
62
    invoker_static: ALERT_ON_DISMISS_INVOKER,
63
    invoker_ty:     AzAlertOnDismissCallbackInvoker,
64
    thunk_fn:       az_alert_on_dismiss_callback_thunk,
65
    setter_fn:      AzApp_setAlertOnDismissCallbackInvoker,
66
    from_handle_fn: AzAlertOnDismissCallback_createFromHostHandle,
67
    extra_args:     [ state: AlertState ],
68
}
69

            
70
/// The semantic colour variant of an [`Alert`] (Bootstrap alert palette).
71
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
72
#[repr(C)]
73
pub enum AlertKind {
74
    /// Blue informational alert — the default.
75
    #[default]
76
    Info,
77
    /// Green success alert.
78
    Success,
79
    /// Yellow warning alert.
80
    Warning,
81
    /// Red danger/error alert.
82
    Danger,
83
}
84

            
85
impl AlertKind {
86
    /// Returns the `(background, border, text)` colours for this alert kind.
87
    #[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
88
162
    const fn colors(&self) -> (ColorU, ColorU, ColorU) {
89
162
        match self {
90
89
            Self::Info => (
91
89
                ColorU { r: 207, g: 244, b: 252, a: 255 }, // #cff4fc
92
89
                ColorU { r: 182, g: 239, b: 251, a: 255 }, // #b6effb
93
89
                ColorU { r: 5, g: 81, b: 96, a: 255 },     // #055160
94
89
            ),
95
22
            Self::Success => (
96
22
                ColorU { r: 209, g: 231, b: 221, a: 255 }, // #d1e7dd
97
22
                ColorU { r: 186, g: 219, b: 204, a: 255 }, // #badbcc
98
22
                ColorU { r: 15, g: 81, b: 50, a: 255 },    // #0f5132
99
22
            ),
100
22
            Self::Warning => (
101
22
                ColorU { r: 255, g: 243, b: 205, a: 255 }, // #fff3cd
102
22
                ColorU { r: 255, g: 236, b: 181, a: 255 }, // #ffecb5
103
22
                ColorU { r: 102, g: 77, b: 3, a: 255 },    // #664d03
104
22
            ),
105
29
            Self::Danger => (
106
29
                ColorU { r: 248, g: 215, b: 218, a: 255 }, // #f8d7da
107
29
                ColorU { r: 245, g: 194, b: 199, a: 255 }, // #f5c2c7
108
29
                ColorU { r: 132, g: 32, b: 41, a: 255 },   // #842029
109
29
            ),
110
        }
111
162
    }
112

            
113
    /// CSS class name for this alert kind (mirrors `ButtonType::class_name`).
114
20
    #[must_use] pub const fn class_name(&self) -> &'static str {
115
20
        match self {
116
5
            Self::Info => "__azul-alert-info",
117
5
            Self::Success => "__azul-alert-success",
118
5
            Self::Warning => "__azul-alert-warning",
119
5
            Self::Danger => "__azul-alert-danger",
120
        }
121
20
    }
122
}
123

            
124
/// A coloured inline message box with an optional dismissible close button.
125
#[derive(Debug, Clone, PartialEq, Eq)]
126
#[repr(C)]
127
pub struct Alert {
128
    /// Runtime state (`visible`) plus the optional dismiss callback.
129
    pub alert_state: AlertStateWrapper,
130
    /// The message text shown inside the alert.
131
    pub message: AzString,
132
    /// The colour variant.
133
    pub kind: AlertKind,
134
    /// Whether to render the "x" close button (hides the alert on click).
135
    pub dismissible: bool,
136
    /// The computed inline style for the container.
137
    pub container_style: CssPropertyWithConditionsVec,
138
}
139

            
140
#[derive(Debug, Default, Clone, PartialEq, Eq)]
141
#[repr(C)]
142
pub struct AlertStateWrapper {
143
    /// Whether the alert is currently visible.
144
    pub inner: AlertState,
145
    /// Optional: function to call when the alert is dismissed.
146
    pub on_dismiss: OptionAlertOnDismiss,
147
}
148

            
149
/// The visible/hidden state of an [`Alert`].
150
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
151
#[repr(C)]
152
pub struct AlertState {
153
    /// `true` (default) = shown, `false` = dismissed/hidden.
154
    pub visible: bool,
155
}
156

            
157
impl Default for AlertState {
158
80
    fn default() -> Self {
159
80
        Self { visible: true }
160
80
    }
161
}
162

            
163
/// Builds the container style for a given [`AlertKind`]. The colours are the
164
/// only kind-dependent properties, so the style is built at runtime per the
165
/// recipe's "runtime vec when param-dependent" path (see `badge::build_badge_style`).
166
132
fn build_alert_style(kind: AlertKind) -> CssPropertyWithConditionsVec {
167
132
    let (bg, border, text) = kind.colors();
168
132
    let bg_vec =
169
132
        StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
170
132
    CssPropertyWithConditionsVec::from_vec(alloc::vec![
171
132
        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
172
132
        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
173
132
            LayoutFlexDirection::Row,
174
        )),
175
132
        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
176
        // Span the full width of a flex-column parent.
177
132
        CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Stretch)),
178
132
        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
179
            0,
180
        ))),
181
        // padding: 12px
182
132
        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
183
            12,
184
        ))),
185
132
        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
186
132
            LayoutPaddingBottom::const_px(12),
187
        )),
188
132
        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
189
132
            LayoutPaddingLeft::const_px(12),
190
        )),
191
132
        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
192
132
            LayoutPaddingRight::const_px(12),
193
        )),
194
        // border: 1px solid <border>
195
132
        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
196
132
            LayoutBorderTopWidth::const_px(1),
197
        )),
198
132
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
199
132
            LayoutBorderBottomWidth::const_px(1),
200
        )),
201
132
        CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
202
132
            LayoutBorderLeftWidth::const_px(1),
203
        )),
204
132
        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
205
132
            LayoutBorderRightWidth::const_px(1),
206
        )),
207
132
        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
208
132
            inner: BorderStyle::Solid,
209
132
        })),
210
132
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
211
132
            StyleBorderBottomStyle {
212
132
                inner: BorderStyle::Solid,
213
132
            },
214
        )),
215
132
        CssPropertyWithConditions::simple(CssProperty::const_border_left_style(
216
132
            StyleBorderLeftStyle {
217
132
                inner: BorderStyle::Solid,
218
132
            },
219
        )),
220
132
        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
221
132
            StyleBorderRightStyle {
222
132
                inner: BorderStyle::Solid,
223
132
            },
224
        )),
225
132
        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
226
132
            inner: border,
227
132
        })),
228
132
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
229
132
            StyleBorderBottomColor { inner: border },
230
        )),
231
132
        CssPropertyWithConditions::simple(CssProperty::const_border_left_color(
232
132
            StyleBorderLeftColor { inner: border },
233
        )),
234
132
        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
235
132
            StyleBorderRightColor { inner: border },
236
        )),
237
        // border-radius: 6px
238
132
        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
239
132
            StyleBorderTopLeftRadius::const_px(6),
240
        )),
241
132
        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
242
132
            StyleBorderTopRightRadius::const_px(6),
243
        )),
244
132
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
245
132
            StyleBorderBottomLeftRadius::const_px(6),
246
        )),
247
132
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
248
132
            StyleBorderBottomRightRadius::const_px(6),
249
        )),
250
132
        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
251
132
        CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
252
        // Text colour is inherited by the message + close children.
253
132
        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
254
132
            inner: text,
255
132
        })),
256
132
        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
257
    ])
258
132
}
259

            
260
/// Message-text style: takes the remaining horizontal space, left-aligned.
261
static ALERT_MESSAGE_STYLE: &[CssPropertyWithConditions] = &[
262
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
263
    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
264
];
265

            
266
/// Close-button ("x") style: a small pointer-cursor box on the right.
267
static ALERT_CLOSE_STYLE: &[CssPropertyWithConditions] = &[
268
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
269
    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
270
    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
271
    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
272
    CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(
273
        12,
274
    ))),
275
];
276

            
277
impl Alert {
278
    /// Creates a new informational (blue) alert with the given message.
279
    #[inline]
280
64
    #[must_use] pub fn create(message: AzString) -> Self {
281
64
        Self::with_kind(message, AlertKind::Info)
282
64
    }
283

            
284
    /// Creates a new alert with the given message and colour variant.
285
    #[inline]
286
76
    #[must_use] pub fn with_kind(message: AzString, kind: AlertKind) -> Self {
287
76
        Self {
288
76
            alert_state: AlertStateWrapper::default(),
289
76
            message,
290
76
            kind,
291
76
            dismissible: false,
292
76
            container_style: build_alert_style(kind),
293
76
        }
294
76
    }
295

            
296
    /// Sets the colour variant, recomputing the container style.
297
    #[inline]
298
24
    pub fn set_kind(&mut self, kind: AlertKind) {
299
24
        self.kind = kind;
300
24
        self.container_style = build_alert_style(kind);
301
24
    }
302

            
303
    /// Builder-style setter for the colour variant.
304
    #[inline]
305
6
    #[must_use] pub fn with_alert_kind(mut self, kind: AlertKind) -> Self {
306
6
        self.set_kind(kind);
307
6
        self
308
6
    }
309

            
310
    /// Sets whether the alert shows a "x" close button.
311
    #[inline]
312
31
    pub const fn set_dismissible(&mut self, dismissible: bool) {
313
31
        self.dismissible = dismissible;
314
31
    }
315

            
316
    /// Builder-style setter for the dismissible flag.
317
    #[inline]
318
23
    #[must_use] pub const fn with_dismissible(mut self, dismissible: bool) -> Self {
319
23
        self.set_dismissible(dismissible);
320
23
        self
321
23
    }
322

            
323
    /// Sets the dismiss callback. Implies `dismissible = true` so the close
324
    /// button is rendered.
325
    #[inline]
326
8
    pub fn set_on_dismiss<C: Into<AlertOnDismissCallback>>(&mut self, data: RefAny, on_dismiss: C) {
327
8
        self.dismissible = true;
328
8
        self.alert_state.on_dismiss = Some(AlertOnDismiss {
329
8
            callback: on_dismiss.into(),
330
8
            refany: data,
331
8
        })
332
8
        .into();
333
8
    }
334

            
335
    /// Builder-style setter for the dismiss callback (implies dismissible).
336
    #[inline]
337
3
    #[must_use] pub fn with_on_dismiss<C: Into<AlertOnDismissCallback>>(
338
3
        mut self,
339
3
        data: RefAny,
340
3
        on_dismiss: C,
341
3
    ) -> Self {
342
3
        self.set_on_dismiss(data, on_dismiss);
343
3
        self
344
3
    }
345

            
346
    /// Replaces `self` with a default (empty info) alert and returns the original.
347
    #[inline]
348
5
    #[must_use] pub fn swap_with_default(&mut self) -> Self {
349
5
        let mut s = Self::create(AzString::from_const_str(""));
350
5
        core::mem::swap(&mut s, self);
351
5
        s
352
5
    }
353

            
354
    /// Converts this alert into a DOM subtree with the `__azul-native-alert` class.
355
    #[inline]
356
26
    #[must_use] pub fn dom(self) -> Dom {
357
        use azul_core::{
358
            callbacks::CoreCallback,
359
            dom::{EventFilter, HoverEventFilter},
360
            refany::OptionRefAny,
361
        };
362

            
363
26
        let message = Dom::create_p_with_text(self.message)
364
26
            .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_MESSAGE_CLASS))
365
26
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ALERT_MESSAGE_STYLE));
366

            
367
26
        let mut children = alloc::vec![message];
368

            
369
26
        if self.dismissible {
370
16
            let close = Dom::create_p_with_text(AzString::from_const_str("\u{00D7}"))
371
16
                .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_CLOSE_CLASS))
372
16
                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ALERT_CLOSE_STYLE))
373
16
                .with_tab_index(TabIndex::Auto)
374
16
                .with_callbacks(
375
16
                    alloc::vec![CoreCallbackData {
376
16
                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
377
16
                        callback: CoreCallback {
378
16
                            cb: default_on_alert_dismiss as usize,
379
16
                            ctx: OptionRefAny::None,
380
16
                        },
381
16
                        refany: RefAny::new(self.alert_state),
382
16
                    }]
383
16
                    .into(),
384
16
                );
385
16
            children.push(close);
386
16
        }
387

            
388
26
        Dom::create_div()
389
26
            .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_CONTAINER_CLASS))
390
26
            .with_css_props(self.container_style)
391
26
            .with_children(children.into())
392
26
    }
393
}
394

            
395
impl Default for Alert {
396
15
    fn default() -> Self {
397
15
        Self::create(AzString::from_const_str(""))
398
15
    }
399
}
400

            
401
/// Close-button click handler. The hit node is the close button (the
402
/// callback-bearing node, per `currentTarget` semantics — see `radio_group`);
403
/// its parent is the alert container. Flips `visible` to `false`, invokes the
404
/// optional user callback, then hides the whole alert via `display: none`.
405
9
extern "C" fn default_on_alert_dismiss(mut data: RefAny, mut info: CallbackInfo) -> Update {
406
9
    let close_node = info.get_hit_node();
407
9
    let Some(container) = info.get_parent(close_node) else {
408
3
        return Update::DoNothing;
409
    };
410

            
411
5
    let result = {
412
6
        let Some(mut alert) = data.downcast_mut::<AlertStateWrapper>() else {
413
1
            return Update::DoNothing;
414
        };
415
5
        alert.inner.visible = false;
416
5
        let inner = alert.inner;
417
5
        let alert = &mut *alert;
418
5
        match alert.on_dismiss.as_mut() {
419
3
            Some(AlertOnDismiss { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
420
2
            None => Update::DoNothing,
421
        }
422
    };
423

            
424
    // TODO2: hides the alert by toggling `display: none` via set_css_property.
425
    // This follows the proven live-restyle pattern of switch/check_box/radio_group
426
    // (which toggle opacity/margin/background); the display:none relayout itself is
427
    // not GUI-verified in this build.
428
5
    info.set_css_property(container, CssProperty::const_display(LayoutDisplay::None));
429

            
430
5
    result
431
9
}
432

            
433
impl From<Alert> for Dom {
434
    fn from(a: Alert) -> Self {
435
        a.dom()
436
    }
437
}
438

            
439
#[cfg(test)]
440
mod autotest_generated {
441
    use std::{
442
        collections::{BTreeMap, HashMap},
443
        sync::{Arc, Mutex},
444
    };
445

            
446
    use azul_core::{
447
        dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
448
        geom::{LogicalRect, OptionLogicalPosition},
449
        gl::OptionGlContextPtr,
450
        hit_test::ScrollPosition,
451
        refany::OptionRefAny,
452
        resources::RendererResources,
453
        styled_dom::{NodeHierarchyItemId, StyledDom},
454
        window::{MonitorVec, RawWindowHandle},
455
    };
456
    use azul_css::system::SystemStyle;
457
    use rust_fontconfig::FcFontCache;
458

            
459
    use super::*;
460
    #[cfg(feature = "icu")]
461
    use crate::icu::IcuLocalizerHandle;
462
    use crate::{
463
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
464
        solver3::{display_list::DisplayList, layout_tree::LayoutTree},
465
        window::{DomLayoutResult, LayoutWindow},
466
        window_state::FullWindowState,
467
    };
468

            
469
    // ------------------------------------------------------------------
470
    // Helpers
471
    // ------------------------------------------------------------------
472

            
473
    const ALL_KINDS: [AlertKind; 4] = [
474
        AlertKind::Info,
475
        AlertKind::Success,
476
        AlertKind::Warning,
477
        AlertKind::Danger,
478
    ];
479

            
480
    /// The text of a text node, looking through the `<p>` block wrapper the
481
    /// label convention mandates (`p > text`).
482
    fn text_of(node: &Dom) -> Option<&str> {
483
        match node.root.get_node_type() {
484
            NodeType::Text(s) => Some(s.as_ref().as_str()),
485
            NodeType::P => match node.children.as_ref() {
486
                [only] => match only.root.get_node_type() {
487
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
488
                    _ => None,
489
                },
490
                _ => None,
491
            },
492
            _ => None,
493
        }
494
    }
495

            
496
    /// The `background-color` of a style vec (first background layer only).
497
    fn background_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
498
        style.as_ref().iter().find_map(|p| match &p.property {
499
            CssProperty::BackgroundContent(v) => match v.get_property()?.as_ref().first()? {
500
                StyleBackgroundContent::Color(c) => Some(*c),
501
                _ => None,
502
            },
503
            _ => None,
504
        })
505
    }
506

            
507
    /// Every `border-*-color` in a style vec, in declaration order.
508
    fn border_colors(style: &CssPropertyWithConditionsVec) -> Vec<ColorU> {
509
        style
510
            .as_ref()
511
            .iter()
512
            .filter_map(|p| match &p.property {
513
                CssProperty::BorderTopColor(v) => v.get_property().map(|c| c.inner),
514
                CssProperty::BorderBottomColor(v) => v.get_property().map(|c| c.inner),
515
                CssProperty::BorderLeftColor(v) => v.get_property().map(|c| c.inner),
516
                CssProperty::BorderRightColor(v) => v.get_property().map(|c| c.inner),
517
                _ => None,
518
            })
519
            .collect()
520
    }
521

            
522
    /// The `color` (text colour) of a style vec.
523
    fn text_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
524
        style.as_ref().iter().find_map(|p| match &p.property {
525
            CssProperty::TextColor(v) => v.get_property().map(|c| c.inner),
526
            _ => None,
527
        })
528
    }
529

            
530
    /// The *kind* of every declared property, in order (ignores the values).
531
    fn property_types(style: &CssPropertyWithConditionsVec) -> Vec<core::mem::Discriminant<CssProperty>> {
532
        style
533
            .as_ref()
534
            .iter()
535
            .map(|p| core::mem::discriminant(&p.property))
536
            .collect()
537
    }
538

            
539
    /// A `RefAny` payload recording every `AlertState` a user `on_dismiss` sees.
540
    struct DismissLog {
541
        calls: Vec<bool>,
542
    }
543

            
544
    extern "C" fn record_dismiss(mut data: RefAny, _: CallbackInfo, state: AlertState) -> Update {
545
        if let Some(mut log) = data.downcast_mut::<DismissLog>() {
546
            log.calls.push(state.visible);
547
        }
548
        Update::RefreshDom
549
    }
550

            
551
    extern "C" fn dismiss_do_nothing(_: RefAny, _: CallbackInfo, _: AlertState) -> Update {
552
        Update::DoNothing
553
    }
554

            
555
    fn dismiss_cb(f: AlertOnDismissCallbackType) -> AlertOnDismissCallback {
556
        f.into()
557
    }
558

            
559
    /// `visible` of an `AlertStateWrapper` payload.
560
    fn wrapper_visible(data: &mut RefAny) -> bool {
561
        data.downcast_ref::<AlertStateWrapper>()
562
            .expect("payload must still be an AlertStateWrapper")
563
            .inner
564
            .visible
565
    }
566

            
567
    /// The `visible` flags recorded by a `DismissLog` payload.
568
    fn log_calls(data: &mut RefAny) -> Vec<bool> {
569
        data.downcast_ref::<DismissLog>()
570
            .expect("payload must still be a DismissLog")
571
            .calls
572
            .clone()
573
    }
574

            
575
    /// A `DomLayoutResult` with an *empty* layout tree: the dismiss handler only
576
    /// walks `styled_dom.node_hierarchy`, so no real layout (and no font) is needed.
577
    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
578
        DomLayoutResult {
579
            styled_dom,
580
            layout_tree: LayoutTree {
581
                nodes: Vec::new(),
582
                warm: Vec::new(),
583
                cold: Vec::new(),
584
                root: 0,
585
                dom_to_layout: BTreeMap::new(),
586
                children_arena: Vec::new(),
587
                children_offsets: Vec::new(),
588
                subtree_needs_intrinsic: Vec::new(),
589
            },
590
            calculated_positions: Vec::new(),
591
            viewport: LogicalRect::zero(),
592
            display_list: Arc::new(DisplayList::default()),
593
            scroll_ids: HashMap::new(),
594
            scroll_id_to_node_id: HashMap::new(),
595
        }
596
    }
597

            
598
    /// The flattened DOM of a dismissible alert: `container(0)`, `message(1)`,
599
    /// `close(2)` — i.e. exactly the hierarchy `default_on_alert_dismiss` walks
600
    /// (hit node -> parent).
601
    fn dismissible_styled_dom() -> StyledDom {
602
        let alert = Alert::create(AzString::from("msg")).with_dismissible(true);
603
        let styled = StyledDom::create_from_dom(alert.dom());
604
        assert_eq!(
605
            styled.node_hierarchy.as_ref().len(),
606
            5,
607
            "fixture must flatten to container / message <p> + text / close <p> + text"
608
        );
609
        styled
610
    }
611

            
612
    /// Invokes `default_on_alert_dismiss` against a `LayoutWindow` holding
613
    /// `styled` (or nothing at all, when `styled` is `None`), with `hit` as the
614
    /// hit node. Returns the `Update` plus every recorded `CallbackChange`.
615
    fn run_dismiss(
616
        styled: Option<StyledDom>,
617
        hit: usize,
618
        data: RefAny,
619
    ) -> (Update, Vec<CallbackChange>) {
620
        let mut layout_window =
621
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
622
        if let Some(sd) = styled {
623
            layout_window
624
                .layout_results
625
                .insert(DomId::ROOT_ID, layout_result(sd));
626
        }
627

            
628
        let renderer_resources = RendererResources::default();
629
        let previous_window_state: Option<FullWindowState> = None;
630
        let current_window_state = FullWindowState::default();
631
        let gl_context = OptionGlContextPtr::None;
632
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
633
            BTreeMap::new();
634
        let window_handle = RawWindowHandle::Unsupported;
635
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
636

            
637
        let ref_data = CallbackInfoRefData {
638
            layout_window: &layout_window,
639
            renderer_resources: &renderer_resources,
640
            previous_window_state: &previous_window_state,
641
            current_window_state: &current_window_state,
642
            gl_context: &gl_context,
643
            current_scroll_manager: &scroll_states,
644
            current_window_handle: &window_handle,
645
            system_callbacks: &system_callbacks,
646
            system_style: Arc::new(SystemStyle::default()),
647
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
648
            #[cfg(feature = "icu")]
649
            icu_localizer: IcuLocalizerHandle::default(),
650
            ctx: OptionRefAny::None,
651
        };
652

            
653
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
654

            
655
        let info = CallbackInfo::new(
656
            &ref_data,
657
            &changes,
658
            DomNodeId {
659
                dom: DomId::ROOT_ID,
660
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
661
            },
662
            OptionLogicalPosition::None,
663
            OptionLogicalPosition::None,
664
        );
665

            
666
        let update = default_on_alert_dismiss(data, info);
667
        let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
668
        (update, recorded)
669
    }
670

            
671
    /// Every `display` write recorded in the change log, as `(node index, display)`.
672
    fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
673
        let mut out = Vec::new();
674
        for change in changes {
675
            if let CallbackChange::ChangeNodeCssProperties {
676
                node_id, properties, ..
677
            } = change
678
            {
679
                for p in properties.as_ref() {
680
                    if let CssProperty::Display(v) = p {
681
                        if let Some(d) = v.get_property() {
682
                            out.push((node_id.index(), *d));
683
                        }
684
                    }
685
                }
686
            }
687
        }
688
        out
689
    }
690

            
691
    // ------------------------------------------------------------------
692
    // AlertKind::colors  (getter)
693
    // ------------------------------------------------------------------
694

            
695
    #[test]
696
    fn kind_colors_are_the_documented_bootstrap_palette() {
697
        let expect = |(r, g, b): (u8, u8, u8)| ColorU { r, g, b, a: 255 };
698

            
699
        assert_eq!(
700
            AlertKind::Info.colors(),
701
            (
702
                expect((207, 244, 252)), // #cff4fc
703
                expect((182, 239, 251)), // #b6effb
704
                expect((5, 81, 96)),     // #055160
705
            )
706
        );
707
        assert_eq!(
708
            AlertKind::Success.colors(),
709
            (
710
                expect((209, 231, 221)), // #d1e7dd
711
                expect((186, 219, 204)), // #badbcc
712
                expect((15, 81, 50)),    // #0f5132
713
            )
714
        );
715
        assert_eq!(
716
            AlertKind::Warning.colors(),
717
            (
718
                expect((255, 243, 205)), // #fff3cd
719
                expect((255, 236, 181)), // #ffecb5
720
                expect((102, 77, 3)),    // #664d03
721
            )
722
        );
723
        assert_eq!(
724
            AlertKind::Danger.colors(),
725
            (
726
                expect((248, 215, 218)), // #f8d7da
727
                expect((245, 194, 199)), // #f5c2c7
728
                expect((132, 32, 41)),   // #842029
729
            )
730
        );
731
    }
732

            
733
    #[test]
734
    fn kind_colors_are_fully_opaque_and_pairwise_distinct() {
735
        for kind in ALL_KINDS {
736
            let (bg, border, text) = kind.colors();
737
            for (name, c) in [("bg", bg), ("border", border), ("text", text)] {
738
                assert_eq!(c.a, 255, "{kind:?}.{name} must be fully opaque");
739
            }
740
            // a coloured banner is only legible if bg != text
741
            assert_ne!(bg, text, "{kind:?}: background must differ from text");
742
        }
743

            
744
        for (i, a) in ALL_KINDS.iter().enumerate() {
745
            for b in &ALL_KINDS[i + 1..] {
746
                assert_ne!(
747
                    a.colors(),
748
                    b.colors(),
749
                    "{a:?} and {b:?} must be visually distinguishable"
750
                );
751
            }
752
        }
753
    }
754

            
755
    #[test]
756
    fn kind_colors_default_is_info_and_call_is_pure() {
757
        assert_eq!(AlertKind::default(), AlertKind::Info);
758
        assert_eq!(AlertKind::default().colors(), AlertKind::Info.colors());
759

            
760
        // repeated calls on the same (Copy) receiver must be stable
761
        let k = AlertKind::Danger;
762
        assert_eq!(k.colors(), k.colors());
763
        assert_eq!(k.colors(), k.colors());
764
    }
765

            
766
    #[test]
767
    fn kind_colors_is_const_evaluable() {
768
        const INFO: (ColorU, ColorU, ColorU) = AlertKind::Info.colors();
769
        assert_eq!(INFO.0, ColorU { r: 207, g: 244, b: 252, a: 255 });
770
    }
771

            
772
    // ------------------------------------------------------------------
773
    // AlertKind::class_name  (getter)
774
    // ------------------------------------------------------------------
775

            
776
    #[test]
777
    fn class_name_exact_values_and_shape() {
778
        assert_eq!(AlertKind::Info.class_name(), "__azul-alert-info");
779
        assert_eq!(AlertKind::Success.class_name(), "__azul-alert-success");
780
        assert_eq!(AlertKind::Warning.class_name(), "__azul-alert-warning");
781
        assert_eq!(AlertKind::Danger.class_name(), "__azul-alert-danger");
782

            
783
        for kind in ALL_KINDS {
784
            let name = kind.class_name();
785
            assert!(
786
                name.starts_with("__azul-alert-"),
787
                "{kind:?} -> {name:?} must keep the widget prefix"
788
            );
789
            assert!(
790
                !name.contains(char::is_whitespace),
791
                "{name:?} must be a single CSS class token"
792
            );
793
            assert!(name.is_ascii(), "{name:?} must stay ASCII");
794
            // stable across calls, and equal for equal kinds
795
            assert_eq!(name, kind.class_name());
796
        }
797
    }
798

            
799
    #[test]
800
    fn class_name_is_unique_per_kind() {
801
        let mut names: Vec<&str> = ALL_KINDS.iter().map(|k| k.class_name()).collect();
802
        names.sort_unstable();
803
        names.dedup();
804
        assert_eq!(names.len(), 4, "every kind needs its own class name");
805
    }
806

            
807
    #[test]
808
    fn class_name_is_const_evaluable() {
809
        const DANGER: &str = AlertKind::Danger.class_name();
810
        assert_eq!(DANGER, "__azul-alert-danger");
811
    }
812

            
813
    // ------------------------------------------------------------------
814
    // build_alert_style
815
    // ------------------------------------------------------------------
816

            
817
    #[test]
818
    fn build_alert_style_declares_the_same_properties_for_every_kind() {
819
        let info = property_types(&build_alert_style(AlertKind::Info));
820
        assert!(!info.is_empty(), "the container style must not be empty");
821

            
822
        for kind in ALL_KINDS {
823
            let style = build_alert_style(kind);
824
            assert_eq!(
825
                property_types(&style),
826
                info,
827
                "{kind:?} must declare the same properties, in the same order, as Info"
828
            );
829
            // the style is unconditional: nothing is gated behind :hover/@media/...
830
            for p in style.as_ref() {
831
                assert!(
832
                    p.apply_if.as_ref().is_empty(),
833
                    "{kind:?}: {:?} must be unconditional",
834
                    p.property
835
                );
836
            }
837
        }
838
    }
839

            
840
    #[test]
841
    fn build_alert_style_declares_no_property_twice() {
842
        // a duplicated property would silently shadow the earlier declaration
843
        for kind in ALL_KINDS {
844
            let types = property_types(&build_alert_style(kind));
845
            for (i, a) in types.iter().enumerate() {
846
                for b in &types[i + 1..] {
847
                    assert_ne!(
848
                        a, b,
849
                        "{kind:?}: the container style declares the same property twice"
850
                    );
851
                }
852
            }
853
        }
854
    }
855

            
856
    #[test]
857
    fn build_alert_style_colors_track_the_kind_palette() {
858
        for kind in ALL_KINDS {
859
            let style = build_alert_style(kind);
860
            let (bg, border, text) = kind.colors();
861

            
862
            assert_eq!(background_color(&style), Some(bg), "{kind:?}: background");
863
            assert_eq!(text_color(&style), Some(text), "{kind:?}: text colour");
864

            
865
            let borders = border_colors(&style);
866
            assert_eq!(borders.len(), 4, "{kind:?}: all four edges must be coloured");
867
            assert!(
868
                borders.iter().all(|c| *c == border),
869
                "{kind:?}: every edge must use the kind's border colour, got {borders:?}"
870
            );
871
        }
872
    }
873

            
874
    #[test]
875
    fn build_alert_style_geometry_is_kind_independent() {
876
        // Everything that is *not* a colour must be identical for all kinds.
877
        let expected = [
878
            CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
879
            CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
880
                LayoutFlexDirection::Row,
881
            )),
882
            CssPropertyWithConditions::simple(CssProperty::const_align_items(
883
                LayoutAlignItems::Start,
884
            )),
885
            CssPropertyWithConditions::simple(CssProperty::const_padding_top(
886
                LayoutPaddingTop::const_px(12),
887
            )),
888
            CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
889
                LayoutBorderTopWidth::const_px(1),
890
            )),
891
            CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
892
                StyleBorderTopLeftRadius::const_px(6),
893
            )),
894
            CssPropertyWithConditions::simple(CssProperty::const_font_size(
895
                StyleFontSize::const_px(14),
896
            )),
897
        ];
898

            
899
        for kind in ALL_KINDS {
900
            let style = build_alert_style(kind);
901
            for want in &expected {
902
                assert!(
903
                    style.as_ref().contains(want),
904
                    "{kind:?}: missing {:?}",
905
                    want.property
906
                );
907
            }
908
        }
909
    }
910

            
911
    #[test]
912
    fn build_alert_style_differs_only_in_the_colours() {
913
        let info = build_alert_style(AlertKind::Info);
914
        for kind in [AlertKind::Success, AlertKind::Warning, AlertKind::Danger] {
915
            let other = build_alert_style(kind);
916
            let differing: Vec<_> = info
917
                .as_ref()
918
                .iter()
919
                .zip(other.as_ref().iter())
920
                .filter(|(a, b)| a != b)
921
                .map(|(a, _)| core::mem::discriminant(&a.property))
922
                .collect();
923

            
924
            // background + 4 border colours + text colour = 6 kind-dependent props
925
            assert_eq!(
926
                differing.len(),
927
                6,
928
                "{kind:?}: only bg + 4 border colours + text colour may depend on the kind"
929
            );
930
        }
931
    }
932

            
933
    // ------------------------------------------------------------------
934
    // Alert::create / with_kind / Default
935
    // ------------------------------------------------------------------
936

            
937
    #[test]
938
    fn create_is_an_info_alert_with_no_close_button() {
939
        let alert = Alert::create(AzString::from("hello"));
940

            
941
        assert_eq!(alert.message.as_str(), "hello");
942
        assert_eq!(alert.kind, AlertKind::Info);
943
        assert!(!alert.dismissible, "a fresh alert has no close button");
944
        assert!(alert.alert_state.inner.visible, "a fresh alert is visible");
945
        assert!(alert.alert_state.on_dismiss.is_none());
946
        assert_eq!(alert.container_style, build_alert_style(AlertKind::Info));
947
    }
948

            
949
    #[test]
950
    fn create_with_empty_message_equals_default_and_is_value_comparable() {
951
        assert_eq!(Alert::create(AzString::from("")), Alert::default());
952
        // equality is structural, not pointer-based
953
        assert_eq!(Alert::create(AzString::from("a")), Alert::create(AzString::from("a")));
954
        assert_ne!(Alert::create(AzString::from("a")), Alert::create(AzString::from("b")));
955
        assert_ne!(
956
            Alert::create(AzString::from("a")),
957
            Alert::with_kind(AzString::from("a"), AlertKind::Danger)
958
        );
959
    }
960

            
961
    #[test]
962
    fn create_survives_extreme_messages_and_round_trips_them_into_the_dom() {
963
        let long = "ab".repeat(50_000);
964
        let cases: Vec<AzString> = alloc::vec![
965
            AzString::from(""),
966
            AzString::from(" "),
967
            AzString::from("a\0b"),                                  // interior NUL
968
            AzString::from("line\nbreak\ttab"),                      // control chars
969
            AzString::from("👨‍👩‍👧‍👦 e\u{0301}\u{0327} مرحبا שלום 🇩🇪"), // ZWJ + combining + RTL
970
            AzString::from("\u{feff}\u{202e}rtl-override"),          // BOM + bidi override
971
            AzString::from("×"),                                     // same glyph as the close button
972
            AzString::from(long.as_str()),                           // 100k chars
973
        ];
974

            
975
        for message in cases {
976
            let alert = Alert::create(message.clone());
977
            assert_eq!(alert.message.as_str(), message.as_str());
978

            
979
            // the message must survive the trip through the DOM byte-for-byte
980
            let dom = alert.dom();
981
            let msg_node = &dom.children.as_ref()[0];
982
            assert_eq!(text_of(msg_node), Some(message.as_str()));
983
        }
984
    }
985

            
986
    #[test]
987
    fn with_kind_stores_both_args_for_every_kind() {
988
        for kind in ALL_KINDS {
989
            let alert = Alert::with_kind(AzString::from("m"), kind);
990

            
991
            assert_eq!(alert.kind, kind);
992
            assert_eq!(alert.message.as_str(), "m");
993
            assert!(!alert.dismissible);
994
            assert!(alert.alert_state.on_dismiss.is_none());
995
            assert!(alert.alert_state.inner.visible);
996
            assert_eq!(
997
                alert.container_style,
998
                build_alert_style(kind),
999
                "{kind:?}: the container style must match the kind it was built with"
            );
        }
    }
    // ------------------------------------------------------------------
    // set_kind / with_alert_kind
    // ------------------------------------------------------------------
    #[test]
    fn set_kind_recomputes_the_style_and_is_idempotent() {
        let mut alert = Alert::create(AzString::from("m"));
        for kind in ALL_KINDS {
            alert.set_kind(kind);
            assert_eq!(alert.kind, kind);
            assert_eq!(alert.container_style, build_alert_style(kind));
            // applying the same kind twice must not append/duplicate anything
            let before = alert.container_style.clone();
            alert.set_kind(kind);
            assert_eq!(alert.container_style, before, "{kind:?}: set_kind must be idempotent");
        }
        // a full cycle back to the original kind restores the original alert
        let original = Alert::create(AzString::from("m"));
        let mut cycled = original.clone();
        for kind in ALL_KINDS {
            cycled.set_kind(kind);
        }
        cycled.set_kind(AlertKind::Info);
        assert_eq!(cycled, original, "kind cycling must not accumulate state");
    }
    #[test]
    fn set_kind_leaves_message_dismissible_and_callback_alone() {
        let log = RefAny::new(DismissLog { calls: Vec::new() });
        let mut alert = Alert::create(AzString::from("keep me"));
        alert.set_on_dismiss(log, dismiss_cb(dismiss_do_nothing));
        alert.alert_state.inner.visible = false;
        alert.set_kind(AlertKind::Warning);
        assert_eq!(alert.message.as_str(), "keep me");
        assert!(alert.dismissible, "set_kind must not clear the close button");
        assert!(alert.alert_state.on_dismiss.is_some(), "set_kind must not drop the callback");
        assert!(!alert.alert_state.inner.visible, "set_kind must not resurrect a dismissed alert");
    }
    #[test]
    fn with_alert_kind_matches_set_kind_and_last_write_wins() {
        for kind in ALL_KINDS {
            let built = Alert::create(AzString::from("m")).with_alert_kind(kind);
            let mut mutated = Alert::create(AzString::from("m"));
            mutated.set_kind(kind);
            assert_eq!(built, mutated, "{kind:?}: builder and setter must agree");
        }
        let alert = Alert::create(AzString::from("m"))
            .with_alert_kind(AlertKind::Danger)
            .with_alert_kind(AlertKind::Success);
        assert_eq!(alert.kind, AlertKind::Success);
        assert_eq!(alert.container_style, build_alert_style(AlertKind::Success));
    }
    // ------------------------------------------------------------------
    // set_dismissible / with_dismissible
    // ------------------------------------------------------------------
    #[test]
    fn set_dismissible_last_write_wins_and_touches_nothing_else() {
        let mut alert = Alert::with_kind(AzString::from("m"), AlertKind::Warning);
        let style_before = alert.container_style.clone();
        for flag in [true, true, false, true, false, false] {
            alert.set_dismissible(flag);
            assert_eq!(alert.dismissible, flag);
        }
        assert_eq!(alert.kind, AlertKind::Warning);
        assert_eq!(alert.message.as_str(), "m");
        assert_eq!(alert.container_style, style_before, "toggling must not restyle");
        assert!(alert.alert_state.on_dismiss.is_none(), "toggling must not invent a callback");
    }
    #[test]
    fn with_dismissible_toggle_sequence_ends_on_the_last_value() {
        assert!(Alert::default().with_dismissible(true).dismissible);
        assert!(!Alert::default().with_dismissible(false).dismissible);
        assert!(
            !Alert::default()
                .with_dismissible(true)
                .with_dismissible(false)
                .dismissible
        );
        assert!(
            Alert::default()
                .with_dismissible(false)
                .with_dismissible(true)
                .dismissible
        );
        // builder == setter
        let mut mutated = Alert::default();
        mutated.set_dismissible(true);
        assert_eq!(Alert::default().with_dismissible(true), mutated);
    }
    // ------------------------------------------------------------------
    // set_on_dismiss / with_on_dismiss
    // ------------------------------------------------------------------
    #[test]
    fn set_on_dismiss_implies_dismissible() {
        let mut alert = Alert::create(AzString::from("m"));
        assert!(!alert.dismissible);
        alert.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
        assert!(alert.dismissible, "a dismiss callback must render a close button");
        assert!(alert.alert_state.on_dismiss.is_some());
        assert!(alert.alert_state.inner.visible, "wiring a callback must not hide the alert");
    }
    #[test]
    fn set_on_dismiss_replaces_rather_than_appends() {
        let mut alert = Alert::create(AzString::from("m"));
        alert.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
        let first = alert
            .alert_state
            .on_dismiss
            .as_ref()
            .expect("first callback")
            .refany
            .get_type_id();
        assert_eq!(first, RefAny::new(1u8).get_type_id());
        // a second call must *replace* the payload + function, not stack another one
        alert.set_on_dismiss(RefAny::new(9i64), dismiss_cb(record_dismiss));
        let second = alert.alert_state.on_dismiss.as_ref().expect("second callback");
        assert_eq!(second.refany.get_type_id(), RefAny::new(9i64).get_type_id());
        assert_eq!(second.callback, dismiss_cb(record_dismiss));
        assert_ne!(second.callback, dismiss_cb(dismiss_do_nothing));
    }
    #[test]
    fn with_on_dismiss_keeps_message_and_kind() {
        let alert = Alert::with_kind(AzString::from("boom"), AlertKind::Danger)
            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(dismiss_do_nothing));
        assert_eq!(alert.message.as_str(), "boom");
        assert_eq!(alert.kind, AlertKind::Danger);
        assert_eq!(alert.container_style, build_alert_style(AlertKind::Danger));
        assert!(alert.dismissible);
        assert!(alert.alert_state.on_dismiss.is_some());
    }
    #[test]
    fn set_dismissible_false_after_set_on_dismiss_silently_drops_the_close_button() {
        // Footgun, pinned as the *current* behaviour: `set_on_dismiss` implies
        // `dismissible = true`, but a later `set_dismissible(false)` wins and the
        // wired-up callback becomes unreachable (no close button is rendered).
        let mut alert = Alert::create(AzString::from("m"));
        alert.set_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
        alert.set_dismissible(false);
        assert!(alert.alert_state.on_dismiss.is_some(), "the callback is still stored");
        let dom = alert.dom();
        assert_eq!(
            dom.children.as_ref().len(),
            1,
            "no close button is rendered, so the callback can never fire"
        );
    }
    // ------------------------------------------------------------------
    // swap_with_default
    // ------------------------------------------------------------------
    #[test]
    fn swap_with_default_returns_the_original_and_resets_self() {
        let mut alert = Alert::with_kind(AzString::from("payload"), AlertKind::Danger)
            .with_dismissible(true);
        let snapshot = alert.clone();
        let returned = alert.swap_with_default();
        assert_eq!(returned, snapshot, "the original must come back untouched");
        assert_eq!(alert, Alert::default(), "self must be reset to a default alert");
        assert_eq!(alert.message.as_str(), "");
        assert_eq!(alert.kind, AlertKind::Info);
        assert!(!alert.dismissible);
        assert!(alert.alert_state.on_dismiss.is_none());
        assert!(alert.alert_state.inner.visible);
    }
    #[test]
    fn swap_with_default_is_stable_when_repeated() {
        let mut alert = Alert::default();
        for _ in 0..3 {
            let returned = alert.swap_with_default();
            assert_eq!(returned, Alert::default());
            assert_eq!(alert, Alert::default());
        }
    }
    #[test]
    fn swap_with_default_moves_the_callback_out_of_self() {
        let mut alert = Alert::create(AzString::from("m"))
            .with_on_dismiss(RefAny::new(7u32), dismiss_cb(record_dismiss));
        let returned = alert.swap_with_default();
        assert!(returned.alert_state.on_dismiss.is_some(), "the callback moves out");
        assert!(
            alert.alert_state.on_dismiss.is_none(),
            "the reset alert must not keep a reference to the old callback"
        );
        assert!(!alert.dismissible);
    }
    // ------------------------------------------------------------------
    // Alert::dom
    // ------------------------------------------------------------------
    #[test]
    fn dom_of_a_plain_alert_is_a_container_with_one_message_child() {
        let alert = Alert::create(AzString::from("hi"));
        let style = alert.container_style.clone();
        let dom = alert.dom();
        assert!(dom.root.has_class("__azul-native-alert"));
        assert!(
            dom.root.get_callbacks().as_ref().is_empty(),
            "a non-dismissible alert must carry no live callback"
        );
        assert_eq!(
            dom.root.style.iter_inline_properties().count(),
            style.len(),
            "every container property must reach the node's inline style"
        );
        let children = dom.children.as_ref();
        assert_eq!(children.len(), 1, "no close button without `dismissible`");
        assert!(children[0].root.has_class("__azul-native-alert-message"));
        assert_eq!(text_of(&children[0]), Some("hi"));
        assert!(children[0].root.get_callbacks().as_ref().is_empty());
        assert!(children[0].root.get_tab_index().is_none());
    }
    #[test]
    fn dom_of_a_dismissible_alert_appends_a_focusable_close_button() {
        let dom = Alert::create(AzString::from("hi")).with_dismissible(true).dom();
        let children = dom.children.as_ref();
        assert_eq!(children.len(), 2, "[message, close]");
        let close = &children[1];
        assert!(close.root.has_class("__azul-native-alert-close"));
        assert_eq!(text_of(close), Some("\u{00D7}"), "the close glyph is U+00D7 MULTIPLICATION SIGN");
        assert!(
            matches!(close.root.get_tab_index(), Some(TabIndex::Auto)),
            "the close button must be keyboard-reachable"
        );
        let callbacks = close.root.get_callbacks();
        assert_eq!(callbacks.as_ref().len(), 1, "exactly one dismiss handler");
        let cb = &callbacks.as_ref()[0];
        assert!(matches!(
            &cb.event,
            EventFilter::Hover(HoverEventFilter::MouseUp)
        ));
        assert_eq!(cb.callback.cb, default_on_alert_dismiss as usize);
        assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
    }
    #[test]
    fn dom_hands_the_alert_state_to_the_close_button() {
        let alert = Alert::create(AzString::from("hi"))
            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
        let dom = alert.dom();
        let close = &dom.children.as_ref()[1];
        let mut payload = close.root.get_callbacks().as_ref()[0].refany.clone();
        assert!(
            wrapper_visible(&mut payload),
            "the close button must receive a live, visible AlertStateWrapper"
        );
        assert!(
            payload
                .downcast_ref::<AlertStateWrapper>()
                .expect("AlertStateWrapper")
                .on_dismiss
                .is_some(),
            "the user callback must travel with the state"
        );
    }
    /// Flat index of the close button in `dismissible_styled_dom`: the tree is
    /// `0 container / 1 message <p> / 2 message text / 3 close <p> / 4 close text`
    /// in depth-first pre-order, and the callback sits on the `<p>`.
    const CLOSE_NODE: usize = 3;
    #[test]
    fn dom_is_stable_across_kinds_and_only_the_container_style_changes() {
        for kind in ALL_KINDS {
            let dom = Alert::with_kind(AzString::from("m"), kind)
                .with_dismissible(true)
                .dom();
            assert!(dom.root.has_class("__azul-native-alert"));
            assert_eq!(dom.children.as_ref().len(), 2);
            // NOTE: `AlertKind::class_name()` is *not* applied to the DOM - the
            // container only ever carries the generic container class.
            assert!(
                !dom.root.has_class(kind.class_name()),
                "current behaviour: the kind class is not emitted"
            );
        }
    }
    // ------------------------------------------------------------------
    // default_on_alert_dismiss
    // ------------------------------------------------------------------
    #[test]
    fn dismiss_hides_the_container_and_flips_visible() {
        let mut data = RefAny::new(AlertStateWrapper::default());
        // CLOSE_NODE == the close button <p>, its parent (node 0) is the container
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), CLOSE_NODE, data.clone());
        assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
        assert_eq!(
            display_writes(&changes),
            alloc::vec![(0usize, LayoutDisplay::None)],
            "the *container* (not the close button) must be hidden"
        );
        assert!(!wrapper_visible(&mut data), "state must flip to hidden");
    }
    #[test]
    fn dismiss_invokes_the_user_callback_with_the_already_flipped_state() {
        let mut log = RefAny::new(DismissLog { calls: Vec::new() });
        let mut data = RefAny::new(AlertStateWrapper {
            inner: AlertState { visible: true },
            on_dismiss: Some(AlertOnDismiss {
                callback: dismiss_cb(record_dismiss),
                refany: log.clone(),
            })
            .into(),
        });
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), CLOSE_NODE, data.clone());
        assert_eq!(update, Update::RefreshDom, "the user callback's Update is returned");
        assert_eq!(
            log_calls(&mut log),
            alloc::vec![false],
            "the callback must see `visible == false` (already dismissed)"
        );
        assert!(!wrapper_visible(&mut data));
        assert_eq!(
            display_writes(&changes),
            alloc::vec![(0usize, LayoutDisplay::None)],
            "the container is hidden even after a user callback ran"
        );
    }
    #[test]
    fn dismiss_twice_is_idempotent() {
        let mut log = RefAny::new(DismissLog { calls: Vec::new() });
        let mut data = RefAny::new(AlertStateWrapper {
            inner: AlertState { visible: true },
            on_dismiss: Some(AlertOnDismiss {
                callback: dismiss_cb(record_dismiss),
                refany: log.clone(),
            })
            .into(),
        });
        for _ in 0..2 {
            let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), CLOSE_NODE, data.clone());
            assert_eq!(update, Update::RefreshDom);
            assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
        }
        assert!(!wrapper_visible(&mut data), "a second dismiss must not un-hide");
        assert_eq!(
            log_calls(&mut log),
            alloc::vec![false, false],
            "each click fires the callback exactly once, always with visible == false"
        );
    }
    #[test]
    fn dismiss_on_a_root_hit_node_is_a_noop() {
        // node 0 has no parent -> there is no container to hide
        let mut data = RefAny::new(AlertStateWrapper::default());
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 0, data.clone());
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty(), "nothing may be restyled without a parent");
        assert!(wrapper_visible(&mut data), "state must not flip");
    }
    #[test]
    fn dismiss_with_a_stale_hit_node_is_a_noop() {
        // node 999 does not exist in the 3-node fixture
        let mut data = RefAny::new(AlertStateWrapper::default());
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 999, data.clone());
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty());
        assert!(wrapper_visible(&mut data));
    }
    #[test]
    fn dismiss_without_any_layout_result_is_a_noop() {
        let mut data = RefAny::new(AlertStateWrapper::default());
        let (update, changes) = run_dismiss(None, 2, data.clone());
        assert_eq!(update, Update::DoNothing);
        assert!(changes.is_empty());
        assert!(wrapper_visible(&mut data), "state must not flip");
    }
    #[test]
    fn dismiss_with_a_foreign_payload_is_a_noop() {
        // the callback-bearing node carries a RefAny of the *wrong* type
        let data = RefAny::new(0xdead_beef_u64);
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), CLOSE_NODE, data.clone());
        assert_eq!(update, Update::DoNothing);
        assert!(
            changes.is_empty(),
            "a foreign payload must not hide the container"
        );
    }
    #[test]
    fn dismiss_end_to_end_through_the_real_dom_payload() {
        // Take the *actual* RefAny the widget wired into its close button and
        // drive the *actual* handler the widget registered against it.
        let alert = Alert::create(AzString::from("bye")).with_dismissible(true);
        let dom = alert.dom();
        let close = &dom.children.as_ref()[1];
        let entry = &close.root.get_callbacks().as_ref()[0];
        assert_eq!(entry.callback.cb, default_on_alert_dismiss as usize);
        let mut payload = entry.refany.clone();
        let styled = StyledDom::create_from_dom(dom);
        let (update, changes) = run_dismiss(Some(styled), CLOSE_NODE, payload.clone());
        assert_eq!(update, Update::DoNothing);
        assert_eq!(
            display_writes(&changes),
            alloc::vec![(0usize, LayoutDisplay::None)]
        );
        assert!(
            !wrapper_visible(&mut payload),
            "the state living in the DOM must be flipped to hidden"
        );
    }
}