use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, Key, MouseButton};
use crate::geometry::{Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{paint_subtree, Widget};
use crate::widgets::label::{Label, LabelAlign};
use crate::widgets::slider_math::{
best_in_range_f64, clamp_value_to_range, normalized_from_value, value_from_normalized,
SliderSpec,
};
mod paint;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SliderOrientation {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SliderClamping {
Never,
Edits,
#[default]
Always,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HandleShape {
Circle,
Rect { aspect_ratio: f64 },
}
impl Default for HandleShape {
fn default() -> Self {
Self::Circle
}
}
const AIM_RADIUS: f64 = 1.5;
const VERT_LEN: f64 = 140.0;
const TRACK_H: f64 = 4.0;
const THUMB_R: f64 = 7.0;
const WIDGET_H: f64 = 22.0;
const VALUE_W: f64 = 44.0;
const VALUE_GAP: f64 = 6.0;
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
#[derive(Clone, Debug)]
pub struct SliderProps {
pub value: f64,
pub min: f64,
pub max: f64,
pub step: f64,
pub show_value: bool,
pub decimals: Option<usize>,
pub font_size: f64,
}
impl Default for SliderProps {
fn default() -> Self {
Self {
value: 0.0,
min: 0.0,
max: 1.0,
step: 0.01,
show_value: true,
decimals: None,
font_size: 12.0,
}
}
}
pub struct Slider {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
pub props: SliderProps,
dragging: bool,
focused: bool,
hovered: bool,
on_change: Option<Box<dyn FnMut(f64)>>,
value_cell: Option<Rc<Cell<f64>>>,
value_label: Label,
last_value_text: String,
spec: SliderSpec,
clamping: SliderClamping,
smart_aim: bool,
orientation: SliderOrientation,
handle_shape: HandleShape,
integer: bool,
suffix: String,
trailing_fill: bool,
}
impl Slider {
pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
let v = clamp_value_to_range(value, min, max);
let default_step = {
let s = (max - min) / 100.0;
if s.is_finite() {
s
} else {
0.0
}
};
let font_size = 12.0;
let value_label = Label::new("", Arc::clone(&font))
.with_font_size(font_size)
.with_align(LabelAlign::Right);
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
props: SliderProps {
value: v,
min,
max,
step: default_step,
show_value: true,
decimals: None,
font_size,
},
dragging: false,
focused: false,
hovered: false,
on_change: None,
value_cell: None,
value_label,
last_value_text: String::new(),
spec: SliderSpec::default(),
clamping: SliderClamping::default(),
smart_aim: true,
orientation: SliderOrientation::Horizontal,
handle_shape: HandleShape::Circle,
integer: false,
suffix: String::new(),
trailing_fill: true,
}
}
pub fn with_step(mut self, step: f64) -> Self {
self.props.step = step;
self
}
pub fn with_logarithmic(mut self, logarithmic: bool) -> Self {
self.spec.logarithmic = logarithmic;
self
}
pub fn with_smallest_positive(mut self, smallest_positive: f64) -> Self {
self.spec.smallest_positive = smallest_positive;
self
}
pub fn with_largest_finite(mut self, largest_finite: f64) -> Self {
self.spec.largest_finite = largest_finite;
self
}
pub fn with_clamping(mut self, clamping: SliderClamping) -> Self {
self.clamping = clamping;
self
}
pub fn with_smart_aim(mut self, smart_aim: bool) -> Self {
self.smart_aim = smart_aim;
self
}
pub fn with_orientation(mut self, orientation: SliderOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn with_handle_shape(mut self, shape: HandleShape) -> Self {
self.handle_shape = shape;
self
}
pub fn with_integer(mut self, integer: bool) -> Self {
self.integer = integer;
if integer {
self.spec.smallest_positive = 1.0;
}
self
}
pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
self.suffix = suffix.into();
self
}
pub fn with_trailing_fill(mut self, trailing_fill: bool) -> Self {
self.trailing_fill = trailing_fill;
self
}
pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
self.props.value = clamp_value_to_range(cell.get(), self.props.min, self.props.max);
self.value_cell = Some(cell);
self
}
pub fn with_show_value(mut self, show: bool) -> Self {
self.props.show_value = show;
self
}
pub fn with_decimals(mut self, decimals: usize) -> Self {
self.props.decimals = Some(decimals);
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
pub fn with_min_size(mut self, s: Size) -> Self {
self.base.min_size = s;
self
}
pub fn with_max_size(mut self, s: Size) -> Self {
self.base.max_size = s;
self
}
pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
self.on_change = Some(Box::new(cb));
self
}
pub fn value(&self) -> f64 {
self.props.value
}
pub fn set_value(&mut self, v: f64) {
if v.is_nan() {
return;
}
self.props.value = self.commit(v);
if let Some(cell) = &self.value_cell {
cell.set(self.props.value);
}
}
fn fire(&mut self) {
let v = self.props.value;
if let Some(cell) = &self.value_cell {
cell.set(v);
}
if let Some(cb) = self.on_change.as_mut() {
cb(v);
}
}
fn is_vertical(&self) -> bool {
matches!(self.orientation, SliderOrientation::Vertical)
}
fn handle_extent(&self) -> f64 {
match self.handle_shape {
HandleShape::Circle => THUMB_R,
HandleShape::Rect { aspect_ratio } => THUMB_R * aspect_ratio,
}
}
fn track_right(&self) -> f64 {
let reserved = if self.props.show_value {
VALUE_W + VALUE_GAP
} else {
0.0
};
(self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
}
fn position_range(&self) -> (f64, f64) {
let hr = self.handle_extent();
if self.is_vertical() {
let bottom = self.bounds.height - hr; let top = hr; (bottom, top)
} else {
(THUMB_R, self.track_right()) }
}
fn normalized(&self) -> f64 {
normalized_from_value(self.props.value, self.props.min, self.props.max, &self.spec)
}
fn thumb_pos(&self) -> f64 {
let (p0, p1) = self.position_range();
p0 + self.normalized().clamp(0.0, 1.0) * (p1 - p0)
}
fn value_from_pixel(&self, px: f64) -> f64 {
let (p0, p1) = self.position_range();
let n = if (p1 - p0).abs() < f64::EPSILON {
0.0
} else {
((px - p0) / (p1 - p0)).clamp(0.0, 1.0)
};
value_from_normalized(n, self.props.min, self.props.max, &self.spec)
}
fn commit(&self, mut value: f64) -> f64 {
if value.is_nan() {
return self.props.value;
}
if self.clamping != SliderClamping::Never {
value = clamp_value_to_range(value, self.props.min, self.props.max);
}
if self.props.step > 0.0 && self.props.step.is_finite() && self.props.min.is_finite() {
let start = self.props.min;
value = start + ((value - start) / self.props.step).round() * self.props.step;
}
if self.integer {
value = value.round();
}
value
}
fn pointer_value(&self, px: f64) -> f64 {
let raw = if self.smart_aim {
best_in_range_f64(
self.value_from_pixel(px - AIM_RADIUS),
self.value_from_pixel(px + AIM_RADIUS),
)
} else {
self.value_from_pixel(px)
};
self.commit(raw)
}
fn nudge(&self, dir: f64) -> f64 {
if self.props.step > 0.0 {
return self.commit(self.props.value + dir * self.props.step);
}
let (p0, p1) = self.position_range();
let cur = self.thumb_pos();
let px = cur + dir * (p1 - p0).signum();
let raw = if self.smart_aim {
best_in_range_f64(
self.value_from_pixel(px - 0.49),
self.value_from_pixel(px + 0.49),
)
} else {
self.value_from_pixel(px)
};
self.commit(raw)
}
fn auto_decimals(&self) -> usize {
if self.integer {
return 0;
}
if self.props.step >= 1.0 {
0
} else if self.props.step >= 0.1 {
1
} else if self.props.step >= 0.01 {
2
} else if self.props.step > 0.0 {
3
} else {
let v = self.props.value.abs();
if v == 0.0 || !v.is_finite() {
2
} else {
(3 - v.log10().floor() as i32).clamp(0, 6) as usize
}
}
}
fn format_value(&self) -> String {
let decimals = self.props.decimals.unwrap_or_else(|| self.auto_decimals());
format!("{:.*}{}", decimals, self.props.value, self.suffix)
}
}
impl Widget for Slider {
fn type_name(&self) -> &'static str {
"Slider"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
#[cfg(feature = "reflect")]
fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
Some(&self.props)
}
#[cfg(feature = "reflect")]
fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
Some(&mut self.props)
}
fn is_focusable(&self) -> bool {
true
}
fn margin(&self) -> Insets {
self.base.margin
}
fn widget_base(&self) -> Option<&WidgetBase> {
Some(&self.base)
}
fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
Some(&mut self.base)
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.base.min_size
}
fn max_size(&self) -> Size {
self.base.max_size
}
fn layout(&mut self, available: Size) -> Size {
if !self.dragging {
if let Some(cell) = &self.value_cell {
let raw = cell.get();
if !raw.is_nan() {
self.props.value = if self.clamping == SliderClamping::Always {
clamp_value_to_range(raw, self.props.min, self.props.max)
} else {
raw
};
}
}
}
if self.props.show_value {
let new_text = self.format_value();
if new_text != self.last_value_text {
self.value_label.set_text(new_text.clone());
self.last_value_text = new_text;
}
let lh = self.props.font_size * 1.5;
let _ = self.value_label.layout(Size::new(VALUE_W, lh));
self.value_label
.set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
}
if self.is_vertical() {
Size::new(available.width, VERT_LEN)
} else {
Size::new(available.width, WIDGET_H)
}
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
if self.is_vertical() {
self.paint_vertical(ctx);
} else {
self.paint_horizontal(ctx);
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
if self.dragging {
let px = if self.is_vertical() { pos.y } else { pos.x };
self.props.value = self.pointer_value(px);
self.fire();
crate::animation::request_draw();
return EventResult::Consumed;
}
if was != self.hovered {
crate::animation::request_draw();
return EventResult::Consumed;
}
EventResult::Ignored
}
Event::MouseDown {
button: MouseButton::Left,
pos,
..
} => {
self.dragging = true;
let px = if self.is_vertical() { pos.y } else { pos.x };
self.props.value = self.pointer_value(px);
self.fire();
crate::animation::request_draw();
EventResult::Consumed
}
Event::MouseUp {
button: MouseButton::Left,
..
} => {
let was = self.dragging;
self.dragging = false;
if was {
crate::animation::request_draw();
}
EventResult::Consumed
}
Event::KeyDown { key, .. } => {
let dir = match (key, self.is_vertical()) {
(Key::ArrowLeft, false) => -1.0,
(Key::ArrowRight, false) => 1.0,
(Key::ArrowDown, true) => -1.0,
(Key::ArrowUp, true) => 1.0,
_ => 0.0,
};
if dir != 0.0 {
self.props.value = self.nudge(dir);
self.fire();
crate::animation::request_draw();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
Event::FocusGained => {
let was = self.focused;
self.focused = true;
if !was {
crate::animation::request_draw();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
Event::FocusLost => {
let was_focused = self.focused;
let was_dragging = self.dragging;
self.focused = false;
self.dragging = false;
if was_focused || was_dragging {
crate::animation::request_draw();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
_ => EventResult::Ignored,
}
}
}
#[cfg(test)]
mod tests;