From 2e3932d99cad187ff0697930f5adb94bf157aa49 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Wed, 3 Nov 2021 17:45:17 -0700 Subject: [PATCH 1/4] fix spelling --- egui/src/widgets/slider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 4d073ac4ca2..a400abd8c6d 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -109,7 +109,7 @@ impl<'a> Slider<'a> { } } - /// Control wether or not the slider shows the current value. + /// Control whether or not the slider shows the current value. /// Default: `true`. pub fn show_value(mut self, show_value: bool) -> Self { self.show_value = show_value; From 3ad8e1ff9f19b2ef29312fd350049cdbe6c07c50 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Thu, 4 Nov 2021 00:26:23 -0700 Subject: [PATCH 2/4] implement vertical slider orientation --- egui/src/widgets/slider.rs | 214 +++++++++++++++++++------ egui_demo_lib/src/apps/demo/sliders.rs | 23 ++- 2 files changed, 182 insertions(+), 55 deletions(-) diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index a400abd8c6d..1ef03250dd4 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -32,7 +32,13 @@ struct SliderSpec { largest_finite: f64, } -/// Control a number by a horizontal slider. +/// Specifies the orientation of a [Slider]. +pub enum SliderOrientation { + Horizontal, + Vertical, +} + +/// Control a number with a slider. /// /// The slider range defines the values you get when pulling the slider to the far edges. /// By default, the slider can still show values outside this range, @@ -41,7 +47,7 @@ struct SliderSpec { /// /// The range can include any numbers, and go from low-to-high or from high-to-low. /// -/// The slider consists of three parts: a horizontal slider, a value display, and an optional text. +/// The slider consists of three parts: a slider, a value display, and an optional text. /// The user can click the value display to edit its value. It can be turned off with `.show_value(false)`. /// /// ``` @@ -60,6 +66,7 @@ pub struct Slider<'a> { clamp_to_range: bool, smart_aim: bool, show_value: bool, + orientation: SliderOrientation, prefix: String, suffix: String, text: String, @@ -69,6 +76,7 @@ pub struct Slider<'a> { } impl<'a> Slider<'a> { + /// Creates a new horizontal slider. pub fn new(value: &'a mut Num, range: RangeInclusive) -> Self { let range_f64 = range.start().to_f64()..=range.end().to_f64(); let slf = Self::from_get_set(range_f64, move |v: Option| { @@ -85,9 +93,44 @@ impl<'a> Slider<'a> { } } + /// Creates a new vertical slider. + pub fn new_vertical( + value: &'a mut Num, + range: RangeInclusive, + ) -> Self { + let range_f64 = range.start().to_f64()..=range.end().to_f64(); + let slf = Self::from_get_set_vertical(range_f64, move |v: Option| { + if let Some(v) = v { + *value = Num::from_f64(v); + } + value.to_f64() + }); + + if Num::INTEGRAL { + slf.integer() + } else { + slf + } + } + pub fn from_get_set( range: RangeInclusive, get_set_value: impl 'a + FnMut(Option) -> f64, + ) -> Self { + Self::from_get_set_inner(range, get_set_value, SliderOrientation::Horizontal) + } + + pub fn from_get_set_vertical( + range: RangeInclusive, + get_set_value: impl 'a + FnMut(Option) -> f64, + ) -> Self { + Self::from_get_set_inner(range, get_set_value, SliderOrientation::Vertical) + } + + fn from_get_set_inner( + range: RangeInclusive, + get_set_value: impl 'a + FnMut(Option) -> f64, + orientation: SliderOrientation, ) -> Self { Self { get_set_value: Box::new(get_set_value), @@ -100,6 +143,7 @@ impl<'a> Slider<'a> { clamp_to_range: true, smart_aim: true, show_value: true, + orientation, prefix: Default::default(), suffix: Default::default(), text: Default::default(), @@ -139,6 +183,11 @@ impl<'a> Slider<'a> { self } + pub fn orientation(mut self, orientation: SliderOrientation) -> Self { + self.orientation = orientation; + self + } + /// Make this a logarithmic slider. /// This is great for when the slider spans a huge range, /// e.g. from one to a million. @@ -249,49 +298,43 @@ impl<'a> Slider<'a> { self.range.clone() } - /// For instance, `x` is the mouse position and `x_range` is the physical location of the slider on the screen. - fn value_from_x(&self, x: f32, x_range: RangeInclusive) -> f64 { - let normalized = remap_clamp(x, x_range, 0.0..=1.0) as f64; + /// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen. + fn value_from_position(&self, position: f32, position_range: RangeInclusive) -> f64 { + let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64; value_from_normalized(normalized, self.range(), &self.spec) } - fn x_from_value(&self, value: f64, x_range: RangeInclusive) -> f32 { + fn position_from_value(&self, value: f64, position_range: RangeInclusive) -> f32 { let normalized = normalized_from_value(value, self.range(), &self.spec); - lerp(x_range, normalized as f32) + lerp(position_range, normalized as f32) } } -fn handle_radius(rect: &Rect) -> f32 { - rect.height() / 2.5 -} - -fn x_range(rect: &Rect) -> RangeInclusive { - let handle_radius = handle_radius(rect); - (rect.left() + handle_radius)..=(rect.right() - handle_radius) -} - impl<'a> Slider<'a> { /// Just the slider, no text - #[allow(clippy::unused_self)] - fn allocate_slider_space(&self, ui: &mut Ui, height: f32) -> Response { - let desired_size = vec2(ui.spacing().slider_width, height); + fn allocate_slider_space(&self, ui: &mut Ui, perpendicular: f32) -> Response { + let desired_size = match self.orientation { + SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, perpendicular), + SliderOrientation::Vertical => vec2(perpendicular, ui.spacing().slider_width), + }; ui.allocate_response(desired_size, Sense::click_and_drag()) } /// Just the slider, no text fn slider_ui(&mut self, ui: &mut Ui, response: &Response) { let rect = &response.rect; - let x_range = x_range(rect); + let position_range = self.position_range(rect); - if let Some(pointer_pos) = response.interact_pointer_pos() { + if let Some(pointer_position_2d) = response.interact_pointer_pos() { + let position = self.pointer_position(pointer_position_2d); let new_value = if self.smart_aim { let aim_radius = ui.input().aim_radius(); emath::smart_aim::best_in_range_f64( - self.value_from_x(pointer_pos.x - aim_radius, x_range.clone()), - self.value_from_x(pointer_pos.x + aim_radius, x_range.clone()), + self.value_from_position(position - aim_radius, position_range.clone()), + self.value_from_position(position + aim_radius, position_range.clone()), ) } else { - self.value_from_x(pointer_pos.x, x_range.clone()) + self.value_from_position(position, position_range.clone()) }; self.set_value(new_value); } @@ -300,21 +343,22 @@ impl<'a> Slider<'a> { response.widget_info(|| WidgetInfo::slider(value, &self.text)); if response.has_focus() { - let kb_step = ui.input().num_presses(Key::ArrowRight) as f32 - - ui.input().num_presses(Key::ArrowLeft) as f32; + let increment = ui.input().num_presses(self.key_increment()); + let decrement = ui.input().num_presses(self.key_decrement()); + let kb_step = increment as f32 - decrement as f32; if kb_step != 0.0 { let prev_value = self.get_value(); - let prev_x = self.x_from_value(prev_value, x_range.clone()); - let new_x = prev_x + kb_step; + let prev_position = self.position_from_value(prev_value, position_range.clone()); + let new_position = prev_position + kb_step; let new_value = if self.smart_aim { let aim_radius = ui.input().aim_radius(); emath::smart_aim::best_in_range_f64( - self.value_from_x(new_x - aim_radius, x_range.clone()), - self.value_from_x(new_x + aim_radius, x_range.clone()), + self.value_from_position(new_position - aim_radius, position_range.clone()), + self.value_from_position(new_position + aim_radius, position_range.clone()), ) } else { - self.value_from_x(new_x, x_range.clone()) + self.value_from_position(new_position, position_range.clone()) }; self.set_value(new_value); } @@ -324,15 +368,10 @@ impl<'a> Slider<'a> { if ui.is_rect_visible(response.rect) { let value = self.get_value(); - let rail_radius = ui - .painter() - .round_to_pixel((rect.height() / 4.0).at_least(2.0)); + let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect)); + let rail_rect = self.rail_rect(rect, rail_radius); - let rail_rect = Rect::from_min_max( - pos2(rect.left(), rect.center().y - rail_radius), - pos2(rect.right(), rect.center().y + rail_radius), - ); - let marker_center_x = self.x_from_value(value, x_range); + let position_1d = self.position_from_value(value, position_range); let visuals = ui.style().interact(response); ui.painter().add(epaint::RectShape { @@ -346,15 +385,85 @@ impl<'a> Slider<'a> { // stroke: ui.visuals().widgets.inactive.bg_stroke, }); + let center = self.marker_center(position_1d, &rail_rect); + ui.painter().add(epaint::CircleShape { - center: pos2(marker_center_x, rail_rect.center().y), - radius: handle_radius(rect) + visuals.expansion, + center, + radius: self.handle_radius(rect) + visuals.expansion, fill: visuals.bg_fill, stroke: visuals.fg_stroke, }); } } + fn marker_center(&self, position_1d: f32, rail_rect: &Rect) -> Pos2 { + match self.orientation { + SliderOrientation::Horizontal => pos2(position_1d, rail_rect.center().y), + SliderOrientation::Vertical => pos2(rail_rect.center().x, position_1d), + } + } + + fn pointer_position(&self, pointer_position_2d: Pos2) -> f32 { + match self.orientation { + SliderOrientation::Horizontal => pointer_position_2d.x, + SliderOrientation::Vertical => pointer_position_2d.y, + } + } + + fn position_range(&self, rect: &Rect) -> RangeInclusive { + let handle_radius = self.handle_radius(rect); + match self.orientation { + SliderOrientation::Horizontal => { + (rect.left() + handle_radius)..=(rect.right() - handle_radius) + } + SliderOrientation::Vertical => { + (rect.top() + handle_radius)..=(rect.bottom() - handle_radius) + } + } + } + + fn key_increment(&self) -> Key { + match self.orientation { + SliderOrientation::Horizontal => Key::ArrowRight, + SliderOrientation::Vertical => Key::ArrowUp, + } + } + + fn key_decrement(&self) -> Key { + match self.orientation { + SliderOrientation::Horizontal => Key::ArrowLeft, + SliderOrientation::Vertical => Key::ArrowDown, + } + } + + fn rail_rect(&self, rect: &Rect, radius: f32) -> Rect { + match self.orientation { + SliderOrientation::Horizontal => Rect::from_min_max( + pos2(rect.left(), rect.center().y - radius), + pos2(rect.right(), rect.center().y + radius), + ), + SliderOrientation::Vertical => Rect::from_min_max( + pos2(rect.center().x - radius, rect.top()), + pos2(rect.center().x + radius, rect.bottom()), + ), + } + } + + fn handle_radius(&self, rect: &Rect) -> f32 { + let limit = match self.orientation { + SliderOrientation::Horizontal => rect.height(), + SliderOrientation::Vertical => rect.width(), + }; + limit / 2.5 + } + + fn rail_radius_limit(&self, rect: &Rect) -> f32 { + match self.orientation { + SliderOrientation::Horizontal => (rect.height() / 4.0).at_least(2.0), + SliderOrientation::Vertical => (rect.width() / 4.0).at_least(2.0), + } + } + fn label_ui(&mut self, ui: &mut Ui) { if !self.text.is_empty() { let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); @@ -363,11 +472,11 @@ impl<'a> Slider<'a> { } } - fn value_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive) { + fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive) { let mut value = self.get_value(); ui.add( DragValue::new(&mut value) - .speed(self.current_gradient(&x_range)) + .speed(self.current_gradient(&position_range)) .clamp_range(self.clamp_range()) .min_decimals(self.min_decimals) .max_decimals_opt(self.max_decimals) @@ -380,13 +489,14 @@ impl<'a> Slider<'a> { } /// delta(value) / delta(points) - fn current_gradient(&mut self, x_range: &RangeInclusive) -> f64 { + fn current_gradient(&mut self, position_range: &RangeInclusive) -> f64 { // TODO: handle clamping let value = self.get_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) - 0.5); - let right_value = value_from_x(x_from_value(value) + 0.5); + let value_from_pos = + |position: f32| self.value_from_position(position, position_range.clone()); + let pos_from_value = |value: f64| self.position_from_value(value, position_range.clone()); + let left_value = value_from_pos(pos_from_value(value) - 0.5); + let right_value = value_from_pos(pos_from_value(value) + 0.5); right_value - left_value } } @@ -394,7 +504,7 @@ impl<'a> Slider<'a> { impl<'a> Widget for Slider<'a> { fn ui(mut self, ui: &mut Ui) -> Response { let text_style = TextStyle::Button; - let height = ui + let perpendicular = ui .fonts() .row_height(text_style) .at_least(ui.spacing().interact_size.y); @@ -402,12 +512,12 @@ impl<'a> Widget for Slider<'a> { let old_value = self.get_value(); let inner_response = ui.horizontal(|ui| { - let slider_response = self.allocate_slider_space(ui, height); + let slider_response = self.allocate_slider_space(ui, perpendicular); self.slider_ui(ui, &slider_response); if self.show_value { - let x_range = x_range(&slider_response.rect); - self.value_ui(ui, x_range); + let position_range = self.position_range(&slider_response.rect); + self.value_ui(ui, position_range); } if !self.text.is_empty() { diff --git a/egui_demo_lib/src/apps/demo/sliders.rs b/egui_demo_lib/src/apps/demo/sliders.rs index cfa4aad713f..c99c17767dd 100644 --- a/egui_demo_lib/src/apps/demo/sliders.rs +++ b/egui_demo_lib/src/apps/demo/sliders.rs @@ -12,6 +12,7 @@ pub struct Sliders { pub clamp_to_range: bool, pub smart_aim: bool, pub integer: bool, + pub vertical: bool, pub value: f64, } @@ -24,6 +25,7 @@ impl Default for Sliders { clamp_to_range: false, smart_aim: true, integer: false, + vertical: false, value: 10.0, } } @@ -54,6 +56,7 @@ impl super::View for Sliders { clamp_to_range, smart_aim, integer, + vertical, value, } = self; @@ -70,6 +73,12 @@ impl super::View for Sliders { *min = min.clamp(type_min, type_max); *max = max.clamp(type_min, type_max); + let orientation = if *vertical { + SliderOrientation::Vertical + } else { + SliderOrientation::Horizontal + }; + if *integer { let mut value_i32 = *value as i32; ui.add( @@ -77,6 +86,7 @@ impl super::View for Sliders { .logarithmic(*logarithmic) .clamp_to_range(*clamp_to_range) .smart_aim(*smart_aim) + .orientation(orientation) .text("i32 demo slider"), ); *value = value_i32 as f64; @@ -86,6 +96,7 @@ impl super::View for Sliders { .logarithmic(*logarithmic) .clamp_to_range(*clamp_to_range) .smart_aim(*smart_aim) + .orientation(orientation) .text("f64 demo slider"), ); @@ -94,12 +105,13 @@ impl super::View for Sliders { You can always see the full precision value by hovering the value.", ); - if ui.button("Assign PI").clicked() { - self.value = std::f64::consts::PI; - } + // if ui.button("Assign PI").clicked() { + // self.value = std::f64::consts::PI; + // } } ui.separator(); + ui.label("Slider range:"); ui.add( Slider::new(min, type_min..=type_max) @@ -121,6 +133,11 @@ impl super::View for Sliders { ui.radio_value(integer, true, "i32"); ui.radio_value(integer, false, "f64"); }); + ui.horizontal(|ui| { + ui.label("Slider orientation:"); + ui.radio_value(vertical, false, "Horizontal"); + ui.radio_value(vertical, true, "Vertical"); + }); ui.label("(f32, usize etc are also possible)"); ui.add_space(8.0); From 5146b628187cbf56554dc14d8ea967717bfb6117 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Thu, 4 Nov 2021 00:42:43 -0700 Subject: [PATCH 3/4] uncomment pi button --- egui_demo_lib/src/apps/demo/sliders.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/sliders.rs b/egui_demo_lib/src/apps/demo/sliders.rs index c99c17767dd..1cf0f33dfd5 100644 --- a/egui_demo_lib/src/apps/demo/sliders.rs +++ b/egui_demo_lib/src/apps/demo/sliders.rs @@ -105,9 +105,9 @@ impl super::View for Sliders { You can always see the full precision value by hovering the value.", ); - // if ui.button("Assign PI").clicked() { - // self.value = std::f64::consts::PI; - // } + if ui.button("Assign PI").clicked() { + self.value = std::f64::consts::PI; + } } ui.separator(); From 95589f8d1a6d1aa05a86b79c0a61b986ffc83100 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 8 Nov 2021 16:50:02 -0800 Subject: [PATCH 4/4] cleanup api, add vertical layout --- egui/src/widgets/slider.rs | 84 ++++++++++++++------------------------ 1 file changed, 30 insertions(+), 54 deletions(-) diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 1ef03250dd4..5c93bd74146 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -32,7 +32,7 @@ struct SliderSpec { largest_finite: f64, } -/// Specifies the orientation of a [Slider]. +/// Specifies the orientation of a [`Slider`]. pub enum SliderOrientation { Horizontal, Vertical, @@ -93,44 +93,9 @@ impl<'a> Slider<'a> { } } - /// Creates a new vertical slider. - pub fn new_vertical( - value: &'a mut Num, - range: RangeInclusive, - ) -> Self { - let range_f64 = range.start().to_f64()..=range.end().to_f64(); - let slf = Self::from_get_set_vertical(range_f64, move |v: Option| { - if let Some(v) = v { - *value = Num::from_f64(v); - } - value.to_f64() - }); - - if Num::INTEGRAL { - slf.integer() - } else { - slf - } - } - pub fn from_get_set( range: RangeInclusive, get_set_value: impl 'a + FnMut(Option) -> f64, - ) -> Self { - Self::from_get_set_inner(range, get_set_value, SliderOrientation::Horizontal) - } - - pub fn from_get_set_vertical( - range: RangeInclusive, - get_set_value: impl 'a + FnMut(Option) -> f64, - ) -> Self { - Self::from_get_set_inner(range, get_set_value, SliderOrientation::Vertical) - } - - fn from_get_set_inner( - range: RangeInclusive, - get_set_value: impl 'a + FnMut(Option) -> f64, - orientation: SliderOrientation, ) -> Self { Self { get_set_value: Box::new(get_set_value), @@ -143,7 +108,7 @@ impl<'a> Slider<'a> { clamp_to_range: true, smart_aim: true, show_value: true, - orientation, + orientation: SliderOrientation::Horizontal, prefix: Default::default(), suffix: Default::default(), text: Default::default(), @@ -183,11 +148,18 @@ impl<'a> Slider<'a> { self } + /// Vertical or horizontal slider? The default is horizontal. pub fn orientation(mut self, orientation: SliderOrientation) -> Self { self.orientation = orientation; self } + /// Make this a vertical slider. + pub fn vertical(mut self) -> Self { + self.orientation = SliderOrientation::Vertical; + self + } + /// Make this a logarithmic slider. /// This is great for when the slider spans a huge range, /// e.g. from one to a million. @@ -417,7 +389,7 @@ impl<'a> Slider<'a> { (rect.left() + handle_radius)..=(rect.right() - handle_radius) } SliderOrientation::Vertical => { - (rect.top() + handle_radius)..=(rect.bottom() - handle_radius) + (rect.bottom() - handle_radius)..=(rect.top() + handle_radius) } } } @@ -499,32 +471,36 @@ impl<'a> Slider<'a> { let right_value = value_from_pos(pos_from_value(value) + 0.5); right_value - left_value } -} -impl<'a> Widget for Slider<'a> { - fn ui(mut self, ui: &mut Ui) -> Response { + fn add_contents(&mut self, ui: &mut Ui) -> Response { let text_style = TextStyle::Button; let perpendicular = ui .fonts() .row_height(text_style) .at_least(ui.spacing().interact_size.y); + let slider_response = self.allocate_slider_space(ui, perpendicular); + self.slider_ui(ui, &slider_response); - let old_value = self.get_value(); + if self.show_value { + let position_range = self.position_range(&slider_response.rect); + self.value_ui(ui, position_range); + } - let inner_response = ui.horizontal(|ui| { - let slider_response = self.allocate_slider_space(ui, perpendicular); - self.slider_ui(ui, &slider_response); + if !self.text.is_empty() { + self.label_ui(ui); + } + slider_response + } +} - if self.show_value { - let position_range = self.position_range(&slider_response.rect); - self.value_ui(ui, position_range); - } +impl<'a> Widget for Slider<'a> { + fn ui(mut self, ui: &mut Ui) -> Response { + let old_value = self.get_value(); - if !self.text.is_empty() { - self.label_ui(ui); - } - slider_response - }); + let inner_response = match self.orientation { + SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)), + SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)), + }; let mut response = inner_response.inner | inner_response.response; response.changed = self.get_value() != old_value;