Slider: use a DragValue for the value, and implement suffix/prefix

This commit is contained in:
Emil Ernerfeldt 2021-02-20 18:29:09 +01:00
parent 32f35c6251
commit ebc2486d22
4 changed files with 86 additions and 88 deletions

View file

@ -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 `egui::plot::Plot` to plot some 2D data.
* Add `Ui::hyperlink_to(label, url)`. * Add `Ui::hyperlink_to(label, url)`.
* Sliders can now have a value prefix and suffix (e.g. "°" as a unit).
### Changed 🔧 ### Changed 🔧
* Improve the positioning of tooltips. * Improve the positioning of tooltips.
* Only show tooltips if mouse is still. * Only show tooltips if mouse is still.
* `Slider` will now show the value display by default, unless turned off with `.show_value(false)`. * `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 ## 0.9.0 - 2021-02-07 - Light Mode and much more

View file

@ -8,19 +8,19 @@ use crate::*;
/// for the borrow checker. /// for the borrow checker.
type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>; type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>;
fn get(value_function: &mut GetSetValue<'_>) -> f64 { fn get(get_set_value: &mut GetSetValue<'_>) -> f64 {
(value_function)(None) (get_set_value)(None)
} }
fn set(value_function: &mut GetSetValue<'_>, value: f64) { fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
(value_function)(Some(value)); (get_set_value)(Some(value));
} }
/// A numeric value that you can change by dragging the number. More compact than a [`Slider`]. /// 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);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct DragValue<'a> { pub struct DragValue<'a> {
value_function: GetSetValue<'a>, get_set_value: GetSetValue<'a>,
speed: f32, speed: f64,
prefix: String, prefix: String,
suffix: String, suffix: String,
clamp_range: RangeInclusive<f64>, clamp_range: RangeInclusive<f64>,
@ -29,9 +29,9 @@ pub struct DragValue<'a> {
} }
impl<'a> DragValue<'a> { impl<'a> DragValue<'a> {
pub(crate) fn from_get_set(value_function: impl 'a + FnMut(Option<f64>) -> f64) -> Self { pub(crate) fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> f64) -> Self {
Self { Self {
value_function: Box::new(value_function), get_set_value: Box::new(get_set_value),
speed: 1.0, speed: 1.0,
prefix: Default::default(), prefix: Default::default(),
suffix: Default::default(), suffix: Default::default(),
@ -80,8 +80,8 @@ impl<'a> DragValue<'a> {
} }
/// How much the value changes when dragged one point (logical pixel). /// How much the value changes when dragged one point (logical pixel).
pub fn speed(mut self, speed: f32) -> Self { pub fn speed(mut self, speed: impl Into<f64>) -> Self {
self.speed = speed; self.speed = speed.into();
self self
} }
@ -91,6 +91,11 @@ impl<'a> DragValue<'a> {
self self
} }
pub fn clamp_range_f64(mut self, clamp_range: RangeInclusive<f64>) -> Self {
self.clamp_range = clamp_range;
self
}
#[deprecated = "Renamed clamp_range"] #[deprecated = "Renamed clamp_range"]
pub fn range(self, clamp_range: RangeInclusive<f32>) -> Self { pub fn range(self, clamp_range: RangeInclusive<f32>) -> Self {
self.clamp_range(clamp_range) self.clamp_range(clamp_range)
@ -127,6 +132,11 @@ impl<'a> DragValue<'a> {
self self
} }
pub fn max_decimals_opt(mut self, max_decimals: Option<usize>) -> Self {
self.max_decimals = max_decimals;
self
}
/// Set an exact number of decimals to display. /// Set an exact number of decimals to display.
/// Values will also be rounded to this number of decimals. /// 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. /// 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> { impl<'a> Widget for DragValue<'a> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Self { let Self {
mut value_function, mut get_set_value,
speed, speed,
clamp_range, clamp_range,
prefix, prefix,
@ -150,13 +160,17 @@ impl<'a> Widget for DragValue<'a> {
max_decimals, max_decimals,
} = self; } = self;
let value = get(&mut value_function); let value = get(&mut get_set_value);
let value = clamp(value, clamp_range.clone()); 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 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 max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
let auto_decimals = clamp(auto_decimals, min_decimals..=max_decimals); 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 kb_edit_id = ui.auto_id_with("edit");
let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id); 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() { if let Ok(parsed_value) = value_text.parse() {
let parsed_value = clamp(parsed_value, clamp_range); 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) { if ui.input().key_pressed(Key::Enter) {
ui.memory().surrender_kb_focus(kb_edit_id); ui.memory().surrender_kb_focus(kb_edit_id);
@ -198,7 +212,7 @@ impl<'a> Widget for DragValue<'a> {
} else if response.dragged() { } else if response.dragged() {
let mdelta = ui.input().pointer.delta(); let mdelta = ui.input().pointer.delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up 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 { if delta_value != 0.0 {
// Since we round the value being dragged, we need to store the full precision value in memory: // Since we round the value being dragged, we need to store the full precision value in memory:
let stored_value = ui let stored_value = ui
@ -212,15 +226,15 @@ impl<'a> Widget for DragValue<'a> {
let rounded_new_value = stored_value; 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( let rounded_new_value = emath::smart_aim::best_in_range_f64(
rounded_new_value - aim_delta as f64, rounded_new_value - aim_delta,
rounded_new_value + aim_delta as f64, rounded_new_value + aim_delta,
); );
let rounded_new_value = let rounded_new_value =
emath::round_to_decimals(rounded_new_value, auto_decimals); emath::round_to_decimals(rounded_new_value, auto_decimals);
let rounded_new_value = clamp(rounded_new_value, clamp_range); 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)); ui.memory().drag_value = Some((response.id, stored_value));
} }

View file

@ -1,8 +1,8 @@
#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString`
#![allow(clippy::float_cmp)] #![allow(clippy::float_cmp)]
use std::ops::RangeInclusive;
use crate::{widgets::Label, *}; use crate::{widgets::Label, *};
use std::ops::RangeInclusive;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -10,12 +10,12 @@ use crate::{widgets::Label, *};
/// for the borrow checker. /// for the borrow checker.
type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>; type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>;
fn get(value_function: &mut GetSetValue<'_>) -> f64 { fn get(get_set_value: &mut GetSetValue<'_>) -> f64 {
(value_function)(None) (get_set_value)(None)
} }
fn set(value_function: &mut GetSetValue<'_>, value: f64) { fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
(value_function)(Some(value)); (get_set_value)(Some(value));
} }
fn to_f64_range<T: Copy>(r: RangeInclusive<T>) -> RangeInclusive<f64> fn to_f64_range<T: Copy>(r: RangeInclusive<T>) -> RangeInclusive<f64>
@ -64,6 +64,8 @@ pub struct Slider<'a> {
clamp_to_range: bool, clamp_to_range: bool,
smart_aim: bool, smart_aim: bool,
show_value: bool, show_value: bool,
prefix: String,
suffix: String,
text: String, text: String,
text_color: Option<Color32>, text_color: Option<Color32>,
min_decimals: usize, min_decimals: usize,
@ -86,6 +88,8 @@ impl<'a> Slider<'a> {
clamp_to_range: false, clamp_to_range: false,
smart_aim: true, smart_aim: true,
show_value: true, show_value: true,
prefix: Default::default(),
suffix: Default::default(),
text: Default::default(), text: Default::default(),
text_color: None, text_color: None,
min_decimals: 0, min_decimals: 0,
@ -159,6 +163,18 @@ impl<'a> Slider<'a> {
self 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). /// Show a text next to the slider (e.g. explaining what the slider controls).
pub fn text(mut self, text: impl Into<String>) -> Self { pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = text.into(); self.text = text.into();
@ -269,6 +285,14 @@ impl<'a> Slider<'a> {
set(&mut self.get_set_value, value); set(&mut self.get_set_value, value);
} }
fn clamp_range(&self) -> RangeInclusive<f64> {
if self.clamp_to_range {
self.range()
} else {
f64::NEG_INFINITY..=f64::INFINITY
}
}
fn range(&self) -> RangeInclusive<f64> { fn range(&self) -> RangeInclusive<f64> {
self.range.clone() self.range.clone()
} }
@ -360,72 +384,30 @@ impl<'a> Slider<'a> {
} }
fn value_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive<f32>) { fn value_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive<f32>) {
let kb_edit_id = ui.auto_id_with("edit"); let mut value = self.get_value();
let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id); ui.add(
DragValue::f64(&mut value)
let aim_radius = ui.input().aim_radius(); .speed(self.current_gradient(&x_range))
let value_text = self.format_value(aim_radius, x_range); .clamp_range_f64(self.clamp_range())
.min_decimals(self.min_decimals)
if is_kb_editing { .max_decimals_opt(self.max_decimals)
let button_width = ui.spacing().interact_size.x; .suffix(self.suffix.clone())
let mut value_text = ui.memory().temp_edit_string.take().unwrap_or(value_text); .prefix(self.prefix.clone()),
ui.add( );
TextEdit::singleline(&mut value_text) if value != self.get_value() {
.id(kb_edit_id) self.set_value(value);
.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
}
} }
} }
fn format_value(&mut self, aim_radius: f32, x_range: RangeInclusive<f32>) -> String { /// delta(value) / delta(points)
fn current_gradient(&mut self, x_range: &RangeInclusive<f32>) -> f64 {
// TODO: handle clamping
let value = self.get_value(); 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 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 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 left_value = value_from_x(x_from_value(value) - 0.5);
let right_value = value_from_x(x_from_value(value) + aim_radius); let right_value = value_from_x(x_from_value(value) + 0.5);
let range = (left_value - right_value).abs(); right_value - left_value
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)
}
} }
} }
@ -457,7 +439,7 @@ impl<'a> Widget for Slider<'a> {
// Helpers for converting slider range to/from normalized [0-1] range. // Helpers for converting slider range to/from normalized [0-1] range.
// Always clamps. // Always clamps.
// Logarithmic sliders are allowed to include zero and infinity, // 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; use std::f64::INFINITY;

View file

@ -144,7 +144,7 @@ impl WidgetGallery {
ui.end_row(); ui.end_row();
ui.add(doc_link_label("Slider", "Slider")); 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.end_row();
ui.add(doc_link_label("DragValue", "DragValue")); ui.add(doc_link_label("DragValue", "DragValue"));