use core::ops::RangeInclusive;
use crate::{
DragValue, EventFilter, Id, Key, NumExt as _, Pos2, Rangef, Rect, Response, TextStyle, Ui,
Vec2, WidgetText, emath, epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape,
vec2,
};
use super::drag_value::clamp_value_to_range;
use super::slider::{SliderClamping, SliderOrientation};
use super::value_format::ValueFormat;
const INFINITY: f64 = f64::INFINITY;
const INF_RANGE_MAGNITUDE: f64 = 10.0;
#[derive(Clone, Debug)]
pub struct SliderSpec {
pub logarithmic: bool,
pub smallest_positive: f64,
pub largest_finite: f64,
}
impl Default for SliderSpec {
fn default() -> Self {
Self {
logarithmic: false,
smallest_positive: 1e-6,
largest_finite: f64::INFINITY,
}
}
}
pub struct SliderGeometry {
pub orientation: SliderOrientation,
pub handle_shape: HandleShape,
pub range: RangeInclusive<f64>,
pub spec: SliderSpec,
pub rect: Rect,
}
impl SliderGeometry {
pub fn handle_radius(&self) -> f32 {
let limit = match self.orientation {
SliderOrientation::Horizontal => self.rect.height(),
SliderOrientation::Vertical => self.rect.width(),
};
limit / 2.5
}
pub fn position_range(&self) -> Rangef {
let handle_radius = self.handle_radius();
let handle_radius = match self.handle_shape {
HandleShape::Circle => handle_radius,
HandleShape::Rect { aspect_ratio } => handle_radius * aspect_ratio,
};
match self.orientation {
SliderOrientation::Horizontal => self.rect.x_range().shrink(handle_radius),
SliderOrientation::Vertical => self.rect.y_range().shrink(handle_radius).flipped(),
}
}
pub fn rail_rect(&self, radius: f32) -> Rect {
match self.orientation {
SliderOrientation::Horizontal => Rect::from_min_max(
pos2(self.rect.left(), self.rect.center().y - radius),
pos2(self.rect.right(), self.rect.center().y + radius),
),
SliderOrientation::Vertical => Rect::from_min_max(
pos2(self.rect.center().x - radius, self.rect.top()),
pos2(self.rect.center().x + radius, self.rect.bottom()),
),
}
}
pub 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),
}
}
pub fn handle_rect(&self, center: Pos2) -> Rect {
let radius = self.handle_radius();
let half_size = match self.handle_shape {
HandleShape::Circle => Vec2::splat(radius),
HandleShape::Rect { aspect_ratio } => match self.orientation {
SliderOrientation::Horizontal => Vec2::new(radius * aspect_ratio, radius),
SliderOrientation::Vertical => Vec2::new(radius, radius * aspect_ratio),
},
};
Rect::from_center_size(center, 2.0 * half_size)
}
pub fn position_grows_with_value(&self) -> bool {
let position_range = self.position_range();
(self.range.start() < self.range.end()) == (position_range.min < position_range.max)
}
pub fn pointer_position(&self, pointer_position_2d: Pos2) -> f32 {
match self.orientation {
SliderOrientation::Horizontal => pointer_position_2d.x,
SliderOrientation::Vertical => pointer_position_2d.y,
}
}
pub fn value_from_position(&self, position: f32) -> f64 {
let normalized = remap_clamp(position, self.position_range(), 0.0..=1.0) as f64;
value_from_normalized(normalized, self.range.clone(), &self.spec)
}
pub fn position_from_value(&self, value: f64) -> f32 {
let normalized = normalized_from_value(value, self.range.clone(), &self.spec);
lerp(self.position_range(), normalized as f32)
}
pub fn gradient_at(&self, value: f64) -> f64 {
let position = self.position_from_value(value);
self.value_from_position(position + 0.5) - self.value_from_position(position - 0.5)
}
}
pub struct DragValueSettings<'a> {
pub show: bool,
pub speed: Option<f64>,
pub format: ValueFormat<'a>,
}
impl Default for DragValueSettings<'_> {
fn default() -> Self {
Self {
show: true,
speed: None,
format: ValueFormat::default(),
}
}
}
pub struct SliderCore<'a> {
pub range: RangeInclusive<f64>,
pub spec: SliderSpec,
pub clamping: SliderClamping,
pub smart_aim: bool,
pub orientation: SliderOrientation,
pub text: WidgetText,
pub step: Option<f64>,
pub drag_value: DragValueSettings<'a>,
pub handle_shape: Option<HandleShape>,
}
impl SliderCore<'_> {
pub fn new(range: RangeInclusive<f64>) -> Self {
Self {
range,
spec: SliderSpec::default(),
clamping: SliderClamping::default(),
smart_aim: true,
orientation: SliderOrientation::Horizontal,
text: WidgetText::default(),
step: None,
drag_value: DragValueSettings::default(),
handle_shape: None,
}
}
pub fn sorted_range(&self) -> RangeInclusive<f64> {
let (start, end) = (*self.range.start(), *self.range.end());
start.min(end)..=start.max(end)
}
pub fn editable_range(&self) -> RangeInclusive<f64> {
if self.clamping == SliderClamping::Never {
f64::NEG_INFINITY..=f64::INFINITY
} else {
self.sorted_range()
}
}
pub fn existing(&self, value: f64) -> f64 {
if self.clamping == SliderClamping::Always {
clamp_value_to_range(value, self.range.clone())
} else {
value
}
}
pub fn rounded(&self, mut value: f64) -> f64 {
if self.clamping != SliderClamping::Never {
value = clamp_value_to_range(value, self.range.clone());
}
if let Some(step) = self.step {
let start = *self.range.start();
value = start + ((value - start) / step).round() * step;
}
self.drag_value.format.round(value)
}
pub fn geometry(&self, rect: Rect, ui: &Ui) -> SliderGeometry {
SliderGeometry {
orientation: self.orientation,
handle_shape: self
.handle_shape
.unwrap_or_else(|| ui.style().visuals.handle_shape),
range: self.range.clone(),
spec: self.spec.clone(),
rect,
}
}
pub fn desired_size(&self, ui: &Ui) -> Vec2 {
let thickness = ui
.text_style_height(&TextStyle::Body)
.at_least(ui.spacing().interact_size.y);
match self.orientation {
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
}
}
pub fn stepped_value(&self, geom: &SliderGeometry, prev_value: f64, rail_steps: f32) -> f64 {
let ui_point_per_step = 1.0; let prev_position = geom.position_from_value(prev_value);
let new_position = prev_position + ui_point_per_step * rail_steps;
let mut new_value = match self.step {
Some(step) => prev_value + (rail_steps * value_direction(geom)) as f64 * step,
None if self.smart_aim => {
let aim_radius = 0.49 * ui_point_per_step; emath::smart_aim::best_in_range_f64(
geom.value_from_position(new_position - aim_radius),
geom.value_from_position(new_position + aim_radius),
)
}
_ => geom.value_from_position(new_position),
};
if let Some(max_decimals) = self.drag_value.format.max_decimals {
let min_increment = 1.0 / (10.0_f64.powi(max_decimals as i32));
new_value = if prev_value < new_value {
f64::max(new_value, prev_value + min_increment * 1.001)
} else if new_value < prev_value {
f64::min(new_value, prev_value - min_increment * 1.001)
} else {
new_value
};
}
new_value
}
pub fn drag_value_speed_at(&self, ui: &Ui, geom: &SliderGeometry, value: f64) -> f64 {
let arrow_presses = ui.input(|input| {
input.num_presses(Key::ArrowUp)
+ input.num_presses(Key::ArrowRight)
+ input.num_presses(Key::ArrowDown)
+ input.num_presses(Key::ArrowLeft)
});
match self.step {
Some(step) if 0 < arrow_presses => step,
_ => self
.drag_value
.speed
.unwrap_or_else(|| geom.gradient_at(value)),
}
}
pub fn drag_value_ui(
&self,
ui: &mut Ui,
value: &mut f64,
bounds: RangeInclusive<f64>,
speed: f64,
) -> Response {
ui.add(
DragValue::new(value)
.speed(speed)
.format(self.drag_value.format.clone())
.range(bounds)
.clamp_existing_to_range(self.clamping == SliderClamping::Always),
)
}
}
pub fn paint_rail(ui: &Ui, geom: &SliderGeometry) -> Rect {
let rail_radius = (ui.spacing().slider_rail_height / 2.0).at_least(0.0);
let rail_rect = geom.rail_rect(rail_radius);
let widget_visuals = &ui.visuals().widgets;
ui.painter().rect_filled(
rail_rect,
widget_visuals.inactive.corner_radius,
widget_visuals.inactive.bg_fill,
);
rail_rect
}
pub fn paint_fill(ui: &Ui, rail_rect: Rect, span: Rangef, orientation: SliderOrientation) {
let corner_radius = ui.visuals().widgets.inactive.corner_radius;
let (span_min, span_max) = (span.min.min(span.max), span.min.max(span.max));
let mut fill_rect = rail_rect;
match orientation {
SliderOrientation::Horizontal => {
let overhang = corner_radius.nw as f32;
fill_rect.min.x = (span_min - overhang).at_least(rail_rect.min.x);
fill_rect.max.x = (span_max + overhang).at_most(rail_rect.max.x);
}
SliderOrientation::Vertical => {
let overhang = corner_radius.se as f32;
fill_rect.min.y = (span_min - overhang).at_least(rail_rect.min.y);
fill_rect.max.y = (span_max + overhang).at_most(rail_rect.max.y);
}
}
ui.painter()
.rect_filled(fill_rect, corner_radius, ui.visuals().selection.bg_fill);
}
pub fn paint_handle(ui: &Ui, geom: &SliderGeometry, center: Pos2, visuals: &style::WidgetVisuals) {
let radius = geom.handle_radius();
match geom.handle_shape {
HandleShape::Circle => {
ui.painter().add(epaint::CircleShape {
center,
radius: radius + visuals.expansion,
fill: visuals.bg_fill,
stroke: visuals.fg_stroke,
});
}
HandleShape::Rect { .. } => {
let rect = geom.handle_rect(center).expand(visuals.expansion);
ui.painter().rect(
rect,
visuals.corner_radius,
visuals.bg_fill,
visuals.fg_stroke,
epaint::StrokeKind::Inside,
);
}
}
}
pub fn value_at_pointer(
ui: &Ui,
geom: &SliderGeometry,
pointer_position_2d: Pos2,
smart_aim: bool,
) -> f64 {
let position = geom.pointer_position(pointer_position_2d);
if smart_aim {
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
geom.value_from_position(position - aim_radius),
geom.value_from_position(position + aim_radius),
)
} else {
geom.value_from_position(position)
}
}
pub fn keyboard_steps(ui: &Ui, response: &Response, geom: &SliderGeometry) -> f32 {
let orientation = geom.orientation;
let mut decrement = 0usize;
let mut increment = 0usize;
if response.has_focus() {
ui.memory_mut(|m| {
m.set_focus_lock_filter(
response.id,
EventFilter {
horizontal_arrows: matches!(orientation, SliderOrientation::Horizontal),
vertical_arrows: matches!(orientation, SliderOrientation::Vertical),
..Default::default()
},
);
});
let (dec_key, inc_key) = match orientation {
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
};
ui.input(|input| {
decrement += input.num_presses(dec_key);
increment += input.num_presses(inc_key);
});
}
let arrow_steps = increment as f32 - decrement as f32;
let value_steps = ui.input(|input| {
use accesskit::Action;
input.num_accesskit_action_requests(response.id, Action::Increment) as f32
- input.num_accesskit_action_requests(response.id, Action::Decrement) as f32
});
arrow_steps + value_steps * value_direction(geom)
}
fn value_direction(geom: &SliderGeometry) -> f32 {
if geom.position_grows_with_value() {
1.0
} else {
-1.0
}
}
pub fn accesskit_set_value_request(ui: &Ui, id: Id) -> Option<f64> {
ui.input(|input| {
use accesskit::{Action, ActionData};
input
.accesskit_action_requests(id, Action::SetValue)
.filter_map(|request| match request.data {
Some(ActionData::NumericValue(value)) => Some(value),
_ => None,
})
.last()
})
}
pub fn declare_accesskit_slider(
ui: &Ui,
id: Id,
value: f64,
bounds: &RangeInclusive<f64>,
editable_range: &RangeInclusive<f64>,
step: Option<f64>,
) {
ui.ctx().accesskit_node_builder(id, |builder| {
use accesskit::Action;
builder.set_min_numeric_value(*bounds.start());
builder.set_max_numeric_value(*bounds.end());
if let Some(step) = step {
builder.set_numeric_value_step(step);
}
builder.add_action(Action::SetValue);
if value < *editable_range.end() {
builder.add_action(Action::Increment);
}
if *editable_range.start() < value {
builder.add_action(Action::Decrement);
}
});
}
fn value_from_normalized(normalized: f64, range: RangeInclusive<f64>, spec: &SliderSpec) -> f64 {
let (min, max) = (*range.start(), *range.end());
if min.is_nan() || max.is_nan() {
f64::NAN
} else if min == max {
min
} else if min > max {
value_from_normalized(1.0 - normalized, max..=min, spec)
} else if normalized <= 0.0 {
min
} else if normalized >= 1.0 {
max
} else if spec.logarithmic {
if max <= 0.0 {
-value_from_normalized(normalized, -min..=-max, spec)
} else if 0.0 <= min {
let (min_log, max_log) = range_log10(min, max, spec);
let log = lerp(min_log..=max_log, normalized);
10.0_f64.powf(log)
} else {
assert!(
min < 0.0 && 0.0 < max,
"min should be negative and max positive, but got min={min} and max={max}"
);
let zero_cutoff = logarithmic_zero_cutoff(min, max);
if normalized < zero_cutoff {
value_from_normalized(
remap(normalized, 0.0..=zero_cutoff, 0.0..=1.0),
min..=0.0,
spec,
)
} else {
value_from_normalized(
remap(normalized, zero_cutoff..=1.0, 0.0..=1.0),
0.0..=max,
spec,
)
}
}
} else {
debug_assert!(
min.is_finite() && max.is_finite(),
"You should use a logarithmic range"
);
lerp(range, normalized.clamp(0.0, 1.0))
}
}
fn normalized_from_value(value: f64, range: RangeInclusive<f64>, spec: &SliderSpec) -> f64 {
let (min, max) = (*range.start(), *range.end());
if min.is_nan() || max.is_nan() {
f64::NAN
} else if min == max {
0.5 } else if min > max {
1.0 - normalized_from_value(value, max..=min, spec)
} else if value <= min {
0.0
} else if value >= max {
1.0
} else if spec.logarithmic {
if max <= 0.0 {
normalized_from_value(-value, -min..=-max, spec)
} else if 0.0 <= min {
let (min_log, max_log) = range_log10(min, max, spec);
let value_log = value.log10();
remap_clamp(value_log, min_log..=max_log, 0.0..=1.0)
} else {
assert!(
min < 0.0 && 0.0 < max,
"min should be negative and max positive, but got min={min} and max={max}"
);
let zero_cutoff = logarithmic_zero_cutoff(min, max);
if value < 0.0 {
remap(
normalized_from_value(value, min..=0.0, spec),
0.0..=1.0,
0.0..=zero_cutoff,
)
} else {
remap(
normalized_from_value(value, 0.0..=max, spec),
0.0..=1.0,
zero_cutoff..=1.0,
)
}
}
} else {
debug_assert!(
min.is_finite() && max.is_finite(),
"You should use a logarithmic range"
);
remap_clamp(value, range, 0.0..=1.0)
}
}
fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) {
assert!(spec.logarithmic, "spec must be logarithmic");
assert!(
min <= max,
"min must be less than or equal to max, but was min={min} and max={max}"
);
if min == 0.0 && max == INFINITY {
(spec.smallest_positive.log10(), INF_RANGE_MAGNITUDE)
} else if min == 0.0 {
if spec.smallest_positive < max {
(spec.smallest_positive.log10(), max.log10())
} else {
(max.log10() - INF_RANGE_MAGNITUDE, max.log10())
}
} else if max == INFINITY {
if min < spec.largest_finite {
(min.log10(), spec.largest_finite.log10())
} else {
(min.log10(), min.log10() + INF_RANGE_MAGNITUDE)
}
} else {
(min.log10(), max.log10())
}
}
fn logarithmic_zero_cutoff(min: f64, max: f64) -> f64 {
assert!(
min < 0.0 && 0.0 < max,
"min must be negative and max positive, but got min={min} and max={max}"
);
let min_magnitude = if min == -INFINITY {
INF_RANGE_MAGNITUDE
} else {
min.abs().log10().abs()
};
let max_magnitude = if max == INFINITY {
INF_RANGE_MAGNITUDE
} else {
max.log10().abs()
};
let cutoff = min_magnitude / (min_magnitude + max_magnitude);
debug_assert!(
0.0 <= cutoff && cutoff <= 1.0,
"Bad cutoff {cutoff:?} for min {min:?} and max {max:?}"
);
cutoff
}
#[cfg(test)]
mod tests {
use super::{SliderGeometry, SliderSpec};
use crate::widgets::slider::SliderOrientation;
use crate::{Rect, pos2};
fn geometry(orientation: SliderOrientation) -> SliderGeometry {
SliderGeometry {
orientation,
handle_shape: crate::style::HandleShape::Circle,
range: 0.0..=100.0,
spec: SliderSpec {
logarithmic: false,
smallest_positive: 1e-6,
largest_finite: f64::INFINITY,
},
rect: Rect::from_min_max(pos2(0.0, 0.0), pos2(200.0, 20.0)),
}
}
#[test]
fn value_position_round_trip() {
for orientation in [SliderOrientation::Horizontal, SliderOrientation::Vertical] {
let geom = geometry(orientation);
for value in [0.0, 1.0, 50.0, 99.0, 100.0] {
let round_tripped = geom.value_from_position(geom.position_from_value(value));
assert!(
(round_tripped - value).abs() < 0.1,
"{orientation:?}: {value} came back as {round_tripped}"
);
}
}
}
#[test]
fn rail_ends_map_to_range_ends() {
let geom = geometry(SliderOrientation::Horizontal);
let positions = geom.position_range();
assert_eq!(geom.value_from_position(positions.min), 0.0);
assert_eq!(geom.value_from_position(positions.max), 100.0);
}
}