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

commit80073d5da9605a90fc3376005cd66e7b887f96ec
parent08162e77b7
authorLucas Galante <[email protected]>
date2026-09-09 10:17
style: flat wells share one frame and no floor

With relief off, TextBox, KeybindRecorder, ColorSelector and FontSelector
each framed their field in their own greys and lit it in a different colour
when active (a blue, a grey, a green, a white wash), and two of them laid an
opaque near-black floor under it while the other two let the plate show.
colors::well_frame_color is now the one frame: idle and hover greys, the
highlight accent while active — the colour the relief wells light their rims
with, so both styles share the focus cue — and every flat well draws no
floor of its own, the plate being the floor as under relief. The canvas
wells' flat frame is the same hairline.

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

 CLAUDE.md                            |  3 +++
 src/color.rs                         | 24 ++++++++++++++++++++++--
 src/scene/paint.rs                   |  4 ++--
 src/widget/input/color_selector.rs   |  8 +-------
 src/widget/input/font_selector.rs    | 17 ++++++-----------
 src/widget/input/keybind_recorder.rs | 21 ++++++++-------------
 src/widget/input/text_box.rs         | 16 ++--------------
 7 files changed, 44 insertions(+), 49 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3c44a5a..33260ef 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -130,6 +130,9 @@ widget does not fit one of them, say so rather than stretching a word.
   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.
+  With relief off, a well is its frame: the one hairline
+  `colors::well_frame_color` gives every well (lit in the highlight while it
+  is active, the relief rim's focus cue), and still no floor of its own.
 - **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 4a45e3a..e848a7f 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -916,8 +916,28 @@ pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 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];
+/// The hairline a well is framed with when relief is off, idle; see
+/// [`well_frame_color`].
+pub const WELL_FRAME: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
+/// [`WELL_FRAME`] under the pointer.
+pub const WELL_FRAME_HOVER: [f32; 4] = [0.25, 0.25, 0.35, 1.0];
+
+/// The frame a flat well is drawn in — the one hairline every well (text,
+/// keybind, colour and font fields, the canvases) wears when relief is off:
+/// neutral greys idle and hovered, the highlight accent while the well is
+/// active (editing, recording, pressed) — the colour the relief wells light
+/// their rims with, so the two styles share one focus cue. A flat well has no
+/// floor of its own any more than a relief one: the plate is the floor.
+pub fn well_frame_color(hovered: bool, active: bool) -> [f32; 4] {
+    if active {
+        let c = highlight_primary_color();
+        [c[0], c[1], c[2], 1.0]
+    } else if hovered {
+        WELL_FRAME_HOVER
+    } else {
+        WELL_FRAME
+    }
+}
 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 e1fe5cb..98e6e27 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1210,7 +1210,7 @@ impl PaintCtx {
     /// 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`]).
+    /// hairline frame every well shares ([`crate::colors::well_frame_color`]).
     pub fn well_rim(&mut self, rect: Rect, radius: f32, relief: bool) {
         let radii = (radius, radius, radius, radius);
         if relief {
@@ -1218,7 +1218,7 @@ impl PaintCtx {
             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);
+            self.border(rect, radii, [0.0; 4], crate::colors::well_frame_color(false, false), 1.0);
         }
     }
 
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 5ce9e2b..5f97634 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -246,13 +246,7 @@ impl Paint for ColorSelector {
         // real text wells draw no fill, so even a neutral one read as "the
         // color selector has a background". Neutral greys for the frame; the
         // caret is the editing affordance.
-        let border_color = if self.editing {
-            [0.45, 0.45, 0.52, 1.0]
-        } else if self.hovered {
-            [0.25, 0.25, 0.35, 1.0]
-        } else {
-            [0.18, 0.18, 0.24, 1.0]
-        };
+        let border_color = crate::colors::well_frame_color(self.hovered, self.editing);
 
         // 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. Rounded at the
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 02b726b..082cc7d 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -140,7 +140,9 @@ impl Layout for FontSelector {
 
 impl Paint for FontSelector {
     fn color(&self) -> [f32; 4] {
-        [0.08, 0.08, 0.12, 1.0]
+        // The plate shows through in both styles: a transparent-faced flush
+        // plate raised, the shared well frame flat.
+        [0.0, 0.0, 0.0, 0.0]
     }
 
     fn widget_font(&self) -> Option<String> {
@@ -178,16 +180,9 @@ impl Paint for FontSelector {
             self.paint_labels(rect, ctx);
             return;
         }
-        // The flat style: the framed dark field, rounded at the selector's radius.
-        let bg_color = [0.08, 0.08, 0.12, 1.0];
-        let border_color = if self.pressed {
-            [0.30, 0.50, 0.32, 1.0]
-        } else if self.hovered {
-            [0.25, 0.25, 0.35, 1.0]
-        } else {
-            [0.18, 0.18, 0.24, 1.0]
-        };
-        ctx.border(rect, (r, r, r, r), bg_color, border_color, 1.0);
+        // The flat style: the one well frame (`colors::well_frame_color`) over
+        // the plate, lit while pressed, rounded at the selector's radius.
+        ctx.border(rect, (r, r, r, r), [0.0; 4], colors::well_frame_color(self.hovered, self.pressed), 1.0);
 
         self.paint_labels(rect, ctx);
     }
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index 6ec55f8..0b65053 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -17,7 +17,7 @@ pub struct KeybindRecorder {
     /// 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.
+    /// `control_relief()`; the flat style is the shared well frame.
     recessed: bool,
     /// Keyboard focus (FocusIn / FocusOut): Enter / Space arm recording.
     focused: bool,
@@ -65,7 +65,8 @@ impl Layout for KeybindRecorder {
 
 impl Paint for KeybindRecorder {
     fn color(&self) -> [f32; 4] {
-        [0.08, 0.08, 0.12, 1.0]
+        // A well: the plate is its floor, in both styles.
+        [0.0, 0.0, 0.0, 0.0]
     }
 
     /// The field text in the TextBox's font: both are wells you type into.
@@ -87,18 +88,12 @@ impl Paint for KeybindRecorder {
             self.paint_text(rect, ctx);
             return;
         }
-        let border_color = if self.recording {
-            colors::HIGHLIGHT_PRIMARY
-        } else if self.pressed {
-            [0.30, 0.50, 0.32, 1.0]
-        } else if self.hovered {
-            [0.25, 0.25, 0.35, 1.0]
-        } else {
-            [0.18, 0.18, 0.24, 1.0]
-        };
-        // The framed dark field, rounded like the text wells.
+        // The flat style: the one well frame (`colors::well_frame_color`), lit
+        // while recording, over the plate — no floor of its own, like the
+        // TextBox it stands beside. Rounded like the text wells.
         let radius = crate::layout::textbox_corner_radius();
-        ctx.border(rect, (radius, radius, radius, radius), [0.08, 0.08, 0.12, 1.0], border_color, 1.0);
+        let frame = colors::well_frame_color(self.hovered, self.recording || self.pressed);
+        ctx.border(rect, (radius, radius, radius, radius), [0.0; 4], frame, 1.0);
 
         self.paint_text(rect, ctx);
     }
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index a373fe3..5b98a87 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1502,13 +1502,7 @@ impl Paint for TextBox {
                     // the tinted recess rim (rounded path) / editing border, not a
                     // surface swap.
                     let bg_color = crate::colors::textbox_background_color();
-                    let border_color = if self.editing {
-                        [0.20, 0.50, 0.85, 1.0]
-                    } else if self.hovered {
-                        [0.25, 0.25, 0.35, 1.0]
-                    } else {
-                        [0.18, 0.18, 0.24, 1.0]
-                    };
+                    let border_color = crate::colors::well_frame_color(self.hovered, self.editing);
                     quads.push((self.rect.x, self.rect.y + top, self.rect.width, visual_h, border_color));
                     quads.push((self.rect.x + border_w, self.rect.y + top + border_w, self.rect.width - 2.0 * border_w, visual_h - 2.0 * border_w, bg_color));
                 }
@@ -1524,13 +1518,7 @@ impl Paint for TextBox {
 
             // One background regardless of focus (see the flat path above).
             let bg_color = crate::colors::textbox_background_color();
-            let border_color = if self.editing {
-                [0.20, 0.50, 0.85, 1.0]
-            } else if self.hovered {
-                [0.25, 0.25, 0.35, 1.0]
-            } else {
-                [0.18, 0.18, 0.24, 1.0]
-            };
+            let border_color = crate::colors::well_frame_color(self.hovered, self.editing);
 
             if self.draw_bg_border {
                 let corners = (true, true, true, true);