git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

commit4f4900aaa0bfd5b61526d1a8ba924a784ae1553d
parent43e864cb3d
authorLucas Galante <[email protected]>
date2026-07-04 16:17
Add togglable control panel plate and support image selection and deletion

 src/main.rs | 275 ++++++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 212 insertions(+), 63 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c24f84b..54c08b8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use wayland_client::QueueHandle;
 use glyphon::{FontSystem, Buffer, Metrics, Attrs};
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Backplate};
+use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Backplate, Plate, Label};
 use image::GenericImageView;
 
 #[derive(Debug, Clone)]
@@ -18,6 +18,7 @@ enum AppMessage {
     SetOpacity95,
     SetOpacity75,
     SetOpacity50,
+    ToggleControlPanel,
     AddNode,
     AddImage,
     AddImageFromPath(std::path::PathBuf),
@@ -74,6 +75,10 @@ struct GraphApp {
     dragging_image_idx: Option<usize>,
     drag_image_ox: f32,
     drag_image_oy: f32,
+    selected_image_idx: Option<usize>,
+    control_panel: Plate,
+    show_control_panel: bool,
+    control_panel_label: Label,
 }
 
 fn get_default_project_path() -> std::path::PathBuf {
@@ -381,13 +386,14 @@ fn save_project_to_kdl_path(path: &std::path::Path, state: &GraphProjectState) -
     Ok(())
 }
 
-fn get_view_options(show_grid: bool, uniform_bg: bool, opacity: f32) -> Vec<String> {
+fn get_view_options(show_grid: bool, uniform_bg: bool, opacity: f32, show_panel: bool) -> Vec<String> {
     vec![
         format!("{} Show Grid", if show_grid { "✓" } else { "  " }),
         format!("{} Uniform Background", if uniform_bg { "✓" } else { "  " }),
         format!("{} Opacity: 95%", if (opacity - 0.95).abs() < 0.05 { "✓" } else { "  " }),
         format!("{} Opacity: 75%", if (opacity - 0.75).abs() < 0.05 { "✓" } else { "  " }),
         format!("{} Opacity: 50%", if (opacity - 0.50).abs() < 0.05 { "✓" } else { "  " }),
+        format!("{} Control Panel", if show_panel { "✓" } else { "  " }),
     ]
 }
 
@@ -476,11 +482,17 @@ impl GraphApp {
                 self.graph.set_selected_node(None);
                 self.needs_rebuild = true;
             }
+        } else if let Some(idx) = self.selected_image_idx {
+            if idx < self.loaded_images.len() {
+                self.loaded_images.remove(idx);
+                self.selected_image_idx = None;
+                self.needs_rebuild = true;
+            }
         }
     }
 
     fn update_view_options(&mut self) {
-        self.dropdown_view.options = get_view_options(self.show_grid, self.uniform_background, self.opacity);
+        self.dropdown_view.options = get_view_options(self.show_grid, self.uniform_background, self.opacity, self.show_control_panel);
     }
 
     fn add_element_labels(
@@ -572,6 +584,13 @@ impl GraphApp {
             &mut self.text_items,
             scale,
         );
+        Self::add_element_labels(
+            &self.control_panel,
+            &self.ui_context,
+            &mut self.font_system,
+            &mut self.text_items,
+            scale,
+        );
     }
 
     fn new_project(&mut self) {
@@ -849,7 +868,7 @@ impl Application for GraphApp {
         .with_custom_display_text("Edit");
 
         let dropdown_view = Dropdown::new(
-            get_view_options(show_grid, uniform_background, opacity),
+            get_view_options(show_grid, uniform_background, opacity, false),
             999,
         )
         .with_custom_display_text("View");
@@ -858,6 +877,17 @@ impl Application for GraphApp {
 
         let root_window = Backplate::new(0.0, 0.0, 1024.0, 768.0);
 
+        let mut control_panel = Plate::new(800.0, 50.0, 210.0, 160.0)
+            .with_color([0.15, 0.15, 0.2, 0.95])
+            .with_solid_border([0.3, 0.3, 0.4, 1.0], 1.0)
+            .with_draggable(true);
+        let show_control_panel = false;
+        control_panel.visible = show_control_panel;
+
+        let control_panel_label = Label::new("No Node Selected")
+            .with_font_size(12.0)
+            .with_color([204, 204, 221]);
+
         let mut app = Self {
             root_window,
             menu_bar,
@@ -883,6 +913,10 @@ impl Application for GraphApp {
             dragging_image_idx: None,
             drag_image_ox: 0.0,
             drag_image_oy: 0.0,
+            selected_image_idx: None,
+            control_panel,
+            show_control_panel,
+            control_panel_label,
         };
         
         app.root_window.set_rect(0.0, 0.0, 1024.0, 768.0);
@@ -1034,6 +1068,13 @@ impl Application for GraphApp {
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
+            AppMessage::ToggleControlPanel => {
+                self.show_control_panel = !self.show_control_panel;
+                self.control_panel.visible = self.show_control_panel;
+                self.update_view_options();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
             AppMessage::AddNode => {
                 let mut nodes = self.graph.get_nodes();
                 let next_id = nodes.len() + 1;
@@ -1077,6 +1118,7 @@ impl Application for GraphApp {
         self.ui_context.clear_popovers();
         cce_ui::widget::popovers::clear();
 
+        let is_first_layout = !self.widgets_registered;
         if !self.widgets_registered {
             unsafe {
                 let self_ptr = self as *mut Self;
@@ -1087,16 +1129,66 @@ impl Application for GraphApp {
                 self.ui_context.register_widget(self.dropdown_edit.base().unwrap().id(), &mut (*self_ptr).dropdown_edit as *mut Dropdown as *mut (dyn Element + 'static));
                 self.ui_context.register_widget(self.dropdown_view.base().unwrap().id(), &mut (*self_ptr).dropdown_view as *mut Dropdown as *mut (dyn Element + 'static));
                 self.ui_context.register_widget(self.graph_id, &mut (*self_ptr).graph as *mut Graph as *mut (dyn Element + 'static));
+                self.ui_context.register_widget(self.control_panel.base().unwrap().id(), &mut (*self_ptr).control_panel as *mut Plate as *mut (dyn Element + 'static));
+                self.ui_context.register_widget(self.control_panel_label.base().unwrap().id(), &mut (*self_ptr).control_panel_label as *mut Label as *mut (dyn Element + 'static));
 
                 self.root_window.add_child(self.menu_bar.as_ptr_mut(), &mut self.ui_context);
                 self.root_window.add_child(self.dropdown_file.as_ptr_mut(), &mut self.ui_context);
                 self.root_window.add_child(self.dropdown_edit.as_ptr_mut(), &mut self.ui_context);
                 self.root_window.add_child(self.dropdown_view.as_ptr_mut(), &mut self.ui_context);
                 self.root_window.add_child(self.graph.as_ptr_mut(), &mut self.ui_context);
+                self.control_panel.add_child(self.control_panel_label.as_ptr_mut(), &mut self.ui_context);
+                self.root_window.add_child(self.control_panel.as_ptr_mut(), &mut self.ui_context);
             }
             self.widgets_registered = true;
         }
 
+        // Check selected node and update control panel label
+        let selected_node_idx = self.graph.selected_node();
+        let label_text = if let Some(idx) = selected_node_idx {
+            let nodes = self.graph.get_nodes();
+            if let Some(node) = nodes.get(idx) {
+                let mut info = format!("Selected Node:\nID: {}\nName: {}\nType: {}\nInputs: {}\nOutputs: {}",
+                    node.id, node.name, node.node_type, node.inputs, node.outputs
+                );
+                if !node.parameters.is_empty() {
+                    info.push_str("\n\nParameters:");
+                    for (name, val, p_type) in &node.parameters {
+                        info.push_str(&format!("\n- {}: {} ({})", name, val, p_type));
+                    }
+                }
+                info
+            } else {
+                "No Object Selected".to_string()
+            }
+        } else if let Some(img_idx) = self.selected_image_idx {
+            if let Some(img) = self.loaded_images.get(img_idx) {
+                let filename = std::path::Path::new(&img.path)
+                    .file_name()
+                    .and_then(|f| f.to_str())
+                    .unwrap_or(&img.path);
+                format!(
+                    "Selected Image:\nName: {}\nPosition: (Col {:.1}, Row {:.1})\nSize: {:.1} x {:.1} cells\nResolution: {} x {} px",
+                    filename, img.position.0, img.position.1,
+                    img.size.0, img.size.1,
+                    img.pixel_width, img.pixel_height
+                )
+            } else {
+                "No Object Selected".to_string()
+            }
+        } else {
+            "No Object Selected".to_string()
+        };
+
+        let text_changed = {
+            let current_text = self.control_panel_label.base().and_then(|b| b.label.as_ref());
+            current_text != Some(&label_text)
+        };
+        if text_changed {
+            self.control_panel_label.set_text(&label_text);
+            self.needs_rebuild = true;
+        }
+
         if self.dropdown_file.popover_rect().is_some() {
             self.ui_context.register_popover(&self.dropdown_file);
             cce_ui::widget::popovers::register(&self.dropdown_file);
@@ -1111,7 +1203,7 @@ impl Application for GraphApp {
         }
 
         let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
-        if self.needs_rebuild || size_changed {
+        if self.needs_rebuild || size_changed || is_first_layout {
             self.width = size.width as u32;
             self.height = size.height as u32;
             self.scale_factor = scale;
@@ -1128,6 +1220,14 @@ impl Application for GraphApp {
             
             // Layout Graph below MenuBar
             self.graph.set_rect(0.0, 42.0, size.width, size.height - 42.0);
+
+            // Initial control panel positioning and bounds settings
+            if size_changed || is_first_layout {
+                let cpx = (size.width - 230.0).max(10.0);
+                let cpy = 55.0; // Float below MenuBar
+                self.control_panel.set_rect(cpx, cpy, 210.0, 160.0);
+            }
+            self.control_panel.set_bounds(0.0, 42.0, size.width, size.height - 42.0);
             
             self.rebuild_text_items();
             self.needs_rebuild = false;
@@ -1166,7 +1266,7 @@ impl Application for GraphApp {
             }
         };
 
-        for img in &self.loaded_images {
+        for (i, img) in self.loaded_images.iter().enumerate() {
             let col = img.position.0;
             let row = img.position.1;
             
@@ -1195,6 +1295,20 @@ impl Application for GraphApp {
                     push_clipped(px_x, px_y, px_w + 0.5, px_h + 0.5, [r, g, b, a], quads);
                 }
             }
+
+            if Some(i) == self.selected_image_idx {
+                let border_thickness = 2.0;
+                let border_color = [0.0, 0.75, 1.0, 1.0]; // Vibrant cyan selection outline
+                
+                // Top border
+                push_clipped(screen_x - border_thickness, screen_y - border_thickness, screen_w + 2.0 * border_thickness, border_thickness, border_color, quads);
+                // Bottom border
+                push_clipped(screen_x - border_thickness, screen_y + screen_h, screen_w + 2.0 * border_thickness, border_thickness, border_color, quads);
+                // Left border
+                push_clipped(screen_x - border_thickness, screen_y, border_thickness, screen_h, border_color, quads);
+                // Right border
+                push_clipped(screen_x + screen_w, screen_y, border_thickness, screen_h, border_color, quads);
+            }
         }
     }
 
@@ -1224,40 +1338,55 @@ impl Application for GraphApp {
             if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
             if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
         } else {
-            // Otherwise feed it to Graph
-            if self.graph.is_dragging() {
-                if self.graph.drag_update(pos.x, pos.y) {
-                    changed = true;
-                }
-            } else if let Some(img_idx) = self.dragging_image_idx {
-                let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
-                let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
-                let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
-                let step_x = grid_size_x + skipped_col_w;
-                let step_y = grid_size_y + skipped_row_h;
-
-                let nx = pos.x - self.drag_image_ox;
-                let ny = pos.y - self.drag_image_oy;
-
-                let mut col = (nx - grid_origin_x) / step_x;
-                let mut row = (ny - grid_origin_y) / step_y;
-
-                if self.graph.grid_snap_enabled() {
-                    col = (col * 2.0).round() / 2.0;
-                    row = (row * 2.0).round() / 2.0;
+            let mut handled_by_panel = false;
+            if self.control_panel.visible {
+                if self.control_panel.is_dragging() || self.control_panel.hit_test(pos.x, pos.y, &self.ui_context) {
+                    if self.control_panel.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
+                    handled_by_panel = true;
+                } else {
+                    if self.control_panel.on_cursor_moved(-1000.0, -1000.0, &mut self.ui_context) { changed = true; }
                 }
+            }
+
+            if !handled_by_panel {
+                // Otherwise feed it to Graph
+                if self.graph.is_dragging() {
+                    if self.graph.drag_update(pos.x, pos.y) {
+                        changed = true;
+                    }
+                } else if let Some(img_idx) = self.dragging_image_idx {
+                    let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
+                    let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
+                    let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
+                    let step_x = grid_size_x + skipped_col_w;
+                    let step_y = grid_size_y + skipped_row_h;
+
+                    let nx = pos.x - self.drag_image_ox;
+                    let ny = pos.y - self.drag_image_oy;
+
+                    let mut col = (nx - grid_origin_x) / step_x;
+                    let mut row = (ny - grid_origin_y) / step_y;
+
+                    if self.graph.grid_snap_enabled() {
+                        col = (col * 2.0).round() / 2.0;
+                        row = (row * 2.0).round() / 2.0;
+                    }
 
-                if let Some(img) = self.loaded_images.get_mut(img_idx) {
-                    if (img.position.0 - col).abs() > 0.001 || (img.position.1 - row).abs() > 0.001 {
-                        img.position = (col, row);
+                    if let Some(img) = self.loaded_images.get_mut(img_idx) {
+                        if (img.position.0 - col).abs() > 0.001 || (img.position.1 - row).abs() > 0.001 {
+                            img.position = (col, row);
+                            changed = true;
+                        }
+                    }
+                } else {
+                    if self.graph.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) {
                         changed = true;
                     }
                 }
             } else {
-                if self.graph.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) {
-                    changed = true;
-                }
+                if self.graph.on_cursor_moved(-1000.0, -1000.0, &mut self.ui_context) { changed = true; }
             }
+            
             // Clear hover states if cursor moved away
             if self.dropdown_file.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
             if self.dropdown_edit.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
@@ -1325,6 +1454,7 @@ impl Application for GraphApp {
                         2 => msg_out = Some(AppMessage::SetOpacity95),
                         3 => msg_out = Some(AppMessage::SetOpacity75),
                         4 => msg_out = Some(AppMessage::SetOpacity50),
+                        5 => msg_out = Some(AppMessage::ToggleControlPanel),
                         _ => {}
                     }
                     self.dropdown_view.selected = 999;
@@ -1343,40 +1473,59 @@ impl Application for GraphApp {
                 }
             }
         } else {
-            // Otherwise route to Graph
-            if button == MouseButton::Left {
-                if state == ElementState::Pressed {
-                    if self.graph.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
-                        if self.graph.is_dragging() {
-                            self.graph.drag_begin(pos.x, pos.y);
-                        }
-                        changed = true;
-                    } else if let Some(img_idx) = self.hit_test_image(pos.x, pos.y) {
-                        let img = &self.loaded_images[img_idx];
-                        let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
-                        let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
-                        let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
-                        let step_x = grid_size_x + skipped_col_w;
-                        let step_y = grid_size_y + skipped_row_h;
-
-                        let img_screen_x = grid_origin_x + img.position.0 * step_x;
-                        let img_screen_y = grid_origin_y + img.position.1 * step_y;
-
-                        self.dragging_image_idx = Some(img_idx);
-                        self.drag_image_ox = pos.x - img_screen_x;
-                        self.drag_image_oy = pos.y - img_screen_y;
+            let mut handled_by_panel = false;
+            if self.control_panel.visible {
+                if self.control_panel.is_dragging() || self.control_panel.hit_test(pos.x, pos.y, &self.ui_context) {
+                    if self.control_panel.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
                         changed = true;
                     }
-                } else if state == ElementState::Released {
-                    if self.graph.is_dragging() {
-                        self.graph.drag_end();
-                        changed = true;
-                    } else if self.dragging_image_idx.is_some() {
-                        self.dragging_image_idx = None;
-                        changed = true;
-                    } else {
+                    handled_by_panel = true;
+                }
+            }
+
+            if !handled_by_panel {
+                // Otherwise route to Graph
+                if button == MouseButton::Left {
+                    if state == ElementState::Pressed {
                         if self.graph.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+                            if self.graph.is_dragging() {
+                                self.graph.drag_begin(pos.x, pos.y);
+                            }
+                            self.selected_image_idx = None;
                             changed = true;
+                        } else if let Some(img_idx) = self.hit_test_image(pos.x, pos.y) {
+                            let img = &self.loaded_images[img_idx];
+                            let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
+                            let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
+                            let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
+                            let step_x = grid_size_x + skipped_col_w;
+                            let step_y = grid_size_y + skipped_row_h;
+
+                            let img_screen_x = grid_origin_x + img.position.0 * step_x;
+                            let img_screen_y = grid_origin_y + img.position.1 * step_y;
+
+                            self.dragging_image_idx = Some(img_idx);
+                            self.drag_image_ox = pos.x - img_screen_x;
+                            self.drag_image_oy = pos.y - img_screen_y;
+                            self.selected_image_idx = Some(img_idx);
+                            self.graph.set_selected_node(None);
+                            changed = true;
+                        } else {
+                            self.selected_image_idx = None;
+                            self.graph.set_selected_node(None);
+                            changed = true;
+                        }
+                    } else if state == ElementState::Released {
+                        if self.graph.is_dragging() {
+                            self.graph.drag_end();
+                            changed = true;
+                        } else if self.dragging_image_idx.is_some() {
+                            self.dragging_image_idx = None;
+                            changed = true;
+                        } else {
+                            if self.graph.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context) {
+                                changed = true;
+                            }
                         }
                     }
                 }