file manager
git clone https://git.lucas.co/cce-files.git
src/pages/network.rs (9.3K)
1 use std::path::{Path, PathBuf};
2 use crate::pages::PageContent;
3 use crate::pages::browse::{BrowseState, DirEntry};
4 use cce_ui::widget::{Adapted, Graph, GraphNode, Breadcrumb, GraphController, PathController};
5
6 pub struct NetworkState {
7 pub graph: Adapted<Graph>,
8 pub breadcrumb: Adapted<Breadcrumb>,
9 pub last_dir: PathBuf,
10 }
11
12 impl Default for NetworkState {
13 fn default() -> Self {
14 let mut graph = Graph::new();
15 // Configure grid settings
16 graph.set_show_network_grid(true);
17 graph.set_grid_sizes(140.0, 70.0);
18 graph.set_skipped_sizes(35.0, 35.0);
19 graph.set_grid_origin(60.0, 60.0);
20 graph.set_grid_snap_enabled(true);
21 graph.set_uniform_background(false);
22 graph.set_network_opacity(0.95);
23
24 let mut breadcrumb = Breadcrumb::new();
25 breadcrumb.set_network_opacity(0.95);
26
27 Self {
28 graph,
29 breadcrumb,
30 last_dir: PathBuf::new(),
31 }
32 }
33 }
34
35 impl NetworkState {
36 pub fn populate_graph(&mut self, current_dir: &Path, entries: &[DirEntry]) {
37 let mut nodes = Vec::new();
38
39 // 1. Parent directory (if any)
40 let parent_node_name = if let Some(parent) = current_dir.parent() {
41 let parent_name = parent
42 .file_name()
43 .map(|s| s.to_string_lossy().to_string())
44 .unwrap_or_else(|| "/".to_string());
45 let name = format!("📁 .. ({})", parent_name);
46 nodes.push(GraphNode {
47 id: String::new(),
48 name: name.clone(),
49 position: (3.0, 0.0),
50 parameters: Vec::new(),
51 geom_visible: true,
52 node_type: String::new(),
53 inputs: 0,
54 outputs: 1,
55 });
56 Some(name)
57 } else {
58 None
59 };
60
61 // 2. Current directory node
62 let current_name = current_dir
63 .file_name()
64 .map(|s| s.to_string_lossy().to_string())
65 .unwrap_or_else(|| "/".to_string());
66 let current_node_name = format!("📁 {}", current_name);
67
68 let current_params = if let Some(ref p_name) = parent_node_name {
69 vec![("input".to_string(), p_name.clone(), "string".to_string())]
70 } else {
71 Vec::new()
72 };
73
74 nodes.push(GraphNode {
75 id: String::new(),
76 name: current_node_name.clone(),
77 position: (3.0, 1.0),
78 parameters: current_params,
79 geom_visible: true,
80 node_type: String::new(),
81 inputs: 1,
82 outputs: 1,
83 });
84
85 // 3. Children nodes
86 for (idx, entry) in entries.iter().enumerate() {
87 let icon = if entry.is_dir { "📁" } else { "📄" };
88 let node_name = format!("{} {}", icon, entry.name);
89
90 // Spreading items in 7 columns starting at row 2
91 let col = (idx % 7) as f32;
92 let row = 2.0 + (idx / 7) as f32;
93
94 nodes.push(GraphNode {
95 id: String::new(),
96 name: node_name,
97 position: (col, row),
98 parameters: vec![("input".to_string(), current_node_name.clone(), "string".to_string())],
99 geom_visible: true,
100 node_type: String::new(),
101 inputs: 1,
102 outputs: 1,
103 });
104 }
105
106 self.graph.set_nodes(&nodes);
107 self.last_dir = current_dir.to_path_buf();
108 }
109 }
110
111 pub fn view(state: &mut NetworkState, browse: &BrowseState, view_dropdown: &mut cce_ui::widget::Adapted<cce_ui::widget::Dropdown>, cx: f32, cy: f32, cw: f32, ch: f32, ctx: &mut cce_ui::context::UiContext) -> PageContent {
112 let mut pc = PageContent::new();
113
114 // Render the Breadcrumb and Dropdown next to it
115 let dropdown_w = 120.0;
116 // TODO(style): the 4/6/16 offsets are a leftover inset from the pane rect
117 // that browse.rs has already dropped; the gap to the dropdown is the rung.
118 let gap = cce_ui::layout::root_plate_gap();
119 let breadcrumb_w = cw - 16.0 - dropdown_w - gap;
120 cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, ctx);
121 {
122 let rect = cce_ui::scene::layout::Rect { x: cx + 4.0, y: cy + 6.0, width: breadcrumb_w, height: 24.0 };
123 crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
124 }
125 cce_ui::layout::render_widget(&mut pc, view_dropdown, cx + 4.0 + breadcrumb_w + gap, cy + 6.0, dropdown_w, 24.0, ctx);
126
127 // Check if directory changed, or if last_dir is empty, and repopulate
128 if state.last_dir != browse.current_dir || state.graph.get_nodes().is_empty() {
129 state.populate_graph(&browse.current_dir, &browse.entries);
130
131 // Update breadcrumb path
132 let mut segments = Vec::new();
133 for component in browse.current_dir.components() {
134 let s = component.as_os_str().to_string_lossy().to_string();
135 if s != "/" && !s.is_empty() {
136 segments.push(s);
137 }
138 }
139 state.breadcrumb.set_path(&segments);
140
141 // Map browse selection to graph node selection if any
142 let has_parent = browse.current_dir.parent().is_some();
143 let offset = if has_parent { 2 } else { 1 };
144 if let Some(sel) = browse.selected {
145 state.graph.set_selected_node(Some(sel + offset));
146 } else {
147 state.graph.set_selected_node(None);
148 }
149 } else {
150 // Sync graph selection with browse selection when they are in sync
151 let has_parent = browse.current_dir.parent().is_some();
152 let offset = if has_parent { 2 } else { 1 };
153
154 if let Some(sel) = browse.selected {
155 let expected_node_idx = sel + offset;
156 if state.graph.selected_node() != Some(expected_node_idx) {
157 if !state.graph.is_dragging() {
158 state.graph.set_selected_node(Some(expected_node_idx));
159 }
160 }
161 } else {
162 if let Some(graph_sel) = state.graph.selected_node() {
163 if graph_sel >= offset {
164 state.graph.set_selected_node(None);
165 }
166 }
167 }
168 }
169
170 // Render the Graph widget into PageContent, shifted down by 28.0 to leave room for the breadcrumb
171 cce_ui::layout::render_widget(&mut pc, &mut state.graph, cx, cy + 28.0, cw, ch - 28.0, ctx);
172
173 pc
174 }
175
176 #[cfg(test)]
177 mod tests {
178 use super::*;
179
180 #[test]
181 fn test_populate_graph_has_parent() {
182 let mut state = NetworkState::default();
183 let current_dir = Path::new("/home/user/project");
184 let entries = vec![
185 DirEntry {
186 name: "file1.txt".to_string(),
187 path: PathBuf::from("/home/user/project/file1.txt"),
188 is_dir: false,
189 size: 100,
190 permissions: 0o644,
191 modified: String::new(),
192 origin: None,
193 },
194 DirEntry {
195 name: "subdir".to_string(),
196 path: PathBuf::from("/home/user/project/subdir"),
197 is_dir: true,
198 size: 4096,
199 permissions: 0o755,
200 modified: String::new(),
201 origin: None,
202 },
203 ];
204
205 state.populate_graph(current_dir, &entries);
206 let nodes = state.graph.get_nodes();
207
208 // 1 parent + 1 current + 2 children = 4 nodes
209 assert_eq!(nodes.len(), 4);
210
211 // Check node names
212 assert_eq!(nodes[0].name, "📁 .. (user)");
213 assert_eq!(nodes[1].name, "📁 project");
214 assert_eq!(nodes[2].name, "📄 file1.txt");
215 assert_eq!(nodes[3].name, "📁 subdir");
216
217 // Check connection parameters
218 // Current directory points to parent
219 assert_eq!(nodes[1].parameters.len(), 1);
220 assert_eq!(nodes[1].parameters[0].0, "input");
221 assert_eq!(nodes[1].parameters[0].1, "📁 .. (user)");
222
223 // Children point to current directory
224 assert_eq!(nodes[2].parameters.len(), 1);
225 assert_eq!(nodes[2].parameters[0].0, "input");
226 assert_eq!(nodes[2].parameters[0].1, "📁 project");
227
228 assert_eq!(nodes[3].parameters.len(), 1);
229 assert_eq!(nodes[3].parameters[0].0, "input");
230 assert_eq!(nodes[3].parameters[0].1, "📁 project");
231 }
232
233 #[test]
234 fn test_populate_graph_root_no_parent() {
235 let mut state = NetworkState::default();
236 let current_dir = Path::new("/");
237 let entries = vec![
238 DirEntry {
239 name: "bin".to_string(),
240 path: PathBuf::from("/bin"),
241 is_dir: true,
242 size: 4096,
243 permissions: 0o755,
244 modified: String::new(),
245 origin: None,
246 },
247 ];
248
249 state.populate_graph(current_dir, &entries);
250 let nodes = state.graph.get_nodes();
251
252 // No parent, so 1 current + 1 child = 2 nodes
253 assert_eq!(nodes.len(), 2);
254 assert_eq!(nodes[0].name, "📁 /");
255 assert_eq!(nodes[1].name, "📁 bin");
256
257 // Current has no parent parameter
258 assert!(nodes[0].parameters.is_empty());
259
260 // Child points to current
261 assert_eq!(nodes[1].parameters.len(), 1);
262 assert_eq!(nodes[1].parameters[0].0, "input");
263 assert_eq!(nodes[1].parameters[0].1, "📁 /");
264 }
265 }