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

commit0e31ca3ab55f901dde9e07845e5f220227797cbc
parente6025cd471
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 10:21
Defer per-event title and settings work to the frame

- Window title: changed pointer events mark title_dirty and tick_frame
  derives it once a frame, instead of serializing the tree per event.
- Dialog sliders: apply_setting saves settings on release, not on every
  motion of the drag.
- Config watch: an unchanged config.kdl is no longer re-read and parsed
  every two seconds.
- gesture_held covers orbit and pan drags, so they don't split undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/app.rs          | 21 ++++++++++++++++++++-
 src/dialog.rs       |  7 ++++++-
 src/edit_history.rs | 10 +++++++++-
 src/window.rs       |  3 ++-
 4 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index f695511..0e499f1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2824,6 +2824,13 @@ pub struct State {
     /// on in `renderer_init`, which runs before the frame has settled and
     /// where relaying the panes would be premature.
     pub page_dirty: bool,
+    /// The window title wants re-deriving (`update_window_title`), at most
+    /// once a frame: deriving it serializes the whole tree to compare with
+    /// the save, and the event path asked on every changed pointer event.
+    pub title_dirty: bool,
+    /// A dialog setting changed during a slider drag and is saved when the
+    /// drag ends (`apply_setting`), not on every motion of it.
+    pub settings_save_pending: bool,
     /// Frame the scene was last built at, so the timeline moving can invalidate it.
     pub last_sim_frame: i32,
     pub plate_menu_slot: Option<usize>,
@@ -8044,6 +8051,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             pan_drag: None,
             pan_exact: None,
             page_dirty: false,
+            title_dirty: false,
+            settings_save_pending: false,
             last_sim_frame: i32::MIN,
             plate_menu_slot: None,
             plate_menu_actions: Vec::new(),
@@ -11885,6 +11894,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
     /// a rebuild. The render half lives in [`State::stage_frame`].
     pub fn tick_frame(&mut self, dt: f32) -> bool {
         let now = Instant::now();
+        // Work the event path deferred to once a frame.
+        if std::mem::take(&mut self.title_dirty) {
+            self.update_window_title();
+        }
+        if self.settings_save_pending && !self.slots.dialog.slider_dragging() {
+            self.settings_save_pending = false;
+            self.save_settings();
+        }
         let light_moved = self.sync_environment();
 
         // A replacement renderer left the page pane with no image; recompose
@@ -11966,7 +11983,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.rebuild_positions();
                 self.apply_layout();
             } else {
-                self.update_inertial_settings();
+                // Unchanged: nothing to re-read. Until 2026-10-06 this read
+                // and parsed config.kdl every two seconds regardless.
+                self.last_config_read = Instant::now();
             }
             let design_path = DesignSettings::file_path();
             if let Ok(m) = std::fs::metadata(&design_path) {
diff --git a/src/dialog.rs b/src/dialog.rs
index aa2bcfe..f967947 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -2573,7 +2573,12 @@ impl State {
         self.sync_grid_settings();
         self.rebuild_scene_geometry();
         self.sync_nodes();
-        self.save_settings();
+        // A slider drag saves once, when it ends (`tick_frame`).
+        if self.slots.dialog.slider_dragging() {
+            self.settings_save_pending = true;
+        } else {
+            self.save_settings();
+        }
         // The params pane may be showing one of these very nodes.
         self.sync_parameters_pane();
         self.refresh_dialog_controls();
diff --git a/src/edit_history.rs b/src/edit_history.rs
index 2f4b1bf..aa4c4d7 100644
--- a/src/edit_history.rs
+++ b/src/edit_history.rs
@@ -387,7 +387,15 @@ impl State {
     /// Whether a drag is held. The graph is not looked at until it is let
     /// go: a dragged node is one step, from where it was picked up.
     fn gesture_held(&self) -> bool {
-        self.drag_widget.is_some() || self.app_drag.is_some() || self.node_drag_group.is_some()
+        self.drag_widget.is_some()
+            || self.app_drag.is_some()
+            || self.node_drag_group.is_some()
+            // A camera orbit or a pan is not an edit, but an orbit with a
+            // camera node active writes its rotation every motion — and a
+            // changed parameter is a full-tree clone here (`rebase_structure`).
+            || self.orbit_drag.is_some()
+            || self.pan_drag.is_some()
+            || self.is_panning
     }
 
     /// Look at the tree, and record what of its structure has changed
diff --git a/src/window.rs b/src/window.rs
index 593bb9c..6462feb 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -191,7 +191,8 @@ impl State {
             }
 
             if changed {
-                state.update_window_title();
+                // Once a frame, not once an event: see `title_dirty`.
+                state.title_dirty = true;
                 if state.syncing_windows() {
                     state.needs_autosave = true;
                 }