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

commit13b7c53dc8424ab8284c98b999fe8f20b1e3eb67
parent0f4a0bcf90
authorLucas Galante <[email protected]>
date2026-07-16 14:18
refactor: design.kdl becomes state.kdl; scroll settings are config-owned

The app-written settings file is renamed to state.kdl (legacy design.kdl
and design.json migrate on load: re-saved, old file deleted). The
scroll_speed / inertial_scroll / scroll_friction fields are removed from
ViewportSettings and GraphSettings: save_settings froze the
config-derived values into the state file as overrides, permanently
shadowing later input.inertial edits in config.kdl — scroll behavior now
has config as its single owner.

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

 CLAUDE.md  |  10 ++++++
 src/app.rs | 120 +++++++++++++++++++++++++++----------------------------------
 2 files changed, 63 insertions(+), 67 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index d39fc41..51c26a2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -106,6 +106,16 @@ The `zcce_inspector_v1` integration (window-position tracking + widget-state
 streaming to cce-test-interface) was dropped in the engine migration; the HTTP API
 is the introspection surface.
 
+### App-written settings: `~/.config/cce/cce-designer/state.kdl`
+
+`DesignSettings` (viewport/graph display state the app rewrites itself:
+colors, grid sizes, show flags) persists to `state.kdl` — deliberately NOT
+`config.kdl`, which is the user-authored toolkit-config override slot that
+cce-ui auto-merges (see `../CLAUDE.md`). Legacy `design.kdl` / `design.json`
+files migrate on load. Scroll behavior (`scroll_speed`, `inertial_scroll`,
+`scroll_friction`) is intentionally absent: it is config-owned
+(`input.inertial` in config.kdl) and must not be shadowed by app state.
+
 ### Runtime paths point into the source tree
 
 Node templates (`nodes/*.json`) and `default_project.json` are located via
diff --git a/src/app.rs b/src/app.rs
index 137abb3..cea9892 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -473,9 +473,6 @@ pub struct ViewportSettings {
     pub square: bool,
     pub show_camera_pivot_enabled: bool,
     pub camera_pivot_size: f32,
-    pub scroll_speed: Option<f32>,
-    pub inertial_scroll: Option<bool>,
-    pub scroll_friction: Option<f32>,
     pub show_grid_enabled: bool,
     pub show_cube_enabled: bool,
     pub show_origin_enabled: bool,
@@ -493,9 +490,6 @@ impl Default for ViewportSettings {
             square: false,
             show_camera_pivot_enabled: false,
             camera_pivot_size: 1.0,
-            scroll_speed: None,
-            inertial_scroll: None,
-            scroll_friction: None,
             show_grid_enabled: true,
             show_cube_enabled: false,
             show_origin_enabled: true,
@@ -512,9 +506,6 @@ pub struct GraphSettings {
     pub grid_size_y: f32,
     pub skipped_row_h: f32,
     pub skipped_col_w: f32,
-    pub scroll_speed: Option<f32>,
-    pub inertial_scroll: Option<bool>,
-    pub scroll_friction: Option<f32>,
 }
 
 impl Default for GraphSettings {
@@ -524,9 +515,6 @@ impl Default for GraphSettings {
             grid_size_y: 40.0,
             skipped_row_h: 20.0,
             skipped_col_w: 20.0,
-            scroll_speed: None,
-            inertial_scroll: None,
-            scroll_friction: None,
         }
     }
 }
@@ -557,48 +545,55 @@ impl DesignSettings {
         path.push(".config");
         path.push("cce");
         path.push("cce-designer");
-        path.push("design.kdl");
+        path.push("state.kdl");
         path
     }
 
-    fn load() -> Self {
-        let path = Self::file_path();
-        if path.exists() {
-            if let Ok(content) = fs::read_to_string(&path) {
-                let mut json_val = cce_ui::config::parse_kdl_to_json(&content);
-                // Convert hex strings back to color arrays
-                if let Some(obj) = json_val.as_object_mut() {
-                    if let Some(viewport) = obj.get_mut("viewport").and_then(|v| v.as_object_mut()) {
-                        if let Some(serde_json::Value::String(hex_str)) = viewport.get("bg_color") {
-                            if let Some(arr) = hex_to_float_array(hex_str) {
-                                if let Ok(arr_val) = serde_json::to_value(arr) {
-                                    viewport.insert("bg_color".to_string(), arr_val);
-                                }
-                            }
+    fn load_kdl(path: &std::path::Path) -> Option<Self> {
+        let content = fs::read_to_string(path).ok()?;
+        let mut json_val = cce_ui::config::parse_kdl_to_json(&content);
+        // Convert hex strings back to color arrays
+        if let Some(obj) = json_val.as_object_mut() {
+            if let Some(viewport) = obj.get_mut("viewport").and_then(|v| v.as_object_mut()) {
+                if let Some(serde_json::Value::String(hex_str)) = viewport.get("bg_color") {
+                    if let Some(arr) = hex_to_float_array(hex_str) {
+                        if let Ok(arr_val) = serde_json::to_value(arr) {
+                            viewport.insert("bg_color".to_string(), arr_val);
                         }
-                        if let Some(serde_json::Value::String(hex_str)) = viewport.get("grid_color") {
-                            if let Some(arr) = hex_to_float_array(hex_str) {
-                                if let Ok(arr_val) = serde_json::to_value(arr) {
-                                    viewport.insert("grid_color".to_string(), arr_val);
-                                }
-                            }
+                    }
+                }
+                if let Some(serde_json::Value::String(hex_str)) = viewport.get("grid_color") {
+                    if let Some(arr) = hex_to_float_array(hex_str) {
+                        if let Ok(arr_val) = serde_json::to_value(arr) {
+                            viewport.insert("grid_color".to_string(), arr_val);
                         }
                     }
                 }
-                return serde_json::from_value::<Self>(json_val).unwrap_or_else(|_| Self::default());
             }
         }
-        
-        // Migration fallback: load from design.json, save to design.kdl, and delete the old JSON
-        let mut old_path = path.clone();
-        old_path.set_extension("json");
-        if old_path.exists() {
-            if let Ok(content) = fs::read_to_string(&old_path) {
-                if let Ok(settings) = serde_json::from_str::<Self>(&content) {
-                    settings.save();
-                    let _ = fs::remove_file(old_path);
-                    return settings;
-                }
+        Some(serde_json::from_value::<Self>(json_val).unwrap_or_else(|_| Self::default()))
+    }
+
+    fn load() -> Self {
+        let path = Self::file_path();
+        if let Some(settings) = Self::load_kdl(&path) {
+            return settings;
+        }
+
+        // Migration fallback: the pre-rename design.kdl (same format), then
+        // the ancient design.json — load, re-save as state.kdl, delete the old.
+        let legacy_kdl = path.with_file_name("design.kdl");
+        if let Some(settings) = Self::load_kdl(&legacy_kdl) {
+            settings.save();
+            let _ = fs::remove_file(legacy_kdl);
+            return settings;
+        }
+        let legacy_json = path.with_file_name("design.json");
+        if let Ok(content) = fs::read_to_string(&legacy_json) {
+            if let Ok(settings) = serde_json::from_str::<Self>(&content) {
+                settings.save();
+                let _ = fs::remove_file(legacy_json);
+                return settings;
             }
         }
         Self::default()
@@ -1184,9 +1179,6 @@ impl State {
                 square: self.square_viewport,
                 show_camera_pivot_enabled: self.viewport().show_camera_pivot,
                 camera_pivot_size: self.camera_pivot_size,
-                scroll_speed: Some(self.viewport().scroll_speed),
-                inertial_scroll: Some(self.viewport().inertial_scroll),
-                scroll_friction: Some(self.viewport().scroll_friction),
                 show_grid_enabled: self.viewport().show_grid,
                 show_cube_enabled: self.viewport().show_cube,
                 show_origin_enabled: self.viewport().show_origin,
@@ -1199,9 +1191,6 @@ impl State {
                 grid_size_y: self.grid_size_y,
                 skipped_row_h: self.skipped_row_h,
                 skipped_col_w: self.skipped_col_w,
-                scroll_speed: Some(self.graph_scroll_speed),
-                inertial_scroll: Some(self.graph_inertial_scroll),
-                scroll_friction: Some(self.graph_scroll_friction),
             },
         };
         settings.save();
@@ -2553,9 +2542,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             last_spreadsheet_node_params: None,
             grid_thickness: settings.viewport.grid_thickness,
             focused_pane: LEFT_MENUBAR_IDX,
-            graph_scroll_speed: settings.graph.scroll_speed.unwrap_or(1.0),
-            graph_inertial_scroll: settings.graph.inertial_scroll.unwrap_or(true),
-            graph_scroll_friction: settings.graph.scroll_friction.unwrap_or(0.90),
+            // Config-owned; update_inertial_settings overwrites these from
+            // config.kdl's input.inertial right after construction.
+            graph_scroll_speed: 1.0,
+            graph_inertial_scroll: true,
+            graph_scroll_friction: 0.90,
             last_config_read: Instant::now(),
             circular_network_pane: is_detached_network,
             circular_network_layout: cce_ui::layout::CircularPaneLayout::new(250.0, 300.0, 180.0),
@@ -2737,13 +2728,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             }
         }
         
-        let settings = DesignSettings::load();
-        self.graph_scroll_speed = settings.graph.scroll_speed.unwrap_or(speed);
-        self.graph_inertial_scroll = settings.graph.inertial_scroll.unwrap_or(enabled);
-        self.graph_scroll_friction = settings.graph.scroll_friction.unwrap_or(friction);
-        self.viewport_mut().scroll_speed = settings.viewport.scroll_speed.unwrap_or(speed);
-        self.viewport_mut().inertial_scroll = settings.viewport.inertial_scroll.unwrap_or(enabled);
-        self.viewport_mut().scroll_friction = settings.viewport.scroll_friction.unwrap_or(friction);
+        // Scroll behavior is config-owned (input.inertial in config.kdl) —
+        // state.kdl carries no copy, so config edits always take effect.
+        self.graph_scroll_speed = speed;
+        self.graph_inertial_scroll = enabled;
+        self.graph_scroll_friction = friction;
+        self.viewport_mut().scroll_speed = speed;
+        self.viewport_mut().inertial_scroll = enabled;
+        self.viewport_mut().scroll_friction = friction;
     }
 
     pub fn update_graph_settings_from_config(&mut self) {
@@ -4743,12 +4735,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                          self.camera_pivot_size = settings.viewport.camera_pivot_size;
                          self.cell_color = cce_ui::color::graph_cell_color();
                          self.gap_color = cce_ui::color::graph_gap_color();
-                          if let Some(val) = settings.graph.scroll_speed { self.graph_scroll_speed = val; }
-                          if let Some(val) = settings.graph.inertial_scroll { self.graph_inertial_scroll = val; }
-                          if let Some(val) = settings.graph.scroll_friction { self.graph_scroll_friction = val; }
-                          if let Some(val) = settings.viewport.scroll_speed { self.viewport_mut().scroll_speed = val; }
-                          if let Some(val) = settings.viewport.inertial_scroll { self.viewport_mut().inertial_scroll = val; }
-                          if let Some(val) = settings.viewport.scroll_friction { self.viewport_mut().scroll_friction = val; }
 
                         colors::set_node_color([self.node_color[0], self.node_color[1], self.node_color[2], 1.0]);