Feathers checkbox (#19900)
Adds checkbox and radio buttons to feathers. Showcase: <img width="378" alt="feathers-checkbox-radio" src="https://github.com/user-attachments/assets/76d35589-6400-49dd-bf98-aeca2f39a472" />
This commit is contained in:
parent
8351da45f8
commit
b980d4ac22
@ -170,6 +170,11 @@ fn radio_group_on_button_click(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Radio button is disabled.
|
||||||
|
if q_radio.get(radio_id).unwrap().1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Gather all the enabled radio group descendants for exclusion.
|
// Gather all the enabled radio group descendants for exclusion.
|
||||||
let radio_buttons = q_children
|
let radio_buttons = q_children
|
||||||
.iter_descendants(ev.target())
|
.iter_descendants(ev.target())
|
||||||
|
@ -17,8 +17,10 @@ bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" }
|
|||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
|
||||||
bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
|
bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
|
||||||
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
|
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
|
||||||
|
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
|
||||||
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
|
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
|
||||||
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
|
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
|
||||||
|
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
|
||||||
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
|
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
|
||||||
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
|
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
|
||||||
"bevy_ui_picking_backend",
|
"bevy_ui_picking_backend",
|
||||||
|
@ -19,5 +19,11 @@ pub mod size {
|
|||||||
use bevy_ui::Val;
|
use bevy_ui::Val;
|
||||||
|
|
||||||
/// Common row size for buttons, sliders, spinners, etc.
|
/// Common row size for buttons, sliders, spinners, etc.
|
||||||
pub const ROW_HEIGHT: Val = Val::Px(22.0);
|
pub const ROW_HEIGHT: Val = Val::Px(24.0);
|
||||||
|
|
||||||
|
/// Width and height of a checkbox
|
||||||
|
pub const CHECKBOX_SIZE: Val = Val::Px(18.0);
|
||||||
|
|
||||||
|
/// Width and height of a radio button
|
||||||
|
pub const RADIO_SIZE: Val = Val::Px(18.0);
|
||||||
}
|
}
|
||||||
|
304
crates/bevy_feathers/src/controls/checkbox.rs
Normal file
304
crates/bevy_feathers/src/controls/checkbox.rs
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
use bevy_app::{Plugin, PreUpdate};
|
||||||
|
use bevy_core_widgets::{Callback, CoreCheckbox};
|
||||||
|
use bevy_ecs::{
|
||||||
|
bundle::Bundle,
|
||||||
|
children,
|
||||||
|
component::Component,
|
||||||
|
entity::Entity,
|
||||||
|
hierarchy::{ChildOf, Children},
|
||||||
|
lifecycle::RemovedComponents,
|
||||||
|
query::{Added, Changed, Has, Or, With},
|
||||||
|
schedule::IntoScheduleConfigs,
|
||||||
|
spawn::{Spawn, SpawnRelated, SpawnableList},
|
||||||
|
system::{Commands, In, Query},
|
||||||
|
};
|
||||||
|
use bevy_input_focus::tab_navigation::TabIndex;
|
||||||
|
use bevy_math::Rot2;
|
||||||
|
use bevy_picking::{hover::Hovered, PickingSystems};
|
||||||
|
use bevy_render::view::Visibility;
|
||||||
|
use bevy_ui::{
|
||||||
|
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
|
||||||
|
Node, PositionType, UiRect, UiTransform, Val,
|
||||||
|
};
|
||||||
|
use bevy_winit::cursor::CursorIcon;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{fonts, size},
|
||||||
|
font_styles::InheritableFont,
|
||||||
|
handle_or_path::HandleOrPath,
|
||||||
|
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parameters for the checkbox template, passed to [`checkbox`] function.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CheckboxProps {
|
||||||
|
/// Change handler
|
||||||
|
pub on_change: Callback<In<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker for the checkbox outline
|
||||||
|
#[derive(Component, Default, Clone)]
|
||||||
|
struct CheckboxOutline;
|
||||||
|
|
||||||
|
/// Marker for the checkbox check mark
|
||||||
|
#[derive(Component, Default, Clone)]
|
||||||
|
struct CheckboxMark;
|
||||||
|
|
||||||
|
/// Template function to spawn a checkbox.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `props` - construction properties for the checkbox.
|
||||||
|
/// * `overrides` - a bundle of components that are merged in with the normal checkbox components.
|
||||||
|
/// * `label` - the label of the checkbox.
|
||||||
|
pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
|
||||||
|
props: CheckboxProps,
|
||||||
|
overrides: B,
|
||||||
|
label: C,
|
||||||
|
) -> impl Bundle {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
justify_content: JustifyContent::Start,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
column_gap: Val::Px(4.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
CoreCheckbox {
|
||||||
|
on_change: props.on_change,
|
||||||
|
},
|
||||||
|
Hovered::default(),
|
||||||
|
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
|
||||||
|
TabIndex(0),
|
||||||
|
ThemeFontColor(tokens::CHECKBOX_TEXT),
|
||||||
|
InheritableFont {
|
||||||
|
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
|
||||||
|
font_size: 14.0,
|
||||||
|
},
|
||||||
|
overrides,
|
||||||
|
Children::spawn((
|
||||||
|
Spawn((
|
||||||
|
Node {
|
||||||
|
width: size::CHECKBOX_SIZE,
|
||||||
|
height: size::CHECKBOX_SIZE,
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
CheckboxOutline,
|
||||||
|
BorderRadius::all(Val::Px(4.0)),
|
||||||
|
ThemeBackgroundColor(tokens::CHECKBOX_BG),
|
||||||
|
ThemeBorderColor(tokens::CHECKBOX_BORDER),
|
||||||
|
children![(
|
||||||
|
// Cheesy checkmark: rotated node with L-shaped border.
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left: Val::Px(4.0),
|
||||||
|
top: Val::Px(0.0),
|
||||||
|
width: Val::Px(6.),
|
||||||
|
height: Val::Px(11.),
|
||||||
|
border: UiRect {
|
||||||
|
bottom: Val::Px(2.0),
|
||||||
|
right: Val::Px(2.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
UiTransform::from_rotation(Rot2::FRAC_PI_4),
|
||||||
|
CheckboxMark,
|
||||||
|
ThemeBorderColor(tokens::CHECKBOX_MARK),
|
||||||
|
)],
|
||||||
|
)),
|
||||||
|
label,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_checkbox_styles(
|
||||||
|
q_checkboxes: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
Has<InteractionDisabled>,
|
||||||
|
Has<Checked>,
|
||||||
|
&Hovered,
|
||||||
|
&ThemeFontColor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
With<CoreCheckbox>,
|
||||||
|
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
|
||||||
|
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {
|
||||||
|
let Some(outline_ent) = q_children
|
||||||
|
.iter_descendants(checkbox_ent)
|
||||||
|
.find(|en| q_outline.contains(*en))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(mark_ent) = q_children
|
||||||
|
.iter_descendants(checkbox_ent)
|
||||||
|
.find(|en| q_mark.contains(*en))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
|
||||||
|
let mark_color = q_mark.get_mut(mark_ent).unwrap();
|
||||||
|
set_checkbox_colors(
|
||||||
|
checkbox_ent,
|
||||||
|
outline_ent,
|
||||||
|
mark_ent,
|
||||||
|
disabled,
|
||||||
|
checked,
|
||||||
|
hovered.0,
|
||||||
|
outline_bg,
|
||||||
|
outline_border,
|
||||||
|
mark_color,
|
||||||
|
font_color,
|
||||||
|
&mut commands,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_checkbox_styles_remove(
|
||||||
|
q_checkboxes: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
Has<InteractionDisabled>,
|
||||||
|
Has<Checked>,
|
||||||
|
&Hovered,
|
||||||
|
&ThemeFontColor,
|
||||||
|
),
|
||||||
|
With<CoreCheckbox>,
|
||||||
|
>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
|
||||||
|
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
|
||||||
|
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||||
|
mut removed_checked: RemovedComponents<Checked>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
removed_disabled
|
||||||
|
.read()
|
||||||
|
.chain(removed_checked.read())
|
||||||
|
.for_each(|ent| {
|
||||||
|
if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =
|
||||||
|
q_checkboxes.get(ent)
|
||||||
|
{
|
||||||
|
let Some(outline_ent) = q_children
|
||||||
|
.iter_descendants(checkbox_ent)
|
||||||
|
.find(|en| q_outline.contains(*en))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mark_ent) = q_children
|
||||||
|
.iter_descendants(checkbox_ent)
|
||||||
|
.find(|en| q_mark.contains(*en))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
|
||||||
|
let mark_color = q_mark.get_mut(mark_ent).unwrap();
|
||||||
|
set_checkbox_colors(
|
||||||
|
checkbox_ent,
|
||||||
|
outline_ent,
|
||||||
|
mark_ent,
|
||||||
|
disabled,
|
||||||
|
checked,
|
||||||
|
hovered.0,
|
||||||
|
outline_bg,
|
||||||
|
outline_border,
|
||||||
|
mark_color,
|
||||||
|
font_color,
|
||||||
|
&mut commands,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_checkbox_colors(
|
||||||
|
checkbox_ent: Entity,
|
||||||
|
outline_ent: Entity,
|
||||||
|
mark_ent: Entity,
|
||||||
|
disabled: bool,
|
||||||
|
checked: bool,
|
||||||
|
hovered: bool,
|
||||||
|
outline_bg: &ThemeBackgroundColor,
|
||||||
|
outline_border: &ThemeBorderColor,
|
||||||
|
mark_color: &ThemeBorderColor,
|
||||||
|
font_color: &ThemeFontColor,
|
||||||
|
commands: &mut Commands,
|
||||||
|
) {
|
||||||
|
let outline_border_token = match (disabled, hovered) {
|
||||||
|
(true, _) => tokens::CHECKBOX_BORDER_DISABLED,
|
||||||
|
(false, true) => tokens::CHECKBOX_BORDER_HOVER,
|
||||||
|
_ => tokens::CHECKBOX_BORDER,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outline_bg_token = match (disabled, checked) {
|
||||||
|
(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,
|
||||||
|
(true, false) => tokens::CHECKBOX_BG_DISABLED,
|
||||||
|
(false, true) => tokens::CHECKBOX_BG_CHECKED,
|
||||||
|
(false, false) => tokens::CHECKBOX_BG,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mark_token = match disabled {
|
||||||
|
true => tokens::CHECKBOX_MARK_DISABLED,
|
||||||
|
false => tokens::CHECKBOX_MARK,
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_color_token = match disabled {
|
||||||
|
true => tokens::CHECKBOX_TEXT_DISABLED,
|
||||||
|
false => tokens::CHECKBOX_TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change outline background
|
||||||
|
if outline_bg.0 != outline_bg_token {
|
||||||
|
commands
|
||||||
|
.entity(outline_ent)
|
||||||
|
.insert(ThemeBackgroundColor(outline_bg_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change outline border
|
||||||
|
if outline_border.0 != outline_border_token {
|
||||||
|
commands
|
||||||
|
.entity(outline_ent)
|
||||||
|
.insert(ThemeBorderColor(outline_border_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change mark color
|
||||||
|
if mark_color.0 != mark_token {
|
||||||
|
commands
|
||||||
|
.entity(mark_ent)
|
||||||
|
.insert(ThemeBorderColor(mark_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change mark visibility
|
||||||
|
commands.entity(mark_ent).insert(match checked {
|
||||||
|
true => Visibility::Visible,
|
||||||
|
false => Visibility::Hidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change font color
|
||||||
|
if font_color.0 != font_color_token {
|
||||||
|
commands
|
||||||
|
.entity(checkbox_ent)
|
||||||
|
.insert(ThemeFontColor(font_color_token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin which registers the systems for updating the checkbox styles.
|
||||||
|
pub struct CheckboxPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CheckboxPlugin {
|
||||||
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
|
app.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,13 @@
|
|||||||
use bevy_app::Plugin;
|
use bevy_app::Plugin;
|
||||||
|
|
||||||
mod button;
|
mod button;
|
||||||
|
mod checkbox;
|
||||||
|
mod radio;
|
||||||
mod slider;
|
mod slider;
|
||||||
|
|
||||||
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
|
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
|
||||||
|
pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps};
|
||||||
|
pub use radio::{radio, RadioPlugin};
|
||||||
pub use slider::{slider, SliderPlugin, SliderProps};
|
pub use slider::{slider, SliderPlugin, SliderProps};
|
||||||
|
|
||||||
/// Plugin which registers all `bevy_feathers` controls.
|
/// Plugin which registers all `bevy_feathers` controls.
|
||||||
@ -12,6 +16,6 @@ pub struct ControlsPlugin;
|
|||||||
|
|
||||||
impl Plugin for ControlsPlugin {
|
impl Plugin for ControlsPlugin {
|
||||||
fn build(&self, app: &mut bevy_app::App) {
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
app.add_plugins((ButtonPlugin, SliderPlugin));
|
app.add_plugins((ButtonPlugin, CheckboxPlugin, RadioPlugin, SliderPlugin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
268
crates/bevy_feathers/src/controls/radio.rs
Normal file
268
crates/bevy_feathers/src/controls/radio.rs
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
use bevy_app::{Plugin, PreUpdate};
|
||||||
|
use bevy_core_widgets::CoreRadio;
|
||||||
|
use bevy_ecs::{
|
||||||
|
bundle::Bundle,
|
||||||
|
children,
|
||||||
|
component::Component,
|
||||||
|
entity::Entity,
|
||||||
|
hierarchy::{ChildOf, Children},
|
||||||
|
lifecycle::RemovedComponents,
|
||||||
|
query::{Added, Changed, Has, Or, With},
|
||||||
|
schedule::IntoScheduleConfigs,
|
||||||
|
spawn::{Spawn, SpawnRelated, SpawnableList},
|
||||||
|
system::{Commands, Query},
|
||||||
|
};
|
||||||
|
use bevy_input_focus::tab_navigation::TabIndex;
|
||||||
|
use bevy_picking::{hover::Hovered, PickingSystems};
|
||||||
|
use bevy_render::view::Visibility;
|
||||||
|
use bevy_ui::{
|
||||||
|
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
|
||||||
|
Node, UiRect, Val,
|
||||||
|
};
|
||||||
|
use bevy_winit::cursor::CursorIcon;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{fonts, size},
|
||||||
|
font_styles::InheritableFont,
|
||||||
|
handle_or_path::HandleOrPath,
|
||||||
|
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Marker for the radio outline
|
||||||
|
#[derive(Component, Default, Clone)]
|
||||||
|
struct RadioOutline;
|
||||||
|
|
||||||
|
/// Marker for the radio check mark
|
||||||
|
#[derive(Component, Default, Clone)]
|
||||||
|
struct RadioMark;
|
||||||
|
|
||||||
|
/// Template function to spawn a radio.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `props` - construction properties for the radio.
|
||||||
|
/// * `overrides` - a bundle of components that are merged in with the normal radio components.
|
||||||
|
/// * `label` - the label of the radio.
|
||||||
|
pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
|
||||||
|
overrides: B,
|
||||||
|
label: C,
|
||||||
|
) -> impl Bundle {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
justify_content: JustifyContent::Start,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
column_gap: Val::Px(4.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
CoreRadio,
|
||||||
|
Hovered::default(),
|
||||||
|
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
|
||||||
|
TabIndex(0),
|
||||||
|
ThemeFontColor(tokens::RADIO_TEXT),
|
||||||
|
InheritableFont {
|
||||||
|
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
|
||||||
|
font_size: 14.0,
|
||||||
|
},
|
||||||
|
overrides,
|
||||||
|
Children::spawn((
|
||||||
|
Spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
width: size::RADIO_SIZE,
|
||||||
|
height: size::RADIO_SIZE,
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
RadioOutline,
|
||||||
|
BorderRadius::MAX,
|
||||||
|
ThemeBorderColor(tokens::RADIO_BORDER),
|
||||||
|
children![(
|
||||||
|
// Cheesy checkmark: rotated node with L-shaped border.
|
||||||
|
Node {
|
||||||
|
width: Val::Px(8.),
|
||||||
|
height: Val::Px(8.),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
BorderRadius::MAX,
|
||||||
|
RadioMark,
|
||||||
|
ThemeBackgroundColor(tokens::RADIO_MARK),
|
||||||
|
)],
|
||||||
|
)),
|
||||||
|
label,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_radio_styles(
|
||||||
|
q_radioes: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
Has<InteractionDisabled>,
|
||||||
|
Has<Checked>,
|
||||||
|
&Hovered,
|
||||||
|
&ThemeFontColor,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
With<CoreRadio>,
|
||||||
|
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
|
||||||
|
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() {
|
||||||
|
let Some(outline_ent) = q_children
|
||||||
|
.iter_descendants(radio_ent)
|
||||||
|
.find(|en| q_outline.contains(*en))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(mark_ent) = q_children
|
||||||
|
.iter_descendants(radio_ent)
|
||||||
|
.find(|en| q_mark.contains(*en))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let outline_border = q_outline.get_mut(outline_ent).unwrap();
|
||||||
|
let mark_color = q_mark.get_mut(mark_ent).unwrap();
|
||||||
|
set_radio_colors(
|
||||||
|
radio_ent,
|
||||||
|
outline_ent,
|
||||||
|
mark_ent,
|
||||||
|
disabled,
|
||||||
|
checked,
|
||||||
|
hovered.0,
|
||||||
|
outline_border,
|
||||||
|
mark_color,
|
||||||
|
font_color,
|
||||||
|
&mut commands,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_radio_styles_remove(
|
||||||
|
q_radioes: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
Has<InteractionDisabled>,
|
||||||
|
Has<Checked>,
|
||||||
|
&Hovered,
|
||||||
|
&ThemeFontColor,
|
||||||
|
),
|
||||||
|
With<CoreRadio>,
|
||||||
|
>,
|
||||||
|
q_children: Query<&Children>,
|
||||||
|
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
|
||||||
|
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
|
||||||
|
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||||
|
mut removed_checked: RemovedComponents<Checked>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
removed_disabled
|
||||||
|
.read()
|
||||||
|
.chain(removed_checked.read())
|
||||||
|
.for_each(|ent| {
|
||||||
|
if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) {
|
||||||
|
let Some(outline_ent) = q_children
|
||||||
|
.iter_descendants(radio_ent)
|
||||||
|
.find(|en| q_outline.contains(*en))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mark_ent) = q_children
|
||||||
|
.iter_descendants(radio_ent)
|
||||||
|
.find(|en| q_mark.contains(*en))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let outline_border = q_outline.get_mut(outline_ent).unwrap();
|
||||||
|
let mark_color = q_mark.get_mut(mark_ent).unwrap();
|
||||||
|
set_radio_colors(
|
||||||
|
radio_ent,
|
||||||
|
outline_ent,
|
||||||
|
mark_ent,
|
||||||
|
disabled,
|
||||||
|
checked,
|
||||||
|
hovered.0,
|
||||||
|
outline_border,
|
||||||
|
mark_color,
|
||||||
|
font_color,
|
||||||
|
&mut commands,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_radio_colors(
|
||||||
|
radio_ent: Entity,
|
||||||
|
outline_ent: Entity,
|
||||||
|
mark_ent: Entity,
|
||||||
|
disabled: bool,
|
||||||
|
checked: bool,
|
||||||
|
hovered: bool,
|
||||||
|
outline_border: &ThemeBorderColor,
|
||||||
|
mark_color: &ThemeBackgroundColor,
|
||||||
|
font_color: &ThemeFontColor,
|
||||||
|
commands: &mut Commands,
|
||||||
|
) {
|
||||||
|
let outline_border_token = match (disabled, hovered) {
|
||||||
|
(true, _) => tokens::RADIO_BORDER_DISABLED,
|
||||||
|
(false, true) => tokens::RADIO_BORDER_HOVER,
|
||||||
|
_ => tokens::RADIO_BORDER,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mark_token = match disabled {
|
||||||
|
true => tokens::RADIO_MARK_DISABLED,
|
||||||
|
false => tokens::RADIO_MARK,
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_color_token = match disabled {
|
||||||
|
true => tokens::RADIO_TEXT_DISABLED,
|
||||||
|
false => tokens::RADIO_TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change outline border
|
||||||
|
if outline_border.0 != outline_border_token {
|
||||||
|
commands
|
||||||
|
.entity(outline_ent)
|
||||||
|
.insert(ThemeBorderColor(outline_border_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change mark color
|
||||||
|
if mark_color.0 != mark_token {
|
||||||
|
commands
|
||||||
|
.entity(mark_ent)
|
||||||
|
.insert(ThemeBorderColor(mark_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change mark visibility
|
||||||
|
commands.entity(mark_ent).insert(match checked {
|
||||||
|
true => Visibility::Visible,
|
||||||
|
false => Visibility::Hidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change font color
|
||||||
|
if font_color.0 != font_color_token {
|
||||||
|
commands
|
||||||
|
.entity(radio_ent)
|
||||||
|
.insert(ThemeFontColor(font_color_token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin which registers the systems for updating the radio styles.
|
||||||
|
pub struct RadioPlugin;
|
||||||
|
|
||||||
|
impl Plugin for RadioPlugin {
|
||||||
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
|
app.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,51 @@ pub fn create_dark_theme() -> ThemeProps {
|
|||||||
tokens::SLIDER_TEXT_DISABLED.into(),
|
tokens::SLIDER_TEXT_DISABLED.into(),
|
||||||
palette::WHITE.with_alpha(0.5),
|
palette::WHITE.with_alpha(0.5),
|
||||||
),
|
),
|
||||||
|
(tokens::CHECKBOX_BG.into(), palette::GRAY_3),
|
||||||
|
(tokens::CHECKBOX_BG_CHECKED.into(), palette::ACCENT),
|
||||||
|
(
|
||||||
|
tokens::CHECKBOX_BG_DISABLED.into(),
|
||||||
|
palette::GRAY_1.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tokens::CHECKBOX_BG_CHECKED_DISABLED.into(),
|
||||||
|
palette::GRAY_3.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(tokens::CHECKBOX_BORDER.into(), palette::GRAY_3),
|
||||||
|
(
|
||||||
|
tokens::CHECKBOX_BORDER_HOVER.into(),
|
||||||
|
palette::GRAY_3.lighter(0.1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tokens::CHECKBOX_BORDER_DISABLED.into(),
|
||||||
|
palette::GRAY_3.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(tokens::CHECKBOX_MARK.into(), palette::WHITE),
|
||||||
|
(tokens::CHECKBOX_MARK_DISABLED.into(), palette::LIGHT_GRAY_2),
|
||||||
|
(tokens::CHECKBOX_TEXT.into(), palette::LIGHT_GRAY_1),
|
||||||
|
(
|
||||||
|
tokens::CHECKBOX_TEXT_DISABLED.into(),
|
||||||
|
palette::LIGHT_GRAY_1.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(tokens::RADIO_BORDER.into(), palette::GRAY_3),
|
||||||
|
(
|
||||||
|
tokens::RADIO_BORDER_HOVER.into(),
|
||||||
|
palette::GRAY_3.lighter(0.1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tokens::RADIO_BORDER_DISABLED.into(),
|
||||||
|
palette::GRAY_3.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(tokens::RADIO_MARK.into(), palette::ACCENT),
|
||||||
|
(
|
||||||
|
tokens::RADIO_MARK_DISABLED.into(),
|
||||||
|
palette::ACCENT.with_alpha(0.5),
|
||||||
|
),
|
||||||
|
(tokens::RADIO_TEXT.into(), palette::LIGHT_GRAY_1),
|
||||||
|
(
|
||||||
|
tokens::RADIO_TEXT_DISABLED.into(),
|
||||||
|
palette::LIGHT_GRAY_1.with_alpha(0.5),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,7 @@ impl Plugin for FeathersPlugin {
|
|||||||
|
|
||||||
app.add_systems(PostUpdate, theme::update_theme)
|
app.add_systems(PostUpdate, theme::update_theme)
|
||||||
.add_observer(theme::on_changed_background)
|
.add_observer(theme::on_changed_background)
|
||||||
|
.add_observer(theme::on_changed_border)
|
||||||
.add_observer(theme::on_changed_font_color)
|
.add_observer(theme::on_changed_font_color)
|
||||||
.add_observer(font_styles::on_changed_font);
|
.add_observer(font_styles::on_changed_font);
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ pub struct ThemedText;
|
|||||||
|
|
||||||
pub(crate) fn update_theme(
|
pub(crate) fn update_theme(
|
||||||
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
|
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
|
||||||
|
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>,
|
||||||
theme: Res<UiTheme>,
|
theme: Res<UiTheme>,
|
||||||
) {
|
) {
|
||||||
if theme.is_changed() {
|
if theme.is_changed() {
|
||||||
@ -80,6 +81,11 @@ pub(crate) fn update_theme(
|
|||||||
for (mut bg, theme_bg) in q_background.iter_mut() {
|
for (mut bg, theme_bg) in q_background.iter_mut() {
|
||||||
bg.0 = theme.color(theme_bg.0);
|
bg.0 = theme.color(theme_bg.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update all border colors
|
||||||
|
for (mut border, theme_border) in q_border.iter_mut() {
|
||||||
|
border.set_all(theme.color(theme_border.0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +103,17 @@ pub(crate) fn on_changed_background(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_changed_border(
|
||||||
|
ev: On<Insert, ThemeBorderColor>,
|
||||||
|
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed<ThemeBorderColor>>,
|
||||||
|
theme: Res<UiTheme>,
|
||||||
|
) {
|
||||||
|
// Update background colors where the design token has changed.
|
||||||
|
if let Ok((mut border, theme_border)) = q_border.get_mut(ev.target()) {
|
||||||
|
border.set_all(theme.color(theme_border.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and
|
/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and
|
||||||
/// propagates downward the text color to all participating text entities.
|
/// propagates downward the text color to all participating text entities.
|
||||||
pub(crate) fn on_changed_font_color(
|
pub(crate) fn on_changed_font_color(
|
||||||
|
@ -60,6 +60,14 @@ pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled";
|
|||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
|
|
||||||
|
/// Checkbox background around the checkmark
|
||||||
|
pub const CHECKBOX_BG: &str = "feathers.checkbox.bg";
|
||||||
|
/// Checkbox border around the checkmark (disabled)
|
||||||
|
pub const CHECKBOX_BG_DISABLED: &str = "feathers.checkbox.bg.disabled";
|
||||||
|
/// Checkbox background around the checkmark
|
||||||
|
pub const CHECKBOX_BG_CHECKED: &str = "feathers.checkbox.bg.checked";
|
||||||
|
/// Checkbox border around the checkmark (disabled)
|
||||||
|
pub const CHECKBOX_BG_CHECKED_DISABLED: &str = "feathers.checkbox.bg.checked.disabled";
|
||||||
/// Checkbox border around the checkmark
|
/// Checkbox border around the checkmark
|
||||||
pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border";
|
pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border";
|
||||||
/// Checkbox border around the checkmark (hovered)
|
/// Checkbox border around the checkmark (hovered)
|
||||||
@ -74,3 +82,20 @@ pub const CHECKBOX_MARK_DISABLED: &str = "feathers.checkbox.mark.disabled";
|
|||||||
pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text";
|
pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text";
|
||||||
/// Checkbox label text (disabled)
|
/// Checkbox label text (disabled)
|
||||||
pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled";
|
pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled";
|
||||||
|
|
||||||
|
// Radio button
|
||||||
|
|
||||||
|
/// Radio border around the checkmark
|
||||||
|
pub const RADIO_BORDER: &str = "feathers.radio.border";
|
||||||
|
/// Radio border around the checkmark (hovered)
|
||||||
|
pub const RADIO_BORDER_HOVER: &str = "feathers.radio.border.hover";
|
||||||
|
/// Radio border around the checkmark (disabled)
|
||||||
|
pub const RADIO_BORDER_DISABLED: &str = "feathers.radio.border.disabled";
|
||||||
|
/// Radio check mark
|
||||||
|
pub const RADIO_MARK: &str = "feathers.radio.mark";
|
||||||
|
/// Radio check mark (disabled)
|
||||||
|
pub const RADIO_MARK_DISABLED: &str = "feathers.radio.mark.disabled";
|
||||||
|
/// Radio label text
|
||||||
|
pub const RADIO_TEXT: &str = "feathers.radio.text";
|
||||||
|
/// Radio label text (disabled)
|
||||||
|
pub const RADIO_TEXT_DISABLED: &str = "feathers.radio.text.disabled";
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
//! This example shows off the various Bevy Feathers widgets.
|
//! This example shows off the various Bevy Feathers widgets.
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
core_widgets::{Callback, CoreWidgetsPlugin, SliderStep},
|
core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugin, SliderStep},
|
||||||
feathers::{
|
feathers::{
|
||||||
controls::{button, slider, ButtonProps, ButtonVariant, SliderProps},
|
controls::{
|
||||||
|
button, checkbox, radio, slider, ButtonProps, ButtonVariant, CheckboxProps, SliderProps,
|
||||||
|
},
|
||||||
dark_theme::create_dark_theme,
|
dark_theme::create_dark_theme,
|
||||||
rounded_corners::RoundedCorners,
|
rounded_corners::RoundedCorners,
|
||||||
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
|
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
|
||||||
@ -14,7 +16,7 @@ use bevy::{
|
|||||||
InputDispatchPlugin,
|
InputDispatchPlugin,
|
||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
ui::InteractionDisabled,
|
ui::{Checked, InteractionDisabled},
|
||||||
winit::WinitSettings,
|
winit::WinitSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +44,19 @@ fn setup(mut commands: Commands) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn demo_root(commands: &mut Commands) -> impl Bundle {
|
fn demo_root(commands: &mut Commands) -> impl Bundle {
|
||||||
|
// Update radio button states based on notification from radio group.
|
||||||
|
let radio_exclusion = commands.register_system(
|
||||||
|
|ent: In<Entity>, q_radio: Query<Entity, With<CoreRadio>>, mut commands: Commands| {
|
||||||
|
for radio in q_radio.iter() {
|
||||||
|
if radio == *ent {
|
||||||
|
commands.entity(radio).insert(Checked);
|
||||||
|
} else {
|
||||||
|
commands.entity(radio).remove::<Checked>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
width: Val::Percent(100.0),
|
width: Val::Percent(100.0),
|
||||||
@ -166,6 +181,47 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
|
|||||||
(),
|
(),
|
||||||
Spawn((Text::new("Button"), ThemedText))
|
Spawn((Text::new("Button"), ThemedText))
|
||||||
),
|
),
|
||||||
|
checkbox(
|
||||||
|
CheckboxProps {
|
||||||
|
on_change: Callback::Ignore,
|
||||||
|
},
|
||||||
|
Checked,
|
||||||
|
Spawn((Text::new("Checkbox"), ThemedText))
|
||||||
|
),
|
||||||
|
checkbox(
|
||||||
|
CheckboxProps {
|
||||||
|
on_change: Callback::Ignore,
|
||||||
|
},
|
||||||
|
InteractionDisabled,
|
||||||
|
Spawn((Text::new("Disabled"), ThemedText))
|
||||||
|
),
|
||||||
|
checkbox(
|
||||||
|
CheckboxProps {
|
||||||
|
on_change: Callback::Ignore,
|
||||||
|
},
|
||||||
|
(InteractionDisabled, Checked),
|
||||||
|
Spawn((Text::new("Disabled+Checked"), ThemedText))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
row_gap: Val::Px(4.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
CoreRadioGroup {
|
||||||
|
on_change: Callback::System(radio_exclusion),
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
radio(Checked, Spawn((Text::new("One"), ThemedText))),
|
||||||
|
radio((), Spawn((Text::new("Two"), ThemedText))),
|
||||||
|
radio((), Spawn((Text::new("Three"), ThemedText))),
|
||||||
|
radio(
|
||||||
|
InteractionDisabled,
|
||||||
|
Spawn((Text::new("Disabled"), ThemedText))
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
slider(
|
slider(
|
||||||
SliderProps {
|
SliderProps {
|
||||||
max: 100.0,
|
max: 100.0,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Bevy Feathers
|
title: Bevy Feathers
|
||||||
authors: ["@viridia", "@Atlas16A"]
|
authors: ["@viridia", "@Atlas16A"]
|
||||||
pull_requests: [19730]
|
pull_requests: [19730, 19900]
|
||||||
---
|
---
|
||||||
|
|
||||||
To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling,
|
To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling,
|
||||||
|
Loading…
Reference in New Issue
Block a user