1use azul_core::{
37 callbacks::{CoreCallback, CoreCallbackData, Update},
38 dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
39 refany::RefAny,
40};
41use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
42use azul_css::{
43 props::{
44 basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, PixelValue, StyleFontSize},
45 layout::{LayoutDisplay, LayoutPosition, LayoutTop, LayoutLeft, LayoutWidth, LayoutHeight, LayoutFlexDirection, LayoutJustifyContent, LayoutAlignItems, LayoutFlexGrow, LayoutMinWidth, LayoutMaxWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutRight},
46 property::{CssProperty, *},
47 style::{StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleUserSelect, StyleCursor},
48 },
49 impl_option_inner, AzString,
50};
51
52use crate::callbacks::{Callback, CallbackInfo};
53
54static MODAL_BACKDROP_CLASS: &[IdOrClass] =
55 &[Class(AzString::from_const_str("__azul-native-modal"))];
56static MODAL_PANEL_CLASS: &[IdOrClass] =
57 &[Class(AzString::from_const_str("__azul-native-modal-panel"))];
58static MODAL_TITLE_CLASS: &[IdOrClass] =
59 &[Class(AzString::from_const_str("__azul-native-modal-title"))];
60static MODAL_CLOSE_CLASS: &[IdOrClass] =
61 &[Class(AzString::from_const_str("__azul-native-modal-close"))];
62static MODAL_CONTENT_CLASS: &[IdOrClass] =
63 &[Class(AzString::from_const_str("__azul-native-modal-content"))];
64
65const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
66const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
67const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
68 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
69
70const PANEL_MIN_WIDTH: isize = 280;
72const PANEL_MAX_WIDTH: isize = 520;
73const PANEL_RADIUS: isize = 8;
74
75const BACKDROP_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 128 };
78const PANEL_BG_COLOR: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
79const PANEL_BORDER_COLOR: ColorU = ColorU { r: 204, g: 204, b: 204, a: 255 }; const TITLE_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 }; const CLOSE_COLOR: ColorU = ColorU { r: 108, g: 117, b: 125, a: 255 }; pub type ModalOnCloseCallbackType = extern "C" fn(RefAny, CallbackInfo, ModalState) -> Update;
86impl_widget_callback!(
87 ModalOnClose,
88 OptionModalOnClose,
89 ModalOnCloseCallback,
90 ModalOnCloseCallbackType
91);
92
93azul_core::impl_managed_callback! {
94 wrapper: ModalOnCloseCallback,
95 info_ty: CallbackInfo,
96 return_ty: Update,
97 default_ret: Update::DoNothing,
98 invoker_static: MODAL_ON_CLOSE_INVOKER,
99 invoker_ty: AzModalOnCloseCallbackInvoker,
100 thunk_fn: az_modal_on_close_callback_thunk,
101 setter_fn: AzApp_setModalOnCloseCallbackInvoker,
102 from_handle_fn: AzModalOnCloseCallback_createFromHostHandle,
103 extra_args: [ state: ModalState ],
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
109#[repr(C)]
110pub struct Modal {
111 pub modal_state: ModalStateWrapper,
113 pub title: AzString,
115 pub content: Dom,
117 pub show_close_button: bool,
119 pub backdrop_style: CssPropertyWithConditionsVec,
121}
122
123#[derive(Debug, Default, Clone, PartialEq, Eq)]
124#[repr(C)]
125pub struct ModalStateWrapper {
126 pub inner: ModalState,
128 pub on_close: OptionModalOnClose,
130}
131
132#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
134#[repr(C)]
135pub struct ModalState {
136 pub open: bool,
138}
139
140fn build_backdrop_style(open: bool) -> CssPropertyWithConditionsVec {
144 let display = if open {
145 LayoutDisplay::Flex
146 } else {
147 LayoutDisplay::None
148 };
149 let bg_vec = StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(
150 BACKDROP_COLOR
151 )]);
152 CssPropertyWithConditionsVec::from_vec(alloc::vec![
153 CssPropertyWithConditions::simple(CssProperty::const_display(display)),
154 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
155 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(0))),
156 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
157 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::Px(
160 PixelValue::const_percent(100),
161 ))),
162 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::Px(
163 PixelValue::const_percent(100),
164 ))),
165 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
167 LayoutFlexDirection::Row,
168 )),
169 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
170 LayoutJustifyContent::Center,
171 )),
172 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
173 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
174 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
175 ])
176}
177
178static MODAL_PANEL_STYLE: &[CssPropertyWithConditions] = &[
183 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
184 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
185 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
186 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
187 CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
188 PANEL_MIN_WIDTH,
189 ))),
190 CssPropertyWithConditions::simple(CssProperty::const_max_width(LayoutMaxWidth::const_px(
191 PANEL_MAX_WIDTH,
192 ))),
193 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(20))),
195 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
196 LayoutPaddingBottom::const_px(20),
197 )),
198 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
199 20,
200 ))),
201 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
202 LayoutPaddingRight::const_px(20),
203 )),
204 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
206 LayoutBorderTopWidth::const_px(1),
207 )),
208 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
209 LayoutBorderBottomWidth::const_px(1),
210 )),
211 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
212 LayoutBorderLeftWidth::const_px(1),
213 )),
214 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
215 LayoutBorderRightWidth::const_px(1),
216 )),
217 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
218 inner: BorderStyle::Solid,
219 })),
220 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
221 StyleBorderBottomStyle {
222 inner: BorderStyle::Solid,
223 },
224 )),
225 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
226 inner: BorderStyle::Solid,
227 })),
228 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
229 StyleBorderRightStyle {
230 inner: BorderStyle::Solid,
231 },
232 )),
233 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
234 inner: PANEL_BORDER_COLOR,
235 })),
236 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
237 StyleBorderBottomColor {
238 inner: PANEL_BORDER_COLOR,
239 },
240 )),
241 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
242 inner: PANEL_BORDER_COLOR,
243 })),
244 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
245 StyleBorderRightColor {
246 inner: PANEL_BORDER_COLOR,
247 },
248 )),
249 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
251 StyleBorderTopLeftRadius::const_px(PANEL_RADIUS),
252 )),
253 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
254 StyleBorderTopRightRadius::const_px(PANEL_RADIUS),
255 )),
256 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
257 StyleBorderBottomLeftRadius::const_px(PANEL_RADIUS),
258 )),
259 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
260 StyleBorderBottomRightRadius::const_px(PANEL_RADIUS),
261 )),
262 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
263 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
264 CssPropertyWithConditions::simple(CssProperty::const_background_content(
265 StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(
266 PANEL_BG_COLOR,
267 )]),
268 )),
269];
270
271static MODAL_TITLE_STYLE: &[CssPropertyWithConditions] = &[
274 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
275 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
276 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
277 inner: TITLE_COLOR,
278 })),
279 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
280 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
281 LayoutPaddingRight::const_px(24),
282 )),
283 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
284 LayoutPaddingBottom::const_px(12),
285 )),
286 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
287];
288
289static MODAL_CLOSE_STYLE: &[CssPropertyWithConditions] = &[
292 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
293 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(8))),
294 CssPropertyWithConditions::simple(CssProperty::const_right(LayoutRight::const_px(12))),
295 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(22))),
296 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
297 inner: CLOSE_COLOR,
298 })),
299 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
300 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
301];
302
303static MODAL_CONTENT_STYLE: &[CssPropertyWithConditions] = &[
305 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
306 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
307];
308
309impl Modal {
310 #[must_use] pub fn create(content: Dom) -> Self {
313 Self {
314 modal_state: ModalStateWrapper::default(),
315 title: AzString::from_const_str(""),
316 content,
317 show_close_button: true,
318 backdrop_style: build_backdrop_style(false),
319 }
320 }
321
322 #[inline]
324 pub fn set_title(&mut self, title: AzString) {
325 self.title = title;
326 }
327
328 #[inline]
330 #[must_use] pub fn with_title(mut self, title: AzString) -> Self {
331 self.set_title(title);
332 self
333 }
334
335 #[inline]
337 pub fn set_content(&mut self, content: Dom) {
338 self.content = content;
339 }
340
341 #[inline]
343 #[must_use] pub fn with_content(mut self, content: Dom) -> Self {
344 self.set_content(content);
345 self
346 }
347
348 #[inline]
350 pub fn set_open(&mut self, open: bool) {
351 self.modal_state.inner.open = open;
352 self.backdrop_style = build_backdrop_style(open);
353 }
354
355 #[inline]
357 #[must_use] pub fn with_open(mut self, open: bool) -> Self {
358 self.set_open(open);
359 self
360 }
361
362 #[inline]
364 pub const fn set_close_button(&mut self, show: bool) {
365 self.show_close_button = show;
366 }
367
368 #[inline]
370 #[must_use] pub const fn with_close_button(mut self, show: bool) -> Self {
371 self.set_close_button(show);
372 self
373 }
374
375 #[inline]
377 pub fn set_on_close<C: Into<ModalOnCloseCallback>>(&mut self, data: RefAny, on_close: C) {
378 self.modal_state.on_close = Some(ModalOnClose {
379 callback: on_close.into(),
380 refany: data,
381 })
382 .into();
383 }
384
385 #[inline]
387 #[must_use] pub fn with_on_close<C: Into<ModalOnCloseCallback>>(
388 mut self,
389 data: RefAny,
390 on_close: C,
391 ) -> Self {
392 self.set_on_close(data, on_close);
393 self
394 }
395
396 #[inline]
398 #[must_use] pub fn swap_with_default(&mut self) -> Self {
399 let mut s = Self::create(Dom::default());
400 core::mem::swap(&mut s, self);
401 s
402 }
403
404 #[must_use] pub fn dom(self) -> Dom {
407 let mut panel_children = Vec::new();
411
412 if self.show_close_button {
413 let close = Dom::create_text(AzString::from_const_str("\u{00D7}"))
414 .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_CLOSE_CLASS))
415 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_CLOSE_STYLE))
416 .with_tab_index(TabIndex::Auto)
417 .with_callbacks(
418 alloc::vec![CoreCallbackData {
419 event: azul_core::dom::EventFilter::Hover(
420 azul_core::dom::HoverEventFilter::MouseUp,
421 ),
422 callback: CoreCallback {
423 cb: on_modal_close as usize,
424 ctx: azul_core::refany::OptionRefAny::None,
425 },
426 refany: RefAny::new(self.modal_state),
427 }]
428 .into(),
429 );
430 panel_children.push(close);
431 }
432
433 if !self.title.as_str().is_empty() {
434 let title = Dom::create_text(self.title)
435 .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_TITLE_CLASS))
436 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_TITLE_STYLE));
437 panel_children.push(title);
438 }
439
440 let content = Dom::create_div()
441 .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_CONTENT_CLASS))
442 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_CONTENT_STYLE))
443 .with_children(DomVec::from_vec(alloc::vec![self.content]));
444 panel_children.push(content);
445
446 let panel = Dom::create_div()
447 .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_PANEL_CLASS))
448 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_PANEL_STYLE))
449 .with_children(DomVec::from_vec(panel_children));
450
451 Dom::create_div()
452 .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_BACKDROP_CLASS))
453 .with_css_props(self.backdrop_style)
454 .with_children(DomVec::from_vec(alloc::vec![panel]))
455 }
456}
457
458impl Default for Modal {
459 fn default() -> Self {
460 Self::create(Dom::default())
461 }
462}
463
464extern "C" fn on_modal_close(mut data: RefAny, mut info: CallbackInfo) -> Update {
470 let close_node = info.get_hit_node();
471 let Some(panel) = info.get_parent(close_node) else {
472 return Update::DoNothing;
473 };
474 let Some(backdrop) = info.get_parent(panel) else {
475 return Update::DoNothing;
476 };
477
478 let result = {
479 let Some(mut modal) = data.downcast_mut::<ModalStateWrapper>() else {
480 return Update::DoNothing;
481 };
482 modal.inner.open = false;
483 let inner = modal.inner;
484 let modal = &mut *modal;
485 match modal.on_close.as_mut() {
486 Some(ModalOnClose { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
487 None => Update::DoNothing,
488 }
489 };
490
491 info.set_css_property(backdrop, CssProperty::const_display(LayoutDisplay::None));
495
496 result
497}
498
499impl From<Modal> for Dom {
500 fn from(m: Modal) -> Self {
501 m.dom()
502 }
503}
504
505#[cfg(test)]
506mod autotest_generated {
507 use std::{
508 collections::{BTreeMap, HashMap},
509 sync::{Arc, Mutex},
510 };
511
512 use azul_core::{
513 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
514 geom::{LogicalRect, OptionLogicalPosition},
515 gl::OptionGlContextPtr,
516 hit_test::ScrollPosition,
517 refany::OptionRefAny,
518 resources::RendererResources,
519 styled_dom::{NodeHierarchyItemId, StyledDom},
520 window::{MonitorVec, RawWindowHandle},
521 };
522 use azul_css::system::SystemStyle;
523 use rust_fontconfig::FcFontCache;
524
525 use super::*;
526 #[cfg(feature = "icu")]
527 use crate::icu::IcuLocalizerHandle;
528 use crate::{
529 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
530 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
531 window::{DomLayoutResult, LayoutWindow},
532 window_state::FullWindowState,
533 };
534
535 const ADVERSARIAL_TEXT: [&str; 8] = [
544 "",
545 " ",
546 "a\0b",
547 "e\u{0301}\u{0301}\u{0301}",
548 "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}",
549 "\u{202E}gnirts desrever\u{202C}",
550 "\u{FFFD}\u{FEFF}\t\n",
551 "\u{200B}",
552 ];
553
554 fn has_class(node: &Dom, name: &str) -> bool {
556 node.root
557 .get_ids_and_classes()
558 .as_ref()
559 .iter()
560 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == name))
561 }
562
563 fn text_of(node: &Dom) -> Option<&str> {
565 match node.root.get_node_type() {
566 NodeType::Text(s) => Some(s.as_ref().as_str()),
567 _ => None,
568 }
569 }
570
571 fn style_props(style: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
573 style.as_ref().iter().map(|p| p.property.clone()).collect()
574 }
575
576 fn property_types(
578 style: &CssPropertyWithConditionsVec,
579 ) -> Vec<core::mem::Discriminant<CssProperty>> {
580 style
581 .as_ref()
582 .iter()
583 .map(|p| core::mem::discriminant(&p.property))
584 .collect()
585 }
586
587 fn inline_props(node: &Dom) -> Vec<CssProperty> {
589 node.root
590 .style
591 .iter_inline_properties()
592 .map(|(p, _)| p.clone())
593 .collect()
594 }
595
596 fn display_of(style: &CssPropertyWithConditionsVec) -> Option<LayoutDisplay> {
598 style.as_ref().iter().find_map(|p| match &p.property {
599 CssProperty::Display(v) => v.get_property().copied(),
600 _ => None,
601 })
602 }
603
604 fn inline_display(node: &Dom) -> Option<LayoutDisplay> {
606 node.root
607 .style
608 .iter_inline_properties()
609 .find_map(|(p, _)| match p {
610 CssProperty::Display(v) => v.get_property().copied(),
611 _ => None,
612 })
613 }
614
615 fn background_color(style: &CssPropertyWithConditionsVec) -> Option<ColorU> {
617 style.as_ref().iter().find_map(|p| match &p.property {
618 CssProperty::BackgroundContent(v) => match v.get_property()?.as_ref().first()? {
619 StyleBackgroundContent::Color(c) => Some(*c),
620 _ => None,
621 },
622 _ => None,
623 })
624 }
625
626 fn panel(dom: &Dom) -> &Dom {
628 let kids = dom.children.as_ref();
629 assert_eq!(kids.len(), 1, "the backdrop must have exactly one child");
630 assert!(
631 has_class(&kids[0], "__azul-native-modal-panel"),
632 "the backdrop's only child must be the panel"
633 );
634 &kids[0]
635 }
636
637 fn panel_kids(dom: &Dom) -> &[Dom] {
638 panel(dom).children.as_ref()
639 }
640
641 fn find_class<'a>(dom: &'a Dom, class: &str) -> Option<&'a Dom> {
643 if has_class(dom, class) {
644 return Some(dom);
645 }
646 dom.children
647 .as_ref()
648 .iter()
649 .find_map(|c| find_class(c, class))
650 }
651
652 fn node_count(dom: &Dom) -> usize {
654 1 + dom
655 .children
656 .as_ref()
657 .iter()
658 .map(node_count)
659 .sum::<usize>()
660 }
661
662 fn count_callbacks(dom: &Dom) -> usize {
664 dom.root.get_callbacks().as_ref().len()
665 + dom
666 .children
667 .as_ref()
668 .iter()
669 .map(count_callbacks)
670 .sum::<usize>()
671 }
672
673 fn nested_content(depth: usize) -> Dom {
675 let mut d = Dom::create_div();
676 for _ in 0..depth {
677 d = Dom::create_div().with_child(d);
678 }
679 d
680 }
681
682 fn expected_backdrop(display: LayoutDisplay) -> Vec<CssProperty> {
684 alloc::vec![
685 CssProperty::const_display(display),
686 CssProperty::const_position(LayoutPosition::Absolute),
687 CssProperty::const_top(LayoutTop::const_px(0)),
688 CssProperty::const_left(LayoutLeft::const_px(0)),
689 CssProperty::const_width(LayoutWidth::Px(PixelValue::const_percent(100))),
690 CssProperty::const_height(LayoutHeight::Px(PixelValue::const_percent(100))),
691 CssProperty::const_flex_direction(LayoutFlexDirection::Row),
692 CssProperty::const_justify_content(LayoutJustifyContent::Center),
693 CssProperty::const_align_items(LayoutAlignItems::Center),
694 CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0)),
695 CssProperty::const_background_content(StyleBackgroundContentVec::from_vec(
696 alloc::vec![StyleBackgroundContent::Color(BACKDROP_COLOR)]
697 )),
698 ]
699 }
700
701 struct CloseLog {
703 calls: Vec<bool>,
704 }
705
706 extern "C" fn record_close(mut data: RefAny, _: CallbackInfo, state: ModalState) -> Update {
707 if let Some(mut log) = data.downcast_mut::<CloseLog>() {
708 log.calls.push(state.open);
709 }
710 Update::RefreshDom
711 }
712
713 extern "C" fn close_do_nothing(_: RefAny, _: CallbackInfo, _: ModalState) -> Update {
714 Update::DoNothing
715 }
716
717 fn close_cb(f: ModalOnCloseCallbackType) -> ModalOnCloseCallback {
718 f.into()
719 }
720
721 fn wrapper_open(data: &mut RefAny) -> bool {
723 data.downcast_ref::<ModalStateWrapper>()
724 .expect("payload must still be a ModalStateWrapper")
725 .inner
726 .open
727 }
728
729 fn log_calls(data: &mut RefAny) -> Vec<bool> {
731 data.downcast_ref::<CloseLog>()
732 .expect("payload must still be a CloseLog")
733 .calls
734 .clone()
735 }
736
737 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
740 DomLayoutResult {
741 styled_dom,
742 layout_tree: LayoutTree {
743 nodes: Vec::new(),
744 warm: Vec::new(),
745 cold: Vec::new(),
746 root: 0,
747 dom_to_layout: BTreeMap::new(),
748 children_arena: Vec::new(),
749 children_offsets: Vec::new(),
750 subtree_needs_intrinsic: Vec::new(),
751 },
752 calculated_positions: Vec::new(),
753 viewport: LogicalRect::zero(),
754 display_list: DisplayList::default(),
755 scroll_ids: HashMap::new(),
756 scroll_id_to_node_id: HashMap::new(),
757 }
758 }
759
760 fn modal_styled_dom() -> StyledDom {
764 let styled = StyledDom::create_from_dom(Modal::create(Dom::create_div()).dom());
765 assert_eq!(
766 styled.node_hierarchy.as_ref().len(),
767 5,
768 "fixture must flatten to backdrop/panel/close/wrapper/content"
769 );
770 styled
771 }
772
773 fn run_close(
777 styled: Option<StyledDom>,
778 hit: usize,
779 data: RefAny,
780 ) -> (Update, Vec<CallbackChange>) {
781 let mut layout_window =
782 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
783 if let Some(sd) = styled {
784 layout_window
785 .layout_results
786 .insert(DomId::ROOT_ID, layout_result(sd));
787 }
788
789 let renderer_resources = RendererResources::default();
790 let previous_window_state: Option<FullWindowState> = None;
791 let current_window_state = FullWindowState::default();
792 let gl_context = OptionGlContextPtr::None;
793 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
794 BTreeMap::new();
795 let window_handle = RawWindowHandle::Unsupported;
796 let system_callbacks = ExternalSystemCallbacks::rust_internal();
797
798 let ref_data = CallbackInfoRefData {
799 layout_window: &layout_window,
800 renderer_resources: &renderer_resources,
801 previous_window_state: &previous_window_state,
802 current_window_state: ¤t_window_state,
803 gl_context: &gl_context,
804 current_scroll_manager: &scroll_states,
805 current_window_handle: &window_handle,
806 system_callbacks: &system_callbacks,
807 system_style: Arc::new(SystemStyle::default()),
808 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
809 #[cfg(feature = "icu")]
810 icu_localizer: IcuLocalizerHandle::default(),
811 ctx: OptionRefAny::None,
812 };
813
814 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
815
816 let info = CallbackInfo::new(
817 &ref_data,
818 &changes,
819 DomNodeId {
820 dom: DomId::ROOT_ID,
821 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
822 },
823 OptionLogicalPosition::None,
824 OptionLogicalPosition::None,
825 );
826
827 let update = on_modal_close(data, info);
828 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
829 (update, recorded)
830 }
831
832 fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
834 let mut out = Vec::new();
835 for change in changes {
836 if let CallbackChange::ChangeNodeCssProperties {
837 node_id, properties, ..
838 } = change
839 {
840 for p in properties.as_ref() {
841 if let CssProperty::Display(v) = p {
842 if let Some(d) = v.get_property() {
843 out.push((node_id.index(), *d));
844 }
845 }
846 }
847 }
848 }
849 out
850 }
851
852 #[test]
857 fn build_backdrop_style_emits_the_documented_property_list() {
858 assert_eq!(
859 style_props(&build_backdrop_style(true)),
860 expected_backdrop(LayoutDisplay::Flex)
861 );
862 assert_eq!(
863 style_props(&build_backdrop_style(false)),
864 expected_backdrop(LayoutDisplay::None)
865 );
866 }
867
868 #[test]
869 fn build_backdrop_style_differs_only_in_the_display() {
870 let open = style_props(&build_backdrop_style(true));
874 let closed = style_props(&build_backdrop_style(false));
875
876 assert_eq!(open.len(), closed.len());
877 let diffs: Vec<usize> = (0..open.len()).filter(|i| open[*i] != closed[*i]).collect();
878 assert_eq!(diffs, alloc::vec![0usize], "only entry 0 (display) may differ");
879
880 assert_eq!(display_of(&build_backdrop_style(true)), Some(LayoutDisplay::Flex));
881 assert_eq!(display_of(&build_backdrop_style(false)), Some(LayoutDisplay::None));
882 }
883
884 #[test]
885 fn build_backdrop_style_declares_no_property_twice() {
886 for open in [true, false] {
889 let types = property_types(&build_backdrop_style(open));
890 for (i, a) in types.iter().enumerate() {
891 for b in &types[i + 1..] {
892 assert_ne!(a, b, "open={open}: the backdrop declares the same property twice");
893 }
894 }
895 }
896 }
897
898 #[test]
899 fn build_backdrop_style_is_unconditional() {
900 for open in [true, false] {
903 for p in build_backdrop_style(open).as_ref() {
904 assert!(
905 p.apply_if.as_ref().is_empty(),
906 "open={open}: {:?} must be unconditional",
907 p.property
908 );
909 }
910 }
911 }
912
913 #[test]
914 fn build_backdrop_style_is_pure_and_repeatable() {
915 for open in [true, false] {
916 assert_eq!(build_backdrop_style(open), build_backdrop_style(open));
917 }
918 assert_ne!(build_backdrop_style(true), build_backdrop_style(false));
919 }
920
921 #[test]
922 fn build_backdrop_style_dims_with_half_transparent_black() {
923 for open in [true, false] {
924 let style = build_backdrop_style(open);
925 assert_eq!(
926 background_color(&style),
927 Some(ColorU { r: 0, g: 0, b: 0, a: 128 }),
928 "the backdrop must stay rgba(0,0,0,0.5) in both states"
929 );
930 }
931 let alpha = BACKDROP_COLOR.a;
933 assert!(alpha > 0 && alpha < 255, "backdrop alpha {alpha} must dim, not erase");
934 }
935
936 #[test]
941 fn create_is_a_closed_modal_with_a_close_button_and_no_title() {
942 let content = Dom::create_div().with_child(Dom::create_text("hi"));
943 let m = Modal::create(content.clone());
944
945 assert!(!m.modal_state.inner.open, "a fresh modal must start closed");
946 assert!(m.modal_state.on_close.is_none());
947 assert_eq!(m.title.as_str(), "");
948 assert_eq!(m.content, content);
949 assert!(m.show_close_button, "the 'x' is on by default");
950 assert_eq!(display_of(&m.backdrop_style), Some(LayoutDisplay::None));
951 }
952
953 #[test]
954 fn default_equals_create_with_a_default_dom() {
955 assert_eq!(Modal::default(), Modal::create(Dom::default()));
956 assert_eq!(Modal::default(), Modal::default());
957 assert!(!Modal::default().modal_state.inner.open);
958 }
959
960 #[test]
961 fn create_survives_extreme_content() {
962 assert_eq!(node_count(&Modal::create(nested_content(200)).content), 201);
964
965 let wide = Dom::create_div().with_children(DomVec::from_vec(
966 (0..5_000).map(|_| Dom::create_div()).collect::<Vec<_>>(),
967 ));
968 assert_eq!(node_count(&Modal::create(wide).content), 5_001);
969
970 let inner = Modal::create(Dom::create_div()).with_title(AzString::from("inner"));
972 let nested = Modal::create(inner.dom());
973 assert_eq!(nested.content.children.as_ref().len(), 1);
974 }
975
976 #[test]
977 fn create_is_clone_and_value_comparable() {
978 let m = Modal::create(Dom::create_div()).with_title(AzString::from("t"));
979 assert_eq!(m.clone(), m);
980 assert_ne!(m, Modal::create(Dom::create_div()));
981 }
982
983 #[test]
988 fn set_title_stores_every_adversarial_string_byte_for_byte() {
989 for s in ADVERSARIAL_TEXT {
990 let mut m = Modal::create(Dom::create_div());
991 m.set_title(AzString::from(s));
992 assert_eq!(m.title.as_str(), s, "title {s:?} must round-trip unchanged");
993 assert_eq!(m.title.as_str().len(), s.len(), "no normalisation may happen");
994 }
995 }
996
997 #[test]
998 fn set_title_survives_a_100k_char_title() {
999 let long = "ä\u{0301}".repeat(50_000);
1000 let mut m = Modal::create(Dom::create_div());
1001 m.set_title(AzString::from(long.as_str()));
1002 assert_eq!(m.title.as_str(), long);
1003
1004 let dom = m.dom();
1005 assert_eq!(
1006 text_of(&panel_kids(&dom)[1]),
1007 Some(long.as_str()),
1008 "the huge title must reach the DOM intact"
1009 );
1010 }
1011
1012 #[test]
1013 fn with_title_matches_set_title_and_last_write_wins() {
1014 for s in ADVERSARIAL_TEXT {
1015 let built = Modal::create(Dom::create_div()).with_title(AzString::from(s));
1016 let mut mutated = Modal::create(Dom::create_div());
1017 mutated.set_title(AzString::from(s));
1018 assert_eq!(built, mutated);
1019 }
1020
1021 let m = Modal::create(Dom::create_div())
1022 .with_title(AzString::from("first"))
1023 .with_title(AzString::from("second"))
1024 .with_title(AzString::from(""));
1025 assert_eq!(m.title.as_str(), "", "the last write must win, even an empty one");
1026 }
1027
1028 #[test]
1029 fn set_title_touches_nothing_else() {
1030 let content = Dom::create_div().with_child(Dom::create_text("body"));
1031 let mut m = Modal::create(content.clone()).with_open(true);
1032 let before = m.backdrop_style.clone();
1033
1034 m.set_title(AzString::from("Title"));
1035
1036 assert!(m.modal_state.inner.open, "the title must not close the dialog");
1037 assert_eq!(m.backdrop_style, before, "the title must not rebuild the backdrop");
1038 assert_eq!(m.content, content);
1039 assert!(m.show_close_button);
1040 }
1041
1042 #[test]
1043 fn only_a_byte_empty_title_suppresses_the_title_node() {
1044 for s in ADVERSARIAL_TEXT {
1045 let dom = Modal::create(Dom::create_div())
1046 .with_title(AzString::from(s))
1047 .dom();
1048 let title = find_class(&dom, "__azul-native-modal-title");
1049
1050 if s.is_empty() {
1051 assert!(title.is_none(), "an empty title must emit no title node");
1052 } else {
1053 let title = title.expect("a non-empty title must emit a title node");
1054 assert_eq!(
1055 text_of(title),
1056 Some(s),
1057 "the title node must carry the string verbatim"
1058 );
1059 }
1060 }
1061 let zwsp = Modal::create(Dom::create_div())
1064 .with_title(AzString::from("\u{200B}"))
1065 .dom();
1066 assert!(find_class(&zwsp, "__azul-native-modal-title").is_some());
1067 assert_eq!(panel_kids(&zwsp).len(), 3, "close + (invisible) title + content");
1068 }
1069
1070 #[test]
1075 fn set_content_replaces_and_last_write_wins() {
1076 let a = Dom::create_div().with_child(Dom::create_text("a"));
1077 let b = Dom::create_text("b");
1078
1079 let mut m = Modal::create(a.clone());
1080 assert_eq!(m.content, a);
1081 m.set_content(b.clone());
1082 assert_eq!(m.content, b, "content must be replaced, not merged");
1083 assert_eq!(node_count(&m.content), 1);
1084 }
1085
1086 #[test]
1087 fn with_content_matches_set_content_and_keeps_everything_else() {
1088 let content = nested_content(32);
1089
1090 let built = Modal::create(Dom::create_div())
1091 .with_title(AzString::from("t"))
1092 .with_open(true)
1093 .with_close_button(false)
1094 .with_content(content.clone());
1095
1096 let mut mutated = Modal::create(Dom::create_div())
1097 .with_title(AzString::from("t"))
1098 .with_open(true)
1099 .with_close_button(false);
1100 mutated.set_content(content.clone());
1101
1102 assert_eq!(built, mutated);
1103 assert_eq!(built.content, content);
1104 assert_eq!(built.title.as_str(), "t");
1105 assert!(built.modal_state.inner.open);
1106 assert!(!built.show_close_button);
1107 }
1108
1109 #[test]
1110 fn content_reaches_the_dom_under_the_content_wrapper() {
1111 let content = nested_content(64);
1112 let dom = Modal::create(content.clone()).dom();
1113
1114 let wrapper = find_class(&dom, "__azul-native-modal-content")
1115 .expect("the content wrapper must exist");
1116 let kids = wrapper.children.as_ref();
1117 assert_eq!(kids.len(), 1, "the wrapper holds exactly the user content");
1118 assert_eq!(kids[0], content, "the content must be handed through untouched");
1119 }
1120
1121 #[test]
1126 fn set_open_flips_the_state_and_the_backdrop_display_together() {
1127 let mut m = Modal::create(Dom::create_div());
1128
1129 m.set_open(true);
1130 assert!(m.modal_state.inner.open);
1131 assert_eq!(display_of(&m.backdrop_style), Some(LayoutDisplay::Flex));
1132
1133 m.set_open(false);
1134 assert!(!m.modal_state.inner.open);
1135 assert_eq!(display_of(&m.backdrop_style), Some(LayoutDisplay::None));
1136 }
1137
1138 #[test]
1139 fn set_open_rebuilds_rather_than_appends() {
1140 let mut m = Modal::create(Dom::create_div());
1143 let len = m.backdrop_style.as_ref().len();
1144
1145 for open in [true, true, false, false, true] {
1146 m.set_open(open);
1147 assert_eq!(
1148 m.backdrop_style.as_ref().len(),
1149 len,
1150 "set_open must rebuild the style, not extend it"
1151 );
1152 let displays: Vec<_> = m
1153 .backdrop_style
1154 .as_ref()
1155 .iter()
1156 .filter(|p| matches!(p.property, CssProperty::Display(_)))
1157 .collect();
1158 assert_eq!(displays.len(), 1, "exactly one `display` may be declared");
1159 }
1160 assert!(m.modal_state.inner.open, "the last write must win");
1161 }
1162
1163 #[test]
1164 fn set_open_is_idempotent() {
1165 let mut once = Modal::create(Dom::create_div());
1166 once.set_open(true);
1167 let mut twice = Modal::create(Dom::create_div());
1168 twice.set_open(true);
1169 twice.set_open(true);
1170 assert_eq!(once, twice);
1171 }
1172
1173 #[test]
1174 fn with_open_matches_set_open_and_keeps_the_other_fields() {
1175 for open in [true, false] {
1176 let built = Modal::create(Dom::create_div())
1177 .with_title(AzString::from("t"))
1178 .with_open(open);
1179 let mut mutated = Modal::create(Dom::create_div()).with_title(AzString::from("t"));
1180 mutated.set_open(open);
1181 assert_eq!(built, mutated);
1182 assert_eq!(built.title.as_str(), "t");
1183 assert!(built.show_close_button);
1184 }
1185 }
1186
1187 #[test]
1188 fn open_state_reaches_the_rendered_backdrop() {
1189 for open in [true, false] {
1190 let dom = Modal::create(Dom::create_div()).with_open(open).dom();
1191 assert_eq!(
1192 inline_display(&dom),
1193 Some(if open { LayoutDisplay::Flex } else { LayoutDisplay::None }),
1194 "open={open}: the backdrop's inline display must match"
1195 );
1196 assert!(has_class(&dom, "__azul-native-modal"));
1197 }
1198 }
1199
1200 #[test]
1205 fn set_close_button_last_write_wins_and_touches_nothing_else() {
1206 let mut m = Modal::create(Dom::create_div())
1207 .with_title(AzString::from("t"))
1208 .with_open(true);
1209 let before = m.backdrop_style.clone();
1210
1211 for show in [false, true, false] {
1212 m.set_close_button(show);
1213 assert_eq!(m.show_close_button, show);
1214 }
1215 assert!(!m.show_close_button);
1216 assert_eq!(m.title.as_str(), "t");
1217 assert!(m.modal_state.inner.open);
1218 assert_eq!(m.backdrop_style, before);
1219 }
1220
1221 #[test]
1222 fn with_close_button_matches_set_close_button() {
1223 for show in [true, false] {
1224 let built = Modal::create(Dom::create_div()).with_close_button(show);
1225 let mut mutated = Modal::create(Dom::create_div());
1226 mutated.set_close_button(show);
1227 assert_eq!(built, mutated);
1228 }
1229 }
1230
1231 #[test]
1232 fn hiding_the_close_button_leaves_the_modal_with_no_way_to_close_itself() {
1233 let dom = Modal::create(Dom::create_div())
1236 .with_close_button(false)
1237 .with_title(AzString::from("stuck"))
1238 .dom();
1239
1240 assert!(find_class(&dom, "__azul-native-modal-close").is_none());
1241 assert_eq!(count_callbacks(&dom), 0, "no handler is wired anywhere");
1242 assert_eq!(panel_kids(&dom).len(), 2, "panel is [title, content] only");
1243 }
1244
1245 #[test]
1250 fn set_on_close_stores_the_callback_and_replaces_rather_than_appends() {
1251 let mut m = Modal::create(Dom::create_div());
1252 assert!(m.modal_state.on_close.is_none());
1253
1254 let first = RefAny::new(1u32);
1255 m.set_on_close(first.clone(), close_cb(record_close));
1256 assert!(m.modal_state.on_close.is_some());
1257 assert_eq!(
1258 m.modal_state.on_close.as_ref().unwrap().callback,
1259 close_cb(record_close)
1260 );
1261
1262 let second = RefAny::new(2u32);
1263 m.set_on_close(second.clone(), close_cb(close_do_nothing));
1264 let stored = m.modal_state.on_close.as_ref().expect("still set");
1265 assert_eq!(stored.callback, close_cb(close_do_nothing), "last write wins");
1266 assert_eq!(stored.refany, second, "the payload is replaced too");
1267 assert_ne!(stored.refany, first);
1268 }
1269
1270 #[test]
1271 fn with_on_close_matches_set_on_close_and_keeps_everything_else() {
1272 let data = RefAny::new(7u64);
1273
1274 let built = Modal::create(Dom::create_div())
1275 .with_title(AzString::from("t"))
1276 .with_open(true)
1277 .with_on_close(data.clone(), close_cb(record_close));
1278
1279 let mut mutated = Modal::create(Dom::create_div())
1280 .with_title(AzString::from("t"))
1281 .with_open(true);
1282 mutated.set_on_close(data.clone(), close_cb(record_close));
1283
1284 assert_eq!(built, mutated);
1285 assert_eq!(built.title.as_str(), "t");
1286 assert!(built.modal_state.inner.open);
1287 assert!(built.show_close_button);
1288 }
1289
1290 #[test]
1291 fn set_on_close_does_not_switch_the_close_button_on() {
1292 let data = RefAny::new(CloseLog { calls: Vec::new() });
1296 let m = Modal::create(Dom::create_div())
1297 .with_close_button(false)
1298 .with_on_close(data.clone(), close_cb(record_close));
1299
1300 assert!(m.modal_state.on_close.is_some(), "the callback is stored");
1301 assert!(!m.show_close_button, "but the button stays off");
1302 assert_eq!(count_callbacks(&m.dom()), 0, "so nothing is wired into the DOM");
1303 }
1304
1305 #[test]
1310 fn swap_with_default_returns_the_original_and_resets_self() {
1311 let content = Dom::create_div().with_child(Dom::create_text("body"));
1312 let mut m = Modal::create(content.clone())
1313 .with_title(AzString::from("Title"))
1314 .with_open(true)
1315 .with_close_button(false);
1316
1317 let old = m.swap_with_default();
1318
1319 assert_eq!(old.title.as_str(), "Title");
1320 assert_eq!(old.content, content);
1321 assert!(old.modal_state.inner.open);
1322 assert!(!old.show_close_button);
1323
1324 assert_eq!(m, Modal::default(), "self must be a pristine modal");
1325 assert_eq!(m.title.as_str(), "");
1326 assert!(!m.modal_state.inner.open);
1327 assert!(m.show_close_button);
1328 assert_eq!(display_of(&m.backdrop_style), Some(LayoutDisplay::None));
1329 }
1330
1331 #[test]
1332 fn swap_with_default_is_stable_when_repeated() {
1333 let mut m = Modal::create(Dom::create_div()).with_title(AzString::from("t"));
1334 let _first = m.swap_with_default();
1335 let second = m.swap_with_default();
1336 assert_eq!(second, Modal::default());
1337 assert_eq!(m, Modal::default());
1338 }
1339
1340 #[test]
1341 fn swap_with_default_moves_the_callback_out_of_self() {
1342 let data = RefAny::new(0u8);
1343 let mut m = Modal::create(Dom::create_div())
1344 .with_on_close(data.clone(), close_cb(record_close));
1345
1346 let old = m.swap_with_default();
1347
1348 assert!(old.modal_state.on_close.is_some(), "the callback moves out");
1349 assert!(m.modal_state.on_close.is_none(), "and must not stay behind");
1350 }
1351
1352 #[test]
1357 fn dom_shape_is_backdrop_panel_close_content() {
1358 let dom = Modal::create(Dom::create_div()).dom();
1359
1360 assert!(has_class(&dom, "__azul-native-modal"));
1361 let kids = panel_kids(&dom);
1362 assert_eq!(kids.len(), 2, "no title -> [close, content]");
1363 assert!(has_class(&kids[0], "__azul-native-modal-close"));
1364 assert!(has_class(&kids[1], "__azul-native-modal-content"));
1365 assert_eq!(node_count(&dom), 5);
1366 }
1367
1368 #[test]
1369 fn dom_with_a_title_puts_the_close_button_first() {
1370 let dom = Modal::create(Dom::create_div())
1373 .with_title(AzString::from("Title"))
1374 .dom();
1375
1376 let kids = panel_kids(&dom);
1377 assert_eq!(kids.len(), 3);
1378 assert!(has_class(&kids[0], "__azul-native-modal-close"));
1379 assert!(has_class(&kids[1], "__azul-native-modal-title"));
1380 assert!(has_class(&kids[2], "__azul-native-modal-content"));
1381 assert_eq!(text_of(&kids[1]), Some("Title"));
1382 assert_eq!(node_count(&dom), 6);
1383 }
1384
1385 #[test]
1386 fn dom_close_button_is_a_focusable_multiplication_sign_with_one_mouseup_handler() {
1387 let dom = Modal::create(Dom::create_div()).dom();
1388 let close = find_class(&dom, "__azul-native-modal-close").expect("close node");
1389
1390 assert_eq!(
1391 text_of(close),
1392 Some("\u{00D7}"),
1393 "the glyph must be U+00D7 MULTIPLICATION SIGN, not ASCII 'x'"
1394 );
1395 assert_eq!(close.root.get_tab_index(), Some(TabIndex::Auto));
1396
1397 let cbs = close.root.get_callbacks();
1398 assert_eq!(cbs.as_ref().len(), 1, "exactly one handler on the 'x'");
1399 let entry = &cbs.as_ref()[0];
1400 assert_eq!(
1401 entry.event,
1402 EventFilter::Hover(HoverEventFilter::MouseUp),
1403 "the modal closes on mouse-up, not mouse-down"
1404 );
1405 assert_eq!(entry.callback.cb, on_modal_close as usize);
1406 assert_eq!(count_callbacks(&dom), 1, "and nowhere else in the tree");
1407 }
1408
1409 #[test]
1410 fn dom_hands_a_snapshot_of_the_modal_state_to_the_close_button() {
1411 for open in [true, false] {
1412 let dom = Modal::create(Dom::create_div()).with_open(open).dom();
1413 let close = find_class(&dom, "__azul-native-modal-close").expect("close node");
1414 let mut payload = close.root.get_callbacks().as_ref()[0].refany.clone();
1415 assert_eq!(
1416 wrapper_open(&mut payload),
1417 open,
1418 "the payload must carry the open state at build time"
1419 );
1420 }
1421 }
1422
1423 #[test]
1424 fn dom_payloads_of_two_clones_are_independent() {
1425 let m = Modal::create(Dom::create_div()).with_open(true);
1428 let a = m.clone().dom();
1429 let b = m.dom();
1430
1431 let mut pa = find_class(&a, "__azul-native-modal-close")
1432 .expect("close")
1433 .root
1434 .get_callbacks()
1435 .as_ref()[0]
1436 .refany
1437 .clone();
1438 let mut pb = find_class(&b, "__azul-native-modal-close")
1439 .expect("close")
1440 .root
1441 .get_callbacks()
1442 .as_ref()[0]
1443 .refany
1444 .clone();
1445
1446 assert_ne!(pa, pb, "two renders must not share one RefAny allocation");
1447 let (_update, _changes) = run_close(Some(StyledDom::create_from_dom(a)), 2, pa.clone());
1448 assert!(!wrapper_open(&mut pa));
1449 assert!(wrapper_open(&mut pb), "the other render must be untouched");
1450 }
1451
1452 #[test]
1453 fn dom_applies_the_static_styles_verbatim() {
1454 let dom = Modal::create(Dom::create_div())
1455 .with_title(AzString::from("t"))
1456 .dom();
1457 let kids = panel_kids(&dom);
1458
1459 let want = |s: &[CssPropertyWithConditions]| {
1460 s.iter().map(|p| p.property.clone()).collect::<Vec<_>>()
1461 };
1462 assert_eq!(inline_props(panel(&dom)), want(MODAL_PANEL_STYLE));
1463 assert_eq!(inline_props(&kids[0]), want(MODAL_CLOSE_STYLE));
1464 assert_eq!(inline_props(&kids[1]), want(MODAL_TITLE_STYLE));
1465 assert_eq!(inline_props(&kids[2]), want(MODAL_CONTENT_STYLE));
1466 }
1467
1468 #[test]
1469 fn dom_backdrop_style_is_exactly_the_builders_backdrop_style() {
1470 for open in [true, false] {
1471 let m = Modal::create(Dom::create_div()).with_open(open);
1472 let expected = style_props(&m.backdrop_style);
1473 assert_eq!(inline_props(&m.dom()), expected);
1474 }
1475 }
1476
1477 #[test]
1478 fn panel_geometry_matches_the_documented_constants() {
1479 let props = style_props(&CssPropertyWithConditionsVec::from_const_slice(
1480 MODAL_PANEL_STYLE,
1481 ));
1482 for want in [
1483 CssProperty::const_min_width(LayoutMinWidth::const_px(PANEL_MIN_WIDTH)),
1484 CssProperty::const_max_width(LayoutMaxWidth::const_px(PANEL_MAX_WIDTH)),
1485 CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(
1486 PANEL_RADIUS,
1487 )),
1488 CssProperty::const_position(LayoutPosition::Relative),
1489 CssProperty::const_display(LayoutDisplay::Flex),
1490 ] {
1491 assert!(props.contains(&want), "panel style is missing {want:?}");
1492 }
1493 let (min, max) = (PANEL_MIN_WIDTH, PANEL_MAX_WIDTH);
1494 assert!(min <= max, "min-width {min} must not exceed max-width {max}");
1495 }
1496
1497 #[test]
1498 fn every_static_style_declares_each_property_at_most_once() {
1499 for (name, style) in [
1500 ("panel", MODAL_PANEL_STYLE),
1501 ("title", MODAL_TITLE_STYLE),
1502 ("close", MODAL_CLOSE_STYLE),
1503 ("content", MODAL_CONTENT_STYLE),
1504 ] {
1505 let types = property_types(&CssPropertyWithConditionsVec::from_const_slice(style));
1506 for (i, a) in types.iter().enumerate() {
1507 for b in &types[i + 1..] {
1508 assert_ne!(a, b, "{name}: the same property is declared twice");
1509 }
1510 }
1511 for p in style {
1512 assert!(
1513 p.apply_if.as_ref().is_empty(),
1514 "{name}: {:?} must be unconditional",
1515 p.property
1516 );
1517 }
1518 }
1519 }
1520
1521 #[test]
1522 fn dom_survives_extreme_content_and_titles() {
1523 let content = Dom::create_div().with_children(DomVec::from_vec(
1525 (0..2_000).map(|_| nested_content(4)).collect::<Vec<_>>(),
1526 ));
1527 let dom = Modal::create(content)
1528 .with_title(AzString::from("\u{202E}\u{1F469}\u{200D}\u{1F467}\0"))
1529 .with_open(true)
1530 .dom();
1531
1532 assert_eq!(node_count(&dom), 6 + 2_000 * 5);
1534 assert_eq!(count_callbacks(&dom), 1);
1535 }
1536
1537 #[test]
1538 fn from_modal_for_dom_equals_dom() {
1539 let m = Modal::create(Dom::create_div())
1542 .with_title(AzString::from("t"))
1543 .with_close_button(false)
1544 .with_open(true);
1545 assert_eq!(Dom::from(m.clone()), m.dom());
1546 }
1547
1548 #[test]
1549 fn two_renders_with_a_close_button_differ_only_in_the_refany_identity() {
1550 let m = Modal::create(Dom::create_div());
1554 assert_ne!(m.clone().dom(), m.clone().dom());
1555
1556 let q = m.with_close_button(false);
1557 assert_eq!(q.clone().dom(), q.dom());
1558 }
1559
1560 #[test]
1565 fn close_hides_the_backdrop_and_flips_open() {
1566 let mut data = RefAny::new(ModalStateWrapper {
1567 inner: ModalState { open: true },
1568 on_close: OptionModalOnClose::None,
1569 });
1570
1571 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data.clone());
1574
1575 assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
1576 assert_eq!(
1577 display_writes(&changes),
1578 alloc::vec![(0usize, LayoutDisplay::None)],
1579 "the *backdrop* (not the panel or the button) must be hidden"
1580 );
1581 assert!(!wrapper_open(&mut data), "state must flip to closed");
1582 }
1583
1584 #[test]
1585 fn close_invokes_the_user_callback_with_the_already_flipped_state() {
1586 let mut log = RefAny::new(CloseLog { calls: Vec::new() });
1587 let mut data = RefAny::new(ModalStateWrapper {
1588 inner: ModalState { open: true },
1589 on_close: Some(ModalOnClose {
1590 callback: close_cb(record_close),
1591 refany: log.clone(),
1592 })
1593 .into(),
1594 });
1595
1596 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data.clone());
1597
1598 assert_eq!(update, Update::RefreshDom, "the user callback's Update is returned");
1599 assert_eq!(
1600 log_calls(&mut log),
1601 alloc::vec![false],
1602 "the callback must see `open == false` (already closed)"
1603 );
1604 assert!(!wrapper_open(&mut data));
1605 assert_eq!(
1606 display_writes(&changes),
1607 alloc::vec![(0usize, LayoutDisplay::None)],
1608 "the backdrop is hidden even after a user callback ran"
1609 );
1610 }
1611
1612 #[test]
1613 fn close_hides_the_backdrop_even_when_the_user_callback_does_nothing() {
1614 let data = RefAny::new(ModalStateWrapper {
1615 inner: ModalState { open: true },
1616 on_close: Some(ModalOnClose {
1617 callback: close_cb(close_do_nothing),
1618 refany: RefAny::new(0u8),
1619 })
1620 .into(),
1621 });
1622
1623 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data);
1624
1625 assert_eq!(update, Update::DoNothing);
1626 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
1627 }
1628
1629 #[test]
1630 fn close_twice_is_idempotent() {
1631 let mut log = RefAny::new(CloseLog { calls: Vec::new() });
1632 let mut data = RefAny::new(ModalStateWrapper {
1633 inner: ModalState { open: true },
1634 on_close: Some(ModalOnClose {
1635 callback: close_cb(record_close),
1636 refany: log.clone(),
1637 })
1638 .into(),
1639 });
1640
1641 for _ in 0..2 {
1642 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data.clone());
1643 assert_eq!(update, Update::RefreshDom);
1644 assert_eq!(display_writes(&changes), alloc::vec![(0usize, LayoutDisplay::None)]);
1645 }
1646
1647 assert!(!wrapper_open(&mut data), "a second close must not re-open");
1648 assert_eq!(
1649 log_calls(&mut log),
1650 alloc::vec![false, false],
1651 "each click fires the callback exactly once, always with open == false"
1652 );
1653 }
1654
1655 #[test]
1656 fn close_on_the_panel_is_a_noop_because_the_backdrop_has_no_parent() {
1657 let mut data = RefAny::new(ModalStateWrapper {
1660 inner: ModalState { open: true },
1661 on_close: OptionModalOnClose::None,
1662 });
1663
1664 let (update, changes) = run_close(Some(modal_styled_dom()), 1, data.clone());
1665
1666 assert_eq!(update, Update::DoNothing);
1667 assert!(changes.is_empty(), "nothing may be restyled without a grandparent");
1668 assert!(wrapper_open(&mut data), "state must not flip");
1669 }
1670
1671 #[test]
1672 fn close_on_the_root_is_a_noop() {
1673 let mut data = RefAny::new(ModalStateWrapper {
1674 inner: ModalState { open: true },
1675 on_close: OptionModalOnClose::None,
1676 });
1677
1678 let (update, changes) = run_close(Some(modal_styled_dom()), 0, data.clone());
1679
1680 assert_eq!(update, Update::DoNothing);
1681 assert!(changes.is_empty());
1682 assert!(wrapper_open(&mut data));
1683 }
1684
1685 #[test]
1686 fn close_with_a_stale_hit_node_is_a_noop() {
1687 let mut data = RefAny::new(ModalStateWrapper {
1689 inner: ModalState { open: true },
1690 on_close: OptionModalOnClose::None,
1691 });
1692
1693 let (update, changes) = run_close(Some(modal_styled_dom()), 999, data.clone());
1694
1695 assert_eq!(update, Update::DoNothing);
1696 assert!(changes.is_empty());
1697 assert!(wrapper_open(&mut data));
1698 }
1699
1700 #[test]
1701 fn close_without_any_layout_result_is_a_noop() {
1702 let mut data = RefAny::new(ModalStateWrapper {
1703 inner: ModalState { open: true },
1704 on_close: OptionModalOnClose::None,
1705 });
1706
1707 let (update, changes) = run_close(None, 2, data.clone());
1708
1709 assert_eq!(update, Update::DoNothing);
1710 assert!(changes.is_empty());
1711 assert!(wrapper_open(&mut data), "state must not flip");
1712 }
1713
1714 #[test]
1715 fn close_with_a_foreign_payload_is_a_noop() {
1716 let data = RefAny::new(0xdead_beef_u64);
1719
1720 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data);
1721
1722 assert_eq!(update, Update::DoNothing);
1723 assert!(changes.is_empty(), "a foreign payload must not hide the backdrop");
1724 }
1725
1726 #[test]
1727 fn close_with_a_plain_modal_state_payload_is_a_noop() {
1728 let data = RefAny::new(ModalState { open: true });
1731
1732 let (update, changes) = run_close(Some(modal_styled_dom()), 2, data);
1733
1734 assert_eq!(update, Update::DoNothing);
1735 assert!(changes.is_empty());
1736 }
1737
1738 #[test]
1739 fn close_hides_the_grandparent_whatever_it_is() {
1740 let deep = StyledDom::create_from_dom(nested_content(3));
1744 assert_eq!(deep.node_hierarchy.as_ref().len(), 4);
1745
1746 let data = RefAny::new(ModalStateWrapper {
1747 inner: ModalState { open: true },
1748 on_close: OptionModalOnClose::None,
1749 });
1750 let (update, changes) = run_close(Some(deep), 3, data);
1751
1752 assert_eq!(update, Update::DoNothing);
1753 assert_eq!(
1754 display_writes(&changes),
1755 alloc::vec![(1usize, LayoutDisplay::None)],
1756 "node 1 (the grandparent), not the root, gets hidden"
1757 );
1758 }
1759
1760 #[test]
1761 fn close_end_to_end_through_the_real_dom_payload() {
1762 let modal = Modal::create(Dom::create_text("body"))
1765 .with_title(AzString::from("Title"))
1766 .with_open(true);
1767 let dom = modal.dom();
1768
1769 let close = find_class(&dom, "__azul-native-modal-close").expect("close node");
1770 let entry = &close.root.get_callbacks().as_ref()[0];
1771 assert_eq!(entry.callback.cb, on_modal_close as usize);
1772 let mut payload = entry.refany.clone();
1773 assert!(wrapper_open(&mut payload), "the snapshot starts open");
1774
1775 let styled = StyledDom::create_from_dom(dom);
1776 assert_eq!(styled.node_hierarchy.as_ref().len(), 6);
1778
1779 let (update, changes) = run_close(Some(styled), 2, payload.clone());
1780
1781 assert_eq!(update, Update::DoNothing);
1782 assert_eq!(
1783 display_writes(&changes),
1784 alloc::vec![(0usize, LayoutDisplay::None)]
1785 );
1786 assert!(
1787 !wrapper_open(&mut payload),
1788 "the state living in the DOM must be flipped to closed"
1789 );
1790 }
1791
1792 #[test]
1793 fn close_end_to_end_invokes_a_user_callback_wired_through_the_builder() {
1794 let mut log = RefAny::new(CloseLog { calls: Vec::new() });
1795 let dom = Modal::create(Dom::create_div())
1796 .with_open(true)
1797 .with_on_close(log.clone(), close_cb(record_close))
1798 .dom();
1799
1800 let payload = find_class(&dom, "__azul-native-modal-close")
1801 .expect("close node")
1802 .root
1803 .get_callbacks()
1804 .as_ref()[0]
1805 .refany
1806 .clone();
1807
1808 let (update, changes) = run_close(Some(StyledDom::create_from_dom(dom)), 2, payload);
1809
1810 assert_eq!(update, Update::RefreshDom);
1811 assert_eq!(log_calls(&mut log), alloc::vec![false]);
1812 assert_eq!(
1813 display_writes(&changes),
1814 alloc::vec![(0usize, LayoutDisplay::None)]
1815 );
1816 }
1817
1818 #[test]
1823 fn modal_state_defaults_to_closed_with_no_callback() {
1824 assert!(!ModalState::default().open);
1825 let w = ModalStateWrapper::default();
1826 assert!(!w.inner.open);
1827 assert!(w.on_close.is_none());
1828 assert_eq!(w, ModalStateWrapper::default());
1829 }
1830
1831 #[test]
1832 fn modal_state_is_copy_and_value_comparable() {
1833 let a = ModalState { open: true };
1834 let b = a; assert_eq!(a, b);
1836 assert_ne!(a, ModalState { open: false });
1837 }
1838}