From 089094708dd4880d4f941f16c03a198ab70bf34d Mon Sep 17 00:00:00 2001 From: eri Date: Sun, 28 Jul 2024 11:40:39 +0200 Subject: [PATCH] refactor: prelude and errors --- .rustfmt.toml | 4 +-- Cargo.lock | 7 +++++ Cargo.toml | 1 + macros/src/lib.rs | 13 +++++----- src/assets.rs | 2 +- src/assets/embedded.rs | 2 -- src/assets/fonts.rs | 3 --- src/assets/meta.rs | 3 --- src/assets/music.rs | 3 --- src/assets/sound.rs | 3 --- src/base.rs | 4 +-- src/base/data.rs | 15 ++++++----- src/base/later.rs | 4 ++- src/base/sets.rs | 2 -- src/base/states.rs | 2 -- src/components.rs | 2 +- src/components/camera.rs | 2 -- src/components/music.rs | 2 +- src/helpers.rs | 16 ++++++++++++ src/input.rs | 4 +-- src/lib.rs | 8 +++++- src/main.rs | 3 +-- src/ui.rs | 50 ++++++++++------------------------- src/ui/navigation.rs | 56 ++++++++++++++++++++++++++++++---------- src/ui/widgets.rs | 51 ++++++++++++++++++++++++++++++++---- 25 files changed, 161 insertions(+), 101 deletions(-) create mode 100644 src/helpers.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index d842816..430a04f 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -10,8 +10,8 @@ normalize_comments = true normalize_doc_attributes = true overflow_delimited_expr = true reorder_impl_items = true -single_line_if_else_max_width = 100 # 50 -single_line_let_else_max_width = 100 # 50 +single_line_if_else_max_width = 80 # 50 +single_line_let_else_max_width = 80 # 50 unstable_features = true use_field_init_shorthand = true use_try_shorthand = true diff --git a/Cargo.lock b/Cargo.lock index 27ea5c8..ea01dbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "approx" version = "0.5.1" @@ -2067,6 +2073,7 @@ dependencies = [ name = "game" version = "0.14.1" dependencies = [ + "anyhow", "bevy", "bevy-trait-query", "bevy_mod_picking", diff --git a/Cargo.toml b/Cargo.toml index ecb0b83..dece976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ log = { version = "*", features = [ macro_rules_attribute = { version = "0.2" } serde = { version = "1.0", features = ["derive"] } toml = { version = "0.8" } +anyhow = { version = "1.0" } [profile.dev] opt-level = 1 diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 446cf4d..b6ab25d 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -33,7 +33,7 @@ pub fn persistent(args: pm::TokenStream, input: pm::TokenStream) -> pm::TokenStr let path = format!("{}/{}.toml", DATA_PATH, args.name); let name = ident.to_string(); - // TODO: Propperly handle errors + // TODO: Wasm support let output = quote! { #[derive(Resource, Serialize, Deserialize)] #input @@ -52,18 +52,19 @@ pub fn persistent(args: pm::TokenStream, input: pm::TokenStream) -> pm::TokenStr }; } - fn persist(&self) { - let data = toml::to_string(self).unwrap(); - let _ = std::fs::write(#path, data); + fn persist(&self) -> Result<()> { + let data = toml::to_string(self).with_context(|| format!("Failed to serialize data for {}", #name))?; + std::fs::write(#path, data).with_context(|| format!("Failed to save serialized data to {} for {}", #path, #name))?; debug!("{} updated, saved in {}", #name, #path); + Ok(()) } - fn update(&mut self, f: impl Fn(&mut #ident)) { + fn update(&mut self, f: impl Fn(&mut #ident)) -> Result<()> { f(self); self.persist() } - fn reset(&mut self) { + fn reset(&mut self) -> Result<()> { *self = Self::default(); self.persist() } diff --git a/src/assets.rs b/src/assets.rs index 0d35896..cbc723a 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,4 +1,4 @@ -use bevy::{prelude::*, utils::HashMap}; +use crate::prelude::*; #[cfg(feature = "embedded")] pub(crate) mod embedded; diff --git a/src/assets/embedded.rs b/src/assets/embedded.rs index 7f9d9ce..dabb824 100644 --- a/src/assets/embedded.rs +++ b/src/assets/embedded.rs @@ -17,9 +17,7 @@ use bevy::{ PathStream, Reader, }, - prelude::*, tasks::futures_lite::{AsyncRead, AsyncSeek, Stream}, - utils::HashMap, }; use include_dir::{include_dir, Dir}; diff --git a/src/assets/fonts.rs b/src/assets/fonts.rs index 946fcc6..b59bf74 100644 --- a/src/assets/fonts.rs +++ b/src/assets/fonts.rs @@ -1,6 +1,3 @@ -use bevy::prelude::*; -use macros::asset_key; - use crate::prelude::*; /// Preloads the font assets when the game starts diff --git a/src/assets/meta.rs b/src/assets/meta.rs index 17598bc..8065bde 100644 --- a/src/assets/meta.rs +++ b/src/assets/meta.rs @@ -1,6 +1,3 @@ -use bevy::prelude::*; -use macros::asset_key; - use crate::prelude::*; /// Preloads the meta assets when the game starts diff --git a/src/assets/music.rs b/src/assets/music.rs index f8b2101..9562c0a 100644 --- a/src/assets/music.rs +++ b/src/assets/music.rs @@ -1,6 +1,3 @@ -use bevy::prelude::*; -use macros::asset_key; - use crate::prelude::*; /// Preloads the music assets when the game starts diff --git a/src/assets/sound.rs b/src/assets/sound.rs index 656ce56..7eb8eb0 100644 --- a/src/assets/sound.rs +++ b/src/assets/sound.rs @@ -1,6 +1,3 @@ -use bevy::prelude::*; -use macros::asset_key; - use crate::prelude::*; /// Preloads the sound assets when the game starts diff --git a/src/base.rs b/src/base.rs index 38986c8..34370b3 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,10 +1,10 @@ +use crate::prelude::*; + mod data; mod later; mod sets; mod states; -use bevy::prelude::*; - pub(super) fn plugin(app: &mut App) { app.add_plugins((data::plugin, later::plugin, sets::plugin, states::plugin)); } diff --git a/src/base/data.rs b/src/base/data.rs index 0439be9..e962206 100644 --- a/src/base/data.rs +++ b/src/base/data.rs @@ -1,11 +1,14 @@ -use bevy::prelude::*; -use macros::persistent; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use crate::prelude::*; + const DATA_PATH: &str = ".data"; // If changed, update in `macros/lib.rs` pub(super) fn plugin(app: &mut App) { - let _ = std::fs::create_dir_all(DATA_PATH); + #[cfg(not(target_arch = "wasm32"))] + if let Err(e) = std::fs::create_dir_all(DATA_PATH) { + warn!("Couldn't create the save directory {}: {}", DATA_PATH, e); + }; app.insert_resource(SaveData::load()); } @@ -19,7 +22,7 @@ pub struct SaveData { pub trait Persistent: Resource + Serialize + DeserializeOwned + Default { fn load() -> Self; fn reload(&mut self); - fn persist(&self); - fn update(&mut self, f: impl Fn(&mut Self)); - fn reset(&mut self); + fn persist(&self) -> Result<()>; + fn update(&mut self, f: impl Fn(&mut Self)) -> Result<()>; + fn reset(&mut self) -> Result<()>; } diff --git a/src/base/later.rs b/src/base/later.rs index 3859a24..12c4059 100644 --- a/src/base/later.rs +++ b/src/base/later.rs @@ -1,6 +1,8 @@ //! Based on the work by dylanj https://discord.com/channels/691052431525675048/937158127491633152/1266369728402948136 -use bevy::{ecs::system::EntityCommands, prelude::*}; +use bevy::ecs::system::EntityCommands; + +use crate::prelude::*; pub(super) fn plugin(app: &mut App) { app.add_systems(PreUpdate, handle_later_commands); diff --git a/src/base/sets.rs b/src/base/sets.rs index f718507..fdf5c58 100644 --- a/src/base/sets.rs +++ b/src/base/sets.rs @@ -1,5 +1,3 @@ -use bevy::prelude::*; - use crate::prelude::*; /// Adds the `PlaySet` to the `App`. diff --git a/src/base/states.rs b/src/base/states.rs index eb49bdc..54cbbb0 100644 --- a/src/base/states.rs +++ b/src/base/states.rs @@ -1,5 +1,3 @@ -use bevy::prelude::*; - use crate::prelude::*; /// Adds the `GameState` to the `App`. diff --git a/src/components.rs b/src/components.rs index 1322fdb..687e358 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use crate::prelude::*; mod camera; mod music; diff --git a/src/components/camera.rs b/src/components/camera.rs index 4f23f13..7e65e8c 100644 --- a/src/components/camera.rs +++ b/src/components/camera.rs @@ -1,5 +1,3 @@ -use bevy::prelude::*; - use crate::prelude::*; pub(super) fn plugin(app: &mut App) { diff --git a/src/components/music.rs b/src/components/music.rs index 5ce7634..a315dd7 100644 --- a/src/components/music.rs +++ b/src/components/music.rs @@ -1,4 +1,4 @@ -use bevy::{audio::PlaybackMode, prelude::*}; +use bevy::audio::PlaybackMode; use crate::prelude::*; diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..ccad14a --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,16 @@ +/// Gets a single `Entity` from a `Query` or returns gracefully (no panic). +#[macro_export] +macro_rules! single { + ($q:expr, $r:expr) => { + match $q.get_single() { + Ok(m) => m, + _ => { + debug!("get single failed for ${}", stringify!($e)); + $r + }, + } + }; + ($q:expr) => { + single!($q, return) + }; +} diff --git a/src/input.rs b/src/input.rs index 7ea1e1a..e143140 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,4 +1,3 @@ -use bevy::prelude::*; use leafwing_input_manager::prelude::*; use crate::prelude::*; @@ -17,10 +16,9 @@ pub mod prelude { /// These are all the possible game actions that have an input mapping /// You can use them like so: /// ``` -/// use bevy::prelude::*; /// use game::prelude::*; /// fn handle_input(input: Query<&ActionState>) { -/// let Ok(input) = input.get_single() else { return }; +/// let input = single!(input); /// if input.just_pressed(&Action::Jump) { /// info!("Hi! c:"); /// } diff --git a/src/lib.rs b/src/lib.rs index d61d619..86aab2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ extern crate macro_rules_attribute; // TODO: Documentation and code examples // Readme // TODO: Main menu -// UI Navigation with input (custom implementation) // TODO: Keybind remapping // TODO: Text to speech // TODO: Migrate proc macros to macro_rules_attribute? @@ -19,16 +18,23 @@ use bevy::{prelude::*, window::WindowResolution}; mod assets; mod base; mod components; +#[macro_use] +mod helpers; mod input; mod ui; pub mod prelude { + pub use anyhow::{Context, Result}; + pub use bevy::{color::palettes::css, prelude::*, utils::HashMap}; + pub use macros::*; + pub use super::{ assets::prelude::*, base::prelude::*, components::prelude::*, input::prelude::*, ui::prelude::*, + GamePlugin, }; // Shorthands for derive macros diff --git a/src/main.rs b/src/main.rs index fa995fd..7eba9bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use bevy::prelude::*; -use game::GamePlugin; +use game::prelude::*; fn main() { App::new().add_plugins(GamePlugin).run(); diff --git a/src/ui.rs b/src/ui.rs index 1be061e..92d67e9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,50 +1,28 @@ -use bevy::prelude::*; - -// use crate::{prelude::*, ui::navigation::*}; +use crate::prelude::*; mod navigation; mod widgets; pub(super) fn plugin(app: &mut App) { - app.add_plugins(navigation::plugin); - // app.register_component_as::() - // .add_systems(OnEnter(GameState::Startup), init); + app.add_plugins((navigation::plugin, widgets::plugin)); + app.add_systems(OnEnter(GameState::Startup), init); } pub mod prelude { pub use bevy_trait_query::RegisterExt; pub use super::{ - navigation::NavBundle, - widgets::{Container, Stylable, Widget}, + navigation::{NavBundle, NavContainer, Navigable}, + widgets::{Container, NavigableExt, Stylable, Widget}, }; } -// #[derive(Component)] -// struct MenuItem { -// label: String, -// } -// -// impl Navigable for MenuItem { -// fn label(&self) -> String { -// self.label.clone() -// } -// -// fn action(&self) { -// info!("action {}", self.label()); -// } -// } -// -// fn init(mut cmd: Commands) { -// let mut root = cmd.ui_root(); -// root.with_children(|node| { -// node.button("hey").insert(MenuItem { -// label: "hey".into(), -// }); -// node.button("hi").insert(MenuItem { label: "hi".into() }); -// node.button("hello").insert(MenuItem { -// label: "hello".into(), -// }); -// }) -// .insert(NavContainer::default()); -// } +fn init(mut cmd: Commands) { + let mut root = cmd.ui_root(); + root.with_children(|node| { + node.button("hey"); + node.button("hi").no_nav(); + node.button("hello"); + }) + .nav_container(); +} diff --git a/src/ui/navigation.rs b/src/ui/navigation.rs index cd61bda..7cf1eec 100644 --- a/src/ui/navigation.rs +++ b/src/ui/navigation.rs @@ -1,8 +1,4 @@ -use bevy::{ - color::palettes::css, - ecs::component::{ComponentHooks, StorageType}, - prelude::*, -}; +use bevy::ecs::component::{ComponentHooks, StorageType}; use bevy_mod_picking::prelude::*; use crate::prelude::*; @@ -19,15 +15,23 @@ pub(super) fn plugin(app: &mut App) { .add_systems(Update, (handle_next_prev, handle_press).chain()); } +/// An UI element that can be navigated to. +/// When clicked, it runs `action`. #[bevy_trait_query::queryable] pub trait Navigable { fn label(&self) -> String; // For tts fn action(&self); } -#[derive(Component, Default)] -pub(super) struct NavContainer; +/// `Navigable` children of entities with this components can be selected and +/// have its focus moved with input actions. +/// Nesting is not properly supported yet. +#[derive(Component)] +pub struct NavContainer; +/// A marker for the selected `Navigable` entity of a `NavContainer`. +/// It has custom component hooks to change properties of its entity when it is +/// added and removed. #[derive(Clone)] struct NavSelected; @@ -36,17 +40,23 @@ impl Component for NavSelected { fn register_component_hooks(hooks: &mut ComponentHooks) { hooks.on_add(|mut world, entity, _id| { - let Some(mut background) = world.get_mut::(entity) else { return }; + // TODO: Color palette + let Some(mut background) = world.get_mut::(entity) else { + return; + }; *background = css::MEDIUM_SEA_GREEN.into(); }); hooks.on_remove(|mut world, entity, _id| { - let Some(mut background) = world.get_mut::(entity) else { return }; + let Some(mut background) = world.get_mut::(entity) else { + return; + }; *background = css::ROYAL_BLUE.into(); }); } } +/// Component bundle for added cursor support for selection changes. #[derive(Bundle)] pub struct NavBundle { pointer_move: On>, @@ -71,13 +81,18 @@ impl Default for NavBundle { } } +/// Prevents navigation movement actions from happenning too quickly. #[derive(Component)] struct InputRepeatDelay; +/// Uses `Action::Move` to switch the focus of the `Selected` entity inside a +/// `NavContainer` to the next or previous entity. It has wrapping and it focus +/// on the first entity if no one is selected. fn handle_next_prev( mut cmd: Commands, input: Query<&ActionState>, navigation: Query<&Children, With>, + navigables: Query<(Entity, &dyn Navigable)>, selected: Query>, repeat_delay: Query>, ) { @@ -85,8 +100,9 @@ fn handle_next_prev( return; }; - let Ok(input) = input.get_single() else { return }; + let input = single!(input); + // If using WASD, S and D will call next and W and A will call prev let val = input.clamped_axis_pair(&Action::Move); let move_forward = if val.length() > 0.2 { val.x > 0. || val.y < 0. @@ -94,6 +110,7 @@ fn handle_next_prev( return; }; + // Schedule a delay to avoid having one focus change every frame let entity = cmd.spawn(InputRepeatDelay).id(); cmd.later(0.2, move |cmd| { cmd.entity(entity).despawn(); @@ -111,8 +128,17 @@ fn handle_next_prev( let next = match curr { Some(prev) => { cmd.entity(children[prev]).remove::(); - let value = if move_forward { prev + 1 } else { prev + children.len() - 1 }; - value % children.len() + let mut next = prev; + for i in 1..children.len() { + info!("{}", i); + let value = if move_forward { prev + i } else { prev + children.len() - i } + % children.len(); + if navigables.contains(children[value]) { + next = value; + break; + } + } + next }, None => 0, }; @@ -120,14 +146,16 @@ fn handle_next_prev( } } +/// When `Action::Act` is pressed, trigger the `Selected` `Navigable::action()` +/// function. fn handle_press( input: Query<&ActionState>, selected: Query<&dyn Navigable, With>, ) { - let Ok(input) = input.get_single() else { return }; + let input = single!(input); if input.just_pressed(&Action::Act) { - let Ok(selected) = selected.get_single() else { return }; + let selected = single!(selected); for selected in &selected { selected.action(); } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index 7b9c26a..3bde193 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -1,19 +1,24 @@ #![allow(dead_code)] -use bevy::{color::palettes::css, ecs::system::EntityCommands, prelude::*, ui::Val::*}; +use bevy::{ecs::system::EntityCommands, ui::Val::*}; use bevy_mod_picking::prelude::*; use crate::prelude::*; const UI_GAP: Val = Px(10.); +pub(super) fn plugin(app: &mut App) { + app.register_component_as::(); +} + pub trait Widget { fn button(&mut self, text: impl Into) -> EntityCommands; fn text(&mut self, text: impl Into) -> EntityCommands; } -impl Widget for T { +impl Widget for T { fn button(&mut self, text: impl Into) -> EntityCommands { + let text = text.into(); let mut button = self.spawn(( NodeBundle { style: Style { @@ -26,6 +31,9 @@ impl Widget for T { background_color: css::ROYAL_BLUE.into(), ..default() }, + SimpleNavigable { + label: text.clone(), + }, NavBundle::default(), )); button.with_children(|node| { @@ -116,21 +124,54 @@ impl Stylable for NodeBundle { } } +#[derive(Component)] +struct SimpleNavigable { + label: String, +} + +impl Navigable for SimpleNavigable { + fn label(&self) -> String { + self.label.clone() + } + + fn action(&self) { + info!("action {}", self.label()); + } +} + +pub trait NavigableExt<'a> { + fn nav_container(&'a mut self) -> &mut EntityCommands; + fn no_nav(&'a mut self) -> &mut EntityCommands; +} + +impl<'a> NavigableExt<'a> for EntityCommands<'a> { + fn nav_container(&'a mut self) -> &mut EntityCommands { + self.insert(NavContainer); + self + } + + fn no_nav(&'a mut self) -> &mut EntityCommands { + self.remove::(); + self.remove::(); + self + } +} + /// An internal trait for types that can spawn entities. /// This is here so that [`Widgets`] can be implemented on all types that /// are able to spawn entities. /// Ideally, this trait should be [part of Bevy itself](https://github.com/bevyengine/bevy/issues/14231). -trait Spawn { +trait SpawnExt { fn spawn(&mut self, bundle: B) -> EntityCommands; } -impl Spawn for Commands<'_, '_> { +impl SpawnExt for Commands<'_, '_> { fn spawn(&mut self, bundle: B) -> EntityCommands { self.spawn(bundle) } } -impl Spawn for ChildBuilder<'_> { +impl SpawnExt for ChildBuilder<'_> { fn spawn(&mut self, bundle: B) -> EntityCommands { self.spawn(bundle) }