Improve decimal logic for Slider and DragValue
* You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`. * `Slider` and `DragValue` uses fewer decimals by default. See the full precision by hovering over the value.
This commit is contained in:
parent
d6d9c4828e
commit
d022765a3c
6 changed files with 205 additions and 126 deletions
|
@ -14,8 +14,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
* Turn off `Window` title bars with `window.title_bar(false)`.
|
||||
* `ImageButton` - `ui.add(ImageButton::new(...))`.
|
||||
* `ui.vertical_centered` and `ui.vertical_centered_justified`.
|
||||
* Mouse-over explanation to duplicate ID warning
|
||||
* Mouse-over explanation to duplicate ID warning.
|
||||
* You can now easily constrain Egui to a portion of the screen using `RawInput::screen_rect`.
|
||||
* You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`.
|
||||
|
||||
### Changed 🔧
|
||||
|
||||
|
@ -23,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
* `SidePanel::left` and `TopPanel::top` now takes `impl Hash` as first argument.
|
||||
* `ui.image` now takes `impl Into<Vec2>` as a `size` argument.
|
||||
* Made some more fields of `RawInput` optional.
|
||||
* `Slider` and `DragValue` uses fewer decimals by default. See the full precision by hovering over the value.
|
||||
|
||||
### Deprecated
|
||||
* Deprecated `RawInput::screen_size` - use `RawInput::screen_rect` instead.
|
||||
|
|
|
@ -67,7 +67,10 @@ impl Sliders {
|
|||
.text("f64 demo slider"),
|
||||
);
|
||||
|
||||
ui.label("Sliders will automatically figure out how many decimals to show.");
|
||||
ui.label(
|
||||
"Sliders will intelligently pick how many decimals to show. \
|
||||
You can always see the full precision value by hovering the value.",
|
||||
);
|
||||
|
||||
if ui.button("Assign PI").clicked {
|
||||
self.value = std::f64::consts::PI;
|
||||
|
|
|
@ -60,7 +60,7 @@ impl paint::FontDefinitions {
|
|||
// TODO: radio button for family
|
||||
ui.add(
|
||||
Slider::f32(size, 4.0..=40.0)
|
||||
.precision(0)
|
||||
.max_decimals(0)
|
||||
.text(format!("{:?}", text_style)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -121,26 +121,41 @@ pub fn ease_in_ease_out(t: f32) -> f32 {
|
|||
pub const TAU: f32 = 2.0 * std::f32::consts::PI;
|
||||
|
||||
/// Round a value to the given number of decimal places.
|
||||
pub fn round_to_precision(value: f64, decimal_places: usize) -> f64 {
|
||||
pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
|
||||
// This is a stupid way of doing this, but stupid works.
|
||||
format!("{:.*}", decimal_places, value)
|
||||
.parse()
|
||||
.unwrap_or(value)
|
||||
}
|
||||
|
||||
pub fn format_with_minimum_precision(value: f32, precision: usize) -> String {
|
||||
debug_assert!(precision < 100);
|
||||
let precision = precision.min(16);
|
||||
let text = format!("{:.*}", precision, value);
|
||||
let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
|
||||
if almost_equal(text.parse::<f32>().unwrap(), value, epsilon) {
|
||||
// Enough precision to show the value accurately - good!
|
||||
text
|
||||
pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
|
||||
format_with_decimals_in_range(value, decimals..=6)
|
||||
}
|
||||
|
||||
pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
|
||||
let min_decimals = *decimal_range.start();
|
||||
let max_decimals = *decimal_range.end();
|
||||
debug_assert!(min_decimals <= max_decimals);
|
||||
debug_assert!(max_decimals < 100);
|
||||
let max_decimals = max_decimals.min(16);
|
||||
let min_decimals = min_decimals.min(max_decimals);
|
||||
|
||||
if min_decimals == max_decimals {
|
||||
format!("{:.*}", max_decimals, value)
|
||||
} else {
|
||||
// Ugly/slow way of doing this. TODO: clean up precision.
|
||||
for decimals in min_decimals..max_decimals {
|
||||
let text = format!("{:.*}", decimals, value);
|
||||
let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
|
||||
if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
|
||||
// Enough precision to show the value accurately - good!
|
||||
return text;
|
||||
}
|
||||
}
|
||||
// The value has more precision than we expected.
|
||||
// Probably the value was set not by the slider, but from outside.
|
||||
// In any case: show the full value
|
||||
value.to_string()
|
||||
format!("{:.*}", max_decimals, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,12 +177,13 @@ pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
|
|||
#[allow(clippy::approx_constant)]
|
||||
#[test]
|
||||
fn test_format() {
|
||||
assert_eq!(format_with_minimum_precision(1_234_567.0, 0), "1234567");
|
||||
assert_eq!(format_with_minimum_precision(1_234_567.0, 1), "1234567.0");
|
||||
assert_eq!(format_with_minimum_precision(3.14, 2), "3.14");
|
||||
assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
|
||||
assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
|
||||
assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
|
||||
assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
|
||||
assert_eq!(
|
||||
format_with_minimum_precision(std::f32::consts::PI, 2),
|
||||
"3.1415927"
|
||||
format_with_minimum_decimals(std::f64::consts::PI, 2),
|
||||
"3.14159"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ pub struct DragValue<'a> {
|
|||
prefix: String,
|
||||
suffix: String,
|
||||
range: RangeInclusive<f64>,
|
||||
min_decimals: usize,
|
||||
max_decimals: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> DragValue<'a> {
|
||||
|
@ -33,51 +35,47 @@ impl<'a> DragValue<'a> {
|
|||
prefix: Default::default(),
|
||||
suffix: Default::default(),
|
||||
range: f64::NEG_INFINITY..=f64::INFINITY,
|
||||
min_decimals: 0,
|
||||
max_decimals: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f32(value: &'a mut f32) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v as f32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v as f32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
|
||||
pub fn f64(value: &'a mut f64) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v
|
||||
}
|
||||
*value
|
||||
})
|
||||
}
|
||||
Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v
|
||||
}
|
||||
*value
|
||||
})
|
||||
}
|
||||
|
||||
pub fn u8(value: &'a mut u8) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u8;
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u8;
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.max_decimals(0)
|
||||
}
|
||||
|
||||
pub fn i32(value: &'a mut i32) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as i32;
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as i32;
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.max_decimals(0)
|
||||
}
|
||||
|
||||
/// How much the value changes when dragged one point (logical pixel).
|
||||
|
@ -103,6 +101,35 @@ impl<'a> DragValue<'a> {
|
|||
self.suffix = suffix.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: we should also have a "min precision".
|
||||
/// Set a minimum number of decimals to display.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
|
||||
self.min_decimals = min_decimals;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: we should also have a "max precision".
|
||||
/// Set a maximum number of decimals to display.
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn max_decimals(mut self, max_decimals: usize) -> Self {
|
||||
self.max_decimals = Some(max_decimals);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an exact number of decimals to display.
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn fixed_decimals(mut self, num_decimals: usize) -> Self {
|
||||
self.min_decimals = num_decimals;
|
||||
self.max_decimals = Some(num_decimals);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for DragValue<'a> {
|
||||
|
@ -113,11 +140,16 @@ impl<'a> Widget for DragValue<'a> {
|
|||
range,
|
||||
prefix,
|
||||
suffix,
|
||||
min_decimals,
|
||||
max_decimals,
|
||||
} = self;
|
||||
|
||||
let value = get(&mut value_function);
|
||||
let aim_rad = ui.input().physical_pixel_size(); // ui.input().aim_radius(); // TODO
|
||||
let precision = (aim_rad / speed.abs()).log10().ceil().at_least(0.0) as usize;
|
||||
let value_text = format_with_minimum_precision(value as f32, precision); // TODO: full precision
|
||||
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().at_least(0.0) as usize;
|
||||
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
||||
let auto_decimals = clamp(auto_decimals, min_decimals..=max_decimals);
|
||||
let value_text = format_with_decimals_in_range(value, auto_decimals..=max_decimals);
|
||||
|
||||
let kb_edit_id = ui.make_position_id().with("edit");
|
||||
let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id);
|
||||
|
@ -146,7 +178,12 @@ impl<'a> Widget for DragValue<'a> {
|
|||
.sense(Sense::click_and_drag())
|
||||
.text_style(TextStyle::Monospace);
|
||||
let response = ui.add(button);
|
||||
let response = response.on_hover_text("Drag to edit or click to enter a value");
|
||||
let response = response.on_hover_text(format!(
|
||||
" {}{}{}\nDrag to edit or click to enter a value.",
|
||||
prefix,
|
||||
value as f32, // Show full precision value on-hover. TODO: figure out f64 vs f32
|
||||
suffix
|
||||
));
|
||||
if response.clicked {
|
||||
ui.memory().request_kb_focus(kb_edit_id);
|
||||
ui.memory().temp_edit_string = None; // Filled in next frame
|
||||
|
@ -156,7 +193,7 @@ impl<'a> Widget for DragValue<'a> {
|
|||
let delta_value = speed * delta_points;
|
||||
if delta_value != 0.0 {
|
||||
let new_value = value + delta_value as f64;
|
||||
let new_value = round_to_precision(new_value, precision);
|
||||
let new_value = round_to_decimals(new_value, auto_decimals);
|
||||
let new_value = clamp(new_value, range);
|
||||
set(&mut value_function, new_value);
|
||||
// TODO: To make use or `smart_aim` for `DragValue` we need to store some state somewhere,
|
||||
|
|
|
@ -44,8 +44,9 @@ pub struct Slider<'a> {
|
|||
smart_aim: bool,
|
||||
// TODO: label: Option<Label>
|
||||
text: Option<String>,
|
||||
precision: Option<usize>,
|
||||
text_color: Option<Srgba>,
|
||||
min_decimals: usize,
|
||||
max_decimals: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Slider<'a> {
|
||||
|
@ -62,79 +63,68 @@ impl<'a> Slider<'a> {
|
|||
},
|
||||
smart_aim: true,
|
||||
text: None,
|
||||
precision: None,
|
||||
text_color: None,
|
||||
min_decimals: 0,
|
||||
max_decimals: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f32(value: &'a mut f32, range: RangeInclusive<f32>) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v as f32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v as f32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
|
||||
pub fn f64(value: &'a mut f64, range: RangeInclusive<f64>) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v
|
||||
}
|
||||
*value
|
||||
})
|
||||
}
|
||||
Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v
|
||||
}
|
||||
*value
|
||||
})
|
||||
}
|
||||
|
||||
pub fn u8(value: &'a mut u8, range: RangeInclusive<u8>) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u8
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u8
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.integer()
|
||||
}
|
||||
|
||||
pub fn i32(value: &'a mut i32, range: RangeInclusive<i32>) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as i32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as i32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.integer()
|
||||
}
|
||||
|
||||
pub fn u32(value: &'a mut u32, range: RangeInclusive<u32>) -> Self {
|
||||
Self {
|
||||
..Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(to_f64_range(range), move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as u32
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.integer()
|
||||
}
|
||||
|
||||
pub fn usize(value: &'a mut usize, range: RangeInclusive<usize>) -> Self {
|
||||
let range = (*range.start() as f64)..=(*range.end() as f64);
|
||||
Self {
|
||||
..Self::from_get_set(range, move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as usize
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
}
|
||||
Self::from_get_set(range, move |v: Option<f64>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as usize
|
||||
}
|
||||
*value as f64
|
||||
})
|
||||
.integer()
|
||||
}
|
||||
|
||||
|
@ -172,12 +162,37 @@ impl<'a> Slider<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Precision (number of decimals) used when displaying the value.
|
||||
/// Values will also be rounded to this precision.
|
||||
#[deprecated = "Use fixed_decimals instea"]
|
||||
pub fn precision(self, precision: usize) -> Self {
|
||||
self.max_decimals(precision)
|
||||
}
|
||||
|
||||
// TODO: we should also have a "min precision".
|
||||
/// Set a minimum number of decimals to display.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// 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 {
|
||||
self.precision = Some(precision);
|
||||
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
|
||||
self.min_decimals = min_decimals;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: we should also have a "max precision".
|
||||
/// Set a maximum number of decimals to display.
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn max_decimals(mut self, max_decimals: usize) -> Self {
|
||||
self.max_decimals = Some(max_decimals);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an exact number of decimals to display.
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn fixed_decimals(mut self, num_decimals: usize) -> Self {
|
||||
self.min_decimals = num_decimals;
|
||||
self.max_decimals = Some(num_decimals);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -185,7 +200,7 @@ impl<'a> Slider<'a> {
|
|||
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
||||
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
||||
pub fn integer(self) -> Self {
|
||||
self.precision(0).smallest_positive(1.0)
|
||||
self.fixed_decimals(0).smallest_positive(1.0)
|
||||
}
|
||||
|
||||
fn get_value(&mut self) -> f64 {
|
||||
|
@ -193,8 +208,8 @@ impl<'a> Slider<'a> {
|
|||
}
|
||||
|
||||
fn set_value(&mut self, mut value: f64) {
|
||||
if let Some(precision) = self.precision {
|
||||
value = round_to_precision(value, precision);
|
||||
if let Some(max_decimals) = self.max_decimals {
|
||||
value = round_to_decimals(value, max_decimals);
|
||||
}
|
||||
set(&mut self.get_set_value, value);
|
||||
}
|
||||
|
@ -325,7 +340,10 @@ impl<'a> Slider<'a> {
|
|||
.text_style(TextStyle::Monospace)
|
||||
.text_color_opt(self.text_color),
|
||||
);
|
||||
let response = response.on_hover_text("Click to enter a value");
|
||||
let response = response.on_hover_text(format!(
|
||||
"{}\nClick to enter a value.",
|
||||
self.get_value() as f32 // Show full precision value on-hover. TODO: figure out f64 vs f32
|
||||
));
|
||||
// let response = ui.interact(response.rect, kb_edit_id, Sense::click());
|
||||
if response.clicked {
|
||||
ui.memory().request_kb_focus(kb_edit_id);
|
||||
|
@ -337,23 +355,26 @@ impl<'a> Slider<'a> {
|
|||
fn format_value(&mut self, aim_radius: f32, x_range: RangeInclusive<f32>) -> String {
|
||||
let value = self.get_value();
|
||||
|
||||
if let Some(precision) = self.precision {
|
||||
format_with_minimum_precision(value as f32, precision)
|
||||
// pick precision based upon how much moving the slider would change the value:
|
||||
let value_from_x = |x: f32| self.value_from_x(x, x_range.clone());
|
||||
let x_from_value = |value: f64| self.x_from_value(value, x_range.clone());
|
||||
let left_value = value_from_x(x_from_value(value) - aim_radius);
|
||||
let right_value = value_from_x(x_from_value(value) + aim_radius);
|
||||
let range = (left_value - right_value).abs();
|
||||
let auto_decimals = ((-range.log10()).ceil().at_least(0.0) as usize).at_most(16);
|
||||
let min_decimals = self.min_decimals;
|
||||
let max_decimals = self.max_decimals.unwrap_or(auto_decimals + 2);
|
||||
|
||||
let auto_decimals = clamp(auto_decimals, min_decimals..=max_decimals);
|
||||
|
||||
if min_decimals == max_decimals {
|
||||
format_with_minimum_decimals(value, max_decimals)
|
||||
} else if value == 0.0 {
|
||||
"0".to_owned()
|
||||
} else if range == 0.0 {
|
||||
value.to_string()
|
||||
} else {
|
||||
// pick precision based upon how much moving the slider would change the value:
|
||||
let value_from_x = |x: f32| self.value_from_x(x, x_range.clone());
|
||||
let x_from_value = |value: f64| self.x_from_value(value, x_range.clone());
|
||||
let left_value = value_from_x(x_from_value(value) - aim_radius);
|
||||
let right_value = value_from_x(x_from_value(value) + aim_radius);
|
||||
let range = (left_value - right_value).abs();
|
||||
if range == 0.0 {
|
||||
value.to_string()
|
||||
} else {
|
||||
let precision = ((-range.log10()).ceil().at_least(0.0) as usize).at_most(16);
|
||||
format_with_minimum_precision(value as f32, precision)
|
||||
}
|
||||
format_with_decimals_in_range(value, auto_decimals..=max_decimals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue