1use 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
48pub 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
72#[repr(C)]
73pub enum AlertKind {
74 #[default]
76 Info,
77 Success,
79 Warning,
81 Danger,
83}
84
85impl AlertKind {
86 #[allow(clippy::trivially_copy_pass_by_ref)] const fn colors(&self) -> (ColorU, ColorU, ColorU) {
89 match self {
90 Self::Info => (
91 ColorU { r: 207, g: 244, b: 252, a: 255 }, ColorU { r: 182, g: 239, b: 251, a: 255 }, ColorU { r: 5, g: 81, b: 96, a: 255 }, ),
95 Self::Success => (
96 ColorU { r: 209, g: 231, b: 221, a: 255 }, ColorU { r: 186, g: 219, b: 204, a: 255 }, ColorU { r: 15, g: 81, b: 50, a: 255 }, ),
100 Self::Warning => (
101 ColorU { r: 255, g: 243, b: 205, a: 255 }, ColorU { r: 255, g: 236, b: 181, a: 255 }, ColorU { r: 102, g: 77, b: 3, a: 255 }, ),
105 Self::Danger => (
106 ColorU { r: 248, g: 215, b: 218, a: 255 }, ColorU { r: 245, g: 194, b: 199, a: 255 }, ColorU { r: 132, g: 32, b: 41, a: 255 }, ),
110 }
111 }
112
113 #[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#[derive(Debug, Clone, PartialEq, Eq)]
126#[repr(C)]
127pub struct Alert {
128 pub alert_state: AlertStateWrapper,
130 pub message: AzString,
132 pub kind: AlertKind,
134 pub dismissible: bool,
136 pub container_style: CssPropertyWithConditionsVec,
138}
139
140#[derive(Debug, Default, Clone, PartialEq, Eq)]
141#[repr(C)]
142pub struct AlertStateWrapper {
143 pub inner: AlertState,
145 pub on_dismiss: OptionAlertOnDismiss,
147}
148
149#[derive(Debug, Copy, Clone, PartialEq, Eq)]
151#[repr(C)]
152pub struct AlertState {
153 pub visible: bool,
155}
156
157impl Default for AlertState {
158 fn default() -> Self {
159 Self { visible: true }
160 }
161}
162
163fn 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 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Stretch)),
178 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
179 0,
180 ))),
181 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 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 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 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
254 inner: text,
255 })),
256 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
257 ])
258}
259
260static 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
266static 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 #[inline]
280 #[must_use] pub fn create(message: AzString) -> Self {
281 Self::with_kind(message, AlertKind::Info)
282 }
283
284 #[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 #[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 #[inline]
305 #[must_use] pub fn with_alert_kind(mut self, kind: AlertKind) -> Self {
306 self.set_kind(kind);
307 self
308 }
309
310 #[inline]
312 pub const fn set_dismissible(&mut self, dismissible: bool) {
313 self.dismissible = dismissible;
314 }
315
316 #[inline]
318 #[must_use] pub const fn with_dismissible(mut self, dismissible: bool) -> Self {
319 self.set_dismissible(dismissible);
320 self
321 }
322
323 #[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 #[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 #[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 #[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
401extern "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 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 const ALL_KINDS: [AlertKind; 4] = [
474 AlertKind::Info,
475 AlertKind::Success,
476 AlertKind::Warning,
477 AlertKind::Danger,
478 ];
479
480 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 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 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 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 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 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 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 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 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 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 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: ¤t_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 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 #[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)), expect((182, 239, 251)), expect((5, 81, 96)), )
698 );
699 assert_eq!(
700 AlertKind::Success.colors(),
701 (
702 expect((209, 231, 221)), expect((186, 219, 204)), expect((15, 81, 50)), )
706 );
707 assert_eq!(
708 AlertKind::Warning.colors(),
709 (
710 expect((255, 243, 205)), expect((255, 236, 181)), expect((102, 77, 3)), )
714 );
715 assert_eq!(
716 AlertKind::Danger.colors(),
717 (
718 expect((248, 215, 218)), expect((245, 194, 199)), expect((132, 32, 41)), )
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 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 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 #[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 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 #[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 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 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 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 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 #[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 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"), AzString::from("line\nbreak\ttab"), AzString::from("๐จโ๐ฉโ๐งโ๐ฆ e\u{0301}\u{0327} ู
ุฑุญุจุง ืฉืืื ๐ฉ๐ช"), AzString::from("\u{feff}\u{202e}rtl-override"), AzString::from("ร"), AzString::from(long.as_str()), ];
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 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 #[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 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 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 #[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 let mut mutated = Alert::default();
1094 mutated.set_dismissible(true);
1095 assert_eq!(Alert::default().with_dismissible(true), mutated);
1096 }
1097
1098 #[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 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 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 #[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 #[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 assert!(
1302 !dom.root.has_class(kind.class_name()),
1303 "current behaviour: the kind class is not emitted"
1304 );
1305 }
1306 }
1307
1308 #[test]
1313 fn dismiss_hides_the_container_and_flips_visible() {
1314 let mut data = RefAny::new(AlertStateWrapper::default());
1315
1316 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 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 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 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 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}