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};
14use 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
29use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq, impl_vec_mut};
30
31use crate::callbacks::{Callback, CallbackInfo};
32
33pub type TreeViewOnNodeClickCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
40impl_widget_callback!(
41 TreeViewOnNodeClick,
42 OptionTreeViewOnNodeClick,
43 TreeViewOnNodeClickCallback,
44 TreeViewOnNodeClickCallbackType
45);
46
47azul_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
60const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
63const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
64const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
65 StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
66
67const TEXT_COLOR: ColorU = ColorU { r: 30, g: 30, b: 30, a: 255 };
70const SELECTED_BG: ColorU = ColorU { r: 0, g: 120, b: 215, a: 255 };
71const SELECTED_TEXT: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
72const HOVER_BG: ColorU = ColorU { r: 229, g: 243, b: 255, a: 255 };
73const ICON_COLOR: ColorU = ColorU { r: 100, g: 100, b: 100, a: 255 };
74
75static 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
85static 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 CssPropertyWithConditions::on_hover(CssProperty::const_background_content(
98 StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(HOVER_BG)]),
99 )),
100];
101
102static 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
122static 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
130static 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
140static 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
147static 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#[derive(Debug, Clone, PartialEq)]
160#[repr(C)]
161pub struct TreeViewNode {
162 pub label: AzString,
164 pub children: TreeViewNodeVec,
166 pub is_expanded: bool,
168 pub is_selected: bool,
170}
171
172impl TreeViewNode {
173 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 pub fn add_child(&mut self, child: TreeViewNode) {
185 self.children.push(child);
186 }
187
188 pub fn with_child(mut self, child: TreeViewNode) -> Self {
190 self.children.push(child);
191 self
192 }
193
194 pub fn with_expanded(mut self, expanded: bool) -> Self {
196 self.is_expanded = expanded;
197 self
198 }
199
200 pub fn with_selected(mut self, selected: bool) -> Self {
202 self.is_selected = selected;
203 self
204 }
205}
206
207impl_option!(TreeViewNode, OptionTreeViewNode, copy = false, [Debug, Clone, PartialEq]);
208impl_vec!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor, TreeViewNodeVecDestructorType, TreeViewNodeVecSlice, OptionTreeViewNode);
209impl_vec_clone!(TreeViewNode, TreeViewNodeVec, TreeViewNodeVecDestructor);
210impl_vec_debug!(TreeViewNode, TreeViewNodeVec);
211impl_vec_partialeq!(TreeViewNode, TreeViewNodeVec);
212impl_vec_mut!(TreeViewNode, TreeViewNodeVec);
213
214#[derive(Debug, Clone, PartialEq)]
216#[repr(C)]
217pub struct TreeView {
218 pub root: TreeViewNode,
220 pub on_node_click: OptionTreeViewOnNodeClick,
222}
223
224impl TreeView {
225 pub fn new(root: TreeViewNode) -> Self {
227 Self {
228 root,
229 on_node_click: None.into(),
230 }
231 }
232
233 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 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 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
275fn 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 let row_style = if node.is_selected {
292 ROW_SELECTED_STYLE
293 } else {
294 ROW_STYLE
295 };
296
297 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 Dom::create_div()
309 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LEAF_SPACER_STYLE))
310 };
311
312 let label = Dom::create_text(node.label.clone())
314 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(LABEL_STYLE));
315
316 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 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 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 count_descendants(node.children.as_slice(), index);
362 }
363}
364
365fn 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
375struct NodeClickData {
380 node_index: usize,
381 on_node_click: OptionTreeViewOnNodeClick,
382}
383
384extern "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
404impl From<TreeView> for Dom {
409 fn from(tv: TreeView) -> Dom {
410 tv.dom()
411 }
412}