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

commit7b17250f4aa4e450019af96378bfdeb069a766cc
parent2724002315
authorLucas Galante <[email protected]>
date2026-09-09 10:09
style: one label font, InfoBox as a plate, Ramp pegs sized to their plot

The gallery showed two label faces: the adapter paired a detached label
with the widget's own `widget_font`, so a widget declaring none
(ProgressBar, UsageBar, StatusDot, Trackpad, the previews, ...) fell to the
engine's sans default while its neighbours read the configured
`style.control.label.font_detached`. The detached label is the adapter's,
so it now always takes that font. The same split ran through inline text:
Checkbox's label, KeybindRecorder's field (also at a hardcoded 12px, now
the TextBox's size — both are wells you type into), Trackpad's hint and
InteractiveListItem's row (now the list font) each declare their font.

InfoBox was a black card in a hairline frame; under relief it is now a
raised pane plate in the DE plate fill, its text in the label font, title
in the highlight accent. The Ramp's key pegs were a fixed 26px ring, which
on an inline ramp a control high swallowed the curve; `key_ring_r` shrinks
them to the plot (paint and both hit tests).

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

 src/widget/display/info_box.rs       | 42 ++++++++++++++++++++++++++----------
 src/widget/display/list_item.rs      |  5 +++++
 src/widget/input/checkbox.rs         |  4 ++++
 src/widget/input/keybind_recorder.rs | 10 +++++++--
 src/widget/input/ramp.rs             | 21 ++++++++++++------
 src/widget/input/trackpad.rs         |  7 +++++-
 src/widget/model.rs                  | 12 ++++++++---
 7 files changed, 78 insertions(+), 23 deletions(-)

diff --git a/src/widget/display/info_box.rs b/src/widget/display/info_box.rs
index 91c495f..8752f32 100644
--- a/src/widget/display/info_box.rs
+++ b/src/widget/display/info_box.rs
@@ -28,20 +28,40 @@ impl Paint for InfoBox {
         [0.0, 0.0, 0.0, 0.0]
     }
 
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::control_label_font())
+    }
+
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         let (x, y) = (rect.x, rect.y);
-        let theme = colors::active_theme();
-        // A card: the themed surface in a hairline frame, rounded at the plate
-        // radius like every other surface set on the plate.
         let r = crate::layout::plate_corner_radius();
-        ctx.border(rect, (r, r, r, r), theme.surface_bg, theme.surface_border, 1.0);
-
-        ctx.text(self.title.clone(), x + 16.0, y + 12.0, 12.0, [89, 165, 229]);
-        let mut current_y = y + 32.0;
-        for (idx, line) in self.lines.iter().enumerate() {
-            let color = if idx == self.lines.len() - 1 { [140, 140, 153] } else { [204, 204, 217] };
-            ctx.text(line.clone(), x + 16.0, current_y, 11.0, color);
-            current_y += 16.0;
+        let radii = (r, r, r, r);
+        if crate::layout::control_relief() {
+            // A pane plate, raised: the DE plate fill under a rolled edge —
+            // the same surface a popover or a menu stands on. Its roll is the
+            // control wall, capped by its height like every plate's.
+            let fill = colors::plate_color().unwrap_or_else(|| colors::active_theme().surface_bg);
+            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+            ctx.plate(rect, radii, fill, depth);
+        } else {
+            // Flat: the themed surface in a hairline frame.
+            let theme = colors::active_theme();
+            ctx.border(rect, radii, theme.surface_bg, theme.surface_border, 1.0);
+        }
+
+        // The title in the DE highlight accent, the lines in the label colour;
+        // sizes from the label font so the box reads like the controls around it.
+        let (_, font_size) = crate::layout::control_label_font_parsed();
+        let hc = colors::to_srgb(crate::color::highlight_primary_color());
+        let title_color = [(hc[0] * 255.0).round() as u8, (hc[1] * 255.0).round() as u8, (hc[2] * 255.0).round() as u8];
+        let line_color = colors::control_label_color_u8();
+        let pad = crate::layout::plate_padding().max(8.0);
+        let line_h = crate::layout::line_height(font_size);
+        ctx.text(self.title.clone(), x + pad, y + pad, font_size, title_color);
+        let mut current_y = y + pad + line_h * 1.4;
+        for line in &self.lines {
+            ctx.text(line.clone(), x + pad, current_y, font_size, line_color);
+            current_y += line_h;
         }
     }
 }
diff --git a/src/widget/display/list_item.rs b/src/widget/display/list_item.rs
index 085c054..2eb8c34 100644
--- a/src/widget/display/list_item.rs
+++ b/src/widget/display/list_item.rs
@@ -79,6 +79,11 @@ impl Layout for InteractiveListItem {
 }
 
 impl Paint for InteractiveListItem {
+    /// A list row's text is in the list font — the rows of a TreeList, a menu's items.
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::list_font())
+    }
+
     fn color(&self) -> [f32; 4] {
         let theme = colors::active_theme();
         if self.selected {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 07ed0db..0d2bb99 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -107,6 +107,10 @@ impl Paint for Checkbox {
         [0.0, 0.0, 0.0, 0.0]
     }
 
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::control_label_font())
+    }
+
     fn sync_label(&mut self, label: &str) {
         self.label = Some(label.to_string());
     }
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index dba1af5..6ec55f8 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -68,6 +68,11 @@ impl Paint for KeybindRecorder {
         [0.08, 0.08, 0.12, 1.0]
     }
 
+    /// The field text in the TextBox's font: both are wells you type into.
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::control_label_font_detached())
+    }
+
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         if self.recessed {
             let radius = crate::layout::textbox_corner_radius();
@@ -108,8 +113,9 @@ impl KeybindRecorder {
         } else {
             (self.value.clone(), [221, 221, 226])
         };
-        let text_y = crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0);
-        ctx.text(display_text, rect.x + 8.0, text_y, 12.0, color);
+        let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+        let text_y = crate::layout::align_text_y(rect.y, rect.height, font_size, 0.0);
+        ctx.text(display_text, rect.x + 8.0, text_y, font_size, color);
     }
 }
 
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index a37227c..e26bf95 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -791,6 +791,14 @@ impl Ramp {
     /// lands on that key.
     const KEY_RING_R: f32 = 26.0;
 
+    /// The key ring radius on THIS plot: the editor's full ring, shrunk so a
+    /// peg never outgrows the plot it sits in (an inline ramp a control high
+    /// draws pegs a few px across, not 26px discs swallowing the curve).
+    fn key_ring_r(&self) -> f32 {
+        let plot = self.plot_rect();
+        Self::KEY_RING_R.min((plot.height * 0.45).max(4.0))
+    }
+
     /// Inner margin between the graph opening's walls and the plotted 0..1
     /// domain, so the 0 and 1 gridlines (and their axis numbers) sit visibly
     /// inside the opening instead of on the walls.
@@ -1171,12 +1179,13 @@ impl Paint for Ramp {
         // stroked once, and each fill keeps to its own side.
         {
             let plot = self.plot_rect();
-            let ring_r = Self::KEY_RING_R; // roll-band centerline
+            let ring_r = self.key_ring_r(); // roll-band centerline
             // The disc surface: flat top out to the roll band's inner edge,
-            // then the rolled perimeter out to ring_r + 2.5.
+            // then the rolled perimeter out to ring_r + 2.5. Band and rim
+            // shrink with the ring so a small peg keeps a flat top.
             let base = [0.5f32, 0.75, 1.0];
-            let fill_r = ring_r - 3.0;
-            let rim_t = 6.0f32;
+            let fill_r = (ring_r - 3.0).max(ring_r * 0.5);
+            let rim_t = 6.0f32.min(ring_r * 0.25).max(1.0);
             // Bevel light: the DE light azimuth the plate shading uses.
             let az = crate::layout::light_source_position();
             let tau = std::f32::consts::TAU;
@@ -1512,7 +1521,7 @@ impl Input for Ramp {
             // Grab the NEAREST key whose ring contains the press — the rings
             // are the pegs' visual extent, and nearest-center also matches the
             // foam walls (perpendicular bisectors) where rings overlap.
-            let hit_r = Self::KEY_RING_R + 2.5;
+            let hit_r = self.key_ring_r() + 2.5;
             let mut best: Option<(usize, f32)> = None;
             for (idx, key) in self.keys.iter().enumerate() {
                 let cx = plot.x + key.pos * plot.width;
@@ -1659,7 +1668,7 @@ impl Input for Ramp {
                 // cursor. Latching also selects the key, so the pad tracks.
                 let plot = self.plot_rect();
                 if ui.scroll_gesture_new {
-                    let hit_r = Self::KEY_RING_R + 2.5;
+                    let hit_r = self.key_ring_r() + 2.5;
                     let mut best: Option<(usize, f32)> = None;
                     for (idx, key) in self.keys.iter().enumerate() {
                         let cx = plot.x + key.pos * plot.width;
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 1d740bc..7731bdb 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -74,6 +74,10 @@ impl Layout for Trackpad {
 }
 
 impl Paint for Trackpad {
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::control_label_font())
+    }
+
     fn color(&self) -> [f32; 4] {
         [0.11, 0.11, 0.16, 0.85]
     }
@@ -126,7 +130,8 @@ impl Paint for Trackpad {
             );
         }
 
-        // 4. The "Touchpad Area" hint (the control label is the adapter's).
+        // 4. The "Touchpad Area" hint (the control label is the adapter's), in
+        //    the label font like every other word a control draws.
         ctx.text(
             "Touchpad Area".to_string(),
             x + 12.0,
diff --git a/src/widget/model.rs b/src/widget/model.rs
index a4c3816..c1a1771 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -983,14 +983,20 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     }
 
     /// The paint-walk view of `own_labels_with_font_and_bounds`: prim-derived text carries the
-    /// widget's content font ([`Paint::text_font`]); the detached base label keeps
-    /// `widget_font` either way (via `own_labels_with_prim_font`).
+    /// widget's content font ([`Paint::text_font`]); the detached base label is drawn in the
+    /// configured detached-label font either way (via `own_labels_with_prim_font`).
     fn own_labels_for_walk(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
         self.own_labels_with_prim_font(ctx, Paint::text_font(&self.inner))
     }
 
     fn own_labels_with_prim_font(&self, _ctx: &UiContext, prim_font: Option<String>) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        let base_font = Paint::widget_font(&self.inner);
+        // The detached label is the adapter's, not the widget's: one font for every
+        // control's label — `style.control.label.font_detached`, whose size
+        // `base_label_fallback` already takes — whatever font the widget's own content
+        // uses (a TreeList's rows, a Breadcrumb's segments) or does not declare. A
+        // widget with no `widget_font` used to fall back to the engine's sans default
+        // here, so half the gallery's labels were in a different face.
+        let base_font = Some(crate::layout::control_label_font_detached());
         let mut fonted: Vec<(TextLabel, Option<String>)> = Vec::new();
         if self.visible() {
             fonted.extend(