git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit71f050404980cf3ae5f3c10c50b5f2d5e7a0bb3b
parentc6e9c5ffbb
authorLucas Galante <[email protected]>
date2026-07-23 15:36
feat: labeled sliders carve their label tab too — the section idiom

The recessed slider's label now sits in the same carve-out tab the labeled
Dropdown grew: a flat recessed well hugging the label run (top/right/left
walls at the track's own depth), bottom open into the track's well, the
well's top wall picking up right of the tab's throat. Slider gains the
Dropdown's 4px detached-label inset so the text clears the tab's left wall.

The ramp's value slider becomes a real labeled slider ("Value") and the
ramp stops hand-drawing any strip labels — every labeled control in the
strip now draws its own tab.

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

 src/widget/input/ramp.rs   | 36 ++++-------------------------
 src/widget/input/slider.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 59 insertions(+), 33 deletions(-)

diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index 445658d..02da506 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -133,7 +133,9 @@ impl Ramp {
             RampKey { pos: 1.0, value: 0.5 },
         ];
         
-        let val_slider = Slider::new();
+        // Labeled like the dropdowns: the slider draws "Value" in its own
+        // carve-out tab.
+        let val_slider = Slider::new().with_label("Value");
         // A square x-icon button (cce-icons); label fallback if the icon set
         // is missing on this machine.
         let del_button = match crate::upload_icon("x", 32) {
@@ -332,33 +334,6 @@ pub fn parse_ramp_spec(spec: &str) -> Option<(Vec<(f32, f32)>, bool)> {
 }
 
 
-impl Ramp {
-    /// The ramp's OWN control labels — just Value-when-selected now: the
-    /// dropdowns carry real labels and draw them themselves (on the expanded
-    /// top wall of their inset). The Value label rides the same label line
-    /// (the dropdown rects' top).
-    fn own_control_labels(&self) -> Vec<(TextLabel, Option<String>)> {
-        let mut labels = Vec::new();
-        if self.selected_key_idx.is_some() {
-            let (_, font_size) = crate::layout::control_label_font_detached_parsed();
-            let label_color = colors::control_label_color_detached_u8();
-            let (val_x, _, _, _) = self.val_slider.rect();
-            let (_, dd_y, _, _) = self.preset_dropdown.rect();
-            labels.push((
-                TextLabel {
-                    text: "Value".to_string(),
-                    x: val_x + 4.0,
-                    y: dd_y,
-                    font_size,
-                    color: label_color,
-                },
-                self.val_slider.widget_font(),
-            ));
-        }
-        labels
-    }
-}
-
 
 impl ColorRamp {
     fn arrange_fields(&mut self) {
@@ -824,7 +799,7 @@ impl Ramp {
             let val_w = (avail - pre_w - line_w).max(40.0);
             self.preset_dropdown.set_rect(track_x, dd_y, pre_w, dd_h);
             self.line_type_dropdown.set_rect(track_x + pre_w + gap, dd_y, line_w, dd_h);
-            self.val_slider.set_rect(track_x + pre_w + line_w + 2.0 * gap, ctrl_y, val_w, ctrl_h);
+            self.val_slider.set_rect(track_x + pre_w + line_w + 2.0 * gap, dd_y, val_w, dd_h);
             self.del_button.set_rect(track_x + track_w - del_w, ctrl_y, del_w, ctrl_h);
         } else {
             // Two columns, preset the wider share.
@@ -1008,9 +983,6 @@ impl Paint for Ramp {
         }
         let depth = crate::layout::bevel_width().min(graph.height * 0.2);
         pc.recess(graph, radii, depth);
-        for (tl, font) in self.own_control_labels() {
-            pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, None);
-        }
         let dummy = UiContext::new();
         self.preset_dropdown.paint_self(&dummy, pc);
         self.line_type_dropdown.paint_self(&dummy, pc);
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 4be0574..92f12ce 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -144,6 +144,21 @@ impl Slider {
         Some((thumb_x + g.thumb_size / 2.0, thumb_y + g.thumb_size / 2.0, diameter / 2.0, color))
     }
 
+    /// The detached-label strip height above the content rect — a replica of
+    /// `Widget::label_offset` over the synced label (zero in side layout or
+    /// unlabeled).
+    fn label_top(&self) -> f32 {
+        if crate::layout::control_label_layout() == "side" {
+            return 0.0;
+        }
+        if self.label.is_some() {
+            let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+            font_size + crate::layout::control_label_margin()
+        } else {
+            0.0
+        }
+    }
+
     fn geom(&self, rect: Rect) -> SliderGeom {
         let side = side_offset(&self.label);
         let x = rect.x + side;
@@ -227,6 +242,12 @@ impl Layout for Slider {
         false // legacy Slider::set_rect stored the assigned rect verbatim
     }
 
+    /// Detached label x inset — keeps the label clear of its carve-out tab's
+    /// left wall (the Dropdown value).
+    fn detached_label_inset(&self) -> f32 {
+        4.0
+    }
+
 
     fn intrinsic_size(&self) -> Option<Size> {
         Some(Size::new(0.0, crate::layout::slider_height()))
@@ -323,7 +344,40 @@ impl Paint for Slider {
         // Carve AFTER the fill so the wall's shading modulates whatever it
         // crosses — the same order TextBox uses for its edit fill.
         if self.recessed {
-            ctx.recess(track_rect, (radius, radius, radius, radius), recess_t);
+            let strip = self.label_top();
+            if strip > 0.0 {
+                // Labeled: the label sits in a CARVE-OUT tab, the section-
+                // title idiom (the labeled Dropdown's composition) — a flat
+                // recessed well hugging the label run, bottom open into the
+                // track's well; the well's top wall picks up right of the
+                // tab's throat.
+                let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
+                let text_w = self
+                    .label
+                    .as_deref()
+                    .map(|l| crate::widget::display::measure_text_width(l, &fam, fsize))
+                    .unwrap_or(0.0);
+                let inset = 4.0; // Layout::detached_label_inset — the label's x offset
+                let tab_w = (text_w + 2.0 * inset).max(2.0 * radius + 8.0).min(g.track_w);
+                let tab_r = g.track_x + tab_w;
+                ctx.recess_edges(
+                    Rect { x: g.track_x, y: g.y - strip, width: tab_w, height: strip },
+                    (radius, radius.min(strip * 0.5), 0.0, 0.0),
+                    recess_t,
+                    (true, true, false, true),
+                );
+                ctx.recess_edges(track_rect, (0.0, 0.0, radius, radius), recess_t, (false, true, true, true));
+                if g.track_x + g.track_w - tab_r > 0.5 {
+                    ctx.recess_edges(
+                        Rect { x: tab_r, y: g.y, width: g.track_x + g.track_w - tab_r, height: g.h },
+                        (0.0, radius, 0.0, 0.0),
+                        recess_t,
+                        (true, false, false, false),
+                    );
+                }
+            } else {
+                ctx.recess(track_rect, (radius, radius, radius, radius), recess_t);
+            }
         }
 
         // Thumb. A real Circle prim, not a full-radius rounded rect: rounded