1use azul_core::{
7 callbacks::{CoreCallback, CoreCallbackData, Update},
8 dom::{
9 Dom, DomVec, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec,
10 TabIndex,
11 },
12 refany::RefAny,
13};
14#[allow(clippy::wildcard_imports)] use azul_css::{
16 dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
17 props::{
18 basic::{
19 color::{ColorU, ColorOrSystem},
20 font::{StyleFontFamily, StyleFontFamilyVec},
21 *,
22 },
23 layout::*,
24 property::CssProperty,
25 style::*,
26 },
27 *,
28};
29
30use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq, impl_vec_mut};
31
32use crate::callbacks::{Callback, CallbackInfo};
33
34pub type TreeViewOnNodeClickCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
41impl_widget_callback!(
42 TreeViewOnNodeClick,
43 OptionTreeViewOnNodeClick,
44 TreeViewOnNodeClickCallback,
45 TreeViewOnNodeClickCallbackType
46);
47
48azul_core::impl_managed_callback! {
49 wrapper: TreeViewOnNodeClickCallback,
50 info_ty: CallbackInfo,
51 return_ty: Update,
52 default_ret: Update::DoNothing,
53 invoker_static: TREE_VIEW_ON_NODE_CLICK_INVOKER,
54 invoker_ty: AzTreeViewOnNodeClickCallbackInvoker,
55 thunk_fn: az_tree_view_on_node_click_callback_thunk,
56 setter_fn: AzApp_setTreeViewOnNodeClickCallbackInvoker,
57 from_handle_fn: AzTreeViewOnNodeClickCallback_createFromHostHandle,
58 extra_args: [ node_index: usize ],
59}
60
61const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
64const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
65const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
66 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
67
68const TEXT_COLOR: ColorU = ColorU { r: 30, g: 30, b: 30, a: 255 };
71const SELECTED_BG: ColorU = ColorU { r: 0, g: 120, b: 215, a: 255 };
72const SELECTED_TEXT: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
73const HOVER_BG: ColorU = ColorU { r: 229, g: 243, b: 255, a: 255 };
74const ICON_COLOR: ColorU = ColorU { r: 100, g: 100, b: 100, a: 255 };
75
76static TREE_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
79 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
80 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
81 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
82 CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
83 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: TEXT_COLOR })),
84];
85
86static ROW_STYLE: &[CssPropertyWithConditions] = &[
89 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
90 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
91 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
92 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
93 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(2))),
94 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
95 CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(4))),
96 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
97 CssPropertyWithConditions::on_hover(CssProperty::const_background_content(
99 StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(HOVER_BG)]),
100 )),
101];
102
103static ROW_SELECTED_STYLE: &[CssPropertyWithConditions] = &[
109 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
110 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
111 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
112 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
113 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(2))),
114 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
115 CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(4))),
116 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
117 CssPropertyWithConditions::simple(CssProperty::const_background_content(
118 StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(SELECTED_BG)]),
119 )),
120 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: SELECTED_TEXT })),
121];
122
123static CHILDREN_STYLE: &[CssPropertyWithConditions] = &[
126 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
127 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
128 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(16))),
129];
130
131static ICON_STYLE: &[CssPropertyWithConditions] = &[
136 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(16))),
137 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
138 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: ICON_COLOR })),
139];
140
141static LEAF_SPACER_STYLE: &[CssPropertyWithConditions] = &[
144 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(16))),
145 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
146];
147
148static LABEL_STYLE: &[CssPropertyWithConditions] = &[
151 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
152 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
153];
154
155#[derive(Debug, Clone, PartialEq)]
161#[repr(C)]
162pub struct TreeViewNode {
163 pub label: AzString,
165 pub children: TreeViewNodeVec,
167 pub is_expanded: bool,
169 pub is_selected: bool,
171}
172
173impl TreeViewNode {
174 pub fn new<S: Into<AzString>>(label: S) -> Self {
176 Self {
177 label: label.into(),
178 children: TreeViewNodeVec::from_const_slice(&[]),
179 is_expanded: false,
180 is_selected: false,
181 }
182 }
183
184 pub fn add_child(&mut self, child: Self) {
186 self.children.push(child);
187 }
188
189 #[must_use] pub fn with_child(mut self, child: Self) -> Self {
191 self.children.push(child);
192 self
193 }
194
195 #[must_use] pub const fn with_expanded(mut self, expanded: bool) -> Self {
197 self.is_expanded = expanded;
198 self
199 }
200
201 #[must_use] pub const fn with_selected(mut self, selected: bool) -> Self {
203 self.is_selected = selected;
204 self
205 }
206}
207
208impl_option!(TreeViewNode, OptionTreeViewNode, copy = false, [Debug, Clone, PartialEq]);
209impl_vec!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor, TreeViewNodeVecDestructorType, TreeViewNodeVecSlice, OptionTreeViewNode);
210impl_vec_clone!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor);
211impl_vec_debug!(TreeViewNode, TreeViewNodeVec);
212impl_vec_partialeq!(TreeViewNode, TreeViewNodeVec);
213impl_vec_mut!(TreeViewNode, TreeViewNodeVec);
214
215#[derive(Debug, Clone, PartialEq)]
217#[repr(C)]
218pub struct TreeView {
219 pub root: TreeViewNode,
221 pub on_node_click: OptionTreeViewOnNodeClick,
223}
224
225impl TreeView {
226 #[must_use] pub fn new(root: TreeViewNode) -> Self {
228 Self {
229 root,
230 on_node_click: None.into(),
231 }
232 }
233
234 pub fn set_on_node_click<C: Into<TreeViewOnNodeClickCallback>>(
236 &mut self,
237 data: RefAny,
238 callback: C,
239 ) {
240 self.on_node_click = Some(TreeViewOnNodeClick {
241 callback: callback.into(),
242 refany: data,
243 })
244 .into();
245 }
246
247 #[must_use]
249 pub fn with_on_node_click<C: Into<TreeViewOnNodeClickCallback>>(
250 mut self,
251 data: RefAny,
252 callback: C,
253 ) -> Self {
254 self.set_on_node_click(data, callback);
255 self
256 }
257
258 #[must_use] pub fn dom(self) -> Dom {
260 const TREE_CLASS: &[IdOrClass] =
261 &[Class(AzString::from_const_str("__azul-native-tree-view"))];
262
263 let on_node_click = self.on_node_click;
264 let root = self.root;
265
266 let mut children = Vec::new();
267 let mut index: usize = 0;
268 render_node(&root, &on_node_click, &mut index, &mut children);
269
270 Dom::create_div()
271 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TREE_CONTAINER_STYLE))
272 .with_ids_and_classes(IdOrClassVec::from_const_slice(TREE_CLASS))
273 .with_children(DomVec::from_vec(children))
274 }
275}
276
277fn render_node(
282 node: &TreeViewNode,
283 on_click: &OptionTreeViewOnNodeClick,
284 index: &mut usize,
285 out: &mut Vec<Dom>,
286) {
287 let current_index = *index;
288 *index += 1;
289
290 let has_children = !node.children.as_slice().is_empty();
291
292 let row_style = if node.is_selected {
294 ROW_SELECTED_STYLE
295 } else {
296 ROW_STYLE
297 };
298
299 let icon_or_spacer = if has_children {
301 let icon_name = if node.is_expanded {
302 "expand_more"
303 } else {
304 "chevron_right"
305 };
306 Dom::create_icon(AzString::from_const_str(icon_name))
307 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ICON_STYLE))
308 } else {
309 Dom::create_div()
311 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LEAF_SPACER_STYLE))
312 };
313
314 let label = Dom::create_text(node.label.clone())
316 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE));
317
318 let mut row = Dom::create_div()
320 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(row_style))
321 .with_tab_index(TabIndex::Auto)
322 .with_children(DomVec::from_vec(vec![icon_or_spacer, label]));
323
324 if let Some(cb) = on_click.as_ref() {
326 let cb_data = NodeClickData {
327 node_index: current_index,
328 on_node_click: Some(TreeViewOnNodeClick {
329 callback: cb.callback.clone(),
330 refany: cb.refany.clone(),
331 })
332 .into(),
333 };
334 row = row.with_callbacks(
335 vec![CoreCallbackData {
336 event: EventFilter::Hover(HoverEventFilter::MouseUp),
337 refany: RefAny::new(cb_data),
338 callback: CoreCallback {
339 cb: on_tree_node_click as usize,
340 ctx: azul_core::refany::OptionRefAny::None,
341 },
342 }]
343 .into(),
344 );
345 }
346
347 out.push(row);
348
349 if has_children && node.is_expanded {
351 let mut child_doms = Vec::new();
352 for child in node.children.as_slice() {
353 render_node(child, on_click, index, &mut child_doms);
354 }
355
356 let children_container = Dom::create_div()
357 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(CHILDREN_STYLE))
358 .with_children(DomVec::from_vec(child_doms));
359
360 out.push(children_container);
361 } else if has_children {
362 count_descendants(node.children.as_slice(), index);
364 }
365}
366
367fn count_descendants(nodes: &[TreeViewNode], index: &mut usize) {
369 for node in nodes {
370 *index += 1;
371 if !node.children.as_slice().is_empty() {
372 count_descendants(node.children.as_slice(), index);
373 }
374 }
375}
376
377struct NodeClickData {
382 node_index: usize,
383 on_node_click: OptionTreeViewOnNodeClick,
384}
385
386extern "C" fn on_tree_node_click(mut refany: RefAny, info: CallbackInfo) -> Update {
391 let Some(mut refany) = refany.downcast_mut::<NodeClickData>() else {
392 return Update::DoNothing;
393 };
394
395 let node_index = refany.node_index;
396
397 match refany.on_node_click.as_mut() {
398 Some(TreeViewOnNodeClick { refany, callback }) => {
399 (callback.cb)(refany.clone(), info, node_index)
400 }
401 None => Update::DoNothing,
402 }
403}
404
405impl From<TreeView> for Dom {
410 fn from(tv: TreeView) -> Self {
411 tv.dom()
412 }
413}
414
415#[cfg(test)]
416mod autotest_generated {
417 use std::{
418 collections::BTreeMap,
419 sync::{Arc, Mutex},
420 };
421
422 use azul_core::{
423 dom::{DomId, DomNodeId, NodeId, NodeType},
424 geom::OptionLogicalPosition,
425 gl::OptionGlContextPtr,
426 hit_test::ScrollPosition,
427 refany::OptionRefAny,
428 resources::RendererResources,
429 styled_dom::NodeHierarchyItemId,
430 window::{MonitorVec, RawWindowHandle},
431 };
432 use azul_css::system::SystemStyle;
433 use rust_fontconfig::FcFontCache;
434
435 use super::*;
436 #[cfg(feature = "icu")]
437 use crate::icu::IcuLocalizerHandle;
438 use crate::{
439 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
440 window::LayoutWindow,
441 window_state::FullWindowState,
442 };
443
444 fn leaf(label: &str) -> TreeViewNode {
449 TreeViewNode::new(label)
450 }
451
452 fn subtree_len(node: &TreeViewNode) -> usize {
456 1 + node
457 .children
458 .as_slice()
459 .iter()
460 .map(subtree_len)
461 .sum::<usize>()
462 }
463
464 fn wide(n: usize, expanded: bool) -> TreeViewNode {
466 let mut root = leaf("wide").with_expanded(expanded);
467 for i in 0..n {
468 root.add_child(leaf(&format!("c{i}")));
469 }
470 root
471 }
472
473 fn chain(depth: usize, expanded: bool) -> TreeViewNode {
477 assert!(depth >= 1, "a chain has at least the root");
478 let mut node = leaf("tip").with_expanded(expanded);
479 for i in 1..depth {
480 node = leaf(&format!("n{i}"))
481 .with_child(node)
482 .with_expanded(expanded);
483 }
484 node
485 }
486
487 fn deep_mixed() -> TreeViewNode {
490 leaf("root")
491 .with_expanded(true)
492 .with_child(
493 leaf("a")
494 .with_expanded(false) .with_child(leaf("a1").with_expanded(true).with_child(leaf("a1x"))),
496 )
497 .with_child(
498 leaf("b")
499 .with_expanded(true)
500 .with_child(leaf("b1"))
501 .with_child(leaf("b2").with_expanded(true).with_child(leaf("b2x"))),
502 )
503 .with_child(leaf("c").with_selected(true))
504 }
505
506 fn shapes() -> Vec<TreeViewNode> {
511 vec![
512 leaf("solo"),
513 leaf("solo-expanded").with_expanded(true), leaf("solo-selected").with_selected(true),
515 leaf("p").with_child(leaf("a")).with_child(leaf("b")),
516 leaf("p")
517 .with_child(leaf("a"))
518 .with_child(leaf("b"))
519 .with_expanded(true),
520 leaf("p")
521 .with_child(leaf("a").with_expanded(true).with_child(leaf("a1")))
522 .with_expanded(true),
523 leaf("p").with_child(leaf("a").with_expanded(true).with_child(leaf("a1"))),
524 leaf("p")
525 .with_child(leaf("a").with_child(leaf("a1")))
526 .with_expanded(true),
527 deep_mixed(),
528 wide(64, false),
529 wide(64, true),
530 ]
531 }
532
533 fn pathological_labels() -> Vec<String> {
538 vec![
539 String::new(),
540 " ".to_string(),
541 "a\u{0}b".to_string(),
542 "👨👩👧👦".to_string(),
543 "مرحبا".to_string(),
544 "e\u{0301}\u{0301}\u{0301}".to_string(),
545 "\u{200b}\u{feff}".to_string(),
546 "\u{202e}gnirts".to_string(),
547 "line\nbreak\ttab\r".to_string(),
548 "chevron_right".to_string(),
549 "x".repeat(100_000),
550 ]
551 }
552
553 fn on_big_stack<F: FnOnce() + Send + 'static>(f: F) {
559 std::thread::Builder::new()
560 .stack_size(64 * 1024 * 1024)
561 .spawn(f)
562 .expect("spawning the deep-recursion thread failed")
563 .join()
564 .expect("deep-recursion thread panicked");
565 }
566
567 fn text_of(dom: &Dom) -> Option<&str> {
572 match dom.root.get_node_type() {
573 NodeType::Text(s) => Some(s.as_ref().as_str()),
574 _ => None,
575 }
576 }
577
578 fn icon_of(dom: &Dom) -> Option<&str> {
579 match dom.root.get_node_type() {
580 NodeType::Icon(s) => Some(s.as_ref().as_str()),
581 _ => None,
582 }
583 }
584
585 fn style_is(dom: &Dom, expected: &'static [CssPropertyWithConditions]) -> bool {
587 *dom.root.get_style()
588 == azul_css::css::Css::from(CssPropertyWithConditionsVec::from_const_slice(expected))
589 }
590
591 fn row_parts(row: &Dom) -> (&Dom, &Dom) {
593 let ch = row.children.as_ref();
594 assert_eq!(ch.len(), 2, "every row is [icon|spacer, label]");
595 (&ch[0], &ch[1])
596 }
597
598 fn collect_rows<'a>(nodes: &'a [Dom], out: &mut Vec<&'a Dom>) {
602 for n in nodes {
603 if n.root.get_tab_index().is_some() {
604 out.push(n);
605 } else {
606 collect_rows(n.children.as_ref(), out);
607 }
608 }
609 }
610
611 fn rows_of(nodes: &[Dom]) -> Vec<&Dom> {
612 let mut out = Vec::new();
613 collect_rows(nodes, &mut out);
614 out
615 }
616
617 fn click_index_of(row: &Dom) -> Option<usize> {
620 let mut data = row.root.get_callbacks().as_ref().first()?.refany.clone();
621 let payload = data
622 .downcast_ref::<NodeClickData>()
623 .expect("a row callback payload is always a NodeClickData");
624 let index = payload.node_index;
625 drop(payload);
626 Some(index)
627 }
628
629 fn rendered_pairs(nodes: &[Dom]) -> Vec<(usize, String)> {
631 rows_of(nodes)
632 .iter()
633 .map(|row| {
634 let (_, label) = row_parts(row);
635 (
636 click_index_of(row).expect("row must carry a click payload"),
637 text_of(label)
638 .expect("a row's second child is the label text node")
639 .to_string(),
640 )
641 })
642 .collect()
643 }
644
645 fn expected_pairs(node: &TreeViewNode, next: &mut usize, out: &mut Vec<(usize, String)>) {
649 let index = *next;
650 *next += 1;
651 out.push((index, node.label.as_str().to_string()));
652
653 let children = node.children.as_slice();
654 if node.is_expanded && !children.is_empty() {
655 for c in children {
656 expected_pairs(c, next, out);
657 }
658 } else {
659 *next += subtree_len(node) - 1;
661 }
662 }
663
664 fn expected_of(tree: &TreeViewNode, start: usize) -> Vec<(usize, String)> {
665 let mut next = start;
666 let mut out = Vec::new();
667 expected_pairs(tree, &mut next, &mut out);
668 out
669 }
670
671 fn recursive_descendants(dom: &Dom) -> usize {
674 dom.children
675 .as_ref()
676 .iter()
677 .map(|c| 1 + recursive_descendants(c))
678 .sum()
679 }
680
681 fn assert_estimates_consistent(dom: &Dom) {
682 assert_eq!(
683 dom.estimated_total_children,
684 recursive_descendants(dom),
685 "estimated_total_children desynced from the real subtree size"
686 );
687 for c in dom.children.as_ref() {
688 assert_estimates_consistent(c);
689 }
690 }
691
692 type ClickLog = Arc<Mutex<Vec<usize>>>;
697
698 const SENTINEL: usize = 1_000_000;
701
702 extern "C" fn record_click(mut data: RefAny, _info: CallbackInfo, node_index: usize) -> Update {
703 if let Some(log) = data.downcast_ref::<ClickLog>() {
704 log.lock().expect("click log poisoned").push(node_index);
705 }
706 Update::RefreshDom
707 }
708
709 extern "C" fn record_click_all_windows(
713 mut data: RefAny,
714 _info: CallbackInfo,
715 node_index: usize,
716 ) -> Update {
717 if let Some(log) = data.downcast_ref::<ClickLog>() {
718 log.lock()
719 .expect("click log poisoned")
720 .push(node_index.wrapping_add(SENTINEL));
721 }
722 Update::RefreshDomAllWindows
723 }
724
725 fn cb(f: TreeViewOnNodeClickCallbackType) -> TreeViewOnNodeClickCallback {
727 f.into()
728 }
729
730 fn new_log() -> ClickLog {
731 Arc::new(Mutex::new(Vec::new()))
732 }
733
734 fn entries(log: &ClickLog) -> Vec<usize> {
735 log.lock().expect("click log poisoned").clone()
736 }
737
738 fn some_click(f: TreeViewOnNodeClickCallbackType, log: &ClickLog) -> OptionTreeViewOnNodeClick {
739 Some(TreeViewOnNodeClick {
740 callback: cb(f),
741 refany: RefAny::new(log.clone()),
742 })
743 .into()
744 }
745
746 fn run_clicks(payloads: Vec<RefAny>) -> Vec<Update> {
750 let layout_window =
751 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
752 let renderer_resources = RendererResources::default();
753 let previous_window_state: Option<FullWindowState> = None;
754 let current_window_state = FullWindowState::default();
755 let gl_context = OptionGlContextPtr::None;
756 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
757 BTreeMap::new();
758 let window_handle = RawWindowHandle::Unsupported;
759 let system_callbacks = ExternalSystemCallbacks::rust_internal();
760
761 let ref_data = CallbackInfoRefData {
762 layout_window: &layout_window,
763 renderer_resources: &renderer_resources,
764 previous_window_state: &previous_window_state,
765 current_window_state: ¤t_window_state,
766 gl_context: &gl_context,
767 current_scroll_manager: &scroll_states,
768 current_window_handle: &window_handle,
769 system_callbacks: &system_callbacks,
770 system_style: Arc::new(SystemStyle::default()),
771 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
772 #[cfg(feature = "icu")]
773 icu_localizer: IcuLocalizerHandle::default(),
774 ctx: OptionRefAny::None,
775 };
776
777 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
778
779 let info = CallbackInfo::new(
780 &ref_data,
781 &changes,
782 DomNodeId {
783 dom: DomId::ROOT_ID,
784 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
785 },
786 OptionLogicalPosition::None,
787 OptionLogicalPosition::None,
788 );
789
790 payloads
791 .into_iter()
792 .map(|p| on_tree_node_click(p, info))
793 .collect()
794 }
795
796 #[test]
801 fn new_defaults_to_a_collapsed_unselected_childless_node() {
802 let node = TreeViewNode::new("Root");
803
804 assert_eq!(node.label.as_str(), "Root");
805 assert!(
806 node.children.as_slice().is_empty(),
807 "a fresh node has no children"
808 );
809 assert_eq!(node.children.len(), 0);
810 assert!(
811 node.children.capacity() >= node.children.len(),
812 "len must never exceed capacity"
813 );
814 assert!(!node.is_expanded, "a fresh node is collapsed");
815 assert!(!node.is_selected, "a fresh node is unselected");
816 }
817
818 #[test]
819 fn new_preserves_pathological_labels_byte_for_byte() {
820 for label in pathological_labels() {
821 let node = TreeViewNode::new(label.clone());
822 assert_eq!(
823 node.label.as_str(),
824 label.as_str(),
825 "label must survive verbatim"
826 );
827 assert_eq!(
828 node.label.as_str().len(),
829 label.len(),
830 "an embedded NUL must not truncate the label"
831 );
832 assert!(!node.is_expanded);
834 assert!(!node.is_selected);
835 assert!(node.children.as_slice().is_empty());
836 }
837 }
838
839 #[test]
840 fn new_accepts_every_into_azstring_source_identically() {
841 let from_str = TreeViewNode::new("same");
842 let from_string = TreeViewNode::new("same".to_string());
843 let from_azstring = TreeViewNode::new(AzString::from("same"));
844
845 assert_eq!(from_str, from_string);
846 assert_eq!(from_str, from_azstring);
847 }
848
849 #[test]
850 fn new_with_a_megabyte_label_does_not_truncate_or_panic() {
851 let huge = "λ".repeat(500_000); let node = TreeViewNode::new(huge.clone());
853 assert_eq!(node.label.as_str().len(), huge.len());
854 assert_eq!(node.label.as_str(), huge);
855 }
856
857 #[test]
862 fn add_child_and_with_child_agree() {
863 let mut mutated = leaf("root");
864 mutated.add_child(leaf("a"));
865 mutated.add_child(leaf("b"));
866
867 let built = leaf("root").with_child(leaf("a")).with_child(leaf("b"));
868
869 assert_eq!(
870 mutated, built,
871 "the builder and the mutator must produce the same node"
872 );
873 }
874
875 #[test]
876 fn add_child_preserves_order_duplicates_and_len_capacity_invariants() {
877 let n = 5_000;
878 let mut root = leaf("root");
879 for i in 0..n {
880 root.add_child(leaf(&format!("c{i}")));
881 assert_eq!(root.children.len(), i + 1, "len must track every push");
882 assert!(
883 root.children.capacity() >= root.children.len(),
884 "capacity must never fall below len"
885 );
886 }
887 root.add_child(leaf("c0"));
889 assert_eq!(root.children.len(), n + 1, "duplicates are kept, not merged");
890 assert_eq!(root.children.as_slice()[0].label.as_str(), "c0");
891 assert_eq!(root.children.as_slice()[n - 1].label.as_str(), "c4999");
892 assert_eq!(root.children.as_slice()[n].label.as_str(), "c0");
893 assert_eq!(subtree_len(&root), n + 2);
894 }
895
896 #[test]
897 fn child_vec_survives_the_borrowed_to_owned_transition() {
898 let mut root = leaf("root");
903 assert_eq!(root.children.capacity(), 0);
904
905 root.add_child(leaf("a"));
906 root.add_child(leaf("b"));
907
908 let mut copy = root.clone();
909 copy.add_child(leaf("c"));
910 copy.children.as_mut()[0].label = AzString::from("mutated");
911
912 assert_eq!(root.children.len(), 2, "the original must not see the push");
913 assert_eq!(
914 root.children.as_slice()[0].label.as_str(),
915 "a",
916 "the clone must own its own child storage"
917 );
918 assert_eq!(copy.children.len(), 3);
919 assert_eq!(copy.children.as_slice()[0].label.as_str(), "mutated");
920
921 drop(copy);
922 assert_eq!(root.children.as_slice()[1].label.as_str(), "b");
924 }
925
926 #[test]
927 fn with_child_nests_arbitrarily_deep_without_panicking() {
928 on_big_stack(|| {
929 let depth = 1_000;
930 let root = chain(depth, true);
931 assert_eq!(subtree_len(&root), depth);
932
933 let copy = root.clone();
935 assert_eq!(copy, root);
936 drop(copy);
937 drop(root);
938 });
939 }
940
941 #[test]
946 fn with_expanded_and_with_selected_are_orthogonal_and_idempotent() {
947 for expanded in [false, true] {
948 for selected in [false, true] {
949 let node = leaf("n").with_expanded(expanded).with_selected(selected);
950 assert_eq!(node.is_expanded, expanded);
951 assert_eq!(node.is_selected, selected);
952
953 let flipped = leaf("n").with_selected(selected).with_expanded(expanded);
955 assert_eq!(node, flipped);
956
957 let twice = node
959 .clone()
960 .with_expanded(expanded)
961 .with_selected(selected);
962 assert_eq!(node, twice);
963
964 let overwritten = node.clone().with_expanded(!expanded);
966 assert_eq!(overwritten.is_expanded, !expanded);
967 assert_eq!(
968 overwritten.is_selected, selected,
969 "with_expanded must not touch is_selected"
970 );
971 }
972 }
973 }
974
975 #[test]
976 fn state_builders_do_not_disturb_label_or_children() {
977 let base = leaf("keep me").with_child(leaf("a")).with_child(leaf("b"));
978 let styled = base
979 .clone()
980 .with_expanded(true)
981 .with_selected(true)
982 .with_expanded(false);
983
984 assert_eq!(styled.label, base.label);
985 assert_eq!(styled.children, base.children);
986 assert!(!styled.is_expanded);
987 assert!(styled.is_selected);
988 }
989
990 #[test]
991 fn nodes_differing_only_in_state_are_not_equal() {
992 let base = leaf("n");
993 assert_ne!(base, base.clone().with_expanded(true));
994 assert_ne!(base, base.clone().with_selected(true));
995 assert_ne!(base, base.clone().with_child(leaf("a")));
996 assert_ne!(base, leaf("m"));
997 }
998
999 #[test]
1000 fn equality_ignores_how_the_child_vec_was_built() {
1001 let pushed = leaf("root").with_child(leaf("a")).with_child(leaf("b"));
1002 let from_vec = TreeViewNode {
1003 label: AzString::from("root"),
1004 children: TreeViewNodeVec::from_vec(vec![leaf("a"), leaf("b")]),
1005 is_expanded: false,
1006 is_selected: false,
1007 };
1008 assert_eq!(
1009 pushed, from_vec,
1010 "the vec's allocation strategy must not leak into equality"
1011 );
1012 }
1013
1014 #[test]
1019 fn treeview_new_keeps_the_root_intact_and_installs_no_callback() {
1020 for root in shapes() {
1021 let tv = TreeView::new(root.clone());
1022 assert_eq!(tv.root, root, "new must not rewrite the tree");
1023 assert!(
1024 tv.on_node_click.as_ref().is_none(),
1025 "new must not install a callback"
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn set_on_node_click_installs_then_overwrites() {
1032 let log = new_log();
1033 let mut tv = TreeView::new(leaf("root"));
1034
1035 tv.set_on_node_click(RefAny::new(log.clone()), cb(record_click));
1036 assert!(tv.on_node_click.as_ref().is_some());
1037
1038 tv.set_on_node_click(RefAny::new(log.clone()), cb(record_click_all_windows));
1039 let installed = tv
1040 .on_node_click
1041 .as_ref()
1042 .expect("a callback is still installed");
1043 assert_eq!(
1044 installed.callback,
1045 cb(record_click_all_windows),
1046 "the last write must win"
1047 );
1048 assert_ne!(installed.callback, cb(record_click));
1049 }
1050
1051 #[test]
1052 fn with_on_node_click_matches_set_on_node_click() {
1053 let data = RefAny::new(new_log());
1057 let mut mutated = TreeView::new(leaf("root"));
1058 mutated.set_on_node_click(data.clone(), cb(record_click));
1059
1060 let built = TreeView::new(leaf("root")).with_on_node_click(data.clone(), cb(record_click));
1061
1062 assert_eq!(mutated, built);
1063 }
1064
1065 #[test]
1070 fn count_descendants_of_an_empty_slice_is_a_no_op_even_at_usize_max() {
1071 for start in [0usize, 1, usize::MAX / 2, usize::MAX - 1, usize::MAX] {
1073 let mut index = start;
1074 count_descendants(&[], &mut index);
1075 assert_eq!(
1076 index, start,
1077 "an empty slice must not touch the counter (and must not overflow at MAX)"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn count_descendants_counts_every_node_regardless_of_expansion() {
1084 for shape in shapes() {
1085 let nodes = shape.children.as_slice();
1086 let expected: usize = nodes.iter().map(subtree_len).sum();
1087
1088 for start in [0usize, 7, 1_000_000] {
1089 let mut index = start;
1090 count_descendants(nodes, &mut index);
1091 assert_eq!(
1092 index - start,
1093 expected,
1094 "collapsed and expanded descendants must count the same"
1095 );
1096 }
1097 }
1098 }
1099
1100 #[test]
1101 fn count_descendants_reaches_exactly_usize_max_without_overflowing() {
1102 let tree = deep_mixed();
1103 let nodes = tree.children.as_slice();
1104 let total: usize = nodes.iter().map(subtree_len).sum();
1105
1106 let mut index = usize::MAX - total;
1107 count_descendants(nodes, &mut index);
1108 assert_eq!(
1109 index,
1110 usize::MAX,
1111 "landing exactly on usize::MAX must not overflow"
1112 );
1113 }
1114
1115 #[test]
1116 fn count_descendants_survives_a_deep_chain() {
1117 on_big_stack(|| {
1118 let depth = 10_000;
1119 let root = chain(depth, false);
1120 let mut index = 0usize;
1121 count_descendants(root.children.as_slice(), &mut index);
1122 assert_eq!(index, depth - 1, "every hidden descendant is counted once");
1123 });
1124 }
1125
1126 #[test]
1131 fn render_node_advance_equals_subtree_size_for_every_shape() {
1132 for shape in shapes() {
1136 let expected = subtree_len(&shape);
1137 for start in [0usize, 1, 12_345, usize::MAX / 4] {
1138 let mut index = start;
1139 let mut out = Vec::new();
1140 render_node(&shape, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
1141 assert_eq!(
1142 index - start,
1143 expected,
1144 "index advance must equal the subtree size, expanded or not"
1145 );
1146 assert!(!out.is_empty(), "every node renders at least its own row");
1147 }
1148 }
1149 }
1150
1151 #[test]
1152 fn render_node_emits_preorder_indices_for_visible_rows_only() {
1153 for shape in shapes() {
1154 let log = new_log();
1155 let on_click = some_click(record_click, &log);
1156
1157 let mut index = 0usize;
1158 let mut out = Vec::new();
1159 render_node(&shape, &on_click, &mut index, &mut out);
1160
1161 assert_eq!(
1162 rendered_pairs(&out),
1163 expected_of(&shape, 0),
1164 "rendered rows must match the independent pre-order model"
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn render_node_appends_and_offsets_from_a_nonzero_start_index() {
1171 let start = 12_345usize;
1172 let shape = deep_mixed();
1173 let log = new_log();
1174 let on_click = some_click(record_click, &log);
1175
1176 let mut out = vec![Dom::create_div(), Dom::create_text("sentinel")];
1178 let mut index = start;
1179 render_node(&shape, &on_click, &mut index, &mut out);
1180
1181 assert_eq!(
1182 text_of(&out[1]),
1183 Some("sentinel"),
1184 "render_node must append to `out`, never rewrite it"
1185 );
1186 assert_eq!(
1187 rendered_pairs(&out[2..]),
1188 expected_of(&shape, start),
1189 "a non-zero start index must offset every emitted index"
1190 );
1191 assert_eq!(index, start + subtree_len(&shape));
1192 }
1193
1194 #[test]
1195 fn render_node_lands_exactly_on_usize_max_without_overflowing() {
1196 let tree = leaf("root")
1199 .with_child(leaf("a"))
1200 .with_child(leaf("b"))
1201 .with_expanded(true);
1202 assert_eq!(subtree_len(&tree), 3);
1203
1204 let log = new_log();
1205 let on_click = some_click(record_click, &log);
1206
1207 let mut index = usize::MAX - 3;
1208 let mut out = Vec::new();
1209 render_node(&tree, &on_click, &mut index, &mut out);
1210
1211 assert_eq!(index, usize::MAX, "must land exactly on MAX, not wrap");
1212 let indices: Vec<usize> = rows_of(&out)
1213 .iter()
1214 .filter_map(|r| click_index_of(r))
1215 .collect();
1216 assert_eq!(
1217 indices,
1218 vec![usize::MAX - 3, usize::MAX - 2, usize::MAX - 1],
1219 "extreme indices must be carried verbatim into the click payloads"
1220 );
1221 }
1222
1223 #[cfg(all(debug_assertions, panic = "unwind"))]
1224 #[test]
1225 fn render_node_index_overflow_is_loud_not_silently_wrapped() {
1226 let node = leaf("boom");
1231 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1232 let mut index = usize::MAX;
1233 let mut out = Vec::new();
1234 render_node(&node, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
1235 index
1236 }));
1237
1238 match result {
1239 Err(_) => {} Ok(index) => assert_eq!(
1241 index, 0,
1242 "without overflow checks the counter must wrap cleanly, not corrupt"
1243 ),
1244 }
1245 }
1246
1247 #[test]
1248 fn render_node_without_a_callback_attaches_none() {
1249 for shape in shapes() {
1250 let mut index = 0usize;
1251 let mut out = Vec::new();
1252 render_node(&shape, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
1253
1254 for row in rows_of(&out) {
1255 assert!(
1256 row.root.get_callbacks().as_ref().is_empty(),
1257 "no callback configured => no callback attached"
1258 );
1259 }
1260 }
1261 }
1262
1263 #[test]
1264 fn render_node_survives_a_deep_expanded_chain() {
1265 on_big_stack(|| {
1266 let depth = 800;
1267 let root = chain(depth, true);
1268
1269 let mut index = 0usize;
1270 let mut out = Vec::new();
1271 render_node(&root, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
1272
1273 assert_eq!(index, depth, "one index per level");
1274 assert_eq!(rows_of(&out).len(), depth, "every level renders one row");
1275 drop(out);
1276 });
1277 }
1278
1279 #[test]
1280 fn render_node_handles_a_wide_fanout() {
1281 let n = 5_000;
1282 let root = wide(n, true);
1283 let log = new_log();
1284 let on_click = some_click(record_click, &log);
1285
1286 let mut index = 0usize;
1287 let mut out = Vec::new();
1288 render_node(&root, &on_click, &mut index, &mut out);
1289
1290 assert_eq!(index, n + 1);
1291 assert_eq!(out.len(), 2, "an expanded parent emits [row, container]");
1292 assert_eq!(out[1].children.as_ref().len(), n, "every child gets a row");
1293
1294 let indices: Vec<usize> = rows_of(&out)
1295 .iter()
1296 .filter_map(|r| click_index_of(r))
1297 .collect();
1298 assert_eq!(indices, (0..=n).collect::<Vec<_>>());
1299 }
1300
1301 #[test]
1306 fn dom_root_carries_the_container_class_and_style() {
1307 let dom = TreeView::new(leaf("root")).dom();
1308
1309 let classes = dom.root.get_ids_and_classes();
1310 assert!(
1311 classes
1312 .as_ref()
1313 .iter()
1314 .any(|c| matches!(c, IdOrClass::Class(s) if s.as_str() == "__azul-native-tree-view")),
1315 "the container must be findable by its widget class"
1316 );
1317 assert!(
1318 style_is(&dom, TREE_CONTAINER_STYLE),
1319 "the container must use the shared const style"
1320 );
1321 }
1322
1323 #[test]
1324 fn dom_leaf_renders_a_spacer_and_no_icon() {
1325 let dom = TreeView::new(leaf("only")).dom();
1326 assert_eq!(dom.children.as_ref().len(), 1, "a leaf emits just its row");
1327
1328 let row = &dom.children.as_ref()[0];
1329 let (icon, label) = row_parts(row);
1330 assert_eq!(icon_of(icon), None, "a childless node gets no disclosure icon");
1331 assert!(
1332 style_is(icon, LEAF_SPACER_STYLE),
1333 "the placeholder must use the leaf-spacer style so labels stay aligned"
1334 );
1335 assert_eq!(text_of(label), Some("only"));
1336 assert!(style_is(label, LABEL_STYLE));
1337 }
1338
1339 #[test]
1340 fn dom_expanded_parent_uses_expand_more_and_emits_a_container() {
1341 let tree = leaf("p")
1342 .with_child(leaf("a"))
1343 .with_child(leaf("b"))
1344 .with_expanded(true);
1345 let dom = TreeView::new(tree).dom();
1346
1347 assert_eq!(
1348 dom.children.as_ref().len(),
1349 2,
1350 "an expanded parent emits [row, children container]"
1351 );
1352 let (icon, _) = row_parts(&dom.children.as_ref()[0]);
1353 assert_eq!(icon_of(icon), Some("expand_more"));
1354 assert!(style_is(icon, ICON_STYLE));
1355
1356 let container = &dom.children.as_ref()[1];
1357 assert!(style_is(container, CHILDREN_STYLE));
1358 assert_eq!(container.children.as_ref().len(), 2, "both children drawn");
1359 }
1360
1361 #[test]
1362 fn dom_collapsed_parent_uses_chevron_and_draws_no_children() {
1363 let tree = leaf("p").with_child(leaf("a")).with_child(leaf("b"));
1364 let dom = TreeView::new(tree).dom();
1365
1366 assert_eq!(
1367 dom.children.as_ref().len(),
1368 1,
1369 "a collapsed parent must not emit a children container"
1370 );
1371 let (icon, _) = row_parts(&dom.children.as_ref()[0]);
1372 assert_eq!(icon_of(icon), Some("chevron_right"));
1373 assert_eq!(rows_of(dom.children.as_ref()).len(), 1, "children stay hidden");
1374 }
1375
1376 #[test]
1377 fn dom_expanded_but_childless_node_still_renders_a_spacer() {
1378 let dom = TreeView::new(leaf("empty").with_expanded(true)).dom();
1380 assert_eq!(dom.children.as_ref().len(), 1, "nothing to expand into");
1381 let (icon, _) = row_parts(&dom.children.as_ref()[0]);
1382 assert_eq!(icon_of(icon), None);
1383 assert!(style_is(icon, LEAF_SPACER_STYLE));
1384 }
1385
1386 #[test]
1387 fn dom_selected_rows_use_the_selected_style() {
1388 let tree = leaf("p")
1389 .with_expanded(true)
1390 .with_child(leaf("a").with_selected(true))
1391 .with_child(leaf("b"));
1392 let dom = TreeView::new(tree).dom();
1393 let rows = rows_of(dom.children.as_ref());
1394 assert_eq!(rows.len(), 3);
1395
1396 assert!(style_is(rows[0], ROW_STYLE), "unselected root uses ROW_STYLE");
1397 assert!(
1398 style_is(rows[1], ROW_SELECTED_STYLE),
1399 "the selected node must switch to the selected style"
1400 );
1401 assert!(style_is(rows[2], ROW_STYLE));
1402 assert!(
1403 !style_is(rows[1], ROW_STYLE),
1404 "the two row styles must be distinguishable"
1405 );
1406 }
1407
1408 #[test]
1409 fn dom_keeps_estimated_total_children_consistent_for_every_shape() {
1410 for shape in shapes() {
1413 let dom = TreeView::new(shape).dom();
1414 assert_estimates_consistent(&dom);
1415 }
1416 }
1417
1418 #[test]
1419 fn dom_labels_survive_the_round_trip_unchanged() {
1420 let labels = pathological_labels();
1421 let mut root = leaf("root").with_expanded(true);
1422 for l in &labels {
1423 root.add_child(TreeViewNode::new(l.clone()));
1424 }
1425
1426 let dom = TreeView::new(root).dom();
1427 let rows = rows_of(dom.children.as_ref());
1428 assert_eq!(rows.len(), labels.len() + 1);
1429
1430 let rendered: Vec<&str> = rows[1..]
1431 .iter()
1432 .map(|r| text_of(row_parts(r).1).expect("label text node"))
1433 .collect();
1434 let expected: Vec<&str> = labels.iter().map(String::as_str).collect();
1435 assert_eq!(rendered, expected, "labels must survive byte-for-byte");
1436 }
1437
1438 #[test]
1439 fn dom_rows_are_focusable_and_carry_exactly_one_click_callback() {
1440 let log = new_log();
1441 let tv = TreeView::new(deep_mixed())
1442 .with_on_node_click(RefAny::new(log.clone()), cb(record_click));
1443 let dom = tv.dom();
1444
1445 for row in rows_of(dom.children.as_ref()) {
1446 assert!(
1447 matches!(row.root.get_tab_index(), Some(TabIndex::Auto)),
1448 "every row must be keyboard focusable"
1449 );
1450 let cbs = row.root.get_callbacks();
1451 assert_eq!(cbs.as_ref().len(), 1, "exactly one click callback per row");
1452 assert_eq!(
1453 cbs.as_ref()[0].event,
1454 EventFilter::Hover(HoverEventFilter::MouseUp),
1455 "rows fire on mouse-up"
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn dom_indices_skip_collapsed_subtrees_but_stay_preorder() {
1462 for shape in shapes() {
1463 let log = new_log();
1464 let dom = TreeView::new(shape.clone())
1465 .with_on_node_click(RefAny::new(log.clone()), cb(record_click))
1466 .dom();
1467
1468 assert_eq!(
1469 rendered_pairs(dom.children.as_ref()),
1470 expected_of(&shape, 0),
1471 "dom() must index nodes pre-order over the whole tree, \
1472 including the collapsed ones it does not draw"
1473 );
1474 }
1475 }
1476
1477 #[test]
1478 fn dom_of_an_empty_labelled_tree_does_not_panic() {
1479 let dom = TreeView::new(leaf("")).dom();
1480 let rows = rows_of(dom.children.as_ref());
1481 assert_eq!(rows.len(), 1);
1482 assert_eq!(text_of(row_parts(rows[0]).1), Some(""));
1483 }
1484
1485 #[test]
1486 fn from_treeview_for_dom_matches_dom() {
1487 for shape in shapes() {
1488 let via_trait: Dom = TreeView::new(shape.clone()).into();
1489 let via_method = TreeView::new(shape).dom();
1490 assert_eq!(via_trait, via_method);
1491 }
1492 }
1493
1494 #[test]
1495 fn dom_survives_a_deep_expanded_chain() {
1496 on_big_stack(|| {
1497 let depth = 800;
1498 let dom = TreeView::new(chain(depth, true)).dom();
1499 assert_eq!(rows_of(dom.children.as_ref()).len(), depth);
1500 assert_estimates_consistent(&dom);
1501 drop(dom);
1502 });
1503 }
1504
1505 #[test]
1510 fn click_with_a_foreign_payload_returns_do_nothing() {
1511 let payloads = vec![
1513 RefAny::new(0usize),
1514 RefAny::new(String::from("not a NodeClickData")),
1515 RefAny::new(leaf("also not one")),
1516 RefAny::new(()),
1517 ];
1518 let updates = run_clicks(payloads);
1519 assert_eq!(
1520 updates,
1521 vec![Update::DoNothing; 4],
1522 "a foreign payload must be a no-op, not a panic or a wild call"
1523 );
1524 }
1525
1526 #[test]
1527 fn click_without_a_user_callback_returns_do_nothing() {
1528 let payloads = vec![
1529 RefAny::new(NodeClickData {
1530 node_index: 0,
1531 on_node_click: OptionTreeViewOnNodeClick::None,
1532 }),
1533 RefAny::new(NodeClickData {
1534 node_index: usize::MAX,
1535 on_node_click: OptionTreeViewOnNodeClick::None,
1536 }),
1537 ];
1538 assert_eq!(run_clicks(payloads), vec![Update::DoNothing; 2]);
1539 }
1540
1541 #[test]
1542 fn click_forwards_the_index_verbatim_including_the_extremes() {
1543 let log = new_log();
1544 let indices = vec![0usize, 1, usize::MAX / 2, usize::MAX - 1, usize::MAX];
1545
1546 let payloads: Vec<RefAny> = indices
1547 .iter()
1548 .map(|i| {
1549 RefAny::new(NodeClickData {
1550 node_index: *i,
1551 on_node_click: some_click(record_click, &log),
1552 })
1553 })
1554 .collect();
1555
1556 let updates = run_clicks(payloads);
1557 assert_eq!(updates, vec![Update::RefreshDom; 5]);
1558 assert_eq!(
1559 entries(&log),
1560 indices,
1561 "the node index must reach the user callback unmodified"
1562 );
1563 }
1564
1565 #[test]
1566 fn click_propagates_the_user_update_verbatim() {
1567 let log = new_log();
1568 let payloads = vec![
1569 RefAny::new(NodeClickData {
1570 node_index: 3,
1571 on_node_click: some_click(record_click, &log),
1572 }),
1573 RefAny::new(NodeClickData {
1574 node_index: 4,
1575 on_node_click: some_click(record_click_all_windows, &log),
1576 }),
1577 ];
1578
1579 assert_eq!(
1580 run_clicks(payloads),
1581 vec![Update::RefreshDom, Update::RefreshDomAllWindows],
1582 "the dispatcher must not downgrade or upgrade the user's Update"
1583 );
1584 assert_eq!(entries(&log), vec![3, 4 + SENTINEL]);
1585 }
1586
1587 #[test]
1588 fn clicking_every_rendered_row_reports_its_visual_index() {
1589 let shape = deep_mixed();
1590 let log = new_log();
1591 let dom = TreeView::new(shape.clone())
1592 .with_on_node_click(RefAny::new(log.clone()), cb(record_click))
1593 .dom();
1594
1595 let payloads: Vec<RefAny> = rows_of(dom.children.as_ref())
1596 .iter()
1597 .map(|row| {
1598 row.root
1599 .get_callbacks()
1600 .as_ref()
1601 .first()
1602 .expect("every row carries the click callback")
1603 .refany
1604 .clone()
1605 })
1606 .collect();
1607
1608 let expected: Vec<usize> = expected_of(&shape, 0).into_iter().map(|(i, _)| i).collect();
1609 let updates = run_clicks(payloads);
1610
1611 assert_eq!(updates, vec![Update::RefreshDom; expected.len()]);
1612 assert_eq!(
1613 entries(&log),
1614 expected,
1615 "clicking row N must report N's pre-order index, collapsed siblings included"
1616 );
1617 }
1618}