Allow changing handle shape of a slider (#3429)
* Closes #1974 <!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to add commits to your PR. * Remember to run `cargo fmt` and `cargo cranky`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review you PR, but my time is limited! --> --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
f20b7b43bf
commit
49eecc4287
|
|
@ -815,6 +815,11 @@ pub struct Visuals {
|
||||||
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
|
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
|
||||||
pub slider_trailing_fill: bool,
|
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?
|
/// 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
|
/// This is consistent with a lot of browser-based applications (vscode, github
|
||||||
|
|
@ -880,6 +885,20 @@ pub struct Selection {
|
||||||
pub stroke: Stroke,
|
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.
|
/// The visuals of widgets for different states of interaction.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
|
@ -1125,6 +1144,7 @@ impl Visuals {
|
||||||
striped: false,
|
striped: false,
|
||||||
|
|
||||||
slider_trailing_fill: false,
|
slider_trailing_fill: false,
|
||||||
|
handle_shape: HandleShape::Circle,
|
||||||
|
|
||||||
interact_cursor: None,
|
interact_cursor: None,
|
||||||
|
|
||||||
|
|
@ -1687,6 +1707,7 @@ impl Visuals {
|
||||||
striped,
|
striped,
|
||||||
|
|
||||||
slider_trailing_fill,
|
slider_trailing_fill,
|
||||||
|
handle_shape,
|
||||||
interact_cursor,
|
interact_cursor,
|
||||||
|
|
||||||
image_loading_spinners,
|
image_loading_spinners,
|
||||||
|
|
@ -1755,6 +1776,8 @@ impl Visuals {
|
||||||
|
|
||||||
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
|
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
|
||||||
|
|
||||||
|
handle_shape.ui(ui);
|
||||||
|
|
||||||
ComboBox::from_label("Interact Cursor")
|
ComboBox::from_label("Interact Cursor")
|
||||||
.selected_text(format!("{interact_cursor:?}"))
|
.selected_text(format!("{interact_cursor:?}"))
|
||||||
.show_ui(ui, |ui| {
|
.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"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use crate::*;
|
use crate::{style::HandleShape, *};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -87,6 +87,7 @@ pub struct Slider<'a> {
|
||||||
custom_formatter: Option<NumFormatter<'a>>,
|
custom_formatter: Option<NumFormatter<'a>>,
|
||||||
custom_parser: Option<NumParser<'a>>,
|
custom_parser: Option<NumParser<'a>>,
|
||||||
trailing_fill: Option<bool>,
|
trailing_fill: Option<bool>,
|
||||||
|
handle_shape: Option<HandleShape>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Slider<'a> {
|
impl<'a> Slider<'a> {
|
||||||
|
|
@ -133,6 +134,7 @@ impl<'a> Slider<'a> {
|
||||||
custom_formatter: None,
|
custom_formatter: None,
|
||||||
custom_parser: None,
|
custom_parser: None,
|
||||||
trailing_fill: None,
|
trailing_fill: None,
|
||||||
|
handle_shape: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,6 +304,16 @@ impl<'a> Slider<'a> {
|
||||||
self
|
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.
|
/// Set custom formatter defining how numbers are converted into text.
|
||||||
///
|
///
|
||||||
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
|
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
|
||||||
|
|
@ -570,7 +582,10 @@ impl<'a> Slider<'a> {
|
||||||
/// Just the slider, no text
|
/// Just the slider, no text
|
||||||
fn slider_ui(&mut self, ui: &Ui, response: &Response) {
|
fn slider_ui(&mut self, ui: &Ui, response: &Response) {
|
||||||
let rect = &response.rect;
|
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() {
|
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
|
||||||
let position = self.pointer_position(pointer_position_2d);
|
let position = self.pointer_position(pointer_position_2d);
|
||||||
|
|
@ -696,12 +711,37 @@ impl<'a> Slider<'a> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.painter().add(epaint::CircleShape {
|
let radius = self.handle_radius(rect);
|
||||||
center,
|
|
||||||
radius: self.handle_radius(rect) + visuals.expansion,
|
let handle_shape = self
|
||||||
fill: visuals.bg_fill,
|
.handle_shape
|
||||||
stroke: visuals.fg_stroke,
|
.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 = 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 {
|
match self.orientation {
|
||||||
SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius),
|
SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius),
|
||||||
// The vertical case has to be flipped because the largest slider value maps to the
|
// 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 slider_response = response.clone();
|
||||||
|
|
||||||
let value_response = if self.show_value {
|
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);
|
let value_response = self.value_ui(ui, position_range);
|
||||||
if value_response.gained_focus()
|
if value_response.gained_focus()
|
||||||
|| value_response.has_focus()
|
|| value_response.has_focus()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use egui::*;
|
use egui::{style::HandleShape, *};
|
||||||
use std::f64::INFINITY;
|
use std::f64::INFINITY;
|
||||||
|
|
||||||
/// Showcase sliders
|
/// Showcase sliders
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct Sliders {
|
||||||
pub vertical: bool,
|
pub vertical: bool,
|
||||||
pub value: f64,
|
pub value: f64,
|
||||||
pub trailing_fill: bool,
|
pub trailing_fill: bool,
|
||||||
|
pub handle_shape: HandleShape,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Sliders {
|
impl Default for Sliders {
|
||||||
|
|
@ -33,6 +34,7 @@ impl Default for Sliders {
|
||||||
vertical: false,
|
vertical: false,
|
||||||
value: 10.0,
|
value: 10.0,
|
||||||
trailing_fill: false,
|
trailing_fill: false,
|
||||||
|
handle_shape: HandleShape::Circle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +69,7 @@ impl super::View for Sliders {
|
||||||
vertical,
|
vertical,
|
||||||
value,
|
value,
|
||||||
trailing_fill,
|
trailing_fill,
|
||||||
|
handle_shape,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.label("You can click a slider value to edit it with the keyboard.");
|
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)
|
.orientation(orientation)
|
||||||
.text("i32 demo slider")
|
.text("i32 demo slider")
|
||||||
.step_by(istep)
|
.step_by(istep)
|
||||||
.trailing_fill(*trailing_fill),
|
.trailing_fill(*trailing_fill)
|
||||||
|
.handle_shape(*handle_shape),
|
||||||
);
|
);
|
||||||
*value = value_i32 as f64;
|
*value = value_i32 as f64;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -111,7 +115,8 @@ impl super::View for Sliders {
|
||||||
.orientation(orientation)
|
.orientation(orientation)
|
||||||
.text("f64 demo slider")
|
.text("f64 demo slider")
|
||||||
.step_by(istep)
|
.step_by(istep)
|
||||||
.trailing_fill(*trailing_fill),
|
.trailing_fill(*trailing_fill)
|
||||||
|
.handle_shape(*handle_shape),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.label(
|
ui.label(
|
||||||
|
|
@ -132,20 +137,26 @@ impl super::View for Sliders {
|
||||||
.logarithmic(true)
|
.logarithmic(true)
|
||||||
.smart_aim(*smart_aim)
|
.smart_aim(*smart_aim)
|
||||||
.text("left")
|
.text("left")
|
||||||
.trailing_fill(*trailing_fill),
|
.trailing_fill(*trailing_fill)
|
||||||
|
.handle_shape(*handle_shape),
|
||||||
);
|
);
|
||||||
ui.add(
|
ui.add(
|
||||||
Slider::new(max, type_min..=type_max)
|
Slider::new(max, type_min..=type_max)
|
||||||
.logarithmic(true)
|
.logarithmic(true)
|
||||||
.smart_aim(*smart_aim)
|
.smart_aim(*smart_aim)
|
||||||
.text("right")
|
.text("right")
|
||||||
.trailing_fill(*trailing_fill),
|
.trailing_fill(*trailing_fill)
|
||||||
|
.handle_shape(*handle_shape),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
ui.checkbox(trailing_fill, "Toggle trailing color");
|
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();
|
ui.separator();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue