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

commit08162e77b7cac486c6afd0b94691bdb1320d34f4
parent7b17250f4a
authorLucas Galante <[email protected]>
date2026-09-09 10:13
style: one canvas well for every opening you look into

Trackpad, Slider2D, BevelPreview and RampPreview each cut their own
opening: an opaque near-black floor on two, a translucent darkening on a
third, a base fill under that on the Trackpad, rims on two of the four,
three different radii. They now share one material through
PaintCtx::well_floor / well_rim (canvas_well): the plate darkened for the
floor (colors::WELL_FLOOR, lifted on hover for the clickable previews), the
recess carved inside the rect for the rim — drawn after the content so the
wall shades over it — and the text wells' radius. Flat falls back to the
one hairline frame. Documented as "canvas well" in the surface vocabulary.

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

 CLAUDE.md                         |  5 +++++
 src/color.rs                      |  9 +++++++++
 src/scene/paint.rs                | 34 ++++++++++++++++++++++++++++++++++
 src/widget/input/bevel_preview.rs | 10 ++++++----
 src/widget/input/ramp_preview.rs  | 11 +++++++----
 src/widget/input/slider2d.rs      | 18 ++++++++----------
 src/widget/input/trackpad.rs      | 22 +++++++---------------
 7 files changed, 76 insertions(+), 33 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 5a4a2ed..3c44a5a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -125,6 +125,11 @@ widget does not fit one of them, say so rather than stretching a word.
   the trackpad pane, the ColorSelector's recess. Things you press are plates;
   things you enter are wells. A well's floor can carry fills (a progress
   fill, a colour swatch) — those are segments of the floor, not plates.
+  A **canvas well** is a well you look into or draw in — Trackpad, Slider2D,
+  the bevel and ramp previews — and every one is cut from the same material:
+  the plate darkened for its floor (`colors::WELL_FLOOR`) and the recess for
+  its rim, through `PaintCtx::well_floor` / `well_rim` (`canvas_well`),
+  rounded like the text wells. The Ramp editor's plot is the reference look.
 - **A group is a lasso.** `widget::Group` owns nothing: it is a set of member
   ids, and its frame is the padded hull of wherever the host's layout put
   them, with a title tab flush on the top edge — the section's frame
diff --git a/src/color.rs b/src/color.rs
index 7b9b04d..4a45e3a 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -909,6 +909,15 @@ pub const SPLITTER_DRAG: [f32; 4] = [0.50, 0.50, 0.60, 1.0];
 
 pub const TEXT_FG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
 pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+
+/// A canvas well's floor: the plate darkened, not a fill of its own, so every
+/// opening you look into — Trackpad, Slider2D, the bevel and ramp previews —
+/// is cut from the one material ([`crate::scene::paint::PaintCtx::well_floor`]).
+pub const WELL_FLOOR: [f32; 4] = [0.0, 0.0, 0.0, 0.18];
+/// [`WELL_FLOOR`] risen toward the plate: a clickable canvas's hover cue.
+pub const WELL_FLOOR_LIFTED: [f32; 4] = [0.0, 0.0, 0.0, 0.10];
+/// The hairline a canvas well is framed with when relief is off.
+pub const WELL_FRAME: [f32; 4] = [0.28, 0.28, 0.38, 1.0];
 pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
 pub const TEXT_ACCENT: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
 
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 3d70cef..e1fe5cb 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1196,6 +1196,40 @@ impl PaintCtx {
         self.trough_tinted(rect, radii, depth, tint);
     }
 
+    /// A canvas well's floor — the opening you look into or draw in (a
+    /// Trackpad, a Slider2D pad, a bevel or ramp preview). It darkens the plate
+    /// it is cut from ([`crate::colors::WELL_FLOOR`]) rather than painting a
+    /// floor of its own, so every canvas sits in the one material. `lifted` is
+    /// a clickable canvas's hover cue: the floor rises toward the plate.
+    pub fn well_floor(&mut self, rect: Rect, radius: f32, lifted: bool) {
+        let tint = if lifted { crate::colors::WELL_FLOOR_LIFTED } else { crate::colors::WELL_FLOOR };
+        self.rounded_rect(rect, radius, (true, true, true, true), tint);
+    }
+
+    /// A canvas well's rim, drawn AFTER the content so the wall's shading falls
+    /// over whatever runs to the edge. Under `relief` it is the recess carved
+    /// inside `rect` ([`crate::layout::carve_inside`], the wall the DE width
+    /// capped at a fifth of the height — every well's rule); flat, the
+    /// hairline frame ([`crate::colors::WELL_FRAME`]).
+    pub fn well_rim(&mut self, rect: Rect, radius: f32, relief: bool) {
+        let radii = (radius, radius, radius, radius);
+        if relief {
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            let (well, radii) = crate::layout::carve_inside(rect, radii, depth);
+            self.recess(well, radii, depth);
+        } else {
+            self.border(rect, radii, [0.0; 4], crate::colors::WELL_FRAME, 1.0);
+        }
+    }
+
+    /// [`well_floor`](Self::well_floor) then [`well_rim`](Self::well_rim) in
+    /// one call — a canvas whose content is drawn over the rim (a Trackpad's
+    /// fingers). Content that should slide under the wall draws between the two.
+    pub fn canvas_well(&mut self, rect: Rect, radius: f32, relief: bool, lifted: bool) {
+        self.well_floor(rect, radius, lifted);
+        self.well_rim(rect, radius, relief);
+    }
+
     /// Emit one [`crate::layout::ReliefCarve`]. The shared application point:
     /// a widget's `paint` carves through here, and a flat host re-emits the
     /// carves it collected through here too, so the two can only ever draw the
diff --git a/src/widget/input/bevel_preview.rs b/src/widget/input/bevel_preview.rs
index 7dc1238..c97c84e 100644
--- a/src/widget/input/bevel_preview.rs
+++ b/src/widget/input/bevel_preview.rs
@@ -74,10 +74,10 @@ impl Paint for BevelPreview {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // The opening: a dark well, slightly lifted on hover (the click cue).
-        let radius = 4.0f32;
-        let bg = if self.hovered { [0.12, 0.12, 0.15, 1.0] } else { [0.08, 0.08, 0.10, 1.0] };
-        ctx.rounded_rect(rect, radius, (true, true, true, true), bg);
+        // The opening: the shared canvas well (`PaintCtx::well_floor`), its floor
+        // lifted on hover (the click cue); the rim is drawn last, over the content.
+        let radius = crate::layout::textbox_corner_radius();
+        ctx.well_floor(rect, radius, self.hovered);
 
         // Mini cutaway: plateau band, the wall over a square-ish domain, then
         // the floor — cce-relief's `draw_section` reduced to swatch scale.
@@ -139,6 +139,8 @@ impl Paint for BevelPreview {
             ctx.vector(prev.0, prev.1, x, y, 1.5, col, Cap::Round);
             prev = (x, y);
         }
+
+        ctx.well_rim(rect, radius, crate::layout::control_relief());
     }
 }
 
diff --git a/src/widget/input/ramp_preview.rs b/src/widget/input/ramp_preview.rs
index 482e6f6..1be4b79 100644
--- a/src/widget/input/ramp_preview.rs
+++ b/src/widget/input/ramp_preview.rs
@@ -85,10 +85,10 @@ impl Paint for RampPreview {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // The opening: a dark well, slightly lifted on hover (the click cue).
-        let radius = 4.0f32;
-        let bg = if self.hovered { [0.12, 0.12, 0.15, 1.0] } else { [0.08, 0.08, 0.10, 1.0] };
-        ctx.rounded_rect(rect, radius, (true, true, true, true), bg);
+        // The opening: the shared canvas well (`PaintCtx::well_floor`), its floor
+        // lifted on hover (the click cue); the rim is drawn last, over the content.
+        let radius = crate::layout::textbox_corner_radius();
+        ctx.well_floor(rect, radius, self.hovered);
 
         let m = 4.0f32;
         let x_l = rect.x + m;
@@ -96,6 +96,7 @@ impl Paint for RampPreview {
         let y_hi = rect.y + m;
         let y_lo = rect.y + rect.height - m;
         if x_r <= x_l || y_lo <= y_hi {
+            ctx.well_rim(rect, radius, crate::layout::control_relief());
             return;
         }
         let y_of = |v: f32| y_lo - v.clamp(0.0, 1.0) * (y_lo - y_hi);
@@ -125,6 +126,8 @@ impl Paint for RampPreview {
             ctx.vector(prev.0, prev.1, x, y, 1.5, col, Cap::Round);
             prev = (x, y);
         }
+
+        ctx.well_rim(rect, radius, crate::layout::control_relief());
     }
 }
 
diff --git a/src/widget/input/slider2d.rs b/src/widget/input/slider2d.rs
index 64cc3f8..49beac1 100644
--- a/src/widget/input/slider2d.rs
+++ b/src/widget/input/slider2d.rs
@@ -96,15 +96,15 @@ impl Paint for Slider2D {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // The well: a dark floor read as an opening cut into the host's plate
-        // (the ramp graph's look in miniature), recess rim drawn last so its
-        // shading falls over the content at the edges.
-        let radius = crate::layout::slider_corner_radius().max(4.0);
-        ctx.rounded_rect(rect, radius, (true, true, true, true), [0.08, 0.08, 0.10, 1.0]);
+        // The well: the shared canvas floor (`PaintCtx::well_floor`) read as an
+        // opening cut into the host's plate, rim drawn last so its shading
+        // falls over the content at the edges. Rounded like the text wells.
+        let radius = crate::layout::textbox_corner_radius();
+        ctx.well_floor(rect, radius, false);
 
         // Crosshair through the thumb — the pad's read of both axis values.
         let (cx, cy) = self.thumb_center(rect);
-        let line = [0.25, 0.25, 0.28, 0.6];
+        let line = [1.0, 1.0, 1.0, 0.16];
         ctx.quad(Rect { x: rect.x + 2.0, y: cy - 0.5, width: rect.width - 4.0, height: 1.0 }, line);
         ctx.quad(Rect { x: cx - 0.5, y: rect.y + 2.0, width: 1.0, height: rect.height - 4.0 }, line);
 
@@ -121,11 +121,9 @@ impl Paint for Slider2D {
             [1.0, 1.0, 1.0, 0.9],
         );
 
-        let depth = crate::layout::bevel_width().min(rect.height * 0.2);
-        // The well's ring, carved inside the pad's rect (`layout::carve_inside`); the
+        // The well's rim (`PaintCtx::well_rim`), carved inside the pad's rect; the
         // control label above is the adapter's, outside the well like every control's.
-        let (inner, radii) = crate::layout::carve_inside(rect, (radius, radius, radius, radius), depth);
-        ctx.recess(inner, radii, depth);
+        ctx.well_rim(rect, radius, crate::layout::control_relief());
     }
 }
 
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 7731bdb..be16a53 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -79,7 +79,8 @@ impl Paint for Trackpad {
     }
 
     fn color(&self) -> [f32; 4] {
-        [0.11, 0.11, 0.16, 0.85]
+        // The floor is the canvas well's (`paint`); no base fill of its own.
+        [0.0, 0.0, 0.0, 0.0]
     }
 
     fn sync_label(&mut self, label: &str) {
@@ -91,20 +92,11 @@ impl Paint for Trackpad {
         let (x, y, w, visual_h) = self.touch_area();
 
         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]);
-            let (well, radii) = crate::layout::carve_inside(area, (radius, radius, radius, radius), depth);
-            ctx.recess(well, radii, depth);
-        } else {
-            // 1+2. The framed dark pane, rounded like the text wells.
-            let radius = crate::layout::textbox_corner_radius();
-            let border_color = [0.28, 0.28, 0.38, 1.0];
-            ctx.border(area, (radius, radius, radius, radius), [0.11, 0.11, 0.16, 0.85], border_color, 1.0);
-        }
+        // 1+2. A canvas well in the plate — the floor tint and rim every draw-in
+        // opening shares (`PaintCtx::canvas_well`), rounded like the text wells;
+        // the fingers draw over the rim.
+        let radius = crate::layout::textbox_corner_radius();
+        ctx.canvas_well(area, radius, self.recessed, false);
 
         // 3. Fingers
         for finger in &self.fingers {