GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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 {