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