1use std::vec::Vec;
22
23use azul_core::{
24 callbacks::{CoreCallback, CoreCallbackData, Update},
25 dom::{
26 Dom, DomVec, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec,
27 TabIndex,
28 },
29 refany::{OptionRefAny, RefAny},
30};
31use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
32use azul_css::{
33 impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut, impl_vec_partialeq,
34 props::{
35 basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, StyleFontSize},
36 layout::{LayoutDisplay, LayoutFlexDirection, LayoutFlexGrow, LayoutOverflow, LayoutAlignItems, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
37 property::{CssProperty, *},
38 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleTextColor, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor, StyleUserSelect, StyleTextAlign},
39 },
40 impl_option_inner, AzString,
41};
42
43use crate::callbacks::{Callback, CallbackInfo};
44
45static ACCORDION_CLASS: &[IdOrClass] =
46 &[Class(AzString::from_const_str("__azul-native-accordion"))];
47static ACCORDION_SECTION_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
48 "__azul-native-accordion-section",
49))];
50static ACCORDION_HEADER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
51 "__azul-native-accordion-header",
52))];
53static ACCORDION_TITLE_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
54 "__azul-native-accordion-title",
55))];
56static ACCORDION_BODY_CLASS: &[IdOrClass] =
57 &[Class(AzString::from_const_str("__azul-native-accordion-body"))];
58
59const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
60const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
61const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
62 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
63
64pub type AccordionOnToggleCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
67impl_widget_callback!(
68 AccordionOnToggle,
69 OptionAccordionOnToggle,
70 AccordionOnToggleCallback,
71 AccordionOnToggleCallbackType
72);
73
74azul_core::impl_managed_callback! {
75 wrapper: AccordionOnToggleCallback,
76 info_ty: CallbackInfo,
77 return_ty: Update,
78 default_ret: Update::DoNothing,
79 invoker_static: ACCORDION_ON_TOGGLE_INVOKER,
80 invoker_ty: AzAccordionOnToggleCallbackInvoker,
81 thunk_fn: az_accordion_on_toggle_callback_thunk,
82 setter_fn: AzApp_setAccordionOnToggleCallbackInvoker,
83 from_handle_fn: AzAccordionOnToggleCallback_createFromHostHandle,
84 extra_args: [ section_index: usize ],
85}
86
87const BORDER_COLOR: ColorU = ColorU { r: 222, g: 226, b: 230, a: 255 }; const HEADER_BG: ColorU = ColorU { r: 248, g: 249, b: 250, a: 255 }; const TEXT_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 }; const HEADER_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(HEADER_BG)];
93const HEADER_BG_VEC: StyleBackgroundContentVec =
94 StyleBackgroundContentVec::from_const_slice(HEADER_BG_ITEMS);
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98#[repr(C)]
99pub struct AccordionSection {
100 pub title: AzString,
102 pub content: Dom,
104 pub is_open: bool,
106}
107
108impl AccordionSection {
109 pub fn new<S: Into<AzString>>(title: S, content: Dom) -> Self {
111 Self {
112 title: title.into(),
113 content,
114 is_open: false,
115 }
116 }
117
118 #[must_use] pub const fn with_open(mut self, open: bool) -> Self {
120 self.is_open = open;
121 self
122 }
123}
124
125impl_option!(AccordionSection, OptionAccordionSection, copy = false, [Debug, Clone, PartialEq, Eq]);
126impl_vec!(
127 AccordionSection,
128 AccordionSectionVec,
129 AccordionSectionVecDestructor,
130 AccordionSectionVecDestructorType,
131 AccordionSectionVecSlice,
132 OptionAccordionSection
133);
134impl_vec_clone!(AccordionSection, AccordionSectionVec, AccordionSectionVecDestructor);
135impl_vec_debug!(AccordionSection, AccordionSectionVec);
136impl_vec_partialeq!(AccordionSection, AccordionSectionVec);
137impl_vec_mut!(AccordionSection, AccordionSectionVec);
138
139#[derive(Debug, Clone, PartialEq)]
141#[repr(C)]
142pub struct Accordion {
143 pub sections: AccordionSectionVec,
145 pub on_toggle: OptionAccordionOnToggle,
147}
148
149static ACCORDION_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
152 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
153 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
154 LayoutFlexDirection::Column,
155 )),
156 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
157 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
158 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
159 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
160 inner: TEXT_COLOR,
161 })),
162 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
164 LayoutBorderTopWidth::const_px(1),
165 )),
166 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
167 LayoutBorderBottomWidth::const_px(1),
168 )),
169 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
170 LayoutBorderLeftWidth::const_px(1),
171 )),
172 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
173 LayoutBorderRightWidth::const_px(1),
174 )),
175 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
176 inner: BorderStyle::Solid,
177 })),
178 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
179 StyleBorderBottomStyle {
180 inner: BorderStyle::Solid,
181 },
182 )),
183 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
184 inner: BorderStyle::Solid,
185 })),
186 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
187 StyleBorderRightStyle {
188 inner: BorderStyle::Solid,
189 },
190 )),
191 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
192 inner: BORDER_COLOR,
193 })),
194 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
195 StyleBorderBottomColor {
196 inner: BORDER_COLOR,
197 },
198 )),
199 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
200 inner: BORDER_COLOR,
201 })),
202 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
203 StyleBorderRightColor {
204 inner: BORDER_COLOR,
205 },
206 )),
207 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
209 StyleBorderTopLeftRadius::const_px(6),
210 )),
211 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
212 StyleBorderTopRightRadius::const_px(6),
213 )),
214 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
215 StyleBorderBottomLeftRadius::const_px(6),
216 )),
217 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
218 StyleBorderBottomRightRadius::const_px(6),
219 )),
220 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
221 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
222];
223
224static ACCORDION_SECTION_STYLE: &[CssPropertyWithConditions] = &[
225 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
226 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
227 LayoutFlexDirection::Column,
228 )),
229 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
230 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
232 LayoutBorderBottomWidth::const_px(1),
233 )),
234 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
235 StyleBorderBottomStyle {
236 inner: BorderStyle::Solid,
237 },
238 )),
239 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
240 StyleBorderBottomColor {
241 inner: BORDER_COLOR,
242 },
243 )),
244];
245
246static ACCORDION_HEADER_STYLE: &[CssPropertyWithConditions] = &[
247 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
248 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
249 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
250 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
251 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
252 10,
253 ))),
254 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
255 LayoutPaddingBottom::const_px(10),
256 )),
257 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
258 12,
259 ))),
260 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
261 LayoutPaddingRight::const_px(12),
262 )),
263 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
264 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
265 CssPropertyWithConditions::simple(CssProperty::const_background_content(HEADER_BG_VEC)),
266];
267
268static ACCORDION_TITLE_STYLE: &[CssPropertyWithConditions] = &[
269 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
270 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
271];
272
273static ACCORDION_BODY_STYLE_OPEN: &[CssPropertyWithConditions] = &[
275 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
276 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
277 12,
278 ))),
279 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
280 LayoutPaddingBottom::const_px(12),
281 )),
282 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
283 12,
284 ))),
285 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
286 LayoutPaddingRight::const_px(12),
287 )),
288];
289
290static ACCORDION_BODY_STYLE_CLOSED: &[CssPropertyWithConditions] = &[
292 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::None)),
293];
294
295impl Accordion {
296 #[must_use] pub fn new(sections: AccordionSectionVec) -> Self {
298 Self {
299 sections,
300 on_toggle: None.into(),
301 }
302 }
303
304 #[must_use] pub fn create() -> Self {
306 Self::new(AccordionSectionVec::from_const_slice(&[]))
307 }
308
309 pub fn set_on_toggle<C: Into<AccordionOnToggleCallback>>(&mut self, data: RefAny, callback: C) {
311 self.on_toggle = Some(AccordionOnToggle {
312 callback: callback.into(),
313 refany: data,
314 })
315 .into();
316 }
317
318 #[must_use] pub fn with_on_toggle<C: Into<AccordionOnToggleCallback>>(
320 mut self,
321 data: RefAny,
322 callback: C,
323 ) -> Self {
324 self.set_on_toggle(data, callback);
325 self
326 }
327
328 #[must_use] pub fn swap_with_default(&mut self) -> Self {
330 let mut s = Self::create();
331 core::mem::swap(&mut s, self);
332 s
333 }
334
335 #[must_use] pub fn dom(self) -> Dom {
337 let on_toggle = self.on_toggle;
338 let sections = self.sections;
339
340 let mut section_doms: Vec<Dom> = Vec::with_capacity(sections.as_ref().len());
341
342 for (index, section) in sections.as_ref().iter().enumerate() {
343 let title = Dom::create_text(section.title.clone())
344 .with_ids_and_classes(IdOrClassVec::from_const_slice(ACCORDION_TITLE_CLASS))
345 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
346 ACCORDION_TITLE_STYLE,
347 ));
348
349 let header_data = HeaderClickData {
351 index,
352 is_open: section.is_open,
353 on_toggle: clone_option_on_toggle(&on_toggle),
354 };
355
356 let header = Dom::create_div()
357 .with_ids_and_classes(IdOrClassVec::from_const_slice(ACCORDION_HEADER_CLASS))
358 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
359 ACCORDION_HEADER_STYLE,
360 ))
361 .with_tab_index(TabIndex::Auto)
362 .with_callbacks(
363 alloc::vec![CoreCallbackData {
364 event: EventFilter::Hover(HoverEventFilter::MouseUp),
365 callback: CoreCallback {
366 cb: on_accordion_header_click as usize,
367 ctx: OptionRefAny::None,
368 },
369 refany: RefAny::new(header_data),
370 }]
371 .into(),
372 )
373 .with_children(DomVec::from_vec(alloc::vec![title]));
374
375 let body_style = if section.is_open {
376 ACCORDION_BODY_STYLE_OPEN
377 } else {
378 ACCORDION_BODY_STYLE_CLOSED
379 };
380 let body = Dom::create_div()
381 .with_ids_and_classes(IdOrClassVec::from_const_slice(ACCORDION_BODY_CLASS))
382 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(body_style))
383 .with_children(DomVec::from_vec(alloc::vec![section.content.clone()]));
384
385 section_doms.push(
386 Dom::create_div()
387 .with_ids_and_classes(IdOrClassVec::from_const_slice(ACCORDION_SECTION_CLASS))
388 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
389 ACCORDION_SECTION_STYLE,
390 ))
391 .with_children(DomVec::from_vec(alloc::vec![header, body])),
392 );
393 }
394
395 Dom::create_div()
396 .with_ids_and_classes(IdOrClassVec::from_const_slice(ACCORDION_CLASS))
397 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
398 ACCORDION_CONTAINER_STYLE,
399 ))
400 .with_children(DomVec::from_vec(section_doms))
401 }
402}
403
404impl Default for Accordion {
405 fn default() -> Self {
406 Self::create()
407 }
408}
409
410fn clone_option_on_toggle(opt: &OptionAccordionOnToggle) -> OptionAccordionOnToggle {
412 match opt.as_ref() {
413 Some(AccordionOnToggle { callback, refany }) => Some(AccordionOnToggle {
414 callback: callback.clone(),
415 refany: refany.clone(),
416 })
417 .into(),
418 None => None.into(),
419 }
420}
421
422struct HeaderClickData {
424 index: usize,
425 is_open: bool,
426 on_toggle: OptionAccordionOnToggle,
427}
428
429extern "C" fn on_accordion_header_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
434 let header = info.get_hit_node();
435 let Some(body) = info.get_next_sibling(header) else {
436 return Update::DoNothing;
437 };
438
439 let (now_open, result) = {
440 let Some(mut hd) = data.downcast_mut::<HeaderClickData>() else {
441 return Update::DoNothing;
442 };
443 hd.is_open = !hd.is_open;
444 let now_open = hd.is_open;
445 let index = hd.index;
446 let result = match hd.on_toggle.as_mut() {
447 Some(AccordionOnToggle { callback, refany }) => {
448 (callback.cb)(refany.clone(), info, index)
449 }
450 None => Update::DoNothing,
451 };
452 (now_open, result)
453 };
454
455 let display = if now_open {
456 LayoutDisplay::Block
457 } else {
458 LayoutDisplay::None
459 };
460 info.set_css_property(body, CssProperty::const_display(display));
461
462 result
463}
464
465impl From<Accordion> for Dom {
466 fn from(a: Accordion) -> Self {
467 a.dom()
468 }
469}
470
471#[cfg(all(test, feature = "std"))]
472mod autotest_generated {
473 use std::{
474 collections::{BTreeMap, HashMap},
475 sync::{Arc, Mutex},
476 };
477
478 use azul_core::{
479 dom::{DomId, DomNodeId, NodeId, NodeType},
480 geom::{LogicalRect, OptionLogicalPosition},
481 gl::OptionGlContextPtr,
482 hit_test::ScrollPosition,
483 resources::RendererResources,
484 styled_dom::{NodeHierarchyItemId, StyledDom},
485 window::{MonitorVec, RawWindowHandle},
486 };
487 use azul_css::system::SystemStyle;
488 use rust_fontconfig::FcFontCache;
489
490 use super::*;
491 #[cfg(feature = "icu")]
492 use crate::icu::IcuLocalizerHandle;
493 use crate::{
494 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
495 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
496 window::{DomLayoutResult, LayoutWindow},
497 window_state::FullWindowState,
498 };
499
500 fn has_class(node: &Dom, name: &str) -> bool {
506 node.root
507 .get_ids_and_classes()
508 .as_ref()
509 .iter()
510 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == name))
511 }
512
513 fn text_of(node: &Dom) -> Option<&str> {
515 match node.root.get_node_type() {
516 NodeType::Text(s) => Some(s.as_ref().as_str()),
517 _ => None,
518 }
519 }
520
521 fn inline_display(node: &Dom) -> Option<LayoutDisplay> {
523 node.root
524 .style
525 .iter_inline_properties()
526 .find_map(|(p, _)| match p {
527 CssProperty::Display(v) => v.get_property().copied(),
528 _ => None,
529 })
530 }
531
532 fn section_parts(dom: &Dom, n: usize) -> (&Dom, &Dom) {
534 let section = &dom.children.as_ref()[n];
535 assert!(has_class(section, "__azul-native-accordion-section"));
536 let children = section.children.as_ref();
537 assert_eq!(children.len(), 2, "a section is exactly [header, body]");
538 (&children[0], &children[1])
539 }
540
541 fn header_body_dom() -> StyledDom {
545 let styled = StyledDom::create_from_dom(
546 Dom::create_div()
547 .with_child(Dom::create_div())
548 .with_child(Dom::create_div()),
549 );
550 assert_eq!(
551 styled.node_hierarchy.as_ref().len(),
552 3,
553 "fixture must flatten to exactly root/header/body"
554 );
555 styled
556 }
557
558 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
561 DomLayoutResult {
562 styled_dom,
563 layout_tree: LayoutTree {
564 nodes: Vec::new(),
565 warm: Vec::new(),
566 cold: Vec::new(),
567 root: 0,
568 dom_to_layout: BTreeMap::new(),
569 children_arena: Vec::new(),
570 children_offsets: Vec::new(),
571 subtree_needs_intrinsic: Vec::new(),
572 },
573 calculated_positions: Vec::new(),
574 viewport: LogicalRect::zero(),
575 display_list: DisplayList::default(),
576 scroll_ids: HashMap::new(),
577 scroll_id_to_node_id: HashMap::new(),
578 }
579 }
580
581 fn run_click(styled: Option<StyledDom>, hit: usize, data: RefAny) -> (Update, Vec<CallbackChange>) {
585 let mut layout_window =
586 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
587 if let Some(sd) = styled {
588 layout_window
589 .layout_results
590 .insert(DomId::ROOT_ID, layout_result(sd));
591 }
592
593 let renderer_resources = RendererResources::default();
594 let previous_window_state: Option<FullWindowState> = None;
595 let current_window_state = FullWindowState::default();
596 let gl_context = OptionGlContextPtr::None;
597 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
598 BTreeMap::new();
599 let window_handle = RawWindowHandle::Unsupported;
600 let system_callbacks = ExternalSystemCallbacks::rust_internal();
601
602 let ref_data = CallbackInfoRefData {
603 layout_window: &layout_window,
604 renderer_resources: &renderer_resources,
605 previous_window_state: &previous_window_state,
606 current_window_state: ¤t_window_state,
607 gl_context: &gl_context,
608 current_scroll_manager: &scroll_states,
609 current_window_handle: &window_handle,
610 system_callbacks: &system_callbacks,
611 system_style: Arc::new(SystemStyle::default()),
612 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
613 #[cfg(feature = "icu")]
614 icu_localizer: IcuLocalizerHandle::default(),
615 ctx: OptionRefAny::None,
616 };
617
618 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
619
620 let info = CallbackInfo::new(
621 &ref_data,
622 &changes,
623 DomNodeId {
624 dom: DomId::ROOT_ID,
625 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(hit))),
626 },
627 OptionLogicalPosition::None,
628 OptionLogicalPosition::None,
629 );
630
631 let update = on_accordion_header_click(data, info);
632 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
633 (update, recorded)
634 }
635
636 fn display_writes(changes: &[CallbackChange]) -> Vec<(usize, LayoutDisplay)> {
638 let mut out = Vec::new();
639 for change in changes {
640 if let CallbackChange::ChangeNodeCssProperties {
641 node_id, properties, ..
642 } = change
643 {
644 for p in properties.as_ref() {
645 if let CssProperty::Display(v) = p {
646 if let Some(d) = v.get_property() {
647 out.push((node_id.index(), *d));
648 }
649 }
650 }
651 }
652 }
653 out
654 }
655
656 fn payload_is_open(data: &mut RefAny) -> bool {
658 data.downcast_ref::<HeaderClickData>()
659 .expect("payload must still be a HeaderClickData")
660 .is_open
661 }
662
663 struct ToggleLog {
665 calls: Vec<usize>,
666 }
667
668 extern "C" fn record_toggle(mut data: RefAny, _: CallbackInfo, index: usize) -> Update {
669 if let Some(mut log) = data.downcast_mut::<ToggleLog>() {
670 log.calls.push(index);
671 }
672 Update::RefreshDom
673 }
674
675 extern "C" fn toggle_do_nothing(_: RefAny, _: CallbackInfo, _: usize) -> Update {
676 Update::DoNothing
677 }
678
679 fn toggle_cb(f: AccordionOnToggleCallbackType) -> AccordionOnToggleCallback {
680 f.into()
681 }
682
683 #[test]
688 fn section_new_stores_args_and_starts_closed() {
689 let content = Dom::create_div().with_child(Dom::create_text("body"));
690 let sec = AccordionSection::new("Title", content.clone());
691
692 assert_eq!(sec.title.as_str(), "Title");
693 assert_eq!(sec.content, content);
694 assert!(!sec.is_open, "a fresh section must start collapsed");
695 }
696
697 #[test]
698 fn section_new_survives_extreme_titles() {
699 let long = "ab".repeat(50_000);
701 let cases: Vec<AzString> = alloc::vec![
702 AzString::from(""),
703 AzString::from("a\0b"),
704 AzString::from("๐จโ๐ฉโ๐งโ๐ฆ e\u{0301}\u{0327} ู
ุฑุญุจุง ืฉืืื ๐ฉ๐ช"),
705 AzString::from("\u{feff}\u{202e}rtl-override"),
706 AzString::from(long.as_str()),
707 ];
708
709 for title in cases {
710 let sec = AccordionSection::new(title.clone(), Dom::create_div());
711 assert_eq!(sec.title.as_str(), title.as_str());
712 assert!(!sec.is_open);
713
714 let dom = Accordion::new(AccordionSectionVec::from_vec(alloc::vec![sec])).dom();
716 let (header, _) = section_parts(&dom, 0);
717 let title_node = &header.children.as_ref()[0];
718 assert_eq!(text_of(title_node), Some(title.as_str()));
719 }
720 }
721
722 #[test]
723 fn section_with_open_sets_flag_without_touching_other_fields() {
724 let content = Dom::create_text("x");
725 let base = AccordionSection::new("t", content.clone());
726
727 let opened = base.clone().with_open(true);
728 assert!(opened.is_open);
729 assert_eq!(opened.title.as_str(), "t");
730 assert_eq!(opened.content, content);
731
732 assert!(!base.clone().with_open(true).with_open(false).is_open);
734 assert!(base.clone().with_open(false).with_open(true).is_open);
735 assert!(base.clone().with_open(true).with_open(true).is_open);
736 assert!(!base.with_open(false).is_open);
737 }
738
739 #[test]
744 fn accordion_new_preserves_section_count_and_has_no_callback() {
745 for count in [0usize, 1, 3, 1000] {
746 let mut sections = Vec::with_capacity(count);
747 for i in 0..count {
748 sections.push(
749 AccordionSection::new(alloc::format!("s{i}"), Dom::create_div())
750 .with_open(i % 2 == 0),
751 );
752 }
753 let acc = Accordion::new(AccordionSectionVec::from_vec(sections));
754
755 assert_eq!(acc.sections.len(), count);
756 assert!(acc.on_toggle.is_none(), "Accordion::new sets no callback");
757 for (i, s) in acc.sections.as_ref().iter().enumerate() {
758 assert_eq!(s.title.as_str(), alloc::format!("s{i}"));
759 assert_eq!(s.is_open, i % 2 == 0);
760 }
761 }
762 }
763
764 #[test]
765 fn accordion_create_is_empty_and_equals_default() {
766 let acc = Accordion::create();
767 assert!(acc.sections.is_empty());
768 assert!(acc.on_toggle.is_none());
769 assert_eq!(acc, Accordion::default());
770 }
771
772 #[test]
777 fn set_on_toggle_last_call_wins() {
778 let mut acc = Accordion::create();
779
780 acc.set_on_toggle(RefAny::new(1u8), toggle_cb(toggle_do_nothing));
781 assert!(acc.on_toggle.is_some());
782 assert_eq!(
783 acc.on_toggle.as_ref().unwrap().refany.get_type_id(),
784 RefAny::new(1u8).get_type_id()
785 );
786
787 acc.set_on_toggle(RefAny::new(9i64), toggle_cb(record_toggle));
789 let set = acc.on_toggle.as_ref().expect("still Some");
790 assert_eq!(set.refany.get_type_id(), RefAny::new(0i64).get_type_id());
791 assert_eq!(set.callback, toggle_cb(record_toggle));
792 assert_ne!(set.callback, toggle_cb(toggle_do_nothing));
793 }
794
795 #[test]
796 fn with_on_toggle_matches_set_on_toggle() {
797 let built = Accordion::create().with_on_toggle(RefAny::new(7u32), toggle_cb(record_toggle));
798
799 let mut mutated = Accordion::create();
800 mutated.set_on_toggle(RefAny::new(7u32), toggle_cb(record_toggle));
801
802 assert!(built.on_toggle.is_some());
803 assert_eq!(
804 built.on_toggle.as_ref().unwrap().callback,
805 mutated.on_toggle.as_ref().unwrap().callback
806 );
807 assert!(built.sections.is_empty());
809 }
810
811 #[test]
812 fn swap_with_default_moves_all_state_out() {
813 let sections = AccordionSectionVec::from_vec(alloc::vec![
814 AccordionSection::new("a", Dom::create_div()),
815 AccordionSection::new("b", Dom::create_div()).with_open(true),
816 ]);
817 let mut acc = Accordion::new(sections).with_on_toggle(RefAny::new(5u8), toggle_cb(record_toggle));
818
819 let original = acc.swap_with_default();
820
821 assert_eq!(original.sections.len(), 2);
822 assert!(original.on_toggle.is_some());
823 assert!(original.sections.as_ref()[1].is_open);
824
825 assert!(acc.sections.is_empty(), "self must be left empty");
826 assert!(acc.on_toggle.is_none(), "self must lose the callback");
827 assert_eq!(acc, Accordion::create());
828
829 let second = acc.swap_with_default();
831 assert_eq!(second, Accordion::create());
832 assert_eq!(acc, Accordion::create());
833 }
834
835 #[test]
840 fn dom_of_empty_accordion_has_no_children() {
841 let dom = Accordion::create().dom();
842 assert!(has_class(&dom, "__azul-native-accordion"));
843 assert!(dom.children.as_ref().is_empty());
844 assert_eq!(dom.estimated_total_children, 0);
845 }
846
847 #[test]
848 fn dom_display_follows_is_open() {
849 let acc = Accordion::new(AccordionSectionVec::from_vec(alloc::vec![
850 AccordionSection::new("closed", Dom::create_text("c0")),
851 AccordionSection::new("open", Dom::create_text("c1")).with_open(true),
852 ]));
853 let dom = acc.dom();
854 assert_eq!(dom.children.as_ref().len(), 2);
855
856 let (h0, b0) = section_parts(&dom, 0);
857 let (h1, b1) = section_parts(&dom, 1);
858
859 assert!(has_class(h0, "__azul-native-accordion-header"));
860 assert!(has_class(b0, "__azul-native-accordion-body"));
861
862 assert_eq!(inline_display(b0), Some(LayoutDisplay::None));
864 assert_eq!(inline_display(b1), Some(LayoutDisplay::Block));
865
866 assert_eq!(text_of(&b0.children.as_ref()[0]), Some("c0"));
868 assert_eq!(text_of(&b1.children.as_ref()[0]), Some("c1"));
869
870 for h in [h0, h1] {
872 assert!(matches!(h.root.get_tab_index(), Some(TabIndex::Auto)));
873 let cbs = h.root.get_callbacks();
874 assert_eq!(cbs.len(), 1);
875 assert_eq!(cbs.as_ref()[0].event, EventFilter::Hover(HoverEventFilter::MouseUp));
876 assert_eq!(
877 cbs.as_ref()[0].callback.cb,
878 on_accordion_header_click as usize
879 );
880 }
881 }
882
883 #[test]
884 fn dom_header_payload_carries_the_section_index_and_open_state() {
885 let count = 64usize;
886 let mut sections = Vec::with_capacity(count);
887 for i in 0..count {
888 sections.push(
889 AccordionSection::new(alloc::format!("s{i}"), Dom::create_div())
890 .with_open(i % 3 == 0),
891 );
892 }
893 let dom = Accordion::new(AccordionSectionVec::from_vec(sections)).dom();
894
895 for i in 0..count {
896 let (header, body) = section_parts(&dom, i);
897 let mut payload = header.root.get_callbacks().as_ref()[0].refany.clone();
898 let hd = payload
899 .downcast_ref::<HeaderClickData>()
900 .expect("header payload is a HeaderClickData");
901
902 assert_eq!(hd.index, i, "each header must know its own section index");
903 assert_eq!(hd.is_open, i % 3 == 0);
904 assert!(hd.on_toggle.is_none(), "no user callback was set");
905 assert_eq!(
906 inline_display(body),
907 Some(if i % 3 == 0 {
908 LayoutDisplay::Block
909 } else {
910 LayoutDisplay::None
911 })
912 );
913 }
914 }
915
916 #[test]
917 fn dom_child_count_cache_stays_consistent() {
918 let mut deep = Dom::create_text("leaf");
922 for _ in 0..64 {
923 deep = Dom::create_div().with_child(deep);
924 }
925
926 let sections = AccordionSectionVec::from_vec(alloc::vec![
927 AccordionSection::new("deep", deep),
928 AccordionSection::new("flat", Dom::create_div()).with_open(true),
929 AccordionSection::new("", Dom::create_div()),
930 ]);
931 let dom = Accordion::new(sections).dom();
932
933 assert_eq!(
934 dom.estimated_total_children,
935 dom.recompute_estimated_total_children(),
936 "cached descendant count desynced from the real tree"
937 );
938 }
939
940 #[test]
941 fn from_accordion_for_dom_matches_dom() {
942 assert_eq!(Dom::from(Accordion::create()), Accordion::create().dom());
945 }
946
947 #[test]
948 fn dom_leaves_the_original_on_toggle_payload_alive() {
949 let log = RefAny::new(ToggleLog { calls: Vec::new() });
950 let mut kept = log.clone();
951
952 let acc = Accordion::new(AccordionSectionVec::from_vec(alloc::vec![
953 AccordionSection::new("a", Dom::create_div()),
954 AccordionSection::new("b", Dom::create_div()),
955 ]))
956 .with_on_toggle(log, toggle_cb(record_toggle));
957
958 let dom = acc.dom();
959
960 for i in 0..2 {
962 let (header, _) = section_parts(&dom, i);
963 let mut payload = header.root.get_callbacks().as_ref()[0].refany.clone();
964 let hd = payload.downcast_ref::<HeaderClickData>().unwrap();
965 assert!(hd.on_toggle.is_some());
966 }
967
968 assert!(kept.downcast_ref::<ToggleLog>().unwrap().calls.is_empty());
970 }
971
972 #[test]
977 fn clone_option_on_toggle_of_none_is_none() {
978 let none: OptionAccordionOnToggle = None.into();
979 assert!(clone_option_on_toggle(&none).is_none());
980 assert!(clone_option_on_toggle(&clone_option_on_toggle(&none)).is_none());
982 }
983
984 #[test]
985 fn clone_option_on_toggle_shares_the_payload() {
986 let mut some: OptionAccordionOnToggle = Some(AccordionOnToggle {
987 callback: toggle_cb(record_toggle),
988 refany: RefAny::new(0usize),
989 })
990 .into();
991
992 let mut cloned = clone_option_on_toggle(&some);
993 let cloned_inner = cloned.as_mut().expect("clone of Some must be Some");
994 assert_eq!(cloned_inner.callback, toggle_cb(record_toggle));
995
996 *cloned_inner
999 .refany
1000 .downcast_mut::<usize>()
1001 .expect("payload type is preserved") = 42;
1002
1003 let original_inner = some.as_mut().unwrap();
1004 assert_eq!(*original_inner.refany.downcast_ref::<usize>().unwrap(), 42);
1005 }
1006
1007 #[test]
1012 fn header_click_without_any_layout_result_is_a_noop() {
1013 let mut data = RefAny::new(HeaderClickData {
1014 index: 0,
1015 is_open: false,
1016 on_toggle: None.into(),
1017 });
1018
1019 let (update, changes) = run_click(None, 0, data.clone());
1020
1021 assert_eq!(update, Update::DoNothing);
1022 assert!(changes.is_empty(), "nothing may be restyled without a body");
1023 assert!(!payload_is_open(&mut data), "state must not flip");
1024 }
1025
1026 #[test]
1027 fn header_click_without_next_sibling_does_not_flip_state() {
1028 let mut data = RefAny::new(HeaderClickData {
1031 index: 3,
1032 is_open: true,
1033 on_toggle: None.into(),
1034 });
1035
1036 let (update, changes) = run_click(Some(header_body_dom()), 2, data.clone());
1037
1038 assert_eq!(update, Update::DoNothing);
1039 assert!(changes.is_empty());
1040 assert!(payload_is_open(&mut data), "state must be untouched");
1041 }
1042
1043 #[test]
1044 fn header_click_with_stale_hit_node_is_a_noop() {
1045 let mut data = RefAny::new(HeaderClickData {
1046 index: 0,
1047 is_open: false,
1048 on_toggle: None.into(),
1049 });
1050
1051 let (update, changes) = run_click(Some(header_body_dom()), 999, data.clone());
1053
1054 assert_eq!(update, Update::DoNothing);
1055 assert!(changes.is_empty());
1056 assert!(!payload_is_open(&mut data));
1057 }
1058
1059 #[test]
1060 fn header_click_with_foreign_payload_is_a_noop() {
1061 let data = RefAny::new(0xdead_beef_u64);
1063
1064 let (update, changes) = run_click(Some(header_body_dom()), 1, data.clone());
1065
1066 assert_eq!(update, Update::DoNothing);
1067 assert!(
1068 changes.is_empty(),
1069 "a foreign payload must not restyle the body"
1070 );
1071 }
1072
1073 #[test]
1074 fn header_click_toggles_body_display_and_flips_state() {
1075 let mut data = RefAny::new(HeaderClickData {
1076 index: 0,
1077 is_open: false,
1078 on_toggle: None.into(),
1079 });
1080
1081 let (update, changes) = run_click(Some(header_body_dom()), 1, data.clone());
1083 assert_eq!(update, Update::DoNothing, "no user callback -> DoNothing");
1084 assert_eq!(
1085 display_writes(&changes),
1086 alloc::vec![(2usize, LayoutDisplay::Block)]
1087 );
1088 assert!(payload_is_open(&mut data));
1089
1090 let (update, changes) = run_click(Some(header_body_dom()), 1, data.clone());
1092 assert_eq!(update, Update::DoNothing);
1093 assert_eq!(
1094 display_writes(&changes),
1095 alloc::vec![(2usize, LayoutDisplay::None)]
1096 );
1097 assert!(!payload_is_open(&mut data));
1098 }
1099
1100 #[test]
1101 fn header_click_invokes_user_callback_and_propagates_its_update() {
1102 let mut log = RefAny::new(ToggleLog { calls: Vec::new() });
1103 let data = RefAny::new(HeaderClickData {
1104 index: 17,
1105 is_open: false,
1106 on_toggle: Some(AccordionOnToggle {
1107 callback: toggle_cb(record_toggle),
1108 refany: log.clone(),
1109 })
1110 .into(),
1111 });
1112
1113 let (update, changes) = run_click(Some(header_body_dom()), 1, data.clone());
1114
1115 assert_eq!(update, Update::RefreshDom);
1117 assert_eq!(
1119 display_writes(&changes),
1120 alloc::vec![(2usize, LayoutDisplay::Block)]
1121 );
1122 assert_eq!(
1123 log.downcast_ref::<ToggleLog>().unwrap().calls.as_slice(),
1124 &[17],
1125 "the user callback must receive this section's index"
1126 );
1127
1128 let (_, _) = run_click(Some(header_body_dom()), 1, data);
1130 assert_eq!(
1131 log.downcast_ref::<ToggleLog>().unwrap().calls.as_slice(),
1132 &[17, 17]
1133 );
1134 }
1135}