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

commit98cb196ca63b978ac2c0c0432c50db88b7c9f95b
parent394cdbaff8
authorLucas Galante <[email protected]>
date2026-09-21 11:51
Command palette: Wireframe Color, a row that previews the colour and lands on Settings

A wireframe_color command (label "Wireframe Color", Viewport context,
unbound). Palette rows gain an optional swatch; this row carries the
live wire colour as a ringed tile ahead of its label — the sRGB hex
value converted to linear for the fill, as the colour control's own
preview does — so the palette shows what the setting is before it is
opened. Every other row has none.

Picking it closes the palette and reopens the dialog on its Settings
half (open_dialog_on_settings), where a new Wireframe section holds a
Wireframe Color row owned by the Render node's "Wire Color" — the
value's one home, which apply_settings_from_menubar_subnets reads back
on every parameter edit. The roster deliberately excludes the render
subnet as per-project look; the wire colour is now the one exception,
and the roster comment says so.

Tested: the row previews the colour, and picking it lands on Settings
with the rgba row present and correctly owned. Verified in a scale-2
shadow: "wire" ranks the row first, Enter opens Settings on the
Wireframe section.

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

 src/app.rs      |  1 +
 src/command.rs  |  1 +
 src/dialog.rs   | 46 +++++++++++++++++++++++++++++++++++++++++++---
 src/main.rs     | 44 +++++++++++++++++++++++++++++++++++++++++++-
 src/shortcut.rs |  1 +
 5 files changed, 89 insertions(+), 4 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 992be70..3be62d9 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5609,6 +5609,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.wireframe = val;
                 self.write_render_toggle("Show Wireframe", val);
             }
+            Action::WireframeColor => self.open_dialog_on_settings(),
             Action::ToggleSquareViewport => {
                 self.square_viewport = !self.square_viewport;
                 let val = self.square_viewport;
diff --git a/src/command.rs b/src/command.rs
index a1e8b17..3a9783a 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -176,6 +176,7 @@ pub const COMMANDS: &[Command] = &[
     Command { id: "toggle_origin", label: "Show Origin", context: Context::Viewport, run: Run::Key(Action::ToggleOrigin), default_chord: None },
     Command { id: "toggle_camera_pivot", label: "Show Camera Pivot", context: Context::Viewport, run: Run::Key(Action::ToggleCameraPivot), default_chord: None },
     Command { id: "toggle_wireframe", label: "Show Wireframe", context: Context::Viewport, run: Run::Key(Action::ToggleWireframe), default_chord: None },
+    Command { id: "wireframe_color", label: "Wireframe Color", context: Context::Viewport, run: Run::Key(Action::WireframeColor), default_chord: None },
     Command { id: "toggle_square_viewport", label: "Square Aspect", context: Context::Viewport, run: Run::Key(Action::ToggleSquareViewport), default_chord: Some("Ctrl+a") },
 
     // --- Parameters ---
diff --git a/src/dialog.rs b/src/dialog.rs
index 2186480..9d64dc6 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -75,6 +75,11 @@ pub struct Row {
     /// replacing it — which the `cce-cloud` palette could only approximate by
     /// padding the label out, since all it could send was one line of text.
     pub chord: String,
+    /// A colour the row previews, drawn as a swatch ahead of the label —
+    /// linear RGBA, as the paint path takes it. `None` for the ordinary row.
+    /// The Wireframe Color command carries the live wire colour here, so
+    /// the palette shows what the setting currently is before it is opened.
+    pub swatch: Option<[f32; 4]>,
 }
 
 /// The dialog's outer size. Fixed rather than proportional: it is a focused
@@ -88,6 +93,8 @@ const PAD: f32 = 12.0;
 const TAB_H: f32 = 30.0;
 const QUERY_H: f32 = 30.0;
 pub const ROW_H: f32 = 24.0;
+/// Side of a row's colour swatch, logical px.
+pub const SWATCH_SIDE: f32 = 14.0;
 /// Gap between the tab strip, the query line and the list.
 const GAP: f32 = 8.0;
 
@@ -522,9 +529,21 @@ impl Paint for Dialog {
             // label is cut by it rather than running under it.
             let label_right = r.x + r.width - 8.0 - if chord_w > 0.0 { chord_w + 12.0 } else { 0.0 };
             let label_color = if i == self.selected { [0xf4, 0xf4, 0xfa] } else { [0xcc, 0xcc, 0xd4] };
+            // The swatch: a small rounded tile ahead of the label, ringed
+            // faintly so a colour near the plate's own does not vanish into
+            // it. The label steps right by the tile.
+            let mut label_x = r.x + 8.0;
+            if let Some(sw) = row.swatch {
+                let side = SWATCH_SIDE;
+                let tile = Rect { x: label_x, y: r.y + (r.height - side) * 0.5, width: side, height: side };
+                let ring = Rect { x: tile.x - 1.0, y: tile.y - 1.0, width: side + 2.0, height: side + 2.0 };
+                ctx.rounded_rect(ring, 4.0, (true, true, true, true), [1.0, 1.0, 1.0, 0.22]);
+                ctx.rounded_rect(tile, 3.0, (true, true, true, true), sw);
+                label_x += side + 8.0;
+            }
             ctx.text_with(
-                fit(&row.label, label_right - (r.x + 8.0)),
-                r.x + 8.0,
+                fit(&row.label, label_right - label_x),
+                label_x,
                 ty,
                 font_size,
                 label_color,
@@ -684,11 +703,16 @@ impl Setting {
 /// the render subnet (per-project look), the pane-visibility toggles (the
 /// View menu and the plate corners already own those, and a settings dialog
 /// is a strange place to hide a pane from), and keybindings, which this DE
-/// edits as `input.kdl` on purpose.
+/// edits as `input.kdl` on purpose. The one exception is the wire colour:
+/// the palette's Wireframe Color command lands on it, so it is a row here,
+/// its owner still the Render node — per-project, unlike the rest of this
+/// table.
 pub const SETTINGS: &[Setting] = &[
     Setting::section("Viewport"),
     Setting::row("Background Color", Owner::Subnet("Main", "Background Color")),
     Setting::row("Square Aspect", Owner::Command("toggle_square_viewport")),
+    Setting::section("Wireframe"),
+    Setting::row("Wireframe Color", Owner::Subnet("Render", "Wire Color")),
     Setting::section("Grid"),
     Setting::row("Show Grid", Owner::Subnet("Guides", "Show Grid Guide")),
     Setting::row("Grid Color", Owner::Subnet("Guides", "Grid Color")),
@@ -789,6 +813,16 @@ impl State {
         });
     }
 
+    /// Open the tabbed dialog on its Settings half — what the Wireframe
+    /// Color command does, so a palette pick lands on the row that edits
+    /// the value rather than on the list it was picked from.
+    pub fn open_dialog_on_settings(&mut self) {
+        if !self.dialog_visible() || self.slots.dialog.mode != Mode::Tabbed {
+            self.open_dialog_in(Mode::Tabbed);
+        }
+        self.set_dialog_tab(Tab::Settings);
+    }
+
     pub fn close_dialog(&mut self) {
         if !self.dialog_visible() {
             return;
@@ -842,6 +876,11 @@ impl State {
                         .chord_for(c.id)
                         .map(|s| s.describe())
                         .unwrap_or_default(),
+                    // The wire colour is kept sRGB-encoded (it round-trips
+                    // through the Render node's hex); the swatch is a fill.
+                    swatch: (c.id == "wireframe_color").then(|| {
+                        cce_ui::color::to_linear([self.wire_color[0], self.wire_color[1], self.wire_color[2], 1.0])
+                    }),
                 })
                 .collect(),
             Mode::AddNode => {
@@ -863,6 +902,7 @@ impl State {
                         id: offered[i].to_string(),
                         label: offered[i].to_string(),
                         chord: String::new(),
+                        swatch: None,
                     })
                     .collect()
             }
diff --git a/src/main.rs b/src/main.rs
index 218a4eb..98b0ba0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3277,7 +3277,7 @@ mod tests {
         d.set_visible(true);
         WidgetHost::set_rect(&mut d, 0.0, 0.0, 520.0, 420.0);
         let rows: Vec<Row> = (0..60)
-            .map(|i| Row { id: format!("c{i}"), label: format!("Command {i}"), chord: String::new() })
+            .map(|i| Row { id: format!("c{i}"), label: format!("Command {i}"), chord: String::new(), swatch: None })
             .collect();
         d.set_rows(rows);
         d.set_page(10);
@@ -3985,6 +3985,48 @@ mod tests {
         assert_eq!(fuzzy_rank("S G", &items), fuzzy_rank("sg", &items));
     }
 
+    /// The Wireframe Color command is a palette row that PREVIEWS the colour
+    /// — the row carries the live wire colour as its swatch — and, picked,
+    /// lands on the Settings half's Wireframe Color row, whose owner is the
+    /// Render node's "Wire Color" (the value's one home). The palette row
+    /// shows the value; the settings row edits it.
+    #[test]
+    fn test_wireframe_color_row_previews_and_lands_on_settings() {
+        use crate::command::{by_id, Run};
+        use crate::dialog::Tab;
+        let cmd = by_id("wireframe_color").expect("no wireframe_color command");
+        assert_eq!(cmd.label, "Wireframe Color");
+        assert_eq!(cmd.run, Run::Key(crate::shortcut::Action::WireframeColor));
+
+        let mut state = State::new(false);
+        state.wire_color = [0.2, 0.6, 0.9, 0.5];
+        state.run_command("command_palette");
+        let row = state
+            .slots
+            .dialog
+            .rows
+            .iter()
+            .find(|r| r.id == "wireframe_color")
+            .expect("the palette lists Wireframe Color");
+        let sw = row.swatch.expect("the row carries a swatch");
+        let want = cce_ui::color::to_linear([0.2, 0.6, 0.9, 1.0]);
+        for k in 0..4 {
+            assert!((sw[k] - want[k]).abs() < 1e-6, "swatch channel {k}: {} vs {}", sw[k], want[k]);
+        }
+        assert!(state.slots.dialog.rows.iter().filter(|r| r.id != "wireframe_color").all(|r| r.swatch.is_none()));
+
+        // Picked from the list: the dialog closes, the command reopens it on
+        // Settings, and the Wireframe Color row is there as a colour control
+        // owned by the Render node.
+        state.take_dialog_pick("wireframe_color".to_string());
+        assert!(state.dialog_visible());
+        assert_eq!(state.dialog_tab(), Tab::Settings);
+        let shown = state.dialog_settings_shown.clone();
+        assert!(shown.iter().any(|(k, _, t)| k == "Wireframe Color" && t == "rgba"), "{shown:?}");
+        let s = crate::dialog::SETTINGS.iter().find(|s| s.label == "Wireframe Color").unwrap();
+        assert_eq!(s.owner, Some(crate::dialog::Owner::Subnet("Render", "Wire Color")));
+    }
+
     /// The wireframe toggle is a palette row that flips the live flag AND
     /// the Render node's "Show Wireframe" switch. The node matters: it is
     /// what `apply_settings_from_menubar_subnets` reads back on every
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 8a62f6a..9298145 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -11,6 +11,7 @@ pub enum Action {
     ToggleOrigin,
     ToggleCameraPivot,
     ToggleWireframe,
+    WireframeColor,
     ToggleCircularPane,
     DetachCircularWindow,
     Save,