git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commitc0425e62fb370b02956cef0078b7c40991add072
parent0bd837c858
authorLucas Galante <[email protected]>
date2026-08-26 15:26
fix: keep a groove between menu rows inside the shared recess

The group well made the rows one continuous face, so the buttons stopped
being distinct. They should share a recess AND stay separate: a seam
groove at every internal boundary.

Two things this needed. The walls are drawn at HALF the well's depth so
they meet at the boundary — at full depth the seam reads as two separate
hairlines about 9px apart with flat floor between them, where the well's
own ring measures a 4px dark-to-light V. And the seams are drawn AFTER
the rows rather than before: recess() is pure shading with no fill, a
hovered row fills its entire rect, and a groove straddles the boundary
between two rows — drawn underneath, the hover erased half of every
groove it touched.

Shadow-verified: rows read as distinct compartments in one well, and
hovering the middle row fills it edge to edge with both its grooves
still carved over the highlight.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/json_layout.rs | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/src/json_layout.rs b/src/json_layout.rs
index ca3d9e6..b7fcf8d 100644
--- a/src/json_layout.rs
+++ b/src/json_layout.rs
@@ -605,6 +605,7 @@ impl cce_ui::widget::Paint for JsonLayoutWidget {
         // bounding box is a single continuous well rather than one per item.
         let radius = cce_ui::layout::button_corner_radius();
         let face = cce_ui::colors::button_background_color();
+        let mut seams: Vec<(Rect, f32)> = Vec::new();
         let mut i = 0;
         while i < self.widgets.len() {
             let w = &self.widgets[i];
@@ -635,6 +636,24 @@ impl cce_ui::widget::Paint for JsonLayoutWidget {
             pc.clip(clip, |pc| {
                 pc.inset_plate(run, (radius, radius, radius, radius), face, depth);
             });
+            // Seams are collected, not drawn yet: they are pure shading and
+            // must land ON TOP of the rows. A hovered row fills its whole
+            // rect, and the groove straddles the boundary between two rows —
+            // drawn underneath, the hover would erase half of each groove it
+            // touches.
+            //
+            // Half depth so the two walls MEET at the boundary rather than
+            // leaving flat floor between them: at full depth the seam reads as
+            // two separate hairlines ~9px apart instead of one groove, against
+            // the well's own ring which measures a 4px dark-to-light V.
+            let seam_d = depth * 0.5;
+            for k in start..end {
+                let seam = self.widgets[k].y + self.widgets[k].h;
+                seams.push((
+                    Rect { x: run.x, y: seam - seam_d, width: run.width, height: 2.0 * seam_d },
+                    seam_d,
+                ));
+            }
             i = end + 1;
         }
 
@@ -661,6 +680,13 @@ impl cce_ui::widget::Paint for JsonLayoutWidget {
                 }
             });
         }
+        // Seam grooves last: pure shading, composed over the rows so a hovered
+        // row's fill cannot erase the grooves it straddles.
+        for (rect, seam_d) in seams {
+            pc.clip(clip, |pc| {
+                pc.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), seam_d, (true, false, true, false));
+            });
+        }
         for (tl, bounds) in self.own_labels_with_bounds(&dummy) {
             pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, None, bounds);
         }