Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_feathers/src/controls/radio.rs
9413 views
1
use bevy_app::{Plugin, PreUpdate};
2
use bevy_camera::visibility::Visibility;
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
reflect::ReflectComponent,
12
schedule::IntoScheduleConfigs,
13
spawn::{Spawn, SpawnRelated, SpawnableList},
14
system::{Commands, Query},
15
};
16
use bevy_input_focus::tab_navigation::TabIndex;
17
use bevy_picking::{hover::Hovered, PickingSystems};
18
use bevy_reflect::{prelude::ReflectDefault, Reflect};
19
use bevy_text::FontSize;
20
use bevy_ui::{
21
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
22
Node, UiRect, Val,
23
};
24
use bevy_ui_widgets::RadioButton;
25
26
use crate::{
27
constants::{fonts, size},
28
cursor::EntityCursor,
29
font_styles::InheritableFont,
30
handle_or_path::HandleOrPath,
31
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
32
tokens,
33
};
34
35
/// Marker for the radio outline
36
#[derive(Component, Default, Clone, Reflect)]
37
#[reflect(Component, Clone, Default)]
38
struct RadioOutline;
39
40
/// Marker for the radio check mark
41
#[derive(Component, Default, Clone, Reflect)]
42
#[reflect(Component, Clone, Default)]
43
struct RadioMark;
44
45
/// Template function to spawn a radio.
46
///
47
/// # Arguments
48
/// * `props` - construction properties for the radio.
49
/// * `overrides` - a bundle of components that are merged in with the normal radio components.
50
/// * `label` - the label of the radio.
51
///
52
/// # Emitted events
53
/// * [`bevy_ui_widgets::ValueChange<bool>`] with the value true when it becomes checked.
54
/// * [`bevy_ui_widgets::ValueChange<Entity>`] with the selected entity's id when a new radio button is selected.
55
///
56
/// These events can be disabled by adding an [`bevy_ui::InteractionDisabled`] component to the entity
57
pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
58
overrides: B,
59
label: C,
60
) -> impl Bundle {
61
(
62
Node {
63
display: Display::Flex,
64
flex_direction: FlexDirection::Row,
65
justify_content: JustifyContent::Start,
66
align_items: AlignItems::Center,
67
column_gap: Val::Px(4.0),
68
..Default::default()
69
},
70
RadioButton,
71
Hovered::default(),
72
EntityCursor::System(bevy_window::SystemCursorIcon::Pointer),
73
TabIndex(0),
74
ThemeFontColor(tokens::RADIO_TEXT),
75
InheritableFont {
76
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
77
font_size: FontSize::Px(14.0),
78
},
79
overrides,
80
Children::spawn((
81
Spawn((
82
Node {
83
display: Display::Flex,
84
align_items: AlignItems::Center,
85
justify_content: JustifyContent::Center,
86
width: size::RADIO_SIZE,
87
height: size::RADIO_SIZE,
88
border: UiRect::all(Val::Px(2.0)),
89
border_radius: BorderRadius::MAX,
90
..Default::default()
91
},
92
RadioOutline,
93
ThemeBorderColor(tokens::RADIO_BORDER),
94
children![(
95
// Cheesy checkmark: rotated node with L-shaped border.
96
Node {
97
width: Val::Px(8.),
98
height: Val::Px(8.),
99
border_radius: BorderRadius::MAX,
100
..Default::default()
101
},
102
RadioMark,
103
ThemeBackgroundColor(tokens::RADIO_MARK),
104
)],
105
)),
106
label,
107
)),
108
)
109
}
110
111
fn update_radio_styles(
112
q_radioes: Query<
113
(
114
Entity,
115
Has<InteractionDisabled>,
116
Has<Checked>,
117
&Hovered,
118
&ThemeFontColor,
119
),
120
(
121
With<RadioButton>,
122
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
123
),
124
>,
125
q_children: Query<&Children>,
126
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
127
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
128
mut commands: Commands,
129
) {
130
for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() {
131
let Some(outline_ent) = q_children
132
.iter_descendants(radio_ent)
133
.find(|en| q_outline.contains(*en))
134
else {
135
continue;
136
};
137
let Some(mark_ent) = q_children
138
.iter_descendants(radio_ent)
139
.find(|en| q_mark.contains(*en))
140
else {
141
continue;
142
};
143
let outline_border = q_outline.get_mut(outline_ent).unwrap();
144
let mark_color = q_mark.get_mut(mark_ent).unwrap();
145
set_radio_styles(
146
radio_ent,
147
outline_ent,
148
mark_ent,
149
disabled,
150
checked,
151
hovered.0,
152
outline_border,
153
mark_color,
154
font_color,
155
&mut commands,
156
);
157
}
158
}
159
160
fn update_radio_styles_remove(
161
q_radioes: Query<
162
(
163
Entity,
164
Has<InteractionDisabled>,
165
Has<Checked>,
166
&Hovered,
167
&ThemeFontColor,
168
),
169
With<RadioButton>,
170
>,
171
q_children: Query<&Children>,
172
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
173
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
174
mut removed_disabled: RemovedComponents<InteractionDisabled>,
175
mut removed_checked: RemovedComponents<Checked>,
176
mut commands: Commands,
177
) {
178
removed_disabled
179
.read()
180
.chain(removed_checked.read())
181
.for_each(|ent| {
182
if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) {
183
let Some(outline_ent) = q_children
184
.iter_descendants(radio_ent)
185
.find(|en| q_outline.contains(*en))
186
else {
187
return;
188
};
189
let Some(mark_ent) = q_children
190
.iter_descendants(radio_ent)
191
.find(|en| q_mark.contains(*en))
192
else {
193
return;
194
};
195
let outline_border = q_outline.get_mut(outline_ent).unwrap();
196
let mark_color = q_mark.get_mut(mark_ent).unwrap();
197
set_radio_styles(
198
radio_ent,
199
outline_ent,
200
mark_ent,
201
disabled,
202
checked,
203
hovered.0,
204
outline_border,
205
mark_color,
206
font_color,
207
&mut commands,
208
);
209
}
210
});
211
}
212
213
fn set_radio_styles(
214
radio_ent: Entity,
215
outline_ent: Entity,
216
mark_ent: Entity,
217
disabled: bool,
218
checked: bool,
219
hovered: bool,
220
outline_border: &ThemeBorderColor,
221
mark_color: &ThemeBackgroundColor,
222
font_color: &ThemeFontColor,
223
commands: &mut Commands,
224
) {
225
let outline_border_token = match (disabled, hovered) {
226
(true, _) => tokens::RADIO_BORDER_DISABLED,
227
(false, true) => tokens::RADIO_BORDER_HOVER,
228
_ => tokens::RADIO_BORDER,
229
};
230
231
let mark_token = match disabled {
232
true => tokens::RADIO_MARK_DISABLED,
233
false => tokens::RADIO_MARK,
234
};
235
236
let font_color_token = match disabled {
237
true => tokens::RADIO_TEXT_DISABLED,
238
false => tokens::RADIO_TEXT,
239
};
240
241
let cursor_shape = match disabled {
242
true => bevy_window::SystemCursorIcon::NotAllowed,
243
false => bevy_window::SystemCursorIcon::Pointer,
244
};
245
246
// Change outline border
247
if outline_border.0 != outline_border_token {
248
commands
249
.entity(outline_ent)
250
.insert(ThemeBorderColor(outline_border_token));
251
}
252
253
// Change mark color
254
if mark_color.0 != mark_token {
255
commands
256
.entity(mark_ent)
257
.insert(ThemeBorderColor(mark_token));
258
}
259
260
// Change mark visibility
261
commands.entity(mark_ent).insert(match checked {
262
true => Visibility::Inherited,
263
false => Visibility::Hidden,
264
});
265
266
// Change font color
267
if font_color.0 != font_color_token {
268
commands
269
.entity(radio_ent)
270
.insert(ThemeFontColor(font_color_token));
271
}
272
273
// Change cursor shape
274
commands
275
.entity(radio_ent)
276
.insert(EntityCursor::System(cursor_shape));
277
}
278
279
/// Plugin which registers the systems for updating the radio styles.
280
pub struct RadioPlugin;
281
282
impl Plugin for RadioPlugin {
283
fn build(&self, app: &mut bevy_app::App) {
284
app.add_systems(
285
PreUpdate,
286
(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last),
287
);
288
}
289
}
290
291