1
//! Tree view widget with expandable/collapsible nodes.
2
//!
3
//! Provides [`TreeView`] and [`TreeViewNode`] for building hierarchical
4
//! tree structures with click callbacks and recursive DOM rendering.
5

            
6
use 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)] // widget/render module pulls in the css property/value types it builds with
15
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

            
30
use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq, impl_vec_mut};
31

            
32
use crate::callbacks::{Callback, CallbackInfo};
33

            
34
// -- Callback type via macro --
35

            
36
/// Callback invoked when a tree node is clicked.
37
///
38
/// The `usize` parameter is the depth-first index of the clicked node
39
/// (0 = root, then incremented in pre-order traversal).
40
pub type TreeViewOnNodeClickCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
41
impl_widget_callback!(
42
    TreeViewOnNodeClick,
43
    OptionTreeViewOnNodeClick,
44
    TreeViewOnNodeClickCallback,
45
    TreeViewOnNodeClickCallbackType
46
);
47

            
48
azul_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

            
61
// -- Font --
62

            
63
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
64
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
65
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
66
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
67

            
68
// -- Colors --
69

            
70
const TEXT_COLOR: ColorU = ColorU { r: 30, g: 30, b: 30, a: 255 };
71
const SELECTED_BG: ColorU = ColorU { r: 0, g: 120, b: 215, a: 255 };
72
const SELECTED_TEXT: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
73
const HOVER_BG: ColorU = ColorU { r: 229, g: 243, b: 255, a: 255 };
74
const ICON_COLOR: ColorU = ColorU { r: 100, g: 100, b: 100, a: 255 };
75

            
76
// -- Tree container style --
77

            
78
static 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

            
86
// -- Row style (each tree node row) --
87

            
88
static 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
    // Hover
98
    CssPropertyWithConditions::on_hover(CssProperty::const_background_content(
99
        StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(HOVER_BG)]),
100
    )),
101
];
102

            
103
// -- Selected row style --
104
// NOTE: Intentionally duplicates base properties from ROW_STYLE because
105
// const-slice styling does not support runtime composition. If you change
106
// padding/layout in ROW_STYLE, update ROW_SELECTED_STYLE to match.
107

            
108
static 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

            
123
// -- Children container style --
124

            
125
static 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

            
131
// -- Disclosure icon style --
132
// NOTE: Icon font-size (16px) must match LEAF_SPACER_STYLE width so that
133
// leaf nodes align with parent nodes that have a disclosure icon.
134

            
135
static 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

            
141
// -- Leaf spacer (same width as icon, for alignment) --
142

            
143
static 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

            
148
// -- Label style --
149

            
150
static 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
// ============================================================================
156
// Data structures
157
// ============================================================================
158

            
159
/// A single node in a tree hierarchy, with optional children.
160
#[derive(Debug, Clone, PartialEq)]
161
#[repr(C)]
162
pub struct TreeViewNode {
163
    /// Display text for this node.
164
    pub label: AzString,
165
    /// Child nodes nested under this node.
166
    pub children: TreeViewNodeVec,
167
    /// Whether children are visible (only meaningful when `children` is non-empty).
168
    pub is_expanded: bool,
169
    /// Whether this node is visually selected.
170
    pub is_selected: bool,
171
}
172

            
173
impl TreeViewNode {
174
    /// Creates a new collapsed, unselected leaf node with the given label.
175
23976
    pub fn new<S: Into<AzString>>(label: S) -> Self {
176
23976
        Self {
177
23976
            label: label.into(),
178
23976
            children: TreeViewNodeVec::from_const_slice(&[]),
179
23976
            is_expanded: false,
180
23976
            is_selected: false,
181
23976
        }
182
23976
    }
183

            
184
    /// Appends a child node.
185
11041
    pub fn add_child(&mut self, child: Self) {
186
11041
        self.children.push(child);
187
11041
    }
188

            
189
    /// Builder method: appends a child node.
190
12789
    #[must_use] pub fn with_child(mut self, child: Self) -> Self {
191
12789
        self.children.push(child);
192
12789
        self
193
12789
    }
194

            
195
    /// Builder method: sets the expanded state.
196
12751
    #[must_use] pub const fn with_expanded(mut self, expanded: bool) -> Self {
197
12751
        self.is_expanded = expanded;
198
12751
        self
199
12751
    }
200

            
201
    /// Builder method: sets the selected state.
202
35
    #[must_use] pub const fn with_selected(mut self, selected: bool) -> Self {
203
35
        self.is_selected = selected;
204
35
        self
205
35
    }
206
}
207

            
208
impl_option!(TreeViewNode, OptionTreeViewNode, copy = false, [Debug, Clone, PartialEq]);
209
impl_vec!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor, TreeViewNodeVecDestructorType, TreeViewNodeVecSlice, OptionTreeViewNode);
210
impl_vec_clone!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor);
211
impl_vec_debug!(TreeViewNode, TreeViewNodeVec);
212
impl_vec_partialeq!(TreeViewNode, TreeViewNodeVec);
213
impl_vec_mut!(TreeViewNode, TreeViewNodeVec);
214

            
215
/// Hierarchical tree view widget with expandable/collapsible nodes.
216
#[derive(Debug, Clone, PartialEq)]
217
#[repr(C)]
218
pub struct TreeView {
219
    /// Root node of the tree hierarchy.
220
    pub root: TreeViewNode,
221
    /// Optional callback fired when any node is clicked.
222
    pub on_node_click: OptionTreeViewOnNodeClick,
223
}
224

            
225
impl TreeView {
226
    /// Creates a new tree view with the given root node and no click callback.
227
71
    #[must_use] pub fn new(root: TreeViewNode) -> Self {
228
71
        Self {
229
71
            root,
230
71
            on_node_click: None.into(),
231
71
        }
232
71
    }
233

            
234
    /// Sets the callback invoked when any tree node is clicked.
235
17
    pub fn set_on_node_click<C: Into<TreeViewOnNodeClickCallback>>(
236
17
        &mut self,
237
17
        data: RefAny,
238
17
        callback: C,
239
17
    ) {
240
17
        self.on_node_click = Some(TreeViewOnNodeClick {
241
17
            callback: callback.into(),
242
17
            refany: data,
243
17
        })
244
17
        .into();
245
17
    }
246

            
247
    /// Builder method: sets the node-click callback.
248
    #[must_use]
249
14
    pub fn with_on_node_click<C: Into<TreeViewOnNodeClickCallback>>(
250
14
        mut self,
251
14
        data: RefAny,
252
14
        callback: C,
253
14
    ) -> Self {
254
14
        self.set_on_node_click(data, callback);
255
14
        self
256
14
    }
257

            
258
    /// Renders the tree view into a [`Dom`] subtree.
259
57
    #[must_use] pub fn dom(self) -> Dom {
260
        const TREE_CLASS: &[IdOrClass] =
261
            &[Class(AzString::from_const_str("__azul-native-tree-view"))];
262

            
263
57
        let on_node_click = self.on_node_click;
264
57
        let root = self.root;
265

            
266
57
        let mut children = Vec::new();
267
57
        let mut index: usize = 0;
268
57
        render_node(&root, &on_node_click, &mut index, &mut children);
269

            
270
57
        Dom::create_div()
271
57
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TREE_CONTAINER_STYLE))
272
57
            .with_ids_and_classes(IdOrClassVec::from_const_slice(TREE_CLASS))
273
57
            .with_children(DomVec::from_vec(children))
274
57
    }
275
}
276

            
277
// ============================================================================
278
// Internal: recursive DOM rendering
279
// ============================================================================
280

            
281
7512
fn render_node(
282
7512
    node: &TreeViewNode,
283
7512
    on_click: &OptionTreeViewOnNodeClick,
284
7512
    index: &mut usize,
285
7512
    out: &mut Vec<Dom>,
286
7512
) {
287
7512
    let current_index = *index;
288
7512
    *index += 1;
289

            
290
7512
    let has_children = !node.children.as_slice().is_empty();
291

            
292
    // Choose row style based on selection state
293
7512
    let row_style = if node.is_selected {
294
24
        ROW_SELECTED_STYLE
295
    } else {
296
7488
        ROW_STYLE
297
    };
298

            
299
    // Build the disclosure icon or spacer
300
7512
    let icon_or_spacer = if has_children {
301
1748
        let icon_name = if node.is_expanded {
302
1694
            "expand_more"
303
        } else {
304
54
            "chevron_right"
305
        };
306
1748
        Dom::create_icon(AzString::from_const_str(icon_name))
307
1748
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ICON_STYLE))
308
    } else {
309
        // Empty spacer for leaf alignment
310
5764
        Dom::create_div()
311
5764
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LEAF_SPACER_STYLE))
312
    };
313

            
314
    // Build the label
315
7512
    let label = Dom::create_p_with_text(node.label.clone())
316
7512
        .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE));
317

            
318
    // Build the row with click callback
319
7512
    let mut row = Dom::create_div()
320
7512
        .with_css_props(CssPropertyWithConditionsVec::from_const_slice(row_style))
321
7512
        .with_tab_index(TabIndex::Auto)
322
7512
        .with_children(DomVec::from_vec(vec![icon_or_spacer, label]));
323

            
324
    // Attach click callback if provided
325
7512
    if let Some(cb) = on_click.as_ref() {
326
5197
        let cb_data = NodeClickData {
327
5197
            node_index: current_index,
328
5197
            on_node_click: Some(TreeViewOnNodeClick {
329
5197
                callback: cb.callback.clone(),
330
5197
                refany: cb.refany.clone(),
331
5197
            })
332
5197
            .into(),
333
5197
        };
334
5197
        row = row.with_callbacks(
335
5197
            vec![CoreCallbackData {
336
5197
                event: EventFilter::Hover(HoverEventFilter::MouseUp),
337
5197
                refany: RefAny::new(cb_data),
338
5197
                callback: CoreCallback {
339
5197
                    cb: on_tree_node_click as usize,
340
5197
                    ctx: azul_core::refany::OptionRefAny::None,
341
5197
                },
342
5197
            }]
343
5197
            .into(),
344
5197
        );
345
5197
    }
346

            
347
7512
    out.push(row);
348

            
349
    // Render children if expanded
350
7512
    if has_children && node.is_expanded {
351
1694
        let mut child_doms = Vec::new();
352
7385
        for child in node.children.as_slice() {
353
7385
            render_node(child, on_click, index, &mut child_doms);
354
7385
        }
355

            
356
1694
        let children_container = Dom::create_div()
357
1694
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(CHILDREN_STYLE))
358
1694
            .with_children(DomVec::from_vec(child_doms));
359

            
360
1694
        out.push(children_container);
361
5818
    } else if has_children {
362
54
        // Still count collapsed children for correct depth-first indexing
363
54
        count_descendants(node.children.as_slice(), index);
364
5764
    }
365
7512
}
366

            
367
/// Advance the index counter past all descendants without rendering them.
368
10140
fn count_descendants(nodes: &[TreeViewNode], index: &mut usize) {
369
21303
    for node in nodes {
370
11163
        *index += 1;
371
11163
        if !node.children.as_slice().is_empty() {
372
10046
            count_descendants(node.children.as_slice(), index);
373
10046
        }
374
    }
375
10140
}
376

            
377
// ============================================================================
378
// Internal callback data
379
// ============================================================================
380

            
381
struct NodeClickData {
382
    node_index: usize,
383
    on_node_click: OptionTreeViewOnNodeClick,
384
}
385

            
386
// ============================================================================
387
// Callbacks
388
// ============================================================================
389

            
390
20
extern "C" fn on_tree_node_click(mut refany: RefAny, info: CallbackInfo) -> Update {
391
20
    let Some(mut refany) = refany.downcast_mut::<NodeClickData>() else {
392
4
        return Update::DoNothing;
393
    };
394

            
395
16
    let node_index = refany.node_index;
396

            
397
16
    match refany.on_node_click.as_mut() {
398
14
        Some(TreeViewOnNodeClick { refany, callback }) => {
399
14
            (callback.cb)(refany.clone(), info, node_index)
400
        }
401
2
        None => Update::DoNothing,
402
    }
403
20
}
404

            
405
// ============================================================================
406
// Trait impls
407
// ============================================================================
408

            
409
impl From<TreeView> for Dom {
410
11
    fn from(tv: TreeView) -> Self {
411
11
        tv.dom()
412
11
    }
413
}
414

            
415
#[cfg(test)]
416
mod 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
    // ------------------------------------------------------------------
445
    // Fixtures: trees
446
    // ------------------------------------------------------------------
447

            
448
    fn leaf(label: &str) -> TreeViewNode {
449
        TreeViewNode::new(label)
450
    }
451

            
452
    /// Total node count of a subtree: the node itself plus every descendant,
453
    /// expanded or not. This is the quantity `render_node` must advance the
454
    /// index counter by, whatever the expansion state.
455
    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
    /// A root with `n` leaf children.
465
    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
    /// A left-spine chain `depth` nodes deep; `expanded` is applied to every
474
    /// level. Built bottom-up so *construction* is iterative — only the
475
    /// functions under test recurse.
476
    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
    /// Four levels with alternating expansion, so both `render_node` branches
488
    /// nest inside each other.
489
    fn deep_mixed() -> TreeViewNode {
490
        leaf("root")
491
            .with_expanded(true)
492
            .with_child(
493
                leaf("a")
494
                    .with_expanded(false) // collapsed: a1/a1x are counted, not drawn
495
                    .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
    /// Every shape whose combination of branches `render_node` /
507
    /// `count_descendants` can take: leaves, expanded-but-childless nodes,
508
    /// collapsed parents, expanded parents, an expanded subtree buried under a
509
    /// collapsed one, and a collapsed subtree under an expanded one.
510
    fn shapes() -> Vec<TreeViewNode> {
511
        vec![
512
            leaf("solo"),
513
            leaf("solo-expanded").with_expanded(true), // expanded but childless
514
            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
    /// Labels chosen to break naive string handling: empty, whitespace-only,
534
    /// embedded NUL (`AzString` is length-based, so it must not truncate),
535
    /// ZWJ emoji, RTL, stacked combining marks, zero-width/BOM, bidi override,
536
    /// control chars, and a string that looks like an icon name.
537
    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
    /// Runs `f` on a thread with a roomy stack. `render_node`,
554
    /// `count_descendants` and `TreeViewNode`'s drop glue all recurse once per
555
    /// tree level, and a blown stack aborts the whole test binary instead of
556
    /// failing one test — the explicit stack keeps the depth assertions
557
    /// meaningful rather than a coin flip on the harness default.
558
    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
    // ------------------------------------------------------------------
568
    // Fixtures: DOM inspection
569
    // ------------------------------------------------------------------
570

            
571
    /// The text of a text node, looking through the `<p>` block wrapper the
572
    /// label convention mandates (`p > text`).
573
    fn text_of(dom: &Dom) -> Option<&str> {
574
        match dom.root.get_node_type() {
575
            NodeType::Text(s) => Some(s.as_ref().as_str()),
576
            NodeType::P => match dom.children.as_ref() {
577
                [only] => match only.root.get_node_type() {
578
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
579
                    _ => None,
580
                },
581
                _ => None,
582
            },
583
            _ => None,
584
        }
585
    }
586

            
587
    fn icon_of(dom: &Dom) -> Option<&str> {
588
        match dom.root.get_node_type() {
589
            NodeType::Icon(s) => Some(s.as_ref().as_str()),
590
            _ => None,
591
        }
592
    }
593

            
594
    /// True when a node's inline style is exactly the given const style slice.
595
    fn style_is(dom: &Dom, expected: &'static [CssPropertyWithConditions]) -> bool {
596
        *dom.root.get_style()
597
            == css::Css::from(CssPropertyWithConditionsVec::from_const_slice(expected))
598
    }
599

            
600
    /// The `(icon-or-spacer, label)` pair of a rendered row.
601
    fn row_parts(row: &Dom) -> (&Dom, &Dom) {
602
        let ch = row.children.as_ref();
603
        assert_eq!(ch.len(), 2, "every row is [icon|spacer, label]");
604
        (&ch[0], &ch[1])
605
    }
606

            
607
    /// Every rendered row in `nodes`, in visual order. Rows are the only nodes
608
    /// `render_node` gives a tab index to; everything else at this level is a
609
    /// children container, which is recursed into.
610
    fn collect_rows<'a>(nodes: &'a [Dom], out: &mut Vec<&'a Dom>) {
611
        for n in nodes {
612
            if n.root.get_tab_index().is_some() {
613
                out.push(n);
614
            } else {
615
                collect_rows(n.children.as_ref(), out);
616
            }
617
        }
618
    }
619

            
620
    fn rows_of(nodes: &[Dom]) -> Vec<&Dom> {
621
        let mut out = Vec::new();
622
        collect_rows(nodes, &mut out);
623
        out
624
    }
625

            
626
    /// The `node_index` the row's click payload carries (`None` when the row
627
    /// has no callback attached).
628
    fn click_index_of(row: &Dom) -> Option<usize> {
629
        let mut data = row.root.get_callbacks().as_ref().first()?.refany.clone();
630
        let payload = data
631
            .downcast_ref::<NodeClickData>()
632
            .expect("a row callback payload is always a NodeClickData");
633
        let index = payload.node_index;
634
        drop(payload);
635
        Some(index)
636
    }
637

            
638
    /// `(node_index, label)` for every rendered row.
639
    fn rendered_pairs(nodes: &[Dom]) -> Vec<(usize, String)> {
640
        rows_of(nodes)
641
            .iter()
642
            .map(|row| {
643
                let (_, label) = row_parts(row);
644
                (
645
                    click_index_of(row).expect("row must carry a click payload"),
646
                    text_of(label)
647
                        .expect("a row's second child is the label text node")
648
                        .to_string(),
649
                )
650
            })
651
            .collect()
652
    }
653

            
654
    /// Independent reference model of what `render_node` should emit:
655
    /// pre-order over the *whole* tree, but only visible nodes produce a row.
656
    /// Written from the documented contract, not from the implementation.
657
    fn expected_pairs(node: &TreeViewNode, next: &mut usize, out: &mut Vec<(usize, String)>) {
658
        let index = *next;
659
        *next += 1;
660
        out.push((index, node.label.as_str().to_string()));
661

            
662
        let children = node.children.as_slice();
663
        if node.is_expanded && !children.is_empty() {
664
            for c in children {
665
                expected_pairs(c, next, out);
666
            }
667
        } else {
668
            // Hidden descendants still consume indices.
669
            *next += subtree_len(node) - 1;
670
        }
671
    }
672

            
673
    fn expected_of(tree: &TreeViewNode, start: usize) -> Vec<(usize, String)> {
674
        let mut next = start;
675
        let mut out = Vec::new();
676
        expected_pairs(tree, &mut next, &mut out);
677
        out
678
    }
679

            
680
    /// The true recursive descendant count — what `estimated_total_children`
681
    /// caches and what `convert_dom_into_compact_dom` allocates from.
682
    fn recursive_descendants(dom: &Dom) -> usize {
683
        dom.children
684
            .as_ref()
685
            .iter()
686
            .map(|c| 1 + recursive_descendants(c))
687
            .sum()
688
    }
689

            
690
    fn assert_estimates_consistent(dom: &Dom) {
691
        assert_eq!(
692
            dom.estimated_total_children,
693
            recursive_descendants(dom),
694
            "estimated_total_children desynced from the real subtree size"
695
        );
696
        for c in dom.children.as_ref() {
697
            assert_estimates_consistent(c);
698
        }
699
    }
700

            
701
    // ------------------------------------------------------------------
702
    // Fixtures: callbacks
703
    // ------------------------------------------------------------------
704

            
705
    type ClickLog = Arc<Mutex<Vec<usize>>>;
706

            
707
    /// Offset applied by `record_click_all_windows` so the two recorders stay
708
    /// distinguishable in the log.
709
    const SENTINEL: usize = 1_000_000;
710

            
711
    extern "C" fn record_click(mut data: RefAny, _info: CallbackInfo, node_index: usize) -> Update {
712
        if let Some(log) = data.downcast_ref::<ClickLog>() {
713
            log.lock().expect("click log poisoned").push(node_index);
714
        }
715
        Update::RefreshDom
716
    }
717

            
718
    /// A second callback with a *deliberately different body*: two identical
719
    /// `extern "C"` bodies are fair game for identical-code folding, which
720
    /// would merge their addresses and make "last write wins" vacuous.
721
    extern "C" fn record_click_all_windows(
722
        mut data: RefAny,
723
        _info: CallbackInfo,
724
        node_index: usize,
725
    ) -> Update {
726
        if let Some(log) = data.downcast_ref::<ClickLog>() {
727
            log.lock()
728
                .expect("click log poisoned")
729
                .push(node_index.wrapping_add(SENTINEL));
730
        }
731
        Update::RefreshDomAllWindows
732
    }
733

            
734
    /// Forces the `fn`-item -> `fn`-pointer coercion the `Into` bound needs.
735
    fn cb(f: TreeViewOnNodeClickCallbackType) -> TreeViewOnNodeClickCallback {
736
        f.into()
737
    }
738

            
739
    fn new_log() -> ClickLog {
740
        Arc::new(Mutex::new(Vec::new()))
741
    }
742

            
743
    fn entries(log: &ClickLog) -> Vec<usize> {
744
        log.lock().expect("click log poisoned").clone()
745
    }
746

            
747
    fn some_click(f: TreeViewOnNodeClickCallbackType, log: &ClickLog) -> OptionTreeViewOnNodeClick {
748
        Some(TreeViewOnNodeClick {
749
            callback: cb(f),
750
            refany: RefAny::new(log.clone()),
751
        })
752
        .into()
753
    }
754

            
755
    /// Invokes `on_tree_node_click` once per payload against one shared
756
    /// `CallbackInfo`. `on_tree_node_click` never touches the layout results,
757
    /// so an empty `LayoutWindow` is enough.
758
    fn run_clicks(payloads: Vec<RefAny>) -> Vec<Update> {
759
        let layout_window =
760
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
761
        let renderer_resources = RendererResources::default();
762
        let previous_window_state: Option<FullWindowState> = None;
763
        let current_window_state = FullWindowState::default();
764
        let gl_context = OptionGlContextPtr::None;
765
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
766
            BTreeMap::new();
767
        let window_handle = RawWindowHandle::Unsupported;
768
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
769

            
770
        let ref_data = CallbackInfoRefData {
771
            layout_window: &layout_window,
772
            renderer_resources: &renderer_resources,
773
            previous_window_state: &previous_window_state,
774
            current_window_state: &current_window_state,
775
            gl_context: &gl_context,
776
            current_scroll_manager: &scroll_states,
777
            current_window_handle: &window_handle,
778
            system_callbacks: &system_callbacks,
779
            system_style: Arc::new(SystemStyle::default()),
780
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
781
            #[cfg(feature = "icu")]
782
            icu_localizer: IcuLocalizerHandle::default(),
783
            ctx: OptionRefAny::None,
784
        };
785

            
786
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
787

            
788
        let info = CallbackInfo::new(
789
            &ref_data,
790
            &changes,
791
            DomNodeId {
792
                dom: DomId::ROOT_ID,
793
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
794
            },
795
            OptionLogicalPosition::None,
796
            OptionLogicalPosition::None,
797
        );
798

            
799
        payloads
800
            .into_iter()
801
            .map(|p| on_tree_node_click(p, info))
802
            .collect()
803
    }
804

            
805
    // ==================================================================
806
    // TreeViewNode::new
807
    // ==================================================================
808

            
809
    #[test]
810
    fn new_defaults_to_a_collapsed_unselected_childless_node() {
811
        let node = TreeViewNode::new("Root");
812

            
813
        assert_eq!(node.label.as_str(), "Root");
814
        assert!(
815
            node.children.as_slice().is_empty(),
816
            "a fresh node has no children"
817
        );
818
        assert_eq!(node.children.len(), 0);
819
        assert!(
820
            node.children.capacity() >= node.children.len(),
821
            "len must never exceed capacity"
822
        );
823
        assert!(!node.is_expanded, "a fresh node is collapsed");
824
        assert!(!node.is_selected, "a fresh node is unselected");
825
    }
826

            
827
    #[test]
828
    fn new_preserves_pathological_labels_byte_for_byte() {
829
        for label in pathological_labels() {
830
            let node = TreeViewNode::new(label.clone());
831
            assert_eq!(
832
                node.label.as_str(),
833
                label.as_str(),
834
                "label must survive verbatim"
835
            );
836
            assert_eq!(
837
                node.label.as_str().len(),
838
                label.len(),
839
                "an embedded NUL must not truncate the label"
840
            );
841
            // …and the state defaults must not depend on the label at all.
842
            assert!(!node.is_expanded);
843
            assert!(!node.is_selected);
844
            assert!(node.children.as_slice().is_empty());
845
        }
846
    }
847

            
848
    #[test]
849
    fn new_accepts_every_into_azstring_source_identically() {
850
        let from_str = TreeViewNode::new("same");
851
        let from_string = TreeViewNode::new("same".to_string());
852
        let from_azstring = TreeViewNode::new(AzString::from("same"));
853

            
854
        assert_eq!(from_str, from_string);
855
        assert_eq!(from_str, from_azstring);
856
    }
857

            
858
    #[test]
859
    fn new_with_a_megabyte_label_does_not_truncate_or_panic() {
860
        let huge = "λ".repeat(500_000); // 1 MB of UTF-8
861
        let node = TreeViewNode::new(huge.clone());
862
        assert_eq!(node.label.as_str().len(), huge.len());
863
        assert_eq!(node.label.as_str(), huge);
864
    }
865

            
866
    // ==================================================================
867
    // TreeViewNode::add_child / with_child
868
    // ==================================================================
869

            
870
    #[test]
871
    fn add_child_and_with_child_agree() {
872
        let mut mutated = leaf("root");
873
        mutated.add_child(leaf("a"));
874
        mutated.add_child(leaf("b"));
875

            
876
        let built = leaf("root").with_child(leaf("a")).with_child(leaf("b"));
877

            
878
        assert_eq!(
879
            mutated, built,
880
            "the builder and the mutator must produce the same node"
881
        );
882
    }
883

            
884
    #[test]
885
    fn add_child_preserves_order_duplicates_and_len_capacity_invariants() {
886
        let n = 5_000;
887
        let mut root = leaf("root");
888
        for i in 0..n {
889
            root.add_child(leaf(&format!("c{i}")));
890
            assert_eq!(root.children.len(), i + 1, "len must track every push");
891
            assert!(
892
                root.children.capacity() >= root.children.len(),
893
                "capacity must never fall below len"
894
            );
895
        }
896
        // Order is insertion order, and nothing is deduplicated.
897
        root.add_child(leaf("c0"));
898
        assert_eq!(root.children.len(), n + 1, "duplicates are kept, not merged");
899
        assert_eq!(root.children.as_slice()[0].label.as_str(), "c0");
900
        assert_eq!(root.children.as_slice()[n - 1].label.as_str(), "c4999");
901
        assert_eq!(root.children.as_slice()[n].label.as_str(), "c0");
902
        assert_eq!(subtree_len(&root), n + 2);
903
    }
904

            
905
    #[test]
906
    fn child_vec_survives_the_borrowed_to_owned_transition() {
907
        // `TreeViewNode::new` seeds `children` from a *const* slice (no
908
        // destructor, zero capacity). The first push has to switch it to an
909
        // owned heap buffer; a clone taken afterwards must be fully
910
        // independent, or dropping either one would free the other's memory.
911
        let mut root = leaf("root");
912
        assert_eq!(root.children.capacity(), 0);
913

            
914
        root.add_child(leaf("a"));
915
        root.add_child(leaf("b"));
916

            
917
        let mut copy = root.clone();
918
        copy.add_child(leaf("c"));
919
        copy.children.as_mut()[0].label = AzString::from("mutated");
920

            
921
        assert_eq!(root.children.len(), 2, "the original must not see the push");
922
        assert_eq!(
923
            root.children.as_slice()[0].label.as_str(),
924
            "a",
925
            "the clone must own its own child storage"
926
        );
927
        assert_eq!(copy.children.len(), 3);
928
        assert_eq!(copy.children.as_slice()[0].label.as_str(), "mutated");
929

            
930
        drop(copy);
931
        // Original still readable after the clone is gone (no shared buffer).
932
        assert_eq!(root.children.as_slice()[1].label.as_str(), "b");
933
    }
934

            
935
    #[test]
936
    fn with_child_nests_arbitrarily_deep_without_panicking() {
937
        on_big_stack(|| {
938
            let depth = 1_000;
939
            let root = chain(depth, true);
940
            assert_eq!(subtree_len(&root), depth);
941

            
942
            // Deep clone + deep drop both recurse per level as well.
943
            let copy = root.clone();
944
            assert_eq!(copy, root);
945
            drop(copy);
946
            drop(root);
947
        });
948
    }
949

            
950
    // ==================================================================
951
    // TreeViewNode::with_expanded / with_selected
952
    // ==================================================================
953

            
954
    #[test]
955
    fn with_expanded_and_with_selected_are_orthogonal_and_idempotent() {
956
        for expanded in [false, true] {
957
            for selected in [false, true] {
958
                let node = leaf("n").with_expanded(expanded).with_selected(selected);
959
                assert_eq!(node.is_expanded, expanded);
960
                assert_eq!(node.is_selected, selected);
961

            
962
                // Order must not matter…
963
                let flipped = leaf("n").with_selected(selected).with_expanded(expanded);
964
                assert_eq!(node, flipped);
965

            
966
                // …and applying the same value twice must be a no-op.
967
                let twice = node
968
                    .clone()
969
                    .with_expanded(expanded)
970
                    .with_selected(selected);
971
                assert_eq!(node, twice);
972

            
973
                // The last write wins when the value is flipped.
974
                let overwritten = node.clone().with_expanded(!expanded);
975
                assert_eq!(overwritten.is_expanded, !expanded);
976
                assert_eq!(
977
                    overwritten.is_selected, selected,
978
                    "with_expanded must not touch is_selected"
979
                );
980
            }
981
        }
982
    }
983

            
984
    #[test]
985
    fn state_builders_do_not_disturb_label_or_children() {
986
        let base = leaf("keep me").with_child(leaf("a")).with_child(leaf("b"));
987
        let styled = base
988
            .clone()
989
            .with_expanded(true)
990
            .with_selected(true)
991
            .with_expanded(false);
992

            
993
        assert_eq!(styled.label, base.label);
994
        assert_eq!(styled.children, base.children);
995
        assert!(!styled.is_expanded);
996
        assert!(styled.is_selected);
997
    }
998

            
999
    #[test]
    fn nodes_differing_only_in_state_are_not_equal() {
        let base = leaf("n");
        assert_ne!(base, base.clone().with_expanded(true));
        assert_ne!(base, base.clone().with_selected(true));
        assert_ne!(base, base.clone().with_child(leaf("a")));
        assert_ne!(base, leaf("m"));
    }
    #[test]
    fn equality_ignores_how_the_child_vec_was_built() {
        let pushed = leaf("root").with_child(leaf("a")).with_child(leaf("b"));
        let from_vec = TreeViewNode {
            label: AzString::from("root"),
            children: TreeViewNodeVec::from_vec(vec![leaf("a"), leaf("b")]),
            is_expanded: false,
            is_selected: false,
        };
        assert_eq!(
            pushed, from_vec,
            "the vec's allocation strategy must not leak into equality"
        );
    }
    // ==================================================================
    // TreeView::new / set_on_node_click / with_on_node_click
    // ==================================================================
    #[test]
    fn treeview_new_keeps_the_root_intact_and_installs_no_callback() {
        for root in shapes() {
            let tv = TreeView::new(root.clone());
            assert_eq!(tv.root, root, "new must not rewrite the tree");
            assert!(
                tv.on_node_click.as_ref().is_none(),
                "new must not install a callback"
            );
        }
    }
    #[test]
    fn set_on_node_click_installs_then_overwrites() {
        let log = new_log();
        let mut tv = TreeView::new(leaf("root"));
        tv.set_on_node_click(RefAny::new(log.clone()), cb(record_click));
        assert!(tv.on_node_click.as_ref().is_some());
        tv.set_on_node_click(RefAny::new(log.clone()), cb(record_click_all_windows));
        let installed = tv
            .on_node_click
            .as_ref()
            .expect("a callback is still installed");
        assert_eq!(
            installed.callback,
            cb(record_click_all_windows),
            "the last write must win"
        );
        assert_ne!(installed.callback, cb(record_click));
    }
    #[test]
    fn with_on_node_click_matches_set_on_node_click() {
        // Both sides get *clones of the same* `RefAny`: `RefAny`'s equality is
        // shared-identity, so two independent `RefAny::new` calls would never
        // compare equal no matter what the builders do.
        let data = RefAny::new(new_log());
        let mut mutated = TreeView::new(leaf("root"));
        mutated.set_on_node_click(data.clone(), cb(record_click));
        let built = TreeView::new(leaf("root")).with_on_node_click(data.clone(), cb(record_click));
        assert_eq!(mutated, built);
    }
    // ==================================================================
    // count_descendants  (numeric: zero / min-max / overflow)
    // ==================================================================
    #[test]
    fn count_descendants_of_an_empty_slice_is_a_no_op_even_at_usize_max() {
        // usize has no negative domain; the adversarial extremes are 0 and MAX.
        for start in [0usize, 1, usize::MAX / 2, usize::MAX - 1, usize::MAX] {
            let mut index = start;
            count_descendants(&[], &mut index);
            assert_eq!(
                index, start,
                "an empty slice must not touch the counter (and must not overflow at MAX)"
            );
        }
    }
    #[test]
    fn count_descendants_counts_every_node_regardless_of_expansion() {
        for shape in shapes() {
            let nodes = shape.children.as_slice();
            let expected: usize = nodes.iter().map(subtree_len).sum();
            for start in [0usize, 7, 1_000_000] {
                let mut index = start;
                count_descendants(nodes, &mut index);
                assert_eq!(
                    index - start,
                    expected,
                    "collapsed and expanded descendants must count the same"
                );
            }
        }
    }
    #[test]
    fn count_descendants_reaches_exactly_usize_max_without_overflowing() {
        let tree = deep_mixed();
        let nodes = tree.children.as_slice();
        let total: usize = nodes.iter().map(subtree_len).sum();
        let mut index = usize::MAX - total;
        count_descendants(nodes, &mut index);
        assert_eq!(
            index,
            usize::MAX,
            "landing exactly on usize::MAX must not overflow"
        );
    }
    #[test]
    fn count_descendants_survives_a_deep_chain() {
        on_big_stack(|| {
            let depth = 10_000;
            let root = chain(depth, false);
            let mut index = 0usize;
            count_descendants(root.children.as_slice(), &mut index);
            assert_eq!(index, depth - 1, "every hidden descendant is counted once");
        });
    }
    // ==================================================================
    // render_node  (numeric: index accounting)
    // ==================================================================
    #[test]
    fn render_node_advance_equals_subtree_size_for_every_shape() {
        // The load-bearing invariant: whether a subtree is drawn or skipped,
        // it must consume exactly one index per node — otherwise a collapsed
        // sibling shifts every later row's click index.
        for shape in shapes() {
            let expected = subtree_len(&shape);
            for start in [0usize, 1, 12_345, usize::MAX / 4] {
                let mut index = start;
                let mut out = Vec::new();
                render_node(&shape, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
                assert_eq!(
                    index - start,
                    expected,
                    "index advance must equal the subtree size, expanded or not"
                );
                assert!(!out.is_empty(), "every node renders at least its own row");
            }
        }
    }
    #[test]
    fn render_node_emits_preorder_indices_for_visible_rows_only() {
        for shape in shapes() {
            let log = new_log();
            let on_click = some_click(record_click, &log);
            let mut index = 0usize;
            let mut out = Vec::new();
            render_node(&shape, &on_click, &mut index, &mut out);
            assert_eq!(
                rendered_pairs(&out),
                expected_of(&shape, 0),
                "rendered rows must match the independent pre-order model"
            );
        }
    }
    #[test]
    fn render_node_appends_and_offsets_from_a_nonzero_start_index() {
        let start = 12_345usize;
        let shape = deep_mixed();
        let log = new_log();
        let on_click = some_click(record_click, &log);
        // Pre-existing content in `out` must be preserved, not clobbered.
        let mut out = vec![Dom::create_div(), Dom::create_text_do_not_use_without_block_level_wrapper("sentinel")];
        let mut index = start;
        render_node(&shape, &on_click, &mut index, &mut out);
        assert_eq!(
            text_of(&out[1]),
            Some("sentinel"),
            "render_node must append to `out`, never rewrite it"
        );
        assert_eq!(
            rendered_pairs(&out[2..]),
            expected_of(&shape, start),
            "a non-zero start index must offset every emitted index"
        );
        assert_eq!(index, start + subtree_len(&shape));
    }
    #[test]
    fn render_node_lands_exactly_on_usize_max_without_overflowing() {
        // Three nodes, started so the *last* index handed out is usize::MAX - 1
        // and the counter finishes on usize::MAX: one node short of the cliff.
        let tree = leaf("root")
            .with_child(leaf("a"))
            .with_child(leaf("b"))
            .with_expanded(true);
        assert_eq!(subtree_len(&tree), 3);
        let log = new_log();
        let on_click = some_click(record_click, &log);
        let mut index = usize::MAX - 3;
        let mut out = Vec::new();
        render_node(&tree, &on_click, &mut index, &mut out);
        assert_eq!(index, usize::MAX, "must land exactly on MAX, not wrap");
        let indices: Vec<usize> = rows_of(&out)
            .iter()
            .filter_map(|r| click_index_of(r))
            .collect();
        assert_eq!(
            indices,
            vec![usize::MAX - 3, usize::MAX - 2, usize::MAX - 1],
            "extreme indices must be carried verbatim into the click payloads"
        );
    }
    #[cfg(all(debug_assertions, panic = "unwind"))]
    #[test]
    fn render_node_index_overflow_is_loud_not_silently_wrapped() {
        // `render_node` does an unguarded `*index += 1`. Starting at
        // usize::MAX must not quietly wrap the counter to 0 (which would give
        // two different rows the same click index); an overflow-checked build
        // has to panic instead.
        let node = leaf("boom");
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let mut index = usize::MAX;
            let mut out = Vec::new();
            render_node(&node, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
            index
        }));
        match result {
            Err(_) => {} // overflow-checked build: panicked, as required
            Ok(index) => assert_eq!(
                index, 0,
                "without overflow checks the counter must wrap cleanly, not corrupt"
            ),
        }
    }
    #[test]
    fn render_node_without_a_callback_attaches_none() {
        for shape in shapes() {
            let mut index = 0usize;
            let mut out = Vec::new();
            render_node(&shape, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
            for row in rows_of(&out) {
                assert!(
                    row.root.get_callbacks().as_ref().is_empty(),
                    "no callback configured => no callback attached"
                );
            }
        }
    }
    #[test]
    fn render_node_survives_a_deep_expanded_chain() {
        on_big_stack(|| {
            let depth = 800;
            let root = chain(depth, true);
            let mut index = 0usize;
            let mut out = Vec::new();
            render_node(&root, &OptionTreeViewOnNodeClick::None, &mut index, &mut out);
            assert_eq!(index, depth, "one index per level");
            assert_eq!(rows_of(&out).len(), depth, "every level renders one row");
            drop(out);
        });
    }
    #[test]
    fn render_node_handles_a_wide_fanout() {
        let n = 5_000;
        let root = wide(n, true);
        let log = new_log();
        let on_click = some_click(record_click, &log);
        let mut index = 0usize;
        let mut out = Vec::new();
        render_node(&root, &on_click, &mut index, &mut out);
        assert_eq!(index, n + 1);
        assert_eq!(out.len(), 2, "an expanded parent emits [row, container]");
        assert_eq!(out[1].children.as_ref().len(), n, "every child gets a row");
        let indices: Vec<usize> = rows_of(&out)
            .iter()
            .filter_map(|r| click_index_of(r))
            .collect();
        assert_eq!(indices, (0..=n).collect::<Vec<_>>());
    }
    // ==================================================================
    // TreeView::dom
    // ==================================================================
    #[test]
    fn dom_root_carries_the_container_class_and_style() {
        let dom = TreeView::new(leaf("root")).dom();
        let classes = dom.root.get_ids_and_classes();
        assert!(
            classes
                .as_ref()
                .iter()
                .any(|c| matches!(c, Class(s) if s.as_str() == "__azul-native-tree-view")),
            "the container must be findable by its widget class"
        );
        assert!(
            style_is(&dom, TREE_CONTAINER_STYLE),
            "the container must use the shared const style"
        );
    }
    #[test]
    fn dom_leaf_renders_a_spacer_and_no_icon() {
        let dom = TreeView::new(leaf("only")).dom();
        assert_eq!(dom.children.as_ref().len(), 1, "a leaf emits just its row");
        let row = &dom.children.as_ref()[0];
        let (icon, label) = row_parts(row);
        assert_eq!(icon_of(icon), None, "a childless node gets no disclosure icon");
        assert!(
            style_is(icon, LEAF_SPACER_STYLE),
            "the placeholder must use the leaf-spacer style so labels stay aligned"
        );
        assert_eq!(text_of(label), Some("only"));
        assert!(style_is(label, LABEL_STYLE));
    }
    #[test]
    fn dom_expanded_parent_uses_expand_more_and_emits_a_container() {
        let tree = leaf("p")
            .with_child(leaf("a"))
            .with_child(leaf("b"))
            .with_expanded(true);
        let dom = TreeView::new(tree).dom();
        assert_eq!(
            dom.children.as_ref().len(),
            2,
            "an expanded parent emits [row, children container]"
        );
        let (icon, _) = row_parts(&dom.children.as_ref()[0]);
        assert_eq!(icon_of(icon), Some("expand_more"));
        assert!(style_is(icon, ICON_STYLE));
        let container = &dom.children.as_ref()[1];
        assert!(style_is(container, CHILDREN_STYLE));
        assert_eq!(container.children.as_ref().len(), 2, "both children drawn");
    }
    #[test]
    fn dom_collapsed_parent_uses_chevron_and_draws_no_children() {
        let tree = leaf("p").with_child(leaf("a")).with_child(leaf("b"));
        let dom = TreeView::new(tree).dom();
        assert_eq!(
            dom.children.as_ref().len(),
            1,
            "a collapsed parent must not emit a children container"
        );
        let (icon, _) = row_parts(&dom.children.as_ref()[0]);
        assert_eq!(icon_of(icon), Some("chevron_right"));
        assert_eq!(rows_of(dom.children.as_ref()).len(), 1, "children stay hidden");
    }
    #[test]
    fn dom_expanded_but_childless_node_still_renders_a_spacer() {
        // `is_expanded` is documented as meaningful only with children.
        let dom = TreeView::new(leaf("empty").with_expanded(true)).dom();
        assert_eq!(dom.children.as_ref().len(), 1, "nothing to expand into");
        let (icon, _) = row_parts(&dom.children.as_ref()[0]);
        assert_eq!(icon_of(icon), None);
        assert!(style_is(icon, LEAF_SPACER_STYLE));
    }
    #[test]
    fn dom_selected_rows_use_the_selected_style() {
        let tree = leaf("p")
            .with_expanded(true)
            .with_child(leaf("a").with_selected(true))
            .with_child(leaf("b"));
        let dom = TreeView::new(tree).dom();
        let rows = rows_of(dom.children.as_ref());
        assert_eq!(rows.len(), 3);
        assert!(style_is(rows[0], ROW_STYLE), "unselected root uses ROW_STYLE");
        assert!(
            style_is(rows[1], ROW_SELECTED_STYLE),
            "the selected node must switch to the selected style"
        );
        assert!(style_is(rows[2], ROW_STYLE));
        assert!(
            !style_is(rows[1], ROW_STYLE),
            "the two row styles must be distinguishable"
        );
    }
    #[test]
    fn dom_keeps_estimated_total_children_consistent_for_every_shape() {
        // A stale estimate makes `convert_dom_into_compact_dom` under-allocate
        // and panic out of bounds, so this is a crash invariant, not cosmetics.
        for shape in shapes() {
            let dom = TreeView::new(shape).dom();
            assert_estimates_consistent(&dom);
        }
    }
    #[test]
    fn dom_labels_survive_the_round_trip_unchanged() {
        let labels = pathological_labels();
        let mut root = leaf("root").with_expanded(true);
        for l in &labels {
            root.add_child(TreeViewNode::new(l.clone()));
        }
        let dom = TreeView::new(root).dom();
        let rows = rows_of(dom.children.as_ref());
        assert_eq!(rows.len(), labels.len() + 1);
        let rendered: Vec<&str> = rows[1..]
            .iter()
            .map(|r| text_of(row_parts(r).1).expect("label text node"))
            .collect();
        let expected: Vec<&str> = labels.iter().map(String::as_str).collect();
        assert_eq!(rendered, expected, "labels must survive byte-for-byte");
    }
    #[test]
    fn dom_rows_are_focusable_and_carry_exactly_one_click_callback() {
        let log = new_log();
        let tv = TreeView::new(deep_mixed())
            .with_on_node_click(RefAny::new(log.clone()), cb(record_click));
        let dom = tv.dom();
        for row in rows_of(dom.children.as_ref()) {
            assert!(
                matches!(row.root.get_tab_index(), Some(TabIndex::Auto)),
                "every row must be keyboard focusable"
            );
            let cbs = row.root.get_callbacks();
            assert_eq!(cbs.as_ref().len(), 1, "exactly one click callback per row");
            assert_eq!(
                cbs.as_ref()[0].event,
                EventFilter::Hover(HoverEventFilter::MouseUp),
                "rows fire on mouse-up"
            );
        }
    }
    #[test]
    fn dom_indices_skip_collapsed_subtrees_but_stay_preorder() {
        for shape in shapes() {
            let log = new_log();
            let dom = TreeView::new(shape.clone())
                .with_on_node_click(RefAny::new(log.clone()), cb(record_click))
                .dom();
            assert_eq!(
                rendered_pairs(dom.children.as_ref()),
                expected_of(&shape, 0),
                "dom() must index nodes pre-order over the whole tree, \
                 including the collapsed ones it does not draw"
            );
        }
    }
    #[test]
    fn dom_of_an_empty_labelled_tree_does_not_panic() {
        let dom = TreeView::new(leaf("")).dom();
        let rows = rows_of(dom.children.as_ref());
        assert_eq!(rows.len(), 1);
        assert_eq!(text_of(row_parts(rows[0]).1), Some(""));
    }
    #[test]
    fn from_treeview_for_dom_matches_dom() {
        for shape in shapes() {
            let via_trait: Dom = TreeView::new(shape.clone()).into();
            let via_method = TreeView::new(shape).dom();
            assert_eq!(via_trait, via_method);
        }
    }
    #[test]
    fn dom_survives_a_deep_expanded_chain() {
        on_big_stack(|| {
            let depth = 800;
            let dom = TreeView::new(chain(depth, true)).dom();
            assert_eq!(rows_of(dom.children.as_ref()).len(), depth);
            assert_estimates_consistent(&dom);
            drop(dom);
        });
    }
    // ==================================================================
    // on_tree_node_click
    // ==================================================================
    #[test]
    fn click_with_a_foreign_payload_returns_do_nothing() {
        // A `RefAny` of the wrong type must be rejected, not reinterpreted.
        let payloads = vec![
            RefAny::new(0usize),
            RefAny::new(String::from("not a NodeClickData")),
            RefAny::new(leaf("also not one")),
            RefAny::new(()),
        ];
        let updates = run_clicks(payloads);
        assert_eq!(
            updates,
            vec![Update::DoNothing; 4],
            "a foreign payload must be a no-op, not a panic or a wild call"
        );
    }
    #[test]
    fn click_without_a_user_callback_returns_do_nothing() {
        let payloads = vec![
            RefAny::new(NodeClickData {
                node_index: 0,
                on_node_click: OptionTreeViewOnNodeClick::None,
            }),
            RefAny::new(NodeClickData {
                node_index: usize::MAX,
                on_node_click: OptionTreeViewOnNodeClick::None,
            }),
        ];
        assert_eq!(run_clicks(payloads), vec![Update::DoNothing; 2]);
    }
    #[test]
    fn click_forwards_the_index_verbatim_including_the_extremes() {
        let log = new_log();
        let indices = vec![0usize, 1, usize::MAX / 2, usize::MAX - 1, usize::MAX];
        let payloads: Vec<RefAny> = indices
            .iter()
            .map(|i| {
                RefAny::new(NodeClickData {
                    node_index: *i,
                    on_node_click: some_click(record_click, &log),
                })
            })
            .collect();
        let updates = run_clicks(payloads);
        assert_eq!(updates, vec![Update::RefreshDom; 5]);
        assert_eq!(
            entries(&log),
            indices,
            "the node index must reach the user callback unmodified"
        );
    }
    #[test]
    fn click_propagates_the_user_update_verbatim() {
        let log = new_log();
        let payloads = vec![
            RefAny::new(NodeClickData {
                node_index: 3,
                on_node_click: some_click(record_click, &log),
            }),
            RefAny::new(NodeClickData {
                node_index: 4,
                on_node_click: some_click(record_click_all_windows, &log),
            }),
        ];
        assert_eq!(
            run_clicks(payloads),
            vec![Update::RefreshDom, Update::RefreshDomAllWindows],
            "the dispatcher must not downgrade or upgrade the user's Update"
        );
        assert_eq!(entries(&log), vec![3, 4 + SENTINEL]);
    }
    #[test]
    fn clicking_every_rendered_row_reports_its_visual_index() {
        let shape = deep_mixed();
        let log = new_log();
        let dom = TreeView::new(shape.clone())
            .with_on_node_click(RefAny::new(log.clone()), cb(record_click))
            .dom();
        let payloads: Vec<RefAny> = rows_of(dom.children.as_ref())
            .iter()
            .map(|row| {
                row.root
                    .get_callbacks()
                    .as_ref()
                    .first()
                    .expect("every row carries the click callback")
                    .refany
                    .clone()
            })
            .collect();
        let expected: Vec<usize> = expected_of(&shape, 0).into_iter().map(|(i, _)| i).collect();
        let updates = run_clicks(payloads);
        assert_eq!(updates, vec![Update::RefreshDom; expected.len()]);
        assert_eq!(
            entries(&log),
            expected,
            "clicking row N must report N's pre-order index, collapsed siblings included"
        );
    }
}