Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 69 additions & 8 deletions crates/bevy_core_widgets/src/core_slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -38,7 +39,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.
Expand Down Expand Up @@ -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.
#[derive(Component, Default)]
pub struct CoreSliderDragState {
Expand All @@ -204,6 +225,7 @@ pub(crate) fn slider_on_pointer_down(
&SliderValue,
&SliderRange,
&SliderStep,
Option<&SliderPrecision>,
&ComputedNode,
&ComputedNodeTarget,
&UiGlobalTransform,
Expand All @@ -217,8 +239,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);
Expand Down Expand Up @@ -257,7 +288,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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(|prec| prec.round(click_val))
.map(|prec| range.clamp(prec.round(click_val)))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already clamped, see previous lines.

.unwrap_or(click_val),
});

if matches!(slider.on_change, Callback::Ignore) {
Expand Down Expand Up @@ -296,6 +329,7 @@ pub(crate) fn slider_on_drag(
&ComputedNode,
&CoreSlider,
&SliderRange,
Option<&SliderPrecision>,
&UiGlobalTransform,
&mut CoreSliderDragState,
Has<InteractionDisabled>,
Expand All @@ -305,7 +339,8 @@ pub(crate) fn slider_on_drag(
mut commands: Commands,
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);
if drag.dragging && !disabled {
Expand All @@ -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 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 = range.clamp(
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);
}
}
}
Expand Down Expand Up @@ -491,3 +531,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);
}
}
2 changes: 1 addition & 1 deletion crates/bevy_core_widgets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions examples/ui/feathers.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -259,7 +261,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
value: 20.0,
..default()
},
SliderStep(10.)
(SliderStep(10.), SliderPrecision(2)),
),
]
),],
Expand Down
2 changes: 1 addition & 1 deletion release-content/release-notes/headless-widgets.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down