Core slider (#19584)
# Objective This is part of the "core widgets" effort: #19236. ## Solution This PR adds the "core slider" widget to the collection. ## Testing Tested using examples `core_widgets` and `core_widgets_observers`. --------- Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
This commit is contained in:
parent
f47b1c00ee
commit
30aa36eaf4
@ -15,8 +15,14 @@ bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.16.0-dev" }
|
||||
bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.16.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
|
||||
bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev", features = [
|
||||
"bevy_ui_picking_backend",
|
||||
] }
|
||||
|
||||
# other
|
||||
accesskit = "0.19"
|
||||
|
509
crates/bevy_core_widgets/src/core_slider.rs
Normal file
509
crates/bevy_core_widgets/src/core_slider.rs
Normal file
@ -0,0 +1,509 @@
|
||||
use core::ops::RangeInclusive;
|
||||
|
||||
use accesskit::{Orientation, Role};
|
||||
use bevy_a11y::AccessibilityNode;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::event::Event;
|
||||
use bevy_ecs::hierarchy::{ChildOf, Children};
|
||||
use bevy_ecs::lifecycle::Insert;
|
||||
use bevy_ecs::query::Has;
|
||||
use bevy_ecs::system::{In, Res, ResMut};
|
||||
use bevy_ecs::world::DeferredWorld;
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
observer::On,
|
||||
query::With,
|
||||
system::{Commands, Query, SystemId},
|
||||
};
|
||||
use bevy_input::keyboard::{KeyCode, KeyboardInput};
|
||||
use bevy_input::ButtonState;
|
||||
use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
|
||||
use bevy_log::warn_once;
|
||||
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
|
||||
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
|
||||
|
||||
/// Defines how the slider should behave when you click on the track (not the thumb).
|
||||
#[derive(Debug, Default)]
|
||||
pub enum TrackClick {
|
||||
/// Clicking on the track lets you drag to edit the value, just like clicking on the thumb.
|
||||
#[default]
|
||||
Drag,
|
||||
/// Clicking on the track increments or decrements the slider by [`SliderStep`].
|
||||
Step,
|
||||
/// Clicking on the track snaps the value to the clicked position.
|
||||
Snap,
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// The presence of the `on_change` property controls whether the slider uses internal or external
|
||||
/// state management. If the `on_change` property is `None`, then the slider updates its own state
|
||||
/// automatically. Otherwise, the `on_change` property contains the id of a one-shot system which is
|
||||
/// passed the new slider value. In this case, the slider value is not modified, it is the
|
||||
/// responsibility of the callback to trigger whatever data-binding mechanism is used to update the
|
||||
/// slider's value.
|
||||
///
|
||||
/// Typically a slider will contain entities representing the "track" and "thumb" elements. The core
|
||||
/// slider makes no assumptions about the hierarchical structure of these elements, but expects that
|
||||
/// the thumb will be marked with a [`CoreSliderThumb`] component.
|
||||
///
|
||||
/// The core slider does not modify the visible position of the thumb: that is the responsibility of
|
||||
/// the stylist. This can be done either in percent or pixel units as desired. To prevent overhang
|
||||
/// at the ends of the slider, the positioning should take into account the thumb width, by reducing
|
||||
/// the amount of travel. So for example, in a slider 100px wide, with a thumb that is 10px, the
|
||||
/// amount of travel is 90px. The core slider's calculations for clicking and dragging assume this
|
||||
/// is the case, and will reduce the travel by the measured size of the thumb entity, which allows
|
||||
/// the movement of the thumb to be perfectly synchronized with the movement of the mouse.
|
||||
///
|
||||
/// In cases where overhang is desired for artistic reasons, the thumb may have additional
|
||||
/// decorative child elements, absolutely positioned, which don't affect the size measurement.
|
||||
#[derive(Component, Debug, Default)]
|
||||
#[require(
|
||||
AccessibilityNode(accesskit::Node::new(Role::Slider)),
|
||||
CoreSliderDragState,
|
||||
SliderValue,
|
||||
SliderRange,
|
||||
SliderStep
|
||||
)]
|
||||
pub struct CoreSlider {
|
||||
/// Callback which is called when the slider is dragged or the value is changed via other user
|
||||
/// interaction. If this value is `None`, then the slider will self-update.
|
||||
pub on_change: Option<SystemId<In<f32>>>,
|
||||
/// Set the track-clicking behavior for this slider.
|
||||
pub track_click: TrackClick,
|
||||
// TODO: Think about whether we want a "vertical" option.
|
||||
}
|
||||
|
||||
/// Marker component that identifies which descendant element is the slider thumb.
|
||||
#[derive(Component, Debug, Default)]
|
||||
pub struct CoreSliderThumb;
|
||||
|
||||
/// A component which stores the current value of the slider.
|
||||
#[derive(Component, Debug, Default, PartialEq, Clone, Copy)]
|
||||
#[component(immutable)]
|
||||
pub struct SliderValue(pub f32);
|
||||
|
||||
/// A component which represents the allowed range of the slider value. Defaults to 0.0..=1.0.
|
||||
#[derive(Component, Debug, PartialEq, Clone, Copy)]
|
||||
#[component(immutable)]
|
||||
pub struct SliderRange {
|
||||
start: f32,
|
||||
end: f32,
|
||||
}
|
||||
|
||||
impl SliderRange {
|
||||
/// Creates a new slider range with the given start and end values.
|
||||
pub fn new(start: f32, end: f32) -> Self {
|
||||
if end < start {
|
||||
warn_once!(
|
||||
"Expected SliderRange::start ({}) <= SliderRange::end ({})",
|
||||
start,
|
||||
end
|
||||
);
|
||||
}
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
/// Creates a new slider range from a Rust range.
|
||||
pub fn from_range(range: RangeInclusive<f32>) -> Self {
|
||||
let (start, end) = range.into_inner();
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
/// Returns the minimum allowed value for this slider.
|
||||
pub fn start(&self) -> f32 {
|
||||
self.start
|
||||
}
|
||||
|
||||
/// Return a new instance of a `SliderRange` with a new start position.
|
||||
pub fn with_start(&self, start: f32) -> Self {
|
||||
Self::new(start, self.end)
|
||||
}
|
||||
|
||||
/// Returns the maximum allowed value for this slider.
|
||||
pub fn end(&self) -> f32 {
|
||||
self.end
|
||||
}
|
||||
|
||||
/// Return a new instance of a `SliderRange` with a new end position.
|
||||
pub fn with_end(&self, end: f32) -> Self {
|
||||
Self::new(self.start, end)
|
||||
}
|
||||
|
||||
/// Returns the full span of the range (max - min).
|
||||
pub fn span(&self) -> f32 {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
/// Returns the center value of the range.
|
||||
pub fn center(&self) -> f32 {
|
||||
(self.start + self.end) / 2.0
|
||||
}
|
||||
|
||||
/// Constrain a value between the minimum and maximum allowed values for this slider.
|
||||
pub fn clamp(&self, value: f32) -> f32 {
|
||||
value.clamp(self.start, self.end)
|
||||
}
|
||||
|
||||
/// Compute the position of the thumb on the slider, as a value between 0 and 1, taking
|
||||
/// into account the proportion of the value between the minimum and maximum limits.
|
||||
pub fn thumb_position(&self, value: f32) -> f32 {
|
||||
if self.end > self.start {
|
||||
(value - self.start) / (self.end - self.start)
|
||||
} else {
|
||||
0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SliderRange {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the amount by which to increment or decrement the slider value when using keyboard
|
||||
/// shorctuts. Defaults to 1.0.
|
||||
#[derive(Component, Debug, PartialEq, Clone)]
|
||||
#[component(immutable)]
|
||||
pub struct SliderStep(pub f32);
|
||||
|
||||
impl Default for SliderStep {
|
||||
fn default() -> Self {
|
||||
Self(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Component used to manage the state of a slider during dragging.
|
||||
#[derive(Component, Default)]
|
||||
pub struct CoreSliderDragState {
|
||||
/// Whether the slider is currently being dragged.
|
||||
pub dragging: bool,
|
||||
|
||||
/// The value of the slider when dragging started.
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_pointer_down(
|
||||
mut trigger: On<Pointer<Press>>,
|
||||
q_slider: Query<(
|
||||
&CoreSlider,
|
||||
&SliderValue,
|
||||
&SliderRange,
|
||||
&SliderStep,
|
||||
&ComputedNode,
|
||||
&ComputedNodeTarget,
|
||||
&UiGlobalTransform,
|
||||
Has<InteractionDisabled>,
|
||||
)>,
|
||||
q_thumb: Query<&ComputedNode, With<CoreSliderThumb>>,
|
||||
q_children: Query<&Children>,
|
||||
q_parents: Query<&ChildOf>,
|
||||
focus: Option<ResMut<InputFocus>>,
|
||||
focus_visible: Option<ResMut<InputFocusVisible>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if q_thumb.contains(trigger.target().unwrap()) {
|
||||
// Thumb click, stop propagation to prevent track click.
|
||||
trigger.propagate(false);
|
||||
|
||||
// Find the slider entity that's an ancestor of the thumb
|
||||
if let Some(slider_entity) = q_parents
|
||||
.iter_ancestors(trigger.target().unwrap())
|
||||
.find(|entity| q_slider.contains(*entity))
|
||||
{
|
||||
// Set focus to slider and hide focus ring
|
||||
if let Some(mut focus) = focus {
|
||||
focus.0 = Some(slider_entity);
|
||||
}
|
||||
if let Some(mut focus_visible) = focus_visible {
|
||||
focus_visible.0 = false;
|
||||
}
|
||||
}
|
||||
} else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) =
|
||||
q_slider.get(trigger.target().unwrap())
|
||||
{
|
||||
// Track click
|
||||
trigger.propagate(false);
|
||||
|
||||
// Set focus to slider and hide focus ring
|
||||
if let Some(mut focus) = focus {
|
||||
focus.0 = trigger.target();
|
||||
}
|
||||
if let Some(mut focus_visible) = focus_visible {
|
||||
focus_visible.0 = false;
|
||||
}
|
||||
|
||||
if disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find thumb size by searching descendants for the first entity with CoreSliderThumb
|
||||
let thumb_size = q_children
|
||||
.iter_descendants(trigger.target().unwrap())
|
||||
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Detect track click.
|
||||
let local_pos = transform.try_inverse().unwrap().transform_point2(
|
||||
trigger.event().pointer_location.position * node_target.scale_factor(),
|
||||
);
|
||||
let track_width = node.size().x - thumb_size;
|
||||
// Avoid division by zero
|
||||
let click_val = if track_width > 0. {
|
||||
local_pos.x * range.span() / track_width + range.center()
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
// Compute new value from click position
|
||||
let new_value = range.clamp(match slider.track_click {
|
||||
TrackClick::Drag => {
|
||||
return;
|
||||
}
|
||||
TrackClick::Step => {
|
||||
if click_val < value.0 {
|
||||
value.0 - step.0
|
||||
} else {
|
||||
value.0 + step.0
|
||||
}
|
||||
}
|
||||
TrackClick::Snap => click_val,
|
||||
});
|
||||
|
||||
if let Some(on_change) = slider.on_change {
|
||||
commands.run_system_with(on_change, new_value);
|
||||
} else {
|
||||
commands
|
||||
.entity(trigger.target().unwrap())
|
||||
.insert(SliderValue(new_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_drag_start(
|
||||
mut trigger: On<Pointer<DragStart>>,
|
||||
mut q_slider: Query<
|
||||
(
|
||||
&SliderValue,
|
||||
&mut CoreSliderDragState,
|
||||
Has<InteractionDisabled>,
|
||||
),
|
||||
With<CoreSlider>,
|
||||
>,
|
||||
) {
|
||||
if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target().unwrap()) {
|
||||
trigger.propagate(false);
|
||||
if !disabled {
|
||||
drag.dragging = true;
|
||||
drag.offset = value.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_drag(
|
||||
mut trigger: On<Pointer<Drag>>,
|
||||
mut q_slider: Query<(
|
||||
&ComputedNode,
|
||||
&CoreSlider,
|
||||
&SliderRange,
|
||||
&UiGlobalTransform,
|
||||
&mut CoreSliderDragState,
|
||||
Has<InteractionDisabled>,
|
||||
)>,
|
||||
q_thumb: Query<&ComputedNode, With<CoreSliderThumb>>,
|
||||
q_children: Query<&Children>,
|
||||
mut commands: Commands,
|
||||
ui_scale: Res<UiScale>,
|
||||
) {
|
||||
if let Ok((node, slider, range, transform, drag, disabled)) =
|
||||
q_slider.get_mut(trigger.target().unwrap())
|
||||
{
|
||||
trigger.propagate(false);
|
||||
if drag.dragging && !disabled {
|
||||
let mut distance = trigger.event().distance / ui_scale.0;
|
||||
distance.y *= -1.;
|
||||
let distance = transform.transform_vector2(distance);
|
||||
// Find thumb size by searching descendants for the first entity with CoreSliderThumb
|
||||
let thumb_size = q_children
|
||||
.iter_descendants(trigger.target().unwrap())
|
||||
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
|
||||
.unwrap_or(0.0);
|
||||
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)
|
||||
} else {
|
||||
range.start() + span * 0.5
|
||||
};
|
||||
|
||||
if let Some(on_change) = slider.on_change {
|
||||
commands.run_system_with(on_change, new_value);
|
||||
} else {
|
||||
commands
|
||||
.entity(trigger.target().unwrap())
|
||||
.insert(SliderValue(new_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_drag_end(
|
||||
mut trigger: On<Pointer<DragEnd>>,
|
||||
mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>,
|
||||
) {
|
||||
if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target().unwrap()) {
|
||||
trigger.propagate(false);
|
||||
if drag.dragging {
|
||||
drag.dragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_key_input(
|
||||
mut trigger: On<FocusedInput<KeyboardInput>>,
|
||||
q_slider: Query<(
|
||||
&CoreSlider,
|
||||
&SliderValue,
|
||||
&SliderRange,
|
||||
&SliderStep,
|
||||
Has<InteractionDisabled>,
|
||||
)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target().unwrap()) {
|
||||
let event = &trigger.event().input;
|
||||
if !disabled && event.state == ButtonState::Pressed {
|
||||
let new_value = match event.key_code {
|
||||
KeyCode::ArrowLeft => range.clamp(value.0 - step.0),
|
||||
KeyCode::ArrowRight => range.clamp(value.0 + step.0),
|
||||
KeyCode::Home => range.start(),
|
||||
KeyCode::End => range.end(),
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
trigger.propagate(false);
|
||||
if let Some(on_change) = slider.on_change {
|
||||
commands.run_system_with(on_change, new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_insert(trigger: On<Insert, CoreSlider>, mut world: DeferredWorld) {
|
||||
let mut entity = world.entity_mut(trigger.target().unwrap());
|
||||
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
|
||||
accessibility.set_orientation(Orientation::Horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_insert_value(trigger: On<Insert, SliderValue>, mut world: DeferredWorld) {
|
||||
let mut entity = world.entity_mut(trigger.target().unwrap());
|
||||
let value = entity.get::<SliderValue>().unwrap().0;
|
||||
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
|
||||
accessibility.set_numeric_value(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_insert_range(trigger: On<Insert, SliderRange>, mut world: DeferredWorld) {
|
||||
let mut entity = world.entity_mut(trigger.target().unwrap());
|
||||
let range = *entity.get::<SliderRange>().unwrap();
|
||||
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
|
||||
accessibility.set_min_numeric_value(range.start().into());
|
||||
accessibility.set_max_numeric_value(range.end().into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slider_on_insert_step(trigger: On<Insert, SliderStep>, mut world: DeferredWorld) {
|
||||
let mut entity = world.entity_mut(trigger.target().unwrap());
|
||||
let step = entity.get::<SliderStep>().unwrap().0;
|
||||
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
|
||||
accessibility.set_numeric_value_step(step.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Event which can be triggered on a slider to modify the value (using the `on_change` callback).
|
||||
/// This can be used to control the slider via gamepad buttons or other inputs. The value will be
|
||||
/// clamped when the event is processed.
|
||||
///
|
||||
/// # Example:
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::system::Commands;
|
||||
/// use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, SetSliderValue};
|
||||
///
|
||||
/// fn setup(mut commands: Commands) {
|
||||
/// // Create a slider
|
||||
/// let slider = commands.spawn((
|
||||
/// CoreSlider::default(),
|
||||
/// SliderValue(0.5),
|
||||
/// SliderRange::new(0.0, 1.0),
|
||||
/// )).id();
|
||||
///
|
||||
/// // Set to an absolute value
|
||||
/// commands.trigger_targets(SetSliderValue::Absolute(0.75), slider);
|
||||
///
|
||||
/// // Adjust relatively
|
||||
/// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider);
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Event)]
|
||||
pub enum SetSliderValue {
|
||||
/// Set the slider value to a specific value.
|
||||
Absolute(f32),
|
||||
/// Add a delta to the slider value.
|
||||
Relative(f32),
|
||||
/// Add a delta to the slider value, multiplied by the step size.
|
||||
RelativeStep(f32),
|
||||
}
|
||||
|
||||
fn slider_on_set_value(
|
||||
mut trigger: On<SetSliderValue>,
|
||||
q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Ok((slider, value, range, step)) = q_slider.get(trigger.target().unwrap()) {
|
||||
trigger.propagate(false);
|
||||
let new_value = match trigger.event() {
|
||||
SetSliderValue::Absolute(new_value) => range.clamp(*new_value),
|
||||
SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta),
|
||||
SetSliderValue::RelativeStep(delta) => {
|
||||
range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default())
|
||||
}
|
||||
};
|
||||
if let Some(on_change) = slider.on_change {
|
||||
commands.run_system_with(on_change, new_value);
|
||||
} else {
|
||||
commands
|
||||
.entity(trigger.target().unwrap())
|
||||
.insert(SliderValue(new_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin that adds the observers for the [`CoreSlider`] widget.
|
||||
pub struct CoreSliderPlugin;
|
||||
|
||||
impl Plugin for CoreSliderPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_observer(slider_on_pointer_down)
|
||||
.add_observer(slider_on_drag_start)
|
||||
.add_observer(slider_on_drag_end)
|
||||
.add_observer(slider_on_drag)
|
||||
.add_observer(slider_on_key_input)
|
||||
.add_observer(slider_on_insert)
|
||||
.add_observer(slider_on_insert_value)
|
||||
.add_observer(slider_on_insert_range)
|
||||
.add_observer(slider_on_insert_step)
|
||||
.add_observer(slider_on_set_value);
|
||||
}
|
||||
}
|
@ -10,11 +10,20 @@
|
||||
//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the
|
||||
//! user interface is showing a live view of dynamic data coming from deeper within the game engine.
|
||||
|
||||
// Note on naming: the `Core` prefix is used on components that would normally be internal to the
|
||||
// styled/opinionated widgets that use them. Components which are directly exposed to users above
|
||||
// the widget level, like `SliderValue`, should not have the `Core` prefix.
|
||||
|
||||
mod core_button;
|
||||
mod core_slider;
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
|
||||
pub use core_button::{CoreButton, CoreButtonPlugin};
|
||||
pub use core_slider::{
|
||||
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
||||
};
|
||||
|
||||
/// A plugin that registers the observers for all of the core widgets. If you don't want to
|
||||
/// use all of the widgets, you can import the individual widget plugins instead.
|
||||
@ -22,6 +31,6 @@ pub struct CoreWidgetsPlugin;
|
||||
|
||||
impl Plugin for CoreWidgetsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(CoreButtonPlugin);
|
||||
app.add_plugins((CoreButtonPlugin, CoreSliderPlugin));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
use bevy::{
|
||||
color::palettes::basic::*,
|
||||
core_widgets::{CoreButton, CoreWidgetsPlugin},
|
||||
core_widgets::{
|
||||
CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
||||
TrackClick,
|
||||
},
|
||||
ecs::system::SystemId,
|
||||
input_focus::{
|
||||
tab_navigation::{TabGroup, TabIndex},
|
||||
@ -19,10 +22,18 @@ fn main() {
|
||||
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
(update_button_style, update_button_style2, toggle_disabled),
|
||||
(
|
||||
update_widget_values,
|
||||
update_button_style,
|
||||
update_button_style2,
|
||||
update_slider_style.after(update_widget_values),
|
||||
update_slider_style2.after(update_widget_values),
|
||||
toggle_disabled,
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
@ -30,11 +41,32 @@ fn main() {
|
||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
|
||||
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
|
||||
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
|
||||
#[derive(Component)]
|
||||
struct DemoButton;
|
||||
|
||||
/// Marker which identifies sliders with a particular style.
|
||||
#[derive(Component, Default)]
|
||||
struct DemoSlider;
|
||||
|
||||
/// Marker which identifies the slider's thumb element.
|
||||
#[derive(Component, Default)]
|
||||
struct DemoSliderThumb;
|
||||
|
||||
/// A struct to hold the state of various widgets shown in the demo.
|
||||
///
|
||||
/// While it is possible to use the widget's own state components as the source of truth,
|
||||
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
|
||||
/// using some kind of data-binding. This example shows how to maintain an external source of
|
||||
/// truth for widget states.
|
||||
#[derive(Resource)]
|
||||
struct DemoWidgetStates {
|
||||
slider_value: f32,
|
||||
}
|
||||
|
||||
fn update_button_style(
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -157,16 +189,45 @@ fn set_button_style(
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the widget states based on the changing resource.
|
||||
fn update_widget_values(
|
||||
res: Res<DemoWidgetStates>,
|
||||
mut sliders: Query<Entity, With<DemoSlider>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if res.is_changed() {
|
||||
for slider_ent in sliders.iter_mut() {
|
||||
commands
|
||||
.entity(slider_ent)
|
||||
.insert(SliderValue(res.slider_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||
// System to print a value when the button is clicked.
|
||||
let on_click = commands.register_system(|| {
|
||||
info!("Button clicked!");
|
||||
});
|
||||
|
||||
// System to update a resource when the slider value changes. Note that we could have
|
||||
// updated the slider value directly, but we want to demonstrate externalizing the state.
|
||||
let on_change_value = commands.register_system(
|
||||
|value: In<f32>, mut widget_states: ResMut<DemoWidgetStates>| {
|
||||
widget_states.slider_value = *value;
|
||||
},
|
||||
);
|
||||
|
||||
// ui camera
|
||||
commands.spawn(Camera2d);
|
||||
commands.spawn(button(&assets, on_click));
|
||||
commands.spawn(demo_root(&assets, on_click, on_change_value));
|
||||
}
|
||||
|
||||
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
fn demo_root(
|
||||
asset_server: &AssetServer,
|
||||
on_click: SystemId,
|
||||
on_change_value: SystemId<In<f32>>,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
@ -175,57 +236,197 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
justify_content: JustifyContent::Center,
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.0),
|
||||
..default()
|
||||
},
|
||||
TabGroup::default(),
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
width: Val::Px(150.0),
|
||||
height: Val::Px(65.0),
|
||||
border: UiRect::all(Val::Px(5.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
DemoButton,
|
||||
CoreButton {
|
||||
on_click: Some(on_click),
|
||||
},
|
||||
Hovered::default(),
|
||||
TabIndex(0),
|
||||
BorderColor::all(Color::BLACK),
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(NORMAL_BUTTON),
|
||||
children![(
|
||||
Text::new("Button"),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 33.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
TextShadow::default(),
|
||||
)]
|
||||
),
|
||||
Text::new("Press 'D' to toggle button disabled state"),
|
||||
button(asset_server, on_click),
|
||||
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
||||
Text::new("Press 'D' to toggle widget disabled states"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Px(150.0),
|
||||
height: Val::Px(65.0),
|
||||
border: UiRect::all(Val::Px(5.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
DemoButton,
|
||||
CoreButton {
|
||||
on_click: Some(on_click),
|
||||
},
|
||||
Hovered::default(),
|
||||
TabIndex(0),
|
||||
BorderColor::all(Color::BLACK),
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(NORMAL_BUTTON),
|
||||
children![(
|
||||
Text::new("Button"),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 33.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
TextShadow::default(),
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a demo slider
|
||||
fn slider(min: f32, max: f32, value: f32, on_change: Option<SystemId<In<f32>>>) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Stretch,
|
||||
justify_items: JustifyItems::Center,
|
||||
column_gap: Val::Px(4.0),
|
||||
height: Val::Px(12.0),
|
||||
width: Val::Percent(30.0),
|
||||
..default()
|
||||
},
|
||||
Name::new("Slider"),
|
||||
Hovered::default(),
|
||||
DemoSlider,
|
||||
CoreSlider {
|
||||
on_change,
|
||||
track_click: TrackClick::Snap,
|
||||
},
|
||||
SliderValue(value),
|
||||
SliderRange::new(min, max),
|
||||
TabIndex(0),
|
||||
Children::spawn((
|
||||
// Slider background rail
|
||||
Spawn((
|
||||
Node {
|
||||
height: Val::Px(6.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
|
||||
BorderRadius::all(Val::Px(3.0)),
|
||||
)),
|
||||
// Invisible track to allow absolute placement of thumb entity. This is narrower than
|
||||
// the actual slider, which allows us to position the thumb entity using simple
|
||||
// percentages, without having to measure the actual width of the slider thumb.
|
||||
Spawn((
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(0.0),
|
||||
// Track is short by 12px to accommodate the thumb.
|
||||
right: Val::Px(12.0),
|
||||
top: Val::Px(0.0),
|
||||
bottom: Val::Px(0.0),
|
||||
..default()
|
||||
},
|
||||
children![(
|
||||
// Thumb
|
||||
DemoSliderThumb,
|
||||
CoreSliderThumb,
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
width: Val::Px(12.0),
|
||||
height: Val::Px(12.0),
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Percent(0.0), // This will be updated by the slider's value
|
||||
..default()
|
||||
},
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(SLIDER_THUMB),
|
||||
)],
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Update the visuals of the slider based on the slider state.
|
||||
fn update_slider_style(
|
||||
sliders: Query<
|
||||
(
|
||||
Entity,
|
||||
&SliderValue,
|
||||
&SliderRange,
|
||||
&Hovered,
|
||||
Has<InteractionDisabled>,
|
||||
),
|
||||
(
|
||||
Or<(
|
||||
Changed<SliderValue>,
|
||||
Changed<SliderRange>,
|
||||
Changed<Hovered>,
|
||||
Added<InteractionDisabled>,
|
||||
)>,
|
||||
With<DemoSlider>,
|
||||
),
|
||||
>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
for (slider_ent, value, range, hovered, disabled) in sliders.iter() {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0);
|
||||
thumb_bg.0 = thumb_color(disabled, hovered.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_slider_style2(
|
||||
sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||
) {
|
||||
removed_disabled.read().for_each(|entity| {
|
||||
if let Ok((slider_ent, hovered, disabled)) = sliders.get(entity) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_bg.0 = thumb_color(disabled, hovered.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn thumb_color(disabled: bool, hovered: bool) -> Color {
|
||||
match (disabled, hovered) {
|
||||
(true, _) => GRAY.into(),
|
||||
|
||||
(false, true) => SLIDER_THUMB.lighter(0.3),
|
||||
|
||||
_ => SLIDER_THUMB,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_disabled(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut interaction_query: Query<(Entity, Has<InteractionDisabled>), With<CoreButton>>,
|
||||
mut interaction_query: Query<
|
||||
(Entity, Has<InteractionDisabled>),
|
||||
Or<(With<CoreButton>, With<CoreSlider>)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::KeyD) {
|
||||
for (entity, disabled) in &mut interaction_query {
|
||||
// disabled.0 = !disabled.0;
|
||||
if disabled {
|
||||
info!("Button enabled");
|
||||
info!("Widgets enabled");
|
||||
commands.entity(entity).remove::<InteractionDisabled>();
|
||||
} else {
|
||||
info!("Button disabled");
|
||||
info!("Widgets disabled");
|
||||
commands.entity(entity).insert(InteractionDisabled);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
use bevy::{
|
||||
color::palettes::basic::*,
|
||||
core_widgets::{CoreButton, CoreWidgetsPlugin},
|
||||
core_widgets::{
|
||||
CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
||||
},
|
||||
ecs::system::SystemId,
|
||||
input_focus::{
|
||||
tab_navigation::{TabGroup, TabIndex},
|
||||
@ -19,25 +21,52 @@ fn main() {
|
||||
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
||||
.add_systems(Startup, setup)
|
||||
.add_observer(on_add_pressed)
|
||||
.add_observer(on_remove_pressed)
|
||||
.add_observer(on_add_disabled)
|
||||
.add_observer(on_remove_disabled)
|
||||
.add_observer(on_change_hover)
|
||||
.add_systems(Update, toggle_disabled)
|
||||
.add_observer(button_on_add_pressed)
|
||||
.add_observer(button_on_remove_pressed)
|
||||
.add_observer(button_on_add_disabled)
|
||||
.add_observer(button_on_remove_disabled)
|
||||
.add_observer(button_on_change_hover)
|
||||
.add_observer(slider_on_add_disabled)
|
||||
.add_observer(slider_on_remove_disabled)
|
||||
.add_observer(slider_on_change_hover)
|
||||
.add_observer(slider_on_change_value)
|
||||
.add_observer(slider_on_change_range)
|
||||
.add_systems(Update, (update_widget_values, toggle_disabled))
|
||||
.run();
|
||||
}
|
||||
|
||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
|
||||
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
|
||||
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
|
||||
#[derive(Component)]
|
||||
struct DemoButton;
|
||||
|
||||
fn on_add_pressed(
|
||||
/// Marker which identifies sliders with a particular style.
|
||||
#[derive(Component, Default)]
|
||||
struct DemoSlider;
|
||||
|
||||
/// Marker which identifies the slider's thumb element.
|
||||
#[derive(Component, Default)]
|
||||
struct DemoSliderThumb;
|
||||
|
||||
/// A struct to hold the state of various widgets shown in the demo.
|
||||
///
|
||||
/// While it is possible to use the widget's own state components as the source of truth,
|
||||
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
|
||||
/// using some kind of data-binding. This example shows how to maintain an external source of
|
||||
/// truth for widget states.
|
||||
#[derive(Resource)]
|
||||
struct DemoWidgetStates {
|
||||
slider_value: f32,
|
||||
}
|
||||
|
||||
fn button_on_add_pressed(
|
||||
trigger: On<Add, Pressed>,
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -66,7 +95,7 @@ fn on_add_pressed(
|
||||
}
|
||||
}
|
||||
|
||||
fn on_remove_pressed(
|
||||
fn button_on_remove_pressed(
|
||||
trigger: On<Remove, Pressed>,
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -95,7 +124,7 @@ fn on_remove_pressed(
|
||||
}
|
||||
}
|
||||
|
||||
fn on_add_disabled(
|
||||
fn button_on_add_disabled(
|
||||
trigger: On<Add, InteractionDisabled>,
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -124,7 +153,7 @@ fn on_add_disabled(
|
||||
}
|
||||
}
|
||||
|
||||
fn on_remove_disabled(
|
||||
fn button_on_remove_disabled(
|
||||
trigger: On<Remove, InteractionDisabled>,
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -153,7 +182,7 @@ fn on_remove_disabled(
|
||||
}
|
||||
}
|
||||
|
||||
fn on_change_hover(
|
||||
fn button_on_change_hover(
|
||||
trigger: On<Insert, Hovered>,
|
||||
mut buttons: Query<
|
||||
(
|
||||
@ -227,16 +256,140 @@ fn set_button_style(
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_add_disabled(
|
||||
trigger: On<Add, InteractionDisabled>,
|
||||
sliders: Query<(Entity, &Hovered), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
if let Ok((slider_ent, hovered)) = sliders.get(trigger.target().unwrap()) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_bg.0 = thumb_color(true, hovered.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_remove_disabled(
|
||||
trigger: On<Remove, InteractionDisabled>,
|
||||
sliders: Query<(Entity, &Hovered), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
if let Ok((slider_ent, hovered)) = sliders.get(trigger.target().unwrap()) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_bg.0 = thumb_color(false, hovered.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_change_hover(
|
||||
trigger: On<Insert, Hovered>,
|
||||
sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
if let Ok((slider_ent, hovered, disabled)) = sliders.get(trigger.target().unwrap()) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_bg.0 = thumb_color(disabled, hovered.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_change_value(
|
||||
trigger: On<Insert, SliderValue>,
|
||||
sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
if let Ok((slider_ent, value, range)) = sliders.get(trigger.target().unwrap()) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_on_change_range(
|
||||
trigger: On<Insert, SliderRange>,
|
||||
sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
|
||||
children: Query<&Children>,
|
||||
mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
|
||||
) {
|
||||
if let Ok((slider_ent, value, range)) = sliders.get(trigger.target().unwrap()) {
|
||||
for child in children.iter_descendants(slider_ent) {
|
||||
if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child) {
|
||||
if is_thumb {
|
||||
thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn thumb_color(disabled: bool, hovered: bool) -> Color {
|
||||
match (disabled, hovered) {
|
||||
(true, _) => GRAY.into(),
|
||||
|
||||
(false, true) => SLIDER_THUMB.lighter(0.3),
|
||||
|
||||
_ => SLIDER_THUMB,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the widget states based on the changing resource.
|
||||
fn update_widget_values(
|
||||
res: Res<DemoWidgetStates>,
|
||||
mut sliders: Query<Entity, With<DemoSlider>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if res.is_changed() {
|
||||
for slider_ent in sliders.iter_mut() {
|
||||
commands
|
||||
.entity(slider_ent)
|
||||
.insert(SliderValue(res.slider_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||
// System to print a value when the button is clicked.
|
||||
let on_click = commands.register_system(|| {
|
||||
info!("Button clicked!");
|
||||
});
|
||||
|
||||
// System to update a resource when the slider value changes. Note that we could have
|
||||
// updated the slider value directly, but we want to demonstrate externalizing the state.
|
||||
let on_change_value = commands.register_system(
|
||||
|value: In<f32>, mut widget_states: ResMut<DemoWidgetStates>| {
|
||||
widget_states.slider_value = *value;
|
||||
},
|
||||
);
|
||||
|
||||
// ui camera
|
||||
commands.spawn(Camera2d);
|
||||
commands.spawn(button(&assets, on_click));
|
||||
commands.spawn(demo_root(&assets, on_click, on_change_value));
|
||||
}
|
||||
|
||||
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
fn demo_root(
|
||||
asset_server: &AssetServer,
|
||||
on_click: SystemId,
|
||||
on_change_value: SystemId<In<f32>>,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
@ -245,57 +398,133 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
justify_content: JustifyContent::Center,
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.0),
|
||||
..default()
|
||||
},
|
||||
TabGroup::default(),
|
||||
children![
|
||||
(
|
||||
button(asset_server, on_click),
|
||||
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
||||
Text::new("Press 'D' to toggle widget disabled states"),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Px(150.0),
|
||||
height: Val::Px(65.0),
|
||||
border: UiRect::all(Val::Px(5.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
DemoButton,
|
||||
CoreButton {
|
||||
on_click: Some(on_click),
|
||||
},
|
||||
Hovered::default(),
|
||||
TabIndex(0),
|
||||
BorderColor::all(Color::BLACK),
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(NORMAL_BUTTON),
|
||||
children![(
|
||||
Text::new("Button"),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 33.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
TextShadow::default(),
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a demo slider
|
||||
fn slider(min: f32, max: f32, value: f32, on_change: Option<SystemId<In<f32>>>) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Stretch,
|
||||
justify_items: JustifyItems::Center,
|
||||
column_gap: Val::Px(4.0),
|
||||
height: Val::Px(12.0),
|
||||
width: Val::Percent(30.0),
|
||||
..default()
|
||||
},
|
||||
Name::new("Slider"),
|
||||
Hovered::default(),
|
||||
DemoSlider,
|
||||
CoreSlider {
|
||||
on_change,
|
||||
..default()
|
||||
},
|
||||
SliderValue(value),
|
||||
SliderRange::new(min, max),
|
||||
TabIndex(0),
|
||||
Children::spawn((
|
||||
// Slider background rail
|
||||
Spawn((
|
||||
Node {
|
||||
width: Val::Px(150.0),
|
||||
height: Val::Px(65.0),
|
||||
border: UiRect::all(Val::Px(5.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
height: Val::Px(6.0),
|
||||
..default()
|
||||
},
|
||||
DemoButton,
|
||||
CoreButton {
|
||||
on_click: Some(on_click),
|
||||
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
|
||||
BorderRadius::all(Val::Px(3.0)),
|
||||
)),
|
||||
// Invisible track to allow absolute placement of thumb entity. This is narrower than
|
||||
// the actual slider, which allows us to position the thumb entity using simple
|
||||
// percentages, without having to measure the actual width of the slider thumb.
|
||||
Spawn((
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(0.0),
|
||||
// Track is short by 12px to accommodate the thumb.
|
||||
right: Val::Px(12.0),
|
||||
top: Val::Px(0.0),
|
||||
bottom: Val::Px(0.0),
|
||||
..default()
|
||||
},
|
||||
Hovered::default(),
|
||||
TabIndex(0),
|
||||
BorderColor::all(Color::BLACK),
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(NORMAL_BUTTON),
|
||||
children![(
|
||||
Text::new("Button"),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 33.0,
|
||||
// Thumb
|
||||
DemoSliderThumb,
|
||||
CoreSliderThumb,
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
width: Val::Px(12.0),
|
||||
height: Val::Px(12.0),
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Percent(0.0), // This will be updated by the slider's value
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
TextShadow::default(),
|
||||
)]
|
||||
),
|
||||
Text::new("Press 'D' to toggle button disabled state"),
|
||||
],
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(SLIDER_THUMB),
|
||||
)],
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_disabled(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut interaction_query: Query<(Entity, Has<InteractionDisabled>), With<CoreButton>>,
|
||||
mut interaction_query: Query<
|
||||
(Entity, Has<InteractionDisabled>),
|
||||
Or<(With<CoreButton>, With<CoreSlider>)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::KeyD) {
|
||||
for (entity, disabled) in &mut interaction_query {
|
||||
// disabled.0 = !disabled.0;
|
||||
if disabled {
|
||||
info!("Button enabled");
|
||||
info!("Widgets enabled");
|
||||
commands.entity(entity).remove::<InteractionDisabled>();
|
||||
} else {
|
||||
info!("Button disabled");
|
||||
info!("Widgets disabled");
|
||||
commands.entity(entity).insert(InteractionDisabled);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Headless Widgets
|
||||
authors: ["@viridia"]
|
||||
pull_requests: [19366]
|
||||
pull_requests: [19366, 19584]
|
||||
---
|
||||
|
||||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
||||
@ -33,7 +33,7 @@ The `bevy_core_widgets` crate provides implementations of unstyled widgets, such
|
||||
sliders, checkboxes and radio buttons.
|
||||
|
||||
- `CoreButton` is a push button. It emits an activation event when clicked.
|
||||
- (More to be added in subsequent PRs)
|
||||
- `CoreSlider` is a standard slider, which lets you edit an `f32` value in a given range.
|
||||
|
||||
## Widget Interaction States
|
||||
|
||||
@ -63,13 +63,13 @@ is using Bevy observers. This approach is useful in cases where you want the wid
|
||||
to bubble up the hierarchy.
|
||||
|
||||
However, in UI work it's often desirable to connect widget interactions in ways that cut across the
|
||||
hierarchy. For these kinds of situations, the core widgets offer an an alternate approach: one-shot
|
||||
hierarchy. For these kinds of situations, the core widgets offer a different approach: one-shot
|
||||
systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can
|
||||
then be passed as a parameter to the widget when it is constructed, so when the button subsequently
|
||||
gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can
|
||||
inject any additional parameters it needs to update the Bevy world in response to the interaction.
|
||||
|
||||
Most of the core widgets use "external state management" - something that is referred to in the
|
||||
Most of the core widgets support "external state management" - something that is referred to in the
|
||||
React.js world as "controlled" widgets. This means that for widgets that edit a parameter value
|
||||
(such as checkboxes and sliders), the widget doesn't automatically update its own internal value,
|
||||
but only sends a notification to the app telling it that the value needs to change. It's the
|
||||
@ -83,6 +83,10 @@ interacting with that widget. Externalizing the state avoids the need for two-wa
|
||||
instead allows simpler one-way data binding that aligns well with the traditional "Model / View /
|
||||
Controller" (MVC) design pattern.
|
||||
|
||||
That being said, the choice of internal or external state management is up to you: if the widget
|
||||
has an `on_change` callback that is not `None`, then the callback is used. If the callback
|
||||
is `None`, however, the widget will update its own state. (This is similar to how React.js does it.)
|
||||
|
||||
There are two exceptions to this rule about external state management. First, widgets which don't
|
||||
edit a value, but which merely trigger an event (such as buttons), don't fall under this rule.
|
||||
Second, widgets which have complex states that are too large and heavyweight to fit within a
|
||||
|
Loading…
Reference in New Issue
Block a user