Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/bevy_feathers/src/assets/icons/_NAMING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The names of files in this directory should refer to what the icons look like ("x", "chevron", etc.)
rather than their assigned meanings ("close", "expand") because the latter can change.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added crates/bevy_feathers/src/assets/icons/x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions crates/bevy_feathers/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ pub mod fonts {
pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf";
}

/// Icon paths
pub mod icons {
/// Downward-pointing chevron
pub const CHEVRON_DOWN: &str = "embedded://bevy_feathers/assets/icons/chevron-down.png";
/// Right-pointing chevron
pub const CHEVRON_RIGHT: &str = "embedded://bevy_feathers/assets/icons/chevron-right.png";
/// Diagonal Cross
pub const X: &str = "embedded://bevy_feathers/assets/icons/x.png";
}

/// Size constants
pub mod size {
use bevy_ui::Val;
Expand Down
329 changes: 329 additions & 0 deletions crates/bevy_feathers/src/controls/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
use alloc::sync::Arc;

use bevy_app::{Plugin, PreUpdate};
use bevy_camera::visibility::Visibility;
use bevy_color::{Alpha, Srgba};
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::Children,
lifecycle::RemovedComponents,
observer::On,
query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs,
system::{Commands, EntityCommands, Query},
};
use bevy_log::info;
use bevy_picking::{
events::{Click, Pointer},
hover::Hovered,
PickingSystems,
};
use bevy_scene2::{prelude::*, template_value};
use bevy_ui::{
AlignItems, BoxShadow, Display, FlexDirection, GlobalZIndex, InteractionDisabled,
JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val,
};
use bevy_ui_widgets::{
popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
MenuAction, MenuEvent, MenuItem, MenuPopup,
};

use crate::{
constants::{fonts, icons, size},
controls::{button, ButtonProps, ButtonVariant},
font_styles::InheritableFont,
icon,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
use bevy_input_focus::tab_navigation::TabIndex;

/// Parameters for the menu button template, passed to [`menu_button`] function.
#[derive(Default)]
pub struct MenuButtonProps {
/// Rounded corners options
pub corners: RoundedCorners,
}

/// Marker for menu items
#[derive(Component, Default, Clone)]
struct MenuItemStyle;

/// Marker for menu popup
#[derive(Component, Default, Clone)]
struct MenuPopupStyle;

/// Marker for menu wrapper
#[derive(Component, Clone, Default)]
struct Menu(Option<Arc<dyn Fn(EntityCommands) + 'static + Send + Sync>>);

/// Menu scene function. This wraps the menu button and provides an anchor for the popopver.
pub fn menu<F: Fn(EntityCommands) + 'static + Send + Sync>(spawn_popover: F) -> impl Scene {
let menu = Menu(Some(Arc::new(spawn_popover)));
bsn! {
Node {
height: size::ROW_HEIGHT,
justify_content: JustifyContent::Stretch,
align_items: AlignItems::Stretch,
}
template_value(menu)
on(|
ev: On<MenuEvent>,
q_menu: Query<(&Menu, &Children)>,
q_popovers: Query<Entity, With<MenuPopupStyle>>,
// mut redraw_events: MessageWriter<RequestRedraw>,
mut commands: Commands| {
match ev.event().action {
// MenuEvent::Open => todo!(),
// MenuEvent::Close => todo!(),
MenuAction::Toggle => {
let mut was_open = false;
let Ok((menu, children)) = q_menu.get(ev.source) else {
return;
};
for child in children.iter() {
if q_popovers.contains(*child) {
commands.entity(*child).despawn();
was_open = true;
}
}
// Spawn the menu if not already open.
if !was_open {
info!("Opening, !was_open");
if let Some(factory) = menu.0.as_ref() {
(*factory)(commands.entity(ev.source));
// redraw_events.write(RequestRedraw);
}
}
},
MenuAction::CloseAll => {
let Ok((_menu, children)) = q_menu.get(ev.source) else {
return;
};
for child in children.iter() {
if q_popovers.contains(*child) {
commands.entity(*child).despawn();
}
}
},
// MenuEvent::FocusRoot => todo!(),
event => {
info!("Menu Event: {:?}", event);
}
}
})
}
}

/// Button scene function.
///
/// # Arguments
/// * `props` - construction properties for the button.
pub fn menu_button(props: MenuButtonProps) -> impl Scene {
bsn! {
:button(ButtonProps {
variant: ButtonVariant::Normal,
corners: props.corners,
})
Node {
// TODO: HACK to deal with lack of intercepted children
flex_direction: FlexDirection::RowReverse,
}
on(|ev: On<Pointer<Click>>, mut commands: Commands| {
commands.trigger(MenuEvent { source: ev.entity, action: MenuAction::Toggle });
})
[
:icon(icons::CHEVRON_DOWN),
Node {
flex_grow: 0.2,
}
]
}
}

/// Menu Popup scene function
pub fn menu_popup() -> impl Scene {
bsn! {
Node {
position_type: PositionType::Absolute,
display: Display::Flex,
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Stretch,
align_items: AlignItems::Stretch,
border: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(4.0)),
}
MenuPopupStyle
MenuPopup
template_value(Visibility::Hidden)
template_value(RoundedCorners::All.to_border_radius(4.0))
ThemeBackgroundColor(tokens::MENU_BG)
ThemeBorderColor(tokens::MENU_BORDER)
BoxShadow::new(
Srgba::BLACK.with_alpha(0.9).into(),
Val::Px(0.0),
Val::Px(0.0),
Val::Px(1.0),
Val::Px(4.0),
)
GlobalZIndex(100)
template_value(
Popover {
positions: vec![
PopoverPlacement {
side: PopoverSide::Bottom,
align: PopoverAlign::Start,
gap: 2.0,
},
PopoverPlacement {
side: PopoverSide::Top,
align: PopoverAlign::Start,
gap: 2.0,
},
],
window_margin: 10.0,
}
)
OverrideClip
}
}

/// Menu item scene function
pub fn menu_item() -> impl Scene {
bsn! {
Node {
height: size::ROW_HEIGHT,
min_width: size::ROW_HEIGHT,
justify_content: JustifyContent::Start,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
}
MenuItemStyle
MenuItem
Hovered
// TODO: port CursonIcon to GetTemplate
// CursorIcon::System(bevy_window::SystemCursorIcon::Pointer)
TabIndex(0)
ThemeBackgroundColor(tokens::MENU_BG) // Same as menu
ThemeFontColor(tokens::MENUITEM_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: 14.0,
}
}
}

fn update_menuitem_styles(
q_menuitems: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Pressed>,
&Hovered,
&ThemeBackgroundColor,
&ThemeFontColor,
),
(
With<MenuItemStyle>,
Or<(Changed<Hovered>, Added<Pressed>, Added<InteractionDisabled>)>,
),
>,
mut commands: Commands,
) {
for (button_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() {
set_menuitem_colors(
button_ent,
disabled,
pressed,
hovered.0,
bg_color,
font_color,
&mut commands,
);
}
}

fn update_menuitem_styles_remove(
q_menuitems: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Pressed>,
&Hovered,
&ThemeBackgroundColor,
&ThemeFontColor,
),
With<MenuItemStyle>,
>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
mut removed_pressed: RemovedComponents<Pressed>,
mut commands: Commands,
) {
removed_disabled
.read()
.chain(removed_pressed.read())
.for_each(|ent| {
if let Ok((button_ent, disabled, pressed, hovered, bg_color, font_color)) =
q_menuitems.get(ent)
{
set_menuitem_colors(
button_ent,
disabled,
pressed,
hovered.0,
bg_color,
font_color,
&mut commands,
);
}
});
}

fn set_menuitem_colors(
button_ent: Entity,
disabled: bool,
pressed: bool,
hovered: bool,
bg_color: &ThemeBackgroundColor,
font_color: &ThemeFontColor,
commands: &mut Commands,
) {
let bg_token = match (pressed, hovered) {
(true, _) => tokens::MENUITEM_BG_PRESSED,
(false, true) => tokens::MENUITEM_BG_HOVER,
(false, false) => tokens::MENU_BG,
};

let font_color_token = match disabled {
true => tokens::MENUITEM_TEXT_DISABLED,
false => tokens::MENUITEM_TEXT,
};

// Change background color
if bg_color.0 != bg_token {
commands
.entity(button_ent)
.insert(ThemeBackgroundColor(bg_token));
}

// Change font color
if font_color.0 != font_color_token {
commands
.entity(button_ent)
.insert(ThemeFontColor(font_color_token));
}
}

/// Plugin which registers the systems for updating the menu and menu button styles.
pub struct MenuPlugin;

impl Plugin for MenuPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(update_menuitem_styles, update_menuitem_styles_remove).in_set(PickingSystems::Last),
);
}
}
3 changes: 3 additions & 0 deletions crates/bevy_feathers/src/controls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod button;
mod checkbox;
mod color_slider;
mod color_swatch;
mod menu;
mod radio;
mod slider;
mod toggle_switch;
Expand All @@ -14,6 +15,7 @@ pub use button::*;
pub use checkbox::*;
pub use color_slider::*;
pub use color_swatch::*;
pub use menu::*;
pub use radio::*;
pub use slider::*;
pub use toggle_switch::*;
Expand All @@ -31,6 +33,7 @@ impl Plugin for ControlsPlugin {
ButtonPlugin,
CheckboxPlugin,
ColorSliderPlugin,
MenuPlugin,
RadioPlugin,
SliderPlugin,
ToggleSwitchPlugin,
Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_feathers/src/dark_theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ pub fn create_dark_theme() -> ThemeProps {
tokens::SWITCH_SLIDE_DISABLED,
palette::LIGHT_GRAY_2.with_alpha(0.3),
),
// Menus
(tokens::MENU_BG, palette::GRAY_1),
(tokens::MENU_BORDER, palette::WARM_GRAY_1),
(tokens::MENUITEM_BG_HOVER, palette::GRAY_1.lighter(0.05)),
(tokens::MENUITEM_BG_PRESSED, palette::GRAY_1.lighter(0.1)),
(tokens::MENUITEM_TEXT, palette::WHITE),
(
tokens::MENUITEM_TEXT_DISABLED,
palette::WHITE.with_alpha(0.5),
),
]),
}
}
Loading