Skip to main content

azul_layout/widgets/
alert.rs

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
18use azul_core::{
19    callbacks::{CoreCallbackData, Update},
20    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
21    refany::RefAny,
22};
23use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
24use 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
34use crate::callbacks::{Callback, CallbackInfo};
35
36static ALERT_CONTAINER_CLASS: &[IdOrClass] =
37    &[Class(AzString::from_const_str("__azul-native-alert"))];
38static ALERT_MESSAGE_CLASS: &[IdOrClass] =
39    &[Class(AzString::from_const_str("__azul-native-alert-message"))];
40static ALERT_CLOSE_CLASS: &[IdOrClass] =
41    &[Class(AzString::from_const_str("__azul-native-alert-close"))];
42
43const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
44const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
45const 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.
49pub type AlertOnDismissCallbackType = extern "C" fn(RefAny, CallbackInfo, AlertState) -> Update;
50impl_widget_callback!(
51    AlertOnDismiss,
52    OptionAlertOnDismiss,
53    AlertOnDismissCallback,
54    AlertOnDismissCallbackType
55);
56
57azul_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)]
73pub 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
85impl 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    const fn colors(&self) -> (ColorU, ColorU, ColorU) {
89        match self {
90            Self::Info => (
91                ColorU { r: 207, g: 244, b: 252, a: 255 }, // #cff4fc
92                ColorU { r: 182, g: 239, b: 251, a: 255 }, // #b6effb
93                ColorU { r: 5, g: 81, b: 96, a: 255 },     // #055160
94            ),
95            Self::Success => (
96                ColorU { r: 209, g: 231, b: 221, a: 255 }, // #d1e7dd
97                ColorU { r: 186, g: 219, b: 204, a: 255 }, // #badbcc
98                ColorU { r: 15, g: 81, b: 50, a: 255 },    // #0f5132
99            ),
100            Self::Warning => (
101                ColorU { r: 255, g: 243, b: 205, a: 255 }, // #fff3cd
102                ColorU { r: 255, g: 236, b: 181, a: 255 }, // #ffecb5
103                ColorU { r: 102, g: 77, b: 3, a: 255 },    // #664d03
104            ),
105            Self::Danger => (
106                ColorU { r: 248, g: 215, b: 218, a: 255 }, // #f8d7da
107                ColorU { r: 245, g: 194, b: 199, a: 255 }, // #f5c2c7
108                ColorU { r: 132, g: 32, b: 41, a: 255 },   // #842029
109            ),
110        }
111    }
112
113    /// CSS class name for this alert kind (mirrors `ButtonType::class_name`).
114    #[must_use] pub const fn class_name(&self) -> &'static str {
115        match self {
116            Self::Info => "__azul-alert-info",
117            Self::Success => "__azul-alert-success",
118            Self::Warning => "__azul-alert-warning",
119            Self::Danger => "__azul-alert-danger",
120        }
121    }
122}
123
124/// A coloured inline message box with an optional dismissible close button.
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[repr(C)]
127pub 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)]
142pub 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)]
152pub struct AlertState {
153    /// `true` (default) = shown, `false` = dismissed/hidden.
154    pub visible: bool,
155}
156
157impl Default for AlertState {
158    fn default() -> Self {
159        Self { visible: true }
160    }
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`).
166fn build_alert_style(kind: AlertKind) -> CssPropertyWithConditionsVec {
167    let (bg, border, text) = kind.colors();
168    let bg_vec =
169        StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
170    CssPropertyWithConditionsVec::from_vec(alloc::vec![
171        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
172        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
173            LayoutFlexDirection::Row,
174        )),
175        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
176        // Span the full width of a flex-column parent.
177        CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Stretch)),
178        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
179            0,
180        ))),
181        // padding: 12px
182        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
183            12,
184        ))),
185        CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
186            LayoutPaddingBottom::const_px(12),
187        )),
188        CssPropertyWithConditions::simple(CssProperty::const_padding_left(
189            LayoutPaddingLeft::const_px(12),
190        )),
191        CssPropertyWithConditions::simple(CssProperty::const_padding_right(
192            LayoutPaddingRight::const_px(12),
193        )),
194        // border: 1px solid <border>
195        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
196            LayoutBorderTopWidth::const_px(1),
197        )),
198        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
199            LayoutBorderBottomWidth::const_px(1),
200        )),
201        CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
202            LayoutBorderLeftWidth::const_px(1),
203        )),
204        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
205            LayoutBorderRightWidth::const_px(1),
206        )),
207        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
208            inner: BorderStyle::Solid,
209        })),
210        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
211            StyleBorderBottomStyle {
212                inner: BorderStyle::Solid,
213            },
214        )),
215        CssPropertyWithConditions::simple(CssProperty::const_border_left_style(
216            StyleBorderLeftStyle {
217                inner: BorderStyle::Solid,
218            },
219        )),
220        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
221            StyleBorderRightStyle {
222                inner: BorderStyle::Solid,
223            },
224        )),
225        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
226            inner: border,
227        })),
228        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
229            StyleBorderBottomColor { inner: border },
230        )),
231        CssPropertyWithConditions::simple(CssProperty::const_border_left_color(
232            StyleBorderLeftColor { inner: border },
233        )),
234        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
235            StyleBorderRightColor { inner: border },
236        )),
237        // border-radius: 6px
238        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
239            StyleBorderTopLeftRadius::const_px(6),
240        )),
241        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
242            StyleBorderTopRightRadius::const_px(6),
243        )),
244        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
245            StyleBorderBottomLeftRadius::const_px(6),
246        )),
247        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
248            StyleBorderBottomRightRadius::const_px(6),
249        )),
250        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
251        CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
252        // Text colour is inherited by the message + close children.
253        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
254            inner: text,
255        })),
256        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
257    ])
258}
259
260/// Message-text style: takes the remaining horizontal space, left-aligned.
261static 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.
267static 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
277impl Alert {
278    /// Creates a new informational (blue) alert with the given message.
279    #[inline]
280    #[must_use] pub fn create(message: AzString) -> Self {
281        Self::with_kind(message, AlertKind::Info)
282    }
283
284    /// Creates a new alert with the given message and colour variant.
285    #[inline]
286    #[must_use] pub fn with_kind(message: AzString, kind: AlertKind) -> Self {
287        Self {
288            alert_state: AlertStateWrapper::default(),
289            message,
290            kind,
291            dismissible: false,
292            container_style: build_alert_style(kind),
293        }
294    }
295
296    /// Sets the colour variant, recomputing the container style.
297    #[inline]
298    pub fn set_kind(&mut self, kind: AlertKind) {
299        self.kind = kind;
300        self.container_style = build_alert_style(kind);
301    }
302
303    /// Builder-style setter for the colour variant.
304    #[inline]
305    #[must_use] pub fn with_alert_kind(mut self, kind: AlertKind) -> Self {
306        self.set_kind(kind);
307        self
308    }
309
310    /// Sets whether the alert shows a "x" close button.
311    #[inline]
312    pub const fn set_dismissible(&mut self, dismissible: bool) {
313        self.dismissible = dismissible;
314    }
315
316    /// Builder-style setter for the dismissible flag.
317    #[inline]
318    #[must_use] pub const fn with_dismissible(mut self, dismissible: bool) -> Self {
319        self.set_dismissible(dismissible);
320        self
321    }
322
323    /// Sets the dismiss callback. Implies `dismissible = true` so the close
324    /// button is rendered.
325    #[inline]
326    pub fn set_on_dismiss<C: Into<AlertOnDismissCallback>>(&mut self, data: RefAny, on_dismiss: C) {
327        self.dismissible = true;
328        self.alert_state.on_dismiss = Some(AlertOnDismiss {
329            callback: on_dismiss.into(),
330            refany: data,
331        })
332        .into();
333    }
334
335    /// Builder-style setter for the dismiss callback (implies dismissible).
336    #[inline]
337    #[must_use] pub fn with_on_dismiss<C: Into<AlertOnDismissCallback>>(
338        mut self,
339        data: RefAny,
340        on_dismiss: C,
341    ) -> Self {
342        self.set_on_dismiss(data, on_dismiss);
343        self
344    }
345
346    /// Replaces `self` with a default (empty info) alert and returns the original.
347    #[inline]
348    #[must_use] pub fn swap_with_default(&mut self) -> Self {
349        let mut s = Self::create(AzString::from_const_str(""));
350        core::mem::swap(&mut s, self);
351        s
352    }
353
354    /// Converts this alert into a DOM subtree with the `__azul-native-alert` class.
355    #[inline]
356    #[must_use] pub fn dom(self) -> Dom {
357        use azul_core::{
358            callbacks::CoreCallback,
359            dom::{EventFilter, HoverEventFilter},
360            refany::OptionRefAny,
361        };
362
363        let message = Dom::create_text(self.message)
364            .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_MESSAGE_CLASS))
365            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ALERT_MESSAGE_STYLE));
366
367        let mut children = alloc::vec![message];
368
369        if self.dismissible {
370            let close = Dom::create_text(AzString::from_const_str("\u{00D7}"))
371                .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_CLOSE_CLASS))
372                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ALERT_CLOSE_STYLE))
373                .with_tab_index(TabIndex::Auto)
374                .with_callbacks(
375                    alloc::vec![CoreCallbackData {
376                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
377                        callback: CoreCallback {
378                            cb: default_on_alert_dismiss as usize,
379                            ctx: OptionRefAny::None,
380                        },
381                        refany: RefAny::new(self.alert_state),
382                    }]
383                    .into(),
384                );
385            children.push(close);
386        }
387
388        Dom::create_div()
389            .with_ids_and_classes(IdOrClassVec::from_const_slice(ALERT_CONTAINER_CLASS))
390            .with_css_props(self.container_style)
391            .with_children(children.into())
392    }
393}
394
395impl Default for Alert {
396    fn default() -> Self {
397        Self::create(AzString::from_const_str(""))
398    }
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`.
405extern "C" fn default_on_alert_dismiss(mut data: RefAny, mut info: CallbackInfo) -> Update {
406    let close_node = info.get_hit_node();
407    let Some(container) = info.get_parent(close_node) else {
408        return Update::DoNothing;
409    };
410
411    let result = {
412        let Some(mut alert) = data.downcast_mut::<AlertStateWrapper>() else {
413            return Update::DoNothing;
414        };
415        alert.inner.visible = false;
416        let inner = alert.inner;
417        let alert = &mut *alert;
418        match alert.on_dismiss.as_mut() {
419            Some(AlertOnDismiss { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
420            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    info.set_css_property(container, CssProperty::const_display(LayoutDisplay::None));
429
430    result
431}
432
433impl From<Alert> for Dom {
434    fn from(a: Alert) -> Self {
435        a.dom()
436    }
437}
438
439#[cfg(test)]
440mod 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 `NodeType::Text` node (`None` for any other node type).
481    fn text_of(node: &Dom) -> Option<&str> {
482        match node.root.get_node_type() {
483            NodeType::Text(s) => Some(s.as_ref().as_str()),
484            _ => None,
485        }
486    }
487
488    /// The `background-color` of a style vec (first background layer only).
489    fn background_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
490        style.as_ref().iter().find_map(|p| match &p.property {
491            CssProperty::BackgroundContent(v) => match v.get_property()?.as_ref().first()? {
492                StyleBackgroundContent::Color(c) => Some(*c),
493                _ => None,
494            },
495            _ => None,
496        })
497    }
498
499    /// Every `border-*-color` in a style vec, in declaration order.
500    fn border_colors(style: &CssPropertyWithConditionsVec) -> Vec<ColorU> {
501        style
502            .as_ref()
503            .iter()
504            .filter_map(|p| match &p.property {
505                CssProperty::BorderTopColor(v) => v.get_property().map(|c| c.inner),
506                CssProperty::BorderBottomColor(v) => v.get_property().map(|c| c.inner),
507                CssProperty::BorderLeftColor(v) => v.get_property().map(|c| c.inner),
508                CssProperty::BorderRightColor(v) => v.get_property().map(|c| c.inner),
509                _ => None,
510            })
511            .collect()
512    }
513
514    /// The `color` (text colour) of a style vec.
515    fn text_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
516        style.as_ref().iter().find_map(|p| match &p.property {
517            CssProperty::TextColor(v) => v.get_property().map(|c| c.inner),
518            _ => None,
519        })
520    }
521
522    /// The *kind* of every declared property, in order (ignores the values).
523    fn property_types(style: &CssPropertyWithConditionsVec) -> Vec<core::mem::Discriminant<CssProperty>> {
524        style
525            .as_ref()
526            .iter()
527            .map(|p| core::mem::discriminant(&p.property))
528            .collect()
529    }
530
531    /// A `RefAny` payload recording every `AlertState` a user `on_dismiss` sees.
532    struct DismissLog {
533        calls: Vec<bool>,
534    }
535
536    extern "C" fn record_dismiss(mut data: RefAny, _: CallbackInfo, state: AlertState) -> Update {
537        if let Some(mut log) = data.downcast_mut::<DismissLog>() {
538            log.calls.push(state.visible);
539        }
540        Update::RefreshDom
541    }
542
543    extern "C" fn dismiss_do_nothing(_: RefAny, _: CallbackInfo, _: AlertState) -> Update {
544        Update::DoNothing
545    }
546
547    fn dismiss_cb(f: AlertOnDismissCallbackType) -> AlertOnDismissCallback {
548        f.into()
549    }
550
551    /// `visible` of an `AlertStateWrapper` payload.
552    fn wrapper_visible(data: &mut RefAny) -> bool {
553        data.downcast_ref::<AlertStateWrapper>()
554            .expect("payload must still be an AlertStateWrapper")
555            .inner
556            .visible
557    }
558
559    /// The `visible` flags recorded by a `DismissLog` payload.
560    fn log_calls(data: &mut RefAny) -> Vec<bool> {
561        data.downcast_ref::<DismissLog>()
562            .expect("payload must still be a DismissLog")
563            .calls
564            .clone()
565    }
566
567    /// A `DomLayoutResult` with an *empty* layout tree: the dismiss handler only
568    /// walks `styled_dom.node_hierarchy`, so no real layout (and no font) is needed.
569    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
570        DomLayoutResult {
571            styled_dom,
572            layout_tree: LayoutTree {
573                nodes: Vec::new(),
574                warm: Vec::new(),
575                cold: Vec::new(),
576                root: 0,
577                dom_to_layout: BTreeMap::new(),
578                children_arena: Vec::new(),
579                children_offsets: Vec::new(),
580                subtree_needs_intrinsic: Vec::new(),
581            },
582            calculated_positions: Vec::new(),
583            viewport: LogicalRect::zero(),
584            display_list: DisplayList::default(),
585            scroll_ids: HashMap::new(),
586            scroll_id_to_node_id: HashMap::new(),
587        }
588    }
589
590    /// The flattened DOM of a dismissible alert: `container(0)`, `message(1)`,
591    /// `close(2)` โ€” i.e. exactly the hierarchy `default_on_alert_dismiss` walks
592    /// (hit node -> parent).
593    fn dismissible_styled_dom() -> StyledDom {
594        let alert = Alert::create(AzString::from("msg")).with_dismissible(true);
595        let styled = StyledDom::create_from_dom(alert.dom());
596        assert_eq!(
597            styled.node_hierarchy.as_ref().len(),
598            3,
599            "fixture must flatten to exactly container/message/close"
600        );
601        styled
602    }
603
604    /// Invokes `default_on_alert_dismiss` against a `LayoutWindow` holding
605    /// `styled` (or nothing at all, when `styled` is `None`), with `hit` as the
606    /// hit node. Returns the `Update` plus every recorded `CallbackChange`.
607    fn run_dismiss(
608        styled: Option<StyledDom>,
609        hit: usize,
610        data: RefAny,
611    ) -> (Update, Vec<CallbackChange>) {
612        let mut layout_window =
613            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
614        if let Some(sd) = styled {
615            layout_window
616                .layout_results
617                .insert(DomId::ROOT_ID, layout_result(sd));
618        }
619
620        let renderer_resources = RendererResources::default();
621        let previous_window_state: Option<FullWindowState> = None;
622        let current_window_state = FullWindowState::default();
623        let gl_context = OptionGlContextPtr::None;
624        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
625            BTreeMap::new();
626        let window_handle = RawWindowHandle::Unsupported;
627        let system_callbacks = ExternalSystemCallbacks::rust_internal();
628
629        let ref_data = CallbackInfoRefData {
630            layout_window: &layout_window,
631            renderer_resources: &renderer_resources,
632            previous_window_state: &previous_window_state,
633            current_window_state: &current_window_state,
634            gl_context: &gl_context,
635            current_scroll_manager: &scroll_states,
636            current_window_handle: &window_handle,
637            system_callbacks: &system_callbacks,
638            system_style: Arc::new(SystemStyle::default()),
639            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
640            #[cfg(feature = "icu")]
641            icu_localizer: IcuLocalizerHandle::default(),
642            ctx: OptionRefAny::None,
643        };
644
645        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
646
647        let info = CallbackInfo::new(
648            &ref_data,
649            &changes,
650            DomNodeId {
651                dom: DomId::ROOT_ID,
652                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
653            },
654            OptionLogicalPosition::None,
655            OptionLogicalPosition::None,
656        );
657
658        let update = default_on_alert_dismiss(data, info);
659        let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
660        (update, recorded)
661    }
662
663    /// Every `display` write recorded in the change log, as `(node index, display)`.
664    fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
665        let mut out = Vec::new();
666        for change in changes {
667            if let CallbackChange::ChangeNodeCssProperties {
668                node_id, properties, ..
669            } = change
670            {
671                for p in properties.as_ref() {
672                    if let CssProperty::Display(v) = p {
673                        if let Some(d) = v.get_property() {
674                            out.push((node_id.index(), *d));
675                        }
676                    }
677                }
678            }
679        }
680        out
681    }
682
683    // ------------------------------------------------------------------
684    // AlertKind::colors  (getter)
685    // ------------------------------------------------------------------
686
687    #[test]
688    fn kind_colors_are_the_documented_bootstrap_palette() {
689        let expect = |(r, g, b): (u8, u8, u8)| ColorU { r, g, b, a: 255 };
690
691        assert_eq!(
692            AlertKind::Info.colors(),
693            (
694                expect((207, 244, 252)), // #cff4fc
695                expect((182, 239, 251)), // #b6effb
696                expect((5, 81, 96)),     // #055160
697            )
698        );
699        assert_eq!(
700            AlertKind::Success.colors(),
701            (
702                expect((209, 231, 221)), // #d1e7dd
703                expect((186, 219, 204)), // #badbcc
704                expect((15, 81, 50)),    // #0f5132
705            )
706        );
707        assert_eq!(
708            AlertKind::Warning.colors(),
709            (
710                expect((255, 243, 205)), // #fff3cd
711                expect((255, 236, 181)), // #ffecb5
712                expect((102, 77, 3)),    // #664d03
713            )
714        );
715        assert_eq!(
716            AlertKind::Danger.colors(),
717            (
718                expect((248, 215, 218)), // #f8d7da
719                expect((245, 194, 199)), // #f5c2c7
720                expect((132, 32, 41)),   // #842029
721            )
722        );
723    }
724
725    #[test]
726    fn kind_colors_are_fully_opaque_and_pairwise_distinct() {
727        for kind in ALL_KINDS {
728            let (bg, border, text) = kind.colors();
729            for (name, c) in [("bg", bg), ("border", border), ("text", text)] {
730                assert_eq!(c.a, 255, "{kind:?}.{name} must be fully opaque");
731            }
732            // a coloured banner is only legible if bg != text
733            assert_ne!(bg, text, "{kind:?}: background must differ from text");
734        }
735
736        for (i, a) in ALL_KINDS.iter().enumerate() {
737            for b in &ALL_KINDS[i + 1..] {
738                assert_ne!(
739                    a.colors(),
740                    b.colors(),
741                    "{a:?} and {b:?} must be visually distinguishable"
742                );
743            }
744        }
745    }
746
747    #[test]
748    fn kind_colors_default_is_info_and_call_is_pure() {
749        assert_eq!(AlertKind::default(), AlertKind::Info);
750        assert_eq!(AlertKind::default().colors(), AlertKind::Info.colors());
751
752        // repeated calls on the same (Copy) receiver must be stable
753        let k = AlertKind::Danger;
754        assert_eq!(k.colors(), k.colors());
755        assert_eq!(k.colors(), k.colors());
756    }
757
758    #[test]
759    fn kind_colors_is_const_evaluable() {
760        const INFO: (ColorU, ColorU, ColorU) = AlertKind::Info.colors();
761        assert_eq!(INFO.0, ColorU { r: 207, g: 244, b: 252, a: 255 });
762    }
763
764    // ------------------------------------------------------------------
765    // AlertKind::class_name  (getter)
766    // ------------------------------------------------------------------
767
768    #[test]
769    fn class_name_exact_values_and_shape() {
770        assert_eq!(AlertKind::Info.class_name(), "__azul-alert-info");
771        assert_eq!(AlertKind::Success.class_name(), "__azul-alert-success");
772        assert_eq!(AlertKind::Warning.class_name(), "__azul-alert-warning");
773        assert_eq!(AlertKind::Danger.class_name(), "__azul-alert-danger");
774
775        for kind in ALL_KINDS {
776            let name = kind.class_name();
777            assert!(
778                name.starts_with("__azul-alert-"),
779                "{kind:?} -> {name:?} must keep the widget prefix"
780            );
781            assert!(
782                !name.contains(char::is_whitespace),
783                "{name:?} must be a single CSS class token"
784            );
785            assert!(name.is_ascii(), "{name:?} must stay ASCII");
786            // stable across calls, and equal for equal kinds
787            assert_eq!(name, kind.class_name());
788        }
789    }
790
791    #[test]
792    fn class_name_is_unique_per_kind() {
793        let mut names: Vec<&str> = ALL_KINDS.iter().map(|k| k.class_name()).collect();
794        names.sort_unstable();
795        names.dedup();
796        assert_eq!(names.len(), 4, "every kind needs its own class name");
797    }
798
799    #[test]
800    fn class_name_is_const_evaluable() {
801        const DANGER: &str = AlertKind::Danger.class_name();
802        assert_eq!(DANGER, "__azul-alert-danger");
803    }
804
805    // ------------------------------------------------------------------
806    // build_alert_style
807    // ------------------------------------------------------------------
808
809    #[test]
810    fn build_alert_style_declares_the_same_properties_for_every_kind() {
811        let info = property_types(&build_alert_style(AlertKind::Info));
812        assert!(!info.is_empty(), "the container style must not be empty");
813
814        for kind in ALL_KINDS {
815            let style = build_alert_style(kind);
816            assert_eq!(
817                property_types(&style),
818                info,
819                "{kind:?} must declare the same properties, in the same order, as Info"
820            );
821            // the style is unconditional: nothing is gated behind :hover/@media/...
822            for p in style.as_ref() {
823                assert!(
824                    p.apply_if.as_ref().is_empty(),
825                    "{kind:?}: {:?} must be unconditional",
826                    p.property
827                );
828            }
829        }
830    }
831
832    #[test]
833    fn build_alert_style_declares_no_property_twice() {
834        // a duplicated property would silently shadow the earlier declaration
835        for kind in ALL_KINDS {
836            let types = property_types(&build_alert_style(kind));
837            for (i, a) in types.iter().enumerate() {
838                for b in &types[i + 1..] {
839                    assert_ne!(
840                        a, b,
841                        "{kind:?}: the container style declares the same property twice"
842                    );
843                }
844            }
845        }
846    }
847
848    #[test]
849    fn build_alert_style_colors_track_the_kind_palette() {
850        for kind in ALL_KINDS {
851            let style = build_alert_style(kind);
852            let (bg, border, text) = kind.colors();
853
854            assert_eq!(background_color(&style), Some(bg), "{kind:?}: background");
855            assert_eq!(text_color(&style), Some(text), "{kind:?}: text colour");
856
857            let borders = border_colors(&style);
858            assert_eq!(borders.len(), 4, "{kind:?}: all four edges must be coloured");
859            assert!(
860                borders.iter().all(|c| *c == border),
861                "{kind:?}: every edge must use the kind's border colour, got {borders:?}"
862            );
863        }
864    }
865
866    #[test]
867    fn build_alert_style_geometry_is_kind_independent() {
868        // Everything that is *not* a colour must be identical for all kinds.
869        let expected = [
870            CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
871            CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
872                LayoutFlexDirection::Row,
873            )),
874            CssPropertyWithConditions::simple(CssProperty::const_align_items(
875                LayoutAlignItems::Start,
876            )),
877            CssPropertyWithConditions::simple(CssProperty::const_padding_top(
878                LayoutPaddingTop::const_px(12),
879            )),
880            CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
881                LayoutBorderTopWidth::const_px(1),
882            )),
883            CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
884                StyleBorderTopLeftRadius::const_px(6),
885            )),
886            CssPropertyWithConditions::simple(CssProperty::const_font_size(
887                StyleFontSize::const_px(14),
888            )),
889        ];
890
891        for kind in ALL_KINDS {
892            let style = build_alert_style(kind);
893            for want in &expected {
894                assert!(
895                    style.as_ref().contains(want),
896                    "{kind:?}: missing {:?}",
897                    want.property
898                );
899            }
900        }
901    }
902
903    #[test]
904    fn build_alert_style_differs_only_in_the_colours() {
905        let info = build_alert_style(AlertKind::Info);
906        for kind in [AlertKind::Success, AlertKind::Warning, AlertKind::Danger] {
907            let other = build_alert_style(kind);
908            let differing: Vec<_> = info
909                .as_ref()
910                .iter()
911                .zip(other.as_ref().iter())
912                .filter(|(a, b)| a != b)
913                .map(|(a, _)| core::mem::discriminant(&a.property))
914                .collect();
915
916            // background + 4 border colours + text colour = 6 kind-dependent props
917            assert_eq!(
918                differing.len(),
919                6,
920                "{kind:?}: only bg + 4 border colours + text colour may depend on the kind"
921            );
922        }
923    }
924
925    // ------------------------------------------------------------------
926    // Alert::create / with_kind / Default
927    // ------------------------------------------------------------------
928
929    #[test]
930    fn create_is_an_info_alert_with_no_close_button() {
931        let alert = Alert::create(AzString::from("hello"));
932
933        assert_eq!(alert.message.as_str(), "hello");
934        assert_eq!(alert.kind, AlertKind::Info);
935        assert!(!alert.dismissible, "a fresh alert has no close button");
936        assert!(alert.alert_state.inner.visible, "a fresh alert is visible");
937        assert!(alert.alert_state.on_dismiss.is_none());
938        assert_eq!(alert.container_style, build_alert_style(AlertKind::Info));
939    }
940
941    #[test]
942    fn create_with_empty_message_equals_default_and_is_value_comparable() {
943        assert_eq!(Alert::create(AzString::from("")), Alert::default());
944        // equality is structural, not pointer-based
945        assert_eq!(Alert::create(AzString::from("a")), Alert::create(AzString::from("a")));
946        assert_ne!(Alert::create(AzString::from("a")), Alert::create(AzString::from("b")));
947        assert_ne!(
948            Alert::create(AzString::from("a")),
949            Alert::with_kind(AzString::from("a"), AlertKind::Danger)
950        );
951    }
952
953    #[test]
954    fn create_survives_extreme_messages_and_round_trips_them_into_the_dom() {
955        let long = "ab".repeat(50_000);
956        let cases: Vec<AzString> = alloc::vec![
957            AzString::from(""),
958            AzString::from(" "),
959            AzString::from("a\0b"),                                  // interior NUL
960            AzString::from("line\nbreak\ttab"),                      // control chars
961            AzString::from("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ e\u{0301}\u{0327} ู…ุฑุญุจุง ืฉืœื•ื ๐Ÿ‡ฉ๐Ÿ‡ช"), // ZWJ + combining + RTL
962            AzString::from("\u{feff}\u{202e}rtl-override"),          // BOM + bidi override
963            AzString::from("ร—"),                                     // same glyph as the close button
964            AzString::from(long.as_str()),                           // 100k chars
965        ];
966
967        for message in cases {
968            let alert = Alert::create(message.clone());
969            assert_eq!(alert.message.as_str(), message.as_str());
970
971            // the message must survive the trip through the DOM byte-for-byte
972            let dom = alert.dom();
973            let msg_node = &dom.children.as_ref()[0];
974            assert_eq!(text_of(msg_node), Some(message.as_str()));
975        }
976    }
977
978    #[test]
979    fn with_kind_stores_both_args_for_every_kind() {
980        for kind in ALL_KINDS {
981            let alert = Alert::with_kind(AzString::from("m"), kind);
982
983            assert_eq!(alert.kind, kind);
984            assert_eq!(alert.message.as_str(), "m");
985            assert!(!alert.dismissible);
986            assert!(alert.alert_state.on_dismiss.is_none());
987            assert!(alert.alert_state.inner.visible);
988            assert_eq!(
989                alert.container_style,
990                build_alert_style(kind),
991                "{kind:?}: the container style must match the kind it was built with"
992            );
993        }
994    }
995
996    // ------------------------------------------------------------------
997    // set_kind / with_alert_kind
998    // ------------------------------------------------------------------
999
1000    #[test]
1001    fn set_kind_recomputes_the_style_and_is_idempotent() {
1002        let mut alert = Alert::create(AzString::from("m"));
1003
1004        for kind in ALL_KINDS {
1005            alert.set_kind(kind);
1006            assert_eq!(alert.kind, kind);
1007            assert_eq!(alert.container_style, build_alert_style(kind));
1008
1009            // applying the same kind twice must not append/duplicate anything
1010            let before = alert.container_style.clone();
1011            alert.set_kind(kind);
1012            assert_eq!(alert.container_style, before, "{kind:?}: set_kind must be idempotent");
1013        }
1014
1015        // a full cycle back to the original kind restores the original alert
1016        let original = Alert::create(AzString::from("m"));
1017        let mut cycled = original.clone();
1018        for kind in ALL_KINDS {
1019            cycled.set_kind(kind);
1020        }
1021        cycled.set_kind(AlertKind::Info);
1022        assert_eq!(cycled, original, "kind cycling must not accumulate state");
1023    }
1024
1025    #[test]
1026    fn set_kind_leaves_message_dismissible_and_callback_alone() {
1027        let log = RefAny::new(DismissLog { calls: Vec::new() });
1028        let mut alert = Alert::create(AzString::from("keep me"));
1029        alert.set_on_dismiss(log, dismiss_cb(dismiss_do_nothing));
1030        alert.alert_state.inner.visible = false;
1031
1032        alert.set_kind(AlertKind::Warning);
1033
1034        assert_eq!(alert.message.as_str(), "keep me");
1035        assert!(alert.dismissible, "set_kind must not clear the close button");
1036        assert!(alert.alert_state.on_dismiss.is_some(), "set_kind must not drop the callback");
1037        assert!(!alert.alert_state.inner.visible, "set_kind must not resurrect a dismissed alert");
1038    }
1039
1040    #[test]
1041    fn with_alert_kind_matches_set_kind_and_last_write_wins() {
1042        for kind in ALL_KINDS {
1043            let built = Alert::create(AzString::from("m")).with_alert_kind(kind);
1044            let mut mutated = Alert::create(AzString::from("m"));
1045            mutated.set_kind(kind);
1046            assert_eq!(built, mutated, "{kind:?}: builder and setter must agree");
1047        }
1048
1049        let alert = Alert::create(AzString::from("m"))
1050            .with_alert_kind(AlertKind::Danger)
1051            .with_alert_kind(AlertKind::Success);
1052        assert_eq!(alert.kind, AlertKind::Success);
1053        assert_eq!(alert.container_style, build_alert_style(AlertKind::Success));
1054    }
1055
1056    // ------------------------------------------------------------------
1057    // set_dismissible / with_dismissible
1058    // ------------------------------------------------------------------
1059
1060    #[test]
1061    fn set_dismissible_last_write_wins_and_touches_nothing_else() {
1062        let mut alert = Alert::with_kind(AzString::from("m"), AlertKind::Warning);
1063        let style_before = alert.container_style.clone();
1064
1065        for flag in [true, true, false, true, false, false] {
1066            alert.set_dismissible(flag);
1067            assert_eq!(alert.dismissible, flag);
1068        }
1069
1070        assert_eq!(alert.kind, AlertKind::Warning);
1071        assert_eq!(alert.message.as_str(), "m");
1072        assert_eq!(alert.container_style, style_before, "toggling must not restyle");
1073        assert!(alert.alert_state.on_dismiss.is_none(), "toggling must not invent a callback");
1074    }
1075
1076    #[test]
1077    fn with_dismissible_toggle_sequence_ends_on_the_last_value() {
1078        assert!(Alert::default().with_dismissible(true).dismissible);
1079        assert!(!Alert::default().with_dismissible(false).dismissible);
1080        assert!(
1081            !Alert::default()
1082                .with_dismissible(true)
1083                .with_dismissible(false)
1084                .dismissible
1085        );
1086        assert!(
1087            Alert::default()
1088                .with_dismissible(false)
1089                .with_dismissible(true)
1090                .dismissible
1091        );
1092        // builder == setter
1093        let mut mutated = Alert::default();
1094        mutated.set_dismissible(true);
1095        assert_eq!(Alert::default().with_dismissible(true), mutated);
1096    }
1097
1098    // ------------------------------------------------------------------
1099    // set_on_dismiss / with_on_dismiss
1100    // ------------------------------------------------------------------
1101
1102    #[test]
1103    fn set_on_dismiss_implies_dismissible() {
1104        let mut alert = Alert::create(AzString::from("m"));
1105        assert!(!alert.dismissible);
1106
1107        alert.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
1108
1109        assert!(alert.dismissible, "a dismiss callback must render a close button");
1110        assert!(alert.alert_state.on_dismiss.is_some());
1111        assert!(alert.alert_state.inner.visible, "wiring a callback must not hide the alert");
1112    }
1113
1114    #[test]
1115    fn set_on_dismiss_replaces_rather_than_appends() {
1116        let mut alert = Alert::create(AzString::from("m"));
1117
1118        alert.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
1119        let first = alert
1120            .alert_state
1121            .on_dismiss
1122            .as_ref()
1123            .expect("first callback")
1124            .refany
1125            .get_type_id();
1126        assert_eq!(first, RefAny::new(1u8).get_type_id());
1127
1128        // a second call must *replace* the payload + function, not stack another one
1129        alert.set_on_dismiss(RefAny::new(9i64), dismiss_cb(record_dismiss));
1130        let second = alert.alert_state.on_dismiss.as_ref().expect("second callback");
1131        assert_eq!(second.refany.get_type_id(), RefAny::new(9i64).get_type_id());
1132        assert_eq!(second.callback, dismiss_cb(record_dismiss));
1133        assert_ne!(second.callback, dismiss_cb(dismiss_do_nothing));
1134    }
1135
1136    #[test]
1137    fn with_on_dismiss_keeps_message_and_kind() {
1138        let alert = Alert::with_kind(AzString::from("boom"), AlertKind::Danger)
1139            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(dismiss_do_nothing));
1140
1141        assert_eq!(alert.message.as_str(), "boom");
1142        assert_eq!(alert.kind, AlertKind::Danger);
1143        assert_eq!(alert.container_style, build_alert_style(AlertKind::Danger));
1144        assert!(alert.dismissible);
1145        assert!(alert.alert_state.on_dismiss.is_some());
1146    }
1147
1148    #[test]
1149    fn set_dismissible_false_after_set_on_dismiss_silently_drops_the_close_button() {
1150        // Footgun, pinned as the *current* behaviour: `set_on_dismiss` implies
1151        // `dismissible = true`, but a later `set_dismissible(false)` wins and the
1152        // wired-up callback becomes unreachable (no close button is rendered).
1153        let mut alert = Alert::create(AzString::from("m"));
1154        alert.set_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
1155        alert.set_dismissible(false);
1156
1157        assert!(alert.alert_state.on_dismiss.is_some(), "the callback is still stored");
1158        let dom = alert.dom();
1159        assert_eq!(
1160            dom.children.as_ref().len(),
1161            1,
1162            "no close button is rendered, so the callback can never fire"
1163        );
1164    }
1165
1166    // ------------------------------------------------------------------
1167    // swap_with_default
1168    // ------------------------------------------------------------------
1169
1170    #[test]
1171    fn swap_with_default_returns_the_original_and_resets_self() {
1172        let mut alert = Alert::with_kind(AzString::from("payload"), AlertKind::Danger)
1173            .with_dismissible(true);
1174        let snapshot = alert.clone();
1175
1176        let returned = alert.swap_with_default();
1177
1178        assert_eq!(returned, snapshot, "the original must come back untouched");
1179        assert_eq!(alert, Alert::default(), "self must be reset to a default alert");
1180        assert_eq!(alert.message.as_str(), "");
1181        assert_eq!(alert.kind, AlertKind::Info);
1182        assert!(!alert.dismissible);
1183        assert!(alert.alert_state.on_dismiss.is_none());
1184        assert!(alert.alert_state.inner.visible);
1185    }
1186
1187    #[test]
1188    fn swap_with_default_is_stable_when_repeated() {
1189        let mut alert = Alert::default();
1190        for _ in 0..3 {
1191            let returned = alert.swap_with_default();
1192            assert_eq!(returned, Alert::default());
1193            assert_eq!(alert, Alert::default());
1194        }
1195    }
1196
1197    #[test]
1198    fn swap_with_default_moves_the_callback_out_of_self() {
1199        let mut alert = Alert::create(AzString::from("m"))
1200            .with_on_dismiss(RefAny::new(7u32), dismiss_cb(record_dismiss));
1201
1202        let returned = alert.swap_with_default();
1203
1204        assert!(returned.alert_state.on_dismiss.is_some(), "the callback moves out");
1205        assert!(
1206            alert.alert_state.on_dismiss.is_none(),
1207            "the reset alert must not keep a reference to the old callback"
1208        );
1209        assert!(!alert.dismissible);
1210    }
1211
1212    // ------------------------------------------------------------------
1213    // Alert::dom
1214    // ------------------------------------------------------------------
1215
1216    #[test]
1217    fn dom_of_a_plain_alert_is_a_container_with_one_message_child() {
1218        let alert = Alert::create(AzString::from("hi"));
1219        let style = alert.container_style.clone();
1220        let dom = alert.dom();
1221
1222        assert!(dom.root.has_class("__azul-native-alert"));
1223        assert!(
1224            dom.root.get_callbacks().as_ref().is_empty(),
1225            "a non-dismissible alert must carry no live callback"
1226        );
1227        assert_eq!(
1228            dom.root.style.iter_inline_properties().count(),
1229            style.len(),
1230            "every container property must reach the node's inline style"
1231        );
1232
1233        let children = dom.children.as_ref();
1234        assert_eq!(children.len(), 1, "no close button without `dismissible`");
1235        assert!(children[0].root.has_class("__azul-native-alert-message"));
1236        assert_eq!(text_of(&children[0]), Some("hi"));
1237        assert!(children[0].root.get_callbacks().as_ref().is_empty());
1238        assert!(children[0].root.get_tab_index().is_none());
1239    }
1240
1241    #[test]
1242    fn dom_of_a_dismissible_alert_appends_a_focusable_close_button() {
1243        let dom = Alert::create(AzString::from("hi")).with_dismissible(true).dom();
1244
1245        let children = dom.children.as_ref();
1246        assert_eq!(children.len(), 2, "[message, close]");
1247
1248        let close = &children[1];
1249        assert!(close.root.has_class("__azul-native-alert-close"));
1250        assert_eq!(text_of(close), Some("\u{00D7}"), "the close glyph is U+00D7 MULTIPLICATION SIGN");
1251        assert!(
1252            matches!(close.root.get_tab_index(), Some(TabIndex::Auto)),
1253            "the close button must be keyboard-reachable"
1254        );
1255
1256        let callbacks = close.root.get_callbacks();
1257        assert_eq!(callbacks.as_ref().len(), 1, "exactly one dismiss handler");
1258        let cb = &callbacks.as_ref()[0];
1259        assert!(matches!(
1260            &cb.event,
1261            EventFilter::Hover(HoverEventFilter::MouseUp)
1262        ));
1263        assert_eq!(cb.callback.cb, default_on_alert_dismiss as usize);
1264        assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
1265    }
1266
1267    #[test]
1268    fn dom_hands_the_alert_state_to_the_close_button() {
1269        let alert = Alert::create(AzString::from("hi"))
1270            .with_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
1271        let dom = alert.dom();
1272
1273        let close = &dom.children.as_ref()[1];
1274        let mut payload = close.root.get_callbacks().as_ref()[0].refany.clone();
1275
1276        assert!(
1277            wrapper_visible(&mut payload),
1278            "the close button must receive a live, visible AlertStateWrapper"
1279        );
1280        assert!(
1281            payload
1282                .downcast_ref::<AlertStateWrapper>()
1283                .expect("AlertStateWrapper")
1284                .on_dismiss
1285                .is_some(),
1286            "the user callback must travel with the state"
1287        );
1288    }
1289
1290    #[test]
1291    fn dom_is_stable_across_kinds_and_only_the_container_style_changes() {
1292        for kind in ALL_KINDS {
1293            let dom = Alert::with_kind(AzString::from("m"), kind)
1294                .with_dismissible(true)
1295                .dom();
1296            assert!(dom.root.has_class("__azul-native-alert"));
1297            assert_eq!(dom.children.as_ref().len(), 2);
1298
1299            // NOTE: `AlertKind::class_name()` is *not* applied to the DOM - the
1300            // container only ever carries the generic container class.
1301            assert!(
1302                !dom.root.has_class(kind.class_name()),
1303                "current behaviour: the kind class is not emitted"
1304            );
1305        }
1306    }
1307
1308    // ------------------------------------------------------------------
1309    // default_on_alert_dismiss
1310    // ------------------------------------------------------------------
1311
1312    #[test]
1313    fn dismiss_hides_the_container_and_flips_visible() {
1314        let mut data = RefAny::new(AlertStateWrapper::default());
1315
1316        // node 2 == the close button, its parent (node 0) is the container
1317        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1318
1319        assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
1320        assert_eq!(
1321            display_writes(&changes),
1322            alloc::vec![(0usize, LayoutDisplay::None)],
1323            "the *container* (not the close button) must be hidden"
1324        );
1325        assert!(!wrapper_visible(&mut data), "state must flip to hidden");
1326    }
1327
1328    #[test]
1329    fn dismiss_invokes_the_user_callback_with_the_already_flipped_state() {
1330        let mut log = RefAny::new(DismissLog { calls: Vec::new() });
1331        let mut data = RefAny::new(AlertStateWrapper {
1332            inner: AlertState { visible: true },
1333            on_dismiss: Some(AlertOnDismiss {
1334                callback: dismiss_cb(record_dismiss),
1335                refany: log.clone(),
1336            })
1337            .into(),
1338        });
1339
1340        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1341
1342        assert_eq!(update, Update::RefreshDom, "the user callback's Update is returned");
1343        assert_eq!(
1344            log_calls(&mut log),
1345            alloc::vec![false],
1346            "the callback must see `visible == false` (already dismissed)"
1347        );
1348        assert!(!wrapper_visible(&mut data));
1349        assert_eq!(
1350            display_writes(&changes),
1351            alloc::vec![(0usize, LayoutDisplay::None)],
1352            "the container is hidden even after a user callback ran"
1353        );
1354    }
1355
1356    #[test]
1357    fn dismiss_twice_is_idempotent() {
1358        let mut log = RefAny::new(DismissLog { calls: Vec::new() });
1359        let mut data = RefAny::new(AlertStateWrapper {
1360            inner: AlertState { visible: true },
1361            on_dismiss: Some(AlertOnDismiss {
1362                callback: dismiss_cb(record_dismiss),
1363                refany: log.clone(),
1364            })
1365            .into(),
1366        });
1367
1368        for _ in 0..2 {
1369            let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1370            assert_eq!(update, Update::RefreshDom);
1371            assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
1372        }
1373
1374        assert!(!wrapper_visible(&mut data), "a second dismiss must not un-hide");
1375        assert_eq!(
1376            log_calls(&mut log),
1377            alloc::vec![false, false],
1378            "each click fires the callback exactly once, always with visible == false"
1379        );
1380    }
1381
1382    #[test]
1383    fn dismiss_on_a_root_hit_node_is_a_noop() {
1384        // node 0 has no parent -> there is no container to hide
1385        let mut data = RefAny::new(AlertStateWrapper::default());
1386
1387        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 0, data.clone());
1388
1389        assert_eq!(update, Update::DoNothing);
1390        assert!(changes.is_empty(), "nothing may be restyled without a parent");
1391        assert!(wrapper_visible(&mut data), "state must not flip");
1392    }
1393
1394    #[test]
1395    fn dismiss_with_a_stale_hit_node_is_a_noop() {
1396        // node 999 does not exist in the 3-node fixture
1397        let mut data = RefAny::new(AlertStateWrapper::default());
1398
1399        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 999, data.clone());
1400
1401        assert_eq!(update, Update::DoNothing);
1402        assert!(changes.is_empty());
1403        assert!(wrapper_visible(&mut data));
1404    }
1405
1406    #[test]
1407    fn dismiss_without_any_layout_result_is_a_noop() {
1408        let mut data = RefAny::new(AlertStateWrapper::default());
1409
1410        let (update, changes) = run_dismiss(None, 2, data.clone());
1411
1412        assert_eq!(update, Update::DoNothing);
1413        assert!(changes.is_empty());
1414        assert!(wrapper_visible(&mut data), "state must not flip");
1415    }
1416
1417    #[test]
1418    fn dismiss_with_a_foreign_payload_is_a_noop() {
1419        // the callback-bearing node carries a RefAny of the *wrong* type
1420        let data = RefAny::new(0xdead_beef_u64);
1421
1422        let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1423
1424        assert_eq!(update, Update::DoNothing);
1425        assert!(
1426            changes.is_empty(),
1427            "a foreign payload must not hide the container"
1428        );
1429    }
1430
1431    #[test]
1432    fn dismiss_end_to_end_through_the_real_dom_payload() {
1433        // Take the *actual* RefAny the widget wired into its close button and
1434        // drive the *actual* handler the widget registered against it.
1435        let alert = Alert::create(AzString::from("bye")).with_dismissible(true);
1436        let dom = alert.dom();
1437        let close = &dom.children.as_ref()[1];
1438        let entry = &close.root.get_callbacks().as_ref()[0];
1439        assert_eq!(entry.callback.cb, default_on_alert_dismiss as usize);
1440        let mut payload = entry.refany.clone();
1441
1442        let styled = StyledDom::create_from_dom(dom);
1443        let (update, changes) = run_dismiss(Some(styled), 2, payload.clone());
1444
1445        assert_eq!(update, Update::DoNothing);
1446        assert_eq!(
1447            display_writes(&changes),
1448            alloc::vec![(0usize, LayoutDisplay::None)]
1449        );
1450        assert!(
1451            !wrapper_visible(&mut payload),
1452            "the state living in the DOM must be flipped to hidden"
1453        );
1454    }
1455}