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

commitd253d0cdeed1137549c9cb039096af7edaeb499e
parent7c1a5f176c
authorLucas Galante <[email protected]>
date2026-09-01 10:32
feat: Add Tab is a page, not a row per candidate

The corner menu's management group offers ONE 'Add Tab' row; clicking
it swaps the menu in place for the Add Tab page — a dimmed header over
the list of addable panes (docked elsewhere, not already here, not
detached). Same anchor, same click contract: a second page of the one
menu, not a second menu. Candidate selection dispatches the same
AddTab action the inline rows used to.

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

 src/plate_corner.rs | 70 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 52 insertions(+), 18 deletions(-)

diff --git a/src/plate_corner.rs b/src/plate_corner.rs
index 1987248..fccc53e 100644
--- a/src/plate_corner.rs
+++ b/src/plate_corner.rs
@@ -55,6 +55,9 @@ pub enum PlateMenuAction {
     ShowTab(usize),
     /// Pull the named pane out of its dock and tab it into this one, active.
     AddTab(usize),
+    /// Swap the menu for the Add Tab page — the list of panes that can be
+    /// pulled in ([`State::open_plate_add_tab_menu`]).
+    AddTabMenu,
     /// Move this pane out of its shared dock into the first empty one.
     SplitTab,
     /// Remove a closable pane (the second network editor) from the docks.
@@ -189,31 +192,26 @@ impl State {
                 actions.push(PlateMenuAction::ShowTab(t));
             }
             let mut managed = false;
-            for other in TAB_CANDIDATES {
-                if other != idx
-                    && !self.dock_tabs[d as usize].contains(&other)
-                    && !self.pane_is_detached(other)
-                {
-                    if !managed {
-                        separate(&mut options, &mut actions);
-                        managed = true;
-                    }
-                    options.push(format!("Add Tab: {}", plate_title(other)));
-                    actions.push(PlateMenuAction::AddTab(other));
-                }
-            }
-            if self.dock_tabs[d as usize].len() > 1 {
+            let mut manage_row = |options: &mut Vec<String>, actions: &mut Vec<PlateMenuAction>| {
                 if !managed {
-                    separate(&mut options, &mut actions);
+                    separate(options, actions);
                     managed = true;
                 }
+            };
+            // ONE "Add Tab" row: clicking it swaps the menu for the page of
+            // addable panes, instead of one row per candidate here.
+            if !self.plate_add_tab_candidates(idx, d).is_empty() {
+                manage_row(&mut options, &mut actions);
+                options.push("Add Tab".to_string());
+                actions.push(PlateMenuAction::AddTabMenu);
+            }
+            if self.dock_tabs[d as usize].len() > 1 {
+                manage_row(&mut options, &mut actions);
                 options.push("Move To Own Plate".to_string());
                 actions.push(PlateMenuAction::SplitTab);
             }
             if idx == NETWORK_PANEL2_IDX {
-                if !managed {
-                    separate(&mut options, &mut actions);
-                }
+                manage_row(&mut options, &mut actions);
                 options.push("Close Tab".to_string());
                 actions.push(PlateMenuAction::CloseTab);
             }
@@ -225,6 +223,41 @@ impl State {
         self.plate_menu_actions = actions;
     }
 
+    /// The panes a plate's Add Tab page can offer: docked (or dockable)
+    /// elsewhere, not already in this dock's list, not detached.
+    fn plate_add_tab_candidates(&self, idx: usize, d: crate::app::Dock) -> Vec<usize> {
+        TAB_CANDIDATES
+            .into_iter()
+            .filter(|&other| {
+                other != idx
+                    && !self.dock_tabs[d as usize].contains(&other)
+                    && !self.pane_is_detached(other)
+            })
+            .collect()
+    }
+
+    /// The Add Tab page: swaps the corner menu in place for the list of
+    /// addable panes, under a dimmed header row. Same anchor, same click
+    /// contract — a second PAGE of the one menu, not a second menu.
+    pub fn open_plate_add_tab_menu(&mut self, idx: usize) {
+        let Some((cx, cy)) = self.plate_corner_center(idx) else { return };
+        let Some(d) = self.dock_of_pane(idx) else { return };
+        let candidates = self.plate_add_tab_candidates(idx, d);
+        if candidates.is_empty() {
+            return;
+        }
+        let mut options = vec!["Add Tab".to_string()];
+        let mut actions = vec![PlateMenuAction::Separator];
+        for other in candidates {
+            options.push(plate_title(other).to_string());
+            actions.push(PlateMenuAction::AddTab(other));
+        }
+        let target = self.slots.get_dyn(idx).base().id();
+        cce_ui::widget::context_menu::show(cx - CORNER_R, cy + CORNER_R, options, 1, target);
+        self.plate_menu_slot = Some(idx);
+        self.plate_menu_actions = actions;
+    }
+
     pub fn plate_menu_open(&self) -> bool {
         cce_ui::widget::context_menu::is_visible() && self.plate_menu_slot.is_some()
     }
@@ -273,6 +306,7 @@ impl State {
                     self.add_dock_tab(d, o);
                 }
             }
+            PlateMenuAction::AddTabMenu => self.open_plate_add_tab_menu(idx),
             PlateMenuAction::SplitTab => self.split_dock_tab(idx),
             PlateMenuAction::CloseTab => self.close_dock_tab(idx),
             PlateMenuAction::Separator => {}