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

commit49286cb193bf40eed7b3ce33156b0f432325d9c7
parentb0ea46cbc4
authorLucas Galante <[email protected]>
date2026-07-09 15:07
feat: both Plates DISSOLVED — cce-graph is embedded-base-free (Phase 6o)

The transparent dropdown-row Plate (a pure layout shim) is replaced by
direct placement: each dropdown sized by the same Element::intrinsic_size
the scene bridge consulted (widest option x configured dropdown height),
row at x=10/y=8 with 10px gaps — A/B'd to the solver's exact rects.

The draggable control panel Plate is replaced by app state (panel_x/y,
drag offset) plus prims replicating Plate's exact visual — config plate
color (else page-low with the drag tint), plate opacity, negative-alpha
blur flag, config border and corner radius (panel area A/B: AE=0) — and
the label as a standalone walked widget positioned by Plate's
centered-first-child rule. Drag is implemented properly app-side
(press-in-panel starts, bounds clamp to the graph area); NB the legacy
Plate::on_cursor_moved only forwarded drags to CHILDREN, so the old
panel's own drag likely never moved it — worth a manual drag check.

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

 src/main.rs | 204 ++++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 145 insertions(+), 59 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 224ff90..2ac105a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use wayland_client::QueueHandle;
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::widget::{Adapted, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Element, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Plate, Label};
+use cce_ui::widget::{Adapted, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Element, Graph, GraphNode, MenuBar, GraphController, WidgetId, Dropdown, Label};
 use image::GenericImageView;
 
 #[derive(Debug, Clone)]
@@ -54,7 +54,7 @@ struct GraphApp {
     dropdown_file: Adapted<Dropdown>,
     dropdown_edit: Adapted<Dropdown>,
     dropdown_view: Adapted<Dropdown>,
-    menu_dropdown_bar: Plate,
+
     graph: Adapted<Graph>,
     graph_id: WidgetId,
     needs_rebuild: bool,
@@ -73,7 +73,13 @@ struct GraphApp {
     drag_image_ox: f32,
     drag_image_oy: f32,
     selected_image_idx: Option<usize>,
-    control_panel: Plate,
+    // Dissolved control panel (was a draggable Plate): position + drag state live here;
+    // its plate is emitted as prims and the label is a standalone walked widget.
+    panel_x: f32,
+    panel_y: f32,
+    panel_dragging: bool,
+    panel_drag_ox: f32,
+    panel_drag_oy: f32,
     show_control_panel: bool,
     control_panel_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
 }
@@ -481,6 +487,58 @@ impl GraphApp {
         self.dropdown_view.options = get_view_options(self.show_grid, self.uniform_background, self.opacity, self.show_control_panel);
     }
 
+    /// The dissolved control panel's rect (fixed 210x160, app-tracked position).
+    fn panel_rect(&self) -> (f32, f32, f32, f32) {
+        (self.panel_x, self.panel_y, 210.0, 160.0)
+    }
+
+    fn panel_hit(&self, px: f32, py: f32) -> bool {
+        let (x, y, w, h) = self.panel_rect();
+        px >= x && px < x + w && py >= y && py < y + h
+    }
+
+    /// Replicates the dissolved Plate's centered-first-child placement for the label
+    /// (plate padding inset, centered, 50px nominal height on first placement).
+    fn position_panel_label(&mut self) {
+        let pad = cce_ui::layout::plate_padding();
+        let (px, py, pw, ph) = self.panel_rect();
+        let left_x = px + pad;
+        let available_w = (pw - 2.0 * pad).max(1.0);
+        let start_y = py + pad;
+        let available_h = (ph - 2.0 * pad).max(1.0);
+        let center_x = left_x + available_w / 2.0;
+        let center_y = start_y + available_h / 2.0;
+
+        let (_, _, lw, lh) = self.control_panel_label.rect();
+        let use_w = if lw > 0.0 { lw.min(available_w) } else { available_w };
+        let use_h = if lh > 0.0 { lh } else { 50.0 };
+        let cx = (center_x - use_w / 2.0).clamp(left_x, (left_x + available_w - use_w).max(left_x));
+        let cy = (center_y - use_h / 2.0).clamp(start_y, (start_y + available_h - use_h).max(start_y));
+        let cw = use_w.min(px + pw - pad - cx);
+        let ch = use_h.min(py + ph - pad - cy);
+        self.control_panel_label.set_rect(cx, cy, cw, ch);
+    }
+
+    /// The dissolved Plate's visual: config plate color (else page-low, with the drag tint),
+    /// plate opacity, negative-alpha blur flag, config border and corner radius.
+    fn panel_visual(&self) -> ([f32; 4], Option<([f32; 4], f32)>, f32) {
+        let mut c = if let Some(c) = cce_ui::colors::plate_color() {
+            c
+        } else if self.panel_dragging {
+            let b = cce_ui::colors::page_low_color();
+            [(b[0] + 0.10).min(1.0), (b[1] + 0.15).min(1.0), (b[2] + 0.12).min(1.0), b[3]]
+        } else {
+            cce_ui::colors::page_low_color()
+        };
+        c[3] *= cce_ui::layout::plate_opacity();
+        if cce_ui::colors::plate_blur() {
+            c[3] = -c[3].abs();
+        }
+        let border = cce_ui::colors::plate_border_color()
+            .map(|bc| (bc, cce_ui::colors::plate_border_thickness()));
+        (c, border, cce_ui::layout::plate_corner_radius())
+    }
+
     fn new_project(&mut self) {
         self.graph.set_nodes(&[]);
         self.loaded_images.clear();
@@ -770,37 +828,18 @@ impl Application for GraphApp {
         let graph_id = WidgetId(cce_ui::widget::NEXT_WIDGET_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
 
 
-        let mut control_panel = Plate::new(800.0, 50.0, 210.0, 160.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]);
 
-        // Transparent Plate that lays out the File/Edit/View dropdown row via the scene engine
-        // (Phase 2b). Pure layout container over the menu bar; the menu_bar draws the bar itself.
-        let menu_dropdown_bar = Plate::new(0.0, 0.0, 0.0, 0.0)
-            .with_color([0.0, 0.0, 0.0, 0.0])
-            .with_blur(false)
-            .with_draggable(false)
-            .with_engine_layout({
-                let mut s = cce_ui::scene::layout::Style::row()
-                    .gap(10.0)
-                    .main_align(cce_ui::scene::layout::MainAlign::Start)
-                    .cross_align(cce_ui::scene::layout::CrossAlign::Start);
-                s.padding = cce_ui::scene::layout::Edges { left: 10.0, right: 0.0, top: 8.0, bottom: 0.0 };
-                s
-            });
-
         let mut app = Self {
 
             menu_bar,
             dropdown_file,
             dropdown_edit,
             dropdown_view,
-            menu_dropdown_bar,
             graph,
             graph_id,
             needs_rebuild: true,
@@ -819,7 +858,11 @@ impl Application for GraphApp {
             drag_image_ox: 0.0,
             drag_image_oy: 0.0,
             selected_image_idx: None,
-            control_panel,
+            panel_x: 800.0,
+            panel_y: 50.0,
+            panel_dragging: false,
+            panel_drag_ox: 0.0,
+            panel_drag_oy: 0.0,
             show_control_panel,
             control_panel_label,
         };
@@ -974,7 +1017,6 @@ impl Application for GraphApp {
             }
             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;
@@ -1038,15 +1080,8 @@ impl Application for GraphApp {
                 self.ui_context.register_widget(self.dropdown_file.base().unwrap().id(), (*self_ptr).dropdown_file.as_ptr_mut());
                 self.ui_context.register_widget(self.dropdown_edit.base().unwrap().id(), (*self_ptr).dropdown_edit.as_ptr_mut());
                 self.ui_context.register_widget(self.dropdown_view.base().unwrap().id(), (*self_ptr).dropdown_view.as_ptr_mut());
-                self.ui_context.register_widget(self.menu_dropdown_bar.base().unwrap().id(), &mut (*self_ptr).menu_dropdown_bar as *mut Plate as *mut (dyn Element + 'static));
                 self.ui_context.register_widget(self.graph_id, (*self_ptr).graph.as_ptr_mut());
-                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(), (*self_ptr).control_panel_label.as_ptr_mut());
-
-                self.menu_dropdown_bar.add_child(self.dropdown_file.as_ptr_mut(), &mut self.ui_context);
-                self.menu_dropdown_bar.add_child(self.dropdown_edit.as_ptr_mut(), &mut self.ui_context);
-                self.menu_dropdown_bar.add_child(self.dropdown_view.as_ptr_mut(), &mut self.ui_context);
-                self.control_panel.add_child(self.control_panel_label.as_ptr_mut(), &mut self.ui_context);
             }
             self.widgets_registered = true;
         }
@@ -1125,26 +1160,34 @@ impl Application for GraphApp {
             // Layout MenuBar at the top
             self.menu_bar.set_rect(0.0, 0.0, size.width, 42.0);
             
-            // Phase 2b: lay out the File/Edit/View dropdown row via the scene layout engine; the
-            // dropdowns size to their labels (Dropdown::intrinsic_size) instead of fixed 70px.
-            let menu_bar_ptr: *mut (dyn cce_ui::widget::Element + 'static) =
-                &mut self.menu_dropdown_bar as *mut _;
-            cce_ui::scene::bridge::layout_subtree(
-                &self.ui_context,
-                menu_bar_ptr,
-                cce_ui::scene::layout::Rect { x: 0.0, y: 0.0, width: size.width, height: 42.0 },
-            );
-            
+            // The File/Edit/View dropdown row, laid out directly (the transparent layout
+            // Plate is DISSOLVED): a row at x=10/y=8 with 10px gaps, each dropdown sized
+            // to its label via measure (the same intrinsic sizes the scene solver used).
+            {
+                let mut x = 10.0;
+                let self_ptr = self as *mut Self;
+                let dds: [&mut cce_ui::widget::Adapted<Dropdown>; 3] = unsafe {
+                    [&mut (*self_ptr).dropdown_file, &mut (*self_ptr).dropdown_edit, &mut (*self_ptr).dropdown_view]
+                };
+                for dd in dds {
+                    // Same sizing entry the scene bridge used: the dropdown's intrinsic size
+                    // (widest option x configured dropdown height).
+                    let sz = cce_ui::widget::Element::intrinsic_size(&*dd)
+                        .unwrap_or(cce_ui::scene::layout::Size::new(70.0, 26.0));
+                    dd.set_rect(x, 8.0, sz.width, sz.height);
+                    x += sz.width + 10.0;
+                }
+            }
+
             // 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
+            // Initial control panel positioning (bounds are clamped at drag time)
             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.panel_x = (size.width - 230.0).max(10.0);
+                self.panel_y = 55.0; // Float below MenuBar
             }
-            self.control_panel.set_bounds(0.0, 42.0, size.width, size.height - 42.0);
+            self.position_panel_label();
             
             self.needs_rebuild = false;
 
@@ -1170,12 +1213,13 @@ impl Application for GraphApp {
         }
         {
             let self_ptr = self as *mut Self;
-            let tops: [*mut (dyn cce_ui::widget::Element + 'static); 4] = unsafe {
+            let tops: [*mut (dyn cce_ui::widget::Element + 'static); 5] = unsafe {
                 [
                     (*self_ptr).menu_bar.as_ptr_mut(),
-                    &mut (*self_ptr).menu_dropdown_bar as *mut Plate as *mut (dyn cce_ui::widget::Element + 'static),
+                    (*self_ptr).dropdown_file.as_ptr_mut(),
+                    (*self_ptr).dropdown_edit.as_ptr_mut(),
+                    (*self_ptr).dropdown_view.as_ptr_mut(),
                     (*self_ptr).graph.as_ptr_mut(),
-                    &mut (*self_ptr).control_panel as *mut Plate as *mut (dyn cce_ui::widget::Element + 'static),
                 ]
             };
             for top in tops {
@@ -1183,6 +1227,27 @@ impl Application for GraphApp {
             }
         }
 
+        // The dissolved control panel, on top: its plate as prims, then the label walked.
+        if self.show_control_panel {
+            use cce_ui::scene::layout::Rect;
+            let (px, py, pw, ph) = self.panel_rect();
+            let rect = Rect { x: px, y: py, width: pw, height: ph };
+            let (fill, border, radius) = self.panel_visual();
+            let radii = (radius, radius, radius, radius);
+            if let Some((bc, thickness)) = border {
+                pc.border(rect, radii, fill, bc, thickness);
+            } else if fill[3].abs() > 0.001 {
+                if radius > 0.1 {
+                    pc.rounded_rect(rect, radius, (true, true, true, true), fill);
+                } else {
+                    pc.quad(rect, fill);
+                }
+            }
+            let label_ptr: *mut (dyn cce_ui::widget::Element + 'static) =
+                unsafe { (*(self as *mut Self)).control_panel_label.as_ptr_mut() };
+            cce_ui::scene::painter::paint_root_into(&self.ui_context, label_ptr, &mut pc);
+        }
+
         // Draw foreground images in the graph grid (above nodes, fully opaque, preserving aspect ratio)
         let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
         let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
@@ -1276,12 +1341,22 @@ impl Application for GraphApp {
             if self.dropdown_view.on_cursor_moved(pos.x, pos.y, &mut self.ui_context) { changed = true; }
         } else {
             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; }
+            if self.show_control_panel {
+                if self.panel_dragging {
+                    // Dissolved Plate drag: move within the graph area's bounds.
+                    let w = self.width as f32;
+                    let h = self.height as f32;
+                    let nx = (pos.x - self.panel_drag_ox).clamp(0.0, (w - 210.0).max(0.0));
+                    let ny = (pos.y - self.panel_drag_oy).clamp(42.0, (42.0 + (h - 42.0) - 160.0).max(42.0));
+                    if (nx - self.panel_x).abs() > 0.01 || (ny - self.panel_y).abs() > 0.01 {
+                        self.panel_x = nx;
+                        self.panel_y = ny;
+                        self.position_panel_label();
+                        changed = true;
+                    }
+                    handled_by_panel = true;
+                } else if self.panel_hit(pos.x, pos.y) {
                     handled_by_panel = true;
-                } else {
-                    if self.control_panel.on_cursor_moved(-1000.0, -1000.0, &mut self.ui_context) { changed = true; }
                 }
             }
 
@@ -1411,13 +1486,24 @@ impl Application for GraphApp {
             }
         } else {
             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;
+            if self.show_control_panel && (self.panel_dragging || self.panel_hit(pos.x, pos.y)) {
+                if button == MouseButton::Left {
+                    match state {
+                        ElementState::Pressed => {
+                            self.panel_dragging = true;
+                            self.panel_drag_ox = pos.x - self.panel_x;
+                            self.panel_drag_oy = pos.y - self.panel_y;
+                            changed = true;
+                        }
+                        ElementState::Released => {
+                            if self.panel_dragging {
+                                self.panel_dragging = false;
+                                changed = true;
+                            }
+                        }
                     }
-                    handled_by_panel = true;
                 }
+                handled_by_panel = true;
             }
 
             if !handled_by_panel {