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

commit45eecf9edcc886263c393de67616f4e050785476
parent3b3e55e0c8
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 11:25
feat(spreadsheet): the plate is as wide as its table

floating_spreadsheet_rect takes the table's width (cce-ui's
Spreadsheet::content_width) within a 150 px floor and the window less its
gaps, where the table scrolls; a refill that changes it lays the
positions out again. The params HUD runs past a plate that stops short.

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

 CLAUDE.md   | 13 +++++++++++--
 src/app.rs  | 25 ++++++++++++++++++++++---
 src/main.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++----
 3 files changed, 85 insertions(+), 9 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9c1e53b..072dc41 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2808,9 +2808,18 @@ the spreadsheet the one pane a dock could hold, and the model went whole:
 tabs first, then the docks.
 
 What is left is two plate edges. The **spreadsheet** is the strip along
-the bottom, a gap in from the window's sides and a gap above the playbar,
+the bottom, a gap in from the window's left and a gap above the playbar,
 as tall as `floating_spreadsheet_height` asks (`floating_spreadsheet_rect`,
-the one derivation); its TOP edge resizes it (`on_spreadsheet_resize_edge`
+the one derivation) and **as wide as its table** (since 2026-10-07,
+cce-ui's `Spreadsheet::content_width`: its columns, each as wide as its
+content), never narrower than `SPREADSHEET_MIN_W` (an empty table) and no
+wider than the window less a gap each side, where the table scrolls. It
+spanned the window, and a narrow table left most of the plate empty over
+the scene; now the params HUD runs down past a plate that stops short of
+it. A refill that changes the table's width lays out positions again at
+the end of `sync_selection_readouts` — positions only, since
+`sync_layout` also moves the grid cursor and with it the selection
+(`the_spreadsheet_plate_is_as_wide_as_its_table`). Its TOP edge resizes it (`on_spreadsheet_resize_edge`
 — its sides tucked it under a side dock's plate). The **params HUD**'s left
 edge sets `params_hud_width`. The plate menus hold the window actions alone
 — Collapse / Expand, Detach / Reattach. `PlateGeometry` saves those two
diff --git a/src/app.rs b/src/app.rs
index b117ba5..385f125 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4600,10 +4600,18 @@ impl State {
             .any(|&(x, y, w, h)| px >= x && px < x + w && py >= y && py < y + h)
     }
 
+    /// The narrowest the spreadsheet's plate is, an empty table's.
+    pub const SPREADSHEET_MIN_W: f32 = 150.0;
+
     /// The spreadsheet's rect — the single derivation the layout pass and
     /// the edge hotspot share: the strip along the bottom, a gap in from
-    /// the window's sides, standing a gap above the playbar, as tall as
-    /// `floating_spreadsheet_height` asks within the window. (Until
+    /// the window's left, standing a gap above the playbar, as tall as
+    /// `floating_spreadsheet_height` asks within the window, and as WIDE AS
+    /// ITS TABLE (since 2026-10-07; `Spreadsheet::content_width`, the
+    /// columns as wide as their content) up to the window less a gap each
+    /// side, where a wider table scrolls — it spanned the window, and a
+    /// narrow table left most of the plate empty over the scene. Never
+    /// narrower than `Self::SPREADSHEET_MIN_W`, which an empty table is. (Until
     /// 2026-10-07 it was the bottom DOCK's rect: panes were assigned to a
     /// left, right and bottom dock, swapped by Move To, and the
     /// spreadsheet's sides could tuck under a side dock's plate.)
@@ -4614,7 +4622,9 @@ impl State {
         let pb_off = if self.show_playbar { playbar_shelf_h() } else { 0.0 };
         let ss_y_end = self.height - STATUS_H - pb_off - gap;
         let ss_h = self.floating_spreadsheet_height.clamp(100.0, (ss_y_end - gap).max(100.0));
-        (gap, ss_y_end - ss_h, (self.width - 2.0 * gap).max(150.0), ss_h)
+        let room = (self.width - 2.0 * gap).max(Self::SPREADSHEET_MIN_W);
+        let w = self.slots.spreadsheet().content_width().ceil().clamp(Self::SPREADSHEET_MIN_W, room);
+        (gap, ss_y_end - ss_h, w, ss_h)
     }
 
     /// Whether (cx, cy) is on the spreadsheet's top edge, which resizes its
@@ -7824,6 +7834,15 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             // re-size without re-evaluating.
             self.rebuild_group_markers();
         }
+        // The plate is as wide as the table: a table that came out wider or
+        // narrower lays the panes out again — last, once everything this
+        // refresh read is in, since the layout pass can come back here.
+        // Positions only: `sync_layout` also moves the grid cursor and with
+        // it the selection, which a refresh of what is selected must not.
+        if self.show_spreadsheet && self.positions[SPREADSHEET_IDX].2 > 0.0 && self.positions[SPREADSHEET_IDX].2 != self.floating_spreadsheet_rect().2 {
+            self.rebuild_positions();
+            self.apply_layout();
+        }
     }
 
     /// How many points of a pull get an arrow. A dozen show the direction
diff --git a/src/main.rs b/src/main.rs
index 191e263..f6d4297 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -488,7 +488,8 @@ mod tests {
             state.execute_menu_action("Show Playbar Pane");
         }
         let (x, y, w, h) = state.positions[SPREADSHEET_IDX];
-        assert_eq!((x, w), (18.0, 1600.0 - 36.0), "the window's width, a gap in");
+        let table = state.slots.spreadsheet().content_width().ceil();
+        assert_eq!((x, w), (18.0, table.clamp(State::SPREADSHEET_MIN_W, 1600.0 - 36.0)), "as wide as its table, within the window");
         assert_eq!(y + h, state.positions[PLAYBAR_IDX].1 - 18.0, "a gap above the playbar");
 
         for idx in [SPREADSHEET_IDX, PARAM_IDX, NETWORK_PANEL_IDX, PLAYBAR_IDX] {
@@ -523,7 +524,7 @@ mod tests {
         b.resize(1600.0, 900.0, 1.0);
         b.load_from_file(&dir).expect("load");
         let (bx, _, bw, _) = b.positions[SPREADSHEET_IDX];
-        assert_eq!((bx, bw), (18.0, 1600.0 - 36.0), "along the bottom");
+        assert_eq!((bx, bw), (18.0, b.floating_spreadsheet_rect().2), "along the bottom");
         assert!((b.params_hud_width - 400.0).abs() < 0.5, "an older save's params width is the HUD's: {}", b.params_hud_width);
         assert!((b.floating_spreadsheet_height - 270.0).abs() < 0.5);
         let _ = fs::remove_dir_all(&dir);
@@ -2106,10 +2107,21 @@ mod tests {
         assert_eq!(hud.1 + hud.3, 900.0 - crate::app::STATUS_H - 18.0, "the viewport's height, a gap in");
 
         // The spreadsheet and the playbar below it: it stops a gap above
-        // the higher of them.
+        // the higher of them. The plate is as wide as its table: a narrow
+        // one, along the left, is not under the HUD and leaves it be.
         state.execute_menu_action("Show Spreadsheet Pane");
         state.execute_menu_action("Show Playbar Pane");
         state.floating_spreadsheet_height = 500.0;
+        state.spreadsheet_mut().set_spreadsheet_data(vec!["a".into()], vec![vec!["1".into()]]);
+        state.rebuild_positions();
+        state.apply_layout();
+        let (sx, _, sw, _) = state.positions[SPREADSHEET_IDX];
+        assert!(sx + sw < hud.0, "a narrow table's plate stops short of the HUD");
+        let below_playbar = state.positions[PARAM_IDX];
+        assert!(below_playbar.1 + below_playbar.3 > state.positions[SPREADSHEET_IDX].1, "and the HUD runs past its top");
+        // A table as wide as the window reaches under the HUD.
+        let headers: Vec<String> = (0..40).map(|i| format!("column{i}")).collect();
+        state.spreadsheet_mut().set_spreadsheet_data(headers.clone(), vec![headers.iter().map(|_| "0.0000".to_string()).collect()]);
         state.rebuild_positions();
         state.apply_layout();
         let (hx, hy, hw, hh) = state.positions[PARAM_IDX];
@@ -2137,7 +2149,10 @@ mod tests {
         assert_eq!(state.positions[PARAM_IDX].2, 420.0);
         assert_eq!(state.positions[PARAM_IDX].0 + 420.0, hud.0 + hud.2, "it keeps its right edge");
 
-        // Rows that do not fit scroll, and the plate fills the HUD.
+        // Rows that do not fit scroll, and the plate fills the HUD. (A
+        // short window: the sphere's table is narrow, its plate stops short
+        // of the HUD and the HUD runs down to the playbar.)
+        state.resize(1600.0, 360.0, 1.0);
         let mut redraw = false;
         let slot = geo(&state.fs_root).children.iter().position(|c| c.node_type == "sphere").unwrap();
         state.apply_action(McpAction::Select { slot }, &mut redraw).unwrap();
@@ -18031,6 +18046,39 @@ mod tests {
         }
     }
 
+    /// The spreadsheet's plate is as wide as its table — the columns, each
+    /// as wide as its content — along the left, never narrower than an
+    /// empty table's, and no wider than the window less a gap each side,
+    /// where the table scrolls. A refill that changes the table's width
+    /// lays the plate out again, and leaves the selection alone.
+    #[test]
+    fn the_spreadsheet_plate_is_as_wide_as_its_table() {
+        use crate::slots::SPREADSHEET_IDX;
+        let mut state = State::new(false);
+        state.resize(1600.0, 900.0, 1.0);
+        if !state.show_spreadsheet {
+            state.execute_menu_action("Show Spreadsheet Pane");
+        }
+        let mut redraw = false;
+        let slot = state.current_dir().children.iter().position(|c| c.name == "sphere1").unwrap();
+        state.apply_action(McpAction::Select { slot }, &mut redraw).unwrap();
+        state.sync_nodes();
+        let table = state.slots.spreadsheet().content_width();
+        let (x, _, w, _) = state.positions[SPREADSHEET_IDX];
+        assert!(table > State::SPREADSHEET_MIN_W && table < 1600.0 - 36.0, "a sphere's table: {table}");
+        assert_eq!((x, w), (18.0, table.ceil()), "the plate is the table's width, on the left");
+        assert_eq!(cce_ui::widget::WidgetHost::rect(&state.slots.spreadsheet).2, w, "and the widget has it");
+        assert_eq!(state.param_editor_selected(), Some(slot), "the selection is as it was");
+
+        state.spreadsheet_mut().set_spreadsheet_data(vec![], vec![]);
+        state.rebuild_positions();
+        assert_eq!(state.positions[SPREADSHEET_IDX].2, State::SPREADSHEET_MIN_W, "an empty table");
+        let headers: Vec<String> = (0..60).map(|i| format!("column{i}")).collect();
+        state.spreadsheet_mut().set_spreadsheet_data(headers, vec![]);
+        state.rebuild_positions();
+        assert_eq!(state.positions[SPREADSHEET_IDX].2, 1600.0 - 36.0, "a table wider than the window fills its room");
+    }
+
     /// The playbar's cache strip, as a rule: a frame is cached when every
     /// simnet in the tree holds it, stale when one of them holds it from
     /// the chain as it was — before an edit the solve went on across, or