1use 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#[derive(Props, Clone, PartialEq)]
14pub(crate) struct TreeRowProps {
15 pub node: TreeNode,
16 pub depth: u32,
17 pub on_event: EventHandler<DirectoryTreeEvent>,
18 pub drag: Option<DragState>,
20 pub theme: ArcTheme,
22}
23
24#[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
43pub 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#[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 let caret_spec = if !is_dir {
100 None
101 } else if is_expanded && !is_loaded {
102 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 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 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}