#![doc = include_str!("../README.md")]
use bevy::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow};
#[cfg(feature = "pretty_nice_input")]
pub use input::{
CloseMenuAction, MenuInputOf, MenuInputs, MenuStackInput, MenuWithInput, close_menu_on_action,
show_menu_on_action,
};
#[cfg(feature = "pretty_nice_input")]
mod input;
#[derive(Default)]
pub struct PrettyNiceMenusPlugin;
impl Plugin for PrettyNiceMenusPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(MenuStack::default())
.add_systems(
PostUpdate,
(remove_despawned_menus, activate_stack_current).chain(),
)
.add_observer(show_mouse)
.add_observer(hide_mouse)
.add_observer(despawn_menus)
.add_observer(spawn_on_stack);
#[cfg(feature = "visibility")]
app.add_observer(show_menus).add_observer(hide_menus);
#[cfg(feature = "pretty_nice_input")]
app.add_plugins(input::PrettyNiceMenusInputPlugin);
}
}
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct MenuWithMouse;
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct MenuWithoutMouse;
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct MenuHidesWhenClosed;
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct MenuDespawnsWhenClosed;
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
pub struct MenuSpawnsOnStack;
fn spawn_on_stack(add: On<Add, MenuSpawnsOnStack>, mut menu_stack: If<ResMut<MenuStack>>) {
menu_stack.push(add.entity);
}
#[derive(Resource, Default, Debug, Reflect)]
#[reflect(Resource, Default, Debug)]
pub struct MenuStack {
stack: Vec<Entity>,
current_top: Option<Entity>,
}
impl MenuStack {
pub fn push(&mut self, menu: Entity) {
self.stack.push(menu);
debug!("Pushed menu {menu:?}, stack is now {self:?}");
}
pub fn remove(&mut self, menu: Entity) {
self.stack.retain(|&entity| entity != menu);
debug!("Removed menu {menu:?}, stack is now {self:?}");
}
pub fn contains(&self, menu: Entity) -> bool {
self.stack.contains(&menu)
}
pub fn toggle(&mut self, menu: Entity) {
if self.contains(menu) {
self.remove(menu);
} else {
self.push(menu);
}
}
}
#[derive(EntityEvent, Debug, Reflect)]
#[reflect(Debug)]
pub struct ActivateMenu {
#[event_target]
pub menu: Entity,
}
#[derive(EntityEvent, Debug, Reflect)]
#[reflect(Debug)]
pub struct DeactivateMenu {
#[event_target]
pub menu: Entity,
}
fn activate_stack_current(mut menu_stack: If<ResMut<MenuStack>>, mut commands: Commands) -> Result {
if !menu_stack.is_changed() {
return Ok(());
}
let new_top = menu_stack.stack.last().cloned();
if let Some(current_top) = menu_stack.current_top
&& new_top != Some(current_top)
{
commands.trigger(DeactivateMenu { menu: current_top });
menu_stack.current_top = None;
}
if menu_stack.current_top.is_none()
&& let Some(new_top) = new_top
{
menu_stack.current_top = Some(new_top);
commands.trigger(ActivateMenu { menu: new_top });
}
Ok(())
}
fn show_mouse(
activate: On<ActivateMenu>,
menus: Query<(), With<MenuWithMouse>>,
mut cursor_options: Query<&mut CursorOptions, With<PrimaryWindow>>,
) {
if menus.get(activate.menu).is_ok()
&& let Ok(mut cursor_options) = cursor_options.single_mut()
{
cursor_options.grab_mode = CursorGrabMode::None;
cursor_options.visible = true;
}
}
fn hide_mouse(
activate: On<ActivateMenu>,
menus: Query<(), With<MenuWithoutMouse>>,
mut cursor_options: Query<&mut CursorOptions, With<PrimaryWindow>>,
) {
if menus.get(activate.menu).is_ok()
&& let Ok(mut cursor_options) = cursor_options.single_mut()
{
cursor_options.grab_mode = CursorGrabMode::Locked;
cursor_options.visible = false;
}
}
pub fn show_menu_on_event<Ev: EntityEvent>(input: On<Ev>, mut menu_stack: ResMut<MenuStack>) {
menu_stack.push(input.event_target());
}
pub fn close_menu_on_event<Ev: EntityEvent>(input: On<Ev>, mut menu_stack: ResMut<MenuStack>) {
menu_stack.remove(input.event_target());
}
#[cfg(feature = "visibility")]
fn show_menus(
activate: On<ActivateMenu>,
mut menus: Query<&mut Visibility, With<MenuHidesWhenClosed>>,
) {
if let Ok(mut visibility) = menus.get_mut(activate.menu) {
*visibility = Visibility::Visible;
}
}
#[cfg(feature = "visibility")]
fn hide_menus(
deactivate: On<DeactivateMenu>,
mut menus: Query<&mut Visibility, With<MenuHidesWhenClosed>>,
) {
if let Ok(mut visibility) = menus.get_mut(deactivate.menu) {
*visibility = Visibility::Hidden;
}
}
fn despawn_menus(
deactivate: On<DeactivateMenu>,
mut menus: Query<Entity, With<MenuDespawnsWhenClosed>>,
mut commands: Commands,
) {
if let Ok(menu) = menus.get_mut(deactivate.menu) {
commands.entity(menu).despawn();
}
}
fn remove_despawned_menus(
mut menu_stack: ResMut<MenuStack>,
mut commands: Commands,
entities: Query<()>,
) {
for menu in menu_stack.stack.clone() {
if entities.get(menu).is_err() {
debug!("Removing despawned menu {:?}", menu);
if menu_stack.current_top == Some(menu) {
menu_stack.current_top = None;
}
menu_stack.remove(menu);
commands.trigger(DeactivateMenu { menu });
}
}
}