git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commitfcecf115ccf0dd9be789ee41afbfd44ca9a0efdf
parentcbf7784afe
authorLucas Galante <[email protected]>
date2026-08-15 19:35
feat: the split divider is a raised bead, not a painted hairline

Gives Prim::Ridge its first production caller in the workspace — the raised
half of the Ridge/Trough pair was previously only ever exercised by test
patches — and the shape is what a ridge is actually for. The two panes are
separate plates at the SAME level with a strip of window plate between
them, so the boundary wants a crest rising out of that strip and falling
back to it: not a step (nothing here is higher or lower) and not a painted
line (everything else in the DE is lit geometry). It is also the handle you
grab to resize, so it earns relief rather than decoration.

ONE wall enabled, not a full ring. A ridge's crest rides the whole outline,
so a closed ring on a thin tall rect gives two parallel rails and a pair of
caps instead of a bead; the rect stops at the gap's centreline with only its
right wall live, which puts a single crest mid-gap.

Depth is a QUARTER of the gap, not half. Half looks like the obvious fit
(the crest straddles its wall by ±depth/2) but the two panes' own lit rims
already spend most of that gap: at gap 12 the genuinely flat strip measures
~4 logical px. Measured against a no-bead frame, a half-gap bead spanned 9
device px and ran its bright lobe (+74) straight into the left pane's rim,
reading as one thick band; the quarter-gap bead spans 6 and sits inside the
flat strip with air either side. Size it to the CLEARANCE, not the nominal
gap.

Profile checks out as a single evaluation: +85/-17 across 6px with ONE sign
change, against the ridge control patch's +81/-19 across 8px.

The resting hairline is gone — divider_quad now returns None at rest and the
bead IS the divider. The hover/drag accent stays a quad, and since a
PageContent emits all its rects before all its reliefs, the accent tints the
strip and the bead's shading then lights it: an active divider reads as a
coloured bead rather than a line laid over one. Verified live by injecting
the pointer: rest neutral, hover bluish across all 960 rows, drag green
(rgb 201,215,204) with the divider tracking the cursor 43px.

PageContent::reliefs grows an `edges` member for this; the three box reliefs
pass all-true.

 src/main.rs      | 68 +++++++++++++++++++++++++++++++++++++++++++++++---------
 src/pages/mod.rs | 38 ++++++++++++++++++++++++++-----
 2 files changed, 89 insertions(+), 17 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f58ff5e..548ff2d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -133,6 +133,9 @@ enum WidgetFx {
     /// A flush inset control (buttons): groove ring carved down around the
     /// rect, beveled lip back up inside, face level with the surface.
     Inset(f32),
+    /// A raised crest riding the rect's boundary, with `edges` selecting which
+    /// walls exist — one wall, so one bead. The split divider.
+    Ridge { depth: f32, edges: (bool, bool, bool, bool) },
     /// A GPU-textured quad; the id comes from `cce_ui::vk::upload_rgba`
     /// (the preview pane's image). `color` is unused.
     Image { id: u32, alpha: f32 },
@@ -256,17 +259,48 @@ impl SplitPane {
         std::mem::take(&mut self.dragging)
     }
 
-    /// The divider quad (`SplitBox::extra_quads`): accent while dragging, tint on hover,
-    /// hairline otherwise.
-    fn divider_quad(&self) -> (f32, f32, f32, f32, [f32; 4]) {
+    /// The divider's active accent (`SplitBox::extra_quads`): accent while
+    /// dragging, tint on hover, and NOTHING at rest — the resting divider is
+    /// [`Self::divider_bead`], a lit crest rather than a painted hairline.
+    fn divider_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
         let (sx, sy, sw, sh) = self.divider_rect();
-        if self.dragging {
-            (sx + sw / 2.0 - 1.0, sy, 2.0, sh, [0.36, 0.56, 0.38, 0.8])
+        let color = if self.dragging {
+            [0.36, 0.56, 0.38, 0.8]
         } else if self.hovered {
-            (sx + sw / 2.0 - 1.0, sy, 2.0, sh, [0.25, 0.25, 0.32, 0.6])
+            [0.25, 0.25, 0.32, 0.6]
         } else {
-            (sx + sw / 2.0 - 0.5, sy, 1.0, sh, [0.15, 0.15, 0.18, 0.4])
-        }
+            return None;
+        };
+        Some((sx + sw / 2.0 - 1.0, sy, 2.0, sh, color))
+    }
+
+    /// The divider as relief: a raised bead running the gap's centreline.
+    ///
+    /// This is the DE's one production `Prim::Ridge` — and the shape is exactly
+    /// what a ridge is for. The two panes are separate plates at the SAME level
+    /// with a strip of window plate between them, so the boundary wants a crest
+    /// that rises out of that strip and falls back to it, not a step (nothing is
+    /// higher or lower here) and not a painted line (everything else in the DE
+    /// is lit geometry). It is also the handle you grab to resize, so it earns
+    /// relief rather than decoration.
+    ///
+    /// Returned as (rect, depth, edges) with ONE wall enabled: a ridge's crest
+    /// rides the whole outline, so a full ring on a thin tall rect would give
+    /// two parallel rails and a pair of caps, not a bead. The enabled wall is
+    /// the right one, and the rect stops at the centreline, so the crest lands
+    /// mid-gap.
+    ///
+    /// Depth is a QUARTER of the gap, not half of it. The crest straddles its
+    /// wall by ±depth/2, so half a gap looks like the obvious fit — but the two
+    /// panes are plates whose own lit rims already spend most of that gap: at
+    /// gap 12 the genuinely flat strip between them measures ~4 logical px, not
+    /// 12. A half-gap bead runs its bright lobe straight into the left pane's
+    /// rim and the two read as one thick band instead of a bead with air around
+    /// it. Size it to the CLEARANCE, not the nominal gap.
+    fn divider_bead(&self) -> ((f32, f32, f32, f32), f32, (bool, bool, bool, bool)) {
+        let (sx, sy, sw, sh) = self.divider_rect();
+        let depth = cce_ui::layout::bevel_width().min(sw * 0.25);
+        ((sx, sy, sw * 0.5, sh), depth, (false, true, false, false))
     }
 }
 
@@ -513,8 +547,18 @@ impl FilesystemApp {
                         Page::Network => &self.network_split,
                         Page::Space => &self.space_split,
                     };
-                    let (dx, dy, dw, dh, dc) = split.divider_quad();
-                    plain_pc.rects.push((dc, dx, dy, dw, dh, 0.0, (true, true, true, true)));
+                    // The bead is the divider's physical form and is always
+                    // there; the quad only marks hover/drag. Call order here is
+                    // irrelevant — display_list emits ALL of a PageContent's
+                    // rects before ALL of its reliefs — and that ordering is the
+                    // one we want: the accent tints the strip, then the bead's
+                    // shading lights it, so an active divider reads as a
+                    // coloured bead rather than a line laid over one.
+                    let ((bx, by, bw, bh), bd, bedges) = split.divider_bead();
+                    plain_pc.relief_ridge(bx, by, bw, bh, bd, bedges);
+                    if let Some((dx, dy, dw, dh, dc)) = split.divider_quad() {
+                        plain_pc.rects.push((dc, dx, dy, dw, dh, 0.0, (true, true, true, true)));
+                    }
                     let (px_r, py_r, pw_r, ph_r) = split.right_rect();
                     // The pane clamps its own text bounds to its rect inside
                     // push_prims (the old SplitBox clamp, absorbed).
@@ -750,7 +794,7 @@ impl FilesystemApp {
                     fx: WidgetFx::Image { id: *id, alpha: *alpha },
                 });
             }
-            for (rx, ry, rw, rh, rr, rd, kind) in &pc_part.reliefs {
+            for (rx, ry, rw, rh, rr, rd, kind, redges) in &pc_part.reliefs {
                 let (mut wy, mut wh) = (*ry, *rh);
                 if is_page_content {
                     match clip_to_viewport(wy, wh, content_y, content_y + content_h) {
@@ -769,6 +813,7 @@ impl FilesystemApp {
                     fx: match *kind {
                         pages::RELIEF_RAISED => WidgetFx::Boss(*rd),
                         pages::RELIEF_INSET => WidgetFx::Inset(*rd),
+                        pages::RELIEF_RIDGE => WidgetFx::Ridge { depth: *rd, edges: *redges },
                         _ => WidgetFx::Recess(*rd),
                     },
                 });
@@ -1375,6 +1420,7 @@ impl Application for FilesystemApp {
                 WidgetFx::Boss(depth) => pc.boss(rect, radii, depth),
                 WidgetFx::Recess(depth) => pc.recess(rect, radii, depth),
                 WidgetFx::Inset(depth) => pc.inset_plate(rect, radii, w.color, depth),
+                WidgetFx::Ridge { depth, edges } => pc.ridge_edges(rect, radii, depth, edges),
                 WidgetFx::Image { id, alpha } => pc.image(id, rect, alpha),
                 WidgetFx::Groove { ax, ay, bx, by, width, depth } => {
                     pc.groove((ax, ay), (bx, by), width, depth, rect)
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index a1ff07a..2f6a27a 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -125,16 +125,22 @@ pub fn dropdown_relief(pc: &mut PageContent, rect: cce_ui::scene::layout::Rect)
 pub const RELIEF_RECESSED: u8 = 0;
 pub const RELIEF_RAISED: u8 = 1;
 pub const RELIEF_INSET: u8 = 2;
+/// A raised crest riding the rect's boundary (`Prim::Ridge`) — used with a
+/// single enabled wall, which is what makes it ONE bead rather than a closed
+/// loop of them. See [`PageContent::relief_ridge`].
+pub const RELIEF_RIDGE: u8 = 3;
 
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
     pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, crate::Message)>,
     /// Relief steps for the control_relief styling — (x, y, w, h, radius, depth,
-    /// kind: [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]). The flat rects
-    /// own the faces; these are the edges-only walls emitted over them (the
+    /// kind, edges). `kind` is [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]/
+    /// [`RELIEF_RIDGE`]; `edges` is (top, right, bottom, left), which walls of the
+    /// shape actually exist — all-true for everything but a ridge bead. The flat
+    /// rects own the faces; these are the edges-only walls emitted over them (the
     /// ParametersBg::reliefs idiom for flat-view hosts).
-    pub reliefs: Vec<(f32, f32, f32, f32, f32, f32, u8)>,
+    pub reliefs: Vec<(f32, f32, f32, f32, f32, f32, u8, (bool, bool, bool, bool))>,
     /// Engraved lines over the flat rects — (ax, ay, bx, by, width, depth) plus
     /// the (x, y, w, h) of the surface being engraved, which the shading fades
     /// out against. Unlike [`reliefs`] these are not axis-aligned: this is the
@@ -191,7 +197,7 @@ impl PageContent {
     pub fn relief_recessed(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
         if cce_ui::layout::control_relief() {
             let depth = cce_ui::layout::bevel_width().min(h * 0.2);
-            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED));
+            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED, (true, true, true, true)));
         }
     }
 
@@ -200,7 +206,27 @@ impl PageContent {
     pub fn relief_raised(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
         if cce_ui::layout::control_relief() {
             let depth = cce_ui::layout::bevel_width().min(h * 0.2);
-            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RAISED));
+            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RAISED, (true, true, true, true)));
+        }
+    }
+
+    /// A raised bead riding ONE wall of (x, y, w, h) — `Prim::Ridge` with the
+    /// other three walls suppressed, so the crest is a single line rather than a
+    /// closed loop. `edges` is (top, right, bottom, left); enable exactly one.
+    ///
+    /// Depth is the caller's, not `h * 0.2` like the box reliefs: those derive it
+    /// from the control's height because they wrap a control, and a bead running
+    /// the height of a pane would take a 9.3px wall from that rule regardless of
+    /// how much room it actually has beside it. A bead is sized by its
+    /// clearance — no-op when control_relief is off.
+    pub fn relief_ridge(
+        &mut self,
+        x: f32, y: f32, w: f32, h: f32,
+        depth: f32,
+        edges: (bool, bool, bool, bool),
+    ) {
+        if cce_ui::layout::control_relief() {
+            self.reliefs.push((x, y, w, h, 0.0, depth, RELIEF_RIDGE, edges));
         }
     }
 
@@ -219,7 +245,7 @@ impl PageContent {
     pub fn relief_inset(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
         if cce_ui::layout::control_relief() {
             let depth = cce_ui::layout::bevel_width().min(h * 0.2);
-            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET));
+            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET, (true, true, true, true)));
         }
     }