From ebc2486d22355eb6ef1f4827f4304210969b60eb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 20 Feb 2021 18:29:09 +0100 Subject: [PATCH] Slider: use a DragValue for the value, and implement suffix/prefix --- CHANGELOG.md | 2 + egui/src/widgets/drag_value.rs | 54 +++++--- egui/src/widgets/slider.rs | 116 ++++++++---------- egui_demo_lib/src/apps/demo/widget_gallery.rs | 2 +- 4 files changed, 86 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6a1f2c..0413b3c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add `egui::plot::Plot` to plot some 2D data. * Add `Ui::hyperlink_to(label, url)`. +* Sliders can now have a value prefix and suffix (e.g. "°" as a unit). ### Changed 🔧 * Improve the positioning of tooltips. * Only show tooltips if mouse is still. * `Slider` will now show the value display by default, unless turned off with `.show_value(false)`. +* The `Slider` value is now a `DragValue` which when dragged can pick values outside of the slider range (unless `clamp_to_range` is set). ## 0.9.0 - 2021-02-07 - Light Mode and much more diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 2ed6b28e..a87896ac 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -8,19 +8,19 @@ use crate::*; /// for the borrow checker. type GetSetValue<'a> = Box) -> f64>; -fn get(value_function: &mut GetSetValue<'_>) -> f64 { - (value_function)(None) +fn get(get_set_value: &mut GetSetValue<'_>) -> f64 { + (get_set_value)(None) } -fn set(value_function: &mut GetSetValue<'_>, value: f64) { - (value_function)(Some(value)); +fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { + (get_set_value)(Some(value)); } /// A numeric value that you can change by dragging the number. More compact than a [`Slider`]. #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct DragValue<'a> { - value_function: GetSetValue<'a>, - speed: f32, + get_set_value: GetSetValue<'a>, + speed: f64, prefix: String, suffix: String, clamp_range: RangeInclusive, @@ -29,9 +29,9 @@ pub struct DragValue<'a> { } impl<'a> DragValue<'a> { - pub(crate) fn from_get_set(value_function: impl 'a + FnMut(Option) -> f64) -> Self { + pub(crate) fn from_get_set(get_set_value: impl 'a + FnMut(Option) -> f64) -> Self { Self { - value_function: Box::new(value_function), + get_set_value: Box::new(get_set_value), speed: 1.0, prefix: Default::default(), suffix: Default::default(), @@ -80,8 +80,8 @@ impl<'a> DragValue<'a> { } /// How much the value changes when dragged one point (logical pixel). - pub fn speed(mut self, speed: f32) -> Self { - self.speed = speed; + pub fn speed(mut self, speed: impl Into) -> Self { + self.speed = speed.into(); self } @@ -91,6 +91,11 @@ impl<'a> DragValue<'a> { self } + pub fn clamp_range_f64(mut self, clamp_range: RangeInclusive) -> Self { + self.clamp_range = clamp_range; + self + } + #[deprecated = "Renamed clamp_range"] pub fn range(self, clamp_range: RangeInclusive) -> Self { self.clamp_range(clamp_range) @@ -127,6 +132,11 @@ impl<'a> DragValue<'a> { self } + pub fn max_decimals_opt(mut self, max_decimals: Option) -> Self { + self.max_decimals = 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. @@ -141,7 +151,7 @@ impl<'a> DragValue<'a> { impl<'a> Widget for DragValue<'a> { fn ui(self, ui: &mut Ui) -> Response { let Self { - mut value_function, + mut get_set_value, speed, clamp_range, prefix, @@ -150,13 +160,17 @@ impl<'a> Widget for DragValue<'a> { max_decimals, } = self; - let value = get(&mut value_function); + let value = get(&mut get_set_value); let value = clamp(value, clamp_range.clone()); - let aim_rad = ui.input().physical_pixel_size(); // ui.input().aim_radius(); // TODO + let aim_rad = ui.input().aim_radius() as f64; 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 = emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals); + let value_text = if value == 0.0 { + "0".to_owned() + } else { + emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals) + }; let kb_edit_id = ui.auto_id_with("edit"); let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id); @@ -172,7 +186,7 @@ impl<'a> Widget for DragValue<'a> { ); if let Ok(parsed_value) = value_text.parse() { let parsed_value = clamp(parsed_value, clamp_range); - set(&mut value_function, parsed_value) + set(&mut get_set_value, parsed_value) } if ui.input().key_pressed(Key::Enter) { ui.memory().surrender_kb_focus(kb_edit_id); @@ -198,7 +212,7 @@ impl<'a> Widget for DragValue<'a> { } else if response.dragged() { let mdelta = ui.input().pointer.delta(); let delta_points = mdelta.x - mdelta.y; // Increase to the right and up - let delta_value = speed * delta_points; + let delta_value = delta_points as f64 * speed; if delta_value != 0.0 { // Since we round the value being dragged, we need to store the full precision value in memory: let stored_value = ui @@ -212,15 +226,15 @@ impl<'a> Widget for DragValue<'a> { let rounded_new_value = stored_value; - let aim_delta = ui.input().aim_radius() * speed; + let aim_delta = aim_rad * speed; let rounded_new_value = emath::smart_aim::best_in_range_f64( - rounded_new_value - aim_delta as f64, - rounded_new_value + aim_delta as f64, + rounded_new_value - aim_delta, + rounded_new_value + aim_delta, ); let rounded_new_value = emath::round_to_decimals(rounded_new_value, auto_decimals); let rounded_new_value = clamp(rounded_new_value, clamp_range); - set(&mut value_function, rounded_new_value); + set(&mut get_set_value, rounded_new_value); ui.memory().drag_value = Some((response.id, stored_value)); } diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 2f83f42c..03fca521 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -1,8 +1,8 @@ +#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString` #![allow(clippy::float_cmp)] -use std::ops::RangeInclusive; - use crate::{widgets::Label, *}; +use std::ops::RangeInclusive; // ---------------------------------------------------------------------------- @@ -10,12 +10,12 @@ use crate::{widgets::Label, *}; /// for the borrow checker. type GetSetValue<'a> = Box) -> f64>; -fn get(value_function: &mut GetSetValue<'_>) -> f64 { - (value_function)(None) +fn get(get_set_value: &mut GetSetValue<'_>) -> f64 { + (get_set_value)(None) } -fn set(value_function: &mut GetSetValue<'_>, value: f64) { - (value_function)(Some(value)); +fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { + (get_set_value)(Some(value)); } fn to_f64_range(r: RangeInclusive) -> RangeInclusive @@ -64,6 +64,8 @@ pub struct Slider<'a> { clamp_to_range: bool, smart_aim: bool, show_value: bool, + prefix: String, + suffix: String, text: String, text_color: Option, min_decimals: usize, @@ -86,6 +88,8 @@ impl<'a> Slider<'a> { clamp_to_range: false, smart_aim: true, show_value: true, + prefix: Default::default(), + suffix: Default::default(), text: Default::default(), text_color: None, min_decimals: 0, @@ -159,6 +163,18 @@ impl<'a> Slider<'a> { self } + /// Show a prefix before the number, e.g. "x: " + pub fn prefix(mut self, prefix: impl ToString) -> Self { + self.prefix = prefix.to_string(); + self + } + + /// Add a suffix to the number, this can be e.g. a unit ("°" or " m") + pub fn suffix(mut self, suffix: impl ToString) -> Self { + self.suffix = suffix.to_string(); + self + } + /// Show a text next to the slider (e.g. explaining what the slider controls). pub fn text(mut self, text: impl Into) -> Self { self.text = text.into(); @@ -269,6 +285,14 @@ impl<'a> Slider<'a> { set(&mut self.get_set_value, value); } + fn clamp_range(&self) -> RangeInclusive { + if self.clamp_to_range { + self.range() + } else { + f64::NEG_INFINITY..=f64::INFINITY + } + } + fn range(&self) -> RangeInclusive { self.range.clone() } @@ -360,72 +384,30 @@ impl<'a> Slider<'a> { } fn value_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive) { - let kb_edit_id = ui.auto_id_with("edit"); - let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id); - - let aim_radius = ui.input().aim_radius(); - let value_text = self.format_value(aim_radius, x_range); - - if is_kb_editing { - let button_width = ui.spacing().interact_size.x; - let mut value_text = ui.memory().temp_edit_string.take().unwrap_or(value_text); - ui.add( - TextEdit::singleline(&mut value_text) - .id(kb_edit_id) - .desired_width(button_width) - .text_color_opt(self.text_color) - .text_style(TextStyle::Monospace), - ); - if let Ok(value) = value_text.parse() { - self.set_value(value); - } - if ui.input().key_pressed(Key::Enter) { - ui.memory().surrender_kb_focus(kb_edit_id); - } else { - ui.memory().temp_edit_string = Some(value_text); - } - } else { - let response = ui.add( - Button::new(value_text) - .text_style(TextStyle::Monospace) - .text_color_opt(self.text_color), - ); - 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); - ui.memory().temp_edit_string = None; // Filled in next frame - } + let mut value = self.get_value(); + ui.add( + DragValue::f64(&mut value) + .speed(self.current_gradient(&x_range)) + .clamp_range_f64(self.clamp_range()) + .min_decimals(self.min_decimals) + .max_decimals_opt(self.max_decimals) + .suffix(self.suffix.clone()) + .prefix(self.prefix.clone()), + ); + if value != self.get_value() { + self.set_value(value); } } - fn format_value(&mut self, aim_radius: f32, x_range: RangeInclusive) -> String { + /// delta(value) / delta(points) + fn current_gradient(&mut self, x_range: &RangeInclusive) -> f64 { + // TODO: handle clamping let value = self.get_value(); - - // 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 { - emath::format_with_minimum_decimals(value, max_decimals) - } else if value == 0.0 { - "0".to_owned() - } else if range == 0.0 { - value.to_string() - } else { - emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals) - } + let left_value = value_from_x(x_from_value(value) - 0.5); + let right_value = value_from_x(x_from_value(value) + 0.5); + right_value - left_value } } @@ -457,7 +439,7 @@ impl<'a> Widget for Slider<'a> { // Helpers for converting slider range to/from normalized [0-1] range. // Always clamps. // Logarithmic sliders are allowed to include zero and infinity, -// even though emathematically it doesn't make sense. +// even though mathematically it doesn't make sense. use std::f64::INFINITY; diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 07cd1924..23dd0f43 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -144,7 +144,7 @@ impl WidgetGallery { ui.end_row(); ui.add(doc_link_label("Slider", "Slider")); - ui.add(egui::Slider::f32(scalar, 0.0..=100.0).text("value")); + ui.add(egui::Slider::f32(scalar, 0.0..=360.0).suffix("°")); ui.end_row(); ui.add(doc_link_label("DragValue", "DragValue"));