GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(slider)!: the band is the only slider style
The thin full-range band that swells at the value — the "band" style cce-designer
and cce-color-editor opted into per app-config — is now every slider's look; the
classic track/fill/thumb (and its recessed carve) is gone, with `with_band`,
`with_recessed`, the `style.control.slider.style` key, `slider_band()`, the
`track_relief` / `thumb_sphere` flat-host bridges (ParametersBg's slider and
Float3 branches, `spheres` now empty) and the track/fill colours: `slider color`
/ `fill_color`, `rangeslider track` / `fill` / `corner_radius` and their getters.
RangeSlider joins: one band with a swell at each end of the range and the band a
thickness heavier between them, the swells where its thumbs' centres were so the
drag geometry is unchanged. `band_profile` and `paint_band_shape` are the one
profile and painter behind Slider, RangeSlider and Float3's rows (Slider's copy
folded into them); the wheel halo, gesture latch and ungated Float3 / pane
scroll paths lose their classic branches.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/bin/cce-relief.rs | 7 +-
src/color.rs | 59 -----
src/layout.rs | 31 +--
src/widget/container/parameters_bg.rs | 125 ++-------
src/widget/display/float3.rs | 9 +-
src/widget/input/slider.rs | 471 ++++++++--------------------------
6 files changed, 138 insertions(+), 564 deletions(-)
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 384781a..239b0d6 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -386,7 +386,6 @@ impl ProfileKnobs {
.with_label(label)
.with_value(v.clamp(0.0, 1.0))
.with_scroll(true)
- .with_band(true)
};
let mut this = Self {
shoulder: knob(s, "Shoulder"),
@@ -1238,16 +1237,14 @@ impl Application for BevelPopup {
.with_value(((depth - dmin) / (dmax - dmin)).clamp(0.0, 1.0))
.with_readout(true)
.with_decimals(2)
- .with_scroll(true)
- .with_band(true),
+ .with_scroll(true),
width_slider: Slider::new()
.with_label("Width")
.with_range(wmin, wmax)
.with_value(((width - wmin) / (wmax - wmin)).clamp(0.0, 1.0))
.with_readout(true)
.with_decimals(1)
- .with_scroll(true)
- .with_band(true),
+ .with_scroll(true),
save_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Save"),
cancel_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Cancel"),
exit_requested: false,
diff --git a/src/color.rs b/src/color.rs
index 50eca5e..7b9b04d 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -22,7 +22,6 @@ pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
use std::sync::RwLock;
-static SLIDER_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_TRACK);
static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
@@ -75,8 +74,6 @@ static CONTROL_PANEL_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.161, 0.173,
static PROGRESS_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_BG);
static PROGRESS_FILL_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_FILL);
-static RANGE_SLIDER_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_TRACK);
-static RANGE_SLIDER_FILL_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_FILL);
static SPINBOX_DISPLAY_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_DISPLAY);
static SPINBOX_BUTTON_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON);
static SPINBOX_BUTTON_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON_HOVER);
@@ -88,7 +85,6 @@ static BUTTON_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
static DROPDOWN_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
static DROPDOWN_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.72305, 0.72305, 0.76008, 1.0]);
-static SLIDER_FILL_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
static SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
static SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
@@ -325,9 +321,6 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/color_borders_color") {
if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/style/control/slider/color").or_else(|| get_color("/layout/slider_track_color")) {
- if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() { *lock = c; }
- }
if let Some(c) = get_color("/layout/paginator_sidebar_color") {
if let Ok(mut lock) = SIDEBAR_BG_COLOR.write() { *lock = c; }
}
@@ -417,9 +410,6 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/style/control/dropdown/text_color").or_else(|| get_color("/style/dropdown/text_color")) {
if let Ok(mut lock) = DROPDOWN_TEXT_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/style/control/slider/fill_color").or_else(|| get_color("/style/slider/fill_color")) {
- if let Ok(mut lock) = SLIDER_FILL_COLOR.write() { *lock = Some(c); }
- }
if let Some(c) = get_color("/style/control/slider/thumb_color").or_else(|| get_color("/style/slider/thumb_color")) {
if let Ok(mut lock) = SLIDER_THUMB_COLOR.write() { *lock = c; }
if let Ok(mut lock_drag) = SLIDER_THUMB_DRAG_COLOR.write() {
@@ -448,12 +438,6 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/style/control/progressbar/fill") {
if let Ok(mut lock) = PROGRESS_FILL_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/style/control/rangeslider/track") {
- if let Ok(mut lock) = RANGE_SLIDER_TRACK_COLOR.write() { *lock = c; }
- }
- if let Some(c) = get_color("/style/control/rangeslider/fill") {
- if let Ok(mut lock) = RANGE_SLIDER_FILL_COLOR.write() { *lock = c; }
- }
let parsed_list_bg = get_color("/layout/list_bg_color");
let parsed_breadcrumb_bg = get_color("/layout/breadcrumb_bg_color");
let parsed_popover_bg = get_color("/layout/popover_bg_color");
@@ -867,17 +851,6 @@ pub fn set_color_borders_color(color: [f32; 4]) {
}
}
-pub fn slider_track() -> [f32; 4] {
- load_colors_once();
- *SLIDER_TRACK_COLOR.read().unwrap()
-}
-
-pub fn set_slider_track(color: [f32; 4]) {
- if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() {
- *lock = color;
- }
-}
-
pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
@@ -1285,28 +1258,6 @@ pub fn set_progress_fill(color: [f32; 4]) {
}
}
-pub fn rangeslider_track() -> [f32; 4] {
- load_colors_once();
- *RANGE_SLIDER_TRACK_COLOR.read().unwrap()
-}
-
-pub fn set_rangeslider_track(color: [f32; 4]) {
- if let Ok(mut lock) = RANGE_SLIDER_TRACK_COLOR.write() {
- *lock = color;
- }
-}
-
-pub fn rangeslider_fill() -> [f32; 4] {
- load_colors_once();
- *RANGE_SLIDER_FILL_COLOR.read().unwrap()
-}
-
-pub fn set_rangeslider_fill(color: [f32; 4]) {
- if let Ok(mut lock) = RANGE_SLIDER_FILL_COLOR.write() {
- *lock = color;
- }
-}
-
pub fn button_border_color() -> Option<[f32; 4]> {
load_colors_once();
*BUTTON_BORDER_COLOR.read().unwrap()
@@ -1340,16 +1291,6 @@ pub fn set_dropdown_text_color(color: [f32; 4]) {
}
}
-pub fn slider_fill() -> Option<[f32; 4]> {
- load_colors_once();
- *SLIDER_FILL_COLOR.read().unwrap()
-}
-
-pub fn set_slider_fill(color: [f32; 4]) {
- if let Ok(mut lock) = SLIDER_FILL_COLOR.write() {
- *lock = Some(color);
- }
-}
pub fn slider_thumb() -> [f32; 4] {
load_colors_once();
diff --git a/src/layout.rs b/src/layout.rs
index fe3d94c..855bf50 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -89,13 +89,11 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.control.label.layout" => "control_label_layout",
"style.control.slider.height" => "slider_height",
"style.control.slider.corner_radius" => "slider_corner_radius",
- "style.control.slider.style" => "slider_style",
"style.control.slider.band_thickness" => "slider_band_thickness",
"style.control.slider.bulge_width" => "slider_bulge_width",
"style.control.slider.bulge_height" => "slider_bulge_height",
"style.control.progressbar.height" => "progressbar_height",
"style.control.rangeslider.height" => "rangeslider_height",
- "style.control.rangeslider.corner_radius" | "style.rangeslider.corner_radius" => "rangeslider_corner_radius",
"style.control.scrollbar.width" => "scrollbar_width",
"style.control.scrollbar.inset" => "scrollbar_inset",
"style.control.spinbox.height" => "spinbox_height",
@@ -1926,21 +1924,9 @@ pub fn toggle_slide() -> bool {
}
-/// The slider's render style: `style.control.slider.style = "band"` swaps the
-/// track/fill/thumb for a thin full-range band that inflates smoothly at the
-/// value (see `Slider::paint`). Anything else — or unset — keeps the default
-/// look, so apps opt in per-config.
-pub fn slider_band() -> bool {
- lazy_init_style_registry();
- get_style_registry()
- .read()
- .unwrap()
- .get_string("slider_style")
- .is_some_and(|s| s == "band")
-}
-
-/// Band-style knobs (`style.control.slider.*`): the flat band's thickness, and
-/// the bulge's half-span / peak height around the value position.
+/// The slider band's knobs (`style.control.slider.*`): the flat band's thickness,
+/// and the swell's half-span / peak height around the value position. The band —
+/// a thin full-range band that swells at the value — is the one slider style.
pub fn slider_band_thickness() -> f32 {
lazy_init_style_registry();
get_style_registry().read().unwrap().get_float("slider_band_thickness").unwrap_or(2.0)
@@ -1963,17 +1949,6 @@ pub fn set_slider_corner_radius(radius: f32) {
}
}
-pub fn rangeslider_corner_radius() -> f32 {
- lazy_init_style_registry();
- get_style_registry().read().unwrap().get_float("rangeslider_corner_radius").unwrap_or(4.0)
-}
-
-pub fn set_rangeslider_corner_radius(radius: f32) {
- lazy_init_style_registry();
- if let Ok(mut registry) = get_style_registry().write() {
- registry.set_float("rangeslider_corner_radius", radius);
- }
-}
pub fn plate_corner_radius() -> f32 {
lazy_init_style_registry();
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 6800273..13254e0 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1230,33 +1230,10 @@ impl ParametersBg {
}
}
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
- // whole rect), the slider-row entry below three times.
- if let Some(f) = &self.float3s[i] {
- for (s, r) in f.sliders().iter().zip(f.get_row_rects()) {
- if let Some((rx, ry, rw, rh, rr, rd)) =
- s.inner().track_relief(Rect { x: r.0, y: r.1, width: r.2, height: r.3 })
- {
- out.push((rx, ry, rw, rh, r4(rr), rd, false, all));
- }
- }
- }
- None
} else {
- if let Some(s) = &self.sliders[i] {
- let (x, y, w, h) = s.rect();
- // The detached top label sits OUTSIDE the carve: shrink to
- // the content band, exactly the rect the widget's own
- // paint receives (the render_widget label_offset shrink).
- let ty = crate::widget::label_offset(s);
- if let Some((rx, ry, rw, rh, rr, rd)) =
- s.inner().track_relief(Rect { x, y: y + ty, width: w, height: h - ty })
- {
- out.push((rx, ry, rw, rh, r4(rr), rd, false, all));
- }
- }
+ // Sliders (and Float3's three rows) are bands: their well is
+ // hand-shaded quads that follow the band's contour, which reach
+ // a flat host through the plain-quad view — no rect carve.
None
};
if let Some((w, radius, raised)) = ctl {
@@ -1393,49 +1370,11 @@ impl ParametersBg {
out
}
- /// The sphere companion to [`Self::rounded_quads`]: the slider rows' thumb
- /// knobs, which are `Prim::Sphere` — a prim NO legacy flat view carries, so
- /// a host rendering this panel through the legacy views must read this
- /// getter or the knobs vanish. Returned unclipped; the host clips to the
- /// pane's scroll viewport and draws these AFTER [`Self::reliefs`], matching
- /// the widget's own fill → carve → thumb order.
+ /// The knobs a flat host draws after [`Self::reliefs`] — none: the sliders
+ /// are bands (their swell is part of the band's own quads), so this is kept
+ /// only for the hosts that still call it.
pub fn spheres(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut out = Vec::new();
- let hidden = self.hidden_rows();
- for (i, p) in self.display_params.iter().enumerate() {
- if hidden[i] {
- continue;
- }
- if p.2.starts_with("float3") {
- // The group's three slider rows, each knob over its row rect.
- if let Some(f) = &self.float3s[i] {
- for (s, r) in f.sliders().iter().zip(f.get_row_rects()) {
- if let Some(sphere) =
- s.inner().thumb_sphere(Rect { x: r.0, y: r.1, width: r.2, height: r.3 })
- {
- out.push(sphere);
- }
- }
- }
- continue;
- }
- if !p.2.starts_with("slider") {
- continue;
- }
- if let Some(s) = &self.sliders[i] {
- let (x, y, w, h) = s.rect();
- // Content band, like `reliefs`: the knob sizes to the track
- // well, not the label-inclusive rect.
- let ty = crate::widget::label_offset(s);
- if let Some(sphere) = s.inner().thumb_sphere(Rect { x, y: y + ty, width: w, height: h - ty }) {
- out.push(sphere);
- }
- }
- }
- out
+ Vec::new()
}
/// The section carves' concave inside-corner fillets — `(cx, cy, radius,
@@ -2633,36 +2572,26 @@ impl Input for ParametersBg {
let rects = self.get_param_rects();
for (i, p) in self.display_params.iter_mut().enumerate() {
if p.2.starts_with("slider") {
- let r = rects[i];
- let row_y = r.1;
- // 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| {
- // The same gesture latch the slider's own wheel
- // test applies: mid-gesture the slider that
- // acquired the scroll keeps it (its halo travels
- // away from the pointer as the value moves).
- let latched = !ui.scroll_gesture_new
- && ui.scroll_initiate_widget_id == Some(s.base().id());
- let (sx, sy, sw, sh) = s.rect();
- let ty = crate::widget::label_offset(s);
- latched
- || 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
- };
+ // The capture zone is the slider's own shape halo
+ // (`Slider::scroll_hit` — the band plus the traveling
+ // swell, inset), so scrolls off the shape fall through
+ // to the pane's viewport scroll below.
+ let in_zone = self.sliders[i].as_ref().map_or(false, |s| {
+ // The same gesture latch the slider's own wheel
+ // test applies: mid-gesture the slider that
+ // acquired the scroll keeps it (its halo travels
+ // away from the pointer as the value moves).
+ let latched = !ui.scroll_gesture_new
+ && ui.scroll_initiate_widget_id == Some(s.base().id());
+ let (sx, sy, sw, sh) = s.rect();
+ let ty = crate::widget::label_offset(s);
+ latched
+ || s.inner().scroll_hit(
+ Rect { x: sx, y: sy + ty, width: sw, height: sh - ty },
+ px,
+ py,
+ )
+ });
if in_zone {
if let Some(s) = &mut self.sliders[i] {
let was_scroll = s.scroll_enabled;
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 48e857b..8bd82b8 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -143,15 +143,10 @@ impl Float3 {
/// it, whether or not the value string ticked over.
pub fn wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ui: &mut UiContext) -> bool {
let rows = self.get_row_rects();
- let band = crate::layout::slider_band();
for (s, r) in self.sliders.iter_mut().zip(rows) {
let rect = Rect { x: r.0, y: r.1, width: r.2, height: r.3 };
- let in_zone = if band {
- let latched = !ui.scroll_gesture_new && ui.scroll_initiate_widget_id == Some(s.base().id());
- latched || s.inner().scroll_hit(rect, px, py)
- } else {
- py >= rect.y && py <= rect.y + rect.height
- };
+ let latched = !ui.scroll_gesture_new && ui.scroll_initiate_widget_id == Some(s.base().id());
+ let in_zone = latched || s.inner().scroll_hit(rect, px, py);
if !in_zone {
continue;
}
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 90c44e8..fd3dccf 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -23,7 +23,6 @@ struct SliderGeom {
h: f32,
track_x: f32,
track_w: f32,
- thumb_size: f32,
}
fn side_offset(label: &Option<String>) -> f32 {
@@ -48,11 +47,6 @@ pub struct Slider {
pub editor_state: TextEditorState,
pub just_changed: bool,
label: Option<String>,
- /// Recessed-track style: the track is a well carved into the plate below
- /// (the TextBox `with_recessed` idiom) — the carve's shading defines the
- /// channel, and with a transparent track color the plate itself is its
- /// floor.
- recessed: bool,
/// Wheel-scroll glide velocity (normalized value units/sec) and the last
/// wheel-event instant — the Ramp hover-scroll idiom: when the event
/// stream stops (fingers lifted), `tick` keeps the value coasting with
@@ -61,9 +55,6 @@ pub struct Slider {
last_wheel: Option<std::time::Instant>,
/// Readout / edit-buffer display precision (decimal places).
decimals: usize,
- /// Per-widget band-style override; `None` follows the DE config
- /// (`style.control.slider.style`).
- band_override: Option<bool>,
}
impl Slider {
@@ -81,11 +72,9 @@ impl Slider {
editor_state: TextEditorState::new(String::new()),
just_changed: false,
label: None,
- recessed: crate::layout::control_relief(),
scroll_vel: 0.0,
last_wheel: None,
decimals: 2,
- band_override: None,
})
}
@@ -127,62 +116,12 @@ impl Slider {
}
}
- /// The "band" style (config `style.control.slider.style = "band"`): a thin
- /// full-range band that inflates smoothly at the value — no track fill, no
- /// thumb ball, no carve.
- fn band(&self) -> bool {
- self.band_override.unwrap_or_else(crate::layout::slider_band)
- }
-
- /// The width the value maps over: the full track under the band style
- /// (there is no thumb to keep inside the ends), thumb-inset otherwise.
+ /// The width the value maps over: the whole track — the band has no thumb
+ /// to keep inside the ends.
fn value_span(&self, g: &SliderGeom) -> f32 {
- if self.band() { g.track_w } else { g.track_w - g.thumb_size }
+ g.track_w
}
- /// The recessed track's carve 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 `rect`, or None when the style is off.
- /// The same geometry `paint` carves.
- pub fn track_relief(&self, rect: Rect) -> Option<(f32, f32, f32, f32, f32, f32)> {
- if !self.recessed || self.band() {
- return None;
- }
- let g = self.geom(rect);
- let radius = crate::layout::slider_corner_radius();
- let depth = crate::layout::bevel_width().min(g.h * 0.2);
- let (well, radii) = crate::layout::carve_inside(
- Rect { x: g.track_x, y: g.y, width: g.track_w, height: g.h },
- (radius, radius, radius, radius),
- depth,
- );
- Some((well.x, well.y, well.width, well.height, radii.0, depth))
- }
-
- /// The thumb knob's circle (cx, cy, radius, color) for hosts that draw this
- /// control through the legacy flat views (see `ParametersBg::spheres`): the
- /// `Prim::Sphere` the rounded-corner paint emits — no flat view can carry
- /// it. None under the square style, whose quad thumb already reaches the
- /// plain-quad view. The same geometry `paint` draws.
- pub fn thumb_sphere(&self, rect: Rect) -> Option<(f32, f32, f32, [f32; 4])> {
- if crate::layout::slider_corner_radius() <= 0.0 || self.band() {
- return None;
- }
- let g = self.geom(rect);
- let recess_t = crate::layout::bevel_width().min(g.h * 0.2);
- let thumb_x = g.track_x + self.value * (g.track_w - g.thumb_size);
- let thumb_y = g.y + (g.h - g.thumb_size) / 2.0;
- let diameter = if self.recessed { g.h - recess_t } else { g.thumb_size };
- let color = if self.dragging { colors::slider_thumb_drag() } else { colors::slider_thumb() };
- Some((thumb_x + g.thumb_size / 2.0, thumb_y + g.thumb_size / 2.0, diameter / 2.0, color))
- }
-
- /// The detached-label strip height above the content rect — a replica of
- /// `Widget::label_offset` over the synced label (zero in side layout or
- /// unlabeled).
- fn label_top(&self) -> f32 {
- detached_strip(&self.label)
- }
fn geom(&self, rect: Rect) -> SliderGeom {
let side = side_offset(&self.label);
@@ -195,31 +134,13 @@ impl Slider {
} else {
(x, w)
};
- SliderGeom { x, y: rect.y, w, h: rect.height, track_x, track_w, thumb_size: rect.height * 0.9 }
+ SliderGeom { x, y: rect.y, w, h: rect.height, track_x, track_w }
}
- /// 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.
+ /// The band's height profile at `x` (`band_profile`, one swell at the value).
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());
- let h = band_t + (bulge_h - band_t) * bell.powf(1.35);
- // Capsule tips: the profile shrinks over a circular cap inside each
- // track end — the band ends round, not square-cut, and the well
- // contour and wheel halo (both measured from here) round with it.
- let d = (x - g.track_x).min(g.track_x + g.track_w - x);
- let r = (h * 0.5).max(0.5);
- if d < r {
- let t = ((r - d.max(0.0)) / r).min(1.0);
- return h * (1.0 - t * t).max(0.0).sqrt();
- }
- h
+ band_profile(g.track_x, g.track_w, g.h, x, &[vx], None)
}
/// The wheel-capture zone. Band style: an inset halo around the DRAWN
@@ -227,12 +148,6 @@ impl Slider {
/// 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 = 14.0;
let g = self.geom(rect);
if px < g.track_x - SCROLL_INSET || px > g.track_x + g.track_w + SCROLL_INSET {
@@ -243,116 +158,11 @@ impl Slider {
(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
- /// swallowed the rodent), not a marker riding a rail. Emitted as `Prim::Quad`s
- /// so legacy plain-quad hosts (`ParametersBg::extra_quads`) carry it verbatim.
+ /// The slider: the band spanning the whole track, swelling at the value
+ /// (`paint_band_shape`).
fn paint_band(&self, g: &SliderGeom, ctx: &mut PaintCtx) {
- let band_t = crate::layout::slider_band_thickness().max(0.5);
- let bulge_w = crate::layout::slider_bulge_width().max(2.0);
let color = if self.dragging { colors::slider_thumb_drag() } else { colors::slider_thumb() };
- let cy = g.y + g.h * 0.5;
- let vx = g.track_x + self.value * g.track_w;
-
- // The well: the band appears INSET — a carve whose contour follows the
- // drawn shape (band + traveling bulge) a small gap outside it. The rect
- // recess prims can't follow a bell, so the walls are hand-shaded per
- // column from the same `band_height_at` profile the fill samples: a
- // shadow band hugging the top contour, a lit band along the bottom
- // (the DE light sits upper-left), stepped alphas like the legacy
- // banded bevels, amplitude riding `bevel_depth` like the rocker's
- // `face_light`.
- const WELL_GAP: f32 = 4.0;
- const WELL_WALL: f32 = 3.0;
- const WALL_STEPS: usize = 3;
- let strength = (crate::layout::bevel_depth() / 0.15).clamp(0.0, 2.0);
- let a_dark = 0.32 * strength;
- let a_light = 0.16 * strength;
- let wx0 = g.track_x - WELL_GAP;
- let wx1 = g.track_x + g.track_w + WELL_GAP;
- // 1px columns, EXACT widths: translucent shading quads must not
- // overlap (a seam double-blends into a visible tick) — unlike the
- // opaque fill columns below, which overlap on purpose against AA gaps.
- let cols = (wx1 - wx0).ceil().max(1.0) as i32;
- let colw = (wx1 - wx0) / cols as f32;
- let sub = WELL_WALL / WALL_STEPS as f32;
- for i in 0..cols {
- let x = wx0 + i as f32 * colw;
- let xm = x + colw * 0.5;
- // Inside the track the contour rides the band profile; past the
- // tips it wraps around them on a WELL_GAP circle — rounded well
- // ends, not square-cut walls.
- let c = if xm < g.track_x {
- let e = g.track_x - xm;
- (WELL_GAP * WELL_GAP - e * e).max(0.0).sqrt()
- } else if xm > g.track_x + g.track_w {
- let e = xm - (g.track_x + g.track_w);
- (WELL_GAP * WELL_GAP - e * e).max(0.0).sqrt()
- } else {
- self.band_height_at(g, xm) * 0.5 + WELL_GAP
- };
- for k in 0..WALL_STEPS {
- let fade = 1.0 - k as f32 / WALL_STEPS as f32;
- // Shadow INSIDE the well below the top contour; the lit lip
- // OUTSIDE below the bottom contour — the textbox-recess read.
- ctx.quad(
- Rect { x, y: cy - c + k as f32 * sub, width: colw, height: sub },
- [0.0, 0.0, 0.0, a_dark * fade],
- );
- ctx.quad(
- Rect { x, y: cy + c + k as f32 * sub, width: colw, height: sub },
- [1.0, 1.0, 1.0, a_light * fade],
- );
- }
- }
- // Flat runs outside the bulge span: one long quad each, except the
- // capsule-tip regions, which sample the rounded profile per column.
- let l0 = g.track_x;
- let r1 = g.track_x + g.track_w;
- let b0 = (vx - bulge_w).max(l0);
- let b1 = (vx + bulge_w).min(r1);
- let tip_columns = |ctx: &mut PaintCtx, from: f32, to: f32| {
- let steps = ((to - from).ceil() as i32).max(1);
- let sw = (to - from) / steps as f32;
- for i in 0..steps {
- let x = from + i as f32 * sw;
- let h = self.band_height_at(g, x + sw * 0.5);
- ctx.quad(Rect { x, y: cy - h * 0.5, width: sw + 0.3, height: h }, color);
- }
- };
- if b0 > l0 {
- let flat_start = (l0 + band_t).min(b0);
- tip_columns(ctx, l0, flat_start);
- ctx.quad(
- Rect { x: flat_start, y: cy - band_t * 0.5, width: b0 - flat_start, height: band_t },
- color,
- );
- }
- if r1 > b1 {
- let flat_end = (r1 - band_t).max(b1);
- ctx.quad(
- Rect { x: b1, y: cy - band_t * 0.5, width: flat_end - b1, height: band_t },
- color,
- );
- tip_columns(ctx, flat_end, r1);
- }
-
- // The swell: symmetric about the band's centerline. The bell is raised
- // to a power so the flanks taper long and the crest stays plump —
- // mid-digestion, not a triangle.
- let span = b1 - b0;
- if span <= 0.0 {
- return;
- }
- let steps = (span.ceil() as i32).max(1);
- let step_w = span / steps as f32;
- for i in 0..steps {
- let x = b0 + i as f32 * step_w;
- 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);
- }
+ paint_band_shape(ctx, g.track_x, g.track_w, g.y + g.h * 0.5, color, &|x| self.band_height_at(g, x));
}
fn scaled_string(&self) -> String {
@@ -402,11 +212,6 @@ impl Adapted<Slider> {
self
}
- /// Recessed-track style: see the `recessed` field.
- pub fn with_recessed(mut self, recessed: bool) -> Self {
- self.recessed = recessed;
- self
- }
pub fn with_value(mut self, val: f32) -> Self {
self.set_value(val);
@@ -424,13 +229,6 @@ impl Adapted<Slider> {
self
}
- /// Force the band style (the thin full-range band swelling at the value)
- /// on or off for this slider, regardless of the DE-wide
- /// `style.control.slider.style` setting.
- pub fn with_band(mut self, band: bool) -> Self {
- self.band_override = Some(band);
- self
- }
}
impl Layout for Slider {
@@ -480,19 +278,6 @@ impl Paint for Slider {
}
};
- // Track. Recessed style draws no background at all — the plate below
- // is the well's floor (the TextBox bare-recess look), and the carve
- // emitted after the fill defines the channel. The band style draws
- // neither: the band IS the whole control.
- let band = self.band();
- let track_rect = Rect { x: g.track_x, y: g.y, width: g.track_w, height: g.h };
- // Carve wall width, the TextBox formula: capped against the bar height
- // (the wall straddles the track boundary, intruding half its width).
- let recess_t = crate::layout::bevel_width().min(g.h * 0.2);
- if !self.recessed && !band {
- rrect(track_rect, radius, rc, colors::slider_track(), ctx);
- }
-
// Readout box (+ focus border) and its text.
if self.show_readout {
let readout_w = 60.0;
@@ -518,54 +303,7 @@ impl Paint for Slider {
ctx.text(text, rx + 8.0, crate::layout::align_text_y(g.y, g.h, 12.0, 0.0), 12.0, [0xee, 0xee, 0xf0]);
}
- // Band style: the full-range band with its value swell replaces fill,
- // carve, and thumb outright.
- if band {
- self.paint_band(&g, ctx);
- return;
- }
-
- // Fill up to the thumb center. Recessed style insets the fill onto the
- // well's flat floor (past the wall, which is carved inside the track), so
- // the liquid sits in the well instead of climbing its walls.
- let thumb_x = g.track_x + self.value * (g.track_w - g.thumb_size);
- if let Some(fill_color) = colors::slider_fill() {
- let (fx, fy, fmax_w, fh) = if self.recessed {
- let inset = recess_t;
- (g.track_x + inset, g.y + inset, g.track_w - 2.0 * inset, g.h - 2.0 * inset)
- } else {
- (g.track_x, g.y, g.track_w, g.h)
- };
- let fill_w = (thumb_x + g.thumb_size / 2.0 - fx).max(0.0).min(fmax_w);
- let fill_rad = if rounded { radius.min(fh / 2.0) } else { radius };
- rrect(Rect { x: fx, y: fy, width: fill_w, height: fh }, fill_rad, (true, true, true, true), fill_color, ctx);
- }
-
- // 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 {
- 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
- // rects follow the DE-wide corner_shape family, and a squircle knob
- // reads wrong — the thumb should stay round under any corner style.
- let thumb_y = g.y + (g.h - g.thumb_size) / 2.0;
- let thumb_color = if self.dragging { colors::slider_thumb_drag() } else { colors::slider_thumb() };
- // Recessed style: the knob sits IN the well, so its diameter is the
- // flat floor between the walls (each intrudes half its width) —
- // drawn size only; the drag/hit geometry keeps the full thumb_size.
- if let Some((cx, cy, r, c)) = self.thumb_sphere(rect) {
- ctx.sphere(cx, cy, r, c);
- } else {
- rrect(
- Rect { x: thumb_x, y: thumb_y, width: g.thumb_size, height: g.thumb_size },
- 0.0,
- (true, true, true, true),
- thumb_color,
- ctx,
- );
- }
+ self.paint_band(&g, ctx);
}
}
@@ -616,28 +354,15 @@ impl Input for Slider {
// pointer is now — the "slider won't take my scroll" feel.
// The default style keeps the gate: only the widget that
// initiated a gesture keeps it.
- let band = crate::layout::slider_band();
- if !band && !ui.scroll_gesture_new && ui.scroll_initiate_widget_id != Some(ectx.id) {
- if crate::scroll_debug() {
- eprintln!(
- "[scroll] slider {:?}: GATE reject (gesture_new=false, initiator={:?}, me={:?})",
- self.label, ui.scroll_initiate_widget_id, ectx.id
- );
- }
- return false;
- }
let r = ectx.rect;
// Band: spatial acquisition + gesture LATCH. The halo travels
// with the bulge, so adjusting slides it away from the pointer
// — without the latch the value moves a little and stalls
// mid-scroll. Once a gesture engages this slider it keeps it
// until the gesture ends; a new gesture re-acquires by halo.
- let latched =
- band && !ui.scroll_gesture_new && ui.scroll_initiate_widget_id == Some(ectx.id);
+ let latched = !ui.scroll_gesture_new && ui.scroll_initiate_widget_id == Some(ectx.id);
if latched || self.scroll_hit(r, *px, *py) {
- if band || ui.scroll_gesture_new {
- ui.scroll_initiate_widget_id = Some(ectx.id);
- }
+ ui.scroll_initiate_widget_id = Some(ectx.id);
let scroll_amount = delta.notches_y();
let new_val = (self.value - scroll_amount * 0.02).clamp(0.0, 1.0);
let applied = new_val - self.value;
@@ -823,10 +548,6 @@ 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 {
@@ -837,7 +558,6 @@ impl RangeSlider {
active_thumb: None,
drag_offset: 0.0,
label: None,
- recessed: crate::layout::control_relief(),
})
}
@@ -856,16 +576,94 @@ 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 band's height at `x`: the flat band thickness (`style.control.slider.
+/// band_thickness`), rising through a raised-cosine bell to the bulge height
+/// around each of `centers` (`bulge_width` half-span, `bulge_height` peak), the
+/// bell raised to a power so the flanks taper long and the crest stays plump —
+/// mid-digestion, not a triangle; and, for a RangeSlider, one band thickness
+/// more across `range` (between its two swells). Capsule tips: the profile
+/// shrinks over a circular cap inside each track end — the band ends round, not
+/// square-cut, and the well contour and wheel halo (both measured from here)
+/// round with it.
+pub(crate) fn band_profile(track_x: f32, track_w: f32, h: f32, x: f32, centers: &[f32], range: Option<(f32, f32)>) -> f32 {
+ let band_t = crate::layout::slider_band_thickness().max(0.5);
+ let bulge_h = crate::layout::slider_bulge_height().clamp(band_t, h);
+ let bulge_w = crate::layout::slider_bulge_width().max(2.0);
+ let mut bell = 0.0f32;
+ for &vx in centers {
+ let t = ((x - vx) / bulge_w).clamp(-1.0, 1.0);
+ bell = bell.max(0.5 * (1.0 + (std::f32::consts::PI * t).cos()));
+ }
+ let base = if range.is_some_and(|(lo, hi)| x >= lo && x <= hi) { 2.0 * band_t } else { band_t };
+ let h = base + (bulge_h - base) * bell.powf(1.35);
+ let d = (x - track_x).min(track_x + track_w - x);
+ let r = (h * 0.5).max(0.5);
+ if d < r {
+ let t = ((r - d.max(0.0)) / r).min(1.0);
+ return h * (1.0 - t * t).max(0.0).sqrt();
+ }
+ h
+}
+
+/// The band, drawn from its height `profile` (`band_profile`): the well first —
+/// the band appears INSET, a carve whose contour follows the drawn shape a small
+/// gap outside it. The rect recess prims can't follow a bell, so the walls are
+/// hand-shaded per column from the same profile the fill samples: a shadow band
+/// hugging the top contour, a lit band along the bottom (the DE light sits
+/// upper-left), stepped alphas like the legacy banded bevels, amplitude riding
+/// `bevel_depth` like the rocker's `face_light`. Then the band itself, one
+/// column per pixel with a hair of overlap so AA seams can't open. The one
+/// painter behind Slider, RangeSlider and Float3's rows.
+pub(crate) fn paint_band_shape(ctx: &mut PaintCtx, track_x: f32, track_w: f32, cy: f32, color: [f32; 4], profile: &dyn Fn(f32) -> f32) {
+ const WELL_GAP: f32 = 4.0;
+ const WELL_WALL: f32 = 3.0;
+ const WALL_STEPS: usize = 3;
+ let strength = (crate::layout::bevel_depth() / 0.15).clamp(0.0, 2.0);
+ let a_dark = 0.32 * strength;
+ let a_light = 0.16 * strength;
+ let wx0 = track_x - WELL_GAP;
+ let wx1 = track_x + track_w + WELL_GAP;
+ // 1px columns, EXACT widths: translucent shading quads must not overlap (a
+ // seam double-blends into a visible tick) — unlike the opaque band columns
+ // below, which overlap on purpose against AA gaps.
+ let cols = (wx1 - wx0).ceil().max(1.0) as i32;
+ let colw = (wx1 - wx0) / cols as f32;
+ let sub = WELL_WALL / WALL_STEPS as f32;
+ for i in 0..cols {
+ let x = wx0 + i as f32 * colw;
+ let xm = x + colw * 0.5;
+ // Inside the track the contour rides the profile; past the tips it wraps
+ // around them on a WELL_GAP circle — rounded well ends, not square-cut.
+ let c = if xm < track_x {
+ let e = track_x - xm;
+ (WELL_GAP * WELL_GAP - e * e).max(0.0).sqrt()
+ } else if xm > track_x + track_w {
+ let e = xm - (track_x + track_w);
+ (WELL_GAP * WELL_GAP - e * e).max(0.0).sqrt()
+ } else {
+ profile(xm) * 0.5 + WELL_GAP
+ };
+ for k in 0..WALL_STEPS {
+ let fade = 1.0 - k as f32 / WALL_STEPS as f32;
+ // Shadow INSIDE the well below the top contour; the lit lip OUTSIDE
+ // below the bottom contour — the textbox-recess read.
+ ctx.quad(Rect { x, y: cy - c + k as f32 * sub, width: colw, height: sub }, [0.0, 0.0, 0.0, a_dark * fade]);
+ ctx.quad(Rect { x, y: cy + c + k as f32 * sub, width: colw, height: sub }, [1.0, 1.0, 1.0, a_light * fade]);
+ }
+ }
+ let steps = (track_w.ceil() as i32).max(1);
+ let step_w = track_w / steps as f32;
+ for i in 0..steps {
+ let x = track_x + i as f32 * step_w;
+ let h = profile(x + step_w * 0.5);
+ ctx.quad(Rect { x, y: cy - h * 0.5, width: step_w + 0.3, height: h }, color);
}
}
-/// 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
+/// The recessed track's carve, in the labeled composition of the Slider2D pad (and
+/// any well that reaches up into its label strip): 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.
@@ -991,84 +789,23 @@ impl Paint for RangeSlider {
[0.0, 0.0, 0.0, 0.0]
}
- fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
- let r = crate::layout::rangeslider_corner_radius();
- if r > 0.0 {
- Some((r, (true, true, true, true)))
- } else {
- None
- }
- }
-
fn sync_label(&mut self, label: &str) {
self.label = Some(label.to_string());
}
+ /// The band with two swells — one at each end of the range — and the band
+ /// itself a thickness heavier between them, so the range reads as the
+ /// swallowed length. The swells sit where the thumbs' centres were, so the
+ /// drag geometry below is unchanged.
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
let side = side_offset(&self.label);
let (x, y, w, h) = (rect.x + side, rect.y, rect.width - side, rect.height);
- let radius = crate::layout::rangeslider_corner_radius();
- let rounded = radius > 0.0;
- let rc = (rounded, rounded, rounded, rounded);
let thumb_size = h * 0.9;
let range = w - thumb_size;
- let thumb_low_x = x + self.value_low * range;
- let thumb_high_x = x + self.value_high * range;
- let thumb_y = y + (h - thumb_size) / 2.0;
- let highlight = Rect {
- x: thumb_low_x + thumb_size / 2.0,
- y: y + h * 0.35,
- width: thumb_high_x - thumb_low_x,
- height: h * 0.3,
- };
- let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
- colors::rangeslider_thumb_drag()
- } else {
- colors::rangeslider_thumb()
- };
- let high_color = if self.active_thumb == Some(ActiveThumb::High) {
- colors::rangeslider_thumb_drag()
- } else {
- colors::rangeslider_thumb()
- };
-
- let rrect = |r: Rect, rad: f32, corners: (bool, bool, bool, bool), c: [f32; 4], ctx: &mut PaintCtx| {
- if rounded {
- ctx.rounded_rect(r, rad, corners, c);
- } else {
- ctx.quad(r, c);
- }
- };
- 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 },
- if rounded { thumb_size / 2.0 } else { 0.0 },
- (true, true, true, true),
- low_color,
- ctx,
- );
- rrect(
- Rect { x: thumb_high_x, y: thumb_y, width: thumb_size, height: thumb_size },
- if rounded { thumb_size / 2.0 } else { 0.0 },
- (true, true, true, true),
- high_color,
- ctx,
- );
+ let lo = x + self.value_low * range + thumb_size / 2.0;
+ let hi = x + self.value_high * range + thumb_size / 2.0;
+ let color = if self.active_thumb.is_some() { colors::rangeslider_thumb_drag() } else { colors::rangeslider_thumb() };
+ paint_band_shape(ctx, x, w, y + h * 0.5, color, &|px| band_profile(x, w, h, px, &[lo, hi], Some((lo, hi))));
}
}