1use azul_core::{
34 callbacks::{CoreCallbackData, Update},
35 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
36 refany::RefAny,
37};
38use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
39use azul_css::{
40 props::{
41 basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
42 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutFlexGrow, LayoutPosition, LayoutInsetBottom, LayoutRight, LayoutMaxWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutMarginLeft},
43 property::{CssProperty, *},
44 style::{StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleCursor, StyleUserSelect},
45 },
46 impl_option_inner, AzString,
47};
48
49use crate::callbacks::{Callback, CallbackInfo};
50
51static TOAST_CONTAINER_CLASS: &[IdOrClass] =
52 &[Class(AzString::from_const_str("__azul-native-toast"))];
53static TOAST_MESSAGE_CLASS: &[IdOrClass] =
54 &[Class(AzString::from_const_str("__azul-native-toast-message"))];
55static TOAST_CLOSE_CLASS: &[IdOrClass] =
56 &[Class(AzString::from_const_str("__azul-native-toast-close"))];
57
58const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
59const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
60const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
61 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
62
63const TOAST_INSET: isize = 24;
65const TOAST_MAX_WIDTH: isize = 360;
67
68pub type ToastOnDismissCallbackType = extern "C" fn(RefAny, CallbackInfo, ToastState) -> Update;
70impl_widget_callback!(
71 ToastOnDismiss,
72 OptionToastOnDismiss,
73 ToastOnDismissCallback,
74 ToastOnDismissCallbackType
75);
76
77azul_core::impl_managed_callback! {
78 wrapper: ToastOnDismissCallback,
79 info_ty: CallbackInfo,
80 return_ty: Update,
81 default_ret: Update::DoNothing,
82 invoker_static: TOAST_ON_DISMISS_INVOKER,
83 invoker_ty: AzToastOnDismissCallbackInvoker,
84 thunk_fn: az_toast_on_dismiss_callback_thunk,
85 setter_fn: AzApp_setToastOnDismissCallbackInvoker,
86 from_handle_fn: AzToastOnDismissCallback_createFromHostHandle,
87 extra_args: [ state: ToastState ],
88}
89
90#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
93#[repr(C)]
94pub enum ToastKind {
95 #[default]
97 Info,
98 Success,
100 Warning,
102 Danger,
104}
105
106impl ToastKind {
107 #[allow(clippy::trivially_copy_pass_by_ref)] const fn colors(&self) -> (ColorU, ColorU, ColorU) {
110 match self {
111 Self::Info => (
112 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 }, ),
116 Self::Success => (
117 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 }, ),
121 Self::Warning => (
122 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 }, ),
126 Self::Danger => (
127 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 }, ),
131 }
132 }
133
134 #[must_use] pub const fn class_name(&self) -> &'static str {
136 match self {
137 Self::Info => "__azul-toast-info",
138 Self::Success => "__azul-toast-success",
139 Self::Warning => "__azul-toast-warning",
140 Self::Danger => "__azul-toast-danger",
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147#[repr(C)]
148pub struct Toast {
149 pub toast_state: ToastStateWrapper,
151 pub message: AzString,
153 pub kind: ToastKind,
155 pub dismissible: bool,
158 pub container_style: CssPropertyWithConditionsVec,
160}
161
162#[derive(Debug, Default, Clone, PartialEq, Eq)]
163#[repr(C)]
164pub struct ToastStateWrapper {
165 pub inner: ToastState,
167 pub on_dismiss: OptionToastOnDismiss,
169}
170
171#[derive(Debug, Copy, Clone, PartialEq, Eq)]
173#[repr(C)]
174pub struct ToastState {
175 pub visible: bool,
177}
178
179impl Default for ToastState {
180 fn default() -> Self {
181 Self { visible: true }
182 }
183}
184
185fn build_toast_style(kind: ToastKind) -> CssPropertyWithConditionsVec {
190 let (bg, border, text) = kind.colors();
191 let bg_vec =
192 StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(bg)]);
193 CssPropertyWithConditionsVec::from_vec(alloc::vec![
194 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
195 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
196 LayoutFlexDirection::Row,
197 )),
198 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
199 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
200 0,
201 ))),
202 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
204 CssPropertyWithConditions::simple(CssProperty::const_bottom(LayoutInsetBottom::const_px(
205 TOAST_INSET,
206 ))),
207 CssPropertyWithConditions::simple(CssProperty::const_right(LayoutRight::const_px(
208 TOAST_INSET,
209 ))),
210 CssPropertyWithConditions::simple(CssProperty::const_max_width(LayoutMaxWidth::const_px(
212 TOAST_MAX_WIDTH,
213 ))),
214 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
216 12,
217 ))),
218 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
219 LayoutPaddingBottom::const_px(12),
220 )),
221 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
222 LayoutPaddingLeft::const_px(12),
223 )),
224 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
225 LayoutPaddingRight::const_px(12),
226 )),
227 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
229 LayoutBorderTopWidth::const_px(1),
230 )),
231 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
232 LayoutBorderBottomWidth::const_px(1),
233 )),
234 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
235 LayoutBorderLeftWidth::const_px(1),
236 )),
237 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
238 LayoutBorderRightWidth::const_px(1),
239 )),
240 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
241 inner: BorderStyle::Solid,
242 })),
243 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
244 StyleBorderBottomStyle {
245 inner: BorderStyle::Solid,
246 },
247 )),
248 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(
249 StyleBorderLeftStyle {
250 inner: BorderStyle::Solid,
251 },
252 )),
253 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
254 StyleBorderRightStyle {
255 inner: BorderStyle::Solid,
256 },
257 )),
258 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
259 inner: border,
260 })),
261 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
262 StyleBorderBottomColor { inner: border },
263 )),
264 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(
265 StyleBorderLeftColor { inner: border },
266 )),
267 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
268 StyleBorderRightColor { inner: border },
269 )),
270 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
272 StyleBorderTopLeftRadius::const_px(6),
273 )),
274 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
275 StyleBorderTopRightRadius::const_px(6),
276 )),
277 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
278 StyleBorderBottomLeftRadius::const_px(6),
279 )),
280 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
281 StyleBorderBottomRightRadius::const_px(6),
282 )),
283 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
284 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
285 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
287 inner: text,
288 })),
289 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
290 ])
291}
292
293static TOAST_MESSAGE_STYLE: &[CssPropertyWithConditions] = &[
295 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
296 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
297];
298
299static TOAST_CLOSE_STYLE: &[CssPropertyWithConditions] = &[
301 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
302 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
303 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
304 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
305 CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(
306 12,
307 ))),
308];
309
310impl Toast {
311 #[inline]
314 #[must_use] pub fn create(message: AzString) -> Self {
315 Self::with_kind(message, ToastKind::Info)
316 }
317
318 #[inline]
320 #[must_use] pub fn with_kind(message: AzString, kind: ToastKind) -> Self {
321 Self {
322 toast_state: ToastStateWrapper::default(),
323 message,
324 kind,
325 dismissible: true,
326 container_style: build_toast_style(kind),
327 }
328 }
329
330 #[inline]
332 pub fn set_kind(&mut self, kind: ToastKind) {
333 self.kind = kind;
334 self.container_style = build_toast_style(kind);
335 }
336
337 #[inline]
339 #[must_use] pub fn with_toast_kind(mut self, kind: ToastKind) -> Self {
340 self.set_kind(kind);
341 self
342 }
343
344 #[inline]
346 pub const fn set_dismissible(&mut self, dismissible: bool) {
347 self.dismissible = dismissible;
348 }
349
350 #[inline]
352 #[must_use] pub const fn with_dismissible(mut self, dismissible: bool) -> Self {
353 self.set_dismissible(dismissible);
354 self
355 }
356
357 #[inline]
360 pub fn set_on_dismiss<C: Into<ToastOnDismissCallback>>(&mut self, data: RefAny, on_dismiss: C) {
361 self.dismissible = true;
362 self.toast_state.on_dismiss = Some(ToastOnDismiss {
363 callback: on_dismiss.into(),
364 refany: data,
365 })
366 .into();
367 }
368
369 #[inline]
371 #[must_use] pub fn with_on_dismiss<C: Into<ToastOnDismissCallback>>(
372 mut self,
373 data: RefAny,
374 on_dismiss: C,
375 ) -> Self {
376 self.set_on_dismiss(data, on_dismiss);
377 self
378 }
379
380 #[inline]
382 #[must_use] pub fn swap_with_default(&mut self) -> Self {
383 let mut s = Self::create(AzString::from_const_str(""));
384 core::mem::swap(&mut s, self);
385 s
386 }
387
388 #[inline]
390 #[must_use] pub fn dom(self) -> Dom {
391 use azul_core::{
392 callbacks::CoreCallback,
393 dom::{EventFilter, HoverEventFilter},
394 refany::OptionRefAny,
395 };
396
397 let message = Dom::create_text(self.message)
398 .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_MESSAGE_CLASS))
399 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TOAST_MESSAGE_STYLE));
400
401 let mut children = alloc::vec![message];
402
403 if self.dismissible {
404 let close = Dom::create_text(AzString::from_const_str("\u{00D7}"))
405 .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_CLOSE_CLASS))
406 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TOAST_CLOSE_STYLE))
407 .with_tab_index(TabIndex::Auto)
408 .with_callbacks(
409 alloc::vec![CoreCallbackData {
410 event: EventFilter::Hover(HoverEventFilter::MouseUp),
411 callback: CoreCallback {
412 cb: default_on_toast_dismiss as usize,
413 ctx: OptionRefAny::None,
414 },
415 refany: RefAny::new(self.toast_state),
416 }]
417 .into(),
418 );
419 children.push(close);
420 }
421
422 Dom::create_div()
423 .with_ids_and_classes(IdOrClassVec::from_const_slice(TOAST_CONTAINER_CLASS))
424 .with_css_props(self.container_style)
425 .with_children(children.into())
426 }
427}
428
429impl Default for Toast {
430 fn default() -> Self {
431 Self::create(AzString::from_const_str(""))
432 }
433}
434
435extern "C" fn default_on_toast_dismiss(mut data: RefAny, mut info: CallbackInfo) -> Update {
440 let close_node = info.get_hit_node();
441 let Some(container) = info.get_parent(close_node) else {
442 return Update::DoNothing;
443 };
444
445 let result = {
446 let Some(mut toast) = data.downcast_mut::<ToastStateWrapper>() else {
447 return Update::DoNothing;
448 };
449 toast.inner.visible = false;
450 let inner = toast.inner;
451 let toast = &mut *toast;
452 match toast.on_dismiss.as_mut() {
453 Some(ToastOnDismiss { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
454 None => Update::DoNothing,
455 }
456 };
457
458 info.set_css_property(container, CssProperty::const_display(LayoutDisplay::None));
464
465 result
466}
467
468impl From<Toast> for Dom {
469 fn from(t: Toast) -> Self {
470 t.dom()
471 }
472}
473
474#[cfg(test)]
475#[allow(clippy::assertions_on_constants)]
480mod autotest_generated {
481 use std::{
482 collections::{BTreeMap, HashMap},
483 sync::{Arc, Mutex},
484 };
485
486 use azul_core::{
487 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
488 geom::{LogicalRect, OptionLogicalPosition},
489 gl::OptionGlContextPtr,
490 hit_test::ScrollPosition,
491 refany::OptionRefAny,
492 resources::RendererResources,
493 styled_dom::{NodeHierarchyItemId, StyledDom},
494 window::{MonitorVec, RawWindowHandle},
495 };
496 use azul_css::{
497 props::basic::{length::SizeMetric, pixel::PixelValue},
498 system::SystemStyle,
499 };
500 use rust_fontconfig::FcFontCache;
501
502 use super::*;
503 #[cfg(feature = "icu")]
504 use crate::icu::IcuLocalizerHandle;
505 use crate::{
506 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
507 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
508 window::{DomLayoutResult, LayoutWindow},
509 window_state::FullWindowState,
510 };
511
512 const ALL_KINDS: [ToastKind; 4] = [
517 ToastKind::Info,
518 ToastKind::Success,
519 ToastKind::Warning,
520 ToastKind::Danger,
521 ];
522
523 fn text_of(node: &Dom) -> Option<&str> {
525 match node.root.get_node_type() {
526 NodeType::Text(s) => Some(s.as_ref().as_str()),
527 _ => None,
528 }
529 }
530
531 fn inline_props(node: &Dom) -> Vec<CssProperty> {
533 node.root
534 .style
535 .iter_inline_properties()
536 .map(|(p, _)| p.clone())
537 .collect()
538 }
539
540 fn background_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
542 style.as_ref().iter().find_map(|p| match &p.property {
543 CssProperty::BackgroundContent(v) => match v.get_property()?.as_ref().first()? {
544 StyleBackgroundContent::Color(c) => Some(*c),
545 _ => None,
546 },
547 _ => None,
548 })
549 }
550
551 fn border_colors(style: &CssPropertyWithConditionsVec) -> Vec<ColorU> {
553 style
554 .as_ref()
555 .iter()
556 .filter_map(|p| match &p.property {
557 CssProperty::BorderTopColor(v) => v.get_property().map(|c| c.inner),
558 CssProperty::BorderBottomColor(v) => v.get_property().map(|c| c.inner),
559 CssProperty::BorderLeftColor(v) => v.get_property().map(|c| c.inner),
560 CssProperty::BorderRightColor(v) => v.get_property().map(|c| c.inner),
561 _ => None,
562 })
563 .collect()
564 }
565
566 fn text_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
568 style.as_ref().iter().find_map(|p| match &p.property {
569 CssProperty::TextColor(v) => v.get_property().map(|c| c.inner),
570 _ => None,
571 })
572 }
573
574 fn position_of(style: &CssPropertyWithConditionsVec) -> Option<LayoutPosition> {
576 style.as_ref().iter().find_map(|p| match &p.property {
577 CssProperty::Position(v) => v.get_property().copied(),
578 _ => None,
579 })
580 }
581
582 fn bottom_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
584 style.as_ref().iter().find_map(|p| match &p.property {
585 CssProperty::Bottom(v) => v.get_property().map(|b| b.inner),
586 _ => None,
587 })
588 }
589
590 fn right_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
592 style.as_ref().iter().find_map(|p| match &p.property {
593 CssProperty::Right(v) => v.get_property().map(|r| r.inner),
594 _ => None,
595 })
596 }
597
598 fn max_width_px(style: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
600 style.as_ref().iter().find_map(|p| match &p.property {
601 CssProperty::MaxWidth(v) => v.get_property().map(|w| w.inner),
602 _ => None,
603 })
604 }
605
606 fn property_types(
608 style: &CssPropertyWithConditionsVec,
609 ) -> Vec<core::mem::Discriminant<CssProperty>> {
610 style
611 .as_ref()
612 .iter()
613 .map(|p| core::mem::discriminant(&p.property))
614 .collect()
615 }
616
617 struct DismissLog {
619 calls: Vec<bool>,
620 }
621
622 extern "C" fn record_dismiss(mut data: RefAny, _: CallbackInfo, state: ToastState) -> Update {
623 if let Some(mut log) = data.downcast_mut::<DismissLog>() {
624 log.calls.push(state.visible);
625 }
626 Update::RefreshDom
627 }
628
629 extern "C" fn dismiss_do_nothing(_: RefAny, _: CallbackInfo, _: ToastState) -> Update {
630 Update::DoNothing
631 }
632
633 fn dismiss_cb(f: ToastOnDismissCallbackType) -> ToastOnDismissCallback {
634 f.into()
635 }
636
637 fn wrapper_visible(data: &mut RefAny) -> bool {
639 data.downcast_ref::<ToastStateWrapper>()
640 .expect("payload must still be a ToastStateWrapper")
641 .inner
642 .visible
643 }
644
645 fn log_calls(data: &mut RefAny) -> Vec<bool> {
647 data.downcast_ref::<DismissLog>()
648 .expect("payload must still be a DismissLog")
649 .calls
650 .clone()
651 }
652
653 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
656 DomLayoutResult {
657 styled_dom,
658 layout_tree: LayoutTree {
659 nodes: Vec::new(),
660 warm: Vec::new(),
661 cold: Vec::new(),
662 root: 0,
663 dom_to_layout: BTreeMap::new(),
664 children_arena: Vec::new(),
665 children_offsets: Vec::new(),
666 subtree_needs_intrinsic: Vec::new(),
667 },
668 calculated_positions: Vec::new(),
669 viewport: LogicalRect::zero(),
670 display_list: DisplayList::default(),
671 scroll_ids: HashMap::new(),
672 scroll_id_to_node_id: HashMap::new(),
673 }
674 }
675
676 fn dismissible_styled_dom() -> StyledDom {
680 let toast = Toast::create(AzString::from("msg"));
681 assert!(
682 toast.dismissible,
683 "a fresh toast must already carry a close button"
684 );
685 let styled = StyledDom::create_from_dom(toast.dom());
686 assert_eq!(
687 styled.node_hierarchy.as_ref().len(),
688 3,
689 "fixture must flatten to exactly container/message/close"
690 );
691 styled
692 }
693
694 fn run_dismiss(
698 styled: Option<StyledDom>,
699 hit: usize,
700 data: RefAny,
701 ) -> (Update, Vec<CallbackChange>) {
702 let mut layout_window =
703 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
704 if let Some(sd) = styled {
705 layout_window
706 .layout_results
707 .insert(DomId::ROOT_ID, layout_result(sd));
708 }
709
710 let renderer_resources = RendererResources::default();
711 let previous_window_state: Option<FullWindowState> = None;
712 let current_window_state = FullWindowState::default();
713 let gl_context = OptionGlContextPtr::None;
714 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
715 BTreeMap::new();
716 let window_handle = RawWindowHandle::Unsupported;
717 let system_callbacks = ExternalSystemCallbacks::rust_internal();
718
719 let ref_data = CallbackInfoRefData {
720 layout_window: &layout_window,
721 renderer_resources: &renderer_resources,
722 previous_window_state: &previous_window_state,
723 current_window_state: ¤t_window_state,
724 gl_context: &gl_context,
725 current_scroll_manager: &scroll_states,
726 current_window_handle: &window_handle,
727 system_callbacks: &system_callbacks,
728 system_style: Arc::new(SystemStyle::default()),
729 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
730 #[cfg(feature = "icu")]
731 icu_localizer: IcuLocalizerHandle::default(),
732 ctx: OptionRefAny::None,
733 };
734
735 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
736
737 let info = CallbackInfo::new(
738 &ref_data,
739 &changes,
740 DomNodeId {
741 dom: DomId::ROOT_ID,
742 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
743 },
744 OptionLogicalPosition::None,
745 OptionLogicalPosition::None,
746 );
747
748 let update = default_on_toast_dismiss(data, info);
749 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
750 (update, recorded)
751 }
752
753 fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
755 let mut out = Vec::new();
756 for change in changes {
757 if let CallbackChange::ChangeNodeCssProperties {
758 node_id, properties, ..
759 } = change
760 {
761 for p in properties.as_ref() {
762 if let CssProperty::Display(v) = p {
763 if let Some(d) = v.get_property() {
764 out.push((node_id.index(), *d));
765 }
766 }
767 }
768 }
769 }
770 out
771 }
772
773 #[test]
778 fn kind_colors_are_the_documented_bootstrap_palette() {
779 let expect = |(r, g, b): (u8, u8, u8)| ColorU { r, g, b, a: 255 };
780
781 assert_eq!(
782 ToastKind::Info.colors(),
783 (
784 expect((207, 244, 252)), expect((182, 239, 251)), expect((5, 81, 96)), )
788 );
789 assert_eq!(
790 ToastKind::Success.colors(),
791 (
792 expect((209, 231, 221)), expect((186, 219, 204)), expect((15, 81, 50)), )
796 );
797 assert_eq!(
798 ToastKind::Warning.colors(),
799 (
800 expect((255, 243, 205)), expect((255, 236, 181)), expect((102, 77, 3)), )
804 );
805 assert_eq!(
806 ToastKind::Danger.colors(),
807 (
808 expect((248, 215, 218)), expect((245, 194, 199)), expect((132, 32, 41)), )
812 );
813 }
814
815 #[test]
816 fn kind_colors_are_fully_opaque_and_pairwise_distinct() {
817 for kind in ALL_KINDS {
818 let (bg, border, text) = kind.colors();
819 for (name, c) in [("bg", bg), ("border", border), ("text", text)] {
820 assert_eq!(c.a, 255, "{kind:?}.{name} must be fully opaque");
821 }
822 assert_ne!(bg, text, "{kind:?}: background must differ from text");
824 assert_ne!(bg, border, "{kind:?}: the border must be visible on the card");
826 }
827
828 for (i, a) in ALL_KINDS.iter().enumerate() {
829 for b in &ALL_KINDS[i + 1..] {
830 assert_ne!(
831 a.colors(),
832 b.colors(),
833 "{a:?} and {b:?} must be visually distinguishable"
834 );
835 }
836 }
837 }
838
839 #[test]
840 fn kind_colors_default_is_info_and_the_call_is_pure() {
841 assert_eq!(ToastKind::default(), ToastKind::Info);
842 assert_eq!(ToastKind::default().colors(), ToastKind::Info.colors());
843
844 let k = ToastKind::Danger;
846 assert_eq!(k.colors(), k.colors());
847 assert_eq!(k.colors(), k.colors());
848 }
849
850 #[test]
851 fn kind_colors_is_const_evaluable() {
852 const INFO: (ColorU, ColorU, ColorU) = ToastKind::Info.colors();
853 assert_eq!(
854 INFO.0,
855 ColorU {
856 r: 207,
857 g: 244,
858 b: 252,
859 a: 255
860 }
861 );
862 }
863
864 #[test]
869 fn class_name_exact_values_and_shape() {
870 assert_eq!(ToastKind::Info.class_name(), "__azul-toast-info");
871 assert_eq!(ToastKind::Success.class_name(), "__azul-toast-success");
872 assert_eq!(ToastKind::Warning.class_name(), "__azul-toast-warning");
873 assert_eq!(ToastKind::Danger.class_name(), "__azul-toast-danger");
874
875 for kind in ALL_KINDS {
876 let name = kind.class_name();
877 assert!(
878 name.starts_with("__azul-toast-"),
879 "{kind:?} -> {name:?} must keep the widget prefix"
880 );
881 assert!(
882 !name.contains(char::is_whitespace),
883 "{name:?} must be a single CSS class token"
884 );
885 assert!(name.is_ascii(), "{name:?} must stay ASCII");
886 assert_eq!(name, kind.class_name());
888 }
889 }
890
891 #[test]
892 fn class_name_is_unique_per_kind() {
893 let mut names: Vec<&str> = ALL_KINDS.iter().map(|k| k.class_name()).collect();
894 names.sort_unstable();
895 names.dedup();
896 assert_eq!(names.len(), 4, "every kind needs its own class name");
897 }
898
899 #[test]
900 fn class_name_never_collides_with_the_structural_classes() {
901 let structural = ["__azul-native-toast", "__azul-native-toast-message",
904 "__azul-native-toast-close"];
905 for kind in ALL_KINDS {
906 for s in structural {
907 assert_ne!(kind.class_name(), s, "{kind:?} must not shadow {s:?}");
908 }
909 }
910 }
911
912 #[test]
913 fn class_name_is_const_evaluable() {
914 const DANGER: &str = ToastKind::Danger.class_name();
915 assert_eq!(DANGER, "__azul-toast-danger");
916 }
917
918 #[test]
923 fn build_toast_style_declares_the_same_properties_for_every_kind() {
924 let info = property_types(&build_toast_style(ToastKind::Info));
925 assert_eq!(
926 info.len(),
927 32,
928 "the container style declares 32 properties (pin: adding/removing one is a \
929 deliberate change)"
930 );
931
932 for kind in ALL_KINDS {
933 let style = build_toast_style(kind);
934 assert_eq!(
935 property_types(&style),
936 info,
937 "{kind:?} must declare the same properties, in the same order, as Info"
938 );
939 for p in style.as_ref() {
941 assert!(
942 p.apply_if.as_ref().is_empty(),
943 "{kind:?}: {:?} must be unconditional",
944 p.property
945 );
946 }
947 }
948 }
949
950 #[test]
951 fn build_toast_style_declares_no_property_twice() {
952 for kind in ALL_KINDS {
954 let types = property_types(&build_toast_style(kind));
955 for (i, a) in types.iter().enumerate() {
956 for b in &types[i + 1..] {
957 assert_ne!(
958 a, b,
959 "{kind:?}: the container style declares the same property twice"
960 );
961 }
962 }
963 }
964 }
965
966 #[test]
967 fn build_toast_style_colors_track_the_kind_palette() {
968 for kind in ALL_KINDS {
969 let style = build_toast_style(kind);
970 let (bg, border, text) = kind.colors();
971
972 assert_eq!(background_color(&style), Some(bg), "{kind:?}: background");
973 assert_eq!(text_color(&style), Some(text), "{kind:?}: text colour");
974
975 let borders = border_colors(&style);
976 assert_eq!(borders.len(), 4, "{kind:?}: all four edges must be coloured");
977 assert!(
978 borders.iter().all(|c| *c == border),
979 "{kind:?}: every edge must use the kind's border colour, got {borders:?}"
980 );
981 }
982 }
983
984 #[test]
985 fn build_toast_style_pins_the_card_to_the_bottom_right_corner() {
986 for kind in ALL_KINDS {
989 let style = build_toast_style(kind);
990
991 assert_eq!(
992 position_of(&style),
993 Some(LayoutPosition::Absolute),
994 "{kind:?}: a toast must float out of flow"
995 );
996 assert_eq!(
997 bottom_px(&style),
998 Some(LayoutInsetBottom::const_px(TOAST_INSET).inner),
999 "{kind:?}: bottom inset"
1000 );
1001 assert_eq!(
1002 right_px(&style),
1003 Some(LayoutRight::const_px(TOAST_INSET).inner),
1004 "{kind:?}: right inset"
1005 );
1006 assert_eq!(
1007 max_width_px(&style),
1008 Some(LayoutMaxWidth::const_px(TOAST_MAX_WIDTH).inner),
1009 "{kind:?}: width cap"
1010 );
1011 assert!(
1013 style.as_ref().contains(&CssPropertyWithConditions::simple(
1014 CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))
1015 )),
1016 "{kind:?}: the container must not flex-grow"
1017 );
1018 }
1019 }
1020
1021 #[test]
1022 fn toast_inset_and_max_width_survive_the_fixed_point_encoding_exactly() {
1023 assert_eq!(TOAST_INSET, 24);
1026 assert_eq!(TOAST_MAX_WIDTH, 360);
1027 assert!(
1028 TOAST_INSET > 0 && TOAST_MAX_WIDTH > TOAST_INSET,
1029 "an inset must push the card inward, and the cap must exceed the inset"
1030 );
1031
1032 let style = build_toast_style(ToastKind::Info);
1033 for (name, got, want) in [
1034 ("bottom", bottom_px(&style), TOAST_INSET),
1035 ("right", right_px(&style), TOAST_INSET),
1036 ("max-width", max_width_px(&style), TOAST_MAX_WIDTH),
1037 ] {
1038 let pv = got.unwrap_or_else(|| panic!("{name} must be declared"));
1039 assert_eq!(
1040 pv.metric,
1041 SizeMetric::Px,
1042 "{name} must be an absolute px length, not a %/em"
1043 );
1044 assert!(
1045 (pv.number.get() - want as f32).abs() < f32::EPSILON,
1046 "{name}: {} px decoded back as {}",
1047 want,
1048 pv.number.get()
1049 );
1050 assert!(
1051 pv.number.get().is_finite(),
1052 "{name} must never decode to NaN/inf"
1053 );
1054 }
1055 }
1056
1057 #[test]
1058 fn build_toast_style_geometry_is_kind_independent() {
1059 let expected = [
1061 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
1062 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
1063 LayoutFlexDirection::Row,
1064 )),
1065 CssPropertyWithConditions::simple(CssProperty::const_align_items(
1066 LayoutAlignItems::Start,
1067 )),
1068 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
1069 CssPropertyWithConditions::simple(CssProperty::const_padding_top(
1070 LayoutPaddingTop::const_px(12),
1071 )),
1072 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
1073 LayoutBorderTopWidth::const_px(1),
1074 )),
1075 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
1076 StyleBorderTopLeftRadius::const_px(6),
1077 )),
1078 CssPropertyWithConditions::simple(CssProperty::const_font_size(
1079 StyleFontSize::const_px(14),
1080 )),
1081 ];
1082
1083 for kind in ALL_KINDS {
1084 let style = build_toast_style(kind);
1085 for want in &expected {
1086 assert!(
1087 style.as_ref().contains(want),
1088 "{kind:?}: missing {:?}",
1089 want.property
1090 );
1091 }
1092 }
1093 }
1094
1095 #[test]
1096 fn build_toast_style_differs_only_in_the_colours() {
1097 let info = build_toast_style(ToastKind::Info);
1098 for kind in [ToastKind::Success, ToastKind::Warning, ToastKind::Danger] {
1099 let other = build_toast_style(kind);
1100 let differing: Vec<_> = info
1101 .as_ref()
1102 .iter()
1103 .zip(other.as_ref().iter())
1104 .filter(|(a, b)| a != b)
1105 .map(|(a, _)| core::mem::discriminant(&a.property))
1106 .collect();
1107
1108 assert_eq!(
1110 differing.len(),
1111 6,
1112 "{kind:?}: only bg + 4 border colours + text colour may depend on the kind"
1113 );
1114 }
1115 }
1116
1117 #[test]
1118 fn build_toast_style_is_pure_and_repeatable() {
1119 for kind in ALL_KINDS {
1120 assert_eq!(
1121 build_toast_style(kind),
1122 build_toast_style(kind),
1123 "{kind:?}: the builder must be deterministic"
1124 );
1125 }
1126 }
1127
1128 #[test]
1133 fn create_is_an_info_toast_that_is_dismissible_by_default() {
1134 let toast = Toast::create(AzString::from("hello"));
1135
1136 assert_eq!(toast.message.as_str(), "hello");
1137 assert_eq!(toast.kind, ToastKind::Info);
1138 assert!(
1139 toast.dismissible,
1140 "unlike Alert, a fresh Toast ships the close button (the only way to dismiss it)"
1141 );
1142 assert!(toast.toast_state.inner.visible, "a fresh toast is visible");
1143 assert!(toast.toast_state.on_dismiss.is_none());
1144 assert_eq!(toast.container_style, build_toast_style(ToastKind::Info));
1145 }
1146
1147 #[test]
1148 fn toast_state_defaults_to_visible_not_to_the_bool_default() {
1149 assert!(ToastState::default().visible);
1152 assert!(ToastStateWrapper::default().inner.visible);
1153 assert!(ToastStateWrapper::default().on_dismiss.is_none());
1154 assert!(Toast::default().toast_state.inner.visible);
1155 }
1156
1157 #[test]
1158 fn create_with_empty_message_equals_default_and_is_value_comparable() {
1159 assert_eq!(Toast::create(AzString::from("")), Toast::default());
1160 assert_eq!(
1162 Toast::create(AzString::from("a")),
1163 Toast::create(AzString::from("a"))
1164 );
1165 assert_ne!(
1166 Toast::create(AzString::from("a")),
1167 Toast::create(AzString::from("b"))
1168 );
1169 assert_ne!(
1170 Toast::create(AzString::from("a")),
1171 Toast::with_kind(AzString::from("a"), ToastKind::Danger)
1172 );
1173 }
1174
1175 #[test]
1176 fn create_survives_extreme_messages_and_round_trips_them_into_the_dom() {
1177 let long = "ab".repeat(50_000);
1178 let cases: Vec<AzString> = alloc::vec![
1179 AzString::from(""),
1180 AzString::from(" "),
1181 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("\u{00D7}\u{00D7}\u{00D7}"), AzString::from(long.as_str()), ];
1189
1190 for message in cases {
1191 let toast = Toast::create(message.clone());
1192 assert_eq!(toast.message.as_str(), message.as_str());
1193
1194 let dom = toast.dom();
1196 let children = dom.children.as_ref();
1197 assert_eq!(
1198 children.len(),
1199 2,
1200 "message content must never change the child count"
1201 );
1202 assert_eq!(text_of(&children[0]), Some(message.as_str()));
1203 assert!(
1205 children[0].root.has_class("__azul-native-toast-message"),
1206 "the first child is always the message"
1207 );
1208 assert!(
1209 children[1].root.has_class("__azul-native-toast-close"),
1210 "the close button is always last"
1211 );
1212 }
1213 }
1214
1215 #[test]
1216 fn with_kind_stores_both_args_for_every_kind() {
1217 for kind in ALL_KINDS {
1218 let toast = Toast::with_kind(AzString::from("m"), kind);
1219
1220 assert_eq!(toast.kind, kind);
1221 assert_eq!(toast.message.as_str(), "m");
1222 assert!(toast.dismissible);
1223 assert!(toast.toast_state.on_dismiss.is_none());
1224 assert!(toast.toast_state.inner.visible);
1225 assert_eq!(
1226 toast.container_style,
1227 build_toast_style(kind),
1228 "{kind:?}: the container style must match the kind it was built with"
1229 );
1230 }
1231 }
1232
1233 #[test]
1234 fn create_is_with_kind_info() {
1235 assert_eq!(
1236 Toast::create(AzString::from("m")),
1237 Toast::with_kind(AzString::from("m"), ToastKind::Info)
1238 );
1239 assert_eq!(
1240 Toast::create(AzString::from("m")),
1241 Toast::with_kind(AzString::from("m"), ToastKind::default())
1242 );
1243 }
1244
1245 #[test]
1250 fn set_kind_recomputes_the_style_and_is_idempotent() {
1251 let mut toast = Toast::create(AzString::from("m"));
1252
1253 for kind in ALL_KINDS {
1254 toast.set_kind(kind);
1255 assert_eq!(toast.kind, kind);
1256 assert_eq!(toast.container_style, build_toast_style(kind));
1257
1258 let before = toast.container_style.clone();
1260 toast.set_kind(kind);
1261 assert_eq!(
1262 toast.container_style, before,
1263 "{kind:?}: set_kind must be idempotent"
1264 );
1265 assert_eq!(
1266 toast.container_style.len(),
1267 32,
1268 "{kind:?}: restyling must not grow the property vec"
1269 );
1270 }
1271
1272 let original = Toast::create(AzString::from("m"));
1274 let mut cycled = original.clone();
1275 for kind in ALL_KINDS {
1276 cycled.set_kind(kind);
1277 }
1278 cycled.set_kind(ToastKind::Info);
1279 assert_eq!(cycled, original, "kind cycling must not accumulate state");
1280 }
1281
1282 #[test]
1283 fn set_kind_leaves_message_dismissible_and_callback_alone() {
1284 let log = RefAny::new(DismissLog { calls: Vec::new() });
1285 let mut toast = Toast::create(AzString::from("keep me"));
1286 toast.set_on_dismiss(log, dismiss_cb(dismiss_do_nothing));
1287 toast.toast_state.inner.visible = false;
1288
1289 toast.set_kind(ToastKind::Warning);
1290
1291 assert_eq!(toast.message.as_str(), "keep me");
1292 assert!(toast.dismissible, "set_kind must not clear the close button");
1293 assert!(
1294 toast.toast_state.on_dismiss.is_some(),
1295 "set_kind must not drop the callback"
1296 );
1297 assert!(
1298 !toast.toast_state.inner.visible,
1299 "set_kind must not resurrect a dismissed toast"
1300 );
1301 }
1302
1303 #[test]
1304 fn with_toast_kind_matches_set_kind_and_last_write_wins() {
1305 for kind in ALL_KINDS {
1306 let built = Toast::create(AzString::from("m")).with_toast_kind(kind);
1307 let mut mutated = Toast::create(AzString::from("m"));
1308 mutated.set_kind(kind);
1309 assert_eq!(built, mutated, "{kind:?}: builder and setter must agree");
1310 }
1311
1312 let toast = Toast::create(AzString::from("m"))
1313 .with_toast_kind(ToastKind::Danger)
1314 .with_toast_kind(ToastKind::Success);
1315 assert_eq!(toast.kind, ToastKind::Success);
1316 assert_eq!(toast.container_style, build_toast_style(ToastKind::Success));
1317 }
1318
1319 #[test]
1324 fn set_dismissible_last_write_wins_and_touches_nothing_else() {
1325 let mut toast = Toast::with_kind(AzString::from("m"), ToastKind::Warning);
1326 let style_before = toast.container_style.clone();
1327
1328 for flag in [true, true, false, true, false, false] {
1329 toast.set_dismissible(flag);
1330 assert_eq!(toast.dismissible, flag);
1331 }
1332
1333 assert_eq!(toast.kind, ToastKind::Warning);
1334 assert_eq!(toast.message.as_str(), "m");
1335 assert_eq!(
1336 toast.container_style, style_before,
1337 "toggling must not restyle"
1338 );
1339 assert!(
1340 toast.toast_state.on_dismiss.is_none(),
1341 "toggling must not invent a callback"
1342 );
1343 assert!(
1344 toast.toast_state.inner.visible,
1345 "toggling the close button must not hide the toast"
1346 );
1347 }
1348
1349 #[test]
1350 fn with_dismissible_toggle_sequence_ends_on_the_last_value() {
1351 assert!(Toast::default().with_dismissible(true).dismissible);
1352 assert!(!Toast::default().with_dismissible(false).dismissible);
1353 assert!(
1354 !Toast::default()
1355 .with_dismissible(true)
1356 .with_dismissible(false)
1357 .dismissible
1358 );
1359 assert!(
1360 Toast::default()
1361 .with_dismissible(false)
1362 .with_dismissible(true)
1363 .dismissible
1364 );
1365 let mut mutated = Toast::default();
1367 mutated.set_dismissible(false);
1368 assert_eq!(Toast::default().with_dismissible(false), mutated);
1369 assert_eq!(
1371 Toast::default()
1372 .with_dismissible(false)
1373 .with_dismissible(true),
1374 Toast::default()
1375 );
1376 }
1377
1378 #[test]
1383 fn set_on_dismiss_forces_dismissible_back_on() {
1384 let mut toast = Toast::create(AzString::from("m")).with_dismissible(false);
1385 assert!(!toast.dismissible);
1386
1387 toast.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
1388
1389 assert!(
1390 toast.dismissible,
1391 "a dismiss callback must re-render the close button"
1392 );
1393 assert!(toast.toast_state.on_dismiss.is_some());
1394 assert!(
1395 toast.toast_state.inner.visible,
1396 "wiring a callback must not hide the toast"
1397 );
1398 }
1399
1400 #[test]
1401 fn set_on_dismiss_replaces_rather_than_appends() {
1402 let mut toast = Toast::create(AzString::from("m"));
1403
1404 toast.set_on_dismiss(RefAny::new(1u8), dismiss_cb(dismiss_do_nothing));
1405 let first = toast
1406 .toast_state
1407 .on_dismiss
1408 .as_ref()
1409 .expect("first callback")
1410 .refany
1411 .get_type_id();
1412 assert_eq!(first, RefAny::new(1u8).get_type_id());
1413
1414 toast.set_on_dismiss(RefAny::new(9i64), dismiss_cb(record_dismiss));
1416 let second = toast.toast_state.on_dismiss.as_ref().expect("second callback");
1417 assert_eq!(second.refany.get_type_id(), RefAny::new(9i64).get_type_id());
1418 assert_eq!(second.callback, dismiss_cb(record_dismiss));
1419 assert_ne!(second.callback, dismiss_cb(dismiss_do_nothing));
1420 }
1421
1422 #[test]
1423 fn with_on_dismiss_keeps_message_and_kind() {
1424 let toast = Toast::with_kind(AzString::from("boom"), ToastKind::Danger)
1425 .with_on_dismiss(RefAny::new(0u8), dismiss_cb(dismiss_do_nothing));
1426
1427 assert_eq!(toast.message.as_str(), "boom");
1428 assert_eq!(toast.kind, ToastKind::Danger);
1429 assert_eq!(toast.container_style, build_toast_style(ToastKind::Danger));
1430 assert!(toast.dismissible);
1431 assert!(toast.toast_state.on_dismiss.is_some());
1432 }
1433
1434 #[test]
1435 fn set_dismissible_false_after_set_on_dismiss_silently_drops_the_close_button() {
1436 let mut toast = Toast::create(AzString::from("m"));
1442 toast.set_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
1443 toast.set_dismissible(false);
1444
1445 assert!(
1446 toast.toast_state.on_dismiss.is_some(),
1447 "the callback is still stored"
1448 );
1449 let dom = toast.dom();
1450 assert_eq!(
1451 dom.children.as_ref().len(),
1452 1,
1453 "no close button is rendered, so the toast is undismissable"
1454 );
1455 assert!(
1456 dom.children.as_ref()[0]
1457 .root
1458 .get_callbacks()
1459 .as_ref()
1460 .is_empty(),
1461 "and no handler is attached anywhere else either"
1462 );
1463 }
1464
1465 #[test]
1470 fn swap_with_default_returns_the_original_and_resets_self() {
1471 let mut toast =
1472 Toast::with_kind(AzString::from("payload"), ToastKind::Danger).with_dismissible(false);
1473 let snapshot = toast.clone();
1474
1475 let returned = toast.swap_with_default();
1476
1477 assert_eq!(returned, snapshot, "the original must come back untouched");
1478 assert_eq!(toast, Toast::default(), "self must be reset to a default toast");
1479 assert_eq!(toast.message.as_str(), "");
1480 assert_eq!(toast.kind, ToastKind::Info);
1481 assert!(
1482 toast.dismissible,
1483 "the reset toast is a *default* toast, so it is dismissible again"
1484 );
1485 assert!(toast.toast_state.on_dismiss.is_none());
1486 assert!(toast.toast_state.inner.visible);
1487 }
1488
1489 #[test]
1490 fn swap_with_default_is_stable_when_repeated() {
1491 let mut toast = Toast::default();
1492 for _ in 0..3 {
1493 let returned = toast.swap_with_default();
1494 assert_eq!(returned, Toast::default());
1495 assert_eq!(toast, Toast::default());
1496 }
1497 }
1498
1499 #[test]
1500 fn swap_with_default_moves_the_callback_out_of_self() {
1501 let mut toast = Toast::create(AzString::from("m"))
1502 .with_on_dismiss(RefAny::new(7u32), dismiss_cb(record_dismiss));
1503
1504 let returned = toast.swap_with_default();
1505
1506 assert!(
1507 returned.toast_state.on_dismiss.is_some(),
1508 "the callback moves out"
1509 );
1510 assert!(
1511 toast.toast_state.on_dismiss.is_none(),
1512 "the reset toast must not keep a reference to the old callback"
1513 );
1514 }
1515
1516 #[test]
1517 fn swap_with_default_round_trips_a_dismissed_toast() {
1518 let mut toast = Toast::create(AzString::from("m"));
1521 toast.toast_state.inner.visible = false;
1522
1523 let returned = toast.swap_with_default();
1524
1525 assert!(!returned.toast_state.inner.visible);
1526 assert!(toast.toast_state.inner.visible, "the fresh toast is visible");
1527 }
1528
1529 #[test]
1534 fn dom_of_a_default_toast_is_a_container_with_message_and_close() {
1535 let toast = Toast::create(AzString::from("hi"));
1536 let style = toast.container_style.clone();
1537 let dom = toast.dom();
1538
1539 assert!(dom.root.has_class("__azul-native-toast"));
1540 assert!(
1541 dom.root.get_callbacks().as_ref().is_empty(),
1542 "the container itself must carry no live callback"
1543 );
1544 assert_eq!(
1545 dom.root.style.iter_inline_properties().count(),
1546 style.len(),
1547 "every container property must reach the node's inline style"
1548 );
1549
1550 let children = dom.children.as_ref();
1551 assert_eq!(children.len(), 2, "[message, close]");
1552 assert!(children[0].root.has_class("__azul-native-toast-message"));
1553 assert_eq!(text_of(&children[0]), Some("hi"));
1554 assert!(children[0].root.get_callbacks().as_ref().is_empty());
1555 assert!(children[0].root.get_tab_index().is_none());
1556 }
1557
1558 #[test]
1559 fn dom_children_carry_exactly_the_static_child_styles() {
1560 let dom = Toast::create(AzString::from("hi")).dom();
1561 let children = dom.children.as_ref();
1562
1563 let want_message: Vec<CssProperty> = TOAST_MESSAGE_STYLE
1564 .iter()
1565 .map(|p| p.property.clone())
1566 .collect();
1567 let want_close: Vec<CssProperty> = TOAST_CLOSE_STYLE
1568 .iter()
1569 .map(|p| p.property.clone())
1570 .collect();
1571
1572 assert_eq!(inline_props(&children[0]), want_message);
1573 assert_eq!(inline_props(&children[1]), want_close);
1574
1575 assert!(want_message.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))));
1577 assert!(want_close.contains(&CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))));
1578 assert!(want_close.contains(&CssProperty::const_cursor(StyleCursor::Pointer)));
1580 assert!(want_close.contains(&CssProperty::user_select(StyleUserSelect::None)));
1581 }
1582
1583 #[test]
1584 fn dom_close_button_is_focusable_and_wired_to_the_dismiss_handler() {
1585 let dom = Toast::create(AzString::from("hi")).dom();
1586
1587 let children = dom.children.as_ref();
1588 assert_eq!(children.len(), 2);
1589
1590 let close = &children[1];
1591 assert!(close.root.has_class("__azul-native-toast-close"));
1592 assert_eq!(
1593 text_of(close),
1594 Some("\u{00D7}"),
1595 "the close glyph is U+00D7 MULTIPLICATION SIGN"
1596 );
1597 assert!(
1598 matches!(close.root.get_tab_index(), Some(TabIndex::Auto)),
1599 "the close button must be keyboard-reachable"
1600 );
1601
1602 let callbacks = close.root.get_callbacks();
1603 assert_eq!(callbacks.as_ref().len(), 1, "exactly one dismiss handler");
1604 let cb = &callbacks.as_ref()[0];
1605 assert!(matches!(
1606 &cb.event,
1607 EventFilter::Hover(HoverEventFilter::MouseUp)
1608 ));
1609 assert_eq!(cb.callback.cb, default_on_toast_dismiss as usize);
1610 assert!(matches!(&cb.callback.ctx, OptionRefAny::None));
1611 }
1612
1613 #[test]
1614 fn dom_hands_the_toast_state_to_the_close_button() {
1615 let toast = Toast::create(AzString::from("hi"))
1616 .with_on_dismiss(RefAny::new(0u8), dismiss_cb(record_dismiss));
1617 let dom = toast.dom();
1618
1619 let close = &dom.children.as_ref()[1];
1620 let mut payload = close.root.get_callbacks().as_ref()[0].refany.clone();
1621
1622 assert!(
1623 wrapper_visible(&mut payload),
1624 "the close button must receive a live, visible ToastStateWrapper"
1625 );
1626 assert!(
1627 payload
1628 .downcast_ref::<ToastStateWrapper>()
1629 .expect("ToastStateWrapper")
1630 .on_dismiss
1631 .is_some(),
1632 "the user callback must travel with the state"
1633 );
1634 }
1635
1636 #[test]
1637 fn dom_of_a_non_dismissible_toast_has_no_callbacks_at_all() {
1638 let dom = Toast::create(AzString::from("m")).with_dismissible(false).dom();
1639
1640 assert!(dom.root.has_class("__azul-native-toast"));
1641 let children = dom.children.as_ref();
1642 assert_eq!(children.len(), 1, "no close button without `dismissible`");
1643 assert!(children[0].root.has_class("__azul-native-toast-message"));
1644 assert!(children[0].root.get_callbacks().as_ref().is_empty());
1645 assert!(dom.root.get_callbacks().as_ref().is_empty());
1646 }
1647
1648 #[test]
1649 fn dom_renders_even_when_the_state_says_hidden() {
1650 let mut toast = Toast::create(AzString::from("gone"));
1655 toast.toast_state.inner.visible = false;
1656
1657 let dom = toast.dom();
1658 assert_eq!(dom.children.as_ref().len(), 2);
1659 assert_eq!(text_of(&dom.children.as_ref()[0]), Some("gone"));
1660
1661 let close = &dom.children.as_ref()[1];
1662 let mut payload = close.root.get_callbacks().as_ref()[0].refany.clone();
1663 assert!(
1664 !wrapper_visible(&mut payload),
1665 "the hidden state travels into the DOM verbatim"
1666 );
1667 }
1668
1669 #[test]
1670 fn dom_is_stable_across_kinds_and_the_kind_class_is_not_emitted() {
1671 for kind in ALL_KINDS {
1672 let dom = Toast::with_kind(AzString::from("m"), kind).dom();
1673 assert!(dom.root.has_class("__azul-native-toast"));
1674 assert_eq!(dom.children.as_ref().len(), 2);
1675
1676 assert!(
1679 !dom.root.has_class(kind.class_name()),
1680 "current behaviour: the kind class is not emitted"
1681 );
1682 }
1683 }
1684
1685 #[test]
1686 fn from_toast_for_dom_is_exactly_dom() {
1687 let toast = Toast::with_kind(AzString::from("m"), ToastKind::Success)
1689 .with_dismissible(false);
1690 let via_from = Dom::from(toast.clone());
1691 let via_method = toast.dom();
1692 assert!(
1693 via_from == via_method,
1694 "`impl From<Toast> for Dom` must delegate to `Toast::dom`"
1695 );
1696 }
1697
1698 #[test]
1703 fn dismiss_hides_the_container_and_flips_visible() {
1704 let mut data = RefAny::new(ToastStateWrapper::default());
1705
1706 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1708
1709 assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
1710 assert_eq!(
1711 display_writes(&changes),
1712 alloc::vec![(0usize, LayoutDisplay::None)],
1713 "the *container* (not the close button) must be hidden"
1714 );
1715 assert!(!wrapper_visible(&mut data), "state must flip to hidden");
1716 }
1717
1718 #[test]
1719 fn dismiss_invokes_the_user_callback_with_the_already_flipped_state() {
1720 let mut log = RefAny::new(DismissLog { calls: Vec::new() });
1721 let mut data = RefAny::new(ToastStateWrapper {
1722 inner: ToastState { visible: true },
1723 on_dismiss: Some(ToastOnDismiss {
1724 callback: dismiss_cb(record_dismiss),
1725 refany: log.clone(),
1726 })
1727 .into(),
1728 });
1729
1730 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1731
1732 assert_eq!(
1733 update,
1734 Update::RefreshDom,
1735 "the user callback's Update is returned"
1736 );
1737 assert_eq!(
1738 log_calls(&mut log),
1739 alloc::vec![false],
1740 "the callback must see `visible == false` (already dismissed)"
1741 );
1742 assert!(!wrapper_visible(&mut data));
1743 assert_eq!(
1744 display_writes(&changes),
1745 alloc::vec![(0usize, LayoutDisplay::None)],
1746 "the container is hidden even after a user callback ran"
1747 );
1748 }
1749
1750 #[test]
1751 fn dismiss_twice_is_idempotent() {
1752 let mut log = RefAny::new(DismissLog { calls: Vec::new() });
1753 let mut data = RefAny::new(ToastStateWrapper {
1754 inner: ToastState { visible: true },
1755 on_dismiss: Some(ToastOnDismiss {
1756 callback: dismiss_cb(record_dismiss),
1757 refany: log.clone(),
1758 })
1759 .into(),
1760 });
1761
1762 for _ in 0..2 {
1763 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1764 assert_eq!(update, Update::RefreshDom);
1765 assert_eq!(
1766 display_writes(&changes),
1767 alloc::vec![(0usize, LayoutDisplay::None)]
1768 );
1769 }
1770
1771 assert!(!wrapper_visible(&mut data), "a second dismiss must not un-hide");
1772 assert_eq!(
1773 log_calls(&mut log),
1774 alloc::vec![false, false],
1775 "each click fires the callback exactly once, always with visible == false"
1776 );
1777 }
1778
1779 #[test]
1780 fn dismiss_from_the_message_node_also_hides_the_container() {
1781 let mut data = RefAny::new(ToastStateWrapper::default());
1786
1787 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 1, data.clone());
1788
1789 assert_eq!(update, Update::DoNothing);
1790 assert_eq!(
1791 display_writes(&changes),
1792 alloc::vec![(0usize, LayoutDisplay::None)]
1793 );
1794 assert!(!wrapper_visible(&mut data));
1795 }
1796
1797 #[test]
1798 fn dismiss_on_a_root_hit_node_is_a_noop() {
1799 let mut data = RefAny::new(ToastStateWrapper::default());
1801
1802 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 0, data.clone());
1803
1804 assert_eq!(update, Update::DoNothing);
1805 assert!(changes.is_empty(), "nothing may be restyled without a parent");
1806 assert!(wrapper_visible(&mut data), "state must not flip");
1807 }
1808
1809 #[test]
1810 fn dismiss_with_a_stale_hit_node_is_a_noop() {
1811 let mut data = RefAny::new(ToastStateWrapper::default());
1813
1814 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 999, data.clone());
1815
1816 assert_eq!(update, Update::DoNothing);
1817 assert!(changes.is_empty());
1818 assert!(wrapper_visible(&mut data));
1819 }
1820
1821 #[test]
1822 fn dismiss_with_an_absurd_hit_node_index_does_not_panic() {
1823 let mut data = RefAny::new(ToastStateWrapper::default());
1825
1826 let (update, changes) =
1827 run_dismiss(Some(dismissible_styled_dom()), usize::MAX / 2, data.clone());
1828
1829 assert_eq!(update, Update::DoNothing);
1830 assert!(changes.is_empty());
1831 assert!(wrapper_visible(&mut data));
1832 }
1833
1834 #[test]
1835 fn dismiss_without_any_layout_result_is_a_noop() {
1836 let mut data = RefAny::new(ToastStateWrapper::default());
1837
1838 let (update, changes) = run_dismiss(None, 2, data.clone());
1839
1840 assert_eq!(update, Update::DoNothing);
1841 assert!(changes.is_empty());
1842 assert!(wrapper_visible(&mut data), "state must not flip");
1843 }
1844
1845 #[test]
1846 fn dismiss_with_a_foreign_payload_is_a_noop() {
1847 let data = RefAny::new(0xdead_beef_u64);
1849
1850 let (update, changes) = run_dismiss(Some(dismissible_styled_dom()), 2, data.clone());
1851
1852 assert_eq!(update, Update::DoNothing);
1853 assert!(
1854 changes.is_empty(),
1855 "a foreign payload must not hide the container"
1856 );
1857 }
1858
1859 #[test]
1860 fn dismiss_end_to_end_through_the_real_dom_payload() {
1861 let toast = Toast::create(AzString::from("bye"));
1864 let dom = toast.dom();
1865 let close = &dom.children.as_ref()[1];
1866 let entry = &close.root.get_callbacks().as_ref()[0];
1867 assert_eq!(entry.callback.cb, default_on_toast_dismiss as usize);
1868 let mut payload = entry.refany.clone();
1869
1870 let styled = StyledDom::create_from_dom(dom);
1871 let (update, changes) = run_dismiss(Some(styled), 2, payload.clone());
1872
1873 assert_eq!(update, Update::DoNothing);
1874 assert_eq!(
1875 display_writes(&changes),
1876 alloc::vec![(0usize, LayoutDisplay::None)]
1877 );
1878 assert!(
1879 !wrapper_visible(&mut payload),
1880 "the state living in the DOM must be flipped to hidden"
1881 );
1882 }
1883
1884 #[test]
1885 fn dismiss_end_to_end_reaches_a_user_callback_wired_through_the_builder() {
1886 let mut log = RefAny::new(DismissLog { calls: Vec::new() });
1887 let dom = Toast::with_kind(AzString::from("bye"), ToastKind::Danger)
1888 .with_on_dismiss(log.clone(), dismiss_cb(record_dismiss))
1889 .dom();
1890 let payload = dom.children.as_ref()[1]
1891 .root
1892 .get_callbacks()
1893 .as_ref()[0]
1894 .refany
1895 .clone();
1896
1897 let styled = StyledDom::create_from_dom(dom);
1898 let (update, changes) = run_dismiss(Some(styled), 2, payload);
1899
1900 assert_eq!(update, Update::RefreshDom);
1901 assert_eq!(
1902 display_writes(&changes),
1903 alloc::vec![(0usize, LayoutDisplay::None)]
1904 );
1905 assert_eq!(
1906 log_calls(&mut log),
1907 alloc::vec![false],
1908 "the builder-wired callback must fire exactly once, with visible == false"
1909 );
1910 }
1911}