1
//! Toast / snackbar widget — a transient notification banner. A near-clone of
2
//! [`crate::widgets::alert::Alert`] (a coloured message box with a "x" dismiss
3
//! affordance and a `visible` state) that, instead of sitting inline, floats as
4
//! an overlay pinned to a corner of its positioned parent
5
//! (`position: absolute; bottom; right`).
6
//!
7
//! Like [`crate::widgets::alert::Alert`] / [`crate::widgets::check_box::CheckBox`]
8
//! it is stateful: it carries a [`ToastStateWrapper`] (`{ visible } + on_dismiss`)
9
//! in a [`RefAny`] attached to the "x" close button. Clicking "x" flips `visible`
10
//! to `false`, invokes the optional user `on_dismiss`, and hides the whole toast
11
//! by setting `display: none` on the container via `set_css_property` (mirroring
12
//! alert's / check_box's live restyle).
13
//!
14
//! TODO2 — **auto-dismiss is intentionally NOT implemented (be honest, don't fake
15
//! it).** A real toast disappears on its own after N seconds. That requires a
16
//! host-driven `Timer`/`Update` loop that re-enters the event loop on a clock
17
//! tick and flips `visible` to `false` — a widget handler cannot *start* such a
18
//! timer (it only runs in response to an input event, with no access to schedule
19
//! a future wakeup). This is the same limitation the spinner hit with CSS
20
//! animation: there is no widget-local timer. So this widget ships a **manually**
21
//! dismissable toast (the "x"); a host that wants auto-timeout must register a
22
//! `Timer` itself and call `set_css_property(display: none)` (or rebuild without
23
//! the toast) when it fires.
24
//!
25
//! TODO2 — covering sibling widgets relies on paint order (being a later sibling)
26
//! because there is no real stacking-context / z-index, and a drop `box-shadow`
27
//! elevation is omitted (it needs a runtime-heap shadow value — see
28
//! `progressbar.rs`); the border + radius over the page convey the floating card.
29
//! The `display:none` relayout itself is not GUI-verified in this build.
30
//!
31
//! Key types: [`Toast`], [`ToastKind`], [`ToastState`], [`ToastOnDismiss`].
32

            
33
use azul_core::{
34
    callbacks::{CoreCallbackData, Update},
35
    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
36
    refany::RefAny,
37
};
38
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
39
use azul_css::{
40
    props::{
41
        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
42
        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutFlexGrow, LayoutPosition, LayoutInsetBottom, LayoutRight, LayoutMaxWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutMarginLeft},
43
        property::{CssProperty, *},
44
        style::{StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleCursor, StyleUserSelect},
45
    },
46
    impl_option_inner, AzString,
47
};
48

            
49
use crate::callbacks::{Callback, CallbackInfo};
50

            
51
static TOAST_CONTAINER_CLASS: &[IdOrClass] =
52
    &[Class(AzString::from_const_str("__azul-native-toast"))];
53
static TOAST_MESSAGE_CLASS: &[IdOrClass] =
54
    &[Class(AzString::from_const_str("__azul-native-toast-message"))];
55
static TOAST_CLOSE_CLASS: &[IdOrClass] =
56
    &[Class(AzString::from_const_str("__azul-native-toast-close"))];
57

            
58
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
59
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
60
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
61
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
62

            
63
/// Distance (logical px) of the toast from the bottom / right edges of its parent.
64
const TOAST_INSET: isize = 24;
65
/// Maximum width (logical px) of the toast card.
66
const TOAST_MAX_WIDTH: isize = 360;
67

            
68
/// Callback function type invoked when a toast's "x" close button is clicked.
69
pub type ToastOnDismissCallbackType = extern "C" fn(RefAny, CallbackInfo, ToastState) -> Update;
70
impl_widget_callback!(
71
    ToastOnDismiss,
72
    OptionToastOnDismiss,
73
    ToastOnDismissCallback,
74
    ToastOnDismissCallbackType
75
);
76

            
77
azul_core::impl_managed_callback! {
78
    wrapper:        ToastOnDismissCallback,
79
    info_ty:        CallbackInfo,
80
    return_ty:      Update,
81
    default_ret:    Update::DoNothing,
82
    invoker_static: TOAST_ON_DISMISS_INVOKER,
83
    invoker_ty:     AzToastOnDismissCallbackInvoker,
84
    thunk_fn:       az_toast_on_dismiss_callback_thunk,
85
    setter_fn:      AzApp_setToastOnDismissCallbackInvoker,
86
    from_handle_fn: AzToastOnDismissCallback_createFromHostHandle,
87
    extra_args:     [ state: ToastState ],
88
}
89

            
90
/// The semantic colour variant of a [`Toast`] (Bootstrap alert palette, mirroring
91
/// [`crate::widgets::alert::AlertKind`]).
92
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
93
#[repr(C)]
94
pub enum ToastKind {
95
    /// Blue informational toast — the default.
96
    #[default]
97
    Info,
98
    /// Green success toast.
99
    Success,
100
    /// Yellow warning toast.
101
    Warning,
102
    /// Red danger/error toast.
103
    Danger,
104
}
105

            
106
impl ToastKind {
107
    /// Returns the `(background, border, text)` colours for this toast kind.
108
    #[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)
109
192
    const fn colors(&self) -> (ColorU, ColorU, ColorU) {
110
192
        match self {
111
108
            Self::Info => (
112
108
                ColorU { r: 207, g: 244, b: 252, a: 255 }, // #cff4fc
113
108
                ColorU { r: 182, g: 239, b: 251, a: 255 }, // #b6effb
114
108
                ColorU { r: 5, g: 81, b: 96, a: 255 },     // #055160
115
108
            ),
116
26
            Self::Success => (
117
26
                ColorU { r: 209, g: 231, b: 221, a: 255 }, // #d1e7dd
118
26
                ColorU { r: 186, g: 219, b: 204, a: 255 }, // #badbcc
119
26
                ColorU { r: 15, g: 81, b: 50, a: 255 },    // #0f5132
120
26
            ),
121
25
            Self::Warning => (
122
25
                ColorU { r: 255, g: 243, b: 205, a: 255 }, // #fff3cd
123
25
                ColorU { r: 255, g: 236, b: 181, a: 255 }, // #ffecb5
124
25
                ColorU { r: 102, g: 77, b: 3, a: 255 },    // #664d03
125
25
            ),
126
33
            Self::Danger => (
127
33
                ColorU { r: 248, g: 215, b: 218, a: 255 }, // #f8d7da
128
33
                ColorU { r: 245, g: 194, b: 199, a: 255 }, // #f5c2c7
129
33
                ColorU { r: 132, g: 32, b: 41, a: 255 },   // #842029
130
33
            ),
131
        }
132
192
    }
133

            
134
    /// CSS class name for this toast kind (mirrors `AlertKind::class_name`).
135
32
    #[must_use] pub const fn class_name(&self) -> &'static str {
136
32
        match self {
137
8
            Self::Info => "__azul-toast-info",
138
8
            Self::Success => "__azul-toast-success",
139
8
            Self::Warning => "__azul-toast-warning",
140
8
            Self::Danger => "__azul-toast-danger",
141
        }
142
32
    }
143
}
144

            
145
/// A transient, floating notification banner with a "x" dismiss button.
146
#[derive(Debug, Clone, PartialEq, Eq)]
147
#[repr(C)]
148
pub struct Toast {
149
    /// Runtime state (`visible`) plus the optional dismiss callback.
150
    pub toast_state: ToastStateWrapper,
151
    /// The message text shown inside the toast.
152
    pub message: AzString,
153
    /// The colour variant.
154
    pub kind: ToastKind,
155
    /// Whether to render the "x" close button (default `true` — the only way to
156
    /// dismiss; see the module-level auto-dismiss TODO2).
157
    pub dismissible: bool,
158
    /// The computed inline style for the (absolutely-positioned) container.
159
    pub container_style: CssPropertyWithConditionsVec,
160
}
161

            
162
#[derive(Debug, Default, Clone, PartialEq, Eq)]
163
#[repr(C)]
164
pub struct ToastStateWrapper {
165
    /// Whether the toast is currently visible.
166
    pub inner: ToastState,
167
    /// Optional: function to call when the toast is dismissed.
168
    pub on_dismiss: OptionToastOnDismiss,
169
}
170

            
171
/// The visible/hidden state of a [`Toast`].
172
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
173
#[repr(C)]
174
pub struct ToastState {
175
    /// `true` (default) = shown, `false` = dismissed/hidden.
176
    pub visible: bool,
177
}
178

            
179
impl Default for ToastState {
180
102
    fn default() -> Self {
181
102
        Self { visible: true }
182
102
    }
183
}
184

            
185
/// Builds the container style for a given [`ToastKind`]. Mirrors
186
/// `alert::build_alert_style` but pins the box to the bottom-right corner of its
187
/// positioned parent (`position: absolute`) and caps its width instead of
188
/// stretching to fill a flex column.
189
162
fn build_toast_style(kind: ToastKind) -> CssPropertyWithConditionsVec {
190
162
    let (bg, border, text) = kind.colors();
191
162
    let bg_vec =
192
162
        StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
193
162
    CssPropertyWithConditionsVec::from_vec(alloc::vec![
194
162
        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
195
162
        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
196
162
            LayoutFlexDirection::Row,
197
        )),
198
162
        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
199
162
        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
200
            0,
201
        ))),
202
        // Float pinned to the bottom-right corner of the positioned parent.
203
162
        CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
204
162
        CssPropertyWithConditions::simple(CssProperty::const_bottom(LayoutInsetBottom::const_px(
205
            TOAST_INSET,
206
        ))),
207
162
        CssPropertyWithConditions::simple(CssProperty::const_right(LayoutRight::const_px(
208
            TOAST_INSET,
209
        ))),
210
        // Cap the width so the toast hugs its content rather than spanning the page.
211
162
        CssPropertyWithConditions::simple(CssProperty::const_max_width(LayoutMaxWidth::const_px(
212
            TOAST_MAX_WIDTH,
213
        ))),
214
        // padding: 12px
215
162
        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
216
            12,
217
        ))),
218
162
        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
219
162
            LayoutPaddingBottom::const_px(12),
220
        )),
221
162
        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
222
162
            LayoutPaddingLeft::const_px(12),
223
        )),
224
162
        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
225
162
            LayoutPaddingRight::const_px(12),
226
        )),
227
        // border: 1px solid <border>
228
162
        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
229
162
            LayoutBorderTopWidth::const_px(1),
230
        )),
231
162
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
232
162
            LayoutBorderBottomWidth::const_px(1),
233
        )),
234
162
        CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
235
162
            LayoutBorderLeftWidth::const_px(1),
236
        )),
237
162
        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
238
162
            LayoutBorderRightWidth::const_px(1),
239
        )),
240
162
        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
241
162
            inner: BorderStyle::Solid,
242
162
        })),
243
162
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
244
162
            StyleBorderBottomStyle {
245
162
                inner: BorderStyle::Solid,
246
162
            },
247
        )),
248
162
        CssPropertyWithConditions::simple(CssProperty::const_border_left_style(
249
162
            StyleBorderLeftStyle {
250
162
                inner: BorderStyle::Solid,
251
162
            },
252
        )),
253
162
        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
254
162
            StyleBorderRightStyle {
255
162
                inner: BorderStyle::Solid,
256
162
            },
257
        )),
258
162
        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
259
162
            inner: border,
260
162
        })),
261
162
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
262
162
            StyleBorderBottomColor { inner: border },
263
        )),
264
162
        CssPropertyWithConditions::simple(CssProperty::const_border_left_color(
265
162
            StyleBorderLeftColor { inner: border },
266
        )),
267
162
        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
268
162
            StyleBorderRightColor { inner: border },
269
        )),
270
        // border-radius: 6px
271
162
        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
272
162
            StyleBorderTopLeftRadius::const_px(6),
273
        )),
274
162
        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
275
162
            StyleBorderTopRightRadius::const_px(6),
276
        )),
277
162
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
278
162
            StyleBorderBottomLeftRadius::const_px(6),
279
        )),
280
162
        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
281
162
            StyleBorderBottomRightRadius::const_px(6),
282
        )),
283
162
        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
284
162
        CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
285
        // Text colour is inherited by the message + close children.
286
162
        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
287
162
            inner: text,
288
162
        })),
289
162
        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
290
    ])
291
162
}
292

            
293
/// Message-text style: takes the remaining horizontal space, left-aligned.
294
static TOAST_MESSAGE_STYLE: &[CssPropertyWithConditions] = &[
295
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
296
    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
297
];
298

            
299
/// Close-button ("x") style: a small pointer-cursor box on the right.
300
static TOAST_CLOSE_STYLE: &[CssPropertyWithConditions] = &[
301
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
302
    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
303
    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
304
    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
305
    CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(
306
        12,
307
    ))),
308
];
309

            
310
impl Toast {
311
    /// Creates a new informational (blue) toast with the given message (visible,
312
    /// with a "x" close button).
313
    #[inline]
314
77
    #[must_use] pub fn create(message: AzString) -> Self {
315
77
        Self::with_kind(message, ToastKind::Info)
316
77
    }
317

            
318
    /// Creates a new toast with the given message and colour variant.
319
    #[inline]
320
93
    #[must_use] pub fn with_kind(message: AzString, kind: ToastKind) -> Self {
321
93
        Self {
322
93
            toast_state: ToastStateWrapper::default(),
323
93
            message,
324
93
            kind,
325
93
            dismissible: true,
326
93
            container_style: build_toast_style(kind),
327
93
        }
328
93
    }
329

            
330
    /// Sets the colour variant, recomputing the container style.
331
    #[inline]
332
24
    pub fn set_kind(&mut self, kind: ToastKind) {
333
24
        self.kind = kind;
334
24
        self.container_style = build_toast_style(kind);
335
24
    }
336

            
337
    /// Builder-style setter for the colour variant.
338
    #[inline]
339
6
    #[must_use] pub fn with_toast_kind(mut self, kind: ToastKind) -> Self {
340
6
        self.set_kind(kind);
341
6
        self
342
6
    }
343

            
344
    /// Sets whether the toast shows a "x" close button.
345
    #[inline]
346
21
    pub const fn set_dismissible(&mut self, dismissible: bool) {
347
21
        self.dismissible = dismissible;
348
21
    }
349

            
350
    /// Builder-style setter for the dismissible flag.
351
    #[inline]
352
13
    #[must_use] pub const fn with_dismissible(mut self, dismissible: bool) -> Self {
353
13
        self.set_dismissible(dismissible);
354
13
        self
355
13
    }
356

            
357
    /// Sets the dismiss callback. Implies `dismissible = true` so the close
358
    /// button is rendered.
359
    #[inline]
360
9
    pub fn set_on_dismiss<C: Into<ToastOnDismissCallback>>(&mut self, data: RefAny, on_dismiss: C) {
361
9
        self.dismissible = true;
362
9
        self.toast_state.on_dismiss = Some(ToastOnDismiss {
363
9
            callback: on_dismiss.into(),
364
9
            refany: data,
365
9
        })
366
9
        .into();
367
9
    }
368

            
369
    /// Builder-style setter for the dismiss callback (implies dismissible).
370
    #[inline]
371
4
    #[must_use] pub fn with_on_dismiss<C: Into<ToastOnDismissCallback>>(
372
4
        mut self,
373
4
        data: RefAny,
374
4
        on_dismiss: C,
375
4
    ) -> Self {
376
4
        self.set_on_dismiss(data, on_dismiss);
377
4
        self
378
4
    }
379

            
380
    /// Replaces `self` with a default (empty info) toast and returns the original.
381
    #[inline]
382
6
    #[must_use] pub fn swap_with_default(&mut self) -> Self {
383
6
        let mut s = Self::create(AzString::from_const_str(""));
384
6
        core::mem::swap(&mut s, self);
385
6
        s
386
6
    }
387

            
388
    /// Converts this toast into a DOM subtree with the `__azul-native-toast` class.
389
    #[inline]
390
35
    #[must_use] pub fn dom(self) -> Dom {
391
        use azul_core::{
392
            callbacks::CoreCallback,
393
            dom::{EventFilter, HoverEventFilter},
394
            refany::OptionRefAny,
395
        };
396

            
397
35
        let message = Dom::create_p_with_text(self.message)
398
35
            .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_MESSAGE_CLASS))
399
35
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TOAST_MESSAGE_STYLE));
400

            
401
35
        let mut children = alloc::vec![message];
402

            
403
35
        if self.dismissible {
404
31
            let close = Dom::create_p_with_text(AzString::from_const_str("\u{00D7}"))
405
31
                .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_CLOSE_CLASS))
406
31
                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TOAST_CLOSE_STYLE))
407
31
                .with_tab_index(TabIndex::Auto)
408
31
                .with_callbacks(
409
31
                    alloc::vec![CoreCallbackData {
410
31
                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
411
31
                        callback: CoreCallback {
412
31
                            cb: default_on_toast_dismiss as usize,
413
31
                            ctx: OptionRefAny::None,
414
31
                        },
415
31
                        refany: RefAny::new(self.toast_state),
416
31
                    }]
417
31
                    .into(),
418
31
                );
419
31
            children.push(close);
420
31
        }
421

            
422
35
        Dom::create_div()
423
35
            .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_CONTAINER_CLASS))
424
35
            .with_css_props(self.container_style)
425
35
            .with_children(children.into())
426
35
    }
427
}
428

            
429
impl Default for Toast {
430
18
    fn default() -> Self {
431
18
        Self::create(AzString::from_const_str(""))
432
18
    }
433
}
434

            
435
/// Close-button click handler. The hit node is the close button (the
436
/// callback-bearing node, per `currentTarget` semantics — see `alert`); its
437
/// parent is the toast container. Flips `visible` to `false`, invokes the
438
/// optional user callback, then hides the whole toast via `display: none`.
439
12
extern "C" fn default_on_toast_dismiss(mut data: RefAny, mut info: CallbackInfo) -> Update {
440
12
    let close_node = info.get_hit_node();
441
12
    let Some(container) = info.get_parent(close_node) else {
442
4
        return Update::DoNothing;
443
    };
444

            
445
7
    let result = {
446
8
        let Some(mut toast) = data.downcast_mut::<ToastStateWrapper>() else {
447
1
            return Update::DoNothing;
448
        };
449
7
        toast.inner.visible = false;
450
7
        let inner = toast.inner;
451
7
        let toast = &mut *toast;
452
7
        match toast.on_dismiss.as_mut() {
453
4
            Some(ToastOnDismiss { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
454
3
            None => Update::DoNothing,
455
        }
456
    };
457

            
458
    // TODO2: hides the toast by toggling `display: none` via set_css_property.
459
    // This follows the proven live-restyle pattern of alert/check_box (which
460
    // toggle display/opacity/background); the display:none relayout itself is not
461
    // GUI-verified in this build. (Auto-timeout dismissal is a host-driven Timer —
462
    // see the module-level TODO2 — and is intentionally not attempted here.)
463
7
    info.set_css_property(container, CssProperty::const_display(LayoutDisplay::None));
464

            
465
7
    result
466
12
}
467

            
468
impl From<Toast> for Dom {
469
1
    fn from(t: Toast) -> Self {
470
1
        t.dom()
471
1
    }
472
}
473

            
474
#[cfg(test)]
475
// `assertions_on_constants`: these are deliberate invariant guards over sibling
476
// `const`s in this module. They are const-foldable *today*, which is exactly the
477
// point — they must go red the moment someone edits one of those constants into an
478
// inconsistent value. Deleting them (clippy's suggestion) would delete the check.
479
#[allow(clippy::assertions_on_constants)]
480
mod autotest_generated {
481
    use std::{
482
        collections::{BTreeMap, HashMap},
483
        sync::{Arc, Mutex},
484
    };
485

            
486
    use azul_core::{
487
        dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
488
        geom::{LogicalRect, OptionLogicalPosition},
489
        gl::OptionGlContextPtr,
490
        hit_test::ScrollPosition,
491
        refany::OptionRefAny,
492
        resources::RendererResources,
493
        styled_dom::{NodeHierarchyItemId, StyledDom},
494
        window::{MonitorVec, RawWindowHandle},
495
    };
496
    use azul_css::{
497
        props::basic::{length::SizeMetric, pixel::PixelValue},
498
        system::SystemStyle,
499
    };
500
    use rust_fontconfig::FcFontCache;
501

            
502
    use super::*;
503
    #[cfg(feature = "icu")]
504
    use crate::icu::IcuLocalizerHandle;
505
    use crate::{
506
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
507
        solver3::{display_list::DisplayList, layout_tree::LayoutTree},
508
        window::{DomLayoutResult, LayoutWindow},
509
        window_state::FullWindowState,
510
    };
511

            
512
    // ------------------------------------------------------------------
513
    // Helpers
514
    // ------------------------------------------------------------------
515

            
516
    const ALL_KINDS: [ToastKind; 4] = [
517
        ToastKind::Info,
518
        ToastKind::Success,
519
        ToastKind::Warning,
520
        ToastKind::Danger,
521
    ];
522

            
523
    /// The text of a text node, looking through the `<p>` block wrapper the
524
    /// label convention mandates (`p > text`).
525
    fn text_of(node: &Dom) -> Option<&str> {
526
        match node.root.get_node_type() {
527
            NodeType::Text(s) => Some(s.as_ref().as_str()),
528
            NodeType::P => match node.children.as_ref() {
529
                [only] => match only.root.get_node_type() {
530
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
531
                    _ => None,
532
                },
533
                _ => None,
534
            },
535
            _ => None,
536
        }
537
    }
538

            
539
    /// The inline (static) CSS properties actually attached to a DOM node.
540
    fn inline_props(node: &Dom) -> Vec<CssProperty> {
541
        node.root
542
            .style
543
            .iter_inline_properties()
544
            .map(|(p, _)| p.clone())
545
            .collect()
546
    }
547

            
548
    /// The `background-color` of a style vec (first background layer only).
549
    fn background_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
550
        style.as_ref().iter().find_map(|p| match &p.property {
551
            CssProperty::BackgroundContent(v) => match v.get_property()?.as_ref().first()? {
552
                StyleBackgroundContent::Color(c) => Some(*c),
553
                _ => None,
554
            },
555
            _ => None,
556
        })
557
    }
558

            
559
    /// Every `border-*-color` in a style vec, in declaration order.
560
    fn border_colors(style: &CssPropertyWithConditionsVec) -> Vec<ColorU> {
561
        style
562
            .as_ref()
563
            .iter()
564
            .filter_map(|p| match &p.property {
565
                CssProperty::BorderTopColor(v) => v.get_property().map(|c| c.inner),
566
                CssProperty::BorderBottomColor(v) => v.get_property().map(|c| c.inner),
567
                CssProperty::BorderLeftColor(v) => v.get_property().map(|c| c.inner),
568
                CssProperty::BorderRightColor(v) => v.get_property().map(|c| c.inner),
569
                _ => None,
570
            })
571
            .collect()
572
    }
573

            
574
    /// The `color` (text colour) of a style vec.
575
    fn text_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
576
        style.as_ref().iter().find_map(|p| match &p.property {
577
            CssProperty::TextColor(v) => v.get_property().map(|c| c.inner),
578
            _ => None,
579
        })
580
    }
581

            
582
    /// The declared `position` of a style vec.
583
    fn position_of(style: &CssPropertyWithConditionsVec) -> Option<LayoutPosition> {
584
        style.as_ref().iter().find_map(|p| match &p.property {
585
            CssProperty::Position(v) => v.get_property().copied(),
586
            _ => None,
587
        })
588
    }
589

            
590
    /// The `bottom` offset of a style vec, as a raw `PixelValue`.
591
    fn bottom_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
592
        style.as_ref().iter().find_map(|p| match &p.property {
593
            CssProperty::Bottom(v) => v.get_property().map(|b| b.inner),
594
            _ => None,
595
        })
596
    }
597

            
598
    /// The `right` offset of a style vec, as a raw `PixelValue`.
599
    fn right_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
600
        style.as_ref().iter().find_map(|p| match &p.property {
601
            CssProperty::Right(v) => v.get_property().map(|r| r.inner),
602
            _ => None,
603
        })
604
    }
605

            
606
    /// The `max-width` of a style vec, as a raw `PixelValue`.
607
    fn max_width_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
608
        style.as_ref().iter().find_map(|p| match &p.property {
609
            CssProperty::MaxWidth(v) => v.get_property().map(|w| w.inner),
610
            _ => None,
611
        })
612
    }
613

            
614
    /// The *kind* of every declared property, in order (ignores the values).
615
    fn property_types(
616
        style: &CssPropertyWithConditionsVec,
617
    ) -> Vec<core::mem::Discriminant<CssProperty>> {
618
        style
619
            .as_ref()
620
            .iter()
621
            .map(|p| core::mem::discriminant(&p.property))
622
            .collect()
623
    }
624

            
625
    /// A `RefAny` payload recording every `ToastState` a user `on_dismiss` sees.
626
    struct DismissLog {
627
        calls: Vec<bool>,
628
    }
629

            
630
    extern "C" fn record_dismiss(mut data: RefAny, _: CallbackInfo, state: ToastState) -> Update {
631
        if let Some(mut log) = data.downcast_mut::<DismissLog>() {
632
            log.calls.push(state.visible);
633
        }
634
        Update::RefreshDom
635
    }
636

            
637
    extern "C" fn dismiss_do_nothing(_: RefAny, _: CallbackInfo, _: ToastState) -> Update {
638
        Update::DoNothing
639
    }
640

            
641
    fn dismiss_cb(f: ToastOnDismissCallbackType) -> ToastOnDismissCallback {
642
        f.into()
643
    }
644

            
645
    /// `visible` of a `ToastStateWrapper` payload.
646
    fn wrapper_visible(data: &mut RefAny) -> bool {
647
        data.downcast_ref::<ToastStateWrapper>()
648
            .expect("payload must still be a ToastStateWrapper")
649
            .inner
650
            .visible
651
    }
652

            
653
    /// The `visible` flags recorded by a `DismissLog` payload.
654
    fn log_calls(data: &mut RefAny) -> Vec<bool> {
655
        data.downcast_ref::<DismissLog>()
656
            .expect("payload must still be a DismissLog")
657
            .calls
658
            .clone()
659
    }
660

            
661
    /// A `DomLayoutResult` with an *empty* layout tree: the dismiss handler only
662
    /// walks `styled_dom.node_hierarchy`, so no real layout (and no font) is needed.
663
    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
664
        DomLayoutResult {
665
            styled_dom,
666
            layout_tree: LayoutTree {
667
                nodes: Vec::new(),
668
                warm: Vec::new(),
669
                cold: Vec::new(),
670
                root: 0,
671
                dom_to_layout: BTreeMap::new(),
672
                children_arena: Vec::new(),
673
                children_offsets: Vec::new(),
674
                subtree_needs_intrinsic: Vec::new(),
675
            },
676
            calculated_positions: Vec::new(),
677
            viewport: LogicalRect::zero(),
678
            display_list: Arc::new(DisplayList::default()),
679
            scroll_ids: HashMap::new(),
680
            scroll_id_to_node_id: HashMap::new(),
681
        }
682
    }
683

            
684
    /// The flattened DOM of a default toast: `container(0)`, `message(1)`,
685
    /// `close(2)` — i.e. exactly the hierarchy `default_on_toast_dismiss` walks
686
    /// (hit node -> parent).
687
    fn dismissible_styled_dom() -> StyledDom {
688
        let toast = Toast::create(AzString::from("msg"));
689
        assert!(
690
            toast.dismissible,
691
            "a fresh toast must already carry a close button"
692
        );
693
        let styled = StyledDom::create_from_dom(toast.dom());
694
        assert_eq!(
695
            styled.node_hierarchy.as_ref().len(),
696
            5,
697
            "fixture must flatten to container / message <p> + text / close <p> + text"
698
        );
699
        styled
700
    }
701

            
702
    /// Invokes `default_on_toast_dismiss` against a `LayoutWindow` holding
703
    /// `styled` (or nothing at all, when `styled` is `None`), with `hit` as the
704
    /// hit node. Returns the `Update` plus every recorded `CallbackChange`.
705
    /// Flat indices in `dismissible_styled_dom`, depth-first pre-order:
706
    /// `0 container / 1 message <p> / 2 message text / 3 close <p> / 4 close text`.
707
    /// Both callbacks and styles sit on the `<p>`s, never on the text nodes.
708
    const MESSAGE_NODE: usize = 1;
709
    const CLOSE_NODE: usize = 3;
710

            
711
    fn run_dismiss(
712
        styled: Option<StyledDom>,
713
        hit: usize,
714
        data: RefAny,
715
    ) -> (Update, Vec<CallbackChange>) {
716
        let mut layout_window =
717
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
718
        if let Some(sd) = styled {
719
            layout_window
720
                .layout_results
721
                .insert(DomId::ROOT_ID, layout_result(sd));
722
        }
723

            
724
        let renderer_resources = RendererResources::default();
725
        let previous_window_state: Option<FullWindowState> = None;
726
        let current_window_state = FullWindowState::default();
727
        let gl_context = OptionGlContextPtr::None;
728
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
729
            BTreeMap::new();
730
        let window_handle = RawWindowHandle::Unsupported;
731
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
732

            
733
        let ref_data = CallbackInfoRefData {
734
            layout_window: &layout_window,
735
            renderer_resources: &renderer_resources,
736
            previous_window_state: &previous_window_state,
737
            current_window_state: &current_window_state,
738
            gl_context: &gl_context,
739
            current_scroll_manager: &scroll_states,
740
            current_window_handle: &window_handle,
741
            system_callbacks: &system_callbacks,
742
            system_style: Arc::new(SystemStyle::default()),
743
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
744
            #[cfg(feature = "icu")]
745
            icu_localizer: IcuLocalizerHandle::default(),
746
            ctx: OptionRefAny::None,
747
        };
748

            
749
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
750

            
751
        let info = CallbackInfo::new(
752
            &ref_data,
753
            &changes,
754
            DomNodeId {
755
                dom: DomId::ROOT_ID,
756
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
757
            },
758
            OptionLogicalPosition::None,
759
            OptionLogicalPosition::None,
760
        );
761

            
762
        let update = default_on_toast_dismiss(data, info);
763
        let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
764
        (update, recorded)
765
    }
766

            
767
    /// Every `display` write recorded in the change log, as `(node index, display)`.
768
    fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
769
        let mut out = Vec::new();
770
        for change in changes {
771
            if let CallbackChange::ChangeNodeCssProperties {
772
                node_id, properties, ..
773
            } = change
774
            {
775
                for p in properties.as_ref() {
776
                    if let CssProperty::Display(v) = p {
777
                        if let Some(d) = v.get_property() {
778
                            out.push((node_id.index(), *d));
779
                        }
780
                    }
781
                }
782
            }
783
        }
784
        out
785
    }
786

            
787
    // ------------------------------------------------------------------
788
    // ToastKind::colors  (getter)
789
    // ------------------------------------------------------------------
790

            
791
    #[test]
792
    fn kind_colors_are_the_documented_bootstrap_palette() {
793
        let expect = |(r, g, b): (u8, u8, u8)| ColorU { r, g, b, a: 255 };
794

            
795
        assert_eq!(
796
            ToastKind::Info.colors(),
797
            (
798
                expect((207, 244, 252)), // #cff4fc
799
                expect((182, 239, 251)), // #b6effb
800
                expect((5, 81, 96)),     // #055160
801
            )
802
        );
803
        assert_eq!(
804
            ToastKind::Success.colors(),
805
            (
806
                expect((209, 231, 221)), // #d1e7dd
807
                expect((186, 219, 204)), // #badbcc
808
                expect((15, 81, 50)),    // #0f5132
809
            )
810
        );
811
        assert_eq!(
812
            ToastKind::Warning.colors(),
813
            (
814
                expect((255, 243, 205)), // #fff3cd
815
                expect((255, 236, 181)), // #ffecb5
816
                expect((102, 77, 3)),    // #664d03
817
            )
818
        );
819
        assert_eq!(
820
            ToastKind::Danger.colors(),
821
            (
822
                expect((248, 215, 218)), // #f8d7da
823
                expect((245, 194, 199)), // #f5c2c7
824
                expect((132, 32, 41)),   // #842029
825
            )
826
        );
827
    }
828

            
829
    #[test]
830
    fn kind_colors_are_fully_opaque_and_pairwise_distinct() {
831
        for kind in ALL_KINDS {
832
            let (bg, border, text) = kind.colors();
833
            for (name, c) in [("bg", bg), ("border", border), ("text", text)] {
834
                assert_eq!(c.a, 255, "{kind:?}.{name} must be fully opaque");
835
            }
836
            // a floating card is only legible if bg != text
837
            assert_ne!(bg, text, "{kind:?}: background must differ from text");
838
            // ... and only visible against the page if bg != border
839
            assert_ne!(bg, border, "{kind:?}: the border must be visible on the card");
840
        }
841

            
842
        for (i, a) in ALL_KINDS.iter().enumerate() {
843
            for b in &ALL_KINDS[i + 1..] {
844
                assert_ne!(
845
                    a.colors(),
846
                    b.colors(),
847
                    "{a:?} and {b:?} must be visually distinguishable"
848
                );
849
            }
850
        }
851
    }
852

            
853
    #[test]
854
    fn kind_colors_default_is_info_and_the_call_is_pure() {
855
        assert_eq!(ToastKind::default(), ToastKind::Info);
856
        assert_eq!(ToastKind::default().colors(), ToastKind::Info.colors());
857

            
858
        // repeated calls on the same (Copy) receiver must be stable
859
        let k = ToastKind::Danger;
860
        assert_eq!(k.colors(), k.colors());
861
        assert_eq!(k.colors(), k.colors());
862
    }
863

            
864
    #[test]
865
    fn kind_colors_is_const_evaluable() {
866
        const INFO: (ColorU, ColorU, ColorU) = ToastKind::Info.colors();
867
        assert_eq!(
868
            INFO.0,
869
            ColorU {
870
                r: 207,
871
                g: 244,
872
                b: 252,
873
                a: 255
874
            }
875
        );
876
    }
877

            
878
    // ------------------------------------------------------------------
879
    // ToastKind::class_name  (getter)
880
    // ------------------------------------------------------------------
881

            
882
    #[test]
883
    fn class_name_exact_values_and_shape() {
884
        assert_eq!(ToastKind::Info.class_name(), "__azul-toast-info");
885
        assert_eq!(ToastKind::Success.class_name(), "__azul-toast-success");
886
        assert_eq!(ToastKind::Warning.class_name(), "__azul-toast-warning");
887
        assert_eq!(ToastKind::Danger.class_name(), "__azul-toast-danger");
888

            
889
        for kind in ALL_KINDS {
890
            let name = kind.class_name();
891
            assert!(
892
                name.starts_with("__azul-toast-"),
893
                "{kind:?} -> {name:?} must keep the widget prefix"
894
            );
895
            assert!(
896
                !name.contains(char::is_whitespace),
897
                "{name:?} must be a single CSS class token"
898
            );
899
            assert!(name.is_ascii(), "{name:?} must stay ASCII");
900
            // stable across calls, and equal for equal kinds
901
            assert_eq!(name, kind.class_name());
902
        }
903
    }
904

            
905
    #[test]
906
    fn class_name_is_unique_per_kind() {
907
        let mut names: Vec<&str> = ALL_KINDS.iter().map(|k| k.class_name()).collect();
908
        names.sort_unstable();
909
        names.dedup();
910
        assert_eq!(names.len(), 4, "every kind needs its own class name");
911
    }
912

            
913
    #[test]
914
    fn class_name_never_collides_with_the_structural_classes() {
915
        // the kind classes live in a different namespace than the three
916
        // `__azul-native-toast*` structural classes emitted by `dom()`
917
        let structural = ["__azul-native-toast", "__azul-native-toast-message",
918
                          "__azul-native-toast-close"];
919
        for kind in ALL_KINDS {
920
            for s in structural {
921
                assert_ne!(kind.class_name(), s, "{kind:?} must not shadow {s:?}");
922
            }
923
        }
924
    }
925

            
926
    #[test]
927
    fn class_name_is_const_evaluable() {
928
        const DANGER: &str = ToastKind::Danger.class_name();
929
        assert_eq!(DANGER, "__azul-toast-danger");
930
    }
931

            
932
    // ------------------------------------------------------------------
933
    // build_toast_style
934
    // ------------------------------------------------------------------
935

            
936
    #[test]
937
    fn build_toast_style_declares_the_same_properties_for_every_kind() {
938
        let info = property_types(&build_toast_style(ToastKind::Info));
939
        assert_eq!(
940
            info.len(),
941
            32,
942
            "the container style declares 32 properties (pin: adding/removing one is a \
943
             deliberate change)"
944
        );
945

            
946
        for kind in ALL_KINDS {
947
            let style = build_toast_style(kind);
948
            assert_eq!(
949
                property_types(&style),
950
                info,
951
                "{kind:?} must declare the same properties, in the same order, as Info"
952
            );
953
            // the style is unconditional: nothing is gated behind :hover/@media/...
954
            for p in style.as_ref() {
955
                assert!(
956
                    p.apply_if.as_ref().is_empty(),
957
                    "{kind:?}: {:?} must be unconditional",
958
                    p.property
959
                );
960
            }
961
        }
962
    }
963

            
964
    #[test]
965
    fn build_toast_style_declares_no_property_twice() {
966
        // a duplicated property would silently shadow the earlier declaration
967
        for kind in ALL_KINDS {
968
            let types = property_types(&build_toast_style(kind));
969
            for (i, a) in types.iter().enumerate() {
970
                for b in &types[i + 1..] {
971
                    assert_ne!(
972
                        a, b,
973
                        "{kind:?}: the container style declares the same property twice"
974
                    );
975
                }
976
            }
977
        }
978
    }
979

            
980
    #[test]
981
    fn build_toast_style_colors_track_the_kind_palette() {
982
        for kind in ALL_KINDS {
983
            let style = build_toast_style(kind);
984
            let (bg, border, text) = kind.colors();
985

            
986
            assert_eq!(background_color(&style), Some(bg), "{kind:?}: background");
987
            assert_eq!(text_color(&style), Some(text), "{kind:?}: text colour");
988

            
989
            let borders = border_colors(&style);
990
            assert_eq!(borders.len(), 4, "{kind:?}: all four edges must be coloured");
991
            assert!(
992
                borders.iter().all(|c| *c == border),
993
                "{kind:?}: every edge must use the kind's border colour, got {borders:?}"
994
            );
995
        }
996
    }
997

            
998
    #[test]
999
    fn build_toast_style_pins_the_card_to_the_bottom_right_corner() {
        // This is what makes a toast a toast (rather than an inline alert):
        // position:absolute + bottom/right insets + a width cap.
        for kind in ALL_KINDS {
            let style = build_toast_style(kind);
            assert_eq!(
                position_of(&style),
                Some(LayoutPosition::Absolute),
                "{kind:?}: a toast must float out of flow"
            );
            assert_eq!(
                bottom_px(&style),
                Some(LayoutInsetBottom::const_px(TOAST_INSET).inner),
                "{kind:?}: bottom inset"
            );
            assert_eq!(
                right_px(&style),
                Some(LayoutRight::const_px(TOAST_INSET).inner),
                "{kind:?}: right inset"
            );
            assert_eq!(
                max_width_px(&style),
                Some(LayoutMaxWidth::const_px(TOAST_MAX_WIDTH).inner),
                "{kind:?}: width cap"
            );
            // an absolutely-positioned card must not also try to grow in a flex row
            assert!(
                style.as_ref().contains(&CssPropertyWithConditions::simple(
                    CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))
                )),
                "{kind:?}: the container must not flex-grow"
            );
        }
    }
    #[test]
    fn toast_inset_and_max_width_survive_the_fixed_point_encoding_exactly() {
        // The `isize`-backed `FloatValue` encoding must reproduce the constants
        // bit-exactly — a drifting inset silently mis-places every toast.
        assert_eq!(TOAST_INSET, 24);
        assert_eq!(TOAST_MAX_WIDTH, 360);
        assert!(
            TOAST_INSET > 0 && TOAST_MAX_WIDTH > TOAST_INSET,
            "an inset must push the card inward, and the cap must exceed the inset"
        );
        let style = build_toast_style(ToastKind::Info);
        for (name, got, want) in [
            ("bottom", bottom_px(&style), TOAST_INSET),
            ("right", right_px(&style), TOAST_INSET),
            ("max-width", max_width_px(&style), TOAST_MAX_WIDTH),
        ] {
            let pv = got.unwrap_or_else(|| panic!("{name} must be declared"));
            assert_eq!(
                pv.metric,
                SizeMetric::Px,
                "{name} must be an absolute px length, not a %/em"
            );
            assert!(
                (pv.number.get() - want as f32).abs() < f32::EPSILON,
                "{name}: {} px decoded back as {}",
                want,
                pv.number.get()
            );
            assert!(
                pv.number.get().is_finite(),
                "{name} must never decode to NaN/inf"
            );
        }
    }
    #[test]
    fn build_toast_style_geometry_is_kind_independent() {
        // Everything that is *not* a colour must be identical for all kinds.
        let expected = [
            CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
            CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
                LayoutFlexDirection::Row,
            )),
            CssPropertyWithConditions::simple(CssProperty::const_align_items(
                LayoutAlignItems::Start,
            )),
            CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
            CssPropertyWithConditions::simple(CssProperty::const_padding_top(
                LayoutPaddingTop::const_px(12),
            )),
            CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
                LayoutBorderTopWidth::const_px(1),
            )),
            CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
                StyleBorderTopLeftRadius::const_px(6),
            )),
            CssPropertyWithConditions::simple(CssProperty::const_font_size(
                StyleFontSize::const_px(14),
            )),
        ];
        for kind in ALL_KINDS {
            let style = build_toast_style(kind);
            for want in &expected {
                assert!(
                    style.as_ref().contains(want),
                    "{kind:?}: missing {:?}",
                    want.property
                );
            }
        }
    }
    #[test]
    fn build_toast_style_differs_only_in_the_colours() {
        let info = build_toast_style(ToastKind::Info);
        for kind in [ToastKind::Success, ToastKind::Warning, ToastKind::Danger] {
            let other = build_toast_style(kind);
            let differing: Vec<_> = info
                .as_ref()
                .iter()
                .zip(other.as_ref().iter())
                .filter(|(a, b)| a != b)
                .map(|(a, _)| core::mem::discriminant(&a.property))
                .collect();
            // background + 4 border colours + text colour = 6 kind-dependent props
            assert_eq!(
                differing.len(),
                6,
                "{kind:?}: only bg + 4 border colours + text colour may depend on the kind"
            );
        }
    }
    #[test]
    fn build_toast_style_is_pure_and_repeatable() {
        for kind in ALL_KINDS {
            assert_eq!(
                build_toast_style(kind),
                build_toast_style(kind),
                "{kind:?}: the builder must be deterministic"
            );
        }
    }
    // ------------------------------------------------------------------
    // Toast::create / with_kind / Default
    // ------------------------------------------------------------------
    #[test]
    fn create_is_an_info_toast_that_is_dismissible_by_default() {
        let toast = Toast::create(AzString::from("hello"));
        assert_eq!(toast.message.as_str(), "hello");
        assert_eq!(toast.kind, ToastKind::Info);
        assert!(
            toast.dismissible,
            "unlike Alert, a fresh Toast ships the close button (the only way to dismiss it)"
        );
        assert!(toast.toast_state.inner.visible, "a fresh toast is visible");
        assert!(toast.toast_state.on_dismiss.is_none());
        assert_eq!(toast.container_style, build_toast_style(ToastKind::Info));
    }
    #[test]
    fn toast_state_defaults_to_visible_not_to_the_bool_default() {
        // `bool::default()` is false — `ToastState` must *override* that, else
        // every default-constructed toast would start out already dismissed.
        assert!(ToastState::default().visible);
        assert!(ToastStateWrapper::default().inner.visible);
        assert!(ToastStateWrapper::default().on_dismiss.is_none());
        assert!(Toast::default().toast_state.inner.visible);
    }
    #[test]
    fn create_with_empty_message_equals_default_and_is_value_comparable() {
        assert_eq!(Toast::create(AzString::from("")), Toast::default());
        // equality is structural, not pointer-based
        assert_eq!(
            Toast::create(AzString::from("a")),
            Toast::create(AzString::from("a"))
        );
        assert_ne!(
            Toast::create(AzString::from("a")),
            Toast::create(AzString::from("b"))
        );
        assert_ne!(
            Toast::create(AzString::from("a")),
            Toast::with_kind(AzString::from("a"), ToastKind::Danger)
        );
    }
    #[test]
    fn create_survives_extreme_messages_and_round_trips_them_into_the_dom() {
        let long = "ab".repeat(50_000);
        let cases: Vec<AzString> = alloc::vec![
            AzString::from(""),
            AzString::from(" "),
            AzString::from("a\0b"),                                  // interior NUL
            AzString::from("line\nbreak\ttab"),                      // control chars
            AzString::from("👨‍👩‍👧‍👦 e\u{0301}\u{0327} مرحبا שלום 🇩🇪"), // ZWJ + combining + RTL
            AzString::from("\u{feff}\u{202e}rtl-override"),          // BOM + bidi override
            AzString::from("×"),                                     // same glyph as the close button
            AzString::from("\u{00D7}\u{00D7}\u{00D7}"),              // three close glyphs
            AzString::from(long.as_str()),                           // 100k chars
        ];
        for message in cases {
            let toast = Toast::create(message.clone());
            assert_eq!(toast.message.as_str(), message.as_str());
            // the message must survive the trip through the DOM byte-for-byte
            let dom = toast.dom();
            let children = dom.children.as_ref();
            assert_eq!(
                children.len(),
                2,
                "message content must never change the child count"
            );
            assert_eq!(text_of(&children[0]), Some(message.as_str()));
            // a "×" in the *message* must not be mistaken for the close button
            assert!(
                children[0].root.has_class("__azul-native-toast-message"),
                "the first child is always the message"
            );
            assert!(
                children[1].root.has_class("__azul-native-toast-close"),
                "the close button is always last"
            );
        }
    }
    #[test]
    fn with_kind_stores_both_args_for_every_kind() {
        for kind in ALL_KINDS {
            let toast = Toast::with_kind(AzString::from("m"), kind);
            assert_eq!(toast.kind, kind);
            assert_eq!(toast.message.as_str(), "m");
            assert!(toast.dismissible);
            assert!(toast.toast_state.on_dismiss.is_none());
            assert!(toast.toast_state.inner.visible);
            assert_eq!(
                toast.container_style,
                build_toast_style(kind),
                "{kind:?}: the container style must match the kind it was built with"
            );
        }
    }
    #[test]
    fn create_is_with_kind_info() {
        assert_eq!(
            Toast::create(AzString::from("m")),
            Toast::with_kind(AzString::from("m"), ToastKind::Info)
        );
        assert_eq!(
            Toast::create(AzString::from("m")),
            Toast::with_kind(AzString::from("m"), ToastKind::default())
        );
    }
    // ------------------------------------------------------------------
    // set_kind / with_toast_kind
    // ------------------------------------------------------------------
    #[test]
    fn set_kind_recomputes_the_style_and_is_idempotent() {
        let mut toast = Toast::create(AzString::from("m"));
        for kind in ALL_KINDS {
            toast.set_kind(kind);
            assert_eq!(toast.kind, kind);
            assert_eq!(toast.container_style, build_toast_style(kind));
            // applying the same kind twice must not append/duplicate anything
            let before = toast.container_style.clone();
            toast.set_kind(kind);
            assert_eq!(
                toast.container_style, before,
                "{kind:?}: set_kind must be idempotent"
            );
            assert_eq!(
                toast.container_style.len(),
                32,
                "{kind:?}: restyling must not grow the property vec"
            );
        }
        // a full cycle back to the original kind restores the original toast
        let original = Toast::create(AzString::from("m"));
        let mut cycled = original.clone();
        for kind in ALL_KINDS {
            cycled.set_kind(kind);
        }
        cycled.set_kind(ToastKind::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 toast = Toast::create(AzString::from("keep me"));
        toast.set_on_dismiss(log, dismiss_cb(dismiss_do_nothing));
        toast.toast_state.inner.visible = false;
        toast.set_kind(ToastKind::Warning);
        assert_eq!(toast.message.as_str(), "keep me");
        assert!(toast.dismissible, "set_kind must not clear the close button");
        assert!(
            toast.toast_state.on_dismiss.is_some(),
            "set_kind must not drop the callback"
        );
        assert!(
            !toast.toast_state.inner.visible,
            "set_kind must not resurrect a dismissed toast"
        );
    }
    #[test]
    fn with_toast_kind_matches_set_kind_and_last_write_wins() {
        for kind in ALL_KINDS {
            let built = Toast::create(AzString::from("m")).with_toast_kind(kind);
            let mut mutated = Toast::create(AzString::from("m"));
            mutated.set_kind(kind);
            assert_eq!(built, mutated, "{kind:?}: builder and setter must agree");
        }
        let toast = Toast::create(AzString::from("m"))
            .with_toast_kind(ToastKind::Danger)
            .with_toast_kind(ToastKind::Success);
        assert_eq!(toast.kind, ToastKind::Success);
        assert_eq!(toast.container_style, build_toast_style(ToastKind::Success));
    }
    // ------------------------------------------------------------------
    // set_dismissible / with_dismissible
    // ------------------------------------------------------------------
    #[test]
    fn set_dismissible_last_write_wins_and_touches_nothing_else() {
        let mut toast = Toast::with_kind(AzString::from("m"), ToastKind::Warning);
        let style_before = toast.container_style.clone();
        for flag in [true, true, false, true, false, false] {
            toast.set_dismissible(flag);
            assert_eq!(toast.dismissible, flag);
        }
        assert_eq!(toast.kind, ToastKind::Warning);
        assert_eq!(toast.message.as_str(), "m");
        assert_eq!(
            toast.container_style, style_before,
            "toggling must not restyle"
        );
        assert!(
            toast.toast_state.on_dismiss.is_none(),
            "toggling must not invent a callback"
        );
        assert!(
            toast.toast_state.inner.visible,
            "toggling the close button must not hide the toast"
        );
    }
    #[test]
    fn with_dismissible_toggle_sequence_ends_on_the_last_value() {
        assert!(Toast::default().with_dismissible(true).dismissible);
        assert!(!Toast::default().with_dismissible(false).dismissible);
        assert!(
            !Toast::default()
                .with_dismissible(true)
                .with_dismissible(false)
                .dismissible
        );
        assert!(
            Toast::default()
                .with_dismissible(false)
                .with_dismissible(true)
                .dismissible
        );
        // builder == setter
        let mut mutated = Toast::default();
        mutated.set_dismissible(false);
        assert_eq!(Toast::default().with_dismissible(false), mutated);
        // and re-enabling restores the exact default value
        assert_eq!(
            Toast::default()
                .with_dismissible(false)
                .with_dismissible(true),
            Toast::default()
        );
    }
    // ------------------------------------------------------------------
    // set_on_dismiss / with_on_dismiss
    // ------------------------------------------------------------------
    #[test]
    fn set_on_dismiss_forces_dismissible_back_on() {
        let mut toast = Toast::create(AzString::from("m")).with_dismissible(false);
        assert!(!toast.dismissible);
        toast.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
        assert!(
            toast.dismissible,
            "a dismiss callback must re-render the close button"
        );
        assert!(toast.toast_state.on_dismiss.is_some());
        assert!(
            toast.toast_state.inner.visible,
            "wiring a callback must not hide the toast"
        );
    }
    #[test]
    fn set_on_dismiss_replaces_rather_than_appends() {
        let mut toast = Toast::create(AzString::from("m"));
        toast.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
        let first = toast
            .toast_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
        toast.set_on_dismiss(RefAny::new(9i64), dismiss_cb(record_dismiss));
        let second = toast.toast_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 toast = Toast::with_kind(AzString::from("boom"), ToastKind::Danger)
            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(dismiss_do_nothing));
        assert_eq!(toast.message.as_str(), "boom");
        assert_eq!(toast.kind, ToastKind::Danger);
        assert_eq!(toast.container_style, build_toast_style(ToastKind::Danger));
        assert!(toast.dismissible);
        assert!(toast.toast_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` forces
        // `dismissible = true`, but a later `set_dismissible(false)` wins and the
        // wired-up callback becomes unreachable. For a toast this is worse than
        // for an alert: the "x" is the *only* dismissal path (no auto-timeout),
        // so the toast can never be dismissed at all.
        let mut toast = Toast::create(AzString::from("m"));
        toast.set_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
        toast.set_dismissible(false);
        assert!(
            toast.toast_state.on_dismiss.is_some(),
            "the callback is still stored"
        );
        let dom = toast.dom();
        assert_eq!(
            dom.children.as_ref().len(),
            1,
            "no close button is rendered, so the toast is undismissable"
        );
        assert!(
            dom.children.as_ref()[0]
                .root
                .get_callbacks()
                .as_ref()
                .is_empty(),
            "and no handler is attached anywhere else either"
        );
    }
    // ------------------------------------------------------------------
    // swap_with_default
    // ------------------------------------------------------------------
    #[test]
    fn swap_with_default_returns_the_original_and_resets_self() {
        let mut toast =
            Toast::with_kind(AzString::from("payload"), ToastKind::Danger).with_dismissible(false);
        let snapshot = toast.clone();
        let returned = toast.swap_with_default();
        assert_eq!(returned, snapshot, "the original must come back untouched");
        assert_eq!(toast, Toast::default(), "self must be reset to a default toast");
        assert_eq!(toast.message.as_str(), "");
        assert_eq!(toast.kind, ToastKind::Info);
        assert!(
            toast.dismissible,
            "the reset toast is a *default* toast, so it is dismissible again"
        );
        assert!(toast.toast_state.on_dismiss.is_none());
        assert!(toast.toast_state.inner.visible);
    }
    #[test]
    fn swap_with_default_is_stable_when_repeated() {
        let mut toast = Toast::default();
        for _ in 0..3 {
            let returned = toast.swap_with_default();
            assert_eq!(returned, Toast::default());
            assert_eq!(toast, Toast::default());
        }
    }
    #[test]
    fn swap_with_default_moves_the_callback_out_of_self() {
        let mut toast = Toast::create(AzString::from("m"))
            .with_on_dismiss(RefAny::new(7u32), dismiss_cb(record_dismiss));
        let returned = toast.swap_with_default();
        assert!(
            returned.toast_state.on_dismiss.is_some(),
            "the callback moves out"
        );
        assert!(
            toast.toast_state.on_dismiss.is_none(),
            "the reset toast must not keep a reference to the old callback"
        );
    }
    #[test]
    fn swap_with_default_round_trips_a_dismissed_toast() {
        // a toast that was already dismissed must hand its `visible == false`
        // state to the caller, not silently reset it in the returned value
        let mut toast = Toast::create(AzString::from("m"));
        toast.toast_state.inner.visible = false;
        let returned = toast.swap_with_default();
        assert!(!returned.toast_state.inner.visible);
        assert!(toast.toast_state.inner.visible, "the fresh toast is visible");
    }
    // ------------------------------------------------------------------
    // Toast::dom
    // ------------------------------------------------------------------
    #[test]
    fn dom_of_a_default_toast_is_a_container_with_message_and_close() {
        let toast = Toast::create(AzString::from("hi"));
        let style = toast.container_style.clone();
        let dom = toast.dom();
        assert!(dom.root.has_class("__azul-native-toast"));
        assert!(
            dom.root.get_callbacks().as_ref().is_empty(),
            "the container itself 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(), 2, "[message, close]");
        assert!(children[0].root.has_class("__azul-native-toast-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_children_carry_exactly_the_static_child_styles() {
        let dom = Toast::create(AzString::from("hi")).dom();
        let children = dom.children.as_ref();
        let want_message: Vec<CssProperty> = TOAST_MESSAGE_STYLE
            .iter()
            .map(|p| p.property.clone())
            .collect();
        let want_close: Vec<CssProperty> = TOAST_CLOSE_STYLE
            .iter()
            .map(|p| p.property.clone())
            .collect();
        assert_eq!(inline_props(&children[0]), want_message);
        assert_eq!(inline_props(&children[1]), want_close);
        // the message takes the free space, the close button never does
        assert!(want_message.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))));
        assert!(want_close.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))));
        // ... and the "x" must not be text-selectable / must show a pointer
        assert!(want_close.contains(&CssProperty::const_cursor(StyleCursor::Pointer)));
        assert!(want_close.contains(&CssProperty::user_select(StyleUserSelect::None)));
    }
    #[test]
    fn dom_close_button_is_focusable_and_wired_to_the_dismiss_handler() {
        let dom = Toast::create(AzString::from("hi")).dom();
        let children = dom.children.as_ref();
        assert_eq!(children.len(), 2);
        let close = &children[1];
        assert!(close.root.has_class("__azul-native-toast-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_toast_dismiss as usize);
        assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
    }
    #[test]
    fn dom_hands_the_toast_state_to_the_close_button() {
        let toast = Toast::create(AzString::from("hi"))
            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
        let dom = toast.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 ToastStateWrapper"
        );
        assert!(
            payload
                .downcast_ref::<ToastStateWrapper>()
                .expect("ToastStateWrapper")
                .on_dismiss
                .is_some(),
            "the user callback must travel with the state"
        );
    }
    #[test]
    fn dom_of_a_non_dismissible_toast_has_no_callbacks_at_all() {
        let dom = Toast::create(AzString::from("m")).with_dismissible(false).dom();
        assert!(dom.root.has_class("__azul-native-toast"));
        let children = dom.children.as_ref();
        assert_eq!(children.len(), 1, "no close button without `dismissible`");
        assert!(children[0].root.has_class("__azul-native-toast-message"));
        assert!(children[0].root.get_callbacks().as_ref().is_empty());
        assert!(dom.root.get_callbacks().as_ref().is_empty());
    }
    #[test]
    fn dom_renders_even_when_the_state_says_hidden() {
        // Pinned current behaviour: `visible` is *only* consulted by the dismiss
        // handler (which restyles the live node); `dom()` ignores it, so a
        // pre-dismissed toast is still emitted at full size.  A host that
        // rebuilds its DOM must filter dismissed toasts out itself.
        let mut toast = Toast::create(AzString::from("gone"));
        toast.toast_state.inner.visible = false;
        let dom = toast.dom();
        assert_eq!(dom.children.as_ref().len(), 2);
        assert_eq!(text_of(&dom.children.as_ref()[0]), Some("gone"));
        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 hidden state travels into the DOM verbatim"
        );
    }
    #[test]
    fn dom_is_stable_across_kinds_and_the_kind_class_is_not_emitted() {
        for kind in ALL_KINDS {
            let dom = Toast::with_kind(AzString::from("m"), kind).dom();
            assert!(dom.root.has_class("__azul-native-toast"));
            assert_eq!(dom.children.as_ref().len(), 2);
            // NOTE: `ToastKind::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"
            );
        }
    }
    #[test]
    fn from_toast_for_dom_is_exactly_dom() {
        // non-dismissible, so no RefAny identity is involved in the comparison
        let toast = Toast::with_kind(AzString::from("m"), ToastKind::Success)
            .with_dismissible(false);
        let via_from = Dom::from(toast.clone());
        let via_method = toast.dom();
        assert!(
            via_from == via_method,
            "`impl From<Toast> for Dom` must delegate to `Toast::dom`"
        );
    }
    // ------------------------------------------------------------------
    // default_on_toast_dismiss
    // ------------------------------------------------------------------
    #[test]
    fn dismiss_hides_the_container_and_flips_visible() {
        let mut data = RefAny::new(ToastStateWrapper::default());
        // node 2 == the close button, 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(ToastStateWrapper {
            inner: ToastState { visible: true },
            on_dismiss: Some(ToastOnDismiss {
                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(ToastStateWrapper {
            inner: ToastState { visible: true },
            on_dismiss: Some(ToastOnDismiss {
                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_from_the_message_node_also_hides_the_container() {
        // Pinned: the handler hides `parent(hit)`, whatever the hit node is.
        // For the toast the message <p>'s parent is the container too, so a
        // mis-wired handler would still "work" - which is why the close button
        // must stay the only node carrying it (see the wiring test above).
        let mut data = RefAny::new(ToastStateWrapper::default());
        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), MESSAGE_NODE, data.clone());
        assert_eq!(update, Update::DoNothing);
        assert_eq!(
            display_writes(&changes),
            alloc::vec![(0usize, LayoutDisplay::None)]
        );
        assert!(!wrapper_visible(&mut data));
    }
    #[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(ToastStateWrapper::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(ToastStateWrapper::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_with_an_absurd_hit_node_index_does_not_panic() {
        // usize::MAX / 2 is far past any allocated NodeId
        let mut data = RefAny::new(ToastStateWrapper::default());
        let (update, changes) =
            run_dismiss(Some(dismissible_styled_dom()), usize::MAX / 2, 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(ToastStateWrapper::default());
        let (update, changes) = run_dismiss(None, CLOSE_NODE, 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 toast = Toast::create(AzString::from("bye"));
        let dom = toast.dom();
        let close = &dom.children.as_ref()[1];
        let entry = &close.root.get_callbacks().as_ref()[0];
        assert_eq!(entry.callback.cb, default_on_toast_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"
        );
    }
    #[test]
    fn dismiss_end_to_end_reaches_a_user_callback_wired_through_the_builder() {
        let mut log = RefAny::new(DismissLog { calls: Vec::new() });
        let dom = Toast::with_kind(AzString::from("bye"), ToastKind::Danger)
            .with_on_dismiss(log.clone(), dismiss_cb(record_dismiss))
            .dom();
        let payload = dom.children.as_ref()[1]
            .root
            .get_callbacks()
            .as_ref()[0]
            .refany
            .clone();
        let styled = StyledDom::create_from_dom(dom);
        let (update, changes) = run_dismiss(Some(styled), CLOSE_NODE, payload);
        assert_eq!(update, Update::RefreshDom);
        assert_eq!(
            display_writes(&changes),
            alloc::vec![(0usize, LayoutDisplay::None)]
        );
        assert_eq!(
            log_calls(&mut log),
            alloc::vec![false],
            "the builder-wired callback must fire exactly once, with visible == false"
        );
    }
}