Slider: use a DragValue for the value, and implement suffix/prefix
This commit is contained in:
parent
32f35c6251
commit
ebc2486d22
4 changed files with 86 additions and 88 deletions
|
@ -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
|
||||
|
|
|
@ -8,19 +8,19 @@ use crate::*;
|
|||
/// for the borrow checker.
|
||||
type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> 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<f64>,
|
||||
|
@ -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>) -> f64) -> Self {
|
||||
pub(crate) fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> 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<f64>) -> 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<f64>) -> Self {
|
||||
self.clamp_range = clamp_range;
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated = "Renamed clamp_range"]
|
||||
pub fn range(self, clamp_range: RangeInclusive<f32>) -> 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<usize>) -> 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));
|
||||
}
|
||||
|
|
|
@ -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<dyn 'a + FnMut(Option<f64>) -> 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<T: Copy>(r: RangeInclusive<T>) -> RangeInclusive<f64>
|
||||
|
@ -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<Color32>,
|
||||
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<String>) -> 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<f64> {
|
||||
if self.clamp_to_range {
|
||||
self.range()
|
||||
} else {
|
||||
f64::NEG_INFINITY..=f64::INFINITY
|
||||
}
|
||||
}
|
||||
|
||||
fn range(&self) -> RangeInclusive<f64> {
|
||||
self.range.clone()
|
||||
}
|
||||
|
@ -360,72 +384,30 @@ impl<'a> Slider<'a> {
|
|||
}
|
||||
|
||||
fn value_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive<f32>) {
|
||||
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);
|
||||
let mut value = self.get_value();
|
||||
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),
|
||||
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 let Ok(value) = value_text.parse() {
|
||||
if value != self.get_value() {
|
||||
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();
|
||||
|
||||
// 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;
|
||||
|
||||
|
|
|
@ -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"));
|
||||
|
|
Loading…
Reference in a new issue