graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat(params): the HUD stops above the spreadsheet and scrolls
The params HUD's bottom stops a gap above the spreadsheet or the
playbar when one lies below it, instead of running underneath where its
rows could be neither seen nor reached; what does not fit scrolls, and
the fitted plate fills the HUD. It is laid out again after the collapse
and detach passes, so a stubbed spreadsheet is what it stops above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 14 +++++++++----
src/app.rs | 37 ++++++++++++++++++++++++++++++-----
src/main.rs | 65 +++++++++++++++++++++++++++++++++++++++++++------------------
3 files changed, 88 insertions(+), 28 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 7237529..8e9c49e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2755,12 +2755,18 @@ right after it and under every plate, and its size has no relation to any
plate. Until this it was the right dock's pane — as wide as that dock, its
bottom raised by a spreadsheet tucked under it, tabbable and movable.
-- **Laid out from the viewport alone** (`State::params_hud_rect`): the
+- **Laid out from the viewport** (`State::params_hud_rect`): the
viewport's top-right corner a gap in, `params_hud_width` wide (its own
field, apart from the right dock's `floating_param_width`; saved as
`PlateGeometry::hud_width`, and an older save's `params_width` is read
- as it), as tall as the viewport. The playbar and the spreadsheet cover
- its bottom; nothing shortens it. Its left edge drags its width
+ as it), as tall as the viewport — but **it stops a gap above the
+ spreadsheet or the playbar when one lies below it** (since later the
+ same day; for an afternoon they covered its bottom, and rows under them
+ could be neither seen nor reached). What does not fit then scrolls, the
+ pane's own scrolling, and the fitted plate fills the HUD. Laid out again
+ after the collapse and detach post-passes in `rebuild_positions`, so a
+ stubbed spreadsheet is what it stops above. A plate in the right dock,
+ over the HUD's top, sizes nothing and is drawn over it. Its left edge drags its width
(`AppDrag::HudResize`, `on_param_resize_edge`), as far down as it claims.
The right dock's own edge is `on_right_dock_resize_edge` /
`AppDrag::RightDockResize`, asked first, its plate being on top.
@@ -2783,7 +2789,7 @@ bottom raised by a spreadsheet tucked under it, tabbable and movable.
of it, a rect at a time (`render::uncovered`, one rect most of the
time), so its labels and controls stop at a plate's edge.
-`the_params_hud_is_under_the_plates_and_sized_by_none` and
+`the_params_hud_is_under_the_plates_and_stops_above_the_bottom_ones` and
`uncovered_takes_the_covers_out_of_a_rect` are the tests. Checked in a
shadow session: the spreadsheet and playbar over the HUD's lower rows, no
label through them, the HUD's size unmoved.
diff --git a/src/app.rs b/src/app.rs
index 1f679a5..5ea07ca 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4500,16 +4500,29 @@ impl State {
w > 0.0 && cx >= x - margin && cx <= x + margin && cy >= y - margin && cy <= y + h + margin
}
- /// The params HUD's rect: laid out from the VIEWPORT alone — its top
- /// right corner, a gap in, as wide as `params_hud_width` asks and as
- /// tall as the viewport. No plate moves or sizes it; plates are drawn
- /// over it (`plates_over_params`).
+ /// The params HUD's rect: laid out from the VIEWPORT — its top right
+ /// corner, a gap in, as wide as `params_hud_width` asks — and as tall as
+ /// the viewport, except that it stops a gap above the spreadsheet or the
+ /// playbar when one lies below it (since 2026-10-06): rows under a plate
+ /// along the bottom could be neither seen nor reached, and with the HUD
+ /// stopped short the pane scrolls them instead. A plate beside or over
+ /// the HUD's top (the right dock's) sizes nothing; it is drawn over the
+ /// HUD (`plates_over_params`).
pub fn params_hud_rect(&self) -> (f32, f32, f32, f32) {
let gap = 18.0_f32;
let (vx, vy, vw, vh) = self.positions[VIEWPORT_IDX];
let (vx, vy, vw, vh) = if vw > 0.0 && vh > 0.0 { (vx, vy, vw, vh) } else { (0.0, 0.0, self.width, self.height - STATUS_H) };
let w = self.params_hud_width.clamp(PARAMS_HUD_MIN_W, (vw - 2.0 * gap).max(PARAMS_HUD_MIN_W));
- (vx + vw - gap - w, vy + gap, w, (vh - 2.0 * gap).max(100.0))
+ let (x, top) = (vx + vw - gap - w, vy + gap);
+ let mut bottom = vy + vh - gap;
+ for idx in [SPREADSHEET_IDX, PLAYBAR_IDX] {
+ let (px, py, pw, ph) = self.positions[idx];
+ let below = pw > 0.0 && ph > 0.0 && self.slots.get_dyn(idx).visible() && px < x + w && px + pw > x && py > top + PARAMS_DOT_D;
+ if below {
+ bottom = bottom.min(py - gap);
+ }
+ }
+ (x, top, w, (bottom - top).max(PARAMS_DOT_D))
}
/// The plates drawn over the params HUD, as rects: the network's
@@ -9684,6 +9697,20 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
self.apply_detached_panes();
self.apply_collapsed_panes();
+ // The params HUD stops above the spreadsheet and the playbar as
+ // they finally stand — a collapse or a detach above moved them —
+ // in the floating layout that lays it out from the viewport.
+ if !self.is_detached_network
+ && !self.detached_circular_network
+ && !self.circular_network_pane
+ && self.detached_pane.is_none()
+ && self.show_parameters
+ && !self.pane_is_stubbed(PARAM_IDX)
+ {
+ let r = self.params_hud_rect();
+ self.positions[PARAM_IDX] = r;
+ self.slots.param.set_rect(r.0, r.1, r.2, r.3);
+ }
// Last of all: the dialog floats over whatever the branches above
// produced, so its rect depends on the window and nothing else.
self.layout_dialog();
diff --git a/src/main.rs b/src/main.rs
index adea491..6443de3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2081,11 +2081,13 @@ mod tests {
assert_eq!(state.point_number_labels().len(), all.len());
}
- /// The params HUD lives on the scene: laid out from the viewport alone,
- /// so no plate moves or sizes it, and under every plate, so where one
- /// covers it the plate takes the pointer and the HUD draws nothing.
+ /// The params HUD lives on the scene: laid out from the viewport, under
+ /// every plate, so where one covers it the plate takes the pointer and
+ /// the HUD draws nothing — but it stops a gap above the spreadsheet and
+ /// the playbar along the bottom, and scrolls what does not fit. A plate
+ /// in the right dock, over its top, sizes nothing.
#[test]
- fn the_params_hud_is_under_the_plates_and_sized_by_none() {
+ fn the_params_hud_is_under_the_plates_and_stops_above_the_bottom_ones() {
use crate::app::{Dock, NO_PANE};
use crate::slots::{NETWORK_PANEL2_IDX, PARAM_IDX, PLAYBAR_IDX, SPREADSHEET_IDX};
let mut state = State::new(false);
@@ -2094,10 +2096,13 @@ mod tests {
state.apply_layout();
let hud = state.positions[PARAM_IDX];
assert_eq!(hud, state.params_hud_rect());
+ assert_eq!(hud.1 + hud.3, 900.0 - crate::app::STATUS_H - 18.0, "the viewport's height, a gap in");
assert_eq!(state.dock_of_pane(PARAM_IDX), None, "the HUD is in no dock");
assert_eq!(state.pane_in_dock(Dock::Right), NO_PANE);
- // Plates coming, going and growing leave it where it is.
+ // The spreadsheet and the playbar below it: it stops a gap above
+ // the higher of them. A plate in the right dock, and the right
+ // dock's width, size nothing.
state.execute_menu_action("Show Spreadsheet Pane");
state.execute_menu_action("Show Playbar Pane");
state.floating_spreadsheet_height = 500.0;
@@ -2106,28 +2111,50 @@ mod tests {
state.floating_param_width = 700.0;
state.rebuild_positions();
state.apply_layout();
- assert_eq!(state.positions[PARAM_IDX], hud, "no plate sizes the HUD");
- assert!(state.slots.get_dyn(PARAM_IDX).visible());
- // Its own width does.
+ let (hx, hy, hw, hh) = state.positions[PARAM_IDX];
+ let (sx, sy, sw, _) = state.positions[SPREADSHEET_IDX];
+ assert_eq!((hx, hy, hw), (hud.0, hud.1, hud.2), "only its bottom moved");
+ assert!(sx < hx + hw && sx + sw > hx, "the spreadsheet is under the HUD's span");
+ assert_eq!(hy + hh, sy - 18.0, "it stops a gap above the spreadsheet");
+ // Collapsed, the spreadsheet is a stub at its own top edge, and the
+ // HUD stops above that.
+ state.set_pane_collapsed(SPREADSHEET_IDX, true);
+ let stub = state.positions[SPREADSHEET_IDX];
+ assert_eq!(stub.1, sy);
+ let (_, hy2, _, hh2) = state.positions[PARAM_IDX];
+ assert_eq!(hy2 + hh2, stub.1 - 18.0);
+ state.set_pane_collapsed(SPREADSHEET_IDX, false);
+ // Hidden, the HUD stops above the playbar instead.
+ state.execute_menu_action("Show Spreadsheet Pane");
+ assert!(!state.show_spreadsheet);
+ let (_, hy3, _, hh3) = state.positions[PARAM_IDX];
+ assert_eq!(hy3 + hh3, state.positions[PLAYBAR_IDX].1 - 18.0, "above the playbar");
+ state.execute_menu_action("Show Spreadsheet Pane");
+ // Its own width still sizes it, about its right edge.
state.params_hud_width = 420.0;
state.rebuild_positions();
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");
- // Under every plate: where one covers the HUD, the HUD claims
- // nothing — not with its own plate on either.
+ // Rows that do not fit scroll, and the plate fills the HUD.
+ 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();
state.params_plate = true;
- let covers = state.plates_over_params();
- for idx in [SPREADSHEET_IDX, PLAYBAR_IDX, NETWORK_PANEL2_IDX] {
- assert!(covers.contains(&state.positions[idx]), "the {idx} plate is over the HUD");
- }
+ state.rebuild_positions();
+ state.apply_layout();
let (hx, hy, hw, hh) = state.positions[PARAM_IDX];
- let (sx, sy, sw, sh) = state.positions[SPREADSHEET_IDX];
- let over = (hx + hw * 0.5, (sy + sh * 0.5).min(hy + hh - 1.0));
- assert!(over.0 >= sx && over.0 < sx + sw && over.1 >= hy, "the spreadsheet runs under the HUD");
+ let pb = state.slots.get_dyn(PARAM_IDX).as_any().downcast_ref::<cce_ui::widget::ParametersBg>().unwrap();
+ assert!(pb.scrollbar_visible(), "the sphere's rows overflow a {hh} px HUD and scroll");
+ assert_eq!(state.params_claim(), (hx, hy, hw, hh), "the plate fills the HUD");
+
+ // Under the plate in the right dock: it takes the pointer there.
+ assert!(state.plates_over_params().contains(&state.positions[NETWORK_PANEL2_IDX]));
+ let (nx, ny, nw, _) = state.positions[NETWORK_PANEL2_IDX];
+ let over = (nx.max(hx) + 5.0, ny.max(hy) + 30.0);
+ assert!(over.0 < nx + nw && over.0 < hx + hw);
assert!(!state.params_claims(over.0, over.1), "a plate over the HUD takes the pointer");
- assert!(state.params_claims(hx + hw * 0.5, hy + 30.0) || state.plate_over_params_at(hx + hw * 0.5, hy + 30.0));
- assert_eq!(state.plate_at(over.0, over.1), Some(SPREADSHEET_IDX));
+ assert_eq!(state.plate_at(over.0, over.1), Some(NETWORK_PANEL2_IDX));
// An older save docked the params pane; it loads out of the dock.
let dir = std::env::temp_dir().join(format!("cce-designer-hud-dock-test-{}", std::process::id()));