From b97c565d0bf4c74f38534305f2db22703efd7f6f Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 09:35:42 -0700 Subject: [PATCH 1/6] SliderPrecision --- crates/bevy_core_widgets/src/core_slider.rs | 69 +++++++++++++++++++-- crates/bevy_core_widgets/src/lib.rs | 2 +- examples/ui/feathers.rs | 6 +- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 521e6fc1d39ff..8601197973775 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -19,6 +19,7 @@ use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; use bevy_input_focus::FocusedInput; use bevy_log::warn_once; +use bevy_math::ops; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; @@ -187,6 +188,24 @@ 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 numberic 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. #[derive(Component, Default)] pub struct CoreSliderDragState { @@ -204,6 +223,7 @@ pub(crate) fn slider_on_pointer_down( &SliderValue, &SliderRange, &SliderStep, + Option<&SliderPrecision>, &ComputedNode, &ComputedNodeTarget, &UiGlobalTransform, @@ -217,8 +237,17 @@ pub(crate) fn slider_on_pointer_down( if q_thumb.contains(trigger.target()) { // Thumb click, stop propagation to prevent track click. trigger.propagate(false); - } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = - q_slider.get(trigger.target()) + } else if let Ok(( + slider, + value, + range, + step, + precision, + node, + node_target, + transform, + disabled, + )) = q_slider.get(trigger.target()) { // Track click trigger.propagate(false); @@ -257,7 +286,9 @@ pub(crate) fn slider_on_pointer_down( 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) { @@ -296,6 +327,7 @@ pub(crate) fn slider_on_drag( &ComputedNode, &CoreSlider, &SliderRange, + Option<&SliderPrecision>, &UiGlobalTransform, &mut CoreSliderDragState, Has, @@ -305,7 +337,8 @@ pub(crate) fn slider_on_drag( mut commands: Commands, ui_scale: Res, ) { - 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); if drag.dragging && !disabled { @@ -324,13 +357,16 @@ pub(crate) fn slider_on_drag( } else { range.start() + span * 0.5 }; + let rounded_value = precision + .map(|prec| prec.round(new_value)) + .unwrap_or(new_value); if matches!(slider.on_change, Callback::Ignore) { commands .entity(trigger.target()) - .insert(SliderValue(new_value)); + .insert(SliderValue(rounded_value)); } else { - commands.notify_with(&slider.on_change, new_value); + commands.notify_with(&slider.on_change, rounded_value); } } } @@ -491,3 +527,24 @@ impl Plugin for CoreSliderPlugin { .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); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 3fc13c5c0ee4f..eb05a18ba669c 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -33,7 +33,7 @@ pub use core_scrollbar::{ }; pub use core_slider::{ 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 diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index da8b1faf27044..5b580483b6775 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -1,7 +1,9 @@ //! This example shows off the various Bevy Feathers widgets. use bevy::{ - core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderStep}, + core_widgets::{ + Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, + }, feathers::{ controls::{ button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant, @@ -259,7 +261,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { value: 20.0, ..default() }, - SliderStep(10.) + (SliderStep(10.), SliderPrecision(2)), ), ] ),], From 98627b3d3428aa384f2a285c1d16a073140fa827 Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 09:38:39 -0700 Subject: [PATCH 2/6] Add to release note. --- release-content/release-notes/headless-widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index 5b3ff3dc1723e..68f3978fbc6ed 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -1,7 +1,7 @@ --- title: Headless Widgets 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 From 76dc3d4d686951792669ca0465a2487e0bdbb30f Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 09:52:04 -0700 Subject: [PATCH 3/6] Review feedback. --- crates/bevy_core_widgets/Cargo.toml | 1 + crates/bevy_core_widgets/src/core_slider.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index 186b2ec820d23..bcfdf5d34e7c7 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -18,6 +18,7 @@ bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ "bevy_ui_picking_backend", ] } diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 8601197973775..0ed1e4ac63ed2 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -21,6 +21,7 @@ use bevy_input_focus::FocusedInput; use bevy_log::warn_once; use bevy_math::ops; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_reflect::Reflect; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; use crate::{Callback, Notify}; @@ -178,7 +179,7 @@ impl Default for SliderRange { /// Defines the amount by which to increment or decrement the slider value when using keyboard /// shortcuts. Defaults to 1.0. -#[derive(Component, Debug, PartialEq, Clone)] +#[derive(Component, Debug, PartialEq, Clone, Reflect)] #[component(immutable)] pub struct SliderStep(pub f32); @@ -188,15 +189,16 @@ 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 numberic input field. +/// A component which controls the rounding of the slider value during dragging. /// -/// 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 +/// 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)] +#[derive(Component, Debug, Default, Clone, Copy, Reflect)] pub struct SliderPrecision(pub i32); impl SliderPrecision { From 9d6069b3fe5763f37638d0ec3ce0db4a04edae49 Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 09:53:23 -0700 Subject: [PATCH 4/6] Doc update. --- crates/bevy_core_widgets/src/core_slider.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 0ed1e4ac63ed2..24eeaffe331ae 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -40,7 +40,8 @@ pub enum TrackClick { /// 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 -/// 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 /// can be useful in a console environment for controlling the value gamepad inputs. From 88c0c08fb0cff65d564d77bb8810568805557535 Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 10:27:17 -0700 Subject: [PATCH 5/6] Revert reflection for now. --- crates/bevy_core_widgets/Cargo.toml | 1 - crates/bevy_core_widgets/src/core_slider.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index bcfdf5d34e7c7..186b2ec820d23 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -18,7 +18,6 @@ bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ "bevy_ui_picking_backend", ] } diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 24eeaffe331ae..ba38f0de26b32 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -21,7 +21,6 @@ use bevy_input_focus::FocusedInput; use bevy_log::warn_once; use bevy_math::ops; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; -use bevy_reflect::Reflect; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; use crate::{Callback, Notify}; @@ -180,7 +179,7 @@ impl Default for SliderRange { /// Defines the amount by which to increment or decrement the slider value when using keyboard /// shortcuts. Defaults to 1.0. -#[derive(Component, Debug, PartialEq, Clone, Reflect)] +#[derive(Component, Debug, PartialEq, Clone)] #[component(immutable)] pub struct SliderStep(pub f32); @@ -199,7 +198,7 @@ impl Default for SliderStep { /// 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, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy)] pub struct SliderPrecision(pub i32); impl SliderPrecision { From 768051f84f2e86ea8fd5991b4940923280a8e4b5 Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 9 Jul 2025 08:04:57 -0700 Subject: [PATCH 6/6] Clamp after rounding. --- crates/bevy_core_widgets/src/core_slider.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index ba38f0de26b32..07efc7e80074d 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -355,13 +355,15 @@ pub(crate) fn slider_on_drag( let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); let span = range.span(); let new_value = if span > 0. { - range.clamp(drag.offset + (distance.x * span) / slider_width) + drag.offset + (distance.x * span) / slider_width } else { range.start() + span * 0.5 }; - let rounded_value = precision - .map(|prec| prec.round(new_value)) - .unwrap_or(new_value); + let rounded_value = range.clamp( + precision + .map(|prec| prec.round(new_value)) + .unwrap_or(new_value), + ); if matches!(slider.on_change, Callback::Ignore) { commands