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

commit83bfb715a5800501115c44b1fba0a217315e453e
parent27323a37cb
authorLucas Galante <[email protected]>
date2026-07-16 18:11
feat: param scrollbar depth, plate consolidation, MCP select tool

- render.rs straddles the param scrollbar around the pane plate: the idle
  scrollbar sinks behind it, the active one rides on top.
- Drops the redundant PARAM_PLATE_IDX/param_plate backing plate — the
  pane widget is now its own single plate. Renumbers the widget-slot
  indices; WIDGET_COUNT 17 -> 16.
- Adds an MCP `select` tool (McpAction::Select { slot }) that selects a
  node and populates the parameter pane, the same effect as clicking it.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/api.rs    |  9 +++++++++
 src/app.rs    | 43 +++++++++++++++++--------------------------
 src/render.rs | 38 ++++++++++++++++++++++++++++++++++++--
 src/window.rs | 12 ++++++++++++
 4 files changed, 74 insertions(+), 28 deletions(-)

diff --git a/src/api.rs b/src/api.rs
index c574359..15cd9cf 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -49,6 +49,15 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
                 "required": ["slot"],
             }),
         ),
+        tool(
+            "select",
+            "Select the node at the given slot in the current network level (like clicking it); its parameters populate the parameter pane.",
+            json!({
+                "type": "object",
+                "properties": { "slot": slot("Child index in the current network level") },
+                "required": ["slot"],
+            }),
+        ),
         tool(
             "set_param",
             "Set a parameter on the node at the given slot. All values are strings (e.g. \"1.5\", \"0.2,0.4,1\").",
diff --git a/src/app.rs b/src/app.rs
index e7762d8..44600a3 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -75,20 +75,19 @@ pub const CONTENT_IDX: usize = 1;
 pub const SPLITTER1_IDX: usize = 2;
 pub const VIEWPORT_IDX: usize = 3;
 pub const SPLITTER2_IDX: usize = 4;
-pub const PARAM_PLATE_IDX: usize = 5;
-pub const PARAM_IDX: usize = 6;
-pub const CANVAS_IDX: usize = 7;
-pub const LEFT_MENUBAR_IDX: usize = 8;
-pub const RIGHT_MENUBAR_IDX: usize = 9;
-pub const PARAM_MENUBAR_IDX: usize = 10;
-pub const STATUS_IDX: usize = 11;
-pub const BREADCRUMB_IDX: usize = 12;
-pub const NODE_PALETTE_IDX: usize = 13;
-pub const SPREADSHEET_IDX: usize = 14;
-pub const SPREADSHEET_MENUBAR_IDX: usize = 15;
-pub const NETWORK_PANEL_IDX: usize = 16;
-
-pub const WIDGET_COUNT: usize = 17;
+pub const PARAM_IDX: usize = 5;
+pub const CANVAS_IDX: usize = 6;
+pub const LEFT_MENUBAR_IDX: usize = 7;
+pub const RIGHT_MENUBAR_IDX: usize = 8;
+pub const PARAM_MENUBAR_IDX: usize = 9;
+pub const STATUS_IDX: usize = 10;
+pub const BREADCRUMB_IDX: usize = 11;
+pub const NODE_PALETTE_IDX: usize = 12;
+pub const SPREADSHEET_IDX: usize = 13;
+pub const SPREADSHEET_MENUBAR_IDX: usize = 14;
+pub const NETWORK_PANEL_IDX: usize = 15;
+
+pub const WIDGET_COUNT: usize = 16;
 
 /// The roster, concretely typed (Phase 6bb): every slot's type is statically known — the
 /// old `Vec<Box<dyn WidgetHost>>` erased that and pinned `WidgetHost`'s full surface through the
@@ -102,7 +101,6 @@ pub struct WidgetSlots {
     pub splitter1: Adapted<Splitter>,
     pub viewport: Adapted<Viewport3D>,
     pub splitter2: Adapted<Splitter>,
-    pub param_plate: Adapted<PassivePlate>,
     pub param: Adapted<ParametersBg>,
     pub canvas: Adapted<Canvas>,
     pub left_menubar: Adapted<MenuBar>,
@@ -128,7 +126,6 @@ impl WidgetSlots {
             SPLITTER1_IDX => self.splitter1.draggable(),
             VIEWPORT_IDX => self.viewport.draggable(),
             SPLITTER2_IDX => self.splitter2.draggable(),
-            PARAM_PLATE_IDX => self.param_plate.draggable(),
             PARAM_IDX => self.param.draggable(),
             CANVAS_IDX => self.canvas.draggable(),
             LEFT_MENUBAR_IDX => self.left_menubar.draggable(),
@@ -151,7 +148,6 @@ impl WidgetSlots {
             SPLITTER1_IDX => self.splitter1.is_dragging(),
             VIEWPORT_IDX => self.viewport.is_dragging(),
             SPLITTER2_IDX => self.splitter2.is_dragging(),
-            PARAM_PLATE_IDX => self.param_plate.is_dragging(),
             PARAM_IDX => self.param.is_dragging(),
             CANVAS_IDX => self.canvas.is_dragging(),
             LEFT_MENUBAR_IDX => self.left_menubar.is_dragging(),
@@ -174,7 +170,6 @@ impl WidgetSlots {
             SPLITTER1_IDX => &self.splitter1,
             VIEWPORT_IDX => &self.viewport,
             SPLITTER2_IDX => &self.splitter2,
-            PARAM_PLATE_IDX => &self.param_plate,
             PARAM_IDX => &self.param,
             CANVAS_IDX => &self.canvas,
             LEFT_MENUBAR_IDX => &self.left_menubar,
@@ -197,7 +192,6 @@ impl WidgetSlots {
             SPLITTER1_IDX => &mut self.splitter1,
             VIEWPORT_IDX => &mut self.viewport,
             SPLITTER2_IDX => &mut self.splitter2,
-            PARAM_PLATE_IDX => &mut self.param_plate,
             PARAM_IDX => &mut self.param,
             CANVAS_IDX => &mut self.canvas,
             LEFT_MENUBAR_IDX => &mut self.left_menubar,
@@ -215,7 +209,7 @@ impl WidgetSlots {
 
     /// Per-slot dyn view in index order (the serialize path's input).
     pub fn dyn_refs(&self) -> [&dyn WidgetHost; WIDGET_COUNT] {
-        [&self.header, &self.content, &self.splitter1, &self.viewport, &self.splitter2, &self.param_plate, &self.param, &self.canvas, &self.left_menubar, &self.right_menubar, &self.param_menubar, &self.status, &self.breadcrumb, &self.node_palette, &self.spreadsheet, &self.spreadsheet_menubar, &self.network_panel]
+        [&self.header, &self.content, &self.splitter1, &self.viewport, &self.splitter2, &self.param, &self.canvas, &self.left_menubar, &self.right_menubar, &self.param_menubar, &self.status, &self.breadcrumb, &self.node_palette, &self.spreadsheet, &self.spreadsheet_menubar, &self.network_panel]
     }
 }
 
@@ -321,6 +315,7 @@ pub struct Project {
 pub enum McpAction {
     Up,
     Enter { slot: usize },
+    Select { slot: usize },
     SetParam { slot: usize, name: String, value: String },
     ResetCamera,
     Load { path: String },
@@ -2429,7 +2424,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             splitter1: Splitter::new(SPLITTER_W),
             viewport: Viewport3D::new(),
             splitter2: Splitter::new(SPLITTER_W),
-            param_plate: PassivePlate::new(colors::PARAM_BG, true),
             param: ParametersBg::new(),
             canvas: Canvas::new(),
             left_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0),
@@ -3255,9 +3249,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 self.slots.get_dyn_mut(idx).set_visible(false);
             }
         }
-        self.positions[PARAM_PLATE_IDX] = self.positions[PARAM_IDX];
-        let p_visible = self.slots.param.visible();
-        self.slots.param_plate.set_visible(p_visible);
     }
 
 
@@ -4114,7 +4105,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         if click_target.is_none() {
                             let mut hit_order: Vec<usize> = (0..WIDGET_COUNT).collect();
                             hit_order.sort_by_key(|&i| {
-                                let z = if i == NETWORK_PANEL_IDX || i == PARAM_PLATE_IDX {
+                                let z = if i == NETWORK_PANEL_IDX {
                                     -5
                                 } else if i == VIEWPORT_IDX {
                                     // Below PARAM_IDX: the params pane floats over the viewport,
@@ -4139,7 +4130,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                 new_pane = Some(LEFT_MENUBAR_IDX);
                             } else if i == RIGHT_MENUBAR_IDX || i == VIEWPORT_IDX {
                                 new_pane = Some(RIGHT_MENUBAR_IDX);
-                            } else if i == PARAM_MENUBAR_IDX || i == PARAM_IDX || i == PARAM_PLATE_IDX {
+                            } else if i == PARAM_MENUBAR_IDX || i == PARAM_IDX {
                                 new_pane = Some(PARAM_MENUBAR_IDX);
                             } else if i == SPREADSHEET_MENUBAR_IDX || i == SPREADSHEET_IDX {
                                 new_pane = Some(SPREADSHEET_MENUBAR_IDX);
diff --git a/src/render.rs b/src/render.rs
index 3f73b16..a51c023 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -4,7 +4,7 @@ use cce_ui::widget::WidgetHost;
 
 use crate::app::{
     State, FsNode, WIDGET_COUNT,
-    CONTENT_IDX, VIEWPORT_IDX, PARAM_IDX, PARAM_PLATE_IDX,
+    CONTENT_IDX, VIEWPORT_IDX, PARAM_IDX,
     BREADCRUMB_IDX, HEADER_IDX, RIGHT_MENUBAR_IDX,
     SPREADSHEET_MENUBAR_IDX, SPREADSHEET_IDX,
     LEFT_MENUBAR_IDX, PARAM_MENUBAR_IDX, NETWORK_PANEL_IDX,
@@ -62,7 +62,7 @@ impl State {
 
         let mut draw_order: Vec<usize> = (0..WIDGET_COUNT).collect();
         draw_order.sort_by_key(|&i| {
-            let base_key = if i == VIEWPORT_IDX || i == PARAM_PLATE_IDX || i == NETWORK_PANEL_IDX {
+            let base_key = if i == VIEWPORT_IDX || i == NETWORK_PANEL_IDX {
                 -5
             } else if i == CONTENT_IDX || i == PARAM_IDX {
                 -4
@@ -183,6 +183,33 @@ impl State {
                 }
             }
         } else {
+            // The params scrollbar sinks behind the pane plate when idle and rises above the
+            // pane content when active (dragged / recently scrolled / hovered). It straddles
+            // the plate here rather than riding the pane's `extra_quads`, so its depth can
+            // change without touching the rest of the pane chrome.
+            let param_scrollbar = if idx == PARAM_IDX {
+                let pb = self
+                    .slots
+                    .param
+                    .as_any()
+                    .downcast_ref::<cce_ui::widget::ParametersBg>()
+                    .expect("PARAM_IDX must be a ParametersBg");
+                if pb.scrollbar_visible() {
+                    Some((pb.scrollbar_quads(), pb.scrollbar_active()))
+                } else {
+                    None
+                }
+            } else {
+                None
+            };
+
+            // Idle: draw the scrollbar first so the translucent pane plate settles over it.
+            if let Some((quads, false)) = &param_scrollbar {
+                for &(qx, qy, qw, qh, qc) in quads {
+                    pc.quad(rect(qx, qy, qw, qh), qc);
+                }
+            }
+
             append_widget_plate(w, pc);
 
             // The params pane serves its chrome through the legacy plain-quad view,
@@ -211,6 +238,13 @@ impl State {
             for (cx, cy, cr, cc) in w.extra_circles() {
                 pc.circle(cx, cy, cr, cc);
             }
+
+            // Active: draw the scrollbar last so it rides above the pane content and plate.
+            if let Some((quads, true)) = &param_scrollbar {
+                for &(qx, qy, qw, qh, qc) in quads {
+                    pc.quad(rect(qx, qy, qw, qh), qc);
+                }
+            }
         }
 
         // Child elements, then the widget's popover on top of them.
diff --git a/src/window.rs b/src/window.rs
index 8a60326..4345606 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -667,6 +667,18 @@ impl State {
                     Err("Not a valid subnet".to_string())
                 }
             }
+            McpAction::Select { slot } => {
+                if slot < state.current_dir().children.len() {
+                    // Same effect as clicking the node: it becomes the selected node and
+                    // its params populate the parameter pane.
+                    state.graph_mut().set_selected_node(Some(slot));
+                    state.sync_parameters_pane();
+                    needs_redraw = true;
+                    Ok("Node selected".to_string())
+                } else {
+                    Err("Slot index out of bounds".to_string())
+                }
+            }
             McpAction::SetParam { slot, name, value } => {
                 let dir = state.current_dir_mut();
                 if let Some(child) = dir.children.get_mut(slot) {