menu system: ITS ALIVE

This commit is contained in:
Schrottkatze 2025-05-23 19:20:57 +02:00
parent 2b18cdea38
commit acbcfe5956
Signed by: schrottkatze
SSH key fingerprint: SHA256:FPOYVeBy3QP20FEM42uWF1Wa/Qhlk+L3S2+Wuau/Auo
7 changed files with 216 additions and 30 deletions

View file

@ -5,7 +5,7 @@ use bevy_inspector_egui::{
quick::{StateInspectorPlugin, WorldInspectorPlugin}, quick::{StateInspectorPlugin, WorldInspectorPlugin},
}; };
use crate::AppState; use crate::{AppState, menus};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(EguiPlugin { app.add_plugins(EguiPlugin {
@ -14,5 +14,6 @@ pub fn plugin(app: &mut App) {
.add_plugins(( .add_plugins((
WorldInspectorPlugin::new(), WorldInspectorPlugin::new(),
StateInspectorPlugin::<AppState>::new(), StateInspectorPlugin::<AppState>::new(),
StateInspectorPlugin::<menus::CurrentMenu>::new(),
)); ));
} }

View file

@ -10,8 +10,8 @@ mod menus;
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash, Reflect)] #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash, Reflect)]
#[allow(unused)] #[allow(unused)]
enum AppState { enum AppState {
Menus,
#[default] #[default]
Menus,
Ingame, Ingame,
PostGame, PostGame,
} }
@ -21,8 +21,10 @@ fn main() {
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_systems(Startup, camera::setup) .add_systems(Startup, camera::setup)
.add_plugins(game::plugin) .add_plugins(game::plugin)
.add_plugins(menus::plugin)
.init_state::<AppState>() .init_state::<AppState>()
.register_type::<AppState>()
.add_plugins(debugging::plugin) .add_plugins(debugging::plugin)
.register_type::<AppState>()
// .insert_state(AppState::Ingame)
.run(); .run();
} }

View file

@ -1,18 +1,21 @@
use bevy::{input::ButtonState, prelude::*}; use bevy::{input::ButtonState, prelude::*};
use lib::{ItemPosition, Menu, MenuItemType, Menus, OnPressAction}; use lib::{ItemPosition, Menu, MenuItemType, Menus, OnPressAction};
use crate::AppState;
mod lib; mod lib;
#[derive(States, Debug, Clone, PartialEq, Eq, Hash)] #[derive(States, Debug, Copy, Clone, PartialEq, Eq, Hash, Default, Reflect)]
enum CurrentMenu { pub enum CurrentMenu {
NotInMenus, NotInMenus,
#[default]
MainMenu, MainMenu,
Settings, Settings,
HighScores, HighScores,
StartGame, StartGame,
} }
pub fn todo_meow() { pub fn plugin(app: &mut App) {
let plugin = Menus::<CurrentMenu>::new() let plugin = Menus::<AppState, CurrentMenu>::new(AppState::Menus)
.add_menu( .add_menu(
CurrentMenu::MainMenu, CurrentMenu::MainMenu,
Menu::new().add_items(&[ Menu::new().add_items(&[
@ -31,7 +34,7 @@ pub fn todo_meow() {
CurrentMenu::Settings, CurrentMenu::Settings,
Menu::new() Menu::new()
.add_items(&[ .add_items(&[
&MenuItemType::Text(String::from("Hello, World!")), &MenuItemType::Text(String::from("Settings")),
&( &(
MenuItemType::Button(String::from("Sample setting")), MenuItemType::Button(String::from("Sample setting")),
OnPressAction::SampleAction, OnPressAction::SampleAction,
@ -46,5 +49,6 @@ pub fn todo_meow() {
), ),
); );
dbg!(plugin); dbg!(&plugin);
app.add_plugins(plugin);
} }

View file

@ -6,30 +6,39 @@ use std::marker::PhantomData;
use bevy::{platform::collections::HashMap, prelude::*}; use bevy::{platform::collections::HashMap, prelude::*};
type ToDo = (); mod item;
#[derive(Debug)]
pub struct Menus<NavState: States> {
menus: HashMap<NavState, Menu<NavState>>,
}
#[derive(Debug)]
pub struct Menu<ParentNavState: States> {
menu_items: Vec<MenuItemInternal<ParentNavState>>,
}
pub use item::types::*; pub use item::types::*;
pub use item::*; pub use item::*;
mod item; type ToDo = ();
impl<NavState: States> Menus<NavState> { #[derive(Debug, Clone)]
pub fn new() -> Self { pub struct Menus<TriggerState: States, NavState: States> {
start: NavState,
/// The state needed to trigger this set of menus
trigger: TriggerState,
menus: HashMap<NavState, Menu<NavState>>,
}
#[derive(Debug, Clone)]
pub struct Menu<ParentNavState: States> {
menu_items: Vec<MenuItemInternal<ParentNavState>>,
}
pub mod plugin;
impl<TriggerState: States, NavState: States + Default> Menus<TriggerState, NavState> {
pub fn new(trigger: TriggerState) -> Self {
Self { Self {
trigger,
start: NavState::default(),
menus: HashMap::new(), menus: HashMap::new(),
} }
} }
}
impl<T: States, NavState: States> Menus<T, NavState> {
pub fn add_menu(mut self, state: NavState, menu: Menu<NavState>) -> Self { pub fn add_menu(mut self, state: NavState, menu: Menu<NavState>) -> Self {
self.menus.insert(state, menu); self.menus.insert(state, menu);
self self

View file

@ -3,11 +3,11 @@ use types::{ItemPosition, MenuItemType, OnPressAction};
pub mod types; pub mod types;
#[derive(Debug)] #[derive(Debug, Clone)]
pub(super) struct MenuItemInternal<NavState: States> { pub(super) struct MenuItemInternal<NavState: States> {
r#type: MenuItemType, pub(super) r#type: MenuItemType,
pos: ItemPosition, pub(super) pos: ItemPosition,
action: Option<OnPressAction<NavState>>, pub(super) action: Option<OnPressAction<NavState>>,
} }
pub(super) trait IntoMenuItemInternal<NavState: States> { pub(super) trait IntoMenuItemInternal<NavState: States> {

View file

@ -3,7 +3,7 @@ use bevy::state::state::States;
use crate::menus::lib::ToDo; use crate::menus::lib::ToDo;
/// positions the item in the menu /// positions the item in the menu
#[derive(Default, Debug)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemPosition { pub enum ItemPosition {
#[default] #[default]
MainView, MainView,
@ -22,12 +22,12 @@ pub enum ItemPosition {
pub enum MenuItemType { pub enum MenuItemType {
Text(String), Text(String),
Button(String), Button(String),
Subsection(Vec<MenuItemType>), // Subsection(Vec<MenuItemType>),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum OnPressAction<NavState: States> { pub enum OnPressAction<NavState: States> {
NavigateTo(NavState), NavigateTo(NavState),
RunSystem(ToDo), DispatchEvent(ToDo),
SampleAction, SampleAction,
} }

170
src/menus/lib/plugin.rs Normal file
View file

@ -0,0 +1,170 @@
use std::marker::PhantomData;
use bevy::{
ecs::spawn::SpawnIter, platform::collections::HashMap, prelude::*,
state::state::FreelyMutableState,
};
use super::{ItemPosition, Menu, MenuItemInternal, MenuItemType, Menus, OnPressAction};
impl<TriggerState: States, NavState: States + FreelyMutableState + Copy> Plugin
for Menus<TriggerState, NavState>
{
fn build(&self, app: &mut App) {
app.insert_state(self.start)
.insert_resource(self.get_store())
.add_observer(build_ui::<NavState>)
// .add_observer(destroy_ui::<NavStates>)
.add_systems(
Update,
(
update_ui_trigger::<NavState>,
handle_press_actions::<NavState>.run_if(in_state(self.trigger.clone())),
),
);
}
}
#[derive(Event)]
struct DestroyUi<S: States>(S);
#[derive(Event)]
struct BuildUi<S: States>(S);
#[derive(Component)]
struct UiParent<S: States>(PhantomData<S>);
#[derive(Component)]
struct UiComponent;
#[derive(Component)]
struct Action<S: States>(OnPressAction<S>);
impl<T: States, S: States> Menus<T, S> {
fn get_store(&self) -> MenusStore<S> {
MenusStore(self.menus.clone())
}
}
#[derive(Resource)]
struct MenusStore<S: States>(HashMap<S, Menu<S>>);
fn build_ui<S: States>(
trigger: Trigger<BuildUi<S>>,
ui_parent: Query<Entity, With<UiParent<S>>>,
mut c: Commands,
menus_data: Res<MenusStore<S>>,
) {
let e = trigger.event();
let Some(menu_structure) = menus_data.0.get(&e.0) else {
return;
};
let items = menu_structure
.menu_items
.clone()
.into_iter()
// TODO: implement other item positions
.filter(|it| it.pos == ItemPosition::MainView);
if let Ok(parent) = ui_parent.single_inner() {
c.entity(parent).despawn();
}
c.spawn((
UiParent(PhantomData::<S>),
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
..default()
},
))
.with_children(|parent| {
for MenuItemInternal { r#type, action, .. } in items {
let mut e = match r#type {
MenuItemType::Text(s) => parent.spawn(components::text(s)),
MenuItemType::Button(s) => parent.spawn(components::button(s)),
};
e.insert(UiComponent);
if let Some(action) = action {
e.insert(Action(action));
}
}
});
}
fn handle_press_actions<S: States + FreelyMutableState>(
mut interaction_query: Query<(&Interaction, &Action<S>), Changed<Interaction>>,
mut nav_state: ResMut<NextState<S>>,
) {
for (interaction, Action(action)) in interaction_query {
if *interaction != Interaction::Pressed {
continue;
}
match action {
OnPressAction::NavigateTo(to) => nav_state.set(to.clone()),
_ => todo!(),
}
}
}
mod components {
use bevy::prelude::*;
use crate::menus::lib::OnPressAction;
use super::Action;
pub fn text(t: String) -> impl Bundle {
(
Text::new(t),
TextFont {
// font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)
}
pub fn button(t: String) -> impl Bundle {
(
Button,
Node {
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
children![text(t)],
)
}
}
fn update_ui_trigger<S: States>(
mut c: Commands,
mut trans_reader: EventReader<StateTransitionEvent<S>>,
menus_data: Res<MenusStore<S>>,
) {
for trans in trans_reader.read() {
info!("{trans:?}");
// if let Some(to_delete) = trans.exited {}
if let Some(to_build) = trans.entered.clone() {
c.trigger(BuildUi(to_build));
}
}
}
// fn build_ui<S: States>(mut c: Commands) {
// }