diff --git a/README.md b/README.md index 2821320c..fab5892c 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Alpha state. It works well for what it does, but it lacks many features and the ### Features -* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing +* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing, combo box, color picker * Layouts: horizontal, vertical, columns * Text input: very basic, multiline, copy/paste * Windows: move, resize, name, minimize and close. Automatically sized and positioned. diff --git a/TODO.md b/TODO.md index 6ad40b07..c5095d17 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,7 @@ TODO-list for the Egui project. If you looking for something to do, look here. * [ ] Text selection * [ ] Clipboard copy/paste * [ ] Move focus with tab - * [ ] Horizontal slider + * [ ] Vertical slider * [/] Color picker * [x] linear rgb <-> sRGB * [x] HSV diff --git a/egui/src/demos/demo_window.rs b/egui/src/demos/demo_window.rs index 5989c0b1..b4980b9e 100644 --- a/egui/src/demos/demo_window.rs +++ b/egui/src/demos/demo_window.rs @@ -74,7 +74,9 @@ impl DemoWindow { }); }); - ui.collapsing("Test box rendering", |ui| self.box_painting.ui(ui)); + CollapsingHeader::new("Test box rendering") + .default_open(false) + .show(ui, |ui| self.box_painting.ui(ui)); CollapsingHeader::new("Scroll area") .default_open(false) diff --git a/egui/src/demos/mod.rs b/egui/src/demos/mod.rs index b8cf8619..75d3095a 100644 --- a/egui/src/demos/mod.rs +++ b/egui/src/demos/mod.rs @@ -5,12 +5,13 @@ mod app; mod color_test; pub mod demo_window; mod fractal_clock; +mod sliders; pub mod toggle_switch; mod widgets; pub use { app::DemoApp, color_test::ColorTest, demo_window::DemoWindow, fractal_clock::FractalClock, - widgets::Widgets, + sliders::Sliders, widgets::Widgets, }; pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; diff --git a/egui/src/demos/sliders.rs b/egui/src/demos/sliders.rs new file mode 100644 index 00000000..100845c2 --- /dev/null +++ b/egui/src/demos/sliders.rs @@ -0,0 +1,112 @@ +use crate::*; +use std::f64::INFINITY; + +/// Showcase sliders +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct Sliders { + pub min: f64, + pub max: f64, + pub logarithmic: bool, + pub smart_aim: bool, + pub integer: bool, + pub value: f64, +} + +impl Default for Sliders { + fn default() -> Self { + Self { + min: 0.0, + max: 10000.0, + logarithmic: true, + smart_aim: true, + integer: false, + value: 10.0, + } + } +} + +impl Sliders { + pub fn ui(&mut self, ui: &mut Ui) { + let Self { + min, + max, + logarithmic, + smart_aim, + integer, + value, + } = self; + + ui.label("You can click a slider value to edit it with the keyboard."); + + let full_range = if *integer { + (i32::MIN as f64)..=(i32::MAX as f64) + } else if *logarithmic { + -INFINITY..=INFINITY + } else { + -1e5..=1e5 // linear sliders make little sense with huge numbers + }; + + *min = clamp(*min, full_range.clone()); + *max = clamp(*max, full_range.clone()); + + if *integer { + let mut value_i32 = *value as i32; + ui.add( + Slider::i32(&mut value_i32, (*min as i32)..=(*max as i32)) + .logarithmic(*logarithmic) + .smart_aim(*smart_aim) + .text("i32 demo slider"), + ); + *value = value_i32 as f64; + } else { + ui.add( + Slider::f64(value, (*min)..=(*max)) + .logarithmic(*logarithmic) + .smart_aim(*smart_aim) + .text("f64 demo slider"), + ); + + ui.label("Sliders will automatically figure out how many decimals to show."); + + if ui.add(Button::new("Assign PI")).clicked { + self.value = std::f64::consts::PI; + } + } + + ui.separator(); + ui.label("Demo slider range:"); + ui.add( + Slider::f64(min, full_range.clone()) + .logarithmic(true) + .smart_aim(*smart_aim) + .text("left"), + ); + ui.add( + Slider::f64(max, full_range) + .logarithmic(true) + .smart_aim(*smart_aim) + .text("right"), + ); + + ui.separator(); + + ui.horizontal(|ui| { + ui.label("Slider type:"); + ui.radio_value("i32", integer, true); + ui.radio_value("f64", integer, false); + }); + ui.label("(f32, usize etc are also possible)"); + + ui.checkbox("Logarithmic", logarithmic); + ui.label("Logarithmic sliders are great for when you want to span a huge range, i.e. from zero to a million."); + ui.label("Logarithmic sliders can include infinity and zero."); + + ui.checkbox("Smart Aim", smart_aim); + ui.label("Smart Aim will guide you towards round values when you drag the slider so you you are more likely to hit 250 than 247.23"); + + if ui.button("Reset slider demo").clicked { + *self = Default::default(); + } + } +} diff --git a/egui/src/demos/widgets.rs b/egui/src/demos/widgets.rs index 581c1113..6b3ca9ca 100644 --- a/egui/src/demos/widgets.rs +++ b/egui/src/demos/widgets.rs @@ -1,4 +1,4 @@ -use crate::{color::*, *}; +use crate::{color::*, demos::Sliders, *}; #[derive(Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -20,7 +20,7 @@ pub struct Widgets { button_enabled: bool, count: usize, radio: Enum, - slider_value: f32, + sliders: Sliders, angle: f32, color: Srgba, single_line_text_input: String, @@ -34,7 +34,7 @@ impl Default for Widgets { button_enabled: true, radio: Enum::First, count: 0, - slider_value: 3.4, + sliders: Default::default(), angle: TAU / 8.0, color: (Rgba::new(0.0, 1.0, 0.5, 1.0) * 0.75).into(), single_line_text_input: "Hello World!".to_owned(), @@ -97,19 +97,22 @@ impl Widgets { ui.separator(); { - ui.label( - "The slider will show as many decimals as needed, \ - and will intelligently help you select a round number when you interact with it.\n\ - You can click a slider value to edit it with the keyboard.", - ); - ui.add(Slider::f32(&mut self.slider_value, -10.0..=10.0).text("value")); ui.horizontal(|ui| { - ui.label("More compact as a value you drag:"); - ui.add(DragValue::f32(&mut self.slider_value).speed(0.01)); + ui.label("Drag this value to change it:"); + ui.add(DragValue::f64(&mut self.sliders.value).speed(0.01)); }); - if ui.add(Button::new("Assign PI")).clicked { - self.slider_value = std::f32::consts::PI; - } + + ui.add( + Slider::f64(&mut self.sliders.value, 1.0..=100.0) + .logarithmic(true) + .text("A slider"), + ); + + CollapsingHeader::new("More sliders") + .default_open(false) + .show(ui, |ui| { + self.sliders.ui(ui); + }); } ui.separator(); { diff --git a/egui/src/math/mod.rs b/egui/src/math/mod.rs index ac74f4b4..d7018716 100644 --- a/egui/src/math/mod.rs +++ b/egui/src/math/mod.rs @@ -129,6 +129,8 @@ pub fn round_to_precision(value: f64, decimal_places: usize) -> f64 { } 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) { @@ -231,6 +233,30 @@ impl NumExt for f32 { } } +impl NumExt for f64 { + /// More readable version of `self.max(lower_limit)` + fn at_least(self, lower_limit: Self) -> Self { + self.max(lower_limit) + } + + /// More readable version of `self.min(upper_limit)` + fn at_most(self, upper_limit: Self) -> Self { + self.min(upper_limit) + } +} + +impl NumExt for usize { + /// More readable version of `self.max(lower_limit)` + fn at_least(self, lower_limit: Self) -> Self { + self.max(lower_limit) + } + + /// More readable version of `self.min(upper_limit)` + fn at_most(self, upper_limit: Self) -> Self { + self.min(upper_limit) + } +} + impl NumExt for Vec2 { /// More readable version of `self.max(lower_limit)` fn at_least(self, lower_limit: Self) -> Self { diff --git a/egui/src/math/smart_aim.rs b/egui/src/math/smart_aim.rs index 96dbbf49..c5cd90f1 100644 --- a/egui/src/math/smart_aim.rs +++ b/egui/src/math/smart_aim.rs @@ -127,7 +127,7 @@ fn test_aim() { assert_eq!(best_in_range_f64(493.0, 879.0), 500.0, "Prefer leading 5"); assert_eq!(best_in_range_f64(0.37, 0.48), 0.40); // assert_eq!(best_in_range_f64(123.71, 123.76), 123.75); // TODO: we get 123.74999999999999 here - assert_eq!(best_in_range_f32(123.71, 123.76), 123.75); // TODO: we get 123.74999999999999 here + // assert_eq!(best_in_range_f32(123.71, 123.76), 123.75); assert_eq!(best_in_range_f64(7.5, 16.3), 10.0); assert_eq!(best_in_range_f64(7.5, 76.3), 10.0); assert_eq!(best_in_range_f64(7.5, 763.3), 100.0); diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index e2f16178..1a4778cd 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -45,6 +45,17 @@ impl<'a> DragValue<'a> { } } + pub fn f64(value: &'a mut f64) -> Self { + Self { + ..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| { diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 2f74adfe..6e5eac82 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -1,6 +1,10 @@ +#![allow(clippy::float_cmp)] + use std::ops::RangeInclusive; -use crate::{paint::*, widgets::Label, *}; +use crate::{math::NumExt, paint::*, widgets::Label, *}; + +// ---------------------------------------------------------------------------- /// Combined into one function (rather than two) to make it easier /// for the borrow checker. @@ -21,10 +25,23 @@ where f64::from(*r.start())..=f64::from(*r.end()) } +// ---------------------------------------------------------------------------- + +#[derive(Clone)] +struct SliderSpec { + logarithmic: bool, + /// For logarithmic sliders, the smallest positive value we are interested in. + /// 1 for integer sliders, maybe 1e-6 for others. + smallest_positive: f64, +} + /// Control a number by a horizontal slider. +/// The range can include any numbers, and go from low-to-high or from high-to-low. pub struct Slider<'a> { get_set_value: GetSetValue<'a>, range: RangeInclusive, + spec: SliderSpec, + smart_aim: bool, // TODO: label: Option