SliderPrecision component (#20032)
This PR adds a `SliderPrecision` component that lets you control the rounding when dragging a slider. Part of #19236
This commit is contained in:
parent
c65ef19b7c
commit
f2d25355c3
@ -19,6 +19,7 @@ use bevy_input::keyboard::{KeyCode, KeyboardInput};
|
|||||||
use bevy_input::ButtonState;
|
use bevy_input::ButtonState;
|
||||||
use bevy_input_focus::FocusedInput;
|
use bevy_input_focus::FocusedInput;
|
||||||
use bevy_log::warn_once;
|
use bevy_log::warn_once;
|
||||||
|
use bevy_math::ops;
|
||||||
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
|
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
|
||||||
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
|
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
|
||||||
|
|
||||||
@ -38,7 +39,8 @@ pub enum TrackClick {
|
|||||||
|
|
||||||
/// A headless slider widget, which can be used to build custom sliders. Sliders have a value
|
/// A headless slider widget, which can be used to build custom sliders. Sliders have a value
|
||||||
/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An
|
/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An
|
||||||
/// optional step size can be specified via [`SliderStep`].
|
/// optional step size can be specified via [`SliderStep`], and you can control the rounding
|
||||||
|
/// during dragging with [`SliderPrecision`].
|
||||||
///
|
///
|
||||||
/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This
|
/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This
|
||||||
/// can be useful in a console environment for controlling the value gamepad inputs.
|
/// can be useful in a console environment for controlling the value gamepad inputs.
|
||||||
@ -187,6 +189,25 @@ impl Default for SliderStep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A component which controls the rounding of the slider value during dragging.
|
||||||
|
///
|
||||||
|
/// Stepping is not affected, although presumably the step size will be an integer multiple of the
|
||||||
|
/// rounding factor. This also doesn't prevent the slider value from being set to non-rounded values
|
||||||
|
/// by other means, such as manually entering digits via a numeric input field.
|
||||||
|
///
|
||||||
|
/// The value in this component represents the number of decimal places of desired precision, so a
|
||||||
|
/// value of 2 would round to the nearest 1/100th. A value of -3 would round to the nearest
|
||||||
|
/// thousand.
|
||||||
|
#[derive(Component, Debug, Default, Clone, Copy)]
|
||||||
|
pub struct SliderPrecision(pub i32);
|
||||||
|
|
||||||
|
impl SliderPrecision {
|
||||||
|
fn round(&self, value: f32) -> f32 {
|
||||||
|
let factor = ops::powf(10.0_f32, self.0 as f32);
|
||||||
|
(value * factor).round() / factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Component used to manage the state of a slider during dragging.
|
/// Component used to manage the state of a slider during dragging.
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct CoreSliderDragState {
|
pub struct CoreSliderDragState {
|
||||||
@ -204,6 +225,7 @@ pub(crate) fn slider_on_pointer_down(
|
|||||||
&SliderValue,
|
&SliderValue,
|
||||||
&SliderRange,
|
&SliderRange,
|
||||||
&SliderStep,
|
&SliderStep,
|
||||||
|
Option<&SliderPrecision>,
|
||||||
&ComputedNode,
|
&ComputedNode,
|
||||||
&ComputedNodeTarget,
|
&ComputedNodeTarget,
|
||||||
&UiGlobalTransform,
|
&UiGlobalTransform,
|
||||||
@ -217,8 +239,17 @@ pub(crate) fn slider_on_pointer_down(
|
|||||||
if q_thumb.contains(trigger.target()) {
|
if q_thumb.contains(trigger.target()) {
|
||||||
// Thumb click, stop propagation to prevent track click.
|
// Thumb click, stop propagation to prevent track click.
|
||||||
trigger.propagate(false);
|
trigger.propagate(false);
|
||||||
} else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) =
|
} else if let Ok((
|
||||||
q_slider.get(trigger.target())
|
slider,
|
||||||
|
value,
|
||||||
|
range,
|
||||||
|
step,
|
||||||
|
precision,
|
||||||
|
node,
|
||||||
|
node_target,
|
||||||
|
transform,
|
||||||
|
disabled,
|
||||||
|
)) = q_slider.get(trigger.target())
|
||||||
{
|
{
|
||||||
// Track click
|
// Track click
|
||||||
trigger.propagate(false);
|
trigger.propagate(false);
|
||||||
@ -257,7 +288,9 @@ pub(crate) fn slider_on_pointer_down(
|
|||||||
value.0 + step.0
|
value.0 + step.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrackClick::Snap => click_val,
|
TrackClick::Snap => precision
|
||||||
|
.map(|prec| prec.round(click_val))
|
||||||
|
.unwrap_or(click_val),
|
||||||
});
|
});
|
||||||
|
|
||||||
if matches!(slider.on_change, Callback::Ignore) {
|
if matches!(slider.on_change, Callback::Ignore) {
|
||||||
@ -296,6 +329,7 @@ pub(crate) fn slider_on_drag(
|
|||||||
&ComputedNode,
|
&ComputedNode,
|
||||||
&CoreSlider,
|
&CoreSlider,
|
||||||
&SliderRange,
|
&SliderRange,
|
||||||
|
Option<&SliderPrecision>,
|
||||||
&UiGlobalTransform,
|
&UiGlobalTransform,
|
||||||
&mut CoreSliderDragState,
|
&mut CoreSliderDragState,
|
||||||
Has<InteractionDisabled>,
|
Has<InteractionDisabled>,
|
||||||
@ -305,7 +339,8 @@ pub(crate) fn slider_on_drag(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
ui_scale: Res<UiScale>,
|
ui_scale: Res<UiScale>,
|
||||||
) {
|
) {
|
||||||
if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target())
|
if let Ok((node, slider, range, precision, transform, drag, disabled)) =
|
||||||
|
q_slider.get_mut(trigger.target())
|
||||||
{
|
{
|
||||||
trigger.propagate(false);
|
trigger.propagate(false);
|
||||||
if drag.dragging && !disabled {
|
if drag.dragging && !disabled {
|
||||||
@ -320,17 +355,22 @@ pub(crate) fn slider_on_drag(
|
|||||||
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
|
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
|
||||||
let span = range.span();
|
let span = range.span();
|
||||||
let new_value = if span > 0. {
|
let new_value = if span > 0. {
|
||||||
range.clamp(drag.offset + (distance.x * span) / slider_width)
|
drag.offset + (distance.x * span) / slider_width
|
||||||
} else {
|
} else {
|
||||||
range.start() + span * 0.5
|
range.start() + span * 0.5
|
||||||
};
|
};
|
||||||
|
let rounded_value = range.clamp(
|
||||||
|
precision
|
||||||
|
.map(|prec| prec.round(new_value))
|
||||||
|
.unwrap_or(new_value),
|
||||||
|
);
|
||||||
|
|
||||||
if matches!(slider.on_change, Callback::Ignore) {
|
if matches!(slider.on_change, Callback::Ignore) {
|
||||||
commands
|
commands
|
||||||
.entity(trigger.target())
|
.entity(trigger.target())
|
||||||
.insert(SliderValue(new_value));
|
.insert(SliderValue(rounded_value));
|
||||||
} else {
|
} else {
|
||||||
commands.notify_with(&slider.on_change, new_value);
|
commands.notify_with(&slider.on_change, rounded_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,3 +531,24 @@ impl Plugin for CoreSliderPlugin {
|
|||||||
.add_observer(slider_on_set_value);
|
.add_observer(slider_on_set_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_slider_precision_rounding() {
|
||||||
|
// Test positive precision values (decimal places)
|
||||||
|
let precision_2dp = SliderPrecision(2);
|
||||||
|
assert_eq!(precision_2dp.round(1.234567), 1.23);
|
||||||
|
assert_eq!(precision_2dp.round(1.235), 1.24);
|
||||||
|
|
||||||
|
// Test zero precision (rounds to integers)
|
||||||
|
let precision_0dp = SliderPrecision(0);
|
||||||
|
assert_eq!(precision_0dp.round(1.4), 1.0);
|
||||||
|
|
||||||
|
// Test negative precision (rounds to tens, hundreds, etc.)
|
||||||
|
let precision_neg1 = SliderPrecision(-1);
|
||||||
|
assert_eq!(precision_neg1.round(14.0), 10.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,7 +33,7 @@ pub use core_scrollbar::{
|
|||||||
};
|
};
|
||||||
pub use core_slider::{
|
pub use core_slider::{
|
||||||
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
||||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
SliderPrecision, SliderRange, SliderStep, SliderValue, TrackClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A plugin group that registers the observers for all of the core widgets. If you don't want to
|
/// A plugin group that registers the observers for all of the core widgets. If you don't want to
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
//! This example shows off the various Bevy Feathers widgets.
|
//! This example shows off the various Bevy Feathers widgets.
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderStep},
|
core_widgets::{
|
||||||
|
Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep,
|
||||||
|
},
|
||||||
feathers::{
|
feathers::{
|
||||||
controls::{
|
controls::{
|
||||||
button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant,
|
button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant,
|
||||||
@ -259,7 +261,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
|
|||||||
value: 20.0,
|
value: 20.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
SliderStep(10.)
|
(SliderStep(10.), SliderPrecision(2)),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),],
|
),],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Headless Widgets
|
title: Headless Widgets
|
||||||
authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"]
|
authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"]
|
||||||
pull_requests: [19366, 19584, 19665, 19778, 19803, 20036]
|
pull_requests: [19366, 19584, 19665, 19778, 19803, 20032, 20036]
|
||||||
---
|
---
|
||||||
|
|
||||||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
||||||
|
Loading…
Reference in New Issue
Block a user