From c385a448b54d9343f2a7a9fd1c127d214f7a42e0 Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sun, 25 Jul 2021 18:14:39 +0200 Subject: [PATCH 1/8] widget: slider: Add stepping functionality A new builder method `with_step(f64)` is added to set the stepping value. Signed-off-by: Christopher N. Hesse --- AUTHORS | 1 + CHANGELOG.md | 1 + druid/src/widget/slider.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 71553959d2..433b42ce09 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,3 +16,4 @@ Robert Wittams Jaap Aarts Maximilian Köstler Bruno Dupuis +Christopher Noel Hesse diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b0671941a..f8fb013958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ You can find its changes [documented below](#070---2021-01-01). - x11: Implement primary_clipboard ([#1867] by [@psychon]) - x11: Set WM_CLASS property ([#1868] by [@psychon]) - Expose `RawWindowHandle` for `WindowHandle` under the `raw-win-handle` feature ([#1828] by [@djeedai]) +- Widget/Slider: Add stepping functionality ([#1875] by [@raymanfx]) ### Changed diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index acd5bf4810..d360d4882b 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -31,6 +31,7 @@ const KNOB_STROKE_WIDTH: f64 = 2.0; pub struct Slider { min: f64, max: f64, + step: Option, knob_pos: Point, knob_hovered: bool, x_offset: f64, @@ -42,6 +43,7 @@ impl Slider { Slider { min: 0., max: 1., + step: None, knob_pos: Default::default(), knob_hovered: Default::default(), x_offset: Default::default(), @@ -56,6 +58,24 @@ impl Slider { self.max = max; self } + + /// Builder-style method to set the stepping. + /// + /// The default step size is `0.0` (smooth). + pub fn with_step(mut self, step: f64) -> Self { + if step < 0.0 { + tracing::warn!("bad stepping (must be positive): {}", step); + return self; + } + self.step = if step > 0.0 { + Some(step) + } else { + // A stepping value of 0.0 would yield an infinite amount of steps. + // Enforce no stepping instead. + None + }; + self + } } impl Slider { @@ -68,7 +88,14 @@ impl Slider { let scalar = ((mouse_x + self.x_offset - knob_width / 2.) / (slider_width - knob_width)) .max(0.0) .min(1.0); - self.min + scalar * (self.max - self.min) + let mut value = self.min + scalar * (self.max - self.min); + if let Some(stepping) = self.step { + // Determine the number of steps and fit the slider position to a discrete step + let steps = (self.max / stepping).round(); + let step = (value * steps / self.max).round(); + value = step / steps * self.max; + } + value } fn normalize(&self, data: f64) -> f64 { From fe7b157d620e2d3fe83ab9e738068f31661e5114 Mon Sep 17 00:00:00 2001 From: Maan2003 Date: Fri, 23 Jul 2021 17:41:52 +0530 Subject: [PATCH 2/8] Handle more cases --- druid/src/widget/slider.rs | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index d360d4882b..6aad43ba16 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -17,7 +17,7 @@ use crate::kurbo::{Circle, Shape}; use crate::widget::prelude::*; use crate::{theme, LinearGradient, Point, Rect, UnitPoint}; -use tracing::{instrument, trace}; +use tracing::{instrument, trace, warn}; const TRACK_THICKNESS: f64 = 4.0; const BORDER_WIDTH: f64 = 2.0; @@ -76,6 +76,31 @@ impl Slider { }; self } + + fn check_step(&mut self) { + if let Some(stepping) = self.step { + // round down max such that max - min is a multiple of step + // say max - min = stepping * n + k + + let n = (self.max - self.min) / stepping; + + // check if n is choose enough to an integer + + // f64::EPSILON doesn't work :/ + if (n.round() - n).abs() > 10e-10 { + let old_max = self.max; + + // floor so that max < old_max, and value remains = min..old_max + let n = n.floor(); + + self.max = self.min + stepping * n; + warn!( + "`max`({}) - `min`({}) should be a multiple of `step`({}), changing max to {}", + old_max, self.min, stepping, self.max + ); + } + } + } } impl Slider { @@ -88,14 +113,21 @@ impl Slider { let scalar = ((mouse_x + self.x_offset - knob_width / 2.) / (slider_width - knob_width)) .max(0.0) .min(1.0); - let mut value = self.min + scalar * (self.max - self.min); if let Some(stepping) = self.step { - // Determine the number of steps and fit the slider position to a discrete step - let steps = (self.max / stepping).round(); - let step = (value * steps / self.max).round(); - value = step / steps * self.max; + // self.max - self.min is always a multiple of stepping + // enforced by `check_step` + + // 0..=steps are the possible steps at which value can be + let steps = ((self.max - self.min) / stepping).round(); + + // scale the scalar from 0 to steps and round it + let curr_step = (scalar * steps).round(); + + // now, value is of form `min + n * stepping` + self.min + curr_step * stepping + } else { + self.min + scalar * (self.max - self.min) } - value } fn normalize(&self, data: f64) -> f64 { @@ -152,8 +184,14 @@ impl Widget for Slider { #[instrument(name = "Slider", level = "trace", skip(self, ctx, event, _data, _env))] fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) { - if let LifeCycle::DisabledChanged(_) = event { - ctx.request_paint(); + match event { + LifeCycle::WidgetAdded => { + self.check_step(); + } + LifeCycle::DisabledChanged(_) => { + ctx.request_paint(); + } + _ => (), } } From fb9003ee74d88770bdf48f57c68598f163209788 Mon Sep 17 00:00:00 2001 From: Maan2003 Date: Fri, 23 Jul 2021 18:03:53 +0530 Subject: [PATCH 3/8] Add stepping slider example to widget gallery --- druid/examples/widget_gallery.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/druid/examples/widget_gallery.rs b/druid/examples/widget_gallery.rs index f4140468d0..f77e16f97e 100644 --- a/druid/examples/widget_gallery.rs +++ b/druid/examples/widget_gallery.rs @@ -152,10 +152,15 @@ fn ui_builder() -> impl Widget { )) .with_child(label_widget( Flex::column() - .with_child(Slider::new().lens(AppData::progressbar)) + .with_child( + Slider::new() + .with_range(0.05, 0.95) + .with_step(0.10) + .lens(AppData::progressbar), + ) .with_spacer(4.0) .with_child(Label::new(|data: &AppData, _: &_| { - format!("{:3.0}%", data.progressbar * 100.0) + format!("{:3.2}%", data.progressbar * 100.) })), "Slider", )) From e3b0783a8430e57540dd7f50b36e0b5a0d26b4b6 Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sat, 24 Jul 2021 04:21:48 +0200 Subject: [PATCH 4/8] widget: slider: Never modify the valid range In case of max % step != 0, the last step is smaller than the others. Signed-off-by: Christopher N. Hesse --- druid/src/widget/slider.rs | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 6aad43ba16..9731cd5e48 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -64,7 +64,7 @@ impl Slider { /// The default step size is `0.0` (smooth). pub fn with_step(mut self, step: f64) -> Self { if step < 0.0 { - tracing::warn!("bad stepping (must be positive): {}", step); + warn!("bad stepping (must be positive): {}", step); return self; } self.step = if step > 0.0 { @@ -77,26 +77,17 @@ impl Slider { self } - fn check_step(&mut self) { + fn check_step(&self) { if let Some(stepping) = self.step { - // round down max such that max - min is a multiple of step - // say max - min = stepping * n + k - let n = (self.max - self.min) / stepping; // check if n is choose enough to an integer // f64::EPSILON doesn't work :/ if (n.round() - n).abs() > 10e-10 { - let old_max = self.max; - - // floor so that max < old_max, and value remains = min..old_max - let n = n.floor(); - - self.max = self.min + stepping * n; warn!( - "`max`({}) - `min`({}) should be a multiple of `step`({}), changing max to {}", - old_max, self.min, stepping, self.max + "max ({}) - min ({}) should be a multiple of step ({})", + self.max, self.min, stepping ); } } @@ -114,17 +105,14 @@ impl Slider { .max(0.0) .min(1.0); if let Some(stepping) = self.step { - // self.max - self.min is always a multiple of stepping - // enforced by `check_step` - // 0..=steps are the possible steps at which value can be - let steps = ((self.max - self.min) / stepping).round(); + let steps = ((self.max - self.min) / stepping).ceil(); // scale the scalar from 0 to steps and round it let curr_step = (scalar * steps).round(); - // now, value is of form `min + n * stepping` - self.min + curr_step * stepping + // now, value is of form `min + n * stepping + rem` + (self.min + curr_step * stepping).min(self.max) } else { self.min + scalar * (self.max - self.min) } From 8e586fd7f50905d279b55c10a35008d9e569c932 Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sun, 25 Jul 2021 12:36:52 +0200 Subject: [PATCH 5/8] widget: slider: Revamp stepping code Handle the edge case where we need an extra step separately. This occurs whenever max % step != 0. Signed-off-by: Christopher N. Hesse --- druid/src/widget/slider.rs | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 9731cd5e48..97f097da32 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -78,16 +78,12 @@ impl Slider { } fn check_step(&self) { - if let Some(stepping) = self.step { - let n = (self.max - self.min) / stepping; - - // check if n is choose enough to an integer - - // f64::EPSILON doesn't work :/ - if (n.round() - n).abs() > 10e-10 { + if let Some(step) = self.step { + let max_step_value = (100.0 / step).round() * step; + if max_step_value < self.max { warn!( "max ({}) - min ({}) should be a multiple of step ({})", - self.max, self.min, stepping + self.max, self.min, step ); } } @@ -104,18 +100,27 @@ impl Slider { let scalar = ((mouse_x + self.x_offset - knob_width / 2.) / (slider_width - knob_width)) .max(0.0) .min(1.0); - if let Some(stepping) = self.step { - // 0..=steps are the possible steps at which value can be - let steps = ((self.max - self.min) / stepping).ceil(); - - // scale the scalar from 0 to steps and round it - let curr_step = (scalar * steps).round(); - - // now, value is of form `min + n * stepping + rem` - (self.min + curr_step * stepping).min(self.max) - } else { - self.min + scalar * (self.max - self.min) + let mut value = self.min + scalar * (self.max - self.min); + if let Some(step) = self.step { + let make_discrete = |value: f64, step: f64, min: f64, max: f64| { + (((value - min) / step).round() * step + min).min(max) + }; + let max_step_value = make_discrete(self.max, step, self.min, self.max); + if value > max_step_value { + // edge case: make sure max is reachable + let left_dist = value - max_step_value; + let right_dist = self.max - value; + value = if left_dist < right_dist { + max_step_value + } else { + self.max + }; + } else { + // snap to discrete intervals + value = make_discrete(value, step, self.min, self.max); + } } + value } fn normalize(&self, data: f64) -> f64 { From 1b91b8bd22db961d64f49032b4352439cfd24521 Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sun, 25 Jul 2021 17:42:24 +0200 Subject: [PATCH 6/8] widget: slider: Remove check_step() We now handle that case and allow for self.max to be reached. Signed-off-by: Christopher N. Hesse --- druid/src/widget/slider.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 97f097da32..41f25d76e6 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -76,18 +76,6 @@ impl Slider { }; self } - - fn check_step(&self) { - if let Some(step) = self.step { - let max_step_value = (100.0 / step).round() * step; - if max_step_value < self.max { - warn!( - "max ({}) - min ({}) should be a multiple of step ({})", - self.max, self.min, step - ); - } - } - } } impl Slider { @@ -178,9 +166,6 @@ impl Widget for Slider { #[instrument(name = "Slider", level = "trace", skip(self, ctx, event, _data, _env))] fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) { match event { - LifeCycle::WidgetAdded => { - self.check_step(); - } LifeCycle::DisabledChanged(_) => { ctx.request_paint(); } From cfdcc876c42320211ef07b502e81d59b45ccbeb9 Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sun, 25 Jul 2021 18:11:15 +0200 Subject: [PATCH 7/8] widget: slider: Fix max_step_value calculation Signed-off-by: Christopher N. Hesse --- druid/src/widget/slider.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 41f25d76e6..28e1377750 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -90,10 +90,7 @@ impl Slider { .min(1.0); let mut value = self.min + scalar * (self.max - self.min); if let Some(step) = self.step { - let make_discrete = |value: f64, step: f64, min: f64, max: f64| { - (((value - min) / step).round() * step + min).min(max) - }; - let max_step_value = make_discrete(self.max, step, self.min, self.max); + let max_step_value = ((self.max - self.min) / step).floor() * step + self.min; if value > max_step_value { // edge case: make sure max is reachable let left_dist = value - max_step_value; @@ -105,7 +102,7 @@ impl Slider { }; } else { // snap to discrete intervals - value = make_discrete(value, step, self.min, self.max); + value = (((value - self.min) / step).round() * step + self.min).min(self.max); } } value From 5a41f52e6a5f14eae6b300bc0517208e60bdf16c Mon Sep 17 00:00:00 2001 From: "Christopher N. Hesse" Date: Sun, 25 Jul 2021 21:05:00 +0200 Subject: [PATCH 8/8] Fix clippy lint Signed-off-by: Christopher N. Hesse --- druid/src/widget/slider.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 28e1377750..f4cbecbec7 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -162,11 +162,8 @@ impl Widget for Slider { #[instrument(name = "Slider", level = "trace", skip(self, ctx, event, _data, _env))] fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) { - match event { - LifeCycle::DisabledChanged(_) => { - ctx.request_paint(); - } - _ => (), + if let LifeCycle::DisabledChanged(_) = event { + ctx.request_paint(); } }