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

commit983f2499230e6bd49ff846fb6e37ce2fce278448
parent2e7c175397
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 19:24
feat(playbar): attach the playbar to the window's bottom edge

The playbar spans the window's full width, flush with the bottom edge,
its bottom corners the window's. The plates above stop a gap short of
its top, and the scale readout and the viewer-state line stand on it
instead of on its transport.

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

 CLAUDE.md     | 13 +++++++++++++
 src/app.rs    | 15 ++++++++++++---
 src/main.rs   | 28 ++++++++++++++++++++++++++++
 src/render.rs | 18 ++++++++++++++++--
 4 files changed, 69 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 6834135..7f372a1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3218,6 +3218,19 @@ Houdini uses: bare hjkl is the cursor, and shift+hjkl is reserved for the
 select family this app cannot implement until the Graph widget has
 multi-selection, so taking `Shift+L` now would have to be given back later.
 
+### The playbar is attached to the bottom edge (since 2026-10-06)
+
+The playbar is a bar along the window's bottom edge, the full width
+(`(0, height - STATUS_H - PLAYBAR_H, width, PLAYBAR_H)` in the floating
+layout), where it floated a gap in from the sides and the bottom like the
+other plates. Its bottom corners are the window's, read off the rect by
+`pane_plate_radii`; its top corners are a plate's. What stands above it —
+the docked plates (`pb_off`, now `PLAYBAR_H` without a gap of its own) and
+the params HUD — stops a gap short of its top, and the viewport's
+bottom-anchored text, the scale readout and a viewer state's line, stands
+on it (`State::scene_text_floor`) rather than on its transport.
+`the_playbar_is_attached_to_the_bottom_edge` is the test.
+
 ### The playbar's right-click menu
 
 A right press on the playbar's plate (`over_playbar`) opens the sixth
diff --git a/src/app.rs b/src/app.rs
index 53835e2..305056e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4715,7 +4715,9 @@ impl State {
         let ss_x = (flush_left - self.floating_spreadsheet_inset_left.max(0.0)).max(gap);
         let ss_end = (flush_right + self.floating_spreadsheet_inset_right.max(0.0)).min(self.width - gap);
         let ss_w = (ss_end - ss_x).max(150.0);
-        let pb_off = if self.show_playbar { PLAYBAR_H + gap } else { 0.0 };
+        // The playbar is attached to the bottom edge: what stands above it
+        // stops a gap short of its top, the gap the bottom edge gives it.
+        let pb_off = if self.show_playbar { PLAYBAR_H } else { 0.0 };
         let ss_y_end = self.height - STATUS_H - pb_off - gap;
         let max_h = if self.spreadsheet_tucks_left() || self.spreadsheet_tucks_right() {
             ss_y_end - (gap + 100.0 + gap)
@@ -9486,7 +9488,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.slots.playbar.set_visible(self.show_playbar);
             } else {
                 let gap = 18.0_f32;
-                let pb_off = if self.show_playbar { PLAYBAR_H + gap } else { 0.0 };
+                // The playbar is attached to the bottom edge (below), so the
+                // plates above stop a gap short of its top.
+                let pb_off = if self.show_playbar { PLAYBAR_H } else { 0.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
@@ -9635,7 +9639,12 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
                 self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
                 self.positions[PLAYBAR_IDX] = if self.show_playbar {
-                    (gap, self.height - STATUS_H - gap - PLAYBAR_H, self.width - 2.0 * gap, PLAYBAR_H)
+                    // Attached to the window's bottom edge, the full width:
+                    // its bottom corners are the window's (`pane_plate_radii`
+                    // reads them off the rect), and the plates above stop a
+                    // gap short of its top (`pb_off`). It floated a gap in
+                    // from the sides and the bottom until 2026-10-06.
+                    (0.0, self.height - STATUS_H - PLAYBAR_H, self.width, PLAYBAR_H)
                 } else {
                     (0.0, 0.0, 0.0, 0.0)
                 };
diff --git a/src/main.rs b/src/main.rs
index 0c9ef5b..36c0c44 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2081,6 +2081,34 @@ mod tests {
         assert_eq!(state.point_number_labels().len(), all.len());
     }
 
+    /// The playbar is attached to the window's bottom edge, the full width;
+    /// the plates above stop a gap short of its top, and the viewport's
+    /// bottom-anchored text stands on it rather than on its transport.
+    #[test]
+    fn the_playbar_is_attached_to_the_bottom_edge() {
+        use crate::app::{PLAYBAR_H, STATUS_H};
+        use crate::slots::{NETWORK_PANEL_IDX, PLAYBAR_IDX, SPREADSHEET_IDX};
+        let mut state = State::new(false);
+        state.resize(1600.0, 900.0, 1.0);
+        state.execute_menu_action("Show Playbar Pane");
+        state.execute_menu_action("Show Spreadsheet Pane");
+        state.rebuild_positions();
+        state.apply_layout();
+        assert!(state.show_playbar && state.show_spreadsheet);
+        let pb = state.positions[PLAYBAR_IDX];
+        assert_eq!(pb, (0.0, 900.0 - STATUS_H - PLAYBAR_H, 1600.0, PLAYBAR_H), "flush to the bottom, the full width");
+        let flags = cce_ui::scene::paint::PlateSpec::window_corner_flags(cce_ui::scene::layout::Rect { x: pb.0, y: pb.1, width: pb.2, height: pb.3 }, 1600.0, 900.0);
+        assert_eq!(flags, (false, false, true, true), "its bottom corners are the window's");
+        for idx in [NETWORK_PANEL_IDX, SPREADSHEET_IDX] {
+            let (_, y, _, h) = state.positions[idx];
+            assert_eq!(y + h, pb.1 - 18.0, "the {idx} plate stops a gap above it");
+        }
+        assert_eq!(state.scene_text_floor(900.0), pb.1, "the scale readout stands on it");
+        state.execute_menu_action("Show Playbar Pane");
+        assert!(!state.show_playbar);
+        assert_eq!(state.scene_text_floor(900.0), 900.0);
+    }
+
     /// 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
diff --git a/src/render.rs b/src/render.rs
index 81311a2..0cc439f 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1184,7 +1184,7 @@ impl State {
             text.push_str("  ·  metric assumed");
         }
         pc.clip(rect(vx, vy, vw, vh), |pc| {
-            pc.text(text, vx + 8.0, vy + vh - 16.0, 10.0, [0xaa, 0xaa, 0xbb]);
+            pc.text(text, vx + 8.0, self.scene_text_floor(vy + vh) - 16.0, 10.0, [0xaa, 0xaa, 0xbb]);
         });
     }
 
@@ -1192,6 +1192,20 @@ impl State {
     /// through the cached scene mvp (like the point numbers above), drawn as
     /// a ringed dot with its index, the control cage as faint segments
     /// between them. Selected point draws larger and brighter.
+    /// Where the viewport's bottom-anchored text stands: the viewport's
+    /// bottom, or the playbar's top while it is shown — the playbar is
+    /// attached to the window's bottom edge, over the scene, and the scale
+    /// readout and the viewer-state line would otherwise sit on its
+    /// transport.
+    pub(crate) fn scene_text_floor(&self, bottom: f32) -> f32 {
+        let (_, py, pw, ph) = self.positions[crate::slots::PLAYBAR_IDX];
+        if self.show_playbar && pw > 0.0 && ph > 0.0 {
+            bottom.min(py)
+        } else {
+            bottom
+        }
+    }
+
     fn append_viewer_state_overlay(&self, pc: &mut PaintCtx) {
         let Some(tool) = &self.viewer_tool else { return };
         if !self.show_viewport {
@@ -1254,7 +1268,7 @@ impl State {
         let hud = tool.hud();
         let size = 11.0;
         let pad = 5.0;
-        let y = vy + vh - 16.0 - (size + pad * 2.0) - 4.0;
+        let y = self.scene_text_floor(vy + vh) - 16.0 - (size + pad * 2.0) - 4.0;
         let width = (hud.chars().count() as f32 * size * 0.52 + pad * 2.0).min(vw - 16.0);
         pc.clip(rect(vx, vy, vw, vh), |pc| {
             pc.quad(rect(vx + 8.0 - pad, y, width, size + pad * 2.0), [0.0, 0.0, 0.0, 0.55]);