Skip to content

Commit a9b985b

Browse files
viridiacart
andauthored
Porting feathers menu to latest bsn branch. (#47)
* Porting feathers menu to latest bsn branch. * Remove unnecessary template_value --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
1 parent 4940d1c commit a9b985b

File tree

19 files changed

+1139
-10
lines changed

19 files changed

+1139
-10
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The names of files in this directory should refer to what the icons look like ("x", "chevron", etc.)
2+
rather than their assigned meanings ("close", "expand") because the latter can change.
209 Bytes
Loading
206 Bytes
Loading
255 Bytes
Loading

crates/bevy_feathers/src/constants.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ pub mod fonts {
1414
pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf";
1515
}
1616

17+
/// Icon paths
18+
pub mod icons {
19+
/// Downward-pointing chevron
20+
pub const CHEVRON_DOWN: &str = "embedded://bevy_feathers/assets/icons/chevron-down.png";
21+
/// Right-pointing chevron
22+
pub const CHEVRON_RIGHT: &str = "embedded://bevy_feathers/assets/icons/chevron-right.png";
23+
/// Diagonal Cross
24+
pub const X: &str = "embedded://bevy_feathers/assets/icons/x.png";
25+
}
26+
1727
/// Size constants
1828
pub mod size {
1929
use bevy_ui::Val;
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
use alloc::sync::Arc;
2+
3+
use bevy_app::{Plugin, PreUpdate};
4+
use bevy_camera::visibility::Visibility;
5+
use bevy_color::{Alpha, Srgba};
6+
use bevy_ecs::{
7+
component::Component,
8+
entity::Entity,
9+
hierarchy::Children,
10+
lifecycle::RemovedComponents,
11+
observer::On,
12+
query::{Added, Changed, Has, Or, With},
13+
schedule::IntoScheduleConfigs,
14+
system::{Commands, EntityCommands, Query},
15+
};
16+
use bevy_log::info;
17+
use bevy_picking::{
18+
events::{Click, Pointer},
19+
hover::Hovered,
20+
PickingSystems,
21+
};
22+
use bevy_scene2::{prelude::*, template_value};
23+
use bevy_ui::{
24+
AlignItems, BoxShadow, Display, FlexDirection, GlobalZIndex, InteractionDisabled,
25+
JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val,
26+
};
27+
use bevy_ui_widgets::{
28+
popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
29+
MenuAction, MenuEvent, MenuItem, MenuPopup,
30+
};
31+
32+
use crate::{
33+
constants::{fonts, icons, size},
34+
controls::{button, ButtonProps, ButtonVariant},
35+
font_styles::InheritableFont,
36+
icon,
37+
rounded_corners::RoundedCorners,
38+
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
39+
tokens,
40+
};
41+
use bevy_input_focus::tab_navigation::TabIndex;
42+
43+
/// Parameters for the menu button template, passed to [`menu_button`] function.
44+
#[derive(Default)]
45+
pub struct MenuButtonProps {
46+
/// Rounded corners options
47+
pub corners: RoundedCorners,
48+
}
49+
50+
/// Marker for menu items
51+
#[derive(Component, Default, Clone)]
52+
struct MenuItemStyle;
53+
54+
/// Marker for menu popup
55+
#[derive(Component, Default, Clone)]
56+
struct MenuPopupStyle;
57+
58+
/// Marker for menu wrapper
59+
#[derive(Component, Clone, Default)]
60+
struct Menu(Option<Arc<dyn Fn(EntityCommands) + 'static + Send + Sync>>);
61+
62+
/// Menu scene function. This wraps the menu button and provides an anchor for the popopver.
63+
pub fn menu<F: Fn(EntityCommands) + 'static + Send + Sync>(spawn_popover: F) -> impl Scene {
64+
let menu = Menu(Some(Arc::new(spawn_popover)));
65+
bsn! {
66+
Node {
67+
height: size::ROW_HEIGHT,
68+
justify_content: JustifyContent::Stretch,
69+
align_items: AlignItems::Stretch,
70+
}
71+
template_value(menu)
72+
on(|
73+
ev: On<MenuEvent>,
74+
q_menu: Query<(&Menu, &Children)>,
75+
q_popovers: Query<Entity, With<MenuPopupStyle>>,
76+
// mut redraw_events: MessageWriter<RequestRedraw>,
77+
mut commands: Commands| {
78+
match ev.event().action {
79+
// MenuEvent::Open => todo!(),
80+
// MenuEvent::Close => todo!(),
81+
MenuAction::Toggle => {
82+
let mut was_open = false;
83+
let Ok((menu, children)) = q_menu.get(ev.source) else {
84+
return;
85+
};
86+
for child in children.iter() {
87+
if q_popovers.contains(*child) {
88+
commands.entity(*child).despawn();
89+
was_open = true;
90+
}
91+
}
92+
// Spawn the menu if not already open.
93+
if !was_open {
94+
info!("Opening, !was_open");
95+
if let Some(factory) = menu.0.as_ref() {
96+
(*factory)(commands.entity(ev.source));
97+
// redraw_events.write(RequestRedraw);
98+
}
99+
}
100+
},
101+
MenuAction::CloseAll => {
102+
let Ok((_menu, children)) = q_menu.get(ev.source) else {
103+
return;
104+
};
105+
for child in children.iter() {
106+
if q_popovers.contains(*child) {
107+
commands.entity(*child).despawn();
108+
}
109+
}
110+
},
111+
// MenuEvent::FocusRoot => todo!(),
112+
event => {
113+
info!("Menu Event: {:?}", event);
114+
}
115+
}
116+
})
117+
}
118+
}
119+
120+
/// Button scene function.
121+
///
122+
/// # Arguments
123+
/// * `props` - construction properties for the button.
124+
pub fn menu_button(props: MenuButtonProps) -> impl Scene {
125+
bsn! {
126+
:button(ButtonProps {
127+
variant: ButtonVariant::Normal,
128+
corners: props.corners,
129+
})
130+
Node {
131+
// TODO: HACK to deal with lack of intercepted children
132+
flex_direction: FlexDirection::RowReverse,
133+
}
134+
on(|ev: On<Pointer<Click>>, mut commands: Commands| {
135+
commands.trigger(MenuEvent { source: ev.entity, action: MenuAction::Toggle });
136+
})
137+
[
138+
:icon(icons::CHEVRON_DOWN),
139+
Node {
140+
flex_grow: 0.2,
141+
}
142+
]
143+
}
144+
}
145+
146+
/// Menu Popup scene function
147+
pub fn menu_popup() -> impl Scene {
148+
bsn! {
149+
Node {
150+
position_type: PositionType::Absolute,
151+
display: Display::Flex,
152+
flex_direction: FlexDirection::Column,
153+
justify_content: JustifyContent::Stretch,
154+
align_items: AlignItems::Stretch,
155+
border: UiRect::all(Val::Px(1.0)),
156+
padding: UiRect::all(Val::Px(4.0)),
157+
}
158+
MenuPopupStyle
159+
MenuPopup
160+
template_value(Visibility::Hidden)
161+
template_value(RoundedCorners::All.to_border_radius(4.0))
162+
ThemeBackgroundColor(tokens::MENU_BG)
163+
ThemeBorderColor(tokens::MENU_BORDER)
164+
BoxShadow::new(
165+
Srgba::BLACK.with_alpha(0.9).into(),
166+
Val::Px(0.0),
167+
Val::Px(0.0),
168+
Val::Px(1.0),
169+
Val::Px(4.0),
170+
)
171+
GlobalZIndex(100)
172+
template_value(
173+
Popover {
174+
positions: vec![
175+
PopoverPlacement {
176+
side: PopoverSide::Bottom,
177+
align: PopoverAlign::Start,
178+
gap: 2.0,
179+
},
180+
PopoverPlacement {
181+
side: PopoverSide::Top,
182+
align: PopoverAlign::Start,
183+
gap: 2.0,
184+
},
185+
],
186+
window_margin: 10.0,
187+
}
188+
)
189+
OverrideClip
190+
}
191+
}
192+
193+
/// Menu item scene function
194+
pub fn menu_item() -> impl Scene {
195+
bsn! {
196+
Node {
197+
height: size::ROW_HEIGHT,
198+
min_width: size::ROW_HEIGHT,
199+
justify_content: JustifyContent::Start,
200+
align_items: AlignItems::Center,
201+
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
202+
}
203+
MenuItemStyle
204+
MenuItem
205+
Hovered
206+
// TODO: port CursonIcon to GetTemplate
207+
// CursorIcon::System(bevy_window::SystemCursorIcon::Pointer)
208+
TabIndex(0)
209+
ThemeBackgroundColor(tokens::MENU_BG) // Same as menu
210+
ThemeFontColor(tokens::MENUITEM_TEXT)
211+
InheritableFont {
212+
font: fonts::REGULAR,
213+
font_size: 14.0,
214+
}
215+
}
216+
}
217+
218+
fn update_menuitem_styles(
219+
q_menuitems: Query<
220+
(
221+
Entity,
222+
Has<InteractionDisabled>,
223+
Has<Pressed>,
224+
&Hovered,
225+
&ThemeBackgroundColor,
226+
&ThemeFontColor,
227+
),
228+
(
229+
With<MenuItemStyle>,
230+
Or<(Changed<Hovered>, Added<Pressed>, Added<InteractionDisabled>)>,
231+
),
232+
>,
233+
mut commands: Commands,
234+
) {
235+
for (button_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() {
236+
set_menuitem_colors(
237+
button_ent,
238+
disabled,
239+
pressed,
240+
hovered.0,
241+
bg_color,
242+
font_color,
243+
&mut commands,
244+
);
245+
}
246+
}
247+
248+
fn update_menuitem_styles_remove(
249+
q_menuitems: Query<
250+
(
251+
Entity,
252+
Has<InteractionDisabled>,
253+
Has<Pressed>,
254+
&Hovered,
255+
&ThemeBackgroundColor,
256+
&ThemeFontColor,
257+
),
258+
With<MenuItemStyle>,
259+
>,
260+
mut removed_disabled: RemovedComponents<InteractionDisabled>,
261+
mut removed_pressed: RemovedComponents<Pressed>,
262+
mut commands: Commands,
263+
) {
264+
removed_disabled
265+
.read()
266+
.chain(removed_pressed.read())
267+
.for_each(|ent| {
268+
if let Ok((button_ent, disabled, pressed, hovered, bg_color, font_color)) =
269+
q_menuitems.get(ent)
270+
{
271+
set_menuitem_colors(
272+
button_ent,
273+
disabled,
274+
pressed,
275+
hovered.0,
276+
bg_color,
277+
font_color,
278+
&mut commands,
279+
);
280+
}
281+
});
282+
}
283+
284+
fn set_menuitem_colors(
285+
button_ent: Entity,
286+
disabled: bool,
287+
pressed: bool,
288+
hovered: bool,
289+
bg_color: &ThemeBackgroundColor,
290+
font_color: &ThemeFontColor,
291+
commands: &mut Commands,
292+
) {
293+
let bg_token = match (pressed, hovered) {
294+
(true, _) => tokens::MENUITEM_BG_PRESSED,
295+
(false, true) => tokens::MENUITEM_BG_HOVER,
296+
(false, false) => tokens::MENU_BG,
297+
};
298+
299+
let font_color_token = match disabled {
300+
true => tokens::MENUITEM_TEXT_DISABLED,
301+
false => tokens::MENUITEM_TEXT,
302+
};
303+
304+
// Change background color
305+
if bg_color.0 != bg_token {
306+
commands
307+
.entity(button_ent)
308+
.insert(ThemeBackgroundColor(bg_token));
309+
}
310+
311+
// Change font color
312+
if font_color.0 != font_color_token {
313+
commands
314+
.entity(button_ent)
315+
.insert(ThemeFontColor(font_color_token));
316+
}
317+
}
318+
319+
/// Plugin which registers the systems for updating the menu and menu button styles.
320+
pub struct MenuPlugin;
321+
322+
impl Plugin for MenuPlugin {
323+
fn build(&self, app: &mut bevy_app::App) {
324+
app.add_systems(
325+
PreUpdate,
326+
(update_menuitem_styles, update_menuitem_styles_remove).in_set(PickingSystems::Last),
327+
);
328+
}
329+
}

crates/bevy_feathers/src/controls/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod button;
55
mod checkbox;
66
mod color_slider;
77
mod color_swatch;
8+
mod menu;
89
mod radio;
910
mod slider;
1011
mod toggle_switch;
@@ -14,6 +15,7 @@ pub use button::*;
1415
pub use checkbox::*;
1516
pub use color_slider::*;
1617
pub use color_swatch::*;
18+
pub use menu::*;
1719
pub use radio::*;
1820
pub use slider::*;
1921
pub use toggle_switch::*;
@@ -31,6 +33,7 @@ impl Plugin for ControlsPlugin {
3133
ButtonPlugin,
3234
CheckboxPlugin,
3335
ColorSliderPlugin,
36+
MenuPlugin,
3437
RadioPlugin,
3538
SliderPlugin,
3639
ToggleSwitchPlugin,

crates/bevy_feathers/src/dark_theme.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ pub fn create_dark_theme() -> ThemeProps {
9595
tokens::SWITCH_SLIDE_DISABLED,
9696
palette::LIGHT_GRAY_2.with_alpha(0.3),
9797
),
98+
// Menus
99+
(tokens::MENU_BG, palette::GRAY_1),
100+
(tokens::MENU_BORDER, palette::WARM_GRAY_1),
101+
(tokens::MENUITEM_BG_HOVER, palette::GRAY_1.lighter(0.05)),
102+
(tokens::MENUITEM_BG_PRESSED, palette::GRAY_1.lighter(0.1)),
103+
(tokens::MENUITEM_TEXT, palette::WHITE),
104+
(
105+
tokens::MENUITEM_TEXT_DISABLED,
106+
palette::WHITE.with_alpha(0.5),
107+
),
98108
]),
99109
}
100110
}

0 commit comments

Comments
 (0)