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
use azul_css::{
15
    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
16
    props::{
17
        basic::{
18
            color::{ColorU, ColorOrSystem},
19
            font::{StyleFontFamily, StyleFontFamilyVec},
20
            *,
21
        },
22
        layout::*,
23
        property::CssProperty,
24
        style::*,
25
    },
26
    *,
27
};
28

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

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

            
33
// -- Callback type via macro --
34

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

            
47
azul_core::impl_managed_callback! {
48
    wrapper:        TreeViewOnNodeClickCallback,
49
    info_ty:        CallbackInfo,
50
    return_ty:      Update,
51
    default_ret:    Update::DoNothing,
52
    invoker_static: TREE_VIEW_ON_NODE_CLICK_INVOKER,
53
    invoker_ty:     AzTreeViewOnNodeClickCallbackInvoker,
54
    thunk_fn:       az_tree_view_on_node_click_callback_thunk,
55
    setter_fn:      AzApp_setTreeViewOnNodeClickCallbackInvoker,
56
    from_handle_fn: AzTreeViewOnNodeClickCallback_createFromHostHandle,
57
    extra_args:     [ node_index: usize ],
58
}
59

            
60
// -- Font --
61

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

            
67
// -- Colors --
68

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

            
75
// -- Tree container style --
76

            
77
static TREE_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
78
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
79
    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
80
    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
81
    CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
82
    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: TEXT_COLOR })),
83
];
84

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

            
87
static ROW_STYLE: &[CssPropertyWithConditions] = &[
88
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
89
    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
90
    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
91
    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
92
    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(2))),
93
    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
94
    CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(4))),
95
    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
96
    // Hover
97
    CssPropertyWithConditions::on_hover(CssProperty::const_background_content(
98
        StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(HOVER_BG)]),
99
    )),
100
];
101

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

            
107
static ROW_SELECTED_STYLE: &[CssPropertyWithConditions] = &[
108
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
109
    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
110
    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
111
    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(2))),
112
    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(LayoutPaddingBottom::const_px(2))),
113
    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
114
    CssPropertyWithConditions::simple(CssProperty::const_padding_right(LayoutPaddingRight::const_px(4))),
115
    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
116
    CssPropertyWithConditions::simple(CssProperty::const_background_content(
117
        StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(SELECTED_BG)]),
118
    )),
119
    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: SELECTED_TEXT })),
120
];
121

            
122
// -- Children container style --
123

            
124
static CHILDREN_STYLE: &[CssPropertyWithConditions] = &[
125
    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
126
    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
127
    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(16))),
128
];
129

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

            
134
static ICON_STYLE: &[CssPropertyWithConditions] = &[
135
    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(16))),
136
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
137
    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor { inner: ICON_COLOR })),
138
];
139

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

            
142
static LEAF_SPACER_STYLE: &[CssPropertyWithConditions] = &[
143
    CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(16))),
144
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
145
];
146

            
147
// -- Label style --
148

            
149
static LABEL_STYLE: &[CssPropertyWithConditions] = &[
150
    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
151
    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(4))),
152
];
153

            
154
// ============================================================================
155
// Data structures
156
// ============================================================================
157

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

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

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

            
188
    /// Builder method: appends a child node.
189
    pub fn with_child(mut self, child: TreeViewNode) -> Self {
190
        self.children.push(child);
191
        self
192
    }
193

            
194
    /// Builder method: sets the expanded state.
195
    pub fn with_expanded(mut self, expanded: bool) -> Self {
196
        self.is_expanded = expanded;
197
        self
198
    }
199

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

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

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

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

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

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

            
256
    /// Renders the tree view into a [`Dom`] subtree.
257
    pub fn dom(self) -> Dom {
258
        let on_node_click = self.on_node_click;
259
        let root = self.root;
260

            
261
        const TREE_CLASS: &[IdOrClass] =
262
            &[Class(AzString::from_const_str("__azul-native-tree-view"))];
263

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

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

            
275
// ============================================================================
276
// Internal: recursive DOM rendering
277
// ============================================================================
278

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

            
288
    let has_children = !node.children.as_slice().is_empty();
289

            
290
    // Choose row style based on selection state
291
    let row_style = if node.is_selected {
292
        ROW_SELECTED_STYLE
293
    } else {
294
        ROW_STYLE
295
    };
296

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

            
312
    // Build the label
313
    let label = Dom::create_text(node.label.clone())
314
        .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE));
315

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

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

            
345
    out.push(row);
346

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

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

            
358
        out.push(children_container);
359
    } else if has_children {
360
        // Still count collapsed children for correct depth-first indexing
361
        count_descendants(node.children.as_slice(), index);
362
    }
363
}
364

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

            
375
// ============================================================================
376
// Internal callback data
377
// ============================================================================
378

            
379
struct NodeClickData {
380
    node_index: usize,
381
    on_node_click: OptionTreeViewOnNodeClick,
382
}
383

            
384
// ============================================================================
385
// Callbacks
386
// ============================================================================
387

            
388
extern "C" fn on_tree_node_click(mut refany: RefAny, info: CallbackInfo) -> Update {
389
    let mut refany = match refany.downcast_mut::<NodeClickData>() {
390
        Some(s) => s,
391
        None => return Update::DoNothing,
392
    };
393

            
394
    let node_index = refany.node_index;
395

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

            
404
// ============================================================================
405
// Trait impls
406
// ============================================================================
407

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