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