Skip to content

Commit f2d2535

Browse files
authored
SliderPrecision component (#20032)
This PR adds a `SliderPrecision` component that lets you control the rounding when dragging a slider. Part of #19236
1 parent c65ef19 commit f2d2535

File tree

4 files changed

+75
-12
lines changed

4 files changed

+75
-12
lines changed

crates/bevy_core_widgets/src/core_slider.rs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use bevy_input::keyboard::{KeyCode, KeyboardInput};
1919
use bevy_input::ButtonState;
2020
use bevy_input_focus::FocusedInput;
2121
use bevy_log::warn_once;
22+
use bevy_math::ops;
2223
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
2324
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
2425

@@ -38,7 +39,8 @@ pub enum TrackClick {
3839

3940
/// A headless slider widget, which can be used to build custom sliders. Sliders have a value
4041
/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An
41-
/// optional step size can be specified via [`SliderStep`].
42+
/// optional step size can be specified via [`SliderStep`], and you can control the rounding
43+
/// during dragging with [`SliderPrecision`].
4244
///
4345
/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This
4446
/// can be useful in a console environment for controlling the value gamepad inputs.
@@ -187,6 +189,25 @@ impl Default for SliderStep {
187189
}
188190
}
189191

192+
/// A component which controls the rounding of the slider value during dragging.
193+
///
194+
/// Stepping is not affected, although presumably the step size will be an integer multiple of the
195+
/// rounding factor. This also doesn't prevent the slider value from being set to non-rounded values
196+
/// by other means, such as manually entering digits via a numeric input field.
197+
///
198+
/// The value in this component represents the number of decimal places of desired precision, so a
199+
/// value of 2 would round to the nearest 1/100th. A value of -3 would round to the nearest
200+
/// thousand.
201+
#[derive(Component, Debug, Default, Clone, Copy)]
202+
pub struct SliderPrecision(pub i32);
203+
204+
impl SliderPrecision {
205+
fn round(&self, value: f32) -> f32 {
206+
let factor = ops::powf(10.0_f32, self.0 as f32);
207+
(value * factor).round() / factor
208+
}
209+
}
210+
190211
/// Component used to manage the state of a slider during dragging.
191212
#[derive(Component, Default)]
192213
pub struct CoreSliderDragState {
@@ -204,6 +225,7 @@ pub(crate) fn slider_on_pointer_down(
204225
&SliderValue,
205226
&SliderRange,
206227
&SliderStep,
228+
Option<&SliderPrecision>,
207229
&ComputedNode,
208230
&ComputedNodeTarget,
209231
&UiGlobalTransform,
@@ -217,8 +239,17 @@ pub(crate) fn slider_on_pointer_down(
217239
if q_thumb.contains(trigger.target()) {
218240
// Thumb click, stop propagation to prevent track click.
219241
trigger.propagate(false);
220-
} else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) =
221-
q_slider.get(trigger.target())
242+
} else if let Ok((
243+
slider,
244+
value,
245+
range,
246+
step,
247+
precision,
248+
node,
249+
node_target,
250+
transform,
251+
disabled,
252+
)) = q_slider.get(trigger.target())
222253
{
223254
// Track click
224255
trigger.propagate(false);
@@ -257,7 +288,9 @@ pub(crate) fn slider_on_pointer_down(
257288
value.0 + step.0
258289
}
259290
}
260-
TrackClick::Snap => click_val,
291+
TrackClick::Snap => precision
292+
.map(|prec| prec.round(click_val))
293+
.unwrap_or(click_val),
261294
});
262295

263296
if matches!(slider.on_change, Callback::Ignore) {
@@ -296,6 +329,7 @@ pub(crate) fn slider_on_drag(
296329
&ComputedNode,
297330
&CoreSlider,
298331
&SliderRange,
332+
Option<&SliderPrecision>,
299333
&UiGlobalTransform,
300334
&mut CoreSliderDragState,
301335
Has<InteractionDisabled>,
@@ -305,7 +339,8 @@ pub(crate) fn slider_on_drag(
305339
mut commands: Commands,
306340
ui_scale: Res<UiScale>,
307341
) {
308-
if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target())
342+
if let Ok((node, slider, range, precision, transform, drag, disabled)) =
343+
q_slider.get_mut(trigger.target())
309344
{
310345
trigger.propagate(false);
311346
if drag.dragging && !disabled {
@@ -320,17 +355,22 @@ pub(crate) fn slider_on_drag(
320355
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
321356
let span = range.span();
322357
let new_value = if span > 0. {
323-
range.clamp(drag.offset + (distance.x * span) / slider_width)
358+
drag.offset + (distance.x * span) / slider_width
324359
} else {
325360
range.start() + span * 0.5
326361
};
362+
let rounded_value = range.clamp(
363+
precision
364+
.map(|prec| prec.round(new_value))
365+
.unwrap_or(new_value),
366+
);
327367

328368
if matches!(slider.on_change, Callback::Ignore) {
329369
commands
330370
.entity(trigger.target())
331-
.insert(SliderValue(new_value));
371+
.insert(SliderValue(rounded_value));
332372
} else {
333-
commands.notify_with(&slider.on_change, new_value);
373+
commands.notify_with(&slider.on_change, rounded_value);
334374
}
335375
}
336376
}
@@ -491,3 +531,24 @@ impl Plugin for CoreSliderPlugin {
491531
.add_observer(slider_on_set_value);
492532
}
493533
}
534+
535+
#[cfg(test)]
536+
mod tests {
537+
use super::*;
538+
539+
#[test]
540+
fn test_slider_precision_rounding() {
541+
// Test positive precision values (decimal places)
542+
let precision_2dp = SliderPrecision(2);
543+
assert_eq!(precision_2dp.round(1.234567), 1.23);
544+
assert_eq!(precision_2dp.round(1.235), 1.24);
545+
546+
// Test zero precision (rounds to integers)
547+
let precision_0dp = SliderPrecision(0);
548+
assert_eq!(precision_0dp.round(1.4), 1.0);
549+
550+
// Test negative precision (rounds to tens, hundreds, etc.)
551+
let precision_neg1 = SliderPrecision(-1);
552+
assert_eq!(precision_neg1.round(14.0), 10.0);
553+
}
554+
}

crates/bevy_core_widgets/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub use core_scrollbar::{
3333
};
3434
pub use core_slider::{
3535
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
36-
SliderRange, SliderStep, SliderValue, TrackClick,
36+
SliderPrecision, SliderRange, SliderStep, SliderValue, TrackClick,
3737
};
3838

3939
/// A plugin group that registers the observers for all of the core widgets. If you don't want to

examples/ui/feathers.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! This example shows off the various Bevy Feathers widgets.
22
33
use bevy::{
4-
core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderStep},
4+
core_widgets::{
5+
Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep,
6+
},
57
feathers::{
68
controls::{
79
button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant,
@@ -259,7 +261,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
259261
value: 20.0,
260262
..default()
261263
},
262-
SliderStep(10.)
264+
(SliderStep(10.), SliderPrecision(2)),
263265
),
264266
]
265267
),],

release-content/release-notes/headless-widgets.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Headless Widgets
33
authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"]
4-
pull_requests: [19366, 19584, 19665, 19778, 19803, 20036]
4+
pull_requests: [19366, 19584, 19665, 19778, 19803, 20032, 20036]
55
---
66

77
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately

0 commit comments

Comments
 (0)