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

commit3cae63a1fd19c1ebbc9dc646a50434e92d2abf3d
parentc219d2e11d
authorLucas Galante <[email protected]>
date2026-08-25 14:06
refactor: plate corner delegates to cce_ui::widget::plate_dock (7c-1)

The affordance protocol this file pioneered is toolkit-owned now
(cce-ui@f9ef0b3): constants re-exported; rect placement (stub exemption
included), hit test, the click-vs-drag threshold, and the standard menu
rows all delegate. Designer policy stays here: PLATE_SLOTS membership,
the circular pane's arc placement, spreadsheet span rows, dock regions,
layout application, and the detach process model. Verified live: corner
menus, the collapse/expand cycle, and dock-drag arming behave
identically.

 src/app.rs          |  6 ++--
 src/plate_corner.rs | 87 +++++++++++++++++++----------------------------------
 2 files changed, 35 insertions(+), 58 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index f971204..9cd1532 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4542,8 +4542,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 // An armed corner-dot press becomes a layout drag once it
                 // moves; stubbed (collapsed/detached) panes stay click-only.
                 if let Some((idx, px, py)) = self.corner_press {
-                    let moved = (self.cursor_x - px).abs().max((self.cursor_y - py).abs());
-                    if moved > 4.0 {
+                    if cce_ui::widget::plate_dock::press_becomes_drag(
+                        (px, py),
+                        (self.cursor_x, self.cursor_y),
+                    ) {
                         self.corner_press = None;
                         if !self.pane_is_stubbed(idx) && idx != PLAYBAR_IDX {
                             self.app_drag = Some(AppDrag::DockDrag { idx });
diff --git a/src/plate_corner.rs b/src/plate_corner.rs
index 7ad5fe3..6af92dc 100644
--- a/src/plate_corner.rs
+++ b/src/plate_corner.rs
@@ -16,15 +16,11 @@ use crate::app::State;
 use crate::slots::{NETWORK_PANEL_IDX, PARAM_IDX, PLAYBAR_IDX, SPREADSHEET_IDX, WIDGET_COUNT};
 
 /// Radius of the control itself.
-pub const CORNER_R: f32 = 8.0;
-
-/// Centre inset from the plate's top-right corner, on both axes. Clears the
-/// plate's own corner arc at the radii the DE ships.
-pub const CORNER_INSET: f32 = 14.0;
-
-/// A plate needs at least this much room before it earns a corner control —
-/// below it the trigger would cover the pane it belongs to.
-const MIN_PLATE_SPAN: f32 = 3.0 * CORNER_INSET;
+// The affordance's geometry and protocol are toolkit-owned since cce-ui RFC
+// Phase 7c generalized this file's machinery (`cce_ui::widget::plate_dock`);
+// the constants re-export so the designer's draw/hit code keeps its names.
+pub use cce_ui::widget::plate_dock::{CORNER_INSET, CORNER_R, MIN_PLATE_SPAN};
+use cce_ui::widget::plate_dock::{self, PlateDockAction, PlateDockState};
 
 /// The plates that carry a corner control. The viewport is deliberately absent:
 /// its "plate" is the window-spanning lip, so a top-right control would sit on
@@ -71,21 +67,9 @@ impl State {
             return Some((c.x + d, c.y - d));
         }
 
-        let (x, y, pw, ph) = w.rect();
-        if pw < MIN_PLATE_SPAN {
-            return None;
-        }
-        if self.pane_is_stubbed(idx) {
-            // A stub is BUILT to carry the control, and is shorter than the
-            // minimum span a full pane must clear — applying that guard here
-            // deleted the only control that can bring the pane back, whether it
-            // was collapsed or detached.
-            return Some((x + pw - CORNER_INSET, y + ph / 2.0));
-        }
-        if ph < MIN_PLATE_SPAN {
-            return None;
-        }
-        Some((x + pw - CORNER_INSET, y + CORNER_INSET))
+        // Rect-based placement is toolkit-owned (stub exemption included);
+        // only the circular-pane arc above stays designer policy.
+        plate_dock::corner_center(w.rect(), self.pane_is_stubbed(idx))
     }
 
     /// The plate whose corner control is under `(px, py)`, if any. Searched in
@@ -93,10 +77,8 @@ impl State {
     /// the user sees on top.
     pub fn plate_corner_at(&self, px: f32, py: f32) -> Option<usize> {
         PLATE_SLOTS.iter().rev().copied().find(|&idx| {
-            self.plate_corner_center(idx).is_some_and(|(cx, cy)| {
-                let (dx, dy) = (px - cx, py - cy);
-                dx * dx + dy * dy <= CORNER_R * CORNER_R
-            })
+            self.plate_corner_center(idx)
+                .is_some_and(|c| plate_dock::corner_hit(c, px, py))
         })
     }
 }
@@ -119,33 +101,32 @@ impl State {
     pub fn open_plate_menu(&mut self, idx: usize) {
         let Some((cx, cy)) = self.plate_corner_center(idx) else { return };
 
+        // The standard rows come from the toolkit protocol (Reattach-only for
+        // a detached pane's stub, Collapse/Expand, Detach when allowed); the
+        // designer appends its app rows after.
+        let state = PlateDockState {
+            collapsed: self.collapsed_panes[idx],
+            detached: self.pane_is_detached(idx),
+        };
         let mut options: Vec<String> = Vec::new();
         let mut actions: Vec<PlateMenuAction> = Vec::new();
-
-        // A detached pane lives in another window: collapsing the stub it left
-        // behind would mean nothing, so the only thing to offer is taking it back.
-        if self.pane_is_detached(idx) {
+        for (label, action) in plate_dock::standard_menu(state, self.plate_can_detach(idx)) {
+            options.push(label);
+            actions.push(match action {
+                PlateDockAction::Collapse => PlateMenuAction::Collapse,
+                PlateDockAction::Expand => PlateMenuAction::Expand,
+                PlateDockAction::Detach => PlateMenuAction::Detach,
+                PlateDockAction::Reattach => PlateMenuAction::Reattach,
+            });
+        }
+        if state.detached {
             let target = self.slots.get_dyn(idx).base().id();
-            cce_ui::widget::context_menu::show(
-                cx - CORNER_R,
-                cy + CORNER_R,
-                vec!["Reattach".to_string()],
-                0,
-                target,
-            );
+            cce_ui::widget::context_menu::show(cx - CORNER_R, cy + CORNER_R, options, 0, target);
             self.plate_menu_slot = Some(idx);
-            self.plate_menu_actions = vec![PlateMenuAction::Reattach];
+            self.plate_menu_actions = actions;
             return;
         }
 
-        if self.collapsed_panes[idx] {
-            options.push("Expand".to_string());
-            actions.push(PlateMenuAction::Expand);
-        } else {
-            options.push("Collapse".to_string());
-            actions.push(PlateMenuAction::Collapse);
-        }
-
         if idx == SPREADSHEET_IDX && !self.collapsed_panes[idx] {
             // Layout spans: full-width is the playbar treatment; the neighbors'
             // bottoms rise to make room via the tuck interlock.
@@ -159,11 +140,6 @@ impl State {
             }
         }
 
-        if self.plate_can_detach(idx) {
-            options.push("Detach".to_string());
-            actions.push(PlateMenuAction::Detach);
-        }
-
         let target = self.slots.get_dyn(idx).base().id();
         cce_ui::widget::context_menu::show(cx - CORNER_R, cy + CORNER_R, options, 0, target);
         self.plate_menu_slot = Some(idx);
@@ -283,9 +259,8 @@ impl State {
     }
 }
 
-/// Height of a collapsed plate: its title stub. Deep enough for the title text
-/// and the corner control that restores it, and no deeper.
-pub const STUB_H: f32 = 26.0;
+/// Height of a collapsed plate: its title stub (toolkit-owned since 7c).
+pub use cce_ui::widget::plate_dock::STUB_H;
 
 impl State {
     /// Rewrite the collapsed plates' rects down to their stubs.