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