diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b27c9e4..e6b4afc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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. diff --git a/egui/src/demos/sliders.rs b/egui/src/demos/sliders.rs index 7cf22297..20efe6aa 100644 --- a/egui/src/demos/sliders.rs +++ b/egui/src/demos/sliders.rs @@ -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; diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 9d967248..f22b0ff2 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -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)), ); } diff --git a/egui/src/math/mod.rs b/egui/src/math/mod.rs index 2cb8f867..f43ca576 100644 --- a/egui/src/math/mod.rs +++ b/egui/src/math/mod.rs @@ -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::().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) -> 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::().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" ); } diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 8bf636df..b00438c4 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -23,6 +23,8 @@ pub struct DragValue<'a> { prefix: String, suffix: String, range: RangeInclusive, + min_decimals: usize, + max_decimals: Option, } 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| { - if let Some(v) = v { - *value = v as f32 - } - *value as f64 - }) - } + Self::from_get_set(move |v: Option| { + 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| { - if let Some(v) = v { - *value = v - } - *value - }) - } + Self::from_get_set(move |v: Option| { + if let Some(v) = v { + *value = v + } + *value + }) } pub fn u8(value: &'a mut u8) -> Self { - Self { - ..Self::from_get_set(move |v: Option| { - if let Some(v) = v { - *value = v.round() as u8; - } - *value as f64 - }) - } + Self::from_get_set(move |v: Option| { + 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| { - if let Some(v) = v { - *value = v.round() as i32; - } - *value as f64 - }) - } + Self::from_get_set(move |v: Option| { + 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, diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index afe523fd..86b6c660 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -44,8 +44,9 @@ pub struct Slider<'a> { smart_aim: bool, // TODO: label: Option