structured data editor
git clone https://git.lucas.co/cce-data-editor.git
feat: (bevel)-typed keys get an inline profile preview that opens cce-bevel
Values annotated (bevel) — the "shoulder,base,bias" knob triples cce-bevel
persists (style.surface.relief.profile_knobs / edge_knobs) — now show a
BevelPreview swatch inline on the selected row: a miniature lit
cross-section of the profile curve. Clicking it opens the cce-bevel
material editor (or refocuses the one this session already spawned;
try_wait reaps an exited child). cce-bevel saves to config.kdl itself and
the existing disk-sync watch reloads the document here, so the preview
tracks the edit with no write-back path of its own.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 7 +++++--
src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 72 insertions(+), 5 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6fe628f..62453e6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -66,7 +66,7 @@ loads (best-effort parse) with an error in the status bar.
### Type-driven inline value editors
-Eight editor widgets exist permanently as fields (`selected_*_editor`); exactly one is
+Nine editor widgets exist permanently as fields (`selected_*_editor`); exactly one is
positioned inline over the selected tree row, the rest are parked off-screen at
`(-1000, -1000)` (the "hide" convention — there is no visibility flag). Which editor
appears is decided in two places that must stay in agreement: the layout block in
@@ -75,7 +75,10 @@ appears is decided in two places that must stay in agreement: the layout block i
- **KDL type annotations** in the document (read via
`cce_ui::config::get_kdl_type_annotation`): `menu:a,b,c` → `Dropdown`,
`button` / `button:<shell-cmd>` → `Button` (clicking spawns the command),
- `keybind` → `KeybindRecorder`, `f64:min-max` → clamps applied values.
+ `keybind` → `KeybindRecorder`, `f64:min-max` → clamps applied values,
+ `bevel` → `BevelPreview` (a mini relief cross-section of the
+ "shoulder,base,bias" value; clicking opens `cce-bevel`, whose Save is picked
+ up by the disk-sync watch).
- **Key-name heuristics**: `font` / `*_font` / `*.font` → `FontSelector`; keybind-ish
names (`key`, `shortcut`, `brightness_up`, …) → `KeybindRecorder`.
- **Value shape**: `#`-prefixed string → `ColorSelector`, bool → `Checkbox`,
diff --git a/src/main.rs b/src/main.rs
index 3b4088f..aa74c69 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -366,6 +366,11 @@ struct DataEditorApp {
selected_keybind_editor: cce_ui::widget::Adapted<KeybindRecorder>,
selected_bool_editor: cce_ui::widget::Adapted<Checkbox>,
selected_button_editor: cce_ui::widget::Adapted<cce_ui::widget::Button>,
+ selected_bevel_editor: cce_ui::widget::Adapted<cce_ui::widget::BevelPreview>,
+ /// A cce-bevel child spawned from the (bevel) preview: kept so a second
+ /// click refocuses it (try_wait reaps an exited one) instead of piling
+ /// up editors.
+ bevel_child: Option<std::process::Child>,
// Right Panel Raw Json
raw_json_editor: cce_ui::widget::Adapted<TextBox>,
@@ -605,6 +610,29 @@ impl DataEditorApp {
self.sync_preview_selection();
}
+ /// The (bevel) preview's click action: open the cce-bevel material
+ /// editor, or refocus the one this session already spawned. cce-bevel
+ /// saves to config.kdl itself; the disk-sync watch reloads the document
+ /// here when it does.
+ fn open_bevel_editor(&mut self) {
+ let home = std::env::var("HOME").unwrap_or_default();
+ if let Some(child) = self.bevel_child.as_mut() {
+ if matches!(child.try_wait(), Ok(None)) {
+ let ccectl = format!("{home}/.local/bin/ccectl");
+ let ccectl = if std::path::Path::new(&ccectl).exists() { ccectl } else { "ccectl".to_string() };
+ let _ = std::process::Command::new(ccectl).args(["focus-window", "cce-bevel"]).spawn();
+ return;
+ }
+ self.bevel_child = None;
+ }
+ let local = format!("{home}/.local/bin/cce-bevel");
+ let cmd = if std::path::Path::new(&local).exists() { local } else { "cce-bevel".to_string() };
+ match std::process::Command::new(&cmd).spawn() {
+ Ok(child) => self.bevel_child = Some(child),
+ Err(e) => self.status_message = Some((format!("cce-bevel launch failed: {e}"), true)),
+ }
+ }
+
fn rebuild_tree(&mut self) {
self.tree_list.selected_key_idx = self.selected_key_idx;
let content = if self.raw_json_editor.editing { &self.raw_json_editor.edit_buffer } else { &self.raw_json_editor.text };
@@ -682,6 +710,7 @@ impl Application for DataEditorApp {
let selected_keybind_editor = KeybindRecorder::new(String::new());
let selected_bool_editor = Checkbox::new();
let selected_button_editor = Button::new(0.0, 0.0, 125.0, 26.0).with_label("Send Test");
+ let selected_bevel_editor = cce_ui::widget::BevelPreview::new();
let mut raw_json_editor = TextBox::new(String::new())
.with_multiline(true)
@@ -796,6 +825,8 @@ impl Application for DataEditorApp {
selected_keybind_editor,
selected_bool_editor,
selected_button_editor,
+ selected_bevel_editor,
+ bevel_child: None,
raw_json_editor,
current_file_path,
status_message: None,
@@ -1389,6 +1420,7 @@ impl Application for DataEditorApp {
self.ui_context.register_widget(self.selected_keybind_editor.base().id(), (*self_ptr).selected_keybind_editor.as_ptr_mut());
self.ui_context.register_widget(self.selected_bool_editor.base().id(), (*self_ptr).selected_bool_editor.as_ptr_mut());
self.ui_context.register_widget(self.selected_button_editor.base().id(), (*self_ptr).selected_button_editor.as_ptr_mut());
+ self.ui_context.register_widget(self.selected_bevel_editor.base().id(), (*self_ptr).selected_bevel_editor.as_ptr_mut());
self.ui_context.register_widget(self.menubar.id(), (*self_ptr).menubar.as_ptr_mut());
self.ui_context.register_widget(self.statusbar.base().id(), (*self_ptr).statusbar.as_ptr_mut());
self.ui_context.register_widget(self.raw_json_editor.base().id(), (*self_ptr).raw_json_editor.as_ptr_mut());
@@ -1493,6 +1525,7 @@ impl Application for DataEditorApp {
let mut is_menu_type = false;
let mut menu_options = Vec::new();
let mut is_button_type = false;
+ let mut is_bevel_type = false;
let mut annotation_str = None;
if let Some(annotation) = cce_ui::config::get_kdl_type_annotation(&self.raw_json_editor.text, key_name) {
annotation_str = Some(annotation.clone());
@@ -1502,10 +1535,30 @@ impl Application for DataEditorApp {
menu_options = opts_str.split(',').map(|s| s.trim().to_string()).collect();
} else if annotation == "button" || annotation.starts_with("button:") {
is_button_type = true;
+ } else if annotation == "bevel" {
+ is_bevel_type = true;
}
}
-
- if is_menu_type {
+ // Parked unless the bevel branch below places it (the other
+ // branches never show it, so one shared park suffices).
+ self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+
+ if is_bevel_type {
+ // The (bevel) preview: a mini lit cross-section of the
+ // knob triple; clicking it opens cce-bevel.
+ if let serde_json::Value::String(st) = val {
+ self.selected_bevel_editor.set_knobs_str(st);
+ }
+ self.selected_bevel_editor.set_rect(row_x + 245.0, row_y + 1.0, 125.0, 26.0);
+ self.selected_choice_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_color_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_spinbox_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_font_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_keybind_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_bool_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_value_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ } else if is_menu_type {
self.selected_choice_editor.options = menu_options.clone();
let val_str = match val {
serde_json::Value::String(st) => st.clone(),
@@ -1606,6 +1659,7 @@ impl Application for DataEditorApp {
self.selected_keybind_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
self.selected_bool_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
}
} else {
self.selected_value_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
@@ -1616,6 +1670,7 @@ impl Application for DataEditorApp {
self.selected_keybind_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
self.selected_bool_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+ self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
}
@@ -1687,7 +1742,7 @@ impl Application for DataEditorApp {
// tree, an editor's box, border and caret all landed under that quad — only its
// text survived, because the engine draws every label after all geometry. Hence
// "the value control has no caret".
- let editors: [&dyn cce_ui::widget::WidgetHost; 8] = [
+ let editors: [&dyn cce_ui::widget::WidgetHost; 9] = [
&self.selected_value_editor,
&self.selected_color_editor,
&self.selected_spinbox_editor,
@@ -1696,6 +1751,7 @@ impl Application for DataEditorApp {
&self.selected_keybind_editor,
&self.selected_bool_editor,
&self.selected_button_editor,
+ &self.selected_bevel_editor,
];
for editor in editors {
cce_ui::scene::painter::paint_root_into(&self.ui_context, editor, &mut pc);
@@ -1809,6 +1865,7 @@ impl Application for DataEditorApp {
if self.ui_context.propagate_event(&ev, self.selected_keybind_editor.id()) { changed = true; }
if self.ui_context.propagate_event(&ev, self.selected_bool_editor.id()) { changed = true; }
if self.ui_context.propagate_event(&ev, self.selected_button_editor.id()) { changed = true; }
+ if self.ui_context.propagate_event(&ev, self.selected_bevel_editor.id()) { changed = true; }
if self.ui_context.propagate_event(&ev, self.raw_json_editor.id()) { changed = true; }
if self.ui_context.propagate_event(&ev, self.tree_list.id()) { changed = true; }
@@ -1940,6 +1997,13 @@ impl Application for DataEditorApp {
changed = true;
editor_handled = true;
}
+ if self.ui_context.propagate_event(&mouse_ev, self.selected_bevel_editor.id()) {
+ changed = true;
+ editor_handled = true;
+ if state == ElementState::Pressed && self.selected_bevel_editor.take_click() {
+ self.open_bevel_editor();
+ }
+ }
if self.ui_context.propagate_event(&mouse_ev, self.selected_button_editor.id()) {
changed = true;
editor_handled = true;