Skip to main content

dioxus_swdir_tree/
item_view.rs

1//! The `ItemTreeView<T>` Dioxus component.
2
3use std::fmt;
4
5use dioxus::prelude::*;
6use dioxus_swdir_tree_core::keyboard::{Modifiers as CoreMods, TreeKey};
7use dioxus_swdir_tree_core::{ItemDragMsg, ItemTree, ItemTreeEvent};
8
9use crate::item_row::ItemTreeRow;
10use crate::row::{ArcTheme, default_theme};
11use crate::style as s;
12
13/// A keyboard-navigable, expandable, searchable tree over caller-supplied
14/// in-memory data.
15///
16/// No coroutine or scan driver required — the caller drives data updates
17/// synchronously via [`ItemTree::set_tree`].
18///
19/// # Minimal wiring
20///
21/// ```no_run
22/// # use std::sync::Arc;
23/// # use dioxus::prelude::*;
24/// # use dioxus_swdir_tree_core::{ItemTree, ItemNode, NodeId, ItemTreeEvent};
25/// # use dioxus_swdir_tree_core::selection::SelectionMode;
26/// # use dioxus_swdir_tree::ItemTreeView;
27/// fn app() -> Element {
28///     let mut tree = use_signal(|| {
29///         ItemTree::new().with_display(|s: &String| s.clone())
30///     });
31///
32///     // Build initial data
33///     let root = ItemNode::branch(NodeId(0), "root".to_string(), vec![
34///         ItemNode::leaf(NodeId(1), "alpha".to_string()),
35///     ]);
36///     use_effect(move || { tree.write().set_tree(root.clone()); });
37///
38///     let on_event = move |ev: ItemTreeEvent| match ev {
39///         ItemTreeEvent::Toggled(id) => tree.write().on_toggled(id),
40///         ItemTreeEvent::Selected(id, mode) => tree.write().on_selected(id, mode),
41///     };
42///
43///     rsx! { ItemTreeView { tree, on_event } }
44/// }
45/// ```
46#[derive(Props, Clone)]
47pub struct ItemTreeViewProps<T: Clone + fmt::Debug + Send + Sync + 'static> {
48    pub tree: Signal<ItemTree<T>>,
49    pub on_event: EventHandler<ItemTreeEvent>,
50    #[props(optional)]
51    pub theme: Option<ArcTheme>,
52}
53
54impl<T: Clone + fmt::Debug + Send + Sync + 'static> PartialEq for ItemTreeViewProps<T> {
55    fn eq(&self, other: &Self) -> bool {
56        // Signal and EventHandler compare by ID, so this never requires T: PartialEq.
57        self.tree == other.tree && self.on_event == other.on_event && self.theme == other.theme
58    }
59}
60
61#[allow(non_snake_case)]
62pub fn ItemTreeView<T: Clone + fmt::Debug + Send + Sync + 'static>(
63    props: ItemTreeViewProps<T>,
64) -> Element {
65    let ItemTreeViewProps {
66        tree,
67        on_event,
68        theme,
69    } = props;
70    let theme = theme.unwrap_or_else(default_theme);
71
72    let t = tree.read();
73    let rows = t.visible_rows();
74    let dnd_enabled = t.is_drag_and_drop_enabled();
75    let hover = t.drop_target();
76    let is_dragging = t.is_dragging();
77    drop(t);
78
79    #[cfg(feature = "default-style")]
80    let default_style_css = Some(s::DEFAULT_CSS);
81    #[cfg(not(feature = "default-style"))]
82    let default_style_css: Option<&str> = None;
83
84    let on_keydown = move |evt: KeyboardEvent| {
85        let tree_key = match evt.key() {
86            Key::ArrowUp => TreeKey::Up,
87            Key::ArrowDown => TreeKey::Down,
88            Key::Home => TreeKey::Home,
89            Key::End => TreeKey::End,
90            Key::Enter => TreeKey::Enter,
91            Key::ArrowLeft => TreeKey::Left,
92            Key::ArrowRight => TreeKey::Right,
93            Key::Escape => TreeKey::Escape,
94            Key::Character(ref ch) if ch == " " => TreeKey::Space,
95            _ => return,
96        };
97        let mods = CoreMods {
98            shift: evt.modifiers().shift(),
99            ctrl: evt.modifiers().ctrl(),
100        };
101        if let Some(ev) = tree.read().handle_key(tree_key, mods) {
102            evt.prevent_default();
103            on_event.call(ev);
104        }
105    };
106
107    rsx! {
108        if let Some(css) = default_style_css {
109            style { "{css}" }
110        }
111
112        div {
113            class: s::CLASS_TREE,
114            tabindex: "0",
115            onkeydown: on_keydown,
116            onmouseup: move |_| {
117                if is_dragging {
118                    on_event.call(ItemTreeEvent::Drag(ItemDragMsg::Cancelled));
119                }
120            },
121
122            for row in rows {
123                ItemTreeRow {
124                    key: "{row.id.0}",
125                    row,
126                    on_event,
127                    theme: theme.clone(),
128                    dnd_enabled,
129                    hover,
130                }
131            }
132        }
133    }
134}