GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: band slider wheel-capture halo conforms to the drawn shape
Scrolling a band-style slider was fighting the pane: the capture zone is
now an 8px inset halo around the DRAWN shape — the thin band plus the
bulge, which travels with the value — measured from the same
band_height_at profile the paint uses, so the zone always matches the
pixels. A scroll inside the halo adjusts the slider (Slider::scroll_hit,
used by both the widget's own wheel test and ParametersBg's forwarding
gate); anywhere else in the row falls through to the pane's viewport
scroll. The default slider style keeps its whole-row strip.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/widget/container/parameters_bg.rs | 23 +++++++++++++++++++-
src/widget/input/slider.rs | 41 +++++++++++++++++++++++++++++++----
2 files changed, 59 insertions(+), 5 deletions(-)
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index de2a302..b541a77 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -2184,7 +2184,28 @@ impl Input for ParametersBg {
if p.2.starts_with("slider") {
let r = rects[i];
let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + r.3 && px >= self.rect.x && px <= self.rect.x + self.rect.width {
+ // Band style: the capture zone is the slider's own
+ // shape halo (`Slider::scroll_hit` — the band plus the
+ // traveling bulge, inset), so scrolls off the shape
+ // fall through to the pane's viewport scroll below.
+ // The default style keeps the whole-row strip.
+ let in_zone = if crate::layout::slider_band() {
+ self.sliders[i].as_ref().map_or(false, |s| {
+ let (sx, sy, sw, sh) = s.rect();
+ let ty = crate::widget::label_offset(s);
+ s.inner().scroll_hit(
+ Rect { x: sx, y: sy + ty, width: sw, height: sh - ty },
+ px,
+ py,
+ )
+ })
+ } else {
+ py >= row_y - 2.0
+ && py <= row_y + r.3
+ && px >= self.rect.x
+ && px <= self.rect.x + self.rect.width
+ };
+ if in_zone {
if let Some(s) = &mut self.sliders[i] {
let was_scroll = s.scroll_enabled;
s.set_scroll(true);
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 461ab5c..8943d9e 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -186,6 +186,41 @@ impl Slider {
SliderGeom { x, y: rect.y, w, h: rect.height, track_x, track_w, thumb_size: rect.height * 0.9 }
}
+ /// The band style's height profile at `x`: the flat band thickness, rising
+ /// through the cosine bell around the value position. The single source
+ /// both the paint and the wheel-capture halo (`scroll_hit`) measure from,
+ /// so the halo always conforms to the drawn shape.
+ fn band_height_at(&self, g: &SliderGeom, x: f32) -> f32 {
+ let band_t = crate::layout::slider_band_thickness().max(0.5);
+ let bulge_h = crate::layout::slider_bulge_height().clamp(band_t, g.h);
+ let bulge_w = crate::layout::slider_bulge_width().max(2.0);
+ let vx = g.track_x + self.value * g.track_w;
+ let t = ((x - vx) / bulge_w).clamp(-1.0, 1.0);
+ let bell = 0.5 * (1.0 + (std::f32::consts::PI * t).cos());
+ band_t + (bulge_h - band_t) * bell.powf(1.35)
+ }
+
+ /// The wheel-capture zone. Band style: an inset halo around the DRAWN
+ /// shape — the thin band and the bulge, which travels with the value — so
+ /// a scroll near the visible slider adjusts it while the rest of the row
+ /// stays the host pane's to scroll. Otherwise: plain rect containment.
+ pub fn scroll_hit(&self, rect: Rect, px: f32, py: f32) -> bool {
+ if !crate::layout::slider_band() {
+ return px >= rect.x
+ && px <= rect.x + rect.width
+ && py >= rect.y
+ && py <= rect.y + rect.height;
+ }
+ const SCROLL_INSET: f32 = 8.0;
+ let g = self.geom(rect);
+ if px < g.track_x - SCROLL_INSET || px > g.track_x + g.track_w + SCROLL_INSET {
+ return false;
+ }
+ let cy = g.y + g.h * 0.5;
+ let x = px.clamp(g.track_x, g.track_x + g.track_w);
+ (py - cy).abs() <= self.band_height_at(&g, x) * 0.5 + SCROLL_INSET
+ }
+
/// The band style's geometry: a thin band spanning the whole track, swelling
/// smoothly around the value position — a cosine bell sampled as ~1px column
/// quads, so the swell reads as one continuous surface (the snake that
@@ -222,9 +257,7 @@ impl Slider {
let step_w = span / steps as f32;
for i in 0..steps {
let x = b0 + i as f32 * step_w;
- let t = ((x + step_w * 0.5 - vx) / bulge_w).clamp(-1.0, 1.0);
- let bell = 0.5 * (1.0 + (std::f32::consts::PI * t).cos());
- let h = band_t + (bulge_h - band_t) * bell.powf(1.35);
+ let h = self.band_height_at(g, x + step_w * 0.5);
// A hair of overlap between columns so AA seams can't open.
ctx.quad(Rect { x, y: cy - h * 0.5, width: step_w + 0.3, height: h }, color);
}
@@ -545,7 +578,7 @@ impl Input for Slider {
return false;
}
let r = ectx.rect;
- if *px >= r.x && *px <= r.x + r.width && *py >= r.y && *py <= r.y + r.height {
+ if self.scroll_hit(r, *px, *py) {
if ui.scroll_gesture_new {
ui.scroll_initiate_widget_id = Some(ectx.id);
}