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

commitcd6a5ada39391ea67cfc9f4f03b98198f5955a30
parent9abb7c7f85
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 16:35
fix: a shrunk window no longer shrinks the plates for good

rebuild_positions clamped each plate's stored width/height to the window
and wrote the clamp back, so any moment the window was narrower than a
plate (a re-tile, a configure at startup) made the plate that small
permanently. The stored sizes are now the size asked for; the clamp is
applied where the on-screen size is read (left_dock_width,
right_dock_width, floating_spreadsheet_rect) and never stored. A drag
still commits what it shows, so an overshoot past the limit is not kept,
and a save caps each fraction at 1 since the loader refuses larger.

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

 src/app.rs     | 67 ++++++++++++++++++++++++++++++++++++++++------------------
 src/main.rs    | 29 +++++++++++++++++++++++++
 src/project.rs |  9 +++++---
 3 files changed, 82 insertions(+), 23 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index db666ae..248be24 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2840,7 +2840,8 @@ impl State {
         if self.circular_network_pane || !self.show_network {
             return None;
         }
-        let (fx, fy, fw, fh) = self.floating_network_layout;
+        let (fx, fy, _, fh) = self.floating_network_layout;
+        let fw = self.left_dock_width();
         let margin = 8.0_f32;
         let on_right = cx >= fx + fw - margin && cx <= fx + fw + margin && cy >= fy - margin && cy <= fy + fh + margin;
         if on_right {
@@ -2856,7 +2857,7 @@ impl State {
             return false;
         }
         let gap = 18.0_f32;
-        let param_w = self.floating_param_width;
+        let param_w = self.right_dock_width();
         let param_x = self.width - gap - param_w;
         let param_y = HEADER_H + gap;
         let param_h = (self.height - HEADER_H - STATUS_H - 2.0 * gap).max(100.0);
@@ -2916,11 +2917,11 @@ impl State {
         let pb_off = if self.show_playbar { PLAYBAR_H + gap } else { 0.0 };
         match dock {
             Dock::Left => {
-                let (fx, fy, fw, fh) = self.floating_network_layout;
-                (fx.max(gap), fy.max(gap), fw, fh)
+                let (fx, fy, _, fh) = self.floating_network_layout;
+                (fx.max(gap), fy.max(gap), self.left_dock_width(), fh)
             }
             Dock::Right => {
-                let param_w = self.floating_param_width.clamp(150.0, (self.width - 2.0 * gap).max(150.0));
+                let param_w = self.right_dock_width();
                 let h = (self.height - STATUS_H - pb_off - 2.0 * gap).max(100.0);
                 (self.width - gap - param_w, gap, param_w, h)
             }
@@ -3038,9 +3039,8 @@ impl State {
     pub fn floating_spreadsheet_rect(&self) -> (f32, f32, f32, f32) {
         let gap = 18.0_f32;
         let fx = gap;
-        let (_, _, mut fw, _) = self.floating_network_layout;
-        fw = fw.clamp(150.0, (self.width - 2.0 * gap).max(150.0));
-        let param_w = self.floating_param_width.clamp(150.0, (self.width - 2.0 * gap).max(150.0));
+        let fw = self.left_dock_width();
+        let param_w = self.right_dock_width();
         let param_x = self.width - gap - param_w;
         let flush_left = if self.dock_shown(Dock::Left) { fx + fw + gap } else { gap };
         let flush_right = if self.dock_shown(Dock::Right) { param_x - gap } else { self.width - gap };
@@ -3059,6 +3059,21 @@ impl State {
         (ss_x, ss_y, ss_w, ss_h)
     }
 
+    /// The left dock's width as drawn: `floating_network_layout.2` (the
+    /// width asked for) fitted to this window. Read this, not the field,
+    /// wherever the plate's ON-SCREEN width matters.
+    pub fn left_dock_width(&self) -> f32 {
+        let gap = 18.0_f32;
+        self.floating_network_layout.2.clamp(150.0, (self.width - 2.0 * gap).max(150.0))
+    }
+
+    /// The right dock's width as drawn — `floating_param_width` fitted to
+    /// this window, as [`Self::left_dock_width`] is for the left.
+    pub fn right_dock_width(&self) -> f32 {
+        let gap = 18.0_f32;
+        self.floating_param_width.clamp(150.0, (self.width - 2.0 * gap).max(150.0))
+    }
+
     /// Whether the spreadsheet is tucked under the network / parameter pane
     /// (side inset active while both panes are shown).
     pub fn spreadsheet_tucks_left(&self) -> bool {
@@ -6525,15 +6540,20 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             } else {
                 let gap = 18.0_f32;
                 let pb_off = if self.show_playbar { PLAYBAR_H + gap } else { 0.0 };
-                let (mut _fx, mut _fy, mut fw, mut _fh) = self.floating_network_layout;
-                fw = fw.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
+                // The stored widths and height are what the user ASKED for;
+                // the clamps below fit them to this window for drawing and are
+                // never written back. Storing the clamp made every transient
+                // shrink permanent — a window briefly narrower than a plate
+                // left it that narrow when the window grew again.
+                let fw = self.left_dock_width();
                 let fx = paginator_w + gap;
                 let fy = gap;
                 let mut fh = (self.height - STATUS_H - pb_off - 2.0 * gap).max(100.0);
-                self.floating_network_layout = (fx, fy, fw, fh);
+                self.floating_network_layout.0 = fx;
+                self.floating_network_layout.1 = fy;
+                self.floating_network_layout.3 = fh;
 
-                let param_w = self.floating_param_width.clamp(150.0, (self.width - paginator_w - 2.0 * gap).max(150.0));
-                self.floating_param_width = param_w;
+                let param_w = self.right_dock_width();
                 let param_x = self.width - gap - param_w;
                 let param_y = gap;
                 let mut param_h = (self.height - STATUS_H - pb_off - 2.0 * gap).max(100.0);
@@ -6543,7 +6563,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 // tucks the spreadsheet UNDER that neighbor: the neighbor's bottom
                 // rises to the spreadsheet's top edge to make room.
                 let (ss_x, ss_y, ss_w, ss_h) = self.floating_spreadsheet_rect();
-                self.floating_spreadsheet_height = ss_h;
                 if self.spreadsheet_tucks_left() {
                     fh = (ss_y - gap - fy).max(100.0);
                     self.floating_network_layout.3 = fh;
@@ -8033,6 +8052,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                 }
 
                                 self.floating_network_layout = (fx, fy, fw, fh);
+                                // A drag commits the width it SHOWS: storing an
+                                // overshoot past the window's limit would leave
+                                // the edge dead on the way back until the pointer
+                                // had unwound it.
+                                self.floating_network_layout.2 = self.left_dock_width();
                                 self.rebuild_positions();
                                 self.apply_layout();
                                 self.sync_grid_settings();
@@ -8042,6 +8066,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                 let dx = self.cursor_x - start_mouse_x;
                                 let new_w = (start_w - dx).max(150.0);
                                 self.floating_param_width = new_w;
+                                self.floating_param_width = self.right_dock_width();
                                 self.rebuild_positions();
                                 self.apply_layout();
                                 changed = true;
@@ -8050,6 +8075,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                 let dy = self.cursor_y - start_mouse_y;
                                 let new_h = (start_h - dy).max(100.0);
                                 self.floating_spreadsheet_height = new_h;
+                                self.floating_spreadsheet_height = self.floating_spreadsheet_rect().3;
                                 self.rebuild_positions();
                                 self.apply_layout();
                                 changed = true;
@@ -8060,8 +8086,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                 // pane's bottom to make room); back to flush un-tucks it.
                                 let dx = self.cursor_x - start_mouse_x;
                                 let gap = 18.0_f32;
-                                let (fx, _, fw, _) = self.floating_network_layout;
-                                let flush_left = fx + fw + gap;
+                                let fx = self.floating_network_layout.0;
+                                let flush_left = fx + self.left_dock_width() + gap;
                                 let max_inset = (flush_left - gap).max(0.0);
                                 self.floating_spreadsheet_inset_left =
                                     (start_inset - dx).clamp(0.0, max_inset);
@@ -8085,7 +8111,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                                 // The right edge tucks under the parameter pane, symmetrically.
                                 let dx = self.cursor_x - start_mouse_x;
                                 let gap = 18.0_f32;
-                                let param_x = self.width - gap - self.floating_param_width;
+                                let param_x = self.width - gap - self.right_dock_width();
                                 let flush_right = param_x - gap;
                                 let max_inset = (self.width - gap - flush_right).max(0.0);
                                 self.floating_spreadsheet_inset_right =
@@ -8372,14 +8398,15 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                         }
 
                         if *button == MouseButton::Left && !self.circular_network_pane && self.show_network {
-                            let (fx, fy, fw, _fh) = self.floating_network_layout;
+                            let (fx, fy, _, _fh) = self.floating_network_layout;
+                            let fw = self.left_dock_width();
                             let cx = self.cursor_x;
                             let cy = self.cursor_y;
 
                             if let Some(dir) = self.network_resize_edge_at(cx, cy) {
                                 self.app_drag = Some(AppDrag::NetworkResize {
                                     dir,
-                                    start_rect: self.floating_network_layout,
+                                    start_rect: (fx, fy, fw, self.floating_network_layout.3),
                                     start_mouse: (cx, cy),
                                 });
                                 self.focused_pane = LEFT_MENUBAR_IDX;
@@ -8414,7 +8441,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
 
                             if self.on_param_resize_edge(cx, cy) {
                                 self.app_drag = Some(AppDrag::ParamResize {
-                                    start_w: self.floating_param_width,
+                                    start_w: self.right_dock_width(),
                                     start_mouse_x: cx,
                                 });
                                 self.focused_pane = PARAM_MENUBAR_IDX;
diff --git a/src/main.rs b/src/main.rs
index 99a5b8e..63c862d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1367,6 +1367,35 @@ mod tests {
         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
+    /// plate that small for good.
+    #[test]
+    fn a_shrunk_window_does_not_shrink_the_plates_for_good() {
+        let mut s = State::new(false);
+        s.resize(1600.0, 900.0, 1.0);
+        s.execute_menu_action("Show Spreadsheet Pane");
+        s.floating_network_layout.2 = 700.0;
+        s.floating_param_width = 650.0;
+        s.floating_spreadsheet_height = 600.0;
+        s.rebuild_positions();
+
+        s.resize(400.0, 300.0, 1.0);
+        s.rebuild_positions();
+        assert!(
+            s.positions[crate::slots::PARAM_IDX].2 <= 400.0 - 2.0 * 18.0 + 0.5,
+            "the plate still fits the small window: {:?}", s.positions[crate::slots::PARAM_IDX]
+        );
+
+        s.resize(1600.0, 900.0, 1.0);
+        s.rebuild_positions();
+        assert!((s.left_dock_width() - 700.0).abs() < 0.5, "network width: {}", s.left_dock_width());
+        assert!((s.right_dock_width() - 650.0).abs() < 0.5, "param width: {}", s.right_dock_width());
+        assert!((s.floating_spreadsheet_rect().3 - 600.0).abs() < 0.5, "spreadsheet height: {:?}", s.floating_spreadsheet_rect());
+        assert!((s.positions[crate::slots::PARAM_IDX].2 - 650.0).abs() < 0.5, "drawn param width: {:?}", s.positions[crate::slots::PARAM_IDX]);
+    }
+
     /// A dragged plate edge is an unsaved change — the save file carries the
     /// plate geometry, so the title's asterisk must follow it, and clear on
     /// save. A window resize alone must NOT dirty it.
diff --git a/src/project.rs b/src/project.rs
index 60051da..6a4f1b4 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -133,11 +133,14 @@ impl State {
                 names
             })
             .collect();
+        // A plate's stored size is what was asked for and may exceed a window
+        // that has since shrunk; the loader refuses a fraction past 1 (and
+        // with it the whole block), so a save caps each at the full window.
         let plates = if self.width > 1.0 && self.height > 1.0 {
             Some(PlateGeometry {
-                network_width: self.floating_network_layout.2 / self.width,
-                params_width: self.floating_param_width / self.width,
-                spreadsheet_height: self.floating_spreadsheet_height / self.height,
+                network_width: (self.floating_network_layout.2 / self.width).min(1.0),
+                params_width: (self.floating_param_width / self.width).min(1.0),
+                spreadsheet_height: (self.floating_spreadsheet_height / self.height).min(1.0),
                 spreadsheet_inset_left: self.floating_spreadsheet_inset_left / self.width,
                 spreadsheet_inset_right: self.floating_spreadsheet_inset_right / self.width,
             })