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