Skip to main content

dioxus_swdir_tree/
row.rs

1//! The per-row component rendered by [`crate::DirectoryTreeView`].
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use dioxus::prelude::*;
7use dioxus_swdir_tree_core::{DragMsg, DragState, IconRole, IconTheme, TreeNode};
8
9use crate::event::DirectoryTreeEvent;
10use crate::style as s;
11
12/// Props for a single visible tree row.
13#[derive(Props, Clone, PartialEq)]
14pub(crate) struct TreeRowProps {
15    pub node: TreeNode,
16    pub depth: u32,
17    pub on_event: EventHandler<DirectoryTreeEvent>,
18    /// Active drag session (if any), for drop-target highlighting.
19    pub drag: Option<DragState>,
20    /// Icon theme wrapper (pointer-equality checked for PartialEq).
21    pub theme: ArcTheme,
22}
23
24// ── ArcTheme wrapper ──────────────────────────────────────────────────────────
25
26/// Thin `Arc<dyn IconTheme>` wrapper that implements `PartialEq` via
27/// pointer equality so it can be used as a Dioxus prop.
28#[derive(Clone)]
29pub struct ArcTheme(pub Arc<dyn IconTheme>);
30
31impl PartialEq for ArcTheme {
32    fn eq(&self, other: &Self) -> bool {
33        Arc::ptr_eq(&self.0, &other.0)
34    }
35}
36
37impl Default for ArcTheme {
38    fn default() -> Self {
39        default_theme()
40    }
41}
42
43/// Return the default theme for the active feature set.
44pub fn default_theme() -> ArcTheme {
45    #[cfg(feature = "icons")]
46    {
47        ArcTheme(Arc::new(dioxus_swdir_tree_core::LucideTheme))
48    }
49    #[cfg(not(feature = "icons"))]
50    {
51        ArcTheme(Arc::new(dioxus_swdir_tree_core::UnicodeTheme))
52    }
53}
54
55// ── TreeRow component ─────────────────────────────────────────────────────────
56
57/// One visible row: indented caret + icon + label.
58#[component]
59pub(crate) fn TreeRow(props: TreeRowProps) -> Element {
60    let TreeRowProps {
61        node,
62        depth,
63        on_event,
64        drag,
65        theme,
66    } = props;
67
68    let path: PathBuf = node.path.clone();
69    let is_dir = node.is_dir;
70    let is_expanded = node.is_expanded;
71    let is_loaded = node.is_loaded;
72    let has_error = node.error.is_some();
73    let is_selected = node.is_selected;
74    let is_drag_active = drag.is_some();
75
76    let is_drop_target = drag
77        .as_ref()
78        .and_then(|d| d.hovered_target.as_ref())
79        .map(|t| *t == path)
80        .unwrap_or(false);
81
82    let indent_px = depth * 16;
83
84    let mut classes = s::CLASS_ROW.to_string();
85    if is_selected {
86        classes.push(' ');
87        classes.push_str(s::CLASS_ROW_SELECTED);
88    }
89    if has_error {
90        classes.push(' ');
91        classes.push_str(s::CLASS_ROW_ERROR);
92    }
93    if is_drop_target {
94        classes.push(' ');
95        classes.push_str(s::CLASS_ROW_DROP_TARGET);
96    }
97
98    // ── Icon glyphs from theme ────────────────────────────────────────
99    let caret_spec = if !is_dir {
100        None
101    } else if is_expanded && !is_loaded {
102        // Loading indicator — no theme role for this; use raw glyph.
103        Some(("…", None, None))
104    } else {
105        let role = if is_expanded {
106            IconRole::CaretDown
107        } else {
108            IconRole::CaretRight
109        };
110        let spec = theme.0.glyph(role);
111        Some((
112            // Safety: lifetime of glyph is 'static since IconSpec uses Cow<'static>
113            Box::leak(spec.glyph.into_owned().into_boxed_str()) as &str,
114            spec.font,
115            spec.size,
116        ))
117    };
118
119    let icon_role = if has_error {
120        IconRole::Error
121    } else if !is_dir {
122        IconRole::File
123    } else if is_expanded {
124        IconRole::FolderOpen
125    } else {
126        IconRole::FolderClosed
127    };
128    let icon_spec = theme.0.glyph(icon_role);
129
130    let caret_str = caret_spec.map(|(g, _, _)| g).unwrap_or(" ");
131    let caret_font = caret_spec.and_then(|(_, f, _)| f).unwrap_or("");
132    let caret_size = caret_spec.and_then(|(_, _, s)| s);
133    let caret_style = if caret_font.is_empty() {
134        String::new()
135    } else {
136        format!("font-family: {caret_font};")
137    };
138    let caret_size_style = caret_size
139        .map(|s| format!("{} font-size: {s}px;", caret_style))
140        .unwrap_or(caret_style);
141
142    let icon_str = Box::leak(icon_spec.glyph.into_owned().into_boxed_str()) as &str;
143    let icon_font = icon_spec.font.unwrap_or("");
144    let icon_size = icon_spec.size;
145    let icon_style = if icon_font.is_empty() {
146        String::new()
147    } else {
148        format!("font-family: {icon_font};")
149    };
150    let icon_size_style = icon_size
151        .map(|s| format!("{} font-size: {s}px;", icon_style))
152        .unwrap_or(icon_style);
153
154    let label = node.file_name().to_string_lossy().into_owned();
155    let error_title: String = node
156        .error
157        .as_ref()
158        .map(|e| e.message().to_string())
159        .unwrap_or_default();
160
161    // ── Event handlers ────────────────────────────────────────────────
162    let caret_path = path.clone();
163    let on_caret_click = move |evt: MouseEvent| {
164        evt.stop_propagation();
165        on_event.call(DirectoryTreeEvent::Toggled(caret_path.clone()));
166    };
167
168    let press_path = path.clone();
169    let on_mousedown = move |_evt: MouseEvent| {
170        on_event.call(DirectoryTreeEvent::Drag(DragMsg::Pressed {
171            path: press_path.clone(),
172            is_dir,
173        }));
174    };
175
176    let enter_path = path.clone();
177    let on_mouseenter = move |_evt: MouseEvent| {
178        on_event.call(DirectoryTreeEvent::Drag(DragMsg::Entered(
179            enter_path.clone(),
180        )));
181    };
182
183    let exit_path = path.clone();
184    let on_mouseleave = move |_evt: MouseEvent| {
185        on_event.call(DirectoryTreeEvent::Drag(DragMsg::Exited(exit_path.clone())));
186    };
187
188    let release_path = path.clone();
189    let on_mouseup = move |evt: MouseEvent| {
190        if is_drag_active {
191            evt.stop_propagation();
192            on_event.call(DirectoryTreeEvent::Drag(DragMsg::Released(
193                release_path.clone(),
194            )));
195        }
196    };
197
198    rsx! {
199        div {
200            class: "{classes}",
201            style: "padding-left: {indent_px}px;",
202            title: "{error_title}",
203            onmousedown: on_mousedown,
204            onmouseenter: on_mouseenter,
205            onmouseleave: on_mouseleave,
206            onmouseup: on_mouseup,
207
208            span {
209                class: s::CLASS_CARET,
210                style: "{caret_size_style}",
211                onclick: on_caret_click,
212                "{caret_str}"
213            }
214            span {
215                class: s::CLASS_ICON,
216                style: "{icon_size_style}",
217                "{icon_str}"
218            }
219            span {
220                class: s::CLASS_LABEL,
221                "{label}"
222            }
223        }
224    }
225}