Skip to content

Commit e677bab

Browse files
authored
Fix and Polish Feathers Checkbox (#23820)
# Objective * Fix toggle behavior * Add more style tokens and polish appearance The original intention for the Feathers Checkbox was to toggle the change immediately on mouse press, but currently only toggles on mouse release, and lacks styling. Both behaviors are desirable depending on context, such as being able to cancel an expensive checkmark click that vastly changes render settings, so this pull request allows both, with the mouse-press behavior enabled via the `ActivateOnPress` component. In addition to adding styling, there will no longer a visible outline for the checkbox in the checked state in the default theme. Having a border and check symbol made it visually busy, especially at small sizes. --- ## Showcase ### Before <img width="161" height="83" alt="Screenshot 2026-04-15 at 7 58 09 PM" src="https://github.com/user-attachments/assets/9cae7bcf-0b2a-46db-954a-b09332a7b06f" /> ### After <img width="161" height="108" alt="Screenshot 2026-04-15 at 8 34 57 PM" src="https://github.com/user-attachments/assets/72d5083f-20db-45cd-8c1b-1ee711fcedf3" /> (Not pictured: new intermediate styling for mouse press)
1 parent 5802ab0 commit e677bab

5 files changed

Lines changed: 241 additions & 33 deletions

File tree

crates/bevy_feathers/src/controls/checkbox.rs

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ use bevy_scene::prelude::*;
2121
use bevy_text::FontWeight;
2222
use bevy_ui::{
2323
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
24-
Node, PositionType, UiRect, UiTransform, Val,
24+
Node, PositionType, Pressed, UiRect, UiTransform, Val,
2525
};
26-
use bevy_ui_widgets::Checkbox;
26+
use bevy_ui_widgets::{ActivateOnPress, Checkbox};
2727

2828
use crate::{
2929
constants::{fonts, size},
@@ -201,20 +201,29 @@ fn update_checkbox_styles(
201201
Entity,
202202
Has<InteractionDisabled>,
203203
Has<Checked>,
204+
Has<Pressed>,
205+
Has<ActivateOnPress>,
204206
&Hovered,
205207
&ThemeFontColor,
206208
),
207209
(
208210
With<CheckboxFrame>,
209-
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
211+
Or<(
212+
Changed<Hovered>,
213+
Added<Checked>,
214+
Added<Pressed>,
215+
Added<InteractionDisabled>,
216+
)>,
210217
),
211218
>,
212219
q_children: Query<&Children>,
213220
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
214221
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
215222
mut commands: Commands,
216223
) {
217-
for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {
224+
for (checkbox_ent, disabled, checked, pressed, activate_on_press, hovered, font_color) in
225+
q_checkboxes.iter()
226+
{
218227
let Some(outline_ent) = q_children
219228
.iter_descendants(checkbox_ent)
220229
.find(|en| q_outline.contains(*en))
@@ -235,7 +244,9 @@ fn update_checkbox_styles(
235244
mark_ent,
236245
disabled,
237246
checked,
247+
pressed,
238248
hovered.0,
249+
activate_on_press,
239250
outline_bg,
240251
outline_border,
241252
mark_color,
@@ -251,6 +262,8 @@ fn update_checkbox_styles_remove(
251262
Entity,
252263
Has<InteractionDisabled>,
253264
Has<Checked>,
265+
Has<Pressed>,
266+
Has<ActivateOnPress>,
254267
&Hovered,
255268
&ThemeFontColor,
256269
),
@@ -261,14 +274,25 @@ fn update_checkbox_styles_remove(
261274
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
262275
mut removed_disabled: RemovedComponents<InteractionDisabled>,
263276
mut removed_checked: RemovedComponents<Checked>,
277+
mut remove_pressed: RemovedComponents<Pressed>,
278+
mut remove_activate_on_press: RemovedComponents<ActivateOnPress>,
264279
mut commands: Commands,
265280
) {
266281
removed_disabled
267282
.read()
268283
.chain(removed_checked.read())
284+
.chain(remove_pressed.read())
285+
.chain(remove_activate_on_press.read())
269286
.for_each(|ent| {
270-
if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =
271-
q_checkboxes.get(ent)
287+
if let Ok((
288+
checkbox_ent,
289+
disabled,
290+
checked,
291+
pressed,
292+
activate_on_press,
293+
hovered,
294+
font_color,
295+
)) = q_checkboxes.get(ent)
272296
{
273297
let Some(outline_ent) = q_children
274298
.iter_descendants(checkbox_ent)
@@ -290,7 +314,9 @@ fn update_checkbox_styles_remove(
290314
mark_ent,
291315
disabled,
292316
checked,
317+
pressed,
293318
hovered.0,
319+
activate_on_press,
294320
outline_bg,
295321
outline_border,
296322
mark_color,
@@ -307,24 +333,57 @@ fn set_checkbox_styles(
307333
mark_ent: Entity,
308334
disabled: bool,
309335
checked: bool,
336+
pressed: bool,
310337
hovered: bool,
338+
activate_on_press: bool,
311339
outline_bg: &ThemeBackgroundColor,
312340
outline_border: &ThemeBorderColor,
313341
mark_color: &ThemeBorderColor,
314342
font_color: &ThemeFontColor,
315343
commands: &mut Commands,
316344
) {
317-
let outline_border_token = match (disabled, hovered) {
318-
(true, _) => tokens::CHECKBOX_BORDER_DISABLED,
319-
(false, true) => tokens::CHECKBOX_BORDER_HOVER,
320-
_ => tokens::CHECKBOX_BORDER,
345+
let outline_border_token = if checked {
346+
if disabled {
347+
tokens::CHECKBOX_BORDER_CHECKED_DISABLED
348+
} else if pressed && !activate_on_press {
349+
tokens::CHECKBOX_BORDER_CHECKED_PRESSED
350+
} else if hovered {
351+
tokens::CHECKBOX_BORDER_CHECKED_HOVER
352+
} else {
353+
tokens::CHECKBOX_BORDER_CHECKED
354+
}
355+
} else {
356+
if disabled {
357+
tokens::CHECKBOX_BORDER_DISABLED
358+
} else if pressed && !activate_on_press {
359+
tokens::CHECKBOX_BORDER_PRESSED
360+
} else if hovered {
361+
tokens::CHECKBOX_BORDER_HOVER
362+
} else {
363+
tokens::CHECKBOX_BORDER
364+
}
321365
};
322366

323-
let outline_bg_token = match (disabled, checked) {
324-
(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,
325-
(true, false) => tokens::CHECKBOX_BG_DISABLED,
326-
(false, true) => tokens::CHECKBOX_BG_CHECKED,
327-
(false, false) => tokens::CHECKBOX_BG,
367+
let outline_bg_token = if checked {
368+
if disabled {
369+
tokens::CHECKBOX_BG_CHECKED_DISABLED
370+
} else if pressed && !activate_on_press {
371+
tokens::CHECKBOX_BG_CHECKED_PRESSED
372+
} else if hovered {
373+
tokens::CHECKBOX_BG_CHECKED_HOVER
374+
} else {
375+
tokens::CHECKBOX_BG_CHECKED
376+
}
377+
} else {
378+
if disabled {
379+
tokens::CHECKBOX_BG_DISABLED
380+
} else if pressed && !activate_on_press {
381+
tokens::CHECKBOX_BG_PRESSED
382+
} else if hovered {
383+
tokens::CHECKBOX_BG_HOVER
384+
} else {
385+
tokens::CHECKBOX_BG
386+
}
328387
};
329388

330389
let mark_token = match disabled {

crates/bevy_feathers/src/dark_theme.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,48 @@ pub fn create_dark_theme() -> ThemeProps {
5454
(tokens::SCROLLBAR_THUMB_HOVER, palette::ACCENT.lighter(0.1)),
5555
// Checkbox
5656
(tokens::CHECKBOX_BG, palette::GRAY_3),
57-
(tokens::CHECKBOX_BG_CHECKED, palette::ACCENT),
57+
(tokens::CHECKBOX_BG_HOVER, palette::GRAY_3),
58+
(tokens::CHECKBOX_BG_PRESSED, palette::GRAY_3),
5859
(
5960
tokens::CHECKBOX_BG_DISABLED,
6061
palette::GRAY_1.with_alpha(0.5),
6162
),
63+
(tokens::CHECKBOX_BG_CHECKED, palette::ACCENT),
64+
(
65+
tokens::CHECKBOX_BG_CHECKED_HOVER,
66+
palette::ACCENT.lighter(0.05),
67+
),
68+
(
69+
tokens::CHECKBOX_BG_CHECKED_PRESSED,
70+
palette::ACCENT.lighter(0.1),
71+
),
6272
(
6373
tokens::CHECKBOX_BG_CHECKED_DISABLED,
64-
palette::GRAY_3.with_alpha(0.5),
74+
palette::GRAY_1.with_alpha(0.5),
6575
),
6676
(tokens::CHECKBOX_BORDER, palette::GRAY_3),
67-
(tokens::CHECKBOX_BORDER_HOVER, palette::GRAY_3.lighter(0.1)),
77+
(tokens::CHECKBOX_BORDER_HOVER, palette::GRAY_3.lighter(0.05)),
78+
(
79+
tokens::CHECKBOX_BORDER_PRESSED,
80+
palette::GRAY_3.lighter(0.1),
81+
),
6882
(
6983
tokens::CHECKBOX_BORDER_DISABLED,
7084
palette::GRAY_3.with_alpha(0.5),
7185
),
86+
(tokens::CHECKBOX_BORDER_CHECKED, palette::ACCENT),
87+
(
88+
tokens::CHECKBOX_BORDER_CHECKED_HOVER,
89+
palette::ACCENT.lighter(0.05),
90+
),
91+
(
92+
tokens::CHECKBOX_BORDER_CHECKED_PRESSED,
93+
palette::ACCENT.lighter(0.1),
94+
),
95+
(
96+
tokens::CHECKBOX_BORDER_CHECKED_DISABLED,
97+
palette::GRAY_3.with_alpha(0.5),
98+
),
7299
(tokens::CHECKBOX_MARK, palette::WHITE),
73100
(tokens::CHECKBOX_MARK_DISABLED, palette::LIGHT_GRAY_2),
74101
(tokens::CHECKBOX_TEXT, palette::LIGHT_GRAY_1),

crates/bevy_feathers/src/tokens.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,47 @@ pub const SCROLLBAR_THUMB_HOVER: ThemeToken =
9393

9494
/// Checkbox background around the checkmark
9595
pub const CHECKBOX_BG: ThemeToken = ThemeToken::new_static("feathers.checkbox.bg");
96+
/// Checkbox background around the checkmark (hovered)
97+
pub const CHECKBOX_BG_HOVER: ThemeToken = ThemeToken::new_static("feathers.checkbox.bg.hover");
98+
/// Checkbox background around the checkmark (pressed)
99+
pub const CHECKBOX_BG_PRESSED: ThemeToken = ThemeToken::new_static("feathers.checkbox.bg.pressed");
96100
/// Checkbox border around the checkmark (disabled)
97101
pub const CHECKBOX_BG_DISABLED: ThemeToken =
98102
ThemeToken::new_static("feathers.checkbox.bg.disabled");
99-
/// Checkbox background around the checkmark
103+
/// Checkbox background around the checkmark (checked)
100104
pub const CHECKBOX_BG_CHECKED: ThemeToken = ThemeToken::new_static("feathers.checkbox.bg.checked");
101-
/// Checkbox border around the checkmark (disabled)
105+
/// Checkbox background around the checkmark (checked+hover)
106+
pub const CHECKBOX_BG_CHECKED_HOVER: ThemeToken =
107+
ThemeToken::new_static("feathers.checkbox.bg.checked.hover");
108+
/// Checkbox background around the checkmark (checked+pressed)
109+
pub const CHECKBOX_BG_CHECKED_PRESSED: ThemeToken =
110+
ThemeToken::new_static("feathers.checkbox.bg.checked.pressed");
111+
/// Checkbox border around the checkmark (checked+disabled)
102112
pub const CHECKBOX_BG_CHECKED_DISABLED: ThemeToken =
103113
ThemeToken::new_static("feathers.checkbox.bg.checked.disabled");
104114
/// Checkbox border around the checkmark
105115
pub const CHECKBOX_BORDER: ThemeToken = ThemeToken::new_static("feathers.checkbox.border");
106116
/// Checkbox border around the checkmark (hovered)
107117
pub const CHECKBOX_BORDER_HOVER: ThemeToken =
108118
ThemeToken::new_static("feathers.checkbox.border.hover");
119+
/// Checkbox border around the checkmark (pressed)
120+
pub const CHECKBOX_BORDER_PRESSED: ThemeToken =
121+
ThemeToken::new_static("feathers.checkbox.border.pressed");
109122
/// Checkbox border around the checkmark (disabled)
110123
pub const CHECKBOX_BORDER_DISABLED: ThemeToken =
111124
ThemeToken::new_static("feathers.checkbox.border.disabled");
125+
/// Checkbox border around the checkmark (checked)
126+
pub const CHECKBOX_BORDER_CHECKED: ThemeToken =
127+
ThemeToken::new_static("feathers.checkbox.border.checked");
128+
/// Checkbox border around the checkmark (checked+hovered)
129+
pub const CHECKBOX_BORDER_CHECKED_HOVER: ThemeToken =
130+
ThemeToken::new_static("feathers.checkbox.border.checked.hover");
131+
/// Checkbox border around the checkmark (checked+pressed)
132+
pub const CHECKBOX_BORDER_CHECKED_PRESSED: ThemeToken =
133+
ThemeToken::new_static("feathers.checkbox.border.checked.pressed");
134+
/// Checkbox border around the checkmark (checked+disabled)
135+
pub const CHECKBOX_BORDER_CHECKED_DISABLED: ThemeToken =
136+
ThemeToken::new_static("feathers.checkbox.border.checked.disabled");
112137
/// Checkbox check mark
113138
pub const CHECKBOX_MARK: ThemeToken = ThemeToken::new_static("feathers.checkbox.mark");
114139
/// Checkbox check mark (disabled)

0 commit comments

Comments
 (0)