[slider] smart aim for picking nice and round slider values
This commit is contained in:
parent
d0bfb0238d
commit
f2b23f1a0d
5 changed files with 189 additions and 1 deletions
|
@ -244,6 +244,18 @@ impl InputState {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Size of a physical pixel in logical gui coordinates (points).
|
||||||
|
pub fn physical_pixel_size(&self) -> f32 {
|
||||||
|
1.0 / self.pixels_per_point
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How imprecise do we expect the mouse/touch input to be?
|
||||||
|
/// Returns imprecision in points.
|
||||||
|
pub fn aim_radius(&self) -> f32 {
|
||||||
|
// TODO: multiply by ~3 for touch inputs because fingers are fat
|
||||||
|
self.physical_pixel_size()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseInput {
|
impl MouseInput {
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::ops::{Add, Mul, RangeInclusive};
|
||||||
mod movement_tracker;
|
mod movement_tracker;
|
||||||
mod pos2;
|
mod pos2;
|
||||||
mod rect;
|
mod rect;
|
||||||
|
pub mod smart_aim;
|
||||||
mod vec2;
|
mod vec2;
|
||||||
|
|
||||||
pub use {movement_tracker::*, pos2::*, rect::*, vec2::*};
|
pub use {movement_tracker::*, pos2::*, rect::*, vec2::*};
|
||||||
|
|
160
egui/src/math/smart_aim.rs
Normal file
160
egui/src/math/smart_aim.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
#![allow(clippy::float_cmp)] // I know what I'm doing
|
||||||
|
|
||||||
|
const NUM_DECIMALS: usize = 15;
|
||||||
|
|
||||||
|
pub fn best_in_range_f32(min: f32, max: f32) -> f32 {
|
||||||
|
best_in_range_f64(min as f64, max as f64) as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits.
|
||||||
|
///
|
||||||
|
/// So in the range `[0.83, 1.354]` you will get `1.0`, and for `[0.37, 0.48]` you will get `0.4`.
|
||||||
|
/// This is used when dragging sliders etc to get the values that users are most likely to desire.
|
||||||
|
/// This assumes a decimal centric user.
|
||||||
|
pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
|
||||||
|
// Avoid NaN if we can:
|
||||||
|
if min.is_nan() {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
if max.is_nan() {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if max < min {
|
||||||
|
return best_in_range_f64(max, min);
|
||||||
|
}
|
||||||
|
if min == max {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
if min <= 0.0 && 0.0 <= max {
|
||||||
|
return 0.0; // always prefer zero
|
||||||
|
}
|
||||||
|
if min < 0.0 {
|
||||||
|
return -best_in_range_f64(-max, -min);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer finite numbers:
|
||||||
|
if !max.is_finite() {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
debug_assert!(min.is_finite() && max.is_finite());
|
||||||
|
|
||||||
|
let min_exponent = min.log10();
|
||||||
|
let max_exponent = max.log10();
|
||||||
|
|
||||||
|
if min_exponent.floor() != max_exponent.floor() {
|
||||||
|
// pick the geometric center of the two:
|
||||||
|
let exponent = (min_exponent + max_exponent) / 2.0;
|
||||||
|
return 10.0_f64.powi(exponent.round() as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_integer(min_exponent) {
|
||||||
|
return 10.0_f64.powf(min_exponent);
|
||||||
|
}
|
||||||
|
if is_integer(max_exponent) {
|
||||||
|
return 10.0_f64.powf(max_exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exp_factor = 10.0_f64.powi(max_exponent.floor() as i32);
|
||||||
|
|
||||||
|
let min_str = to_decimal_string(min / exp_factor);
|
||||||
|
let max_str = to_decimal_string(max / exp_factor);
|
||||||
|
|
||||||
|
// eprintln!("min_str: {:?}", min_str);
|
||||||
|
// eprintln!("max_str: {:?}", max_str);
|
||||||
|
|
||||||
|
let mut ret_str = [0; NUM_DECIMALS];
|
||||||
|
|
||||||
|
// Select the common prefix:
|
||||||
|
let mut i = 0;
|
||||||
|
while i < NUM_DECIMALS && max_str[i] == min_str[i] {
|
||||||
|
ret_str[i] = max_str[i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < NUM_DECIMALS {
|
||||||
|
// Pick the deciding digit.
|
||||||
|
// Note that "to_decimal_string" rounds down, so we that's why we add 1 here
|
||||||
|
ret_str[i] = simplest_digit_closed_range(min_str[i] + 1, max_str[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
from_decimal_string(&ret_str) * exp_factor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_integer(f: f64) -> bool {
|
||||||
|
f.round() == f
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_decimal_string(v: f64) -> [i32; NUM_DECIMALS] {
|
||||||
|
debug_assert!(v < 10.0, "{:?}", v);
|
||||||
|
let mut digits = [0; NUM_DECIMALS];
|
||||||
|
let mut v = v.abs();
|
||||||
|
for r in digits.iter_mut() {
|
||||||
|
let digit = v.floor();
|
||||||
|
*r = digit as i32;
|
||||||
|
v -= digit;
|
||||||
|
v *= 10.0;
|
||||||
|
}
|
||||||
|
digits
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_decimal_string(s: &[i32]) -> f64 {
|
||||||
|
let mut ret: f64 = 0.0;
|
||||||
|
for (i, &digit) in s.iter().enumerate() {
|
||||||
|
ret += (digit as f64) * 10.0_f64.powi(-(i as i32));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the simplest integer in the range [min, max]
|
||||||
|
fn simplest_digit_closed_range(min: i32, max: i32) -> i32 {
|
||||||
|
debug_assert!(1 <= min && min <= max && max <= 9);
|
||||||
|
if min <= 5 && 5 <= max {
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
(min + max) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aim() {
|
||||||
|
assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero");
|
||||||
|
assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero");
|
||||||
|
assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero");
|
||||||
|
assert_eq!(best_in_range_f64(0.2, 0.0), 0.0, "Prefer zero");
|
||||||
|
assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
|
||||||
|
assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
|
||||||
|
assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
|
||||||
|
assert_eq!(best_in_range_f64(0.4, 0.9), 0.5, "Prefer ending on 5");
|
||||||
|
assert_eq!(best_in_range_f64(14.1, 19.99), 15.0, "Prefer ending on 5");
|
||||||
|
assert_eq!(best_in_range_f64(12.3, 65.9), 50.0, "Prefer leading 5");
|
||||||
|
assert_eq!(best_in_range_f64(493.0, 879.0), 500.0, "Prefer leading 5");
|
||||||
|
assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
|
||||||
|
// assert_eq!(best_in_range_f64(123.71, 123.76), 123.75); // TODO: we get 123.74999999999999 here
|
||||||
|
assert_eq!(best_in_range_f32(123.71, 123.76), 123.75); // TODO: we get 123.74999999999999 here
|
||||||
|
assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
|
||||||
|
assert_eq!(best_in_range_f64(7.5, 76.3), 10.0);
|
||||||
|
assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
|
||||||
|
assert_eq!(best_in_range_f64(7.5, 1_345.0), 100.0);
|
||||||
|
assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0, "Geometric mean");
|
||||||
|
assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
|
||||||
|
assert_eq!(best_in_range_f64(10.000, 99.999), 10.0);
|
||||||
|
assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
|
||||||
|
assert_eq!(best_in_range_f64(10.001, 100.000), 100.0);
|
||||||
|
assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
|
||||||
|
assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
|
||||||
|
|
||||||
|
use std::f64::{INFINITY, NAN, NEG_INFINITY};
|
||||||
|
assert!(best_in_range_f64(NAN, NAN).is_nan());
|
||||||
|
assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
|
||||||
|
assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);
|
||||||
|
assert_eq!(best_in_range_f64(1.2, NAN), 1.2);
|
||||||
|
assert_eq!(best_in_range_f64(1.2, INFINITY), 1.2);
|
||||||
|
assert_eq!(best_in_range_f64(INFINITY, 1.2), 1.2);
|
||||||
|
assert_eq!(best_in_range_f64(NEG_INFINITY, 1.2), 0.0);
|
||||||
|
assert_eq!(best_in_range_f64(NEG_INFINITY, -2.7), -2.7);
|
||||||
|
assert_eq!(best_in_range_f64(INFINITY, INFINITY), INFINITY);
|
||||||
|
assert_eq!(best_in_range_f64(NEG_INFINITY, NEG_INFINITY), NEG_INFINITY);
|
||||||
|
assert_eq!(best_in_range_f64(NEG_INFINITY, INFINITY), 0.0);
|
||||||
|
assert_eq!(best_in_range_f64(INFINITY, NEG_INFINITY), 0.0);
|
||||||
|
}
|
|
@ -581,6 +581,8 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
if delta_value != 0.0 {
|
if delta_value != 0.0 {
|
||||||
*value += delta_value;
|
*value += delta_value;
|
||||||
*value = round_to_precision(*value, precision);
|
*value = round_to_precision(*value, precision);
|
||||||
|
// TODO: To make use or `smart_aim` for `DragValue` we need to store some state somewhere,
|
||||||
|
// otherwise we will just keep rounding to the same value while moving the mouse.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interact.into()
|
interact.into()
|
||||||
|
|
|
@ -82,6 +82,9 @@ impl<'a> Slider<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Precision (number of decimals) used when displaying the value.
|
||||||
|
/// Values will also be rounded to this precision.
|
||||||
|
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||||
pub fn precision(mut self, precision: usize) -> Self {
|
pub fn precision(mut self, precision: usize) -> Self {
|
||||||
self.precision = precision;
|
self.precision = precision;
|
||||||
self
|
self
|
||||||
|
@ -95,6 +98,11 @@ impl<'a> Slider<'a> {
|
||||||
value = round_to_precision(value, self.precision);
|
value = round_to_precision(value, self.precision);
|
||||||
(self.get_set_value)(Some(value));
|
(self.get_set_value)(Some(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For instance, `point` is the mouse position and `point_range` is the physical location of the slider on the screen.
|
||||||
|
fn value_from_point(&self, point: f32, point_range: RangeInclusive<f32>) -> f32 {
|
||||||
|
remap_clamp(point, point_range, self.range.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Slider<'a> {
|
impl<'a> Widget for Slider<'a> {
|
||||||
|
@ -154,7 +162,12 @@ impl<'a> Widget for Slider<'a> {
|
||||||
|
|
||||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||||
if interact.active {
|
if interact.active {
|
||||||
self.set_value_f32(remap_clamp(mouse_pos.x, left..=right, range.clone()));
|
let aim_radius = ui.input().aim_radius();
|
||||||
|
let new_value = crate::math::smart_aim::best_in_range_f32(
|
||||||
|
self.value_from_point(mouse_pos.x - aim_radius, left..=right),
|
||||||
|
self.value_from_point(mouse_pos.x + aim_radius, left..=right),
|
||||||
|
);
|
||||||
|
self.set_value_f32(new_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue