feat: enhance PlainSlider to eliminate visual artifacts and improve boundary handling
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-24 19:11:01 +03:00
parent ee6ad2dc20
commit 844fd4a026
2 changed files with 184 additions and 35 deletions
@@ -110,6 +110,25 @@ impl<Message> PlainSlider<Message> {
self.snap(*self.range.start() + (*self.range.end() - *self.range.start()) * fraction)
}
/// Computes the slider value for the current cursor position while dragging.
///
/// **Purpose:** Allow an active drag to continue updating even when the cursor moves
/// slightly outside the widget bounds, so 0 % and 100 % can always be reached.
/// **Logic & Workflow:** If the cursor is inside the widget bounds, use its local X.
/// Otherwise fall back to the global cursor position and convert it to widget-local
/// X by subtracting `bounds.x`. Clamp the resulting X to the draggable track width.
/// **Arguments:** `cursor` is the current mouse cursor; `bounds` is the widget rectangle.
/// **Returns:** The stepped value at the clamped cursor position.
fn drag_value(&self, cursor: mouse::Cursor, bounds: Rectangle) -> f32 {
let track_width = (bounds.width - SPINNER_WIDTH).max(1.0);
let x = match cursor.position_in(bounds) {
Some(local) => local.x,
None => cursor.position().map_or(0.0, |p| p.x - bounds.x),
}
.clamp(0.0, track_width);
self.value_at(x, bounds.width)
}
/// Returns the normalized fill fraction for the current value.
fn fraction(&self) -> f32 {
let span = *self.range.end() - *self.range.start();
@@ -170,22 +189,10 @@ impl<Message> canvas::Program<Message> for PlainSlider<Message> {
let track_width = (bounds.width - SPINNER_WIDTH).max(0.0);
let fill_width = track_width * self.fraction();
if fill_width > 0.0 {
let bands = 4;
for band in 0..bands {
let t = band as f32 / (bands - 1) as f32;
let mut fill = mix_color(self.colors.accent, self.colors.accent_hover, t * 0.55);
fill.a = if self.colors.is_light { 0.30 } else { 0.25 };
frame.fill_rectangle(
Point::new(0.0, bounds.height * band as f32 / bands as f32),
Size::new(fill_width, bounds.height / bands as f32 + 0.5),
fill,
);
}
frame.fill_rectangle(
Point::new((fill_width - 1.5).max(0.0), 1.0),
Size::new(3.0_f32.min(fill_width), (bounds.height - 2.0).max(0.0)),
self.colors.accent,
);
let mut fill = self.colors.accent;
fill.a = if self.colors.is_light { 0.30 } else { 0.25 };
let fill_rect = Path::rounded_rectangle(Point::new(0.0, 0.0), Size::new(fill_width, bounds.height), radius);
frame.fill(&fill_rect, fill);
}
frame.stroke(
@@ -298,14 +305,11 @@ impl<Message> canvas::Program<Message> for PlainSlider<Message> {
(canvas::event::Status::Ignored, None)
}
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
if let Some(position) = local {
let value = self.value_at(position.x, bounds.width);
return (
canvas::event::Status::Captured,
Some((self.on_change)(value)),
);
}
(canvas::event::Status::Captured, None)
let value = self.drag_value(cursor, bounds);
return (
canvas::event::Status::Captured,
Some((self.on_change)(value)),
);
}
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if state.dragging {
@@ -409,17 +413,6 @@ fn is_numeric_fragment(text: &str) -> bool {
.all(|character| character.is_ascii_digit() || matches!(character, '.' | ',' | '-'))
}
/// Blends two theme colors while preserving alpha interpolation.
fn mix_color(from: iced::Color, to: iced::Color, amount: f32) -> iced::Color {
let amount = amount.clamp(0.0, 1.0);
iced::Color::new(
from.r + (to.r - from.r) * amount,
from.g + (to.g - from.g) * amount,
from.b + (to.b - from.b) * amount,
from.a + (to.a - from.a) * amount,
)
}
#[cfg(test)]
mod tests {
use super::{is_numeric_fragment, PlainSlider, State};
@@ -463,4 +456,11 @@ mod tests {
assert_eq!(slider.value_at(100.0, 124.0), 1.0);
assert!((slider.value_at(33.0, 124.0) - 0.33).abs() < 1e-6);
}
#[test]
fn pointer_mapping_clamps_outside_track() {
let slider = test_slider("");
assert_eq!(slider.value_at(-10.0, 124.0), 0.0);
assert_eq!(slider.value_at(120.0, 124.0), 1.0);
}
}