graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat(playbar): the playbar is a shelf of the window's bottom edge
It was a plate laid on the edge, rolled all round: its top corners rounded
in from the window's side lips and its own roll stood beside the window's
at its ends and along its bottom. Now its plate runs out past the window's
sides and bottom with square corners, so only its top edge is rolled on
screen, and the window's own lip (factored out of the viewport arm as
append_window_lip) is drawn again over the shelf — the side lips run down
unbroken into the bottom corners. The shelf is PLAYBAR_H over the lip
(playbar_shelf_h), and the transport stands clear of the lip
(Playbar::frame).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 31 +++++++++++----
src/app.rs | 31 +++++++++++----
src/main.rs | 13 ++++++-
src/playbar.rs | 19 ++++++++-
src/render.rs | 121 +++++++++++++++++++++++++++++++++++----------------------
5 files changed, 149 insertions(+), 66 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 0f5e167..ff737d8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3224,13 +3224,30 @@ 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
+The playbar is a SHELF of the window's bottom edge, the full width
+(`(0, height - STATUS_H - playbar_shelf_h(), width, playbar_shelf_h())` in
+the floating layout), where it floated a gap in from the sides and the
+bottom like the other plates. Its height is `PLAYBAR_H` over the window's
+bottom lip (`playbar_shelf_h`, `PLAYBAR_H + bevel_width`): the shelf runs
+down INTO the lip, and only its top edge is a plate's.
+
+- **Drawn as part of the window's edge** (the PLAYBAR_IDX render arm):
+ its plate runs out past the window's sides and bottom with square
+ corners, so only the TOP is rolled on screen, and the window's own lip
+ (`State::append_window_lip`, the viewport's) is drawn again over the
+ shelf, clipped to it — so the side lips run down unbroken into the
+ bottom corners and along the bottom, as around a window with no bar.
+ For its first hours it was a plate rolled all round, its top corners
+ rounded in from the side lips, and the two rolls stood side by side at
+ its ends and along its bottom. The lip goes on after the transport (a
+ quad between a plate and its carves drops them to the overlay shading).
+- **The transport stands clear of the lip**: `Playbar::frame` is the lip's
+ width, and the widget lays its buttons, track and readout out in its
+ rect less that on the left, right and bottom (`Playbar::content`). Zero
+ in the legacy column layouts, which draw the old plate.
+
+What stands above it — the docked plates (`pb_off`, `playbar_shelf_h()`)
+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.
diff --git a/src/app.rs b/src/app.rs
index 146ac90..1a301de 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -78,6 +78,13 @@ pub fn breadcrumb_h() -> f32 {
}
pub const PLAYBAR_H: f32 = 36.0;
+/// The attached playbar's whole height: the transport's [`PLAYBAR_H`] over
+/// the window's bottom lip, which the playbar's shelf runs down into (see
+/// `Playbar::frame`).
+pub fn playbar_shelf_h() -> f32 {
+ PLAYBAR_H + cce_ui::layout::bevel_width()
+}
+
pub use crate::param::{invalid_params, is_param_name, misnamed_params, param_name_of, unknown_param_kinds, ParamDef, ParamKind, ParamSlot, ParamValue};
/// Expand a leading `~` to the home directory. A path typed into a text field
@@ -4722,7 +4729,7 @@ impl State {
let ss_w = (ss_end - ss_x).max(150.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 pb_off = if self.show_playbar { playbar_shelf_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)
@@ -9354,6 +9361,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
self.positions[PARAM_MENUBAR_IDX] = (col_r_x, HEADER_H, col_r_w, if right_visible { MENUBAR_H } else { 0.0 });
self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
self.positions[PLAYBAR_IDX] = (0.0, HEADER_H + body_h, self.width, pb_h);
+ self.slots.playbar.inner_mut().frame = 0.0;
self.slots.header.set_visible(true);
self.slots.status.set_visible(false);
@@ -9468,6 +9476,7 @@ 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] = (0.0, body_h, self.width, pb_h);
+ self.slots.playbar.inner_mut().frame = 0.0;
self.slots.header.set_visible(false);
self.slots.status.set_visible(false);
@@ -9488,7 +9497,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let gap = 18.0_f32;
// 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 };
+ let pb_off = if self.show_playbar { playbar_shelf_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
@@ -9637,15 +9646,21 @@ 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 {
- // 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)
+ // A SHELF of the window's bottom edge, the full width and
+ // down into the window's lip: its sides and bottom are the
+ // window's own edge, drawn over it, and only its top is a
+ // plate's (the PLAYBAR_IDX render arm). The transport
+ // stands clear of the lip (`Playbar::frame`), 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, and was a plate of its own laid on the
+ // window's edge, rolled all round, until later that day.
+ let h = playbar_shelf_h();
+ (0.0, self.height - STATUS_H - h, self.width, h)
} else {
(0.0, 0.0, 0.0, 0.0)
};
+ self.slots.playbar.inner_mut().frame = cce_ui::layout::bevel_width();
// Tab-aware visibility: a pane WAITING in a dock's tab list
// is hidden regardless of its View flag — a zero rect alone
diff --git a/src/main.rs b/src/main.rs
index ea19a5c..7febd41 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2110,7 +2110,7 @@ mod tests {
/// 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::app::{playbar_shelf_h, 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);
@@ -2120,9 +2120,18 @@ mod tests {
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 lip = cce_ui::layout::bevel_width();
+ assert_eq!(playbar_shelf_h(), PLAYBAR_H + lip);
+ assert_eq!(pb, (0.0, 900.0 - STATUS_H - playbar_shelf_h(), 1600.0, playbar_shelf_h()), "flush to the bottom, the full width, down into the lip");
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");
+ // A shelf of the window's edge: the transport stands clear of the
+ // window's lip, which is drawn over the shelf's sides and bottom.
+ assert_eq!(state.slots.playbar.inner().frame, lip);
+ let pb_rect = cce_ui::scene::layout::Rect { x: pb.0, y: pb.1, width: pb.2, height: pb.3 };
+ let prev = state.slots.playbar.inner().transport_button_rect(pb_rect, -1).expect("the step buttons are on");
+ assert!(prev.x >= lip, "clear of the left lip: {prev:?}");
+ assert!(prev.y + prev.height <= 900.0 - STATUS_H - lip, "clear of the bottom lip: {prev:?}");
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");
diff --git a/src/playbar.rs b/src/playbar.rs
index 9e0973a..7bf6690 100644
--- a/src/playbar.rs
+++ b/src/playbar.rs
@@ -33,6 +33,12 @@ pub struct Playbar {
/// (`playbar_step_buttons`). On by default. Off, the play button and
/// the track stand where they always did.
pub step_buttons: bool,
+ /// The window lip the transport stands clear of, on the left, the right
+ /// and the bottom: the pane is a SHELF of the window's edge (see the
+ /// PLAYBAR_IDX render arm), its rect running out to the window's sides
+ /// and bottom, and the lip is drawn over that band. Zero where the
+ /// playbar is laid out anywhere else.
+ pub frame: f32,
dragging: bool,
}
@@ -60,6 +66,7 @@ impl Playbar {
fps: 24.0,
repeat: true,
step_buttons: true,
+ frame: 0.0,
dragging: false,
})
}
@@ -88,6 +95,13 @@ impl Playbar {
self.current_frame = (self.current_frame.round() + by).clamp(self.start_frame, self.end_frame);
}
+ /// The part of the pane the transport is laid out in: the pane less the
+ /// window lip on its left, right and bottom ([`Playbar::frame`]).
+ fn content(&self, rect: Rect) -> Rect {
+ let f = self.frame;
+ Rect { x: rect.x + f, y: rect.y, width: (rect.width - 2.0 * f).max(0.0), height: (rect.height - f).max(0.0) }
+ }
+
/// The transport's square buttons, left to right: Previous Frame, Play,
/// Next Frame with the step buttons on, Play alone with them off. Glyph
/// buttons, so square at the toolkit's button height (clamped to the
@@ -118,7 +132,7 @@ impl Playbar {
0 => Btn::Play,
_ => Btn::Next,
};
- self.button_rects(rect).into_iter().find(|(b, _)| *b == want).map(|(_, r)| r)
+ self.button_rects(self.content(rect)).into_iter().find(|(b, _)| *b == want).map(|(_, r)| r)
}
fn track_rect(&self, rect: Rect) -> Rect {
@@ -185,6 +199,7 @@ impl Paint for Playbar {
if rect.width <= 0.0 || rect.height <= 0.0 {
return;
}
+ let rect = self.content(rect);
let relief = cce_ui::layout::control_relief();
let accent = colors::highlight_primary_color();
@@ -296,7 +311,7 @@ impl Input for Playbar {
}
fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
- let rect = ectx.rect;
+ let rect = self.content(ectx.rect);
match event {
Event::MouseButton { button: MouseButton::Left, state, x, y, .. } => match state {
ElementState::Pressed => {
diff --git a/src/render.rs b/src/render.rs
index 0cc439f..4806f8f 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -55,6 +55,56 @@ impl State {
/// that arc there (what a full-window root plate does in one piece).
/// Interior corners keep the widget-scale nominal plate radius, matching
/// the squircles of the sibling panes around them.
+ /// The window's own edge, over the whole window: the scene viewer's lip
+ /// (the VIEWPORT_IDX arm) and again over the playbar's shelf, which
+ /// runs down into it.
+ ///
+ /// The 3D canvas is full-bleed (CANVAS_IDX covers the window; the other
+ /// panes float over it), so the lip spans the WHOLE window with the
+ /// window radius on all four corners (the SHARED silhouette value,
+ /// concentric with the compositor clip). Under control_relief it is the
+ /// fill-less ROLL OVERLAY (negative-depth Plate): exactly the roll other
+ /// windows' root plates wear — same width, profile, crest and specular,
+ /// full band inside the silhouette — screened over what is beneath,
+ /// since a filled Plate would cover it (and a frosting fill would blur
+ /// it). It replaced the Boss rim, whose boundary-straddling wall lost its
+ /// outer half to the compositor clip: the visible band ran half a roll
+ /// wide and started at mid-slope. Focus adds the fill-less tinted Bevel
+ /// — the wrapped accent glint on the same silhouette, the network
+ /// cursor's prim — matching the focused plates' treatment (shading
+ /// unchanged, glint in accent). Without control_relief it degrades to
+ /// the flat plate-border stroke, exactly a bordered plate's
+ /// outline→relief degradation, and append_context_border owns the focus
+ /// ring.
+ fn append_window_lip(&self, pc: &mut PaintCtx) {
+ let Some(bc) = cce_ui::colors::plate_border_color() else { return };
+ let (px, py, pw, ph) = (0.0, 0.0, self.width, self.height);
+ if pw <= 0.0 || ph <= 0.0 {
+ return;
+ }
+ let vp_rect = rect(px, py, pw, ph);
+ let radii = self.pane_plate_radii(px, py, pw, ph);
+ if cce_ui::layout::control_relief() {
+ // The window-edge roll width (style.surface.relief width), read
+ // directly as the root plates of other windows read it. The
+ // interior pane plates roll over the same number through
+ // `plate_bevel_width` — one roll width since 2026-09-28; the
+ // `plate.bevel_width` key that once set theirs apart is retired.
+ let depth = cce_ui::layout::bevel_width();
+ pc.plate_spec(&cce_ui::scene::paint::PlateSpec {
+ rect: vp_rect,
+ material: cce_ui::scene::Material::opaque([0.0; 4]),
+ window_corners: (true, true, true, true),
+ depth: -depth,
+ });
+ if let Some(tint) = self.plate_focus_tint(VIEWPORT_IDX) {
+ pc.bevel_tinted(vp_rect, radii, &cce_ui::scene::Material::from_fill([0.0; 4]), depth, tint);
+ }
+ } else {
+ pc.border(vp_rect, radii, [0.0; 4], bc, cce_ui::colors::plate_border_thickness());
+ }
+ }
+
fn pane_plate_radii(&self, x: f32, y: f32, w: f32, h: f32) -> (f32, f32, f32, f32) {
// Delegates to the toolkit since RFC Phase 7b moved this math into
// PlateSpec: window-corner arcs follow the SHARED silhouette curve
@@ -371,8 +421,28 @@ impl State {
// text (a subtree painter; append_frame_text skips this slot so the
// text isn't doubled).
let (wx, wy, ww2, wh2) = w.rect();
- append_widget_plate_radii(w, pc, None, self.pane_plate_radii(wx, wy, ww2, wh2));
- w.paint_self(&self.ui_context, pc);
+ if self.slots.playbar.inner().frame > 0.0 {
+ // A SHELF of the window's bottom edge, not a plate laid on
+ // it: the plate runs out past the window's sides and bottom
+ // with square corners, so only its TOP is rolled on screen,
+ // and the window's own lip is drawn again over its band —
+ // the side lips run down unbroken into the bottom corners
+ // and along the bottom, as they do around a window with no
+ // bar. Until this the bar was a plate rolled all round,
+ // its top corners rounded in from the side lips, and the
+ // two rolls stood side by side at its ends and its bottom.
+ // The lip goes on AFTER the transport: a quad between a
+ // plate and its carves drops them to the overlay shading,
+ // and the transport stands clear of the lip
+ // (`Playbar::frame`), so nothing it draws is under it.
+ let e = cce_ui::colors::plate_bevel_width() + 1.0;
+ append_plate_at(w, pc, rect(wx - e, wy, ww2 + 2.0 * e, wh2 + e), None, (0.0, 0.0, 0.0, 0.0));
+ w.paint_self(&self.ui_context, pc);
+ pc.clip(rect(wx, wy, ww2, wh2), |pc| self.append_window_lip(pc));
+ } else {
+ append_widget_plate_radii(w, pc, None, self.pane_plate_radii(wx, wy, ww2, wh2));
+ w.paint_self(&self.ui_context, pc);
+ }
} else if idx == BREADCRUMB_IDX || idx == crate::slots::BREADCRUMB2_IDX {
// Modern-paint control: Breadcrumb's whole look lives in its
// Paint::paint() (the cce-ui restyle — per-segment plates on the
@@ -402,51 +472,8 @@ impl State {
append_widget_plate_radii(w, pc, self.plate_focus_tint(idx), self.pane_plate_radii(wx, wy, ww2, wh2));
w.paint_self(&self.ui_context, pc);
} else if idx == VIEWPORT_IDX {
- // The scene viewer's lip is the window's own root plate edge: the
- // 3D canvas is full-bleed (CANVAS_IDX covers the window; the other
- // panes float over it), so the lip spans the WHOLE window with the
- // window radius on all four corners (the SHARED silhouette value,
- // concentric with the compositor clip). Under control_relief it is
- // the fill-less ROLL OVERLAY (negative-depth Plate): exactly the
- // roll other windows' root plates wear — same width, profile,
- // crest and specular, full band inside the silhouette — screened
- // over the 3D scene, since a filled Plate would cover it (and a
- // frosting fill would blur it). It replaced the Boss rim, whose
- // boundary-straddling wall lost its outer half to the compositor
- // clip: the visible band ran half a roll wide and started at
- // mid-slope. Focus adds the fill-less tinted Bevel — the wrapped
- // accent glint on the same silhouette, the network cursor's prim —
- // matching the focused plates' treatment (shading unchanged, glint
- // in accent). Without control_relief it degrades to the flat
- // plate-border stroke, exactly a bordered plate's outline→relief
- // degradation, and append_context_border owns the focus ring.
- if let Some(bc) = cce_ui::colors::plate_border_color() {
- let (px, py, pw, ph) = (0.0, 0.0, self.width, self.height);
- if pw > 0.0 && ph > 0.0 {
- let vp_rect = rect(px, py, pw, ph);
- let radii = self.pane_plate_radii(px, py, pw, ph);
- if cce_ui::layout::control_relief() {
- // The window-edge roll width (style.surface.relief
- // width), read directly as the root plates of other
- // windows read it. The interior pane plates roll over
- // the same number through `plate_bevel_width` — one
- // roll width since 2026-09-28; the `plate.bevel_width`
- // key that once set theirs apart is retired.
- let depth = cce_ui::layout::bevel_width();
- pc.plate_spec(&cce_ui::scene::paint::PlateSpec {
- rect: vp_rect,
- material: cce_ui::scene::Material::opaque([0.0; 4]),
- window_corners: (true, true, true, true),
- depth: -depth,
- });
- if let Some(tint) = self.plate_focus_tint(idx) {
- pc.bevel_tinted(vp_rect, radii, &cce_ui::scene::Material::from_fill([0.0; 4]), depth, tint);
- }
- } else {
- pc.border(vp_rect, radii, [0.0; 4], bc, cce_ui::colors::plate_border_thickness());
- }
- }
- }
+ // The scene viewer's lip is the window's own edge.
+ self.append_window_lip(pc);
for (qx, qy, qw, qh, qc) in w.extra_quads() {
pc.quad(rect(qx, qy, qw, qh), qc);
}