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

commit969f87348ea7964cc22af0c4f014fc87a03f8f75
parentd48b6223d8
authorLucas Galante <[email protected]>
date2026-09-08 15:18
feat(widgets): the relief idiom for the controls still drawn flat

Under `control_relief` (the default) a control is worked out of the plate it sits
on: a pressable is a flush inset trough or a raised plate, a field or track a well
carved into the plate with no fill of its own. Button, Toggle, Dropdown, TextBox,
Slider and Spinbox had that; these did not, and read as leftovers of the framed
dark-panel look beside them. Each gains the style as its default, with the flat
look kept behind `with_recessed(false)` / `with_raised(false)` (the FontSelector
follows the Dropdown's naming), so a `control_relief = 0` config still gets it:

- ButtonStrip: ONE well around the whole run (the menu's recess), the segments on
  its floor, the selected one a plateau raised back out of it. The segment fills
  stay plain quads — they are what reaches the flat hosts reading `extra_quads`.
  The strip now keeps clear of its detached label (it drew its segments over the
  label strip, relief or not): `sync_label` + the content rect.
- FontSelector: the closed-dropdown trough, hover/press a wash inside it; the
  picker glyph is "Aa" (the emoji rendered as tofu without an emoji font).
- KeybindRecorder: the TextBox well, rim lit in the highlight while recording.
- ColorSelector: the hex field a well (lit while editing), the swatch a raised
  bevel plate of its colour over the alpha checker; `field_relief` feeds
  ParametersBg's flat-host bridge, like Slider's `track_relief`.
- RangeSlider: the Slider's composition — no track fill, the range band on the
  floor, sphere thumbs, and the label in its carve-out tab.
- ProgressBar, UsageBar: the fill on the well floor, then the carve.
- Trackpad: a well with a faint floor instead of the framed pane.

The labeled-well carve (tab, fillet, bridge) moves out of Slider::paint into
`carve_labeled_well`, shared with RangeSlider; `detached_strip` and
`detached_label_width` with it. Tests: the legacy-geometry tests pin the flat
style explicitly; ProgressBar and UsageBar get a recessed-order test.

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

 src/widget/container/parameters_bg.rs |  14 +++
 src/widget/display/progress_bar.rs    |  59 +++++++++-
 src/widget/display/usage_bar.rs       |  41 ++++++-
 src/widget/input/button_strip.rs      |  56 +++++++++-
 src/widget/input/color_selector.rs    |  89 ++++++++++++++-
 src/widget/input/font_selector.rs     |  52 ++++++++-
 src/widget/input/keybind_recorder.rs  |  33 ++++++
 src/widget/input/slider.rs            | 205 +++++++++++++++++++++-------------
 src/widget/input/trackpad.rs          |  44 ++++++--
 9 files changed, 483 insertions(+), 110 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index f8e58c3..6800273 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1216,6 +1216,20 @@ impl ParametersBg {
                 self.spinboxes[i]
                     .as_ref()
                     .map(|w| (w as &dyn WidgetHost, crate::layout::spinbox_corner_radius(), false))
+            } else if p.2.starts_with("color") || p.2 == "rgb" || p.2 == "rgba" {
+                // The hex field's well only (`ColorSelector::field_relief`, the
+                // same geometry its paint carves); the swatch's bevel plate is
+                // a fill with its own lit edge and stays on the widget's paint.
+                if let Some(c) = &self.colors[i] {
+                    let (x, y, w, h) = c.rect();
+                    let ty = crate::widget::label_offset(c);
+                    if let Some((rx, ry, rw, rh, rr, rd)) =
+                        c.inner().field_relief(Rect { x, y: y + ty, width: w, height: h - ty })
+                    {
+                        out.push((rx, ry, rw, rh, r4(rr), rd, false, all));
+                    }
+                }
+                None
             } else if p.2.starts_with("float3") {
                 // Three standard slider rows: each row's track carve over its
                 // own row rect (an unlabeled slider's content rect is its
diff --git a/src/widget/display/progress_bar.rs b/src/widget/display/progress_bar.rs
index ce68e67..b479974 100644
--- a/src/widget/display/progress_bar.rs
+++ b/src/widget/display/progress_bar.rs
@@ -10,11 +10,24 @@ use crate::widget::{Adapted, Input, Layout, Paint};
 
 pub struct ProgressBar {
     value: f32,
+    /// Recessed-track style, the Slider's: the track is a well carved into the
+    /// plate below — no track fill, the plate is the floor — with the progress
+    /// fill inset onto that floor. Defaults to `control_relief()`; the flat
+    /// style keeps the filled, rounded track.
+    recessed: bool,
 }
 
 impl ProgressBar {
     pub fn new(value: f32) -> Adapted<ProgressBar> {
-        Adapted::new(ProgressBar { value })
+        Adapted::new(ProgressBar { value, recessed: crate::layout::control_relief() })
+    }
+}
+
+impl Adapted<ProgressBar> {
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
     }
 }
 
@@ -36,6 +49,25 @@ impl Paint for ProgressBar {
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         let radius = crate::layout::slider_corner_radius();
+        if self.recessed {
+            // The Slider's recessed composition: the fill sits on the well's flat
+            // floor (past the wall's inner half-span), the carve comes after it so
+            // the walls' shading modulates what they cross.
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            let inset = depth * 0.5;
+            let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
+            let fill_w = floor.width * self.value.clamp(0.0, 1.0);
+            if fill_w > 0.0 {
+                ctx.rounded_rect(
+                    Rect { width: fill_w, ..floor },
+                    radius.min(floor.height / 2.0),
+                    (true, true, true, true),
+                    colors::progress_fill(),
+                );
+            }
+            ctx.recess(rect, (radius, radius, radius, radius), depth);
+            return;
+        }
         // Track.
         ctx.rounded_rect(rect, radius, (true, true, true, true), colors::progress_bg());
         // Fill.
@@ -63,7 +95,7 @@ mod tests {
     #[test]
     fn reverse_bridge_matches_legacy_geometry() {
         let ctx = UiContext::new();
-        let mut bar = ProgressBar::new(0.5);
+        let mut bar = ProgressBar::new(0.5).with_recessed(false);
         WidgetHost::set_rect(&mut bar, 10.0, 20.0, 100.0, 8.0);
 
         let quads = WidgetHost::all_rounded_quads(&bar, &ctx);
@@ -80,23 +112,40 @@ mod tests {
     #[test]
     fn fill_clamps_to_track() {
         let ctx = UiContext::new();
-        let mut over = ProgressBar::new(2.0);
+        let mut over = ProgressBar::new(2.0).with_recessed(false);
         WidgetHost::set_rect(&mut over, 0.0, 0.0, 100.0, 8.0);
         let quads = WidgetHost::all_rounded_quads(&over, &ctx);
         assert_eq!(quads[1].2, 100.0, "over-1 value fills the whole track");
 
-        let mut empty = ProgressBar::new(0.0);
+        let mut empty = ProgressBar::new(0.0).with_recessed(false);
         WidgetHost::set_rect(&mut empty, 0.0, 0.0, 100.0, 8.0);
         assert_eq!(WidgetHost::all_rounded_quads(&empty, &ctx).len(), 1, "zero value emits track only");
     }
 
+    /// The recessed style draws no track of its own: the fill on the well floor, then
+    /// the carve — and nothing else, so the plate below is the floor.
+    #[test]
+    fn recessed_style_is_fill_then_carve() {
+        use crate::scene::paint::Prim;
+        let bar = ProgressBar::new(0.5).with_recessed(true);
+        let mut pc = PaintCtx::new();
+        Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
+        let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+        assert_eq!(prims.len(), 2, "fill + carve: {prims:?}");
+        assert!(matches!(prims[0], Prim::RoundedRect { .. }), "the fill first");
+        assert!(matches!(prims[1], Prim::Recess { .. }), "then the well");
+        if let Prim::RoundedRect { rect, .. } = &prims[0] {
+            assert!(rect.x > 0.0 && rect.width < 50.0, "the fill is inset onto the floor: {rect:?}");
+        }
+    }
+
     /// The detached-label convention survives the adapter: `set_rect` grows the widget by the
     /// label offset, and painting is inset below the label region (config-independent: the
     /// expected offset is derived from the observed rect).
     #[test]
     fn label_inflates_rect_and_insets_paint() {
         let ctx = UiContext::new();
-        let mut bar = ProgressBar::new(0.5).with_label("Progress");
+        let mut bar = ProgressBar::new(0.5).with_recessed(false).with_label("Progress");
         WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0);
 
         let (_, y, _, h) = WidgetHost::rect(&bar);
diff --git a/src/widget/display/usage_bar.rs b/src/widget/display/usage_bar.rs
index 57c0a93..c7e4a68 100644
--- a/src/widget/display/usage_bar.rs
+++ b/src/widget/display/usage_bar.rs
@@ -11,6 +11,10 @@ pub struct UsageBar {
     pub value: f32, // 0.0 to 1.0
     pub fill_color: [f32; 4],
     pub bg_color: [f32; 4],
+    /// Recessed-track style, the ProgressBar's: a well carved into the plate,
+    /// `bg_color` unused (the plate is the floor), the fill inset onto it.
+    /// Defaults to `control_relief()`.
+    recessed: bool,
 }
 
 impl UsageBar {
@@ -19,6 +23,7 @@ impl UsageBar {
             value: value.clamp(0.0, 1.0),
             fill_color: [0.30, 0.50, 0.32, 1.0], // green-ish
             bg_color: [0.15, 0.15, 0.24, 1.0],   // dark-ish
+            recessed: crate::layout::control_relief(),
         })
     }
 
@@ -35,6 +40,12 @@ impl Adapted<UsageBar> {
         self.bg_color = bg;
         self
     }
+
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
 }
 
 impl Layout for UsageBar {
@@ -50,6 +61,20 @@ impl Paint for UsageBar {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        if self.recessed {
+            // The ProgressBar's recessed composition: fill on the well floor, then
+            // the carve, rounded like the sliders' tracks.
+            let radius = crate::layout::slider_corner_radius();
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            let inset = depth * 0.5;
+            let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
+            let fill_w = floor.width * self.value;
+            if fill_w > 0.0 {
+                ctx.rounded_rect(Rect { width: fill_w, ..floor }, radius.min(floor.height / 2.0), (true, true, true, true), self.fill_color);
+            }
+            ctx.recess(rect, (radius, radius, radius, radius), depth);
+            return;
+        }
         ctx.quad(rect, self.bg_color);
         ctx.quad(Rect { width: rect.width * self.value, ..rect }, self.fill_color);
     }
@@ -66,7 +91,7 @@ mod tests {
     /// scaled by the clamped value.
     #[test]
     fn bridge_matches_legacy_extra_quads() {
-        let mut bar = UsageBar::new(0.5).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
+        let mut bar = UsageBar::new(0.5).with_recessed(false).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
         WidgetHost::set_rect(&mut bar, 12.0, 30.0, 200.0, 8.0);
         assert_eq!(
             WidgetHost::extra_quads(&bar),
@@ -79,6 +104,20 @@ mod tests {
         assert!(WidgetHost::all_rounded_quads(&bar, &crate::widget::UiContext::new()).is_empty());
     }
 
+    /// Recessed: no bg quad at all — the fill on the floor, then the carve.
+    #[test]
+    fn recessed_style_is_fill_then_carve() {
+        use crate::scene::paint::{PaintCtx, Prim};
+        let bar = UsageBar::new(0.5).with_recessed(true);
+        let mut pc = PaintCtx::new();
+        Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
+        let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+        assert_eq!(prims.len(), 2, "{prims:?}");
+        assert!(matches!(prims[0], Prim::RoundedRect { .. }));
+        assert!(matches!(prims[1], Prim::Recess { .. }));
+        assert!(WidgetHost::extra_quads(&bar).is_empty(), "no plain bg quad leaks to flat hosts");
+    }
+
     #[test]
     fn value_clamps_on_both_paths() {
         let bar = UsageBar::new(7.0);
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 8f37aa4..b2f977a 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -20,6 +20,15 @@ pub struct ButtonStrip {
     pub last_font: Option<String>,
     pub last_scale: Option<f32>,
     pub inherit_menubar_font: bool,
+    /// The detached control label, synced from the adapter (`Paint::sync_label`):
+    /// the strip's geometry keeps clear of the label strip above it.
+    label: Option<String>,
+    /// Recessed style: the strip is ONE well carved into the plate below (the
+    /// menu's recess around its run), the segments butting together on its
+    /// floor; the selected segment is a plateau raised back out of it, hover
+    /// and press a wash. Defaults to `control_relief()`; the flat style keeps
+    /// the plain state quads.
+    pub recessed: bool,
 }
 
 impl ButtonStrip {
@@ -41,19 +50,30 @@ impl ButtonStrip {
             last_font: None,
             last_scale: None,
             inherit_menubar_font: false,
+            label: None,
+            recessed: crate::layout::control_relief(),
         }
     }
 
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+
     pub fn with_inherit_menubar_font(mut self, inherit: bool) -> Self {
         self.inherit_menubar_font = inherit;
         self.generate_rotated_labels();
         self
     }
 
-    /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned` (or the
-    /// constructor arguments until the first layout).
+    /// The laid-out content rect: the rect mirrored from the adapter by
+    /// `Layout::rect_assigned` (or the constructor arguments until the first layout)
+    /// less the detached label strip the adapter inflated it by, so the segments,
+    /// the well and the hit-testing all sit below the label.
     fn rect(&self) -> (f32, f32, f32, f32) {
-        (self.x, self.y, self.w, self.h)
+        let strip = crate::widget::input::slider::detached_strip(&self.label);
+        (self.x, self.y + strip, self.w, (self.h - strip).max(0.0))
     }
 
     fn current_font(&self) -> String {
@@ -316,10 +336,28 @@ impl crate::widget::Paint for ButtonStrip {
         Some(self.current_font())
     }
 
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
     fn paint(&self, _rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx) {
         use crate::scene::layout::Rect;
-        // The legacy extra_quads body: per-item state backgrounds, plus the rotated
-        // (SVG-rasterized) vertical tab text clamped to the strip.
+        // The strip's well: one recess around the whole run, rounded like the
+        // buttons, before the segments so their fills sit on its floor. The
+        // segment fills stay plain quads either way — they are what reaches the
+        // flat hosts that read this widget through `extra_quads`.
+        let (sx, sy, sw, sh) = self.rect();
+        let radius = crate::layout::button_corner_radius();
+        let depth = if self.recessed {
+            let short = if self.vertical { sw } else { sh };
+            let depth = crate::layout::bevel_width().min(short * 0.2);
+            pc.recess(Rect { x: sx, y: sy, width: sw, height: sh }, (radius, radius, radius, radius), depth);
+            depth
+        } else {
+            0.0
+        };
+        // Per-item state backgrounds, plus the rotated (SVG-rasterized) vertical
+        // tab text clamped to the strip.
         for i in 0..self.buttons.len() {
             let r = self.item_rect(i);
             let mut bg_color = [0.0, 0.0, 0.0, 0.0];
@@ -333,6 +371,14 @@ impl crate::widget::Paint for ButtonStrip {
             if bg_color != [0.0, 0.0, 0.0, 0.0] {
                 pc.quad(Rect { x: r.0, y: r.1, width: r.2, height: r.3 }, bg_color);
             }
+            if self.recessed && Some(i) == self.selected {
+                // The selected segment: a plateau raised back out of the well,
+                // inset by the wall's inner half-span so it stands on the floor.
+                let inset = depth * 0.5;
+                let plateau = Rect { x: r.0 + inset, y: r.1 + inset, width: (r.2 - 2.0 * inset).max(0.0), height: (r.3 - 2.0 * inset).max(0.0) };
+                let pr = (radius - inset).max(0.0);
+                pc.boss(plateau, (pr, pr, pr, pr), depth);
+            }
 
             if self.vertical {
                 if i < self.tab_text_quads.len() {
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 03815e6..c8937a9 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -31,6 +31,11 @@ pub struct ColorSelector {
     /// `measure_text` prefix it used before reports inked extent, which drifts
     /// off the glyph advances. Empty until the first shape.
     glyph_offsets: Vec<f32>,
+    /// Recessed style: the hex field is a well carved into the plate below
+    /// (the TextBox's, rim lit while editing) and the swatch a raised bevel
+    /// plate of its colour, instead of the hairline frame and the flat swatch
+    /// with its glow. Defaults to `control_relief()`.
+    recessed: bool,
 }
 
 impl Clone for ColorSelector {
@@ -52,6 +57,7 @@ impl Clone for ColorSelector {
             live_rx: None,
             revert_hex: None,
             glyph_offsets: self.glyph_offsets.clone(),
+            recessed: self.recessed,
         }
     }
 }
@@ -75,6 +81,7 @@ impl ColorSelector {
             live_rx: None,
             revert_hex: None,
             glyph_offsets: Vec::new(),
+            recessed: crate::layout::control_relief(),
         })
     }
 
@@ -96,12 +103,34 @@ impl ColorSelector {
             live_rx: None,
             revert_hex: None,
             glyph_offsets: Vec::new(),
+            recessed: crate::layout::control_relief(),
         })
     }
 
+    /// The hex field's well for hosts that draw this control through the legacy
+    /// flat views (see `ParametersBg::reliefs`): (x, y, w, h, radius, depth) over the
+    /// widget's assigned content `rect`, or None when the style is off. The same
+    /// geometry `paint` carves (untinted).
+    pub fn field_relief(&self, rect: Rect) -> Option<(f32, f32, f32, f32, f32, f32)> {
+        if !self.recessed {
+            return None;
+        }
+        let well_h = crate::layout::color_selector_height().min(rect.height);
+        let field_w = rect.width * 0.65;
+        let radius = crate::layout::textbox_corner_radius();
+        let depth = crate::layout::bevel_width().min(well_h * 0.2);
+        Some((rect.x, rect.y, field_w, well_h, radius, depth))
+    }
+
 }
 
 impl Adapted<ColorSelector> {
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+
     pub fn with_alpha(mut self, with_alpha: bool) -> Self {
         self.with_alpha = with_alpha;
         self
@@ -221,11 +250,13 @@ impl Paint for ColorSelector {
 
         // A real frame, not a border-quad-under-fill-quad: with no fill, the
         // old full-rect border quad would read as a solid slab.
-        let bw = 1.0;
-        quads.push((rect.x, rect.y, rect.width, bw, border_color));
-        quads.push((rect.x, rect.y + visual_h - bw, rect.width, bw, border_color));
-        quads.push((rect.x, rect.y, bw, visual_h, border_color));
-        quads.push((rect.x + rect.width - bw, rect.y, bw, visual_h, border_color));
+        if !self.recessed {
+            let bw = 1.0;
+            quads.push((rect.x, rect.y, rect.width, bw, border_color));
+            quads.push((rect.x, rect.y + visual_h - bw, rect.width, bw, border_color));
+            quads.push((rect.x, rect.y, bw, visual_h, border_color));
+            quads.push((rect.x + rect.width - bw, rect.y, bw, visual_h, border_color));
+        }
 
         if self.editing {
             let font_size = 12.0;
@@ -292,6 +323,54 @@ impl Paint for ColorSelector {
             }
         };
 
+        if self.recessed {
+            // The well first (the caret quad above rides over it), then the swatch
+            // as a raised bevel plate of the colour — over a checker when the
+            // colour carries alpha, so the transparency reads through the plate.
+            for (qx, qy, qw, qh, qc) in quads.drain(..) {
+                ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+            }
+            if let Some((wx, wy, ww, wh, radius, depth)) = self.field_relief(rect) {
+                let well = Rect { x: wx, y: wy, width: ww, height: wh };
+                let radii = (radius, radius, radius, radius);
+                if self.editing {
+                    let hc = crate::color::highlight_primary_color();
+                    ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
+                } else {
+                    ctx.recess(well, radii, depth);
+                }
+            }
+            let swatch = Rect { x: px, y: py, width: pw, height: ph };
+            if self.with_alpha {
+                let mut checker = Vec::new();
+                add_rounded_rect(&mut checker, [0.8, 0.8, 0.8, 1.0], px, py, pw, ph, preview_radius);
+                let grid_size = 6.0;
+                let cols = (pw / grid_size).ceil() as i32;
+                let rows = (ph / grid_size).ceil() as i32;
+                for r in 0..rows {
+                    for c in 0..cols {
+                        if (r + c) % 2 == 1 {
+                            let qx = px + c as f32 * grid_size;
+                            let qy = py + r as f32 * grid_size;
+                            let qw = grid_size.min(px + pw - qx);
+                            let qh = grid_size.min(py + ph - qy);
+                            if qw > 0.0 && qh > 0.0 {
+                                checker.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
+                            }
+                        }
+                    }
+                }
+                for (qx, qy, qw, qh, qc) in checker {
+                    ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+                }
+            }
+            let depth = crate::layout::bevel_width().min(ph * 0.2);
+            ctx.bevel(swatch, (preview_radius, preview_radius, preview_radius, preview_radius), linear_c, depth);
+            let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
+            ctx.text(hex, rect.x + 4.0, crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0), 12.0, [0xcc, 0xcc, 0xd4]);
+            return;
+        }
+
         let steps = 6;
         for i in (1..=steps).rev() {
             let offset = i as f32 * 0.75;
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 28d68ee..3af17ec 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -1,3 +1,4 @@
+use crate::colors;
 use crate::scene::layout::{Rect, Size};
 use crate::scene::paint::PaintCtx;
 use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
@@ -15,6 +16,11 @@ pub struct FontSelector {
     pressed: bool,
     hovered: bool,
     child: Arc<Mutex<Option<std::process::Child>>>,
+    /// Raised style, the closed Dropdown's: the field is a flush inset trough
+    /// with a transparent face (the plate shows through), the hover and press
+    /// states a wash inside it. Defaults to `control_relief()`; the flat style
+    /// keeps the framed dark field.
+    raised: bool,
 }
 
 impl FontSelector {
@@ -25,6 +31,7 @@ impl FontSelector {
             pressed: false,
             hovered: false,
             child: Arc::new(Mutex::new(None)),
+            raised: crate::layout::control_relief(),
         })
     }
 
@@ -98,9 +105,12 @@ impl FontSelector {
             }
         }
 
+        // The picker glyph: "Aa", the font-picker convention, in the dropdown arrow's
+        // grey — a text glyph every face has (the emoji this drew rendered as tofu
+        // wherever no emoji font was installed).
         labels.push(TextLabel {
-            text: "🔤".to_string(),
-            x: rect.x + rect.width - 20.0,
+            text: "Aa".to_string(),
+            x: rect.x + rect.width - 24.0,
             y: crate::layout::align_text_y(rect.y, rect.height, 11.0, 0.0),
             font_size: 11.0,
             color: [0x83, 0x83, 0x8a],
@@ -110,6 +120,14 @@ impl FontSelector {
     }
 }
 
+impl Adapted<FontSelector> {
+    /// Raised style: see the `raised` field.
+    pub fn with_raised(mut self, raised: bool) -> Self {
+        self.raised = raised;
+        self
+    }
+}
+
 impl Layout for FontSelector {
     fn intrinsic_size(&self) -> Option<Size> {
         Some(Size::new(0.0, crate::layout::font_selector_height()))
@@ -135,8 +153,26 @@ impl Paint for FontSelector {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // Rounded base plate (the legacy leaf default from color + corner style)
         let r = crate::layout::font_selector_corner_radius();
+        if self.raised {
+            // The closed-dropdown chrome: a flush inset trough with a transparent
+            // face, the state fill rounded to sit inside it.
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            ctx.inset_plate(rect, (r, r, r, r), [0.0; 4], depth);
+            let wash = if self.pressed {
+                Some(colors::button_press_color())
+            } else if self.hovered {
+                Some(colors::button_hover_color())
+            } else {
+                None
+            };
+            if let Some(c) = wash {
+                ctx.rounded_rect(rect, r, (true, true, true, true), c);
+            }
+            self.paint_labels(rect, ctx);
+            return;
+        }
+        // Rounded base plate (the legacy leaf default from color + corner style)
         if r > 0.0 {
             ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
         }
@@ -156,8 +192,14 @@ impl Paint for FontSelector {
             bg_color,
         );
 
-        // Labels: family text clipped short of the picker glyph (the legacy per-label
-        // bounds), glyph unclipped.
+        self.paint_labels(rect, ctx);
+    }
+}
+
+impl FontSelector {
+    /// Labels: family text clipped short of the picker glyph (the legacy per-label
+    /// bounds), glyph unclipped.
+    fn paint_labels(&self, rect: Rect, ctx: &mut PaintCtx) {
         let font = self.widget_font();
         let clip_right = rect.x + rect.width - 24.0;
         let bounds = Some([rect.x, rect.y, clip_right, rect.y + rect.height]);
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index 9140129..8cf55db 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -14,6 +14,11 @@ pub struct KeybindRecorder {
     pub just_changed: bool,
     pressed: bool,
     hovered: bool,
+    /// Recessed style, the TextBox's: the field is a well carved into the
+    /// plate below with no fill of its own, its rim lit in the highlight
+    /// accent while recording (the TextBox's editing treatment). Defaults to
+    /// `control_relief()`; the flat style keeps the framed dark field.
+    recessed: bool,
 }
 
 impl KeybindRecorder {
@@ -24,6 +29,7 @@ impl KeybindRecorder {
             just_changed: false,
             pressed: false,
             hovered: false,
+            recessed: crate::layout::control_relief(),
         })
     }
 
@@ -40,6 +46,14 @@ impl KeybindRecorder {
     }
 }
 
+impl Adapted<KeybindRecorder> {
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+}
+
 impl Layout for KeybindRecorder {
     fn intrinsic_size(&self) -> Option<Size> {
         Some(Size::new(0.0, crate::layout::textbox_height()))
@@ -52,6 +66,19 @@ impl Paint for KeybindRecorder {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        if self.recessed {
+            let radius = crate::layout::textbox_corner_radius();
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            let radii = (radius, radius, radius, radius);
+            if self.recording {
+                let hc = crate::color::highlight_primary_color();
+                ctx.recess_tinted(rect, radii, depth, [hc[0], hc[1], hc[2]]);
+            } else {
+                ctx.recess(rect, radii, depth);
+            }
+            self.paint_text(rect, ctx);
+            return;
+        }
         let border_color = if self.recording {
             colors::HIGHLIGHT_PRIMARY
         } else if self.pressed {
@@ -67,6 +94,12 @@ impl Paint for KeybindRecorder {
             [0.08, 0.08, 0.12, 1.0],
         );
 
+        self.paint_text(rect, ctx);
+    }
+}
+
+impl KeybindRecorder {
+    fn paint_text(&self, rect: Rect, ctx: &mut PaintCtx) {
         let (display_text, color) = if self.recording {
             ("[ Press Keys... ]".to_string(), [135, 135, 153])
         } else if self.value.is_empty() {
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 08f879d..c944f78 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -176,15 +176,7 @@ impl Slider {
     /// `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
-        }
+        detached_strip(&self.label)
     }
 
     fn geom(&self, rect: Rect) -> SliderGeom {
@@ -552,76 +544,7 @@ 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 {
-            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;
-                // The labeled-Dropdown composition: pieces extend `recess_t`
-                // past interior seams (host-fade crossfade), the tab's right
-                // wall ends at the fillet's vertical tangent (or it ghosts
-                // through the arc), and a left-only bridge carries the left
-                // wall across the fillet span.
-                let fr = 6.0_f32.min(strip * 0.5);
-                let filleted = g.track_x + g.track_w - tab_r > fr + 4.0;
-                let tab_bottom = if filleted { g.y - fr } else { g.y };
-                ctx.recess_edges(
-                    Rect { x: g.track_x, y: g.y - strip, width: tab_w, height: tab_bottom - (g.y - strip) + recess_t },
-                    (radius, radius.min(strip * 0.5), 0.0, 0.0),
-                    recess_t,
-                    (true, true, false, true),
-                );
-                if filleted {
-                    ctx.recess_edges(
-                        Rect { x: g.track_x, y: g.y - fr, width: tab_w, height: fr + recess_t },
-                        (0.0, 0.0, 0.0, 0.0),
-                        recess_t,
-                        (false, false, false, true),
-                    );
-                }
-                ctx.recess_edges(track_rect, (0.0, 0.0, radius, radius), recess_t, (false, true, true, true));
-                if filleted {
-                    ctx.concave_fillet(
-                        tab_r + fr,
-                        g.y - fr,
-                        fr,
-                        recess_t,
-                        std::f32::consts::FRAC_PI_2,
-                        false,
-                    );
-                    ctx.recess_edges(
-                        Rect {
-                            x: tab_r + fr - recess_t,
-                            y: g.y,
-                            width: g.track_x + g.track_w - tab_r - fr + recess_t,
-                            height: g.h,
-                        },
-                        (0.0, radius, 0.0, 0.0),
-                        recess_t,
-                        (true, false, false, false),
-                    );
-                } else if g.track_x + g.track_w - tab_r > 0.5 {
-                    ctx.recess_edges(
-                        Rect { x: tab_r - recess_t, y: g.y, width: g.track_x + g.track_w - tab_r + recess_t, 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);
-            }
+            carve_labeled_well(ctx, track_rect, self.label_top(), detached_label_width(&self.label), radius, recess_t);
         }
 
         // Thumb. A real Circle prim, not a full-radius rounded rect: rounded
@@ -900,6 +823,10 @@ pub struct RangeSlider {
     pub(crate) active_thumb: Option<ActiveThumb>,
     drag_offset: f32,
     label: Option<String>,
+    /// Recessed-track style, the Slider's: the track is a well carved into the
+    /// plate below, the fill sits on its floor and the thumbs are spheres in the
+    /// channel. Defaults to `control_relief()`.
+    recessed: bool,
 }
 
 impl RangeSlider {
@@ -910,6 +837,7 @@ impl RangeSlider {
             active_thumb: None,
             drag_offset: 0.0,
             label: None,
+            recessed: crate::layout::control_relief(),
         })
     }
 
@@ -928,6 +856,109 @@ impl Adapted<RangeSlider> {
         self.set_values(low, high);
         self
     }
+
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+}
+
+/// The recessed track's carve, in the labeled composition shared by Slider and
+/// RangeSlider: with a detached label (`strip` > 0, the label strip's height above
+/// `track`) 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.
+/// Pieces extend `depth` past interior seams (host-fade crossfade), the tab's right
+/// wall ends at the fillet's vertical tangent (or it ghosts through the arc), and a
+/// left-only bridge carries the left wall across the fillet span. Unlabeled, one
+/// plain recess.
+pub(crate) fn carve_labeled_well(ctx: &mut PaintCtx, track: Rect, strip: f32, label_w: f32, radius: f32, depth: f32) {
+    let track_end = track.x + track.width;
+    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 inset = 4.0; // Layout::detached_label_inset — the label's x offset
+        let tab_w = (label_w + 2.0 * inset).max(2.0 * radius + 8.0).min(track.width);
+        let tab_r = track.x + tab_w;
+        // The labeled-Dropdown composition: pieces extend `depth`
+        // past interior seams (host-fade crossfade), the tab's right
+        // wall ends at the fillet's vertical tangent (or it ghosts
+        // through the arc), and a left-only bridge carries the left
+        // wall across the fillet span.
+        let fr = 6.0_f32.min(strip * 0.5);
+        let filleted = track_end - tab_r > fr + 4.0;
+        let tab_bottom = if filleted { track.y - fr } else { track.y };
+        ctx.recess_edges(
+            Rect { x: track.x, y: track.y - strip, width: tab_w, height: tab_bottom - (track.y - strip) + depth },
+            (radius, radius.min(strip * 0.5), 0.0, 0.0),
+            depth,
+            (true, true, false, true),
+        );
+        if filleted {
+            ctx.recess_edges(
+                Rect { x: track.x, y: track.y - fr, width: tab_w, height: fr + depth },
+                (0.0, 0.0, 0.0, 0.0),
+                depth,
+                (false, false, false, true),
+            );
+        }
+        ctx.recess_edges(track, (0.0, 0.0, radius, radius), depth, (false, true, true, true));
+        if filleted {
+            ctx.concave_fillet(
+                tab_r + fr,
+                track.y - fr,
+                fr,
+                depth,
+                std::f32::consts::FRAC_PI_2,
+                false,
+            );
+            ctx.recess_edges(
+                Rect {
+                    x: tab_r + fr - depth,
+                    y: track.y,
+                    width: track_end - tab_r - fr + depth,
+                    height: track.height,
+                },
+                (0.0, radius, 0.0, 0.0),
+                depth,
+                (true, false, false, false),
+            );
+        } else if track_end - tab_r > 0.5 {
+            ctx.recess_edges(
+                Rect { x: tab_r - depth, y: track.y, width: track_end - tab_r + depth, height: track.height },
+                (0.0, radius, 0.0, 0.0),
+                depth,
+                (true, false, false, false),
+            );
+        }
+    } else {
+        ctx.recess(track, (radius, radius, radius, radius), depth);
+    }
+}
+
+/// The detached-label strip height above a content rect (zero in side layout or
+/// unlabeled) — the adapter's `label_offset`, replicated for widgets that reach up
+/// into the strip to carve the label tab.
+pub(crate) fn detached_strip(label: &Option<String>) -> f32 {
+    if crate::layout::control_label_layout() == "side" {
+        return 0.0;
+    }
+    if label.is_some() {
+        let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+        font_size + crate::layout::control_label_margin()
+    } else {
+        0.0
+    }
+}
+
+/// The detached label's measured width (the tab hugs it), zero when unlabeled.
+pub(crate) fn detached_label_width(label: &Option<String>) -> f32 {
+    let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
+    label.as_deref().map(|l| crate::widget::display::measure_text_width(l, &fam, fsize)).unwrap_or(0.0)
 }
 
 impl Layout for RangeSlider {
@@ -993,7 +1024,21 @@ impl Paint for RangeSlider {
                 ctx.quad(r, c);
             }
         };
-        rrect(Rect { x, y, width: w, height: h }, radius, rc, colors::rangeslider_track(), ctx);
+        let track = Rect { x, y, width: w, height: h };
+        if self.recessed {
+            // The Slider's recessed composition: no track fill (the plate below is
+            // the well's floor), the range band on the floor, the carve after it so
+            // the walls' shading modulates what they cross, and sphere thumbs sized
+            // to the flat floor between the walls.
+            let depth = crate::layout::bevel_width().min(h * 0.2);
+            rrect(highlight, radius.min(highlight.height / 2.0), (true, true, true, true), colors::rangeslider_fill(), ctx);
+            carve_labeled_well(ctx, track, detached_strip(&self.label), detached_label_width(&self.label), radius, depth);
+            let r = (h - depth) / 2.0;
+            ctx.sphere(thumb_low_x + thumb_size / 2.0, thumb_y + thumb_size / 2.0, r, low_color);
+            ctx.sphere(thumb_high_x + thumb_size / 2.0, thumb_y + thumb_size / 2.0, r, high_color);
+            return;
+        }
+        rrect(track, radius, rc, colors::rangeslider_track(), ctx);
         rrect(highlight, radius.min(highlight.height / 2.0), (true, true, true, true), colors::rangeslider_fill(), ctx);
         rrect(
             Rect { x: thumb_low_x, y: thumb_y, width: thumb_size, height: thumb_size },
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 99e6edc..ab411a0 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -20,6 +20,10 @@ pub struct Trackpad {
     label: Option<String>,
     hovered: bool,
     pub fingers: Vec<Finger>,
+    /// Recessed style: the touch area is a well carved into the plate below,
+    /// a faint dark wash for its floor, instead of the framed dark pane.
+    /// Defaults to `control_relief()`.
+    recessed: bool,
 }
 
 impl Trackpad {
@@ -29,12 +33,24 @@ impl Trackpad {
             label: None,
             hovered: false,
             fingers: Vec::new(),
+            recessed: crate::layout::control_relief(),
         })
     }
 
     pub fn set_fingers(&mut self, fingers: Vec<Finger>) {
         self.fingers = fingers;
     }
+}
+
+impl Adapted<Trackpad> {
+    /// Recessed style: see the `recessed` field.
+    pub fn with_recessed(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
+}
+
+impl Trackpad {
 
     fn label_offset(&self) -> f32 {
         if crate::layout::control_label_layout() == "side" {
@@ -99,15 +115,25 @@ impl Paint for Trackpad {
         let _ = rect; // geometry reads the assignment cache (events/drags share it)
         let (x, y, w, visual_h) = self.touch_area();
 
-        // 1. Background
-        ctx.quad(Rect { x, y, width: w, height: visual_h }, [0.11, 0.11, 0.16, 0.85]);
-
-        // 2. Borders
-        let border_color = [0.28, 0.28, 0.38, 1.0];
-        ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_color);
-        ctx.quad(Rect { x, y: y + visual_h - 1.0, width: w, height: 1.0 }, border_color);
-        ctx.quad(Rect { x, y, width: 1.0, height: visual_h }, border_color);
-        ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: visual_h }, border_color);
+        let area = Rect { x, y, width: w, height: visual_h };
+        if self.recessed {
+            // 1+2. A well in the plate: a faint dark floor (the fingers need the
+            // contrast) and the carve around it, rounded like the text wells.
+            let radius = crate::layout::textbox_corner_radius();
+            let depth = crate::layout::bevel_width().min(visual_h * 0.2);
+            ctx.rounded_rect(area, radius, (true, true, true, true), [0.0, 0.0, 0.0, 0.18]);
+            ctx.recess(area, (radius, radius, radius, radius), depth);
+        } else {
+            // 1. Background
+            ctx.quad(area, [0.11, 0.11, 0.16, 0.85]);
+
+            // 2. Borders
+            let border_color = [0.28, 0.28, 0.38, 1.0];
+            ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_color);
+            ctx.quad(Rect { x, y: y + visual_h - 1.0, width: w, height: 1.0 }, border_color);
+            ctx.quad(Rect { x, y, width: 1.0, height: visual_h }, border_color);
+            ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: visual_h }, border_color);
+        }
 
         // 3. Fingers
         for finger in &self.fingers {