graphic design tool
git clone https://git.lucas.co/cce-designer.git
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,