1use azul_core::{
30 callbacks::{CoreCallbackData, Update},
31 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
32 refany::RefAny,
33};
34use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
35use azul_css::{
36 props::{
37 basic::{color::ColorU, *},
38 layout::{LayoutDisplay, LayoutPosition, LayoutFlexGrow, LayoutTop, LayoutLeft, LayoutMinWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
39 property::{CssProperty, *},
40 style::{StyleCursor, StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius},
41 },
42 impl_option_inner, AzString,
43};
44
45use crate::callbacks::{Callback, CallbackInfo};
46
47static POPOVER_WRAPPER_CLASS: &[IdOrClass] =
48 &[Class(AzString::from_const_str("__azul-native-popover"))];
49static POPOVER_TRIGGER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
50 "__azul-native-popover-trigger",
51))];
52static POPOVER_CONTENT_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
53 "__azul-native-popover-content",
54))];
55
56const CONTENT_OFFSET_Y: isize = 32;
60const CONTENT_MIN_WIDTH: isize = 160;
62const CONTENT_RADIUS: isize = 6;
63
64const CONTENT_BG_COLOR: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
67const CONTENT_BORDER_COLOR: ColorU = ColorU { r: 204, g: 204, b: 204, a: 255 };
69
70pub type PopoverOnToggleCallbackType = extern "C" fn(RefAny, CallbackInfo, PopoverState) -> Update;
73impl_widget_callback!(
74 PopoverOnToggle,
75 OptionPopoverOnToggle,
76 PopoverOnToggleCallback,
77 PopoverOnToggleCallbackType
78);
79
80azul_core::impl_managed_callback! {
81 wrapper: PopoverOnToggleCallback,
82 info_ty: CallbackInfo,
83 return_ty: Update,
84 default_ret: Update::DoNothing,
85 invoker_static: POPOVER_ON_TOGGLE_INVOKER,
86 invoker_ty: AzPopoverOnToggleCallbackInvoker,
87 thunk_fn: az_popover_on_toggle_callback_thunk,
88 setter_fn: AzApp_setPopoverOnToggleCallbackInvoker,
89 from_handle_fn: AzPopoverOnToggleCallback_createFromHostHandle,
90 extra_args: [ state: PopoverState ],
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95#[repr(C)]
96pub struct Popover {
97 pub popover_state: PopoverStateWrapper,
99 pub anchor: Dom,
101 pub content: Dom,
103 pub wrapper_style: CssPropertyWithConditionsVec,
105 pub content_style: CssPropertyWithConditionsVec,
107}
108
109#[derive(Debug, Default, Clone, PartialEq, Eq)]
110#[repr(C)]
111pub struct PopoverStateWrapper {
112 pub inner: PopoverState,
114 pub on_toggle: OptionPopoverOnToggle,
116}
117
118#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
120#[repr(C)]
121pub struct PopoverState {
122 pub open: bool,
124}
125
126static POPOVER_WRAPPER_STYLE: &[CssPropertyWithConditions] = &[
129 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
130 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
131 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
132];
133
134static POPOVER_TRIGGER_STYLE: &[CssPropertyWithConditions] = &[
136 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
137 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
138 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
139];
140
141fn build_content_style(open: bool) -> CssPropertyWithConditionsVec {
146 let display = if open {
147 LayoutDisplay::Block
148 } else {
149 LayoutDisplay::None
150 };
151 let bg_vec = StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(
152 CONTENT_BG_COLOR
153 )]);
154 CssPropertyWithConditionsVec::from_vec(alloc::vec![
155 CssPropertyWithConditions::simple(CssProperty::const_display(display)),
156 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
157 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(
158 CONTENT_OFFSET_Y,
159 ))),
160 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
161 CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
162 CONTENT_MIN_WIDTH,
163 ))),
164 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
166 8,
167 ))),
168 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
169 LayoutPaddingBottom::const_px(8),
170 )),
171 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
172 LayoutPaddingLeft::const_px(8),
173 )),
174 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
175 LayoutPaddingRight::const_px(8),
176 )),
177 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
179 LayoutBorderTopWidth::const_px(1),
180 )),
181 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
182 LayoutBorderBottomWidth::const_px(1),
183 )),
184 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
185 LayoutBorderLeftWidth::const_px(1),
186 )),
187 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
188 LayoutBorderRightWidth::const_px(1),
189 )),
190 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
191 inner: BorderStyle::Solid,
192 })),
193 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
194 StyleBorderBottomStyle {
195 inner: BorderStyle::Solid,
196 },
197 )),
198 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
199 inner: BorderStyle::Solid,
200 })),
201 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
202 StyleBorderRightStyle {
203 inner: BorderStyle::Solid,
204 },
205 )),
206 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
207 inner: CONTENT_BORDER_COLOR,
208 })),
209 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
210 StyleBorderBottomColor {
211 inner: CONTENT_BORDER_COLOR,
212 },
213 )),
214 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
215 inner: CONTENT_BORDER_COLOR,
216 })),
217 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
218 StyleBorderRightColor {
219 inner: CONTENT_BORDER_COLOR,
220 },
221 )),
222 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
224 StyleBorderTopLeftRadius::const_px(CONTENT_RADIUS),
225 )),
226 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
227 StyleBorderTopRightRadius::const_px(CONTENT_RADIUS),
228 )),
229 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
230 StyleBorderBottomLeftRadius::const_px(CONTENT_RADIUS),
231 )),
232 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
233 StyleBorderBottomRightRadius::const_px(CONTENT_RADIUS),
234 )),
235 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
236 ])
237}
238
239impl Popover {
240 #[must_use] pub fn new(anchor: Dom, content: Dom) -> Self {
243 Self {
244 popover_state: PopoverStateWrapper::default(),
245 anchor,
246 content,
247 wrapper_style: CssPropertyWithConditionsVec::from_const_slice(POPOVER_WRAPPER_STYLE),
248 content_style: build_content_style(false),
249 }
250 }
251
252 #[inline]
254 pub fn set_open(&mut self, open: bool) {
255 self.popover_state.inner.open = open;
256 self.content_style = build_content_style(open);
257 }
258
259 #[inline]
261 #[must_use] pub fn with_open(mut self, open: bool) -> Self {
262 self.set_open(open);
263 self
264 }
265
266 #[inline]
268 pub fn set_on_toggle<C: Into<PopoverOnToggleCallback>>(&mut self, data: RefAny, on_toggle: C) {
269 self.popover_state.on_toggle = Some(PopoverOnToggle {
270 callback: on_toggle.into(),
271 refany: data,
272 })
273 .into();
274 }
275
276 #[inline]
278 #[must_use] pub fn with_on_toggle<C: Into<PopoverOnToggleCallback>>(
279 mut self,
280 data: RefAny,
281 on_toggle: C,
282 ) -> Self {
283 self.set_on_toggle(data, on_toggle);
284 self
285 }
286
287 #[inline]
289 #[must_use] pub fn swap_with_default(&mut self) -> Self {
290 let mut s = Self::new(Dom::default(), Dom::default());
291 core::mem::swap(&mut s, self);
292 s
293 }
294
295 #[must_use] pub fn dom(self) -> Dom {
298 use azul_core::{callbacks::CoreCallback, dom::{EventFilter, HoverEventFilter}, refany::OptionRefAny};
299
300 let trigger = Dom::create_div()
306 .with_ids_and_classes(IdOrClassVec::from_const_slice(POPOVER_TRIGGER_CLASS))
307 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(POPOVER_TRIGGER_STYLE))
308 .with_tab_index(TabIndex::Auto)
309 .with_callbacks(
310 vec![CoreCallbackData {
311 event: EventFilter::Hover(HoverEventFilter::MouseUp),
312 callback: CoreCallback {
313 cb: on_popover_toggle as usize,
314 ctx: OptionRefAny::None,
315 },
316 refany: RefAny::new(self.popover_state),
317 }]
318 .into(),
319 )
320 .with_children(vec![self.anchor].into());
321
322 let content = Dom::create_div()
323 .with_ids_and_classes(IdOrClassVec::from_const_slice(POPOVER_CONTENT_CLASS))
324 .with_css_props(self.content_style)
325 .with_children(vec![self.content].into());
326
327 Dom::create_div()
328 .with_ids_and_classes(IdOrClassVec::from_const_slice(POPOVER_WRAPPER_CLASS))
329 .with_css_props(self.wrapper_style)
330 .with_children(vec![trigger, content].into())
332 }
333}
334
335impl Default for Popover {
336 fn default() -> Self {
337 Self::new(Dom::default(), Dom::default())
338 }
339}
340
341extern "C" fn on_popover_toggle(mut data: RefAny, mut info: CallbackInfo) -> Update {
346 let trigger = info.get_hit_node();
347 let Some(content) = info.get_next_sibling(trigger) else {
348 return Update::DoNothing;
349 };
350
351 let (now_open, result) = {
352 let Some(mut pop) = data.downcast_mut::<PopoverStateWrapper>() else {
353 return Update::DoNothing;
354 };
355 pop.inner.open = !pop.inner.open;
356 let now_open = pop.inner.open;
357 let inner = pop.inner;
358 let pop = &mut *pop;
359 let result = match pop.on_toggle.as_mut() {
360 Some(PopoverOnToggle { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
361 None => Update::DoNothing,
362 };
363 (now_open, result)
364 };
365
366 let display = if now_open {
370 LayoutDisplay::Block
371 } else {
372 LayoutDisplay::None
373 };
374 info.set_css_property(content, CssProperty::const_display(display));
375
376 result
377}
378
379impl From<Popover> for Dom {
380 fn from(p: Popover) -> Self {
381 p.dom()
382 }
383}
384
385#[cfg(test)]
386mod autotest_generated {
387 use std::{
388 collections::{BTreeMap, HashMap},
389 sync::{Arc, Mutex},
390 };
391
392 use azul_core::{
393 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
394 geom::{LogicalRect, OptionLogicalPosition},
395 gl::OptionGlContextPtr,
396 hit_test::ScrollPosition,
397 refany::OptionRefAny,
398 resources::RendererResources,
399 styled_dom::{NodeHierarchyItemId, StyledDom},
400 window::{MonitorVec, RawWindowHandle},
401 };
402 use azul_css::{props::property::CssPropertyType, system::SystemStyle};
403 use rust_fontconfig::FcFontCache;
404
405 use super::*;
406 #[cfg(feature = "icu")]
407 use crate::icu::IcuLocalizerHandle;
408 use crate::{
409 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
410 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
411 window::{DomLayoutResult, LayoutWindow},
412 window_state::FullWindowState,
413 };
414
415 fn has_class(node: &Dom, name: &str) -> bool {
421 node.root
422 .get_ids_and_classes()
423 .as_ref()
424 .iter()
425 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == name))
426 }
427
428 fn text_of(node: &Dom) -> Option<&str> {
430 match node.root.get_node_type() {
431 NodeType::Text(s) => Some(s.as_ref().as_str()),
432 _ => None,
433 }
434 }
435
436 fn inline_display(node: &Dom) -> Option<LayoutDisplay> {
438 node.root
439 .style
440 .iter_inline_properties()
441 .find_map(|(p, _)| match p {
442 CssProperty::Display(v) => v.get_property().copied(),
443 _ => None,
444 })
445 }
446
447 fn prop_types(style: &CssPropertyWithConditionsVec) -> Vec<CssPropertyType> {
449 style
450 .as_ref()
451 .iter()
452 .map(|p| p.property.get_type())
453 .collect()
454 }
455
456 fn displays_in(style: &CssPropertyWithConditionsVec) -> Vec<LayoutDisplay> {
459 style
460 .as_ref()
461 .iter()
462 .filter_map(|p| match &p.property {
463 CssProperty::Display(v) => v.get_property().copied(),
464 _ => None,
465 })
466 .collect()
467 }
468
469 fn display_ty() -> CssPropertyType {
471 CssProperty::const_display(LayoutDisplay::None).get_type()
472 }
473
474 fn trigger_panel_dom() -> StyledDom {
478 let styled = StyledDom::create_from_dom(
479 Dom::create_div()
480 .with_child(Dom::create_div())
481 .with_child(Dom::create_div()),
482 );
483 assert_eq!(
484 styled.node_hierarchy.as_ref().len(),
485 3,
486 "fixture must flatten to exactly wrapper/trigger/panel"
487 );
488 styled
489 }
490
491 fn index_of_class(styled: &StyledDom, class: &str) -> usize {
493 styled
494 .node_data
495 .as_ref()
496 .iter()
497 .position(|nd| {
498 nd.get_ids_and_classes()
499 .as_ref()
500 .iter()
501 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == class))
502 })
503 .unwrap_or_else(|| panic!("no node with class {class} in the flattened DOM"))
504 }
505
506 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
509 DomLayoutResult {
510 styled_dom,
511 layout_tree: LayoutTree {
512 nodes: Vec::new(),
513 warm: Vec::new(),
514 cold: Vec::new(),
515 root: 0,
516 dom_to_layout: BTreeMap::new(),
517 children_arena: Vec::new(),
518 children_offsets: Vec::new(),
519 subtree_needs_intrinsic: Vec::new(),
520 },
521 calculated_positions: Vec::new(),
522 viewport: LogicalRect::zero(),
523 display_list: DisplayList::default(),
524 scroll_ids: HashMap::new(),
525 scroll_id_to_node_id: HashMap::new(),
526 }
527 }
528
529 fn run_toggle(
533 styled: Option<StyledDom>,
534 hit: usize,
535 data: RefAny,
536 ) -> (Update, Vec<CallbackChange>) {
537 let mut layout_window =
538 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
539 if let Some(sd) = styled {
540 layout_window
541 .layout_results
542 .insert(DomId::ROOT_ID, layout_result(sd));
543 }
544
545 let renderer_resources = RendererResources::default();
546 let previous_window_state: Option<FullWindowState> = None;
547 let current_window_state = FullWindowState::default();
548 let gl_context = OptionGlContextPtr::None;
549 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
550 BTreeMap::new();
551 let window_handle = RawWindowHandle::Unsupported;
552 let system_callbacks = ExternalSystemCallbacks::rust_internal();
553
554 let ref_data = CallbackInfoRefData {
555 layout_window: &layout_window,
556 renderer_resources: &renderer_resources,
557 previous_window_state: &previous_window_state,
558 current_window_state: ¤t_window_state,
559 gl_context: &gl_context,
560 current_scroll_manager: &scroll_states,
561 current_window_handle: &window_handle,
562 system_callbacks: &system_callbacks,
563 system_style: Arc::new(SystemStyle::default()),
564 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
565 #[cfg(feature = "icu")]
566 icu_localizer: IcuLocalizerHandle::default(),
567 ctx: OptionRefAny::None,
568 };
569
570 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
571
572 let info = CallbackInfo::new(
573 &ref_data,
574 &changes,
575 DomNodeId {
576 dom: DomId::ROOT_ID,
577 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
578 },
579 OptionLogicalPosition::None,
580 OptionLogicalPosition::None,
581 );
582
583 let update = on_popover_toggle(data, info);
584 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
585 (update, recorded)
586 }
587
588 fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
590 let mut out = Vec::new();
591 for change in changes {
592 if let CallbackChange::ChangeNodeCssProperties {
593 node_id, properties, ..
594 } = change
595 {
596 for p in properties.as_ref() {
597 if let CssProperty::Display(v) = p {
598 if let Some(d) = v.get_property() {
599 out.push((node_id.index(), *d));
600 }
601 }
602 }
603 }
604 }
605 out
606 }
607
608 fn payload_open(data: &mut RefAny) -> bool {
610 data.downcast_ref::<PopoverStateWrapper>()
611 .expect("payload must still be a PopoverStateWrapper")
612 .inner
613 .open
614 }
615
616 struct ToggleLog {
618 calls: Vec<bool>,
619 }
620
621 extern "C" fn record_toggle(mut data: RefAny, _: CallbackInfo, state: PopoverState) -> Update {
622 if let Some(mut log) = data.downcast_mut::<ToggleLog>() {
623 log.calls.push(state.open);
624 }
625 Update::RefreshDom
626 }
627
628 extern "C" fn toggle_do_nothing(_: RefAny, _: CallbackInfo, _: PopoverState) -> Update {
629 Update::DoNothing
630 }
631
632 fn toggle_cb(f: PopoverOnToggleCallbackType) -> PopoverOnToggleCallback {
633 f.into()
634 }
635
636 #[test]
641 fn content_style_open_and_closed_differ_only_in_display() {
642 let closed = build_content_style(false);
643 let open = build_content_style(true);
644
645 assert_eq!(
646 closed.len(),
647 open.len(),
648 "both states must declare the same props so the runtime display toggle \
649 has everything it needs"
650 );
651 assert_eq!(prop_types(&closed), prop_types(&open));
652
653 let differing: Vec<usize> = closed
654 .as_ref()
655 .iter()
656 .zip(open.as_ref().iter())
657 .enumerate()
658 .filter_map(|(i, (c, o))| (c != o).then_some(i))
659 .collect();
660
661 assert_eq!(
662 differing.len(),
663 1,
664 "exactly one declaration may differ between open and closed"
665 );
666 assert_eq!(
667 closed.as_ref()[differing[0]].property.get_type(),
668 display_ty(),
669 "the only difference must be `display`"
670 );
671 }
672
673 #[test]
674 fn content_style_declares_display_exactly_once_and_correctly() {
675 assert_eq!(
678 displays_in(&build_content_style(false)),
679 alloc::vec![LayoutDisplay::None]
680 );
681 assert_eq!(
682 displays_in(&build_content_style(true)),
683 alloc::vec![LayoutDisplay::Block]
684 );
685 }
686
687 #[test]
688 fn content_style_has_no_duplicate_property_types() {
689 for open in [false, true] {
690 let mut types = prop_types(&build_content_style(open));
691 let declared = types.len();
692 assert!(declared > 0, "the panel style must not be empty");
693 types.sort_unstable();
694 types.dedup();
695 assert_eq!(
696 types.len(),
697 declared,
698 "a duplicated property type would make the later declaration silently win \
699 (open = {open})"
700 );
701 }
702 }
703
704 #[test]
705 fn content_style_is_pure_and_unconditional() {
706 for open in [false, true] {
707 let a = build_content_style(open);
708 let b = build_content_style(open);
709 assert_eq!(a, b, "build_content_style must be a pure function of `open`");
710 assert!(
711 a.as_ref().iter().all(|p| p.apply_if.as_ref().is_empty()),
712 "the panel style must apply unconditionally — a stray condition would \
713 leave the panel unstyled (open = {open})"
714 );
715 }
716 }
717
718 #[test]
719 fn content_style_carries_the_documented_geometry_in_both_states() {
720 let expected = alloc::vec![
724 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
725 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(
726 CONTENT_OFFSET_Y
727 ))),
728 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
729 CssPropertyWithConditions::simple(CssProperty::const_min_width(
730 LayoutMinWidth::const_px(CONTENT_MIN_WIDTH)
731 )),
732 ];
733
734 for open in [false, true] {
735 let style = build_content_style(open);
736 for e in &expected {
737 assert!(
738 style.as_ref().contains(e),
739 "{:?} missing from the {} panel style",
740 e.property.get_type(),
741 if open { "open" } else { "closed" }
742 );
743 }
744 }
745 }
746
747 #[test]
752 fn new_stores_both_doms_and_starts_closed() {
753 let anchor = Dom::create_div().with_child(Dom::create_text("anchor"));
754 let content = Dom::create_text("panel");
755 let pop = Popover::new(anchor.clone(), content.clone());
756
757 assert_eq!(pop.anchor, anchor, "the anchor must be stored verbatim");
758 assert_eq!(pop.content, content, "the content must be stored verbatim");
759 assert!(
760 !pop.popover_state.inner.open,
761 "a fresh popover must start closed"
762 );
763 assert!(
764 pop.popover_state.on_toggle.is_none(),
765 "Popover::new sets no callback"
766 );
767 assert_eq!(
768 pop.content_style,
769 build_content_style(false),
770 "content_style must match the closed state it was constructed with"
771 );
772 assert_eq!(
773 pop.wrapper_style,
774 CssPropertyWithConditionsVec::from_const_slice(POPOVER_WRAPPER_STYLE)
775 );
776 }
777
778 #[test]
779 fn default_equals_new_with_empty_doms() {
780 assert_eq!(
781 Popover::default(),
782 Popover::new(Dom::default(), Dom::default())
783 );
784 assert!(!Popover::default().popover_state.inner.open);
785 }
786
787 #[test]
788 fn new_survives_extreme_doms() {
789 let mut deep = Dom::create_text("leaf");
792 for _ in 0..128 {
793 deep = Dom::create_div().with_child(deep);
794 }
795 let wide_children: Vec<Dom> = (0..2000)
796 .map(|i| Dom::create_text(alloc::format!("{i}")))
797 .collect();
798 let wide = Dom::create_div().with_children(wide_children.clone().into());
799
800 let pop = Popover::new(deep.clone(), wide.clone());
801
802 assert_eq!(pop.anchor, deep);
803 assert_eq!(pop.content, wide);
804 assert_eq!(pop.content.children.as_ref().len(), 2000);
805 assert!(!pop.popover_state.inner.open);
806 }
807
808 #[test]
809 fn new_accepts_the_same_dom_as_anchor_and_content() {
810 let shared = Dom::create_div().with_child(Dom::create_text("x"));
813 let pop = Popover::new(shared.clone(), shared.clone());
814
815 assert_eq!(pop.anchor, shared);
816 assert_eq!(pop.content, shared);
817
818 let dom = pop.dom();
819 let children = dom.children.as_ref();
820 assert_eq!(text_of(&children[0].children.as_ref()[0].children.as_ref()[0]), Some("x"));
821 assert_eq!(text_of(&children[1].children.as_ref()[0].children.as_ref()[0]), Some("x"));
822 }
823
824 #[test]
829 fn set_open_round_trips_state_and_style() {
830 let mut pop = Popover::new(Dom::create_text("a"), Dom::create_text("c"));
831
832 for open in [true, false, false, true, true, false, true] {
835 pop.set_open(open);
836 assert_eq!(pop.popover_state.inner.open, open);
837 assert_eq!(
838 pop.content_style,
839 build_content_style(open),
840 "content_style desynced from the open flag"
841 );
842 assert_eq!(
843 displays_in(&pop.content_style),
844 alloc::vec![if open {
845 LayoutDisplay::Block
846 } else {
847 LayoutDisplay::None
848 }]
849 );
850 }
851
852 assert_eq!(pop.anchor, Dom::create_text("a"));
854 assert_eq!(pop.content, Dom::create_text("c"));
855 }
856
857 #[test]
858 fn with_open_matches_set_open() {
859 for open in [false, true] {
860 let mut mutated = Popover::new(Dom::create_text("a"), Dom::create_text("c"));
861 mutated.set_open(open);
862 let built = Popover::new(Dom::create_text("a"), Dom::create_text("c")).with_open(open);
863 assert_eq!(built, mutated, "builder and setter must agree (open = {open})");
864 }
865 }
866
867 #[test]
868 fn with_open_last_write_wins() {
869 let base = Popover::new(Dom::create_div(), Dom::create_div());
870
871 assert!(
872 !base
873 .clone()
874 .with_open(true)
875 .with_open(false)
876 .popover_state
877 .inner
878 .open
879 );
880 assert!(
881 base.clone()
882 .with_open(false)
883 .with_open(true)
884 .popover_state
885 .inner
886 .open
887 );
888 assert!(
889 base.clone()
890 .with_open(true)
891 .with_open(true)
892 .popover_state
893 .inner
894 .open,
895 "applying the same value twice must be idempotent"
896 );
897
898 assert_eq!(
900 base.with_open(true).with_open(false).content_style,
901 build_content_style(false)
902 );
903 }
904
905 #[test]
910 fn set_on_toggle_last_call_wins() {
911 let mut pop = Popover::default();
912
913 pop.set_on_toggle(RefAny::new(1u8), toggle_cb(toggle_do_nothing));
914 assert!(pop.popover_state.on_toggle.is_some());
915
916 pop.set_on_toggle(RefAny::new(9i64), toggle_cb(record_toggle));
918 let set = pop.popover_state.on_toggle.as_ref().expect("still Some");
919 assert_eq!(set.refany.get_type_id(), RefAny::new(0i64).get_type_id());
920 assert_eq!(set.callback, toggle_cb(record_toggle));
921 assert_ne!(set.callback, toggle_cb(toggle_do_nothing));
922 }
923
924 #[test]
925 fn set_on_toggle_does_not_disturb_state_style_or_doms() {
926 let mut pop = Popover::new(Dom::create_text("a"), Dom::create_text("c")).with_open(true);
927 let style_before = pop.content_style.clone();
928
929 pop.set_on_toggle(RefAny::new(0u8), toggle_cb(toggle_do_nothing));
930
931 assert!(pop.popover_state.inner.open, "open flag must survive");
932 assert_eq!(pop.content_style, style_before, "style must survive");
933 assert_eq!(pop.anchor, Dom::create_text("a"));
934 assert_eq!(pop.content, Dom::create_text("c"));
935 }
936
937 #[test]
938 fn with_on_toggle_matches_set_on_toggle() {
939 let built = Popover::default().with_on_toggle(RefAny::new(7u32), toggle_cb(record_toggle));
940
941 let mut mutated = Popover::default();
942 mutated.set_on_toggle(RefAny::new(7u32), toggle_cb(record_toggle));
943
944 assert!(built.popover_state.on_toggle.is_some());
945 assert_eq!(
946 built.popover_state.on_toggle.as_ref().unwrap().callback,
947 mutated.popover_state.on_toggle.as_ref().unwrap().callback
948 );
949 assert_eq!(built.anchor, Dom::default());
951 assert!(!built.popover_state.inner.open);
952 assert_eq!(built.content_style, build_content_style(false));
953 }
954
955 #[test]
956 fn on_toggle_refany_is_shared_not_copied() {
957 let mut shared = RefAny::new(ToggleLog { calls: Vec::new() });
958 let pop = Popover::default().with_on_toggle(shared.clone(), toggle_cb(record_toggle));
959
960 {
962 let stored = pop.popover_state.on_toggle.as_ref().unwrap();
963 let mut handle = stored.refany.clone();
964 handle
965 .downcast_mut::<ToggleLog>()
966 .expect("payload type preserved")
967 .calls
968 .push(true);
969 }
970
971 assert_eq!(
972 shared.downcast_ref::<ToggleLog>().unwrap().calls.as_slice(),
973 &[true]
974 );
975 }
976
977 #[test]
982 fn swap_with_default_moves_all_state_out() {
983 let mut pop = Popover::new(Dom::create_text("a"), Dom::create_text("c"))
984 .with_open(true)
985 .with_on_toggle(RefAny::new(5u8), toggle_cb(record_toggle));
986
987 let original = pop.swap_with_default();
988
989 assert_eq!(original.anchor, Dom::create_text("a"));
990 assert_eq!(original.content, Dom::create_text("c"));
991 assert!(original.popover_state.inner.open);
992 assert!(original.popover_state.on_toggle.is_some());
993 assert_eq!(original.content_style, build_content_style(true));
994
995 assert_eq!(pop, Popover::default(), "self must be left as a default popover");
996 assert!(
997 pop.popover_state.on_toggle.is_none(),
998 "self must lose the callback"
999 );
1000 assert!(!pop.popover_state.inner.open, "self must be re-closed");
1001 assert_eq!(pop.content_style, build_content_style(false));
1002 }
1003
1004 #[test]
1005 fn swap_with_default_twice_is_a_noop() {
1006 let mut pop = Popover::default();
1007 let first = pop.swap_with_default();
1008 assert_eq!(first, Popover::default());
1009
1010 let second = pop.swap_with_default();
1011 assert_eq!(second, Popover::default());
1012 assert_eq!(pop, Popover::default());
1013 }
1014
1015 #[test]
1020 fn dom_structure_classes_and_callback() {
1021 let dom = Popover::new(Dom::create_text("anchor"), Dom::create_text("panel")).dom();
1022
1023 assert!(has_class(&dom, "__azul-native-popover"));
1024 let children = dom.children.as_ref();
1025 assert_eq!(children.len(), 2, "the wrapper is exactly [trigger, panel]");
1026
1027 let (trigger, panel) = (&children[0], &children[1]);
1028 assert!(has_class(trigger, "__azul-native-popover-trigger"));
1029 assert!(has_class(panel, "__azul-native-popover-content"));
1030
1031 assert_eq!(text_of(&trigger.children.as_ref()[0]), Some("anchor"));
1033 assert_eq!(text_of(&panel.children.as_ref()[0]), Some("panel"));
1034
1035 assert!(matches!(trigger.root.get_tab_index(), Some(TabIndex::Auto)));
1037 let cbs = trigger.root.get_callbacks();
1038 assert_eq!(cbs.len(), 1);
1039 assert_eq!(
1040 cbs.as_ref()[0].event,
1041 EventFilter::Hover(HoverEventFilter::MouseUp)
1042 );
1043 assert_eq!(cbs.as_ref()[0].callback.cb, on_popover_toggle as usize);
1044
1045 assert!(
1048 panel.root.get_callbacks().as_ref().is_empty(),
1049 "the panel must carry no callbacks"
1050 );
1051 }
1052
1053 #[test]
1054 fn dom_panel_display_follows_open_state() {
1055 for open in [false, true] {
1056 let dom = Popover::new(Dom::create_div(), Dom::create_div())
1057 .with_open(open)
1058 .dom();
1059 let panel = &dom.children.as_ref()[1];
1060 assert_eq!(
1061 inline_display(panel),
1062 Some(if open {
1063 LayoutDisplay::Block
1064 } else {
1065 LayoutDisplay::None
1066 }),
1067 "the rendered panel's display must match the open flag"
1068 );
1069 }
1070 }
1071
1072 #[test]
1073 fn dom_payload_is_the_popover_state() {
1074 for open in [false, true] {
1075 let dom = Popover::new(Dom::create_div(), Dom::create_div())
1076 .with_open(open)
1077 .dom();
1078 let mut payload = dom.children.as_ref()[0].root.get_callbacks().as_ref()[0]
1079 .refany
1080 .clone();
1081 let state = payload
1082 .downcast_ref::<PopoverStateWrapper>()
1083 .expect("the trigger payload must be a PopoverStateWrapper");
1084
1085 assert_eq!(
1086 state.inner.open, open,
1087 "the trigger's payload must agree with the panel's display"
1088 );
1089 assert!(state.on_toggle.is_none(), "no user callback was set");
1090 }
1091 }
1092
1093 #[test]
1094 fn dom_keeps_the_user_callback_payload_alive() {
1095 let log = RefAny::new(ToggleLog { calls: Vec::new() });
1096 let mut kept = log.clone();
1097
1098 let dom = Popover::new(Dom::create_div(), Dom::create_div())
1099 .with_on_toggle(log, toggle_cb(record_toggle))
1100 .dom();
1101
1102 let mut payload = dom.children.as_ref()[0].root.get_callbacks().as_ref()[0]
1103 .refany
1104 .clone();
1105 assert!(
1106 payload
1107 .downcast_ref::<PopoverStateWrapper>()
1108 .unwrap()
1109 .on_toggle
1110 .is_some(),
1111 "the user callback must survive the move into the trigger payload"
1112 );
1113
1114 assert!(kept.downcast_ref::<ToggleLog>().unwrap().calls.is_empty());
1116 }
1117
1118 #[test]
1119 fn each_dom_gets_its_own_state_refany() {
1120 let a = Popover::default().dom();
1121 let b = Popover::default().dom();
1122
1123 let ra = a.children.as_ref()[0].root.get_callbacks().as_ref()[0]
1124 .refany
1125 .clone();
1126 let rb = b.children.as_ref()[0].root.get_callbacks().as_ref()[0]
1127 .refany
1128 .clone();
1129
1130 assert_ne!(ra, rb, "two popovers must not share toggle state");
1131 }
1132
1133 #[test]
1134 fn dom_child_count_cache_stays_consistent() {
1135 let mut deep = Dom::create_text("leaf");
1139 for _ in 0..64 {
1140 deep = Dom::create_div().with_child(deep);
1141 }
1142 let wide_children: Vec<Dom> = (0..256).map(|_| Dom::create_div()).collect();
1143 let wide = Dom::create_div().with_children(wide_children.into());
1144
1145 let dom = Popover::new(deep, wide).dom();
1146
1147 assert_eq!(
1148 dom.estimated_total_children,
1149 dom.recompute_estimated_total_children(),
1150 "cached descendant count desynced from the real tree"
1151 );
1152 }
1153
1154 #[test]
1155 fn dom_of_default_popover_is_well_formed() {
1156 let dom = Popover::default().dom();
1157 assert!(has_class(&dom, "__azul-native-popover"));
1158 assert_eq!(dom.children.as_ref().len(), 2);
1159 assert_eq!(
1160 inline_display(&dom.children.as_ref()[1]),
1161 Some(LayoutDisplay::None),
1162 "a default popover renders a hidden panel"
1163 );
1164 assert_eq!(
1165 dom.estimated_total_children,
1166 dom.recompute_estimated_total_children()
1167 );
1168 }
1169
1170 #[test]
1171 fn from_popover_for_dom_matches_dom_structurally() {
1172 let make = || Popover::new(Dom::create_text("a"), Dom::create_text("c")).with_open(true);
1176 let via_from = Dom::from(make());
1177 let via_dom = make().dom();
1178
1179 assert!(has_class(&via_from, "__azul-native-popover"));
1180 assert_eq!(via_from.children.as_ref().len(), 2);
1181 assert!(has_class(
1182 &via_from.children.as_ref()[0],
1183 "__azul-native-popover-trigger"
1184 ));
1185 assert!(has_class(
1186 &via_from.children.as_ref()[1],
1187 "__azul-native-popover-content"
1188 ));
1189 assert_eq!(
1190 inline_display(&via_from.children.as_ref()[1]),
1191 inline_display(&via_dom.children.as_ref()[1])
1192 );
1193 assert_eq!(
1194 via_from.children.as_ref()[1].children,
1195 via_dom.children.as_ref()[1].children
1196 );
1197 assert_eq!(
1198 via_from.estimated_total_children,
1199 via_dom.estimated_total_children
1200 );
1201 }
1202
1203 #[test]
1208 fn toggle_without_any_layout_result_is_a_noop() {
1209 let mut data = RefAny::new(PopoverStateWrapper::default());
1210
1211 let (update, changes) = run_toggle(None, 0, data.clone());
1212
1213 assert_eq!(update, Update::DoNothing);
1214 assert!(changes.is_empty(), "nothing may be restyled without a panel");
1215 assert!(!payload_open(&mut data), "state must not flip");
1216 }
1217
1218 #[test]
1219 fn toggle_without_next_sibling_does_not_flip_state() {
1220 let mut data = RefAny::new(PopoverStateWrapper {
1223 inner: PopoverState { open: true },
1224 on_toggle: None.into(),
1225 });
1226
1227 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 2, data.clone());
1228
1229 assert_eq!(update, Update::DoNothing);
1230 assert!(changes.is_empty());
1231 assert!(payload_open(&mut data), "state must be untouched");
1232 }
1233
1234 #[test]
1235 fn toggle_with_stale_hit_node_is_a_noop() {
1236 let mut data = RefAny::new(PopoverStateWrapper::default());
1237
1238 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 999, data.clone());
1240
1241 assert_eq!(update, Update::DoNothing);
1242 assert!(changes.is_empty());
1243 assert!(!payload_open(&mut data));
1244 }
1245
1246 #[test]
1247 fn toggle_with_foreign_payload_is_a_noop() {
1248 let data = RefAny::new(0xdead_beef_u64);
1250
1251 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 1, data.clone());
1252
1253 assert_eq!(update, Update::DoNothing);
1254 assert!(
1255 changes.is_empty(),
1256 "a foreign payload must not restyle the panel"
1257 );
1258 }
1259
1260 #[test]
1261 fn toggle_flips_state_and_panel_display() {
1262 let mut data = RefAny::new(PopoverStateWrapper::default());
1263
1264 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 1, data.clone());
1266 assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
1267 assert_eq!(
1268 display_writes(&changes),
1269 alloc::vec![(2usize, LayoutDisplay::Block)]
1270 );
1271 assert!(payload_open(&mut data));
1272
1273 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 1, data.clone());
1275 assert_eq!(update, Update::DoNothing);
1276 assert_eq!(
1277 display_writes(&changes),
1278 alloc::vec![(2usize, LayoutDisplay::None)]
1279 );
1280 assert!(!payload_open(&mut data));
1281 }
1282
1283 #[test]
1284 fn toggle_is_an_involution_over_many_clicks() {
1285 let mut data = RefAny::new(PopoverStateWrapper::default());
1286
1287 for i in 0..8u32 {
1288 let (_, changes) = run_toggle(Some(trigger_panel_dom()), 1, data.clone());
1289 let expected_open = i % 2 == 0;
1290 assert_eq!(
1291 display_writes(&changes),
1292 alloc::vec![(
1293 2usize,
1294 if expected_open {
1295 LayoutDisplay::Block
1296 } else {
1297 LayoutDisplay::None
1298 }
1299 )],
1300 "click {i} wrote the wrong display"
1301 );
1302 assert_eq!(payload_open(&mut data), expected_open);
1303 }
1304
1305 assert!(!payload_open(&mut data));
1307 }
1308
1309 #[test]
1310 fn toggle_display_agrees_with_build_content_style() {
1311 let data = RefAny::new(PopoverStateWrapper::default());
1314 let (_, changes) = run_toggle(Some(trigger_panel_dom()), 1, data);
1315
1316 assert_eq!(
1317 display_writes(&changes)
1318 .into_iter()
1319 .map(|(_, d)| d)
1320 .collect::<Vec<_>>(),
1321 displays_in(&build_content_style(true))
1322 );
1323 }
1324
1325 #[test]
1326 fn toggle_invokes_user_callback_with_the_new_state() {
1327 let mut log = RefAny::new(ToggleLog { calls: Vec::new() });
1328 let data = RefAny::new(PopoverStateWrapper {
1329 inner: PopoverState { open: false },
1330 on_toggle: Some(PopoverOnToggle {
1331 callback: toggle_cb(record_toggle),
1332 refany: log.clone(),
1333 })
1334 .into(),
1335 });
1336
1337 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 1, data.clone());
1338
1339 assert_eq!(update, Update::RefreshDom);
1341 assert_eq!(
1343 display_writes(&changes),
1344 alloc::vec![(2usize, LayoutDisplay::Block)]
1345 );
1346 assert_eq!(
1347 log.downcast_ref::<ToggleLog>().unwrap().calls.as_slice(),
1348 &[true],
1349 "the callback must receive the *new* (post-flip) state"
1350 );
1351
1352 let (_, _) = run_toggle(Some(trigger_panel_dom()), 1, data);
1354 assert_eq!(
1355 log.downcast_ref::<ToggleLog>().unwrap().calls.as_slice(),
1356 &[true, false]
1357 );
1358 }
1359
1360 #[test]
1361 fn toggle_still_restyles_when_the_user_callback_does_nothing() {
1362 let data = RefAny::new(PopoverStateWrapper {
1363 inner: PopoverState { open: false },
1364 on_toggle: Some(PopoverOnToggle {
1365 callback: toggle_cb(toggle_do_nothing),
1366 refany: RefAny::new(0u8),
1367 })
1368 .into(),
1369 });
1370
1371 let (update, changes) = run_toggle(Some(trigger_panel_dom()), 1, data);
1372
1373 assert_eq!(update, Update::DoNothing);
1374 assert_eq!(
1375 display_writes(&changes),
1376 alloc::vec![(2usize, LayoutDisplay::Block)],
1377 "the panel must be shown regardless of what the user callback returns"
1378 );
1379 }
1380
1381 #[test]
1382 fn toggle_targets_the_panel_in_a_really_rendered_popover() {
1383 let styled =
1387 StyledDom::create_from_dom(Popover::new(Dom::create_div(), Dom::create_div()).dom());
1388 let trigger_idx = index_of_class(&styled, "__azul-native-popover-trigger");
1389 let panel_idx = index_of_class(&styled, "__azul-native-popover-content");
1390
1391 let data = RefAny::new(PopoverStateWrapper::default());
1392 let (_, changes) = run_toggle(Some(styled), trigger_idx, data);
1393
1394 assert_eq!(
1395 display_writes(&changes),
1396 alloc::vec![(panel_idx, LayoutDisplay::Block)],
1397 "the toggle must restyle the popover's own panel, not a stray sibling"
1398 );
1399 }
1400
1401 #[test]
1402 fn toggle_on_the_wrapper_node_does_not_touch_the_panel() {
1403 let styled =
1406 StyledDom::create_from_dom(Popover::new(Dom::create_div(), Dom::create_div()).dom());
1407 let wrapper_idx = index_of_class(&styled, "__azul-native-popover");
1408
1409 let mut data = RefAny::new(PopoverStateWrapper::default());
1410 let (update, changes) = run_toggle(Some(styled), wrapper_idx, data.clone());
1411
1412 assert_eq!(update, Update::DoNothing);
1413 assert!(changes.is_empty());
1414 assert!(!payload_open(&mut data));
1415 }
1416}