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}