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_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
|
||||||
bevy_input = { path = "../bevy_input", 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_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_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
|
# other
|
||||||
accesskit = "0.19"
|
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
|
//! 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.
|
//! 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_button;
|
||||||
|
mod core_slider;
|
||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
|
|
||||||
pub use core_button::{CoreButton, CoreButtonPlugin};
|
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
|
/// 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.
|
/// use all of the widgets, you can import the individual widget plugins instead.
|
||||||
@ -22,6 +31,6 @@ pub struct CoreWidgetsPlugin;
|
|||||||
|
|
||||||
impl Plugin for CoreWidgetsPlugin {
|
impl Plugin for CoreWidgetsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins(CoreButtonPlugin);
|
app.add_plugins((CoreButtonPlugin, CoreSliderPlugin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
color::palettes::basic::*,
|
color::palettes::basic::*,
|
||||||
core_widgets::{CoreButton, CoreWidgetsPlugin},
|
core_widgets::{
|
||||||
|
CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
||||||
|
TrackClick,
|
||||||
|
},
|
||||||
ecs::system::SystemId,
|
ecs::system::SystemId,
|
||||||
input_focus::{
|
input_focus::{
|
||||||
tab_navigation::{TabGroup, TabIndex},
|
tab_navigation::{TabGroup, TabIndex},
|
||||||
@ -19,10 +22,18 @@ fn main() {
|
|||||||
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
||||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
.insert_resource(WinitSettings::desktop_app())
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
|
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
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();
|
.run();
|
||||||
}
|
}
|
||||||
@ -30,11 +41,32 @@ fn main() {
|
|||||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
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 HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
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".
|
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct DemoButton;
|
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(
|
fn update_button_style(
|
||||||
mut buttons: Query<
|
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>) {
|
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||||
|
// System to print a value when the button is clicked.
|
||||||
let on_click = commands.register_system(|| {
|
let on_click = commands.register_system(|| {
|
||||||
info!("Button clicked!");
|
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
|
// ui camera
|
||||||
commands.spawn(Camera2d);
|
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 {
|
Node {
|
||||||
width: Val::Percent(100.0),
|
width: Val::Percent(100.0),
|
||||||
@ -175,57 +236,197 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
|||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
display: Display::Flex,
|
display: Display::Flex,
|
||||||
flex_direction: FlexDirection::Column,
|
flex_direction: FlexDirection::Column,
|
||||||
|
row_gap: Val::Px(10.0),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TabGroup::default(),
|
TabGroup::default(),
|
||||||
children![
|
children![
|
||||||
(
|
button(asset_server, on_click),
|
||||||
Node {
|
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
||||||
width: Val::Px(150.0),
|
Text::new("Press 'D' to toggle widget disabled states"),
|
||||||
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"),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
fn toggle_disabled(
|
||||||
input: Res<ButtonInput<KeyCode>>,
|
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,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
if input.just_pressed(KeyCode::KeyD) {
|
if input.just_pressed(KeyCode::KeyD) {
|
||||||
for (entity, disabled) in &mut interaction_query {
|
for (entity, disabled) in &mut interaction_query {
|
||||||
// disabled.0 = !disabled.0;
|
|
||||||
if disabled {
|
if disabled {
|
||||||
info!("Button enabled");
|
info!("Widgets enabled");
|
||||||
commands.entity(entity).remove::<InteractionDisabled>();
|
commands.entity(entity).remove::<InteractionDisabled>();
|
||||||
} else {
|
} else {
|
||||||
info!("Button disabled");
|
info!("Widgets disabled");
|
||||||
commands.entity(entity).insert(InteractionDisabled);
|
commands.entity(entity).insert(InteractionDisabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
color::palettes::basic::*,
|
color::palettes::basic::*,
|
||||||
core_widgets::{CoreButton, CoreWidgetsPlugin},
|
core_widgets::{
|
||||||
|
CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
||||||
|
},
|
||||||
ecs::system::SystemId,
|
ecs::system::SystemId,
|
||||||
input_focus::{
|
input_focus::{
|
||||||
tab_navigation::{TabGroup, TabIndex},
|
tab_navigation::{TabGroup, TabIndex},
|
||||||
@ -19,25 +21,52 @@ fn main() {
|
|||||||
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
.add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin))
|
||||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
.insert_resource(WinitSettings::desktop_app())
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
|
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_observer(on_add_pressed)
|
.add_observer(button_on_add_pressed)
|
||||||
.add_observer(on_remove_pressed)
|
.add_observer(button_on_remove_pressed)
|
||||||
.add_observer(on_add_disabled)
|
.add_observer(button_on_add_disabled)
|
||||||
.add_observer(on_remove_disabled)
|
.add_observer(button_on_remove_disabled)
|
||||||
.add_observer(on_change_hover)
|
.add_observer(button_on_change_hover)
|
||||||
.add_systems(Update, toggle_disabled)
|
.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();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
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 HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
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".
|
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct DemoButton;
|
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>,
|
trigger: On<Add, Pressed>,
|
||||||
mut buttons: Query<
|
mut buttons: Query<
|
||||||
(
|
(
|
||||||
@ -66,7 +95,7 @@ fn on_add_pressed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_remove_pressed(
|
fn button_on_remove_pressed(
|
||||||
trigger: On<Remove, Pressed>,
|
trigger: On<Remove, Pressed>,
|
||||||
mut buttons: Query<
|
mut buttons: Query<
|
||||||
(
|
(
|
||||||
@ -95,7 +124,7 @@ fn on_remove_pressed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_add_disabled(
|
fn button_on_add_disabled(
|
||||||
trigger: On<Add, InteractionDisabled>,
|
trigger: On<Add, InteractionDisabled>,
|
||||||
mut buttons: Query<
|
mut buttons: Query<
|
||||||
(
|
(
|
||||||
@ -124,7 +153,7 @@ fn on_add_disabled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_remove_disabled(
|
fn button_on_remove_disabled(
|
||||||
trigger: On<Remove, InteractionDisabled>,
|
trigger: On<Remove, InteractionDisabled>,
|
||||||
mut buttons: Query<
|
mut buttons: Query<
|
||||||
(
|
(
|
||||||
@ -153,7 +182,7 @@ fn on_remove_disabled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_change_hover(
|
fn button_on_change_hover(
|
||||||
trigger: On<Insert, Hovered>,
|
trigger: On<Insert, Hovered>,
|
||||||
mut buttons: Query<
|
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>) {
|
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||||
|
// System to print a value when the button is clicked.
|
||||||
let on_click = commands.register_system(|| {
|
let on_click = commands.register_system(|| {
|
||||||
info!("Button clicked!");
|
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
|
// ui camera
|
||||||
commands.spawn(Camera2d);
|
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 {
|
Node {
|
||||||
width: Val::Percent(100.0),
|
width: Val::Percent(100.0),
|
||||||
@ -245,57 +398,133 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
|||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
display: Display::Flex,
|
display: Display::Flex,
|
||||||
flex_direction: FlexDirection::Column,
|
flex_direction: FlexDirection::Column,
|
||||||
|
row_gap: Val::Px(10.0),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TabGroup::default(),
|
TabGroup::default(),
|
||||||
children![
|
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 {
|
Node {
|
||||||
width: Val::Px(150.0),
|
height: Val::Px(6.0),
|
||||||
height: Val::Px(65.0),
|
|
||||||
border: UiRect::all(Val::Px(5.0)),
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
DemoButton,
|
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
|
||||||
CoreButton {
|
BorderRadius::all(Val::Px(3.0)),
|
||||||
on_click: Some(on_click),
|
)),
|
||||||
|
// 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![(
|
children![(
|
||||||
Text::new("Button"),
|
// Thumb
|
||||||
TextFont {
|
DemoSliderThumb,
|
||||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
CoreSliderThumb,
|
||||||
font_size: 33.0,
|
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()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
BorderRadius::MAX,
|
||||||
TextShadow::default(),
|
BackgroundColor(SLIDER_THUMB),
|
||||||
)]
|
)],
|
||||||
),
|
)),
|
||||||
Text::new("Press 'D' to toggle button disabled state"),
|
)),
|
||||||
],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_disabled(
|
fn toggle_disabled(
|
||||||
input: Res<ButtonInput<KeyCode>>,
|
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,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
if input.just_pressed(KeyCode::KeyD) {
|
if input.just_pressed(KeyCode::KeyD) {
|
||||||
for (entity, disabled) in &mut interaction_query {
|
for (entity, disabled) in &mut interaction_query {
|
||||||
// disabled.0 = !disabled.0;
|
|
||||||
if disabled {
|
if disabled {
|
||||||
info!("Button enabled");
|
info!("Widgets enabled");
|
||||||
commands.entity(entity).remove::<InteractionDisabled>();
|
commands.entity(entity).remove::<InteractionDisabled>();
|
||||||
} else {
|
} else {
|
||||||
info!("Button disabled");
|
info!("Widgets disabled");
|
||||||
commands.entity(entity).insert(InteractionDisabled);
|
commands.entity(entity).insert(InteractionDisabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Headless Widgets
|
title: Headless Widgets
|
||||||
authors: ["@viridia"]
|
authors: ["@viridia"]
|
||||||
pull_requests: [19366]
|
pull_requests: [19366, 19584]
|
||||||
---
|
---
|
||||||
|
|
||||||
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
|
||||||
@ -33,7 +33,7 @@ The `bevy_core_widgets` crate provides implementations of unstyled widgets, such
|
|||||||
sliders, checkboxes and radio buttons.
|
sliders, checkboxes and radio buttons.
|
||||||
|
|
||||||
- `CoreButton` is a push button. It emits an activation event when clicked.
|
- `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
|
## 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.
|
to bubble up the hierarchy.
|
||||||
|
|
||||||
However, in UI work it's often desirable to connect widget interactions in ways that cut across the
|
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
|
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
|
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
|
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.
|
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
|
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,
|
(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
|
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 /
|
instead allows simpler one-way data binding that aligns well with the traditional "Model / View /
|
||||||
Controller" (MVC) design pattern.
|
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
|
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.
|
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
|
Second, widgets which have complex states that are too large and heavyweight to fit within a
|
||||||
|
Loading…
Reference in New Issue
Block a user