GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widgets): the relief idiom for the controls still drawn flat
Under `control_relief` (the default) a control is worked out of the plate it sits
on: a pressable is a flush inset trough or a raised plate, a field or track a well
carved into the plate with no fill of its own. Button, Toggle, Dropdown, TextBox,
Slider and Spinbox had that; these did not, and read as leftovers of the framed
dark-panel look beside them. Each gains the style as its default, with the flat
look kept behind `with_recessed(false)` / `with_raised(false)` (the FontSelector
follows the Dropdown's naming), so a `control_relief = 0` config still gets it:
- ButtonStrip: ONE well around the whole run (the menu's recess), the segments on
its floor, the selected one a plateau raised back out of it. The segment fills
stay plain quads — they are what reaches the flat hosts reading `extra_quads`.
The strip now keeps clear of its detached label (it drew its segments over the
label strip, relief or not): `sync_label` + the content rect.
- FontSelector: the closed-dropdown trough, hover/press a wash inside it; the
picker glyph is "Aa" (the emoji rendered as tofu without an emoji font).
- KeybindRecorder: the TextBox well, rim lit in the highlight while recording.
- ColorSelector: the hex field a well (lit while editing), the swatch a raised
bevel plate of its colour over the alpha checker; `field_relief` feeds
ParametersBg's flat-host bridge, like Slider's `track_relief`.
- RangeSlider: the Slider's composition — no track fill, the range band on the
floor, sphere thumbs, and the label in its carve-out tab.
- ProgressBar, UsageBar: the fill on the well floor, then the carve.
- Trackpad: a well with a faint floor instead of the framed pane.
The labeled-well carve (tab, fillet, bridge) moves out of Slider::paint into
`carve_labeled_well`, shared with RangeSlider; `detached_strip` and
`detached_label_width` with it. Tests: the legacy-geometry tests pin the flat
style explicitly; ProgressBar and UsageBar get a recessed-order test.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/widget/container/parameters_bg.rs | 14 +++
src/widget/display/progress_bar.rs | 59 +++++++++-
src/widget/display/usage_bar.rs | 41 ++++++-
src/widget/input/button_strip.rs | 56 +++++++++-
src/widget/input/color_selector.rs | 89 ++++++++++++++-
src/widget/input/font_selector.rs | 52 ++++++++-
src/widget/input/keybind_recorder.rs | 33 ++++++
src/widget/input/slider.rs | 205 +++++++++++++++++++++-------------
src/widget/input/trackpad.rs | 44 ++++++--
9 files changed, 483 insertions(+), 110 deletions(-)
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index f8e58c3..6800273 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1216,6 +1216,20 @@ impl ParametersBg {
self.spinboxes[i]
.as_ref()
.map(|w| (w as &dyn WidgetHost, crate::layout::spinbox_corner_radius(), false))
+ } else if p.2.starts_with("color") || p.2 == "rgb" || p.2 == "rgba" {
+ // The hex field's well only (`ColorSelector::field_relief`, the
+ // same geometry its paint carves); the swatch's bevel plate is
+ // a fill with its own lit edge and stays on the widget's paint.
+ if let Some(c) = &self.colors[i] {
+ let (x, y, w, h) = c.rect();
+ let ty = crate::widget::label_offset(c);
+ if let Some((rx, ry, rw, rh, rr, rd)) =
+ c.inner().field_relief(Rect { x, y: y + ty, width: w, height: h - ty })
+ {
+ out.push((rx, ry, rw, rh, r4(rr), rd, false, all));
+ }
+ }
+ None
} else if p.2.starts_with("float3") {
// Three standard slider rows: each row's track carve over its
// own row rect (an unlabeled slider's content rect is its
diff --git a/src/widget/display/progress_bar.rs b/src/widget/display/progress_bar.rs
index ce68e67..b479974 100644
--- a/src/widget/display/progress_bar.rs
+++ b/src/widget/display/progress_bar.rs
@@ -10,11 +10,24 @@ use crate::widget::{Adapted, Input, Layout, Paint};
pub struct ProgressBar {
value: f32,
+ /// Recessed-track style, the Slider's: the track is a well carved into the
+ /// plate below — no track fill, the plate is the floor — with the progress
+ /// fill inset onto that floor. Defaults to `control_relief()`; the flat
+ /// style keeps the filled, rounded track.
+ recessed: bool,
}
impl ProgressBar {
pub fn new(value: f32) -> Adapted<ProgressBar> {
- Adapted::new(ProgressBar { value })
+ Adapted::new(ProgressBar { value, recessed: crate::layout::control_relief() })
+ }
+}
+
+impl Adapted<ProgressBar> {
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
}
}
@@ -36,6 +49,25 @@ impl Paint for ProgressBar {
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
let radius = crate::layout::slider_corner_radius();
+ if self.recessed {
+ // The Slider's recessed composition: the fill sits on the well's flat
+ // floor (past the wall's inner half-span), the carve comes after it so
+ // the walls' shading modulates what they cross.
+ let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+ let inset = depth * 0.5;
+ let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
+ let fill_w = floor.width * self.value.clamp(0.0, 1.0);
+ if fill_w > 0.0 {
+ ctx.rounded_rect(
+ Rect { width: fill_w, ..floor },
+ radius.min(floor.height / 2.0),
+ (true, true, true, true),
+ colors::progress_fill(),
+ );
+ }
+ ctx.recess(rect, (radius, radius, radius, radius), depth);
+ return;
+ }
// Track.
ctx.rounded_rect(rect, radius, (true, true, true, true), colors::progress_bg());
// Fill.
@@ -63,7 +95,7 @@ mod tests {
#[test]
fn reverse_bridge_matches_legacy_geometry() {
let ctx = UiContext::new();
- let mut bar = ProgressBar::new(0.5);
+ let mut bar = ProgressBar::new(0.5).with_recessed(false);
WidgetHost::set_rect(&mut bar, 10.0, 20.0, 100.0, 8.0);
let quads = WidgetHost::all_rounded_quads(&bar, &ctx);
@@ -80,23 +112,40 @@ mod tests {
#[test]
fn fill_clamps_to_track() {
let ctx = UiContext::new();
- let mut over = ProgressBar::new(2.0);
+ let mut over = ProgressBar::new(2.0).with_recessed(false);
WidgetHost::set_rect(&mut over, 0.0, 0.0, 100.0, 8.0);
let quads = WidgetHost::all_rounded_quads(&over, &ctx);
assert_eq!(quads[1].2, 100.0, "over-1 value fills the whole track");
- let mut empty = ProgressBar::new(0.0);
+ let mut empty = ProgressBar::new(0.0).with_recessed(false);
WidgetHost::set_rect(&mut empty, 0.0, 0.0, 100.0, 8.0);
assert_eq!(WidgetHost::all_rounded_quads(&empty, &ctx).len(), 1, "zero value emits track only");
}
+ /// The recessed style draws no track of its own: the fill on the well floor, then
+ /// the carve — and nothing else, so the plate below is the floor.
+ #[test]
+ fn recessed_style_is_fill_then_carve() {
+ use crate::scene::paint::Prim;
+ let bar = ProgressBar::new(0.5).with_recessed(true);
+ let mut pc = PaintCtx::new();
+ Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
+ let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+ assert_eq!(prims.len(), 2, "fill + carve: {prims:?}");
+ assert!(matches!(prims[0], Prim::RoundedRect { .. }), "the fill first");
+ assert!(matches!(prims[1], Prim::Recess { .. }), "then the well");
+ if let Prim::RoundedRect { rect, .. } = &prims[0] {
+ assert!(rect.x > 0.0 && rect.width < 50.0, "the fill is inset onto the floor: {rect:?}");
+ }
+ }
+
/// The detached-label convention survives the adapter: `set_rect` grows the widget by the
/// label offset, and painting is inset below the label region (config-independent: the
/// expected offset is derived from the observed rect).
#[test]
fn label_inflates_rect_and_insets_paint() {
let ctx = UiContext::new();
- let mut bar = ProgressBar::new(0.5).with_label("Progress");
+ let mut bar = ProgressBar::new(0.5).with_recessed(false).with_label("Progress");
WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0);
let (_, y, _, h) = WidgetHost::rect(&bar);
diff --git a/src/widget/display/usage_bar.rs b/src/widget/display/usage_bar.rs
index 57c0a93..c7e4a68 100644
--- a/src/widget/display/usage_bar.rs
+++ b/src/widget/display/usage_bar.rs
@@ -11,6 +11,10 @@ pub struct UsageBar {
pub value: f32, // 0.0 to 1.0
pub fill_color: [f32; 4],
pub bg_color: [f32; 4],
+ /// Recessed-track style, the ProgressBar's: a well carved into the plate,
+ /// `bg_color` unused (the plate is the floor), the fill inset onto it.
+ /// Defaults to `control_relief()`.
+ recessed: bool,
}
impl UsageBar {
@@ -19,6 +23,7 @@ impl UsageBar {
value: value.clamp(0.0, 1.0),
fill_color: [0.30, 0.50, 0.32, 1.0], // green-ish
bg_color: [0.15, 0.15, 0.24, 1.0], // dark-ish
+ recessed: crate::layout::control_relief(),
})
}
@@ -35,6 +40,12 @@ impl Adapted<UsageBar> {
self.bg_color = bg;
self
}
+
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
}
impl Layout for UsageBar {
@@ -50,6 +61,20 @@ impl Paint for UsageBar {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ if self.recessed {
+ // The ProgressBar's recessed composition: fill on the well floor, then
+ // the carve, rounded like the sliders' tracks.
+ let radius = crate::layout::slider_corner_radius();
+ let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+ let inset = depth * 0.5;
+ let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
+ let fill_w = floor.width * self.value;
+ if fill_w > 0.0 {
+ ctx.rounded_rect(Rect { width: fill_w, ..floor }, radius.min(floor.height / 2.0), (true, true, true, true), self.fill_color);
+ }
+ ctx.recess(rect, (radius, radius, radius, radius), depth);
+ return;
+ }
ctx.quad(rect, self.bg_color);
ctx.quad(Rect { width: rect.width * self.value, ..rect }, self.fill_color);
}
@@ -66,7 +91,7 @@ mod tests {
/// scaled by the clamped value.
#[test]
fn bridge_matches_legacy_extra_quads() {
- let mut bar = UsageBar::new(0.5).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
+ let mut bar = UsageBar::new(0.5).with_recessed(false).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
WidgetHost::set_rect(&mut bar, 12.0, 30.0, 200.0, 8.0);
assert_eq!(
WidgetHost::extra_quads(&bar),
@@ -79,6 +104,20 @@ mod tests {
assert!(WidgetHost::all_rounded_quads(&bar, &crate::widget::UiContext::new()).is_empty());
}
+ /// Recessed: no bg quad at all — the fill on the floor, then the carve.
+ #[test]
+ fn recessed_style_is_fill_then_carve() {
+ use crate::scene::paint::{PaintCtx, Prim};
+ let bar = UsageBar::new(0.5).with_recessed(true);
+ let mut pc = PaintCtx::new();
+ Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
+ let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+ assert_eq!(prims.len(), 2, "{prims:?}");
+ assert!(matches!(prims[0], Prim::RoundedRect { .. }));
+ assert!(matches!(prims[1], Prim::Recess { .. }));
+ assert!(WidgetHost::extra_quads(&bar).is_empty(), "no plain bg quad leaks to flat hosts");
+ }
+
#[test]
fn value_clamps_on_both_paths() {
let bar = UsageBar::new(7.0);
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 8f37aa4..b2f977a 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -20,6 +20,15 @@ pub struct ButtonStrip {
pub last_font: Option<String>,
pub last_scale: Option<f32>,
pub inherit_menubar_font: bool,
+ /// The detached control label, synced from the adapter (`Paint::sync_label`):
+ /// the strip's geometry keeps clear of the label strip above it.
+ label: Option<String>,
+ /// Recessed style: the strip is ONE well carved into the plate below (the
+ /// menu's recess around its run), the segments butting together on its
+ /// floor; the selected segment is a plateau raised back out of it, hover
+ /// and press a wash. Defaults to `control_relief()`; the flat style keeps
+ /// the plain state quads.
+ pub recessed: bool,
}
impl ButtonStrip {
@@ -41,19 +50,30 @@ impl ButtonStrip {
last_font: None,
last_scale: None,
inherit_menubar_font: false,
+ label: None,
+ recessed: crate::layout::control_relief(),
}
}
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+
pub fn with_inherit_menubar_font(mut self, inherit: bool) -> Self {
self.inherit_menubar_font = inherit;
self.generate_rotated_labels();
self
}
- /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned` (or the
- /// constructor arguments until the first layout).
+ /// The laid-out content rect: the rect mirrored from the adapter by
+ /// `Layout::rect_assigned` (or the constructor arguments until the first layout)
+ /// less the detached label strip the adapter inflated it by, so the segments,
+ /// the well and the hit-testing all sit below the label.
fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y, self.w, self.h)
+ let strip = crate::widget::input::slider::detached_strip(&self.label);
+ (self.x, self.y + strip, self.w, (self.h - strip).max(0.0))
}
fn current_font(&self) -> String {
@@ -316,10 +336,28 @@ impl crate::widget::Paint for ButtonStrip {
Some(self.current_font())
}
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
+ }
+
fn paint(&self, _rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx) {
use crate::scene::layout::Rect;
- // The legacy extra_quads body: per-item state backgrounds, plus the rotated
- // (SVG-rasterized) vertical tab text clamped to the strip.
+ // The strip's well: one recess around the whole run, rounded like the
+ // buttons, before the segments so their fills sit on its floor. The
+ // segment fills stay plain quads either way — they are what reaches the
+ // flat hosts that read this widget through `extra_quads`.
+ let (sx, sy, sw, sh) = self.rect();
+ let radius = crate::layout::button_corner_radius();
+ let depth = if self.recessed {
+ let short = if self.vertical { sw } else { sh };
+ let depth = crate::layout::bevel_width().min(short * 0.2);
+ pc.recess(Rect { x: sx, y: sy, width: sw, height: sh }, (radius, radius, radius, radius), depth);
+ depth
+ } else {
+ 0.0
+ };
+ // Per-item state backgrounds, plus the rotated (SVG-rasterized) vertical
+ // tab text clamped to the strip.
for i in 0..self.buttons.len() {
let r = self.item_rect(i);
let mut bg_color = [0.0, 0.0, 0.0, 0.0];
@@ -333,6 +371,14 @@ impl crate::widget::Paint for ButtonStrip {
if bg_color != [0.0, 0.0, 0.0, 0.0] {
pc.quad(Rect { x: r.0, y: r.1, width: r.2, height: r.3 }, bg_color);
}
+ if self.recessed && Some(i) == self.selected {
+ // The selected segment: a plateau raised back out of the well,
+ // inset by the wall's inner half-span so it stands on the floor.
+ let inset = depth * 0.5;
+ let plateau = Rect { x: r.0 + inset, y: r.1 + inset, width: (r.2 - 2.0 * inset).max(0.0), height: (r.3 - 2.0 * inset).max(0.0) };
+ let pr = (radius - inset).max(0.0);
+ pc.boss(plateau, (pr, pr, pr, pr), depth);
+ }
if self.vertical {
if i < self.tab_text_quads.len() {
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 03815e6..c8937a9 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -31,6 +31,11 @@ pub struct ColorSelector {
/// `measure_text` prefix it used before reports inked extent, which drifts
/// off the glyph advances. Empty until the first shape.
glyph_offsets: Vec<f32>,
+ /// Recessed style: the hex field is a well carved into the plate below
+ /// (the TextBox's, rim lit while editing) and the swatch a raised bevel
+ /// plate of its colour, instead of the hairline frame and the flat swatch
+ /// with its glow. Defaults to `control_relief()`.
+ recessed: bool,
}
impl Clone for ColorSelector {
@@ -52,6 +57,7 @@ impl Clone for ColorSelector {
live_rx: None,
revert_hex: None,
glyph_offsets: self.glyph_offsets.clone(),
+ recessed: self.recessed,
}
}
}
@@ -75,6 +81,7 @@ impl ColorSelector {
live_rx: None,
revert_hex: None,
glyph_offsets: Vec::new(),
+ recessed: crate::layout::control_relief(),
})
}
@@ -96,12 +103,34 @@ impl ColorSelector {
live_rx: None,
revert_hex: None,
glyph_offsets: Vec::new(),
+ recessed: crate::layout::control_relief(),
})
}
+ /// The hex field's well for hosts that draw this control through the legacy
+ /// flat views (see `ParametersBg::reliefs`): (x, y, w, h, radius, depth) over the
+ /// widget's assigned content `rect`, or None when the style is off. The same
+ /// geometry `paint` carves (untinted).
+ pub fn field_relief(&self, rect: Rect) -> Option<(f32, f32, f32, f32, f32, f32)> {
+ if !self.recessed {
+ return None;
+ }
+ let well_h = crate::layout::color_selector_height().min(rect.height);
+ let field_w = rect.width * 0.65;
+ let radius = crate::layout::textbox_corner_radius();
+ let depth = crate::layout::bevel_width().min(well_h * 0.2);
+ Some((rect.x, rect.y, field_w, well_h, radius, depth))
+ }
+
}
impl Adapted<ColorSelector> {
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+
pub fn with_alpha(mut self, with_alpha: bool) -> Self {
self.with_alpha = with_alpha;
self
@@ -221,11 +250,13 @@ impl Paint for ColorSelector {
// 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.
- let bw = 1.0;
- quads.push((rect.x, rect.y, rect.width, bw, border_color));
- quads.push((rect.x, rect.y + visual_h - bw, rect.width, bw, border_color));
- quads.push((rect.x, rect.y, bw, visual_h, border_color));
- quads.push((rect.x + rect.width - bw, rect.y, bw, visual_h, border_color));
+ if !self.recessed {
+ let bw = 1.0;
+ quads.push((rect.x, rect.y, rect.width, bw, border_color));
+ quads.push((rect.x, rect.y + visual_h - bw, rect.width, bw, border_color));
+ quads.push((rect.x, rect.y, bw, visual_h, border_color));
+ quads.push((rect.x + rect.width - bw, rect.y, bw, visual_h, border_color));
+ }
if self.editing {
let font_size = 12.0;
@@ -292,6 +323,54 @@ impl Paint for ColorSelector {
}
};
+ if self.recessed {
+ // The well first (the caret quad above rides over it), then the swatch
+ // as a raised bevel plate of the colour — over a checker when the
+ // colour carries alpha, so the transparency reads through the plate.
+ for (qx, qy, qw, qh, qc) in quads.drain(..) {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
+ if let Some((wx, wy, ww, wh, radius, depth)) = self.field_relief(rect) {
+ let well = Rect { x: wx, y: wy, width: ww, height: wh };
+ let radii = (radius, radius, radius, radius);
+ if self.editing {
+ let hc = crate::color::highlight_primary_color();
+ ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
+ } else {
+ ctx.recess(well, radii, depth);
+ }
+ }
+ let swatch = Rect { x: px, y: py, width: pw, height: ph };
+ if self.with_alpha {
+ let mut checker = Vec::new();
+ add_rounded_rect(&mut checker, [0.8, 0.8, 0.8, 1.0], px, py, pw, ph, preview_radius);
+ let grid_size = 6.0;
+ let cols = (pw / grid_size).ceil() as i32;
+ let rows = (ph / grid_size).ceil() as i32;
+ for r in 0..rows {
+ for c in 0..cols {
+ if (r + c) % 2 == 1 {
+ let qx = px + c as f32 * grid_size;
+ let qy = py + r as f32 * grid_size;
+ let qw = grid_size.min(px + pw - qx);
+ let qh = grid_size.min(py + ph - qy);
+ if qw > 0.0 && qh > 0.0 {
+ checker.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
+ }
+ }
+ }
+ }
+ for (qx, qy, qw, qh, qc) in checker {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
+ }
+ let depth = crate::layout::bevel_width().min(ph * 0.2);
+ ctx.bevel(swatch, (preview_radius, preview_radius, preview_radius, preview_radius), linear_c, depth);
+ let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
+ ctx.text(hex, rect.x + 4.0, crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0), 12.0, [0xcc, 0xcc, 0xd4]);
+ return;
+ }
+
let steps = 6;
for i in (1..=steps).rev() {
let offset = i as f32 * 0.75;
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 28d68ee..3af17ec 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -1,3 +1,4 @@
+use crate::colors;
use crate::scene::layout::{Rect, Size};
use crate::scene::paint::PaintCtx;
use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
@@ -15,6 +16,11 @@ pub struct FontSelector {
pressed: bool,
hovered: bool,
child: Arc<Mutex<Option<std::process::Child>>>,
+ /// Raised style, the closed Dropdown's: the field is a flush inset trough
+ /// with a transparent face (the plate shows through), the hover and press
+ /// states a wash inside it. Defaults to `control_relief()`; the flat style
+ /// keeps the framed dark field.
+ raised: bool,
}
impl FontSelector {
@@ -25,6 +31,7 @@ impl FontSelector {
pressed: false,
hovered: false,
child: Arc::new(Mutex::new(None)),
+ raised: crate::layout::control_relief(),
})
}
@@ -98,9 +105,12 @@ impl FontSelector {
}
}
+ // The picker glyph: "Aa", the font-picker convention, in the dropdown arrow's
+ // grey — a text glyph every face has (the emoji this drew rendered as tofu
+ // wherever no emoji font was installed).
labels.push(TextLabel {
- text: "🔤".to_string(),
- x: rect.x + rect.width - 20.0,
+ text: "Aa".to_string(),
+ x: rect.x + rect.width - 24.0,
y: crate::layout::align_text_y(rect.y, rect.height, 11.0, 0.0),
font_size: 11.0,
color: [0x83, 0x83, 0x8a],
@@ -110,6 +120,14 @@ impl FontSelector {
}
}
+impl Adapted<FontSelector> {
+ /// Raised style: see the `raised` field.
+ pub fn with_raised(mut self, raised: bool) -> Self {
+ self.raised = raised;
+ self
+ }
+}
+
impl Layout for FontSelector {
fn intrinsic_size(&self) -> Option<Size> {
Some(Size::new(0.0, crate::layout::font_selector_height()))
@@ -135,8 +153,26 @@ impl Paint for FontSelector {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- // Rounded base plate (the legacy leaf default from color + corner style)
let r = crate::layout::font_selector_corner_radius();
+ if self.raised {
+ // The closed-dropdown chrome: a flush inset trough with a transparent
+ // face, the state fill rounded to sit inside it.
+ let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+ ctx.inset_plate(rect, (r, r, r, r), [0.0; 4], depth);
+ let wash = if self.pressed {
+ Some(colors::button_press_color())
+ } else if self.hovered {
+ Some(colors::button_hover_color())
+ } else {
+ None
+ };
+ if let Some(c) = wash {
+ ctx.rounded_rect(rect, r, (true, true, true, true), c);
+ }
+ self.paint_labels(rect, ctx);
+ return;
+ }
+ // Rounded base plate (the legacy leaf default from color + corner style)
if r > 0.0 {
ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
}
@@ -156,8 +192,14 @@ impl Paint for FontSelector {
bg_color,
);
- // Labels: family text clipped short of the picker glyph (the legacy per-label
- // bounds), glyph unclipped.
+ self.paint_labels(rect, ctx);
+ }
+}
+
+impl FontSelector {
+ /// Labels: family text clipped short of the picker glyph (the legacy per-label
+ /// bounds), glyph unclipped.
+ fn paint_labels(&self, rect: Rect, ctx: &mut PaintCtx) {
let font = self.widget_font();
let clip_right = rect.x + rect.width - 24.0;
let bounds = Some([rect.x, rect.y, clip_right, rect.y + rect.height]);
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index 9140129..8cf55db 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -14,6 +14,11 @@ pub struct KeybindRecorder {
pub just_changed: bool,
pressed: bool,
hovered: bool,
+ /// 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.
+ recessed: bool,
}
impl KeybindRecorder {
@@ -24,6 +29,7 @@ impl KeybindRecorder {
just_changed: false,
pressed: false,
hovered: false,
+ recessed: crate::layout::control_relief(),
})
}
@@ -40,6 +46,14 @@ impl KeybindRecorder {
}
}
+impl Adapted<KeybindRecorder> {
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+}
+
impl Layout for KeybindRecorder {
fn intrinsic_size(&self) -> Option<Size> {
Some(Size::new(0.0, crate::layout::textbox_height()))
@@ -52,6 +66,19 @@ impl Paint for KeybindRecorder {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ if self.recessed {
+ let radius = crate::layout::textbox_corner_radius();
+ let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+ let radii = (radius, radius, radius, radius);
+ if self.recording {
+ let hc = crate::color::highlight_primary_color();
+ ctx.recess_tinted(rect, radii, depth, [hc[0], hc[1], hc[2]]);
+ } else {
+ ctx.recess(rect, radii, depth);
+ }
+ self.paint_text(rect, ctx);
+ return;
+ }
let border_color = if self.recording {
colors::HIGHLIGHT_PRIMARY
} else if self.pressed {
@@ -67,6 +94,12 @@ impl Paint for KeybindRecorder {
[0.08, 0.08, 0.12, 1.0],
);
+ self.paint_text(rect, ctx);
+ }
+}
+
+impl KeybindRecorder {
+ fn paint_text(&self, rect: Rect, ctx: &mut PaintCtx) {
let (display_text, color) = if self.recording {
("[ Press Keys... ]".to_string(), [135, 135, 153])
} else if self.value.is_empty() {
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 08f879d..c944f78 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -176,15 +176,7 @@ impl Slider {
/// `Widget::label_offset` over the synced label (zero in side layout or
/// unlabeled).
fn label_top(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
+ detached_strip(&self.label)
}
fn geom(&self, rect: Rect) -> SliderGeom {
@@ -552,76 +544,7 @@ impl Paint for Slider {
// Carve AFTER the fill so the wall's shading modulates whatever it
// crosses — the same order TextBox uses for its edit fill.
if self.recessed {
- let strip = self.label_top();
- if strip > 0.0 {
- // Labeled: the label sits in a CARVE-OUT tab, the section-
- // title idiom (the labeled Dropdown's composition) — a flat
- // recessed well hugging the label run, bottom open into the
- // track's well; the well's top wall picks up right of the
- // tab's throat.
- let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
- let text_w = self
- .label
- .as_deref()
- .map(|l| crate::widget::display::measure_text_width(l, &fam, fsize))
- .unwrap_or(0.0);
- let inset = 4.0; // Layout::detached_label_inset — the label's x offset
- let tab_w = (text_w + 2.0 * inset).max(2.0 * radius + 8.0).min(g.track_w);
- let tab_r = g.track_x + tab_w;
- // The labeled-Dropdown composition: pieces extend `recess_t`
- // past interior seams (host-fade crossfade), the tab's right
- // wall ends at the fillet's vertical tangent (or it ghosts
- // through the arc), and a left-only bridge carries the left
- // wall across the fillet span.
- let fr = 6.0_f32.min(strip * 0.5);
- let filleted = g.track_x + g.track_w - tab_r > fr + 4.0;
- let tab_bottom = if filleted { g.y - fr } else { g.y };
- ctx.recess_edges(
- Rect { x: g.track_x, y: g.y - strip, width: tab_w, height: tab_bottom - (g.y - strip) + recess_t },
- (radius, radius.min(strip * 0.5), 0.0, 0.0),
- recess_t,
- (true, true, false, true),
- );
- if filleted {
- ctx.recess_edges(
- Rect { x: g.track_x, y: g.y - fr, width: tab_w, height: fr + recess_t },
- (0.0, 0.0, 0.0, 0.0),
- recess_t,
- (false, false, false, true),
- );
- }
- ctx.recess_edges(track_rect, (0.0, 0.0, radius, radius), recess_t, (false, true, true, true));
- if filleted {
- ctx.concave_fillet(
- tab_r + fr,
- g.y - fr,
- fr,
- recess_t,
- std::f32::consts::FRAC_PI_2,
- false,
- );
- ctx.recess_edges(
- Rect {
- x: tab_r + fr - recess_t,
- y: g.y,
- width: g.track_x + g.track_w - tab_r - fr + recess_t,
- height: g.h,
- },
- (0.0, radius, 0.0, 0.0),
- recess_t,
- (true, false, false, false),
- );
- } else if g.track_x + g.track_w - tab_r > 0.5 {
- ctx.recess_edges(
- Rect { x: tab_r - recess_t, y: g.y, width: g.track_x + g.track_w - tab_r + recess_t, height: g.h },
- (0.0, radius, 0.0, 0.0),
- recess_t,
- (true, false, false, false),
- );
- }
- } else {
- ctx.recess(track_rect, (radius, radius, radius, radius), recess_t);
- }
+ carve_labeled_well(ctx, track_rect, self.label_top(), detached_label_width(&self.label), radius, recess_t);
}
// Thumb. A real Circle prim, not a full-radius rounded rect: rounded
@@ -900,6 +823,10 @@ pub struct RangeSlider {
pub(crate) active_thumb: Option<ActiveThumb>,
drag_offset: f32,
label: Option<String>,
+ /// Recessed-track style, the Slider's: the track is a well carved into the
+ /// plate below, the fill sits on its floor and the thumbs are spheres in the
+ /// channel. Defaults to `control_relief()`.
+ recessed: bool,
}
impl RangeSlider {
@@ -910,6 +837,7 @@ impl RangeSlider {
active_thumb: None,
drag_offset: 0.0,
label: None,
+ recessed: crate::layout::control_relief(),
})
}
@@ -928,6 +856,109 @@ impl Adapted<RangeSlider> {
self.set_values(low, high);
self
}
+
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+}
+
+/// The recessed track's carve, in the labeled composition shared by Slider and
+/// RangeSlider: with a detached label (`strip` > 0, the label strip's height above
+/// `track`) the label sits in a CARVE-OUT tab, the section-title idiom (the labeled
+/// Dropdown's composition) — a flat recessed well hugging the label run, bottom open
+/// into the track's well; the well's top wall picks up right of the tab's throat.
+/// Pieces extend `depth` past interior seams (host-fade crossfade), the tab's right
+/// wall ends at the fillet's vertical tangent (or it ghosts through the arc), and a
+/// left-only bridge carries the left wall across the fillet span. Unlabeled, one
+/// plain recess.
+pub(crate) fn carve_labeled_well(ctx: &mut PaintCtx, track: Rect, strip: f32, label_w: f32, radius: f32, depth: f32) {
+ let track_end = track.x + track.width;
+ if strip > 0.0 {
+ // Labeled: the label sits in a CARVE-OUT tab, the section-
+ // title idiom (the labeled Dropdown's composition) — a flat
+ // recessed well hugging the label run, bottom open into the
+ // track's well; the well's top wall picks up right of the
+ // tab's throat.
+ let inset = 4.0; // Layout::detached_label_inset — the label's x offset
+ let tab_w = (label_w + 2.0 * inset).max(2.0 * radius + 8.0).min(track.width);
+ let tab_r = track.x + tab_w;
+ // The labeled-Dropdown composition: pieces extend `depth`
+ // past interior seams (host-fade crossfade), the tab's right
+ // wall ends at the fillet's vertical tangent (or it ghosts
+ // through the arc), and a left-only bridge carries the left
+ // wall across the fillet span.
+ let fr = 6.0_f32.min(strip * 0.5);
+ let filleted = track_end - tab_r > fr + 4.0;
+ let tab_bottom = if filleted { track.y - fr } else { track.y };
+ ctx.recess_edges(
+ Rect { x: track.x, y: track.y - strip, width: tab_w, height: tab_bottom - (track.y - strip) + depth },
+ (radius, radius.min(strip * 0.5), 0.0, 0.0),
+ depth,
+ (true, true, false, true),
+ );
+ if filleted {
+ ctx.recess_edges(
+ Rect { x: track.x, y: track.y - fr, width: tab_w, height: fr + depth },
+ (0.0, 0.0, 0.0, 0.0),
+ depth,
+ (false, false, false, true),
+ );
+ }
+ ctx.recess_edges(track, (0.0, 0.0, radius, radius), depth, (false, true, true, true));
+ if filleted {
+ ctx.concave_fillet(
+ tab_r + fr,
+ track.y - fr,
+ fr,
+ depth,
+ std::f32::consts::FRAC_PI_2,
+ false,
+ );
+ ctx.recess_edges(
+ Rect {
+ x: tab_r + fr - depth,
+ y: track.y,
+ width: track_end - tab_r - fr + depth,
+ height: track.height,
+ },
+ (0.0, radius, 0.0, 0.0),
+ depth,
+ (true, false, false, false),
+ );
+ } else if track_end - tab_r > 0.5 {
+ ctx.recess_edges(
+ Rect { x: tab_r - depth, y: track.y, width: track_end - tab_r + depth, height: track.height },
+ (0.0, radius, 0.0, 0.0),
+ depth,
+ (true, false, false, false),
+ );
+ }
+ } else {
+ ctx.recess(track, (radius, radius, radius, radius), depth);
+ }
+}
+
+/// The detached-label strip height above a content rect (zero in side layout or
+/// unlabeled) — the adapter's `label_offset`, replicated for widgets that reach up
+/// into the strip to carve the label tab.
+pub(crate) fn detached_strip(label: &Option<String>) -> f32 {
+ if crate::layout::control_label_layout() == "side" {
+ return 0.0;
+ }
+ if label.is_some() {
+ let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+ font_size + crate::layout::control_label_margin()
+ } else {
+ 0.0
+ }
+}
+
+/// The detached label's measured width (the tab hugs it), zero when unlabeled.
+pub(crate) fn detached_label_width(label: &Option<String>) -> f32 {
+ let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
+ label.as_deref().map(|l| crate::widget::display::measure_text_width(l, &fam, fsize)).unwrap_or(0.0)
}
impl Layout for RangeSlider {
@@ -993,7 +1024,21 @@ impl Paint for RangeSlider {
ctx.quad(r, c);
}
};
- rrect(Rect { x, y, width: w, height: h }, radius, rc, colors::rangeslider_track(), ctx);
+ let track = Rect { x, y, width: w, height: h };
+ if self.recessed {
+ // The Slider's recessed composition: no track fill (the plate below is
+ // the well's floor), the range band on the floor, the carve after it so
+ // the walls' shading modulates what they cross, and sphere thumbs sized
+ // to the flat floor between the walls.
+ let depth = crate::layout::bevel_width().min(h * 0.2);
+ rrect(highlight, radius.min(highlight.height / 2.0), (true, true, true, true), colors::rangeslider_fill(), ctx);
+ carve_labeled_well(ctx, track, detached_strip(&self.label), detached_label_width(&self.label), radius, depth);
+ let r = (h - depth) / 2.0;
+ ctx.sphere(thumb_low_x + thumb_size / 2.0, thumb_y + thumb_size / 2.0, r, low_color);
+ ctx.sphere(thumb_high_x + thumb_size / 2.0, thumb_y + thumb_size / 2.0, r, high_color);
+ return;
+ }
+ rrect(track, radius, rc, colors::rangeslider_track(), ctx);
rrect(highlight, radius.min(highlight.height / 2.0), (true, true, true, true), colors::rangeslider_fill(), ctx);
rrect(
Rect { x: thumb_low_x, y: thumb_y, width: thumb_size, height: thumb_size },
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 99e6edc..ab411a0 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -20,6 +20,10 @@ pub struct Trackpad {
label: Option<String>,
hovered: bool,
pub fingers: Vec<Finger>,
+ /// Recessed style: the touch area is a well carved into the plate below,
+ /// a faint dark wash for its floor, instead of the framed dark pane.
+ /// Defaults to `control_relief()`.
+ recessed: bool,
}
impl Trackpad {
@@ -29,12 +33,24 @@ impl Trackpad {
label: None,
hovered: false,
fingers: Vec::new(),
+ recessed: crate::layout::control_relief(),
})
}
pub fn set_fingers(&mut self, fingers: Vec<Finger>) {
self.fingers = fingers;
}
+}
+
+impl Adapted<Trackpad> {
+ /// Recessed style: see the `recessed` field.
+ pub fn with_recessed(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+}
+
+impl Trackpad {
fn label_offset(&self) -> f32 {
if crate::layout::control_label_layout() == "side" {
@@ -99,15 +115,25 @@ impl Paint for Trackpad {
let _ = rect; // geometry reads the assignment cache (events/drags share it)
let (x, y, w, visual_h) = self.touch_area();
- // 1. Background
- ctx.quad(Rect { x, y, width: w, height: visual_h }, [0.11, 0.11, 0.16, 0.85]);
-
- // 2. Borders
- let border_color = [0.28, 0.28, 0.38, 1.0];
- ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_color);
- ctx.quad(Rect { x, y: y + visual_h - 1.0, width: w, height: 1.0 }, border_color);
- ctx.quad(Rect { x, y, width: 1.0, height: visual_h }, border_color);
- ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: visual_h }, border_color);
+ 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]);
+ ctx.recess(area, (radius, radius, radius, radius), depth);
+ } else {
+ // 1. Background
+ ctx.quad(area, [0.11, 0.11, 0.16, 0.85]);
+
+ // 2. Borders
+ let border_color = [0.28, 0.28, 0.38, 1.0];
+ ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_color);
+ ctx.quad(Rect { x, y: y + visual_h - 1.0, width: w, height: 1.0 }, border_color);
+ ctx.quad(Rect { x, y, width: 1.0, height: visual_h }, border_color);
+ ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: visual_h }, border_color);
+ }
// 3. Fingers
for finger in &self.fingers {