Skip to content

Commit 6c867fa

Browse files
authored
Fix and Polish Feathers Toggle Switch (#23830)
# Objective This is a continuation of #23820 for toggle switches to fix their clicking behavior and add more style tokens. Again, adding the `ActivateOnPress` component will change its state instantly on mouse-press rather than mouse-release. Styling is also changed for visibility and disambiguation. --- ## Showcase ### Before <img width="120" height="31" alt="Screenshot 2026-04-16 at 1 53 12 PM" src="https://github.com/user-attachments/assets/22fd0374-76cb-40bb-9428-cbd7c69f2e50" /> ### After <img width="159" height="31" alt="Screenshot 2026-04-16 at 3 32 21 PM" src="https://github.com/user-attachments/assets/ecb5bee6-0dbd-4598-8487-cdb333575f78" /> (Not pictured: new intermediate styling for mouse press)
1 parent 9c27c26 commit 6c867fa

4 files changed

Lines changed: 326 additions & 47 deletions

File tree

crates/bevy_feathers/src/controls/toggle_switch.rs

Lines changed: 161 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ use bevy_input_focus::tab_navigation::TabIndex;
1818
use bevy_picking::{hover::Hovered, PickingSystems};
1919
use bevy_reflect::{prelude::ReflectDefault, Reflect};
2020
use bevy_scene::prelude::*;
21-
use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val};
22-
use bevy_ui_widgets::Checkbox;
21+
use bevy_ui::{
22+
BorderRadius, Checked, InteractionDisabled, Node, PositionType, Pressed, UiRect, Val,
23+
};
24+
use bevy_ui_widgets::{ActivateOnPress, Checkbox};
2325

2426
use crate::{
2527
constants::size,
@@ -69,10 +71,12 @@ pub fn toggle_switch() -> impl Scene {
6971
top: Val::Px(0.),
7072
bottom: Val::Px(0.),
7173
width: Val::Percent(50.),
74+
border: UiRect::all(Val::Px(2.0)),
7275
border_radius: BorderRadius::all(Val::Px(3.0)),
7376
}
7477
ToggleSwitchSlide
75-
ThemeBackgroundColor(tokens::SWITCH_SLIDE)
78+
ThemeBackgroundColor(tokens::SWITCH_SLIDE_BG)
79+
ThemeBorderColor(tokens::SWITCH_SLIDE_BORDER)
7680
)]
7781
}
7882
}
@@ -114,11 +118,13 @@ pub fn toggle_switch_bundle<B: Bundle>(overrides: B) -> impl Bundle {
114118
top: Val::Px(0.),
115119
bottom: Val::Px(0.),
116120
width: Val::Percent(50.),
121+
border: UiRect::all(Val::Px(2.0)),
117122
border_radius: BorderRadius::all(Val::Px(3.0)),
118123
..Default::default()
119124
},
120125
ToggleSwitchSlide,
121-
ThemeBackgroundColor(tokens::SWITCH_SLIDE),
126+
ThemeBackgroundColor(tokens::SWITCH_SLIDE_BG),
127+
ThemeBorderColor(tokens::SWITCH_SLIDE_BORDER)
122128
)],
123129
)
124130
}
@@ -129,38 +135,62 @@ fn update_switch_styles(
129135
Entity,
130136
Has<InteractionDisabled>,
131137
Has<Checked>,
138+
Has<Pressed>,
139+
Has<ActivateOnPress>,
132140
&Hovered,
133141
&ThemeBackgroundColor,
134142
&ThemeBorderColor,
135143
),
136144
(
137145
With<ToggleSwitchOutline>,
138-
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
146+
Or<(
147+
Changed<Hovered>,
148+
Added<Checked>,
149+
Added<Pressed>,
150+
Added<InteractionDisabled>,
151+
)>,
139152
),
140153
>,
141154
q_children: Query<&Children>,
142-
mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,
155+
mut q_slide: Query<
156+
(&mut Node, &ThemeBackgroundColor, &ThemeBorderColor),
157+
With<ToggleSwitchSlide>,
158+
>,
143159
mut commands: Commands,
144160
) {
145-
for (switch_ent, disabled, checked, hovered, outline_bg, outline_border) in q_switches.iter() {
161+
for (
162+
switch_ent,
163+
disabled,
164+
checked,
165+
pressed,
166+
activate_on_press,
167+
hovered,
168+
outline_bg,
169+
outline_border,
170+
) in q_switches.iter()
171+
{
146172
let Some(slide_ent) = q_children
147173
.iter_descendants(switch_ent)
148174
.find(|en| q_slide.contains(*en))
149175
else {
150176
continue;
151177
};
152178
// Safety: since we just checked the query, should always work.
153-
let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();
179+
let (ref mut slide_style, slide_bg_color, slide_border_color) =
180+
q_slide.get_mut(slide_ent).unwrap();
154181
set_switch_styles(
155182
switch_ent,
156183
slide_ent,
157184
disabled,
158185
checked,
186+
pressed,
159187
hovered.0,
188+
activate_on_press,
160189
outline_bg,
161190
outline_border,
162191
slide_style,
163-
slide_color,
192+
slide_bg_color,
193+
slide_border_color,
164194
&mut commands,
165195
);
166196
}
@@ -172,24 +202,41 @@ fn update_switch_styles_remove(
172202
Entity,
173203
Has<InteractionDisabled>,
174204
Has<Checked>,
205+
Has<Pressed>,
206+
Has<ActivateOnPress>,
175207
&Hovered,
176208
&ThemeBackgroundColor,
177209
&ThemeBorderColor,
178210
),
179211
With<ToggleSwitchOutline>,
180212
>,
181213
q_children: Query<&Children>,
182-
mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With<ToggleSwitchSlide>>,
214+
mut q_slide: Query<
215+
(&mut Node, &ThemeBackgroundColor, &ThemeBorderColor),
216+
With<ToggleSwitchSlide>,
217+
>,
183218
mut removed_disabled: RemovedComponents<InteractionDisabled>,
184219
mut removed_checked: RemovedComponents<Checked>,
220+
mut remove_pressed: RemovedComponents<Pressed>,
221+
mut remove_activate_on_press: RemovedComponents<ActivateOnPress>,
185222
mut commands: Commands,
186223
) {
187224
removed_disabled
188225
.read()
189226
.chain(removed_checked.read())
227+
.chain(remove_pressed.read())
228+
.chain(remove_activate_on_press.read())
190229
.for_each(|ent| {
191-
if let Ok((switch_ent, disabled, checked, hovered, outline_bg, outline_border)) =
192-
q_switches.get(ent)
230+
if let Ok((
231+
switch_ent,
232+
disabled,
233+
checked,
234+
pressed,
235+
activate_on_press,
236+
hovered,
237+
outline_bg,
238+
outline_border,
239+
)) = q_switches.get(ent)
193240
{
194241
let Some(slide_ent) = q_children
195242
.iter_descendants(switch_ent)
@@ -198,17 +245,21 @@ fn update_switch_styles_remove(
198245
return;
199246
};
200247
// Safety: since we just checked the query, should always work.
201-
let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap();
248+
let (ref mut slide_style, slide_bg_color, slide_border_color) =
249+
q_slide.get_mut(slide_ent).unwrap();
202250
set_switch_styles(
203251
switch_ent,
204252
slide_ent,
205253
disabled,
206254
checked,
255+
pressed,
207256
hovered.0,
257+
activate_on_press,
208258
outline_bg,
209259
outline_border,
210260
slide_style,
211-
slide_color,
261+
slide_bg_color,
262+
slide_border_color,
212263
&mut commands,
213264
);
214265
}
@@ -220,29 +271,102 @@ fn set_switch_styles(
220271
slide_ent: Entity,
221272
disabled: bool,
222273
checked: bool,
274+
pressed: bool,
223275
hovered: bool,
276+
activate_on_press: bool,
224277
outline_bg: &ThemeBackgroundColor,
225278
outline_border: &ThemeBorderColor,
226279
slide_style: &mut Mut<Node>,
227-
slide_color: &ThemeBackgroundColor,
280+
slide_bg_color: &ThemeBackgroundColor,
281+
slide_border_color: &ThemeBorderColor,
228282
commands: &mut Commands,
229283
) {
230-
let outline_border_token = match (disabled, hovered) {
231-
(true, _) => tokens::SWITCH_BORDER_DISABLED,
232-
(false, true) => tokens::SWITCH_BORDER_HOVER,
233-
_ => tokens::SWITCH_BORDER,
284+
let outline_border_token = if checked {
285+
if disabled {
286+
tokens::SWITCH_BORDER_CHECKED_DISABLED
287+
} else if pressed && !activate_on_press {
288+
tokens::SWITCH_BORDER_CHECKED_PRESSED
289+
} else if hovered {
290+
tokens::SWITCH_BORDER_CHECKED_HOVER
291+
} else {
292+
tokens::SWITCH_BORDER_CHECKED
293+
}
294+
} else {
295+
if disabled {
296+
tokens::SWITCH_BORDER_DISABLED
297+
} else if pressed && !activate_on_press {
298+
tokens::SWITCH_BORDER_PRESSED
299+
} else if hovered {
300+
tokens::SWITCH_BORDER_HOVER
301+
} else {
302+
tokens::SWITCH_BORDER
303+
}
234304
};
235305

236-
let outline_bg_token = match (disabled, checked) {
237-
(true, true) => tokens::SWITCH_BG_CHECKED_DISABLED,
238-
(true, false) => tokens::SWITCH_BG_DISABLED,
239-
(false, true) => tokens::SWITCH_BG_CHECKED,
240-
(false, false) => tokens::SWITCH_BG,
306+
let outline_bg_token = if checked {
307+
if disabled {
308+
tokens::SWITCH_BG_CHECKED_DISABLED
309+
} else if pressed && !activate_on_press {
310+
tokens::SWITCH_BG_CHECKED_PRESSED
311+
} else if hovered {
312+
tokens::SWITCH_BG_CHECKED_HOVER
313+
} else {
314+
tokens::SWITCH_BG_CHECKED
315+
}
316+
} else {
317+
if disabled {
318+
tokens::SWITCH_BG_DISABLED
319+
} else if pressed && !activate_on_press {
320+
tokens::SWITCH_BG_PRESSED
321+
} else if hovered {
322+
tokens::SWITCH_BG_HOVER
323+
} else {
324+
tokens::SWITCH_BG
325+
}
241326
};
242327

243-
let slide_token = match disabled {
244-
true => tokens::SWITCH_SLIDE_DISABLED,
245-
false => tokens::SWITCH_SLIDE,
328+
let slide_border_token = if checked {
329+
if disabled {
330+
tokens::SWITCH_SLIDE_BORDER_CHECKED_DISABLED
331+
} else if pressed && !activate_on_press {
332+
tokens::SWITCH_SLIDE_BORDER_CHECKED_PRESSED
333+
} else if hovered {
334+
tokens::SWITCH_SLIDE_BORDER_CHECKED_HOVER
335+
} else {
336+
tokens::SWITCH_SLIDE_BORDER_CHECKED
337+
}
338+
} else {
339+
if disabled {
340+
tokens::SWITCH_SLIDE_BORDER_DISABLED
341+
} else if pressed && !activate_on_press {
342+
tokens::SWITCH_SLIDE_BORDER_PRESSED
343+
} else if hovered {
344+
tokens::SWITCH_SLIDE_BORDER_HOVER
345+
} else {
346+
tokens::SWITCH_SLIDE_BORDER
347+
}
348+
};
349+
350+
let slide_bg_token = if checked {
351+
if disabled {
352+
tokens::SWITCH_SLIDE_BG_CHECKED_DISABLED
353+
} else if pressed && !activate_on_press {
354+
tokens::SWITCH_SLIDE_BG_CHECKED_PRESSED
355+
} else if hovered {
356+
tokens::SWITCH_SLIDE_BG_CHECKED_HOVER
357+
} else {
358+
tokens::SWITCH_SLIDE_BG_CHECKED
359+
}
360+
} else {
361+
if disabled {
362+
tokens::SWITCH_SLIDE_BG_DISABLED
363+
} else if pressed && !activate_on_press {
364+
tokens::SWITCH_SLIDE_BG_PRESSED
365+
} else if hovered {
366+
tokens::SWITCH_SLIDE_BG_HOVER
367+
} else {
368+
tokens::SWITCH_SLIDE_BG
369+
}
246370
};
247371

248372
let slide_pos = match checked {
@@ -269,11 +393,18 @@ fn set_switch_styles(
269393
.insert(ThemeBorderColor(outline_border_token));
270394
}
271395

272-
// Change slide color
273-
if slide_color.0 != slide_token {
396+
// Change slide background color
397+
if slide_bg_color.0 != slide_bg_token {
398+
commands
399+
.entity(slide_ent)
400+
.insert(ThemeBackgroundColor(slide_bg_token));
401+
}
402+
403+
// Change slide border color
404+
if slide_border_color.0 != slide_border_token {
274405
commands
275406
.entity(slide_ent)
276-
.insert(ThemeBackgroundColor(slide_token));
407+
.insert(ThemeBorderColor(slide_border_token));
277408
}
278409

279410
// Change slide position

0 commit comments

Comments
 (0)