menu system: system dispatching (and the menus can now be closed)

This commit is contained in:
Schrottkatze 2025-06-02 22:20:25 +02:00
parent acbcfe5956
commit fc83ab8452
Signed by: schrottkatze
SSH key fingerprint: SHA256:FPOYVeBy3QP20FEM42uWF1Wa/Qhlk+L3S2+Wuau/Auo
7 changed files with 202 additions and 132 deletions

View file

@ -1,6 +1,5 @@
use bevy::prelude::*;
use bevy_third_person_camera::ThirdPersonCameraTarget;
use log::trace;
use crate::cleanup;

View file

@ -1,5 +1,8 @@
use bevy::{input::ButtonState, prelude::*};
use lib::{ItemPosition, Menu, MenuItemType, Menus, OnPressAction};
use bevy::{prelude::*, state::state::FreelyMutableState};
use lib::{
ItemPosition, MenuItemType, OnPressAction,
menus::{Menu, Menus},
};
use crate::AppState;
@ -14,15 +17,26 @@ pub enum CurrentMenu {
HighScores,
StartGame,
}
fn switch_state<S: States + FreelyMutableState>(target: S) -> impl Fn(ResMut<NextState<S>>) {
move |mut next_state: ResMut<NextState<S>>| {
next_state.set(target.clone());
}
}
pub fn plugin(app: &mut App) {
let plugin = Menus::<AppState, CurrentMenu>::new(AppState::Menus)
let start_game_state = app.register_system(switch_state(AppState::Ingame));
let plugin = Menus::<AppState, CurrentMenu>::new(AppState::Menus, CurrentMenu::NotInMenus)
.add_menu(
CurrentMenu::MainMenu,
Menu::new().add_items(&[
&MenuItemType::Text(String::from("Hello, World!")),
&(
MenuItemType::Button(String::from("Start Game")),
OnPressAction::SampleAction,
OnPressAction::Multiple(vec![
OnPressAction::DispatchSystem(start_game_state),
OnPressAction::Close,
]),
),
&(
MenuItemType::Button(String::from("Settings")),
@ -33,13 +47,7 @@ pub fn plugin(app: &mut App) {
.add_menu(
CurrentMenu::Settings,
Menu::new()
.add_items(&[
&MenuItemType::Text(String::from("Settings")),
&(
MenuItemType::Button(String::from("Sample setting")),
OnPressAction::SampleAction,
),
])
.add_items(&[&MenuItemType::Text(String::from("Settings"))])
.add_items_positioned(
ItemPosition::SouthEast,
&[&(

View file

@ -1,71 +1,23 @@
#![allow(unused, reason = "Temporary.")]
//! goal is a custom ui/menu library that allows simple and declarative menus.
//! bevy ui is annoying.
//!
//! TODOs:
//! - [ ] cleanup
//! - [ ] trigger game events (trigger or normal eventwriter?)
//! - [ ] styling
//! - [ ] more components
//! - [ ] more shorthands
use std::marker::PhantomData;
use bevy::{platform::collections::HashMap, prelude::*};
mod item;
pub mod plugin;
pub mod menus;
pub use item::types::*;
pub use item::*;
type ToDo = ();
#[derive(Debug, Clone)]
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 {
trigger,
start: NavState::default(),
menus: HashMap::new(),
}
}
}
impl<T: States, NavState: States> Menus<T, NavState> {
pub fn add_menu(mut self, state: NavState, menu: Menu<NavState>) -> Self {
self.menus.insert(state, menu);
self
}
}
impl<ParentNavState: States> Menu<ParentNavState> {
pub fn new() -> Self {
Self {
menu_items: Vec::new(),
}
}
/// Adds `items` in the default `ItemPosition::MainView`.
pub fn add_items(mut self, items: &[&dyn MenuItem<ParentNavState>]) -> Self {
self.add_items_positioned(ItemPosition::MainView, items)
}
pub fn add_items_positioned(
mut self,
position: ItemPosition,
items: &[&dyn MenuItem<ParentNavState>],
) -> Self {
items
.iter()
.map(|it| (*it).item())
.collect_into(&mut self.menu_items);
self
}
}

View file

@ -1,4 +1,12 @@
use bevy::state::state::States;
use std::ops::{Deref, DerefMut};
use bevy::{
ecs::{
event::Event,
system::{Commands, SystemId},
},
state::state::{FreelyMutableState, NextState, States},
};
use crate::menus::lib::ToDo;
@ -28,6 +36,24 @@ pub enum MenuItemType {
#[derive(Clone, Debug)]
pub enum OnPressAction<NavState: States> {
NavigateTo(NavState),
DispatchEvent(ToDo),
SampleAction,
DispatchSystem(SystemId),
Multiple(Vec<OnPressAction<NavState>>),
Close,
}
impl<S: States + FreelyMutableState> OnPressAction<S> {
pub fn run(&self, c: &mut Commands, mut nav_state: &mut NextState<S>, closed_state: &S) {
dbg!(self);
match self {
OnPressAction::NavigateTo(to) => nav_state.set(to.clone()),
OnPressAction::DispatchSystem(system_id) => c.run_system(*system_id),
OnPressAction::Multiple(actions) => {
let mut actions = actions.clone();
while let Some(action) = actions.pop() {
action.run(c, nav_state, closed_state);
}
}
OnPressAction::Close => nav_state.set(closed_state.clone()),
}
}
}

72
src/menus/lib/menus.rs Normal file
View file

@ -0,0 +1,72 @@
use bevy::{platform::collections::HashMap, prelude::*};
use super::{IntoMenuItemInternal, ItemPosition, MenuItem, MenuItemInternal};
#[derive(Debug, Clone)]
pub struct Menus<TriggerState, NavState>
where
TriggerState: States,
NavState: States,
{
pub(super) start: NavState,
pub(super) closed_when: NavState,
/// The state needed to trigger this set of menus
pub(super) trigger: TriggerState,
pub(super) menus: HashMap<NavState, Menu<NavState>>,
}
#[derive(Debug, Clone)]
pub struct Menu<ParentNavState: States> {
pub(super) menu_items: Vec<MenuItemInternal<ParentNavState>>,
}
impl<TriggerState, NavState> Menus<TriggerState, NavState>
where
TriggerState: States,
NavState: States + Default,
{
pub fn new(trigger: TriggerState, closed_when: NavState) -> Self {
Self {
trigger,
closed_when,
start: NavState::default(),
menus: HashMap::new(),
}
}
}
impl<T, NavState> Menus<T, NavState>
where
T: States,
NavState: States,
{
pub fn add_menu(mut self, state: NavState, menu: Menu<NavState>) -> Self {
self.menus.insert(state, menu);
self
}
}
impl<ParentNavState: States> Menu<ParentNavState> {
pub fn new() -> Self {
Self {
menu_items: Vec::new(),
}
}
/// Adds `items` in the default `ItemPosition::MainView`.
pub fn add_items(mut self, items: &[&dyn MenuItem<ParentNavState>]) -> Self {
self.add_items_positioned(ItemPosition::MainView, items)
}
pub fn add_items_positioned(
mut self,
position: ItemPosition,
items: &[&dyn MenuItem<ParentNavState>],
) -> Self {
items
.iter()
.map(|it| (*it).item())
.collect_into(&mut self.menu_items);
self
}
}

View file

@ -5,15 +5,24 @@ use bevy::{
state::state::FreelyMutableState,
};
use super::{ItemPosition, Menu, MenuItemInternal, MenuItemType, Menus, OnPressAction};
use super::{
ItemPosition, MenuItemInternal, MenuItemType, OnPressAction,
menus::{Menu, Menus},
};
impl<TriggerState: States, NavState: States + FreelyMutableState + Copy> Plugin
for Menus<TriggerState, NavState>
mod components;
impl<TriggerState, NavState> Plugin for Menus<TriggerState, NavState>
where
TriggerState: States,
NavState: States + FreelyMutableState + Copy,
{
fn build(&self, app: &mut App) {
app.insert_state(self.start)
.insert_resource(self.get_store())
.insert_resource(MenusClosedWhen(self.closed_when))
.add_observer(build_ui::<NavState>)
.add_observer(destroy_ui::<NavState>)
// .add_observer(destroy_ui::<NavStates>)
.add_systems(
Update,
@ -26,7 +35,7 @@ impl<TriggerState: States, NavState: States + FreelyMutableState + Copy> Plugin
}
#[derive(Event)]
struct DestroyUi<S: States>(S);
struct DestroyUi<S: States>(PhantomData<S>);
#[derive(Event)]
struct BuildUi<S: States>(S);
@ -46,6 +55,19 @@ impl<T: States, S: States> Menus<T, S> {
#[derive(Resource)]
struct MenusStore<S: States>(HashMap<S, Menu<S>>);
#[derive(Resource)]
struct MenusClosedWhen<S: States>(S);
fn destroy_ui<S: States>(
trigger: Trigger<DestroyUi<S>>,
ui_parent: Query<Entity, With<UiParent<S>>>,
mut c: Commands,
) {
trace!("destroy_ui called");
if let Ok(parent) = ui_parent.single_inner() {
c.entity(parent).despawn();
}
}
fn build_ui<S: States>(
trigger: Trigger<BuildUi<S>>,
@ -63,11 +85,7 @@ fn build_ui<S: States>(
.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();
}
.filter(|it| ItemPosition::MainView == it.pos);
c.spawn((
UiParent(PhantomData::<S>),
@ -96,75 +114,33 @@ fn build_ui<S: States>(
}
fn handle_press_actions<S: States + FreelyMutableState>(
mut c: Commands,
mut interaction_query: Query<(&Interaction, &Action<S>), Changed<Interaction>>,
mut nav_state: ResMut<NextState<S>>,
closed_when: Res<MenusClosedWhen<S>>,
) {
for (interaction, Action(action)) in interaction_query {
if *interaction != Interaction::Pressed {
continue;
}
match action {
OnPressAction::NavigateTo(to) => nav_state.set(to.clone()),
_ => todo!(),
}
action.run(&mut c, &mut nav_state, &closed_when.0);
}
}
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>(
fn update_ui_trigger<S: States + PartialEq>(
mut c: Commands,
mut trans_reader: EventReader<StateTransitionEvent<S>>,
menus_data: Res<MenusStore<S>>,
closed_when: Res<MenusClosedWhen<S>>,
) {
for trans in trans_reader.read() {
info!("{trans:?}");
// if let Some(to_delete) = trans.exited {}
c.trigger(DestroyUi(PhantomData::<S>));
if let Some(to_build) = trans.entered.clone() {
c.trigger(BuildUi(to_build));
if to_build != closed_when.0 {
c.trigger(BuildUi(to_build));
}
}
}
}
// fn build_ui<S: States>(mut c: Commands) {
// }

View file

@ -0,0 +1,37 @@
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)],
)
}