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

commit066dcce5d0368452aba36371a9e90d3bc1ea6d39
parentfc9ae94037
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-26 19:32
fix: plates loaded at startup fit the real window, not the placeholder

The startup project loads in Application::new, before the compositor's
first configure, so its plate fractions were scaled onto State::new's
1280x800 placeholder. resize() keeps plates at their pixel size, so every
launch in any other window opened with the plates scaled by 1280/width
(and 800/height), and saving then stored the shrunken fractions, which
ratcheted smaller on each save and relaunch. A load before the first
configure now holds the fractions in pending_plates, and the first real
resize applies them again. Later resizes behave as before.

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

 src/app.rs     | 17 +++++++++++++++++
 src/main.rs    | 38 ++++++++++++++++++++++++++++++++++++++
 src/project.rs | 38 +++++++++++++++++++++++---------------
 3 files changed, 78 insertions(+), 15 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 4b7e8a4..9d1e3b5 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2129,6 +2129,16 @@ pub struct State {
     /// raises the neighbor's bottom edge to the spreadsheet's top.
     pub floating_spreadsheet_inset_left: f32,
     pub floating_spreadsheet_inset_right: f32,
+    /// Whether the compositor has told us the window's size yet. Until it
+    /// has, `width`/`height` are `State::new`'s 1280x800 placeholder.
+    pub window_configured: bool,
+    /// Plate fractions a load applied against that placeholder, held so the
+    /// first real `resize` can apply them again at the real size. The
+    /// startup project loads before the first configure, and `resize`
+    /// keeps plates at their pixel size, so without this every launch in a
+    /// window other than 1280x800 opened with the plates scaled by
+    /// 1280/width — and a save then stored the scaled fractions.
+    pub pending_plates: Option<PlateGeometry>,
     pub loaded_project_path: Option<std::path::PathBuf>,
     /// The configured startup project (`DesignSettings::default_project`),
     /// mirrored live so "Set As Default" can rewrite it and `save_settings` —
@@ -5769,6 +5779,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             floating_spreadsheet_height: 250.0,
             floating_spreadsheet_inset_left: 0.0,
             floating_spreadsheet_inset_right: 0.0,
+            window_configured: false,
+            pending_plates: None,
             loaded_project_path: None,
             default_project_setting: settings.default_project.clone(),
             gpu_preference: settings.gpu.clone(),
@@ -7759,6 +7771,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.slots.splitter2.set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
             }
 
+            self.window_configured = true;
+            if let Some(pg) = self.pending_plates.take() {
+                self.apply_plate_geometry(pg);
+            }
+
             self.sync_layout();
             self.read_panel_offsets();
             self.keep_cursor_in_view();
diff --git a/src/main.rs b/src/main.rs
index f3968f1..45a754b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1407,6 +1407,44 @@ mod tests {
         let _ = fs::remove_dir_all(&dir);
     }
 
+    /// The startup project loads before the compositor's first configure, so
+    /// its plate fractions land on `State::new`'s 1280x800 placeholder; the
+    /// first real size must apply them again. Before, the plates kept the
+    /// placeholder's pixels, opening every launch scaled by 1280/width (and
+    /// 800/height), and a save then stored the shrunken fractions.
+    #[test]
+    fn plates_loaded_before_the_first_configure_fit_the_real_window() {
+        let dir = std::env::temp_dir().join(format!("cce_designer_prefconfigure_{}", std::process::id()));
+        let _ = fs::remove_dir_all(&dir);
+        fs::create_dir_all(&dir).unwrap();
+        let mut a = State::new(false);
+        a.resize(1400.0, 1080.0, 1.0);
+        a.execute_menu_action("Show Spreadsheet Pane");
+        a.floating_network_layout.2 = 350.0;
+        a.floating_param_width = 310.0;
+        a.floating_spreadsheet_height = 100.0;
+        a.rebuild_positions();
+        a.save_to_file(&dir).expect("save");
+
+        // No resize before the load: this is the startup order.
+        let mut b = State::new(false);
+        assert!(!b.window_configured);
+        b.load_from_file(&dir).expect("load");
+        b.resize(1400.0, 1080.0, 1.0);
+        assert!((b.left_dock_width() - 350.0).abs() < 0.5, "network width: {}", b.left_dock_width());
+        assert!((b.right_dock_width() - 310.0).abs() < 0.5, "param width: {}", b.right_dock_width());
+        assert!((b.floating_spreadsheet_height - 100.0).abs() < 0.5, "spreadsheet height: {}", b.floating_spreadsheet_height);
+        let pg = b.project_view_state().plates.expect("plates");
+        assert!((pg.network_width - 350.0 / 1400.0).abs() < 1e-4, "a save writes back what was loaded: {:?}", pg);
+
+        // Only the FIRST configure: a later window resize keeps the plates'
+        // pixels, as it always has.
+        b.resize(1000.0, 1080.0, 1.0);
+        assert!((b.left_dock_width() - 350.0).abs() < 0.5, "a later resize rescaled the plate: {}", b.left_dock_width());
+
+        let _ = fs::remove_dir_all(&dir);
+    }
+
     /// A window briefly narrower than its plates squeezes them for as long as
     /// it lasts and no longer: the layout used to store the clamped size, so
     /// one transient shrink (a re-tile, a configure at startup) left every
diff --git a/src/project.rs b/src/project.rs
index 0efa450..0bd58d1 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -396,22 +396,30 @@ impl State {
                 self.apply_layout();
             }
         }
-        // Plate geometry: the fractions scale back onto this window, and
-        // the layout pass clamps them exactly as a drag would (minimum
-        // widths, the spreadsheet's tuck limits). A save with a nonsense
-        // value keeps the live geometry rather than loading half of one.
+        // Plate geometry: fractions of the window, see `apply_plate_geometry`.
         if let Some(pg) = vs.plates {
-            let sane = |f: f32| f.is_finite() && (0.0..=1.0).contains(&f);
-            let all = [pg.network_width, pg.params_width, pg.spreadsheet_height, pg.spreadsheet_inset_left, pg.spreadsheet_inset_right];
-            if self.width > 1.0 && self.height > 1.0 && all.iter().all(|&f| sane(f)) {
-                self.floating_network_layout.2 = pg.network_width * self.width;
-                self.floating_param_width = pg.params_width * self.width;
-                self.floating_spreadsheet_height = pg.spreadsheet_height * self.height;
-                self.floating_spreadsheet_inset_left = pg.spreadsheet_inset_left * self.width;
-                self.floating_spreadsheet_inset_right = pg.spreadsheet_inset_right * self.width;
-                self.rebuild_positions();
-                self.apply_layout();
-            }
+            self.apply_plate_geometry(pg);
+        }
+        // Before the first configure the fractions just landed on the
+        // placeholder size; the first real resize applies them again.
+        self.pending_plates = if self.window_configured { None } else { vs.plates };
+    }
+
+    /// Scale saved plate fractions onto this window. The layout pass clamps
+    /// them exactly as a drag would (minimum widths, the spreadsheet's tuck
+    /// limits). A save with a nonsense value keeps the live geometry rather
+    /// than loading half of one.
+    pub(crate) fn apply_plate_geometry(&mut self, pg: crate::app::PlateGeometry) {
+        let sane = |f: f32| f.is_finite() && (0.0..=1.0).contains(&f);
+        let all = [pg.network_width, pg.params_width, pg.spreadsheet_height, pg.spreadsheet_inset_left, pg.spreadsheet_inset_right];
+        if self.width > 1.0 && self.height > 1.0 && all.iter().all(|&f| sane(f)) {
+            self.floating_network_layout.2 = pg.network_width * self.width;
+            self.floating_param_width = pg.params_width * self.width;
+            self.floating_spreadsheet_height = pg.spreadsheet_height * self.height;
+            self.floating_spreadsheet_inset_left = pg.spreadsheet_inset_left * self.width;
+            self.floating_spreadsheet_inset_right = pg.spreadsheet_inset_right * self.width;
+            self.rebuild_positions();
+            self.apply_layout();
         }
     }