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 `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
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
Loading…
Reference in a new issue