git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitafe0f78d3a8e8202ed7de80b579b2bdd249660bb
parentb4ac763b01
authorLucas Galante <[email protected]>
date2026-07-13 14:41
feat(api): menu_action executes label-matched menu-pane items

The ~250-line label match (pane toggles, File/Edit actions, zoom) moves
verbatim out of sync_parameters_to_project into execute_menu_action(&str)
-> bool; the triggered-buttons loop now calls it per button, unchanged.
A new HTTP action {"action":"menu_action","label":"Show Spreadsheet
Pane"} drives it directly — these items live outside the menubars, so
the index-matched menu_click could never reach them. Unknown labels
reply an error. The handler re-uploads vertices after a successful
action (the arms relayout themselves, but render() draws the last
uploaded buffer — the ToggleCircularPane ritual).

Live-verified: spreadsheet pane toggles on via curl with the pane
appearing at its exact 250px rect, toggles off byte-identical (AE=0),
unknown label errors.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fb2AGNKjfQZxfbxP43fVbC

 src/app.rs    | 504 ++++++++++++++++++++++++++++++----------------------------
 src/window.rs |  11 ++
 2 files changed, 269 insertions(+), 246 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 452784c..6c707a8 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -337,6 +337,9 @@ pub enum HttpAction {
     ToggleCircularPane,
     MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
     MenuClosed { widget_idx: usize, menu_idx: usize },
+    /// Execute a label-matched menu-pane action ("Show Spreadsheet Pane", "Save", ...)
+    /// — the items `menu_click`'s index-matched menubar dispatch cannot reach.
+    MenuAction { label: String },
 }
 
 #[derive(Debug)]
@@ -1496,252 +1499,7 @@ impl State {
                         self.sync_nodes();
 
                         for btn_name in triggered_buttons {
-                            match btn_name.as_str() {
-                                "Update Parameters" => {
-                                    if let Some(slot_idx) = self.graph().selected_node() {
-                                        let dir = self.current_dir_mut();
-                                        if let Some(child) = dir.children.get_mut(slot_idx) {
-                                            if child.node_type == "opencl" {
-                                                let code_val = child.params.iter()
-                                                    .find(|p| p.name == "Code")
-                                                    .map(|p| p.default.clone())
-                                                    .unwrap_or_default();
-                                                let parsed_params = crate::geometry::parse_dynamic_params(&code_val);
-                                                let mut new_params = Vec::new();
-                                                for base_name in &["Input", "Code", "Update Parameters"] {
-                                                    if let Some(p) = child.params.iter().find(|p| p.name == *base_name) {
-                                                        new_params.push(p.clone());
-                                                    }
-                                                }
-                                                for mut parsed in parsed_params {
-                                                    if let Some(existing) = child.params.iter().find(|p| p.name == parsed.name) {
-                                                        parsed.default = existing.default.clone();
-                                                    }
-                                                    new_params.push(parsed);
-                                                }
-                                                child.params = new_params;
-                                                let updated_disp = param_display(&child.params);
-                                                self.param_mut().set_display_params(&updated_disp);
-                                                self.rebuild_scene_geometry();
-                                                self.sync_nodes();
-                                            }
-                                        }
-                                    }
-                                }
-                                "New Project" | "New" => {
-                                    self.new_project();
-                                }
-                                "Open" => {
-                                    self.open_file_chooser();
-                                }
-                                "Save" => {
-                                    let path_opt = self.loaded_project_path.clone();
-                                    if let Some(path) = path_opt {
-                                        if let Err(e) = self.save_to_file(&path) {
-                                            eprintln!("Failed to save project: {:?}", e);
-                                            self.update_status_text(&format!("Failed to save: {:?}", e));
-                                        } else {
-                                            self.update_status_text(&format!("Project saved to {}", path.display()));
-                                            self.add_recent_file(path);
-                                        }
-                                    } else {
-                                        self.save_file_chooser();
-                                    }
-                                }
-                                "Save As" => {
-                                    self.save_file_chooser();
-                                }
-                                "Exit" => {
-                                    self.exit_requested = true;
-                                }
-                                "Zoom In" => {
-                                    self.zoom(1.15, None);
-                                }
-                                "Zoom Out" => {
-                                    self.zoom(1.0 / 1.15, None);
-                                }
-                                "Reset Zoom" => {
-                                    self.grid_size_x = 150.0;
-                                    self.grid_size_y = 75.0;
-                                    self.skipped_col_w = 37.5;
-                                    self.skipped_row_h = 37.5;
-                                    self.sync_grid_settings();
-                                }
-                                "Detach Circular Window" | "Detach Pane" => {
-                                    self.execute_action(Action::DetachCircularWindow);
-                                }
-                                "Show Network Pane" => {
-                                    self.show_network = !self.show_network;
-                                    self.slots.content.set_visible(self.show_network);
-                                    self.slots.left_menubar.set_visible(self.show_network);
-                                    self.slots.breadcrumb.set_visible(self.show_network);
-                                    let val = self.show_network;
-                                    self.menu_mut(HEADER_IDX).set_item_checked(2, 4, val);
-                                    if !self.show_network && self.focused_pane == LEFT_MENUBAR_IDX {
-                                        self.focused_pane = get_next_visible_pane(
-                                            self.focused_pane,
-                                            self.show_network,
-                                            self.show_viewport,
-                                            self.show_parameters,
-                                            self.show_spreadsheet,
-                                            false,
-                                        );
-                                    }
-                                    self.rebuild_positions();
-                                    self.apply_layout();
-                                    self.sync_pane_focus();
-                                }
-                                "Show Viewport Pane" => {
-                                    self.show_viewport = !self.show_viewport;
-                                    self.slots.viewport.set_visible(self.show_viewport);
-                                    self.slots.right_menubar.set_visible(self.show_viewport);
-                                    let val = self.show_viewport;
-                                    self.menu_mut(HEADER_IDX).set_item_checked(2, 5, val);
-                                    if !self.show_viewport && self.focused_pane == RIGHT_MENUBAR_IDX {
-                                        self.focused_pane = get_next_visible_pane(
-                                            self.focused_pane,
-                                            self.show_network,
-                                            self.show_viewport,
-                                            self.show_parameters,
-                                            self.show_spreadsheet,
-                                            false,
-                                        );
-                                    }
-                                    self.rebuild_positions();
-                                    self.apply_layout();
-                                    self.sync_pane_focus();
-                                }
-                                "Show Parameters Pane" => {
-                                    self.show_parameters = !self.show_parameters;
-                                    self.slots.param.set_visible(self.show_parameters);
-                                    self.slots.param_menubar.set_visible(self.show_parameters);
-                                    let val = self.show_parameters;
-                                    self.menu_mut(HEADER_IDX).set_item_checked(2, 6, val);
-                                    if !self.show_parameters && self.focused_pane == PARAM_MENUBAR_IDX {
-                                        self.focused_pane = get_next_visible_pane(
-                                            self.focused_pane,
-                                            self.show_network,
-                                            self.show_viewport,
-                                            self.show_parameters,
-                                            self.show_spreadsheet,
-                                            false,
-                                        );
-                                    }
-                                    self.rebuild_positions();
-                                    self.apply_layout();
-                                    self.sync_pane_focus();
-                                }
-                                "Show Spreadsheet Pane" => {
-                                    self.show_spreadsheet = !self.show_spreadsheet;
-                                    self.slots.spreadsheet.set_visible(self.show_spreadsheet);
-                                    self.slots.spreadsheet_menubar.set_visible(self.show_spreadsheet);
-                                    let val = self.show_spreadsheet;
-                                    self.menu_mut(HEADER_IDX).set_item_checked(2, 7, val);
-                                    if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
-                                        self.focused_pane = get_next_visible_pane(
-                                            self.focused_pane,
-                                            self.show_network,
-                                            self.show_viewport,
-                                            self.show_parameters,
-                                            self.show_spreadsheet,
-                                            false,
-                                        );
-                                    }
-                                    self.rebuild_positions();
-                                    self.apply_layout();
-                                    self.sync_pane_focus();
-                                }
-                                "Close Pane" => {
-                                    let mut parent_name = "";
-                                    if self.current_path.len() >= 1 {
-                                        let root_idx = self.current_path[0];
-                                        if let Some(r_node) = self.fs_root.children.get(root_idx) {
-                                            parent_name = r_node.name.as_str();
-                                        }
-                                    }
-                                    match parent_name {
-                                        "Network" => {
-                                            self.show_network = false;
-                                            self.slots.content.set_visible(false);
-                                            self.slots.left_menubar.set_visible(false);
-                                            self.slots.breadcrumb.set_visible(false);
-                                            self.menu_mut(HEADER_IDX).set_item_checked(2, 4, false);
-                                            if self.focused_pane == LEFT_MENUBAR_IDX {
-                                                self.focused_pane = get_next_visible_pane(
-                                                    self.focused_pane,
-                                                    self.show_network,
-                                                    self.show_viewport,
-                                                    self.show_parameters,
-                                                    self.show_spreadsheet,
-                                                    false,
-                                                );
-                                            }
-                                            self.rebuild_positions();
-                                            self.apply_layout();
-                                            self.sync_pane_focus();
-                                        }
-                                        "Viewport" => {
-                                            self.show_viewport = false;
-                                            self.slots.viewport.set_visible(false);
-                                            self.slots.right_menubar.set_visible(false);
-                                            self.menu_mut(HEADER_IDX).set_item_checked(2, 5, false);
-                                            if self.focused_pane == RIGHT_MENUBAR_IDX {
-                                                self.focused_pane = get_next_visible_pane(
-                                                    self.focused_pane,
-                                                    self.show_network,
-                                                    self.show_viewport,
-                                                    self.show_parameters,
-                                                    self.show_spreadsheet,
-                                                    false,
-                                                );
-                                            }
-                                            self.rebuild_positions();
-                                            self.apply_layout();
-                                            self.sync_pane_focus();
-                                        }
-                                        "Parameters" => {
-                                            self.show_parameters = false;
-                                            self.slots.param.set_visible(false);
-                                            self.slots.param_menubar.set_visible(false);
-                                            self.menu_mut(HEADER_IDX).set_item_checked(2, 6, false);
-                                            if self.focused_pane == PARAM_MENUBAR_IDX {
-                                                self.focused_pane = get_next_visible_pane(
-                                                    self.focused_pane,
-                                                    self.show_network,
-                                                    self.show_viewport,
-                                                    self.show_parameters,
-                                                    self.show_spreadsheet,
-                                                    false,
-                                                );
-                                            }
-                                            self.rebuild_positions();
-                                            self.apply_layout();
-                                            self.sync_pane_focus();
-                                        }
-                                        "Spreadsheet" => {
-                                            self.show_spreadsheet = false;
-                                            self.slots.spreadsheet.set_visible(false);
-                                            self.slots.spreadsheet_menubar.set_visible(false);
-                                            self.menu_mut(HEADER_IDX).set_item_checked(2, 7, false);
-                                            if self.focused_pane == SPREADSHEET_MENUBAR_IDX {
-                                                self.focused_pane = get_next_visible_pane(
-                                                    self.focused_pane,
-                                                    self.show_network,
-                                                    self.show_viewport,
-                                                    self.show_parameters,
-                                                    self.show_spreadsheet,
-                                                    false,
-                                                );
-                                            }
-                                            self.rebuild_positions();
-                                            self.apply_layout();
-                                            self.sync_pane_focus();
-                                        }
-                                        _ => {}
-                                    }
-                                }
-                                _ => {}
-                            }
+                            self.execute_menu_action(&btn_name);
                         }
                     }
                 }
@@ -1765,6 +1523,260 @@ impl State {
         }
     }
 
+    /// One label-matched menu action: the button-param menu pane's items, drained
+    /// per triggered button by `sync_parameters_to_project`, and reachable directly
+    /// through the HTTP API's `menu_action` (the index-matched `menu_click` cannot
+    /// reach these). Returns false for an unrecognized label.
+    pub fn execute_menu_action(&mut self, label: &str) -> bool {
+        match label {
+            "Update Parameters" => {
+                if let Some(slot_idx) = self.graph().selected_node() {
+                    let dir = self.current_dir_mut();
+                    if let Some(child) = dir.children.get_mut(slot_idx) {
+                        if child.node_type == "opencl" {
+                            let code_val = child.params.iter()
+                                .find(|p| p.name == "Code")
+                                .map(|p| p.default.clone())
+                                .unwrap_or_default();
+                            let parsed_params = crate::geometry::parse_dynamic_params(&code_val);
+                            let mut new_params = Vec::new();
+                            for base_name in &["Input", "Code", "Update Parameters"] {
+                                if let Some(p) = child.params.iter().find(|p| p.name == *base_name) {
+                                    new_params.push(p.clone());
+                                }
+                            }
+                            for mut parsed in parsed_params {
+                                if let Some(existing) = child.params.iter().find(|p| p.name == parsed.name) {
+                                    parsed.default = existing.default.clone();
+                                }
+                                new_params.push(parsed);
+                            }
+                            child.params = new_params;
+                            let updated_disp = param_display(&child.params);
+                            self.param_mut().set_display_params(&updated_disp);
+                            self.rebuild_scene_geometry();
+                            self.sync_nodes();
+                        }
+                    }
+                }
+            }
+            "New Project" | "New" => {
+                self.new_project();
+            }
+            "Open" => {
+                self.open_file_chooser();
+            }
+            "Save" => {
+                let path_opt = self.loaded_project_path.clone();
+                if let Some(path) = path_opt {
+                    if let Err(e) = self.save_to_file(&path) {
+                        eprintln!("Failed to save project: {:?}", e);
+                        self.update_status_text(&format!("Failed to save: {:?}", e));
+                    } else {
+                        self.update_status_text(&format!("Project saved to {}", path.display()));
+                        self.add_recent_file(path);
+                    }
+                } else {
+                    self.save_file_chooser();
+                }
+            }
+            "Save As" => {
+                self.save_file_chooser();
+            }
+            "Exit" => {
+                self.exit_requested = true;
+            }
+            "Zoom In" => {
+                self.zoom(1.15, None);
+            }
+            "Zoom Out" => {
+                self.zoom(1.0 / 1.15, None);
+            }
+            "Reset Zoom" => {
+                self.grid_size_x = 150.0;
+                self.grid_size_y = 75.0;
+                self.skipped_col_w = 37.5;
+                self.skipped_row_h = 37.5;
+                self.sync_grid_settings();
+            }
+            "Detach Circular Window" | "Detach Pane" => {
+                self.execute_action(Action::DetachCircularWindow);
+            }
+            "Show Network Pane" => {
+                self.show_network = !self.show_network;
+                self.slots.content.set_visible(self.show_network);
+                self.slots.left_menubar.set_visible(self.show_network);
+                self.slots.breadcrumb.set_visible(self.show_network);
+                let val = self.show_network;
+                self.menu_mut(HEADER_IDX).set_item_checked(2, 4, val);
+                if !self.show_network && self.focused_pane == LEFT_MENUBAR_IDX {
+                    self.focused_pane = get_next_visible_pane(
+                        self.focused_pane,
+                        self.show_network,
+                        self.show_viewport,
+                        self.show_parameters,
+                        self.show_spreadsheet,
+                        false,
+                    );
+                }
+                self.rebuild_positions();
+                self.apply_layout();
+                self.sync_pane_focus();
+            }
+            "Show Viewport Pane" => {
+                self.show_viewport = !self.show_viewport;
+                self.slots.viewport.set_visible(self.show_viewport);
+                self.slots.right_menubar.set_visible(self.show_viewport);
+                let val = self.show_viewport;
+                self.menu_mut(HEADER_IDX).set_item_checked(2, 5, val);
+                if !self.show_viewport && self.focused_pane == RIGHT_MENUBAR_IDX {
+                    self.focused_pane = get_next_visible_pane(
+                        self.focused_pane,
+                        self.show_network,
+                        self.show_viewport,
+                        self.show_parameters,
+                        self.show_spreadsheet,
+                        false,
+                    );
+                }
+                self.rebuild_positions();
+                self.apply_layout();
+                self.sync_pane_focus();
+            }
+            "Show Parameters Pane" => {
+                self.show_parameters = !self.show_parameters;
+                self.slots.param.set_visible(self.show_parameters);
+                self.slots.param_menubar.set_visible(self.show_parameters);
+                let val = self.show_parameters;
+                self.menu_mut(HEADER_IDX).set_item_checked(2, 6, val);
+                if !self.show_parameters && self.focused_pane == PARAM_MENUBAR_IDX {
+                    self.focused_pane = get_next_visible_pane(
+                        self.focused_pane,
+                        self.show_network,
+                        self.show_viewport,
+                        self.show_parameters,
+                        self.show_spreadsheet,
+                        false,
+                    );
+                }
+                self.rebuild_positions();
+                self.apply_layout();
+                self.sync_pane_focus();
+            }
+            "Show Spreadsheet Pane" => {
+                self.show_spreadsheet = !self.show_spreadsheet;
+                self.slots.spreadsheet.set_visible(self.show_spreadsheet);
+                self.slots.spreadsheet_menubar.set_visible(self.show_spreadsheet);
+                let val = self.show_spreadsheet;
+                self.menu_mut(HEADER_IDX).set_item_checked(2, 7, val);
+                if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
+                    self.focused_pane = get_next_visible_pane(
+                        self.focused_pane,
+                        self.show_network,
+                        self.show_viewport,
+                        self.show_parameters,
+                        self.show_spreadsheet,
+                        false,
+                    );
+                }
+                self.rebuild_positions();
+                self.apply_layout();
+                self.sync_pane_focus();
+            }
+            "Close Pane" => {
+                let mut parent_name = "";
+                if self.current_path.len() >= 1 {
+                    let root_idx = self.current_path[0];
+                    if let Some(r_node) = self.fs_root.children.get(root_idx) {
+                        parent_name = r_node.name.as_str();
+                    }
+                }
+                match parent_name {
+                    "Network" => {
+                        self.show_network = false;
+                        self.slots.content.set_visible(false);
+                        self.slots.left_menubar.set_visible(false);
+                        self.slots.breadcrumb.set_visible(false);
+                        self.menu_mut(HEADER_IDX).set_item_checked(2, 4, false);
+                        if self.focused_pane == LEFT_MENUBAR_IDX {
+                            self.focused_pane = get_next_visible_pane(
+                                self.focused_pane,
+                                self.show_network,
+                                self.show_viewport,
+                                self.show_parameters,
+                                self.show_spreadsheet,
+                                false,
+                            );
+                        }
+                        self.rebuild_positions();
+                        self.apply_layout();
+                        self.sync_pane_focus();
+                    }
+                    "Viewport" => {
+                        self.show_viewport = false;
+                        self.slots.viewport.set_visible(false);
+                        self.slots.right_menubar.set_visible(false);
+                        self.menu_mut(HEADER_IDX).set_item_checked(2, 5, false);
+                        if self.focused_pane == RIGHT_MENUBAR_IDX {
+                            self.focused_pane = get_next_visible_pane(
+                                self.focused_pane,
+                                self.show_network,
+                                self.show_viewport,
+                                self.show_parameters,
+                                self.show_spreadsheet,
+                                false,
+                            );
+                        }
+                        self.rebuild_positions();
+                        self.apply_layout();
+                        self.sync_pane_focus();
+                    }
+                    "Parameters" => {
+                        self.show_parameters = false;
+                        self.slots.param.set_visible(false);
+                        self.slots.param_menubar.set_visible(false);
+                        self.menu_mut(HEADER_IDX).set_item_checked(2, 6, false);
+                        if self.focused_pane == PARAM_MENUBAR_IDX {
+                            self.focused_pane = get_next_visible_pane(
+                                self.focused_pane,
+                                self.show_network,
+                                self.show_viewport,
+                                self.show_parameters,
+                                self.show_spreadsheet,
+                                false,
+                            );
+                        }
+                        self.rebuild_positions();
+                        self.apply_layout();
+                        self.sync_pane_focus();
+                    }
+                    "Spreadsheet" => {
+                        self.show_spreadsheet = false;
+                        self.slots.spreadsheet.set_visible(false);
+                        self.slots.spreadsheet_menubar.set_visible(false);
+                        self.menu_mut(HEADER_IDX).set_item_checked(2, 7, false);
+                        if self.focused_pane == SPREADSHEET_MENUBAR_IDX {
+                            self.focused_pane = get_next_visible_pane(
+                                self.focused_pane,
+                                self.show_network,
+                                self.show_viewport,
+                                self.show_parameters,
+                                self.show_spreadsheet,
+                                false,
+                            );
+                        }
+                        self.rebuild_positions();
+                        self.apply_layout();
+                        self.sync_pane_focus();
+                    }
+                    _ => {}
+                }
+            }
+            _ => return false,
+        }
+        true
+    }
+
     pub fn sync_parameters_pane(&mut self) {
         let params = if !self.is_detached_network {
             if let Some(slot_idx) = self.graph().selected_node() {
diff --git a/src/window.rs b/src/window.rs
index d4627e8..cce70a4 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1574,6 +1574,17 @@ impl AppState {
                                 Err(e) => Err(e),
                             }
                         }
+                        HttpAction::MenuAction { label } => {
+                            if state.execute_menu_action(&label) {
+                                // The arms relayout themselves but render() draws the last
+                                // uploaded buffer (same ritual as ToggleCircularPane).
+                                state.upload_vertices();
+                                needs_redraw = true;
+                                Ok(format!("Menu action executed: {}", label.replace(['"', '\\'], "'")))
+                            } else {
+                                Err(format!("unknown menu action label: {}", label.replace(['"', '\\'], "'")))
+                            }
+                        }
                         HttpAction::MenuClosed { widget_idx, menu_idx } => {
                             if state.active_menu_cloud_idx == Some((widget_idx, menu_idx)) {
                                 state.active_menu_cloud_pid = None;