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}