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.
|
||||
let radio_buttons = q_children
|
||||
.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_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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
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(),
|
||||
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)
|
||||
.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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user