From b9655372c84a2d3838a94b9d5cb5874183265e72 Mon Sep 17 00:00:00 2001 From: eri Date: Sat, 13 Jul 2024 00:48:29 +0200 Subject: [PATCH] feat: background and menu option rows --- .github/workflows/examples.yaml | 4 +- .github/workflows/release.yaml | 6 ++- README.md | 6 +-- Trunk.toml | 2 - examples/dvd.rs | 15 ------ examples/jump.rs | 36 ++++++-------- src/camera.rs | 24 +++++++-- src/data.rs | 20 +++++++- src/ui.rs | 3 +- src/ui/menu.rs | 3 +- src/ui/menu/main.rs | 12 ++++- src/ui/menu/mappings.rs | 42 ++++++++-------- src/ui/menu/options.rs | 45 ++++++++++------- src/ui/navigation.rs | 40 +++++++++++---- src/ui/tts.rs | 88 +++++++++++++++++++++------------ src/ui/widgets.rs | 68 +++++++++++++++++++++++++ 16 files changed, 279 insertions(+), 135 deletions(-) delete mode 100644 Trunk.toml diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml index 3bb6df7..87d37df 100644 --- a/.github/workflows/examples.yaml +++ b/.github/workflows/examples.yaml @@ -32,7 +32,9 @@ jobs: uses: nixbuild/nix-quick-install-action@v28 - name: Use nix flake - uses: nicknovitski/nix-develop@v1 + uses: nicknovitski/nix-develop@v1 + with: + arguments: ".#web" - name: Rust cache uses: swatinem/rust-cache@v2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8d396a8..1c8ac61 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -43,7 +43,9 @@ jobs: uses: nixbuild/nix-quick-install-action@v28 - name: Use nix flake - uses: nicknovitski/nix-develop@v1 + uses: nicknovitski/nix-develop@v1 + with: + arguments: ".#web" - name: Rust cache uses: swatinem/rust-cache@v2 @@ -101,7 +103,7 @@ jobs: uses: nixbuild/nix-quick-install-action@v28 - name: Use nix flake - uses: nicknovitski/nix-develop@v1 + uses: nicknovitski/nix-develop@v1 - name: Rust cache uses: swatinem/rust-cache@v2 diff --git a/README.md b/README.md index f49fa1a..62f6c08 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,7 @@ cargo run you can also play around with some of the included examples with `cargo run --example `. and if you want to get started quickly, copy any example to `src/main.rs`! -in order to have a development environment up and ready, run `nix develop` or use [`nix-direnv`](https://github.com/nix-community/nix-direnv) installed create a `.envrc` like this: - -```sh -echo "use nix" > .envrc -``` +if you have nix installed, running `nix develop` you get a shell with all the dependencies already installed. ### release 馃尰 diff --git a/Trunk.toml b/Trunk.toml deleted file mode 100644 index adbb88a..0000000 --- a/Trunk.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm/index.html" diff --git a/examples/dvd.rs b/examples/dvd.rs index ee1b3e0..eb5c227 100644 --- a/examples/dvd.rs +++ b/examples/dvd.rs @@ -5,7 +5,6 @@ use bevy::{ use hello_bevy::{ assets::{CoreAssets, ExampleAssets}, camera::GameCamera, - ui::menu::BACKGROUND_COLOR, AppConfig, GamePlugin, GameState, }; @@ -77,20 +76,6 @@ fn init_sample( let size = Vec2::new(win.width(), win.height()); cmd.insert_resource(Bounds(size)); - // Background - cmd.spawn(( - SpriteBundle { - sprite: Sprite { - color: BACKGROUND_COLOR, - custom_size: Some(size), - ..default() - }, - transform: Transform::from_xyz(0., 0., -10.), - ..default() - }, - Background, - )); - // Sprites for velocity in [ Vec2::new(300., 250.), diff --git a/examples/jump.rs b/examples/jump.rs index 1c3f3d4..0a7585c 100644 --- a/examples/jump.rs +++ b/examples/jump.rs @@ -5,8 +5,8 @@ use bevy::{math::bounding::*, prelude::*}; use hello_bevy::{ assets::CoreAssets, camera::GameCamera, + data::{GameOptions, Persistent}, input::{Action, ActionState}, - ui::{menu::BACKGROUND_COLOR, widgets::BUTTON_COLOR}, AppConfig, GamePlugin, GameState, }; use rand::Rng; @@ -105,21 +105,12 @@ struct CameraFollow { // Systems // 路路路路路路路 -fn init_sample(mut cmd: Commands, assets: Res, cam: Query>) { - // Background - cmd.spawn(( - SpriteBundle { - sprite: Sprite { - color: BACKGROUND_COLOR, - custom_size: Some(LEVEL_SIZE), - ..default() - }, - transform: Transform::from_xyz(0., 0., -10.), - ..default() - }, - CameraFollow::default(), - )); - +fn init_sample( + mut cmd: Commands, + assets: Res, + options: Res>, + cam: Query>, +) { // Player cmd.spawn(( SpriteBundle { @@ -141,7 +132,7 @@ fn init_sample(mut cmd: Commands, assets: Res, cam: Query, cam: Query, player: Query<& text.sections[0].value = counter.0.to_string(); } -fn spawn_platforms(mut cmd: Commands, mut info: ResMut, player: Query<&Player>) { +fn spawn_platforms( + mut cmd: Commands, + options: Res>, + mut info: ResMut, + player: Query<&Player>, +) { let Ok(player) = player.get_single() else { return; }; @@ -323,7 +319,7 @@ fn spawn_platforms(mut cmd: Commands, mut info: ResMut, player: Qu cmd.spawn(( SpriteBundle { sprite: Sprite { - color: BUTTON_COLOR, + color: options.accent_color, custom_size: Some(PLATFORM_SIZE), ..default() }, diff --git a/src/camera.rs b/src/camera.rs index 180cad3..92c112e 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -2,6 +2,11 @@ use bevy::prelude::*; +use crate::data::{GameOptions, Persistent}; + +/// The luminance of the background color +pub const BACKGROUND_LUMINANCE: f32 = 0.05; + // 路路路路路路 // Plugin // 路路路路路路 @@ -13,7 +18,7 @@ pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { - app.add_systems(PreStartup, init); + app.add_systems(Startup, init); } } @@ -36,12 +41,25 @@ pub struct FinalCamera; // 路路路路路路路 /// Creates the main cameras before the game starts -fn init(mut cmd: Commands) { +fn init(mut cmd: Commands, options: Res>) { + let clear_color = + ClearColorConfig::Custom(options.base_color.with_luminance(BACKGROUND_LUMINANCE)); + #[cfg(not(feature = "3d_camera"))] - let camera_bundle = Camera2dBundle { ..default() }; + let camera_bundle = Camera2dBundle { + camera: Camera { + clear_color, + ..default() + }, + ..default() + }; #[cfg(feature = "3d_camera")] let camera_bundle = Camera3dBundle { + camera: Camera { + clear_color, + ..default() + }, transform: Transform::from_xyz(0.0, 0.0, 10.0), ..default() }; diff --git a/src/data.rs b/src/data.rs index 251a0a0..be7fcc6 100644 --- a/src/data.rs +++ b/src/data.rs @@ -19,7 +19,7 @@ pub struct DataPlugin; impl Plugin for DataPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, init); + app.add_systems(PreStartup, init); } } @@ -30,13 +30,29 @@ impl Plugin for DataPlugin { /// Game options /// Useful for accesibility and the settings menu /// CHANGE: Add any configurable game options here -#[derive(Default, Resource, Serialize, Deserialize)] +#[derive(Resource, Serialize, Deserialize)] pub struct GameOptions { + /// Base color of the game, used for backgrounds, etc + pub base_color: Color, + /// Accent color, meant to contrast with the base color + pub accent_color: Color, + /// Controlls if text to speech is enabled for menu navigation #[cfg(feature = "tts")] pub text_to_speech: bool, } +impl Default for GameOptions { + fn default() -> Self { + Self { + base_color: Color::srgb(0.3, 0.5, 0.9), + accent_color: Color::srgb(0.3, 0.5, 0.9), + #[cfg(feature = "tts")] + text_to_speech: default(), + } + } +} + /// Save data /// A place to save the player's progress /// CHANGE: Add relevant save data here diff --git a/src/ui.rs b/src/ui.rs index 0c0eb23..dc325ab 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -23,7 +23,8 @@ pub struct UiPlugin; impl Plugin for UiPlugin { fn build(&self, app: &mut App) { - app.add_plugins(SickleUiPlugin).add_systems(Startup, init); + app.add_plugins(SickleUiPlugin) + .add_systems(PostStartup, init); #[cfg(feature = "menu")] app.add_plugins(menu::MenuPlugin); diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 6065bbb..64e1b04 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -12,8 +12,6 @@ mod mappings; mod options; const UI_GAP: Val = Val::Px(16.); -/// Background color of the menu -pub const BACKGROUND_COLOR: Color = Color::srgba(0.0, 0.05, 0.1, 0.8); // 路路路路路路 // Plugin @@ -92,6 +90,7 @@ enum MenuButton { /// Indicates what is the state being refreshed #[derive(Component)] struct MenuRefreshState(MenuState); + // 路路路路路路路 // Systems // 路路路路路路路 diff --git a/src/ui/menu/main.rs b/src/ui/menu/main.rs index 85c8ddb..6558aa0 100644 --- a/src/ui/menu/main.rs +++ b/src/ui/menu/main.rs @@ -5,8 +5,10 @@ use sickle_ui::prelude::*; use crate::{ assets::CoreAssets, + camera::BACKGROUND_LUMINANCE, + data::{GameOptions, Persistent}, ui::{ - menu::{MenuButton, MenuState, BACKGROUND_COLOR, UI_GAP}, + menu::{MenuButton, MenuState, UI_GAP}, widgets::{UiButtonWidget, UiTextWidget}, UiRootContainer, }, @@ -24,6 +26,7 @@ pub(super) fn open( mut cmd: Commands, root: Query>, assets: Res, + options: Res>, ) { let Ok(root) = root.get_single() else { return; @@ -55,5 +58,10 @@ pub(super) fn open( }) .insert(StateScoped(MenuState::Main)) .style() - .background_color(BACKGROUND_COLOR); + .background_color( + options + .base_color + .with_luminance(BACKGROUND_LUMINANCE) + .with_alpha(0.8), + ); } diff --git a/src/ui/menu/mappings.rs b/src/ui/menu/mappings.rs index c0359f5..997bc4f 100644 --- a/src/ui/menu/mappings.rs +++ b/src/ui/menu/mappings.rs @@ -10,11 +10,13 @@ use sickle_ui::prelude::*; use crate::{ assets::CoreAssets, + camera::BACKGROUND_LUMINANCE, + data::{GameOptions, Persistent}, input::Action, ui::{ - menu::{MenuButton, MenuState, BACKGROUND_COLOR, UI_GAP}, + menu::{MenuButton, MenuState, UI_GAP}, navigation::FocusableHoverFill, - widgets::{UiButtonWidget, UiImageWidget, UiTextWidget}, + widgets::{UiButtonWidget, UiImageWidget, UiOptionRowWidget, UiTextWidget}, UiRootContainer, }, }; @@ -30,6 +32,7 @@ pub(super) fn open( input_map: Query<&InputMap>, asset_server: Res, assets: Res, + options: Res>, ) { let Ok(root) = root.get_single() else { return; @@ -54,23 +57,19 @@ pub(super) fn open( .iter() .sorted_by_key(|(&a, _)| a.variant_name().to_string()) { - column.row(|row| { - row.style() - .width(Val::Percent(80.)) - .justify_content(JustifyContent::Center) - .column_gap(Val::Px(4.)); - - row.text( - action.variant_name().into(), - assets.font.clone(), - ) - .style() - .flex_grow(1.); - - for map in maps { - row_mapping((**map).as_reflect(), row, &asset_server); - } - }); + let mut row = column.option_row( + MenuButton::None, + action.variant_name().into(), + assets.font.clone(), + ); + + for map in maps { + row_mapping( + (**map).as_reflect(), + &mut row, + &asset_server, + ); + } } column.button(MenuButton::ExitOrBack, |button| { @@ -79,7 +78,7 @@ pub(super) fn open( }) .insert(StateScoped(MenuState::Mappings)) .style() - .background_color(BACKGROUND_COLOR); + .background_color(options.base_color.with_luminance(BACKGROUND_LUMINANCE)); } // 路路路路路路路 @@ -115,11 +114,12 @@ fn row_mapping(map: &dyn Reflect, row: &mut UiBuilder, asset_server: &As for prompt in prompts { // Dynamic loading to avoid having all icons in memory - row.button(MenuButton::None, |button| { + row.option_button(|button| { button.image(asset_server.load(&prompt)); }) .insert(BorderRadius::all(Val::Px(16.))) .insert(BorderColor::from(Srgba::NONE)) + .insert(BackgroundColor::from(Srgba::NONE)) .insert(FocusableHoverFill) .style() .width(Val::Px(64.)) diff --git a/src/ui/menu/options.rs b/src/ui/menu/options.rs index 3803db8..d748cac 100644 --- a/src/ui/menu/options.rs +++ b/src/ui/menu/options.rs @@ -3,13 +3,13 @@ use bevy::prelude::*; use sickle_ui::prelude::*; -#[cfg(feature = "tts")] -use crate::data::{GameOptions, Persistent}; use crate::{ assets::CoreAssets, + camera::BACKGROUND_LUMINANCE, + data::{GameOptions, Persistent}, ui::{ - menu::{MenuButton, MenuState, BACKGROUND_COLOR, UI_GAP}, - widgets::{UiButtonWidget, UiTextWidget}, + menu::{MenuButton, MenuState, UI_GAP}, + widgets::{UiButtonWidget, UiOptionRowWidget, UiTextWidget}, UiRootContainer, }, }; @@ -23,7 +23,7 @@ pub(super) fn open( mut cmd: Commands, root: Query>, assets: Res, - #[cfg(feature = "tts")] options: Res>, + options: Res>, ) { let Ok(root) = root.get_single() else { return; @@ -40,21 +40,30 @@ pub(super) fn open( column.title("Options".into(), assets.font.clone()); - // TODO: Refactor into propper options #[cfg(feature = "tts")] - column.button(MenuButton::Speech, |button| { - button.text( - format!( - "Speech: {}", - if options.text_to_speech { "Enabled" } else { "Disabled" } - ), + column + .option_row( + MenuButton::Speech, + "Speech".into(), assets.font.clone(), - ); - }); + ) + .insert(crate::ui::menu::Focusable::new().prioritized()) + .option_button(|button| { + button.text( + (if options.text_to_speech { "Enabled" } else { "Disabled" }).into(), + assets.font.clone(), + ); + }); - column.button(MenuButton::Mappings, |button| { - button.text("Mappings".into(), assets.font.clone()); - }); + column + .option_row( + MenuButton::Mappings, + "Mappings".into(), + assets.font.clone(), + ) + .option_button(|button| { + button.text("View".into(), assets.font.clone()); + }); column.button(MenuButton::ExitOrBack, |button| { button.text("Back".into(), assets.font.clone()); @@ -62,5 +71,5 @@ pub(super) fn open( }) .insert(StateScoped(MenuState::Options)) .style() - .background_color(BACKGROUND_COLOR); + .background_color(options.base_color.with_luminance(BACKGROUND_LUMINANCE)); } diff --git a/src/ui/navigation.rs b/src/ui/navigation.rs index 67aaaff..6598773 100644 --- a/src/ui/navigation.rs +++ b/src/ui/navigation.rs @@ -27,7 +27,11 @@ impl Plugin for NavigationPlugin { .add_systems(Startup, init) .add_systems( Update, - ((handle_input, update_focus).run_if(in_state(GameState::Menu)),), + (( + handle_input.before(NavRequestSystem), + update_focus.after(NavRequestSystem), + ) + .run_if(in_state(GameState::Menu)),), ); #[cfg(feature = "menu")] @@ -61,6 +65,10 @@ pub(super) enum UiAction { #[derive(Component)] pub(super) struct FocusableHoverFill; +/// A focusable that should hightlight its children, not itself +#[derive(Component)] +pub(super) struct HightlightChild; + // 路路路路路路路 // Systems // 路路路路路路路 @@ -100,17 +108,31 @@ fn init(mut cmd: Commands) { fn update_focus( mut focusables: Query< ( + Entity, &Focusable, - Option<&mut BorderColor>, - Option<&mut BackgroundColor>, - Option<&FocusableHoverFill>, + Option<&Children>, + Option<&HightlightChild>, ), - (Changed,), + Changed, >, + mut border: Query<&mut BorderColor>, + mut background: Query<&mut BackgroundColor>, + fill: Query<&FocusableHoverFill>, ) { - for (focus, border, background, fill) in focusables.iter_mut() { - if fill.is_some() { - let Some(mut color) = background else { + for (entity, focus, children, highlight_child) in focusables.iter_mut() { + let entity = match highlight_child { + Some(_) => { + if let Some(children) = children { + *children.last().unwrap_or(&entity) + } else { + entity + } + }, + None => entity, + }; + + if fill.contains(entity) { + let Ok(mut color) = background.get_mut(entity) else { continue; }; *color = match focus.state() { @@ -119,7 +141,7 @@ fn update_focus( } .into(); } else { - let Some(mut color) = border else { + let Ok(mut color) = border.get_mut(entity) else { continue; }; *color = match focus.state() { diff --git a/src/ui/tts.rs b/src/ui/tts.rs index 52394fa..c7e9abb 100644 --- a/src/ui/tts.rs +++ b/src/ui/tts.rs @@ -17,7 +17,7 @@ pub struct SpeechPlugin; impl Plugin for SpeechPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, init); + app.add_systems(PostStartup, init); #[cfg(feature = "navigation")] app.add_systems( @@ -94,8 +94,15 @@ fn init(mut cmd: Commands) { #[cfg(feature = "navigation")] fn navigation_speech( - focusables: Query>, - speech_tag: Query<(Entity, &SpeechTag, Option<&Parent>)>, + query: Query< + ( + Entity, + Option<&Focusable>, + Option<&Children>, + ), + With, + >, + speech_tag: Query<&SpeechTag>, options: Res>, speech: Option>, mut nav_event_reader: EventReader, @@ -108,20 +115,24 @@ fn navigation_speech( } for event in nav_event_reader.read() { - match event { - NavEvent::FocusChanged { to, from: _ } => speak_focusable( - to.first(), - &focusables, - &speech_tag, - &mut speech, - ), - NavEvent::InitiallyFocused(to) => speak_focusable( - to, - &focusables, + let to = match event { + NavEvent::FocusChanged { to, from: _ } => to.first(), + NavEvent::InitiallyFocused(to) => to, + _ => { + continue; + }, + }; + for (entity, focusable, _) in query.iter() { + if focusable.is_none() || entity != *to { + continue; + } + speak_focusable( + entity, + &query, &speech_tag, &mut speech, - ), - _ => {}, + true, + ); } } } @@ -131,24 +142,37 @@ fn navigation_speech( // 路路路路路路路 fn speak_focusable( - to: &Entity, - focusables: &Query>, - speech_tag: &Query<(Entity, &SpeechTag, Option<&Parent>)>, + current: Entity, + query: &Query< + ( + Entity, + Option<&Focusable>, + Option<&Children>, + ), + With, + >, + speech_tag: &Query<&SpeechTag>, speech: &mut Speech, -) { - for (entity, tag, parent) in speech_tag.iter() { - let focus = focusables.get(entity).unwrap_or({ - let Some(parent) = parent else { - continue; - }; - let Ok(focus) = focusables.get(parent.get()) else { - continue; - }; - focus - }); - if *to == focus { - speech.speak(&tag.0, true); - return; + interrupt: bool, +) -> bool { + let Ok((entity, _, children)) = query.get(current) else { + return false; + }; + + let mut interrupt = interrupt; + if let Ok(tag) = speech_tag.get(entity) { + speech.speak(&tag.0, interrupt); + interrupt = false; + } + + if let Some(children) = children { + info!("call children"); + for &child in children { + interrupt = speak_focusable( + child, query, speech_tag, speech, interrupt, + ); } } + + interrupt } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index b68937d..9384d02 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -7,6 +7,8 @@ use bevy::prelude::*; use sickle_ui::prelude::*; +use super::navigation::HightlightChild; + const BUTTON_WIDTH: Val = Val::Px(256.); const BUTTON_HEIGHT: Val = Val::Px(64.); @@ -118,3 +120,69 @@ impl UiButtonWidget for UiBuilder<'_, Entity> { ) } } + +/// Creates an option row +/// It consist of a name to the left and anything to the left +pub trait UiOptionRowWidget { + /// Append an option row element + fn option_row( + &mut self, + component: T, + text: String, + font: Handle, + ) -> UiBuilder; + + /// Option row button + fn option_button( + &mut self, + spawn_children: impl FnOnce(&mut UiBuilder), + ) -> UiBuilder; +} + +impl UiOptionRowWidget for UiBuilder<'_, Entity> { + fn option_row( + &mut self, + component: T, + text: String, + font: Handle, + ) -> UiBuilder { + self.row(|row| { + row.style() + .width(Val::Percent(80.)) + .justify_content(JustifyContent::Center) + .column_gap(Val::Px(4.)); + + row.text(text, font).style().flex_grow(1.); + + row.insert(( + #[cfg(feature = "navigation")] + bevy_alt_ui_navigation_lite::prelude::Focusable::default(), + component, + HightlightChild, + )); + }) + } + + fn option_button( + &mut self, + spawn_children: impl FnOnce(&mut UiBuilder), + ) -> UiBuilder { + self.container( + ButtonType { + style: Style { + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + border: UiRect::all(Val::Px(6.0)), + ..default() + }, + background_color: BUTTON_COLOR.into(), + #[cfg(not(feature = "pixel_perfect"))] + border_radius: BorderRadius::MAX, + ..default() + }, + spawn_children, + ) + } +}