feat: enhance PlainSlider to eliminate visual artifacts and improve boundary handling
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user