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:
Talin 2025-06-30 23:59:14 -07:00 committed by GitHub
parent 8351da45f8
commit b980d4ac22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 739 additions and 6 deletions

View File

@ -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.
let radio_buttons = q_children
.iter_descendants(ev.target())

View File

@ -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_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
bevy_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_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
"bevy_ui_picking_backend",

View File

@ -19,5 +19,11 @@ pub mod size {
use bevy_ui::Val;
/// 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);
}

View 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),
);
}
}

View File

@ -2,9 +2,13 @@
use bevy_app::Plugin;
mod button;
mod checkbox;
mod radio;
mod slider;
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps};
pub use radio::{radio, RadioPlugin};
pub use slider::{slider, SliderPlugin, SliderProps};
/// Plugin which registers all `bevy_feathers` controls.
@ -12,6 +16,6 @@ pub struct ControlsPlugin;
impl Plugin for ControlsPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_plugins((ButtonPlugin, SliderPlugin));
app.add_plugins((ButtonPlugin, CheckboxPlugin, RadioPlugin, SliderPlugin));
}
}

View 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),
);
}
}

View File

@ -48,6 +48,51 @@ pub fn create_dark_theme() -> ThemeProps {
tokens::SLIDER_TEXT_DISABLED.into(),
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),
),
]),
}
}

View File

@ -67,6 +67,7 @@ impl Plugin for FeathersPlugin {
app.add_systems(PostUpdate, theme::update_theme)
.add_observer(theme::on_changed_background)
.add_observer(theme::on_changed_border)
.add_observer(theme::on_changed_font_color)
.add_observer(font_styles::on_changed_font);
}

View File

@ -73,6 +73,7 @@ pub struct ThemedText;
pub(crate) fn update_theme(
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>,
theme: Res<UiTheme>,
) {
if theme.is_changed() {
@ -80,6 +81,11 @@ pub(crate) fn update_theme(
for (mut bg, theme_bg) in q_background.iter_mut() {
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
/// propagates downward the text color to all participating text entities.
pub(crate) fn on_changed_font_color(

View File

@ -60,6 +60,14 @@ pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled";
// 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
pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border";
/// 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";
/// Checkbox label 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";

View File

@ -1,9 +1,11 @@
//! This example shows off the various Bevy Feathers widgets.
use bevy::{
core_widgets::{Callback, CoreWidgetsPlugin, SliderStep},
core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugin, SliderStep},
feathers::{
controls::{button, slider, ButtonProps, ButtonVariant, SliderProps},
controls::{
button, checkbox, radio, slider, ButtonProps, ButtonVariant, CheckboxProps, SliderProps,
},
dark_theme::create_dark_theme,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
@ -14,7 +16,7 @@ use bevy::{
InputDispatchPlugin,
},
prelude::*,
ui::InteractionDisabled,
ui::{Checked, InteractionDisabled},
winit::WinitSettings,
};
@ -42,6 +44,19 @@ fn setup(mut commands: Commands) {
}
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 {
width: Val::Percent(100.0),
@ -166,6 +181,47 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
(),
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(
SliderProps {
max: 100.0,

View File

@ -1,7 +1,7 @@
---
title: Bevy Feathers
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,