git.lucas.co / cce-files
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 }