Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_feathers/src/controls/menu.rs
30636 views
1
use bevy_app::{Plugin, PreUpdate};
2
use bevy_camera::visibility::Visibility;
3
use bevy_color::{Alpha, Srgba};
4
use bevy_ecs::{
5
change_detection::DetectChanges,
6
entity::Entity,
7
hierarchy::Children,
8
lifecycle::RemovedComponents,
9
observer::On,
10
query::{Added, Changed, Has, Or, With},
11
schedule::IntoScheduleConfigs,
12
system::{Commands, Query, Res, ResMut},
13
};
14
use bevy_log::warn;
15
use bevy_picking::{hover::Hovered, PickingSystems};
16
use bevy_scene::prelude::*;
17
use bevy_text::FontWeight;
18
use bevy_ui::{
19
px, AlignItems, AlignSelf, BoxShadow, Display, FlexDirection, GlobalZIndex,
20
InteractionDisabled, JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect,
21
};
22
use bevy_ui_widgets::{
23
popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
24
ActivateOnPress, MenuAction, MenuButton, MenuEvent, MenuFocusState, MenuItem, MenuPopup,
25
};
26
27
use crate::{
28
constants::{fonts, icons, size},
29
controls::{ButtonVariant, FeathersButton},
30
cursor::EntityCursor,
31
display::icon,
32
font_styles::InheritableFont,
33
rounded_corners::RoundedCorners,
34
theme::{InheritableThemeTextColor, ThemeBackgroundColor, ThemeBorderColor},
35
tokens,
36
};
37
use bevy_input_focus::{
38
tab_navigation::{NavAction, TabIndex},
39
FocusCause, InputFocus, InputFocusVisible,
40
};
41
42
/// Top-level menu container. This wraps the menu button and provides an anchor for the popover.
43
///
44
/// This is spawnable by inheriting it as a "scene component".
45
#[derive(SceneComponent, Clone, Default)]
46
pub struct FeathersMenu;
47
48
impl FeathersMenu {
49
fn scene() -> impl Scene {
50
bsn! {
51
Node {
52
height: size::ROW_HEIGHT,
53
justify_content: JustifyContent::Stretch,
54
align_items: AlignItems::Stretch,
55
}
56
FeathersMenu
57
on(on_menu_event)
58
}
59
}
60
}
61
62
fn on_menu_event(
63
mut ev: On<MenuEvent>,
64
q_menu_children: Query<&Children>,
65
q_popovers: Query<&mut Visibility, With<FeathersMenuPopup>>,
66
q_buttons: Query<(), With<FeathersMenuButton>>,
67
mut commands: Commands,
68
mut focus: ResMut<InputFocus>,
69
) {
70
match ev.event().action {
71
MenuAction::Open(nav) => {
72
let Ok(children) = q_menu_children.get(ev.source) else {
73
return;
74
};
75
ev.propagate(false);
76
for child in children.iter() {
77
if q_popovers.contains(*child) {
78
commands
79
.entity(*child)
80
.insert((Visibility::Visible, MenuFocusState::Opening(nav)));
81
return;
82
}
83
}
84
warn!("Menu popup not found");
85
}
86
MenuAction::Toggle => {
87
let Ok(children) = q_menu_children.get(ev.source) else {
88
return;
89
};
90
for child in children.iter() {
91
if let Ok(visibility) = q_popovers.get(*child) {
92
ev.propagate(false);
93
if visibility == Visibility::Visible {
94
commands.entity(*child).insert(Visibility::Hidden);
95
} else {
96
commands.entity(*child).insert((
97
Visibility::Visible,
98
MenuFocusState::Opening(NavAction::First),
99
));
100
}
101
return;
102
}
103
}
104
warn!("Menu popup not found");
105
}
106
MenuAction::CloseAll => {
107
let Ok(children) = q_menu_children.get(ev.source) else {
108
return;
109
};
110
for child in children.iter() {
111
if q_popovers.contains(*child) {
112
ev.propagate(false);
113
commands.entity(*child).insert(Visibility::Hidden);
114
}
115
}
116
}
117
MenuAction::FocusRoot => {
118
let Ok(children) = q_menu_children.get(ev.source) else {
119
return;
120
};
121
for child in children.iter() {
122
if q_buttons.contains(*child) {
123
ev.propagate(false);
124
focus.set(*child, FocusCause::Navigated);
125
break;
126
}
127
}
128
}
129
}
130
}
131
132
/// A menu button widget. This produces a button that has a dropdown arrow.
133
///
134
/// This is spawnable by inheriting it as a "scene component" with optional [`FeathersMenuButtonProps`].
135
#[derive(SceneComponent, Default, Clone)]
136
#[scene(FeathersMenuButtonProps)]
137
pub struct FeathersMenuButton;
138
139
/// Props used to construct a [`FeathersMenuButton`] scene.
140
pub struct FeathersMenuButtonProps {
141
/// Label for this menu button
142
pub caption: Box<dyn SceneList>,
143
/// Rounded corners options
144
pub corners: RoundedCorners,
145
/// Include the standard downward-pointing chevron (default true).
146
pub arrow: bool,
147
}
148
149
impl Default for FeathersMenuButtonProps {
150
fn default() -> Self {
151
Self {
152
caption: Box::new(bsn_list!()),
153
corners: Default::default(),
154
arrow: true,
155
}
156
}
157
}
158
impl FeathersMenuButton {
159
fn scene(props: FeathersMenuButtonProps) -> impl Scene {
160
bsn! {
161
@FeathersButton {
162
@caption: {props.caption},
163
@variant: ButtonVariant::Normal,
164
@corners: {props.corners},
165
}
166
ActivateOnPress
167
MenuButton
168
FeathersMenuButton
169
// Additional children for menu chevron
170
Children [
171
{
172
if props.arrow {
173
Box::new(bsn_list!(
174
Node {
175
flex_grow: 1.0,
176
},
177
icon(icons::CHEVRON_DOWN),
178
)) as Box<dyn SceneList>
179
} else {
180
Box::new(bsn_list!()) as Box<dyn SceneList>
181
}
182
}
183
]
184
}
185
}
186
}
187
188
/// A menu popup widget.
189
#[derive(SceneComponent, Default, Clone)]
190
pub struct FeathersMenuPopup;
191
192
impl FeathersMenuPopup {
193
fn scene() -> impl Scene {
194
bsn! {
195
Node {
196
position_type: PositionType::Absolute,
197
display: Display::Flex,
198
flex_direction: FlexDirection::Column,
199
justify_content: JustifyContent::Stretch,
200
align_items: AlignItems::Stretch,
201
border: px(1),
202
padding: UiRect::axes(px(0), px(4)),
203
border_radius: {RoundedCorners::All.to_border_radius(4.0)},
204
}
205
FeathersMenuPopup
206
MenuPopup
207
Visibility::Hidden
208
ThemeBackgroundColor(tokens::MENU_BG)
209
ThemeBorderColor(tokens::MENU_BORDER)
210
BoxShadow::new(
211
Srgba::BLACK.with_alpha(0.9).into(),
212
px(0),
213
px(0),
214
px(1),
215
px(4),
216
)
217
GlobalZIndex(100)
218
Popover {
219
positions: {vec![
220
PopoverPlacement {
221
side: PopoverSide::Bottom,
222
align: PopoverAlign::Start,
223
gap: 2.0,
224
},
225
PopoverPlacement {
226
side: PopoverSide::Top,
227
align: PopoverAlign::Start,
228
gap: 2.0,
229
},
230
]},
231
window_margin: 10.0,
232
}
233
OverrideClip
234
}
235
}
236
}
237
238
/// A menu item widget.
239
///
240
/// This is spawnable by inheriting it as a "scene component" with optional [`FeathersMenuItemProps`].
241
#[derive(SceneComponent, Default, Clone)]
242
#[scene(FeathersMenuItemProps)]
243
pub struct FeathersMenuItem;
244
245
/// Props used to construct a [`FeathersMenuItem`] scene.
246
pub struct FeathersMenuItemProps {
247
/// Label for this menu item
248
pub caption: Box<dyn SceneList>,
249
}
250
251
impl Default for FeathersMenuItemProps {
252
fn default() -> Self {
253
Self {
254
caption: Box::new(bsn_list!()),
255
}
256
}
257
}
258
259
impl FeathersMenuItem {
260
fn scene(props: FeathersMenuItemProps) -> impl Scene {
261
bsn! {
262
Node {
263
height: size::ROW_HEIGHT,
264
min_width: size::ROW_HEIGHT,
265
justify_content: JustifyContent::Start,
266
align_items: AlignItems::Center,
267
padding: UiRect::horizontal(px(8)),
268
}
269
FeathersMenuItem
270
MenuItem
271
Hovered
272
EntityCursor::System(bevy_window::SystemCursorIcon::Pointer)
273
TabIndex(0)
274
ThemeBackgroundColor(tokens::MENU_BG) // Same as menu
275
InheritableThemeTextColor(tokens::MENUITEM_TEXT)
276
InheritableFont {
277
font: fonts::REGULAR,
278
font_size: size::MEDIUM_FONT,
279
weight: FontWeight::NORMAL,
280
}
281
Children [
282
{props.caption}
283
]
284
}
285
}
286
}
287
288
fn update_menuitem_styles(
289
q_menuitems: Query<
290
(
291
Entity,
292
Has<InteractionDisabled>,
293
Has<Pressed>,
294
&Hovered,
295
&ThemeBackgroundColor,
296
&InheritableThemeTextColor,
297
),
298
(
299
With<FeathersMenuItem>,
300
Or<(Changed<Hovered>, Added<Pressed>, Added<InteractionDisabled>)>,
301
),
302
>,
303
mut commands: Commands,
304
focus: Res<InputFocus>,
305
focus_visible: Res<InputFocusVisible>,
306
) {
307
for (item_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() {
308
set_menuitem_colors(
309
item_ent,
310
disabled,
311
pressed,
312
hovered.0,
313
Some(item_ent) == focus.get() && focus_visible.0,
314
bg_color,
315
font_color,
316
&mut commands,
317
);
318
}
319
}
320
321
fn update_menuitem_styles_remove(
322
q_menuitems: Query<
323
(
324
Entity,
325
Has<InteractionDisabled>,
326
Has<Pressed>,
327
&Hovered,
328
&ThemeBackgroundColor,
329
&InheritableThemeTextColor,
330
),
331
With<FeathersMenuItem>,
332
>,
333
mut removed_disabled: RemovedComponents<InteractionDisabled>,
334
mut removed_pressed: RemovedComponents<Pressed>,
335
focus: Res<InputFocus>,
336
focus_visible: Res<InputFocusVisible>,
337
mut commands: Commands,
338
) {
339
removed_disabled
340
.read()
341
.chain(removed_pressed.read())
342
.for_each(|ent| {
343
if let Ok((item_ent, disabled, pressed, hovered, bg_color, font_color)) =
344
q_menuitems.get(ent)
345
{
346
set_menuitem_colors(
347
item_ent,
348
disabled,
349
pressed,
350
hovered.0,
351
Some(item_ent) == focus.get() && focus_visible.0,
352
bg_color,
353
font_color,
354
&mut commands,
355
);
356
}
357
});
358
}
359
360
fn update_menuitem_styles_focus_changed(
361
q_menuitems: Query<
362
(
363
Entity,
364
Has<InteractionDisabled>,
365
Has<Pressed>,
366
&Hovered,
367
&ThemeBackgroundColor,
368
&InheritableThemeTextColor,
369
),
370
With<FeathersMenuItem>,
371
>,
372
focus: Res<InputFocus>,
373
focus_visible: Res<InputFocusVisible>,
374
mut commands: Commands,
375
) {
376
if focus.is_changed() || focus_visible.is_changed() {
377
for (item_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() {
378
set_menuitem_colors(
379
item_ent,
380
disabled,
381
pressed,
382
hovered.0,
383
Some(item_ent) == focus.get() && focus_visible.0,
384
bg_color,
385
font_color,
386
&mut commands,
387
);
388
}
389
}
390
}
391
392
fn set_menuitem_colors(
393
button_ent: Entity,
394
disabled: bool,
395
pressed: bool,
396
hovered: bool,
397
focused: bool,
398
bg_color: &ThemeBackgroundColor,
399
font_color: &InheritableThemeTextColor,
400
commands: &mut Commands,
401
) {
402
let bg_token = match (focused, pressed, hovered) {
403
(true, _, _) => tokens::MENUITEM_BG_FOCUSED,
404
(false, true, _) => tokens::MENUITEM_BG_PRESSED,
405
(false, false, true) => tokens::MENUITEM_BG_HOVER,
406
(false, false, false) => tokens::MENU_BG,
407
};
408
409
let font_color_token = match disabled {
410
true => tokens::MENUITEM_TEXT_DISABLED,
411
false => tokens::MENUITEM_TEXT,
412
};
413
414
// Change background color
415
if bg_color.0 != bg_token {
416
commands
417
.entity(button_ent)
418
.insert(ThemeBackgroundColor(bg_token));
419
}
420
421
// Change font color
422
if font_color.0 != font_color_token {
423
commands
424
.entity(button_ent)
425
.insert(InheritableThemeTextColor(font_color_token));
426
}
427
}
428
429
/// A decorative divider between menu items
430
#[derive(SceneComponent, Default, Clone)]
431
pub struct FeathersMenuDivider;
432
433
impl FeathersMenuDivider {
434
fn scene() -> impl Scene {
435
bsn! {
436
Node {
437
height: px(1),
438
justify_content: JustifyContent::Start,
439
align_self: AlignSelf::Stretch,
440
margin: UiRect::vertical(px(2)),
441
}
442
ThemeBackgroundColor(tokens::MENU_BORDER) // Same as menu
443
}
444
}
445
}
446
447
/// Plugin which registers the systems for updating the menu and menu button styles.
448
pub struct MenuPlugin;
449
450
impl Plugin for MenuPlugin {
451
fn build(&self, app: &mut bevy_app::App) {
452
app.add_systems(
453
PreUpdate,
454
(
455
update_menuitem_styles,
456
update_menuitem_styles_remove,
457
update_menuitem_styles_focus_changed,
458
)
459
.in_set(PickingSystems::Last),
460
);
461
}
462
}
463
464