git.lucas.co / cce-color-editor
color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git

commitfd6eb2d69403f4b241db9a1b68126dc5a0904a72
parentd937914be1
authorLucas Galante <[email protected]>
date2026-09-24 14:46
Spacing: the ladder, not literals

Every inset and gap now comes from the cce-ui spacing ladder; the app
keeps only sizes (row height, label/readout column widths, track height,
preview and button dimensions).

- SLIDER_START_Y (12) -> root_plate_inset(): the first row's distance
  from the window's top edge.
- SLIDER_LABEL_X (12), used at both the label's left and the readout's
  right edge -> root_plate_inset(): the slider rows are now laid out
  inset from the window edge (x = inset, width = window - 2*inset), so
  the label and readout sit flush with the row rect.
- SLIDER_TRACK_X (32) and the readout's `- 68.0` -> the row inset plus
  SLIDER_LABEL_W (20) / SLIDER_VALUE_W (56), the two column widths the
  literals were hiding.
- PREVIEW_X (12) -> root_plate_inset().
- PREVIEW_Y (240) -> inset + rows * SLIDER_ROW_H + root_plate_gap().
- BUTTON_Y (324) -> preview_y + PREVIEW_H + root_plate_gap().
- BUTTON_GAP (12) -> root_plate_gap() between the two buttons.
- The hex readout's `+ 16.0` after the preview -> root_plate_gap().
- The four hard-coded window heights (324/360/428/464) -> window_height(),
  derived from the same ladder so the bottom inset matches the top.
- WELL_GAP -> WELL_CLEARANCE, marked deliberate: the well's stand-off
  from the band is carve geometry, not a sibling gap.
- The readout's `+ 26.0` vertical placement stays with a TODO(style):
  it is an alignment, not a gap.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/main.rs | 111 ++++++++++++++++++++++++++++++++++++------------------------
 1 file changed, 67 insertions(+), 44 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index cf13ebc..a904458 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,19 +10,35 @@ use wayland_client::QueueHandle;
 use std::io::IsTerminal;
 
 
+// Sizes only. Every inset and gap comes from the cce-ui spacing ladder
+// (`root_plate_inset` at the window edge, `root_plate_gap` between the
+// siblings standing on the root plate: the slider rows, the preview, the
+// buttons) — see `rebuild_layout` and `window_height`.
 const SLIDER_ROW_H: f32 = 36.0;
-const SLIDER_START_Y: f32 = 12.0;
-const SLIDER_LABEL_X: f32 = 12.0;
-const SLIDER_TRACK_X: f32 = 32.0;
+/// The label column: the channel letter's slot before the track begins.
+const SLIDER_LABEL_W: f32 = 20.0;
+/// The readout column: the right-aligned value's slot after the track ends.
+const SLIDER_VALUE_W: f32 = 56.0;
 const SLIDER_TRACK_H: f32 = 20.0;
-const PREVIEW_X: f32 = 12.0;
-const PREVIEW_Y: f32 = 240.0;
 const PREVIEW_W: f32 = 160.0;
 const PREVIEW_H: f32 = 72.0;
-const BUTTON_Y: f32 = 324.0;
 const BUTTON_H: f32 = 32.0;
 const BUTTON_W: f32 = 100.0;
-const BUTTON_GAP: f32 = 12.0;
+
+/// The window's height for a given layout: the slider rows, the preview and
+/// (when the picker reports a result) the button row, each a root-plate
+/// sibling one `root_plate_gap` apart, the whole inset from the window edge
+/// by `root_plate_inset` top and bottom.
+fn window_height(with_alpha: bool, expecting_output: bool) -> u32 {
+    let inset = cce_ui::layout::root_plate_inset();
+    let gap = cce_ui::layout::root_plate_gap();
+    let rows = if with_alpha { 7.0 } else { 6.0 };
+    let mut bottom = inset + rows * SLIDER_ROW_H + gap + PREVIEW_H;
+    if expecting_output {
+        bottom += gap + BUTTON_H;
+    }
+    (bottom + inset).ceil() as u32
+}
 
 // ── PageContent for custom Target Rendering ────────────────────────
 
@@ -101,9 +117,11 @@ impl ColorSlider {
 }
 
 /// The track's geometry within the slider's laid-out row rect (shared by paint and events).
+/// The row is already inset from the window edge; the track sits between the
+/// label column and the readout column.
 fn track_rect(rect: Rect) -> (f32, f32, f32, f32) {
-    let track_x = rect.x + SLIDER_TRACK_X;
-    let track_w = rect.width - SLIDER_TRACK_X - 68.0;
+    let track_x = rect.x + SLIDER_LABEL_W;
+    let track_w = rect.width - SLIDER_LABEL_W - SLIDER_VALUE_W;
     let track_h = SLIDER_TRACK_H;
     let track_y = rect.y + (rect.height - track_h) / 2.0;
     (track_x, track_y, track_w, track_h)
@@ -148,21 +166,24 @@ impl ColorSlider {
         // The well: shadow hugging the top contour, lit lip along the bottom
         // (DE light sits upper-left), stepped alphas riding bevel_depth.
         // 1px columns with EXACT widths — translucent quads must not overlap.
-        const WELL_GAP: f32 = 4.0;
+        // style: deliberate — the well's clearance around the band is carve
+        // geometry (how far the wall stands off the band's contour), not a gap
+        // between siblings; it has to hug the track, not follow the ladder.
+        const WELL_CLEARANCE: f32 = 4.0;
         const WELL_WALL: f32 = 3.0;
         const WALL_STEPS: usize = 3;
         let strength = (cce_ui::layout::bevel_depth() / 0.15).clamp(0.0, 2.0);
         let a_dark = 0.32 * strength;
         let a_light = 0.16 * strength;
-        let wx0 = track_x - WELL_GAP;
-        let wx1 = track_x + track_w + WELL_GAP;
+        let wx0 = track_x - WELL_CLEARANCE;
+        let wx1 = track_x + track_w + WELL_CLEARANCE;
         let cols = (wx1 - wx0).ceil().max(1.0) as i32;
         let colw = (wx1 - wx0) / cols as f32;
         let sub = WELL_WALL / WALL_STEPS as f32;
         for i in 0..cols {
             let x = wx0 + i as f32 * colw;
             let xm = (x + colw * 0.5).clamp(track_x, track_x + track_w);
-            let c = height_at(xm) * 0.5 + WELL_GAP;
+            let c = height_at(xm) * 0.5 + WELL_CLEARANCE;
             for k in 0..WALL_STEPS {
                 let fade = 1.0 - k as f32 / WALL_STEPS as f32;
                 ctx.quad(
@@ -176,8 +197,8 @@ impl ColorSlider {
             }
         }
         // End walls close the well.
-        let c0 = height_at(track_x) * 0.5 + WELL_GAP;
-        let c1 = height_at(track_x + track_w) * 0.5 + WELL_GAP;
+        let c0 = height_at(track_x) * 0.5 + WELL_CLEARANCE;
+        let c1 = height_at(track_x + track_w) * 0.5 + WELL_CLEARANCE;
         for k in 0..WALL_STEPS {
             let fade = 1.0 - k as f32 / WALL_STEPS as f32;
             ctx.quad(
@@ -235,12 +256,14 @@ impl cce_ui::widget::Paint for ColorSlider {
         // 5. Own labels: channel letter + value readout, both sitting on the
         // track's centerline (align_text_y — the stock Slider's centering),
         // the readout right-aligned so every row's value shares one flush
-        // edge, mirroring the label's left inset.
+        // edge, mirroring the label's left edge. The row rect is already
+        // inset from the window edge (root_plate_inset), so both sit flush
+        // with it.
         let label_size = 12.0;
         let value_size = 11.0;
         ctx.text(
             self.label.clone(),
-            rect.x + SLIDER_LABEL_X,
+            rect.x,
             cce_ui::layout::align_text_y(rect.y, rect.height, label_size, 0.0),
             label_size,
             [0xaa, 0xaa, 0xbb],
@@ -261,7 +284,7 @@ impl cce_ui::widget::Paint for ColorSlider {
         let vw = cce_ui::widget::display::measure_text_width(&val_str, &sans, value_size);
         ctx.text(
             val_str,
-            rect.x + rect.width - SLIDER_LABEL_X - vw,
+            rect.x + rect.width - vw,
             cce_ui::layout::align_text_y(rect.y, rect.height, value_size, 0.0),
             value_size,
             [0xcc, 0xcc, 0xdd],
@@ -524,24 +547,31 @@ impl ColorApp {
         let mut texts = Vec::new();
 
         // 1. Layout elements (root plate container DISSOLVED: widgets are top-level; its plate
-        // is emitted below as the first tuple)
-        let preview_y_offset = if self.with_alpha { SLIDER_ROW_H } else { 0.0 };
-        let preview_y = PREVIEW_Y + preview_y_offset;
-        let button_y = BUTTON_Y + preview_y_offset;
+        // is emitted below as the first tuple). Everything here stands on the root
+        // plate: inset from the window edge by root_plate_inset, the slider rows,
+        // the preview and the button row one root_plate_gap apart, the two
+        // buttons one root_plate_gap apart.
+        let inset = cce_ui::layout::root_plate_inset();
+        let gap = cce_ui::layout::root_plate_gap();
+        let content_w = self.width as f32 - 2.0 * inset;
+
+        for (i, slider) in self.sliders.iter_mut().enumerate() {
+            let row_y = inset + i as f32 * SLIDER_ROW_H;
+            slider.set_rect(inset, row_y, content_w, SLIDER_ROW_H);
+        }
 
-        let apply_x = PREVIEW_X;
-        let cancel_x = PREVIEW_X + BUTTON_W + BUTTON_GAP;
+        let preview_x = inset;
+        let preview_y = inset + self.sliders.len() as f32 * SLIDER_ROW_H + gap;
+        let button_y = preview_y + PREVIEW_H + gap;
+
+        let apply_x = inset;
+        let cancel_x = inset + BUTTON_W + gap;
 
         if self.expecting_output {
             self.apply_btn.set_rect(apply_x, button_y, BUTTON_W, BUTTON_H);
             self.cancel_btn.set_rect(cancel_x, button_y, BUTTON_W, BUTTON_H);
         }
 
-        for (i, slider) in self.sliders.iter_mut().enumerate() {
-            let row_y = SLIDER_START_Y + i as f32 * SLIDER_ROW_H;
-            slider.set_rect(0.0, row_y, self.width as f32, SLIDER_ROW_H);
-        }
-
         // 2. Each top-level widget rendered through the same immediate-mode path the root
         // recursion used: the sliders' plain gradient quads, then the rounded buttons.
         // The window base is not in this list — `display_list` emits the standard root
@@ -572,7 +602,7 @@ impl ColorApp {
 
         if self.with_alpha {
             // Draw checkerboard behind the preview box
-            let px = PREVIEW_X;
+            let px = preview_x;
             let py = preview_y;
             let pw = PREVIEW_W;
             let ph = PREVIEW_H;
@@ -604,13 +634,15 @@ impl ColorApp {
             self.blue,
             if self.with_alpha { self.alpha } else { 1.0 },
         ]);
-        custom_pc.rect(linear_col, PREVIEW_X, preview_y, PREVIEW_W, PREVIEW_H);
+        custom_pc.rect(linear_col, preview_x, preview_y, PREVIEW_W, PREVIEW_H);
 
-        // Hex string readout
+        // Hex string readout, one root_plate_gap to the right of the preview.
         let hex = self.hex();
         custom_pc.text(
             &hex,
-            PREVIEW_X + PREVIEW_W + 16.0,
+            preview_x + PREVIEW_W + gap,
+            // TODO(style): the readout's vertical placement in the preview's
+            // height is an alignment, not a gap — align_text_y is the honest form.
             preview_y + 26.0,
             16.0,
             [0.88, 0.88, 0.91, 1.0],
@@ -681,12 +713,7 @@ impl Application for ColorApp {
         let expecting_output = !std::io::stdout().is_terminal();
 
         let initial_w = 380;
-        let initial_h = if expecting_output {
-            if with_alpha { 464 } else { 428 }
-        } else {
-            if with_alpha { 360 } else { 324 }
-        };
-
+        let initial_h = window_height(with_alpha, expecting_output);
 
         let apply_btn = Button::new(0.0, 0.0, BUTTON_W, BUTTON_H)
             .with_label("Apply")
@@ -745,11 +772,7 @@ impl Application for ColorApp {
     }
 
     fn settings(&self) -> WindowSettings {
-        let win_h = if self.expecting_output {
-            if self.with_alpha { 464 } else { 428 }
-        } else {
-            if self.with_alpha { 360 } else { 324 }
-        };
+        let win_h = window_height(self.with_alpha, self.expecting_output);
         WindowSettings {
             title: "Color Editor".to_string(),
             app_id: "cce-color-editor".to_string(),