From 49eecc42876ff90d09fb290e1cd45ed36db1e625 Mon Sep 17 00:00:00 2001 From: YgorSouza <43298013+YgorSouza@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:24:29 +0100 Subject: [PATCH] Allow changing handle shape of a slider (#3429) * Closes #1974 --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/style.rs | 41 +++++++++++++++ crates/egui/src/widgets/slider.rs | 67 ++++++++++++++++++++---- crates/egui_demo_lib/src/demo/sliders.rs | 23 +++++--- 3 files changed, 115 insertions(+), 16 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index ba0cc1b9e83..b6a7d23052c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -815,6 +815,11 @@ pub struct Visuals { /// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`]. pub slider_trailing_fill: bool, + /// Shape of the handle for sliders and similar widgets. + /// + /// Changing this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::handle_shape`]. + pub handle_shape: HandleShape, + /// Should the cursor change when the user hovers over an interactive/clickable item? /// /// This is consistent with a lot of browser-based applications (vscode, github @@ -880,6 +885,20 @@ pub struct Selection { pub stroke: Stroke, } +/// Shape of the handle for sliders and similar widgets. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum HandleShape { + /// Circular handle + Circle, + + /// Rectangular handle + Rect { + /// Aspect ratio of the rectangle. Set to < 1.0 to make it narrower. + aspect_ratio: f32, + }, +} + /// The visuals of widgets for different states of interaction. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -1125,6 +1144,7 @@ impl Visuals { striped: false, slider_trailing_fill: false, + handle_shape: HandleShape::Circle, interact_cursor: None, @@ -1687,6 +1707,7 @@ impl Visuals { striped, slider_trailing_fill, + handle_shape, interact_cursor, image_loading_spinners, @@ -1755,6 +1776,8 @@ impl Visuals { ui.checkbox(slider_trailing_fill, "Add trailing color to sliders"); + handle_shape.ui(ui); + ComboBox::from_label("Interact Cursor") .selected_text(format!("{interact_cursor:?}")) .show_ui(ui, |ui| { @@ -1877,3 +1900,21 @@ fn rounding_ui(ui: &mut Ui, rounding: &mut Rounding) { } }); } + +impl HandleShape { + pub fn ui(&mut self, ui: &mut Ui) { + ui.label("Widget handle shape"); + ui.horizontal(|ui| { + ui.radio_value(self, HandleShape::Circle, "Circle"); + if ui + .radio(matches!(self, HandleShape::Rect { .. }), "Rectangle") + .clicked() + { + *self = HandleShape::Rect { aspect_ratio: 0.5 }; + } + if let HandleShape::Rect { aspect_ratio } = self { + ui.add(Slider::new(aspect_ratio, 0.1..=3.0).text("Aspect ratio")); + } + }); + } +} diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 9e29f856dce..45b0b42e440 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -2,7 +2,7 @@ use std::ops::RangeInclusive; -use crate::*; +use crate::{style::HandleShape, *}; // ---------------------------------------------------------------------------- @@ -87,6 +87,7 @@ pub struct Slider<'a> { custom_formatter: Option>, custom_parser: Option>, trailing_fill: Option, + handle_shape: Option, } impl<'a> Slider<'a> { @@ -133,6 +134,7 @@ impl<'a> Slider<'a> { custom_formatter: None, custom_parser: None, trailing_fill: None, + handle_shape: None, } } @@ -302,6 +304,16 @@ impl<'a> Slider<'a> { self } + /// Change the shape of the slider handle + /// + /// This setting can be enabled globally for all sliders with [`Visuals::handle_shape`]. + /// Changing it here will override the above setting ONLY for this individual slider. + #[inline] + pub fn handle_shape(mut self, handle_shape: HandleShape) -> Self { + self.handle_shape = Some(handle_shape); + self + } + /// Set custom formatter defining how numbers are converted into text. /// /// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive` representing @@ -570,7 +582,10 @@ impl<'a> Slider<'a> { /// Just the slider, no text fn slider_ui(&mut self, ui: &Ui, response: &Response) { let rect = &response.rect; - let position_range = self.position_range(rect); + let handle_shape = self + .handle_shape + .unwrap_or_else(|| ui.style().visuals.handle_shape); + let position_range = self.position_range(rect, &handle_shape); if let Some(pointer_position_2d) = response.interact_pointer_pos() { let position = self.pointer_position(pointer_position_2d); @@ -696,12 +711,37 @@ impl<'a> Slider<'a> { ); } - ui.painter().add(epaint::CircleShape { - center, - radius: self.handle_radius(rect) + visuals.expansion, - fill: visuals.bg_fill, - stroke: visuals.fg_stroke, - }); + let radius = self.handle_radius(rect); + + let handle_shape = self + .handle_shape + .unwrap_or_else(|| ui.style().visuals.handle_shape); + match handle_shape { + style::HandleShape::Circle => { + ui.painter().add(epaint::CircleShape { + center, + radius: radius + visuals.expansion, + fill: visuals.bg_fill, + stroke: visuals.fg_stroke, + }); + } + style::HandleShape::Rect { aspect_ratio } => { + let v = match self.orientation { + SliderOrientation::Horizontal => Vec2::new(radius * aspect_ratio, radius), + SliderOrientation::Vertical => Vec2::new(radius, radius * aspect_ratio), + }; + let v = v + Vec2::splat(visuals.expansion); + let rect = Rect::from_center_size(center, 2.0 * v); + ui.painter().add(epaint::RectShape { + fill: visuals.bg_fill, + stroke: visuals.fg_stroke, + rect, + rounding: visuals.rounding, + fill_texture_id: Default::default(), + uv: Rect::ZERO, + }); + } + } } } @@ -719,8 +759,12 @@ impl<'a> Slider<'a> { } } - fn position_range(&self, rect: &Rect) -> Rangef { + fn position_range(&self, rect: &Rect, handle_shape: &style::HandleShape) -> Rangef { let handle_radius = self.handle_radius(rect); + let handle_radius = match handle_shape { + style::HandleShape::Circle => handle_radius, + style::HandleShape::Rect { aspect_ratio } => handle_radius * aspect_ratio, + }; match self.orientation { SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius), // The vertical case has to be flipped because the largest slider value maps to the @@ -842,7 +886,10 @@ impl<'a> Slider<'a> { let slider_response = response.clone(); let value_response = if self.show_value { - let position_range = self.position_range(&response.rect); + let handle_shape = self + .handle_shape + .unwrap_or_else(|| ui.style().visuals.handle_shape); + let position_range = self.position_range(&response.rect, &handle_shape); let value_response = self.value_ui(ui, position_range); if value_response.gained_focus() || value_response.has_focus() diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index 1f565a544d5..d2b065403c5 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -1,4 +1,4 @@ -use egui::*; +use egui::{style::HandleShape, *}; use std::f64::INFINITY; /// Showcase sliders @@ -17,6 +17,7 @@ pub struct Sliders { pub vertical: bool, pub value: f64, pub trailing_fill: bool, + pub handle_shape: HandleShape, } impl Default for Sliders { @@ -33,6 +34,7 @@ impl Default for Sliders { vertical: false, value: 10.0, trailing_fill: false, + handle_shape: HandleShape::Circle, } } } @@ -67,6 +69,7 @@ impl super::View for Sliders { vertical, value, trailing_fill, + handle_shape, } = self; ui.label("You can click a slider value to edit it with the keyboard."); @@ -99,7 +102,8 @@ impl super::View for Sliders { .orientation(orientation) .text("i32 demo slider") .step_by(istep) - .trailing_fill(*trailing_fill), + .trailing_fill(*trailing_fill) + .handle_shape(*handle_shape), ); *value = value_i32 as f64; } else { @@ -111,7 +115,8 @@ impl super::View for Sliders { .orientation(orientation) .text("f64 demo slider") .step_by(istep) - .trailing_fill(*trailing_fill), + .trailing_fill(*trailing_fill) + .handle_shape(*handle_shape), ); ui.label( @@ -132,20 +137,26 @@ impl super::View for Sliders { .logarithmic(true) .smart_aim(*smart_aim) .text("left") - .trailing_fill(*trailing_fill), + .trailing_fill(*trailing_fill) + .handle_shape(*handle_shape), ); ui.add( Slider::new(max, type_min..=type_max) .logarithmic(true) .smart_aim(*smart_aim) .text("right") - .trailing_fill(*trailing_fill), + .trailing_fill(*trailing_fill) + .handle_shape(*handle_shape), ); ui.separator(); ui.checkbox(trailing_fill, "Toggle trailing color"); - ui.label("When enabled, trailing color will be painted up until the circle."); + ui.label("When enabled, trailing color will be painted up until the handle."); + + ui.separator(); + + handle_shape.ui(ui); ui.separator();