Skip to content

Commit b980d4a

Browse files
authored
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" />
1 parent 8351da4 commit b980d4a

File tree

12 files changed

+739
-6
lines changed

12 files changed

+739
-6
lines changed

crates/bevy_core_widgets/src/core_radio.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ fn radio_group_on_button_click(
170170
}
171171
};
172172

173+
// Radio button is disabled.
174+
if q_radio.get(radio_id).unwrap().1 {
175+
return;
176+
}
177+
173178
// Gather all the enabled radio group descendants for exclusion.
174179
let radio_buttons = q_children
175180
.iter_descendants(ev.target())

crates/bevy_feathers/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" }
1717
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
1818
bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
1919
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
20+
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
2021
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
2122
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
23+
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
2224
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
2325
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
2426
"bevy_ui_picking_backend",

crates/bevy_feathers/src/constants.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@ pub mod size {
1919
use bevy_ui::Val;
2020

2121
/// Common row size for buttons, sliders, spinners, etc.
22-
pub const ROW_HEIGHT: Val = Val::Px(22.0);
22+
pub const ROW_HEIGHT: Val = Val::Px(24.0);
23+
24+
/// Width and height of a checkbox
25+
pub const CHECKBOX_SIZE: Val = Val::Px(18.0);
26+
27+
/// Width and height of a radio button
28+
pub const RADIO_SIZE: Val = Val::Px(18.0);
2329
}
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use bevy_app::{Plugin, PreUpdate};
2+
use bevy_core_widgets::{Callback, CoreCheckbox};
3+
use bevy_ecs::{
4+
bundle::Bundle,
5+
children,
6+
component::Component,
7+
entity::Entity,
8+
hierarchy::{ChildOf, Children},
9+
lifecycle::RemovedComponents,
10+
query::{Added, Changed, Has, Or, With},
11+
schedule::IntoScheduleConfigs,
12+
spawn::{Spawn, SpawnRelated, SpawnableList},
13+
system::{Commands, In, Query},
14+
};
15+
use bevy_input_focus::tab_navigation::TabIndex;
16+
use bevy_math::Rot2;
17+
use bevy_picking::{hover::Hovered, PickingSystems};
18+
use bevy_render::view::Visibility;
19+
use bevy_ui::{
20+
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
21+
Node, PositionType, UiRect, UiTransform, Val,
22+
};
23+
use bevy_winit::cursor::CursorIcon;
24+
25+
use crate::{
26+
constants::{fonts, size},
27+
font_styles::InheritableFont,
28+
handle_or_path::HandleOrPath,
29+
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
30+
tokens,
31+
};
32+
33+
/// Parameters for the checkbox template, passed to [`checkbox`] function.
34+
#[derive(Default)]
35+
pub struct CheckboxProps {
36+
/// Change handler
37+
pub on_change: Callback<In<bool>>,
38+
}
39+
40+
/// Marker for the checkbox outline
41+
#[derive(Component, Default, Clone)]
42+
struct CheckboxOutline;
43+
44+
/// Marker for the checkbox check mark
45+
#[derive(Component, Default, Clone)]
46+
struct CheckboxMark;
47+
48+
/// Template function to spawn a checkbox.
49+
///
50+
/// # Arguments
51+
/// * `props` - construction properties for the checkbox.
52+
/// * `overrides` - a bundle of components that are merged in with the normal checkbox components.
53+
/// * `label` - the label of the checkbox.
54+
pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
55+
props: CheckboxProps,
56+
overrides: B,
57+
label: C,
58+
) -> impl Bundle {
59+
(
60+
Node {
61+
display: Display::Flex,
62+
flex_direction: FlexDirection::Row,
63+
justify_content: JustifyContent::Start,
64+
align_items: AlignItems::Center,
65+
column_gap: Val::Px(4.0),
66+
..Default::default()
67+
},
68+
CoreCheckbox {
69+
on_change: props.on_change,
70+
},
71+
Hovered::default(),
72+
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
73+
TabIndex(0),
74+
ThemeFontColor(tokens::CHECKBOX_TEXT),
75+
InheritableFont {
76+
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
77+
font_size: 14.0,
78+
},
79+
overrides,
80+
Children::spawn((
81+
Spawn((
82+
Node {
83+
width: size::CHECKBOX_SIZE,
84+
height: size::CHECKBOX_SIZE,
85+
border: UiRect::all(Val::Px(2.0)),
86+
..Default::default()
87+
},
88+
CheckboxOutline,
89+
BorderRadius::all(Val::Px(4.0)),
90+
ThemeBackgroundColor(tokens::CHECKBOX_BG),
91+
ThemeBorderColor(tokens::CHECKBOX_BORDER),
92+
children![(
93+
// Cheesy checkmark: rotated node with L-shaped border.
94+
Node {
95+
position_type: PositionType::Absolute,
96+
left: Val::Px(4.0),
97+
top: Val::Px(0.0),
98+
width: Val::Px(6.),
99+
height: Val::Px(11.),
100+
border: UiRect {
101+
bottom: Val::Px(2.0),
102+
right: Val::Px(2.0),
103+
..Default::default()
104+
},
105+
..Default::default()
106+
},
107+
UiTransform::from_rotation(Rot2::FRAC_PI_4),
108+
CheckboxMark,
109+
ThemeBorderColor(tokens::CHECKBOX_MARK),
110+
)],
111+
)),
112+
label,
113+
)),
114+
)
115+
}
116+
117+
fn update_checkbox_styles(
118+
q_checkboxes: Query<
119+
(
120+
Entity,
121+
Has<InteractionDisabled>,
122+
Has<Checked>,
123+
&Hovered,
124+
&ThemeFontColor,
125+
),
126+
(
127+
With<CoreCheckbox>,
128+
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
129+
),
130+
>,
131+
q_children: Query<&Children>,
132+
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
133+
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
134+
mut commands: Commands,
135+
) {
136+
for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {
137+
let Some(outline_ent) = q_children
138+
.iter_descendants(checkbox_ent)
139+
.find(|en| q_outline.contains(*en))
140+
else {
141+
continue;
142+
};
143+
let Some(mark_ent) = q_children
144+
.iter_descendants(checkbox_ent)
145+
.find(|en| q_mark.contains(*en))
146+
else {
147+
continue;
148+
};
149+
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
150+
let mark_color = q_mark.get_mut(mark_ent).unwrap();
151+
set_checkbox_colors(
152+
checkbox_ent,
153+
outline_ent,
154+
mark_ent,
155+
disabled,
156+
checked,
157+
hovered.0,
158+
outline_bg,
159+
outline_border,
160+
mark_color,
161+
font_color,
162+
&mut commands,
163+
);
164+
}
165+
}
166+
167+
fn update_checkbox_styles_remove(
168+
q_checkboxes: Query<
169+
(
170+
Entity,
171+
Has<InteractionDisabled>,
172+
Has<Checked>,
173+
&Hovered,
174+
&ThemeFontColor,
175+
),
176+
With<CoreCheckbox>,
177+
>,
178+
q_children: Query<&Children>,
179+
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
180+
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
181+
mut removed_disabled: RemovedComponents<InteractionDisabled>,
182+
mut removed_checked: RemovedComponents<Checked>,
183+
mut commands: Commands,
184+
) {
185+
removed_disabled
186+
.read()
187+
.chain(removed_checked.read())
188+
.for_each(|ent| {
189+
if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =
190+
q_checkboxes.get(ent)
191+
{
192+
let Some(outline_ent) = q_children
193+
.iter_descendants(checkbox_ent)
194+
.find(|en| q_outline.contains(*en))
195+
else {
196+
return;
197+
};
198+
let Some(mark_ent) = q_children
199+
.iter_descendants(checkbox_ent)
200+
.find(|en| q_mark.contains(*en))
201+
else {
202+
return;
203+
};
204+
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
205+
let mark_color = q_mark.get_mut(mark_ent).unwrap();
206+
set_checkbox_colors(
207+
checkbox_ent,
208+
outline_ent,
209+
mark_ent,
210+
disabled,
211+
checked,
212+
hovered.0,
213+
outline_bg,
214+
outline_border,
215+
mark_color,
216+
font_color,
217+
&mut commands,
218+
);
219+
}
220+
});
221+
}
222+
223+
fn set_checkbox_colors(
224+
checkbox_ent: Entity,
225+
outline_ent: Entity,
226+
mark_ent: Entity,
227+
disabled: bool,
228+
checked: bool,
229+
hovered: bool,
230+
outline_bg: &ThemeBackgroundColor,
231+
outline_border: &ThemeBorderColor,
232+
mark_color: &ThemeBorderColor,
233+
font_color: &ThemeFontColor,
234+
commands: &mut Commands,
235+
) {
236+
let outline_border_token = match (disabled, hovered) {
237+
(true, _) => tokens::CHECKBOX_BORDER_DISABLED,
238+
(false, true) => tokens::CHECKBOX_BORDER_HOVER,
239+
_ => tokens::CHECKBOX_BORDER,
240+
};
241+
242+
let outline_bg_token = match (disabled, checked) {
243+
(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,
244+
(true, false) => tokens::CHECKBOX_BG_DISABLED,
245+
(false, true) => tokens::CHECKBOX_BG_CHECKED,
246+
(false, false) => tokens::CHECKBOX_BG,
247+
};
248+
249+
let mark_token = match disabled {
250+
true => tokens::CHECKBOX_MARK_DISABLED,
251+
false => tokens::CHECKBOX_MARK,
252+
};
253+
254+
let font_color_token = match disabled {
255+
true => tokens::CHECKBOX_TEXT_DISABLED,
256+
false => tokens::CHECKBOX_TEXT,
257+
};
258+
259+
// Change outline background
260+
if outline_bg.0 != outline_bg_token {
261+
commands
262+
.entity(outline_ent)
263+
.insert(ThemeBackgroundColor(outline_bg_token));
264+
}
265+
266+
// Change outline border
267+
if outline_border.0 != outline_border_token {
268+
commands
269+
.entity(outline_ent)
270+
.insert(ThemeBorderColor(outline_border_token));
271+
}
272+
273+
// Change mark color
274+
if mark_color.0 != mark_token {
275+
commands
276+
.entity(mark_ent)
277+
.insert(ThemeBorderColor(mark_token));
278+
}
279+
280+
// Change mark visibility
281+
commands.entity(mark_ent).insert(match checked {
282+
true => Visibility::Visible,
283+
false => Visibility::Hidden,
284+
});
285+
286+
// Change font color
287+
if font_color.0 != font_color_token {
288+
commands
289+
.entity(checkbox_ent)
290+
.insert(ThemeFontColor(font_color_token));
291+
}
292+
}
293+
294+
/// Plugin which registers the systems for updating the checkbox styles.
295+
pub struct CheckboxPlugin;
296+
297+
impl Plugin for CheckboxPlugin {
298+
fn build(&self, app: &mut bevy_app::App) {
299+
app.add_systems(
300+
PreUpdate,
301+
(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last),
302+
);
303+
}
304+
}

crates/bevy_feathers/src/controls/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
use bevy_app::Plugin;
33

44
mod button;
5+
mod checkbox;
6+
mod radio;
57
mod slider;
68

79
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
10+
pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps};
11+
pub use radio::{radio, RadioPlugin};
812
pub use slider::{slider, SliderPlugin, SliderProps};
913

1014
/// Plugin which registers all `bevy_feathers` controls.
1115
pub struct ControlsPlugin;
1216

1317
impl Plugin for ControlsPlugin {
1418
fn build(&self, app: &mut bevy_app::App) {
15-
app.add_plugins((ButtonPlugin, SliderPlugin));
19+
app.add_plugins((ButtonPlugin, CheckboxPlugin, RadioPlugin, SliderPlugin));
1620
}
1721
}

0 commit comments

Comments
 (0)