Skip to content

Commit 6b4a003

Browse files
authored
Disclosure toggle widget. (#23817)
# Objective Part of #19236 The "disclosure toggle" is a small checkbox-like widget which displays a chevron that toggles between pointing right and pointing down. ## Testing Manual testing
1 parent cb3cba9 commit 6b4a003

3 files changed

Lines changed: 174 additions & 6 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use bevy_app::{App, Plugin, PreUpdate};
2+
use bevy_ecs::{
3+
component::Component,
4+
hierarchy::Children,
5+
lifecycle::RemovedComponents,
6+
query::{Added, Has, Or, With},
7+
reflect::ReflectComponent,
8+
schedule::IntoScheduleConfigs,
9+
system::{Query, Res},
10+
};
11+
use bevy_input_focus::tab_navigation::TabIndex;
12+
use bevy_math::Rot2;
13+
use bevy_picking::PickingSystems;
14+
use bevy_reflect::{prelude::ReflectDefault, Reflect};
15+
use bevy_scene::{bsn, Scene};
16+
use bevy_ui::{
17+
px, widget::ImageNode, AlignItems, Checked, Display, InteractionDisabled, JustifyContent, Node,
18+
UiTransform,
19+
};
20+
use bevy_ui_widgets::Checkbox;
21+
use bevy_window::SystemCursorIcon;
22+
23+
use crate::{
24+
constants::icons, cursor::EntityCursor, display::icon, focus::FocusIndicator, theme::UiTheme,
25+
tokens,
26+
};
27+
28+
/// Marker for the disclosure toggle widget
29+
#[derive(Component, Default, Clone, Reflect)]
30+
#[reflect(Component, Clone, Default)]
31+
struct DisclosureToggleStyle;
32+
33+
/// A toggle button which shows a chevron that points either right or down, used to expand or
34+
/// collapse a panel. Functionally, this is equivalent to a checkbox, and has a [`Checked`]
35+
/// state.
36+
pub fn disclosure_toggle() -> impl Scene {
37+
bsn!(
38+
Node {
39+
width: px(12),
40+
height: px(12),
41+
display: Display::Flex,
42+
align_items: AlignItems::Center,
43+
justify_content: JustifyContent::Center,
44+
}
45+
Checkbox
46+
DisclosureToggleStyle
47+
EntityCursor::System(SystemCursorIcon::Pointer)
48+
FocusIndicator
49+
TabIndex(0)
50+
Children [
51+
:icon(icons::CHEVRON_RIGHT)
52+
]
53+
)
54+
}
55+
56+
fn update_toggle_styles(
57+
mut q_toggle: Query<
58+
(
59+
Has<InteractionDisabled>,
60+
Has<Checked>,
61+
&mut UiTransform,
62+
&Children,
63+
),
64+
(
65+
With<DisclosureToggleStyle>,
66+
Or<(Added<Checkbox>, Added<Checked>, Added<InteractionDisabled>)>,
67+
),
68+
>,
69+
mut q_icon: Query<&mut ImageNode>,
70+
theme: Res<UiTheme>,
71+
) {
72+
for (disabled, checked, mut transform, children) in q_toggle.iter_mut() {
73+
let Some(child_id) = children.first() else {
74+
continue;
75+
};
76+
let Ok(mut icon_child) = q_icon.get_mut(*child_id) else {
77+
continue;
78+
};
79+
set_toggle_styles(
80+
disabled,
81+
checked,
82+
transform.as_mut(),
83+
&mut icon_child,
84+
&theme,
85+
);
86+
}
87+
}
88+
89+
fn update_toggle_styles_remove(
90+
mut q_toggle: Query<
91+
(
92+
Has<InteractionDisabled>,
93+
Has<Checked>,
94+
&mut UiTransform,
95+
&Children,
96+
),
97+
With<DisclosureToggleStyle>,
98+
>,
99+
mut q_icon: Query<&mut ImageNode>,
100+
mut removed_disabled: RemovedComponents<InteractionDisabled>,
101+
mut removed_checked: RemovedComponents<Checked>,
102+
theme: Res<UiTheme>,
103+
) {
104+
removed_disabled
105+
.read()
106+
.chain(removed_checked.read())
107+
.for_each(|ent| {
108+
if let Ok((disabled, checked, mut transform, children)) = q_toggle.get_mut(ent) {
109+
let Some(child_id) = children.first() else {
110+
return;
111+
};
112+
let Ok(mut icon_child) = q_icon.get_mut(*child_id) else {
113+
return;
114+
};
115+
set_toggle_styles(
116+
disabled,
117+
checked,
118+
transform.as_mut(),
119+
&mut icon_child,
120+
&theme,
121+
);
122+
}
123+
});
124+
}
125+
126+
fn set_toggle_styles(
127+
disabled: bool,
128+
checked: bool,
129+
transform: &mut UiTransform,
130+
image_node: &mut ImageNode,
131+
theme: &Res<'_, UiTheme>,
132+
) {
133+
// It's effectively the same color as the caption of a "plain" variant tool button with an icon.
134+
let icon_color = match disabled {
135+
true => theme.color(&tokens::BUTTON_TEXT_DISABLED),
136+
false => theme.color(&tokens::BUTTON_TEXT),
137+
};
138+
139+
// Change icon color
140+
if image_node.color != icon_color {
141+
image_node.color = icon_color;
142+
}
143+
144+
match checked {
145+
true => {
146+
transform.rotation = Rot2::turn_fraction(0.25);
147+
}
148+
false => {
149+
transform.rotation = Rot2::turn_fraction(0.0);
150+
}
151+
};
152+
}
153+
154+
/// Plugin which registers the systems for updating the toggle switch styles.
155+
pub struct DisclosureTogglePlugin;
156+
157+
impl Plugin for DisclosureTogglePlugin {
158+
fn build(&self, app: &mut App) {
159+
app.add_systems(
160+
PreUpdate,
161+
(update_toggle_styles, update_toggle_styles_remove).in_set(PickingSystems::Last),
162+
);
163+
}
164+
}

crates/bevy_feathers/src/controls/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod checkbox;
66
mod color_plane;
77
mod color_slider;
88
mod color_swatch;
9+
mod disclosure_toggle;
910
mod menu;
1011
mod radio;
1112
mod slider;
@@ -18,6 +19,7 @@ pub use checkbox::*;
1819
pub use color_plane::*;
1920
pub use color_slider::*;
2021
pub use color_swatch::*;
22+
pub use disclosure_toggle::*;
2123
pub use menu::*;
2224
pub use radio::*;
2325
pub use slider::*;
@@ -40,6 +42,7 @@ impl Plugin for ControlsPlugin {
4042
ColorPlanePlugin,
4143
ColorSliderPlugin,
4244
ColorSwatchPlugin,
45+
DisclosureTogglePlugin,
4346
MenuPlugin,
4447
RadioPlugin,
4548
SliderPlugin,

examples/ui/widgets/feathers_gallery.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use bevy::{
99
pane_header_divider, subpane, subpane_body, subpane_header,
1010
},
1111
controls::{
12-
button, checkbox, color_plane, color_slider, color_swatch, menu, menu_button,
13-
menu_divider, menu_item, menu_popup, radio, slider, text_input, text_input_container,
14-
toggle_switch, tool_button, ButtonProps, ButtonVariant, CheckboxProps, ColorChannel,
15-
ColorPlane, ColorPlaneValue, ColorSlider, ColorSliderProps, ColorSwatch,
16-
ColorSwatchValue, MenuButtonProps, MenuItemProps, RadioProps, SliderBaseColor,
17-
SliderProps, TextInputProps,
12+
button, checkbox, color_plane, color_slider, color_swatch, disclosure_toggle, menu,
13+
menu_button, menu_divider, menu_item, menu_popup, radio, slider, text_input,
14+
text_input_container, toggle_switch, tool_button, ButtonProps, ButtonVariant,
15+
CheckboxProps, ColorChannel, ColorPlane, ColorPlaneValue, ColorSlider,
16+
ColorSliderProps, ColorSwatch, ColorSwatchValue, MenuButtonProps, MenuItemProps,
17+
RadioProps, SliderBaseColor, SliderProps, TextInputProps,
1818
},
1919
cursor::{EntityCursor, OverrideCursor},
2020
dark_theme::create_dark_theme,
@@ -406,6 +406,7 @@ fn demo_column_1() -> impl Scene {
406406
(toggle_switch() on(checkbox_self_update)),
407407
(toggle_switch() InteractionDisabled on(checkbox_self_update)),
408408
(toggle_switch() InteractionDisabled Checked on(checkbox_self_update)),
409+
(disclosure_toggle() on(checkbox_self_update)),
409410
]
410411
),
411412
(

0 commit comments

Comments
 (0)