diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f17c90e..46e33b2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -251,7 +251,7 @@ jobs: tag: ${{ github.ref }} overwrite: true - check-upload-to-itch: + check-if-upload-to-itch-is-configured: runs-on: ubuntu-latest outputs: should-upload: ${{ steps.check-env.outputs.has-itch-target }} @@ -259,21 +259,22 @@ jobs: - id: check-env run: | if [[ -z "$itch_target" ]]; then - echo "has-itch-target == no >> $GITHUB_OUTPUT" + echo "has-itch-target=no" >> $GITHUB_OUTPUT else - echo "has-itch-target == yes >> $GITHUB_OUTPUT" + echo "has-itch-target=yes" >> $GITHUB_OUTPUT fi upload-to-itch: runs-on: ubuntu-latest needs: - - get-version - - check-upload-to-itch + - check-if-upload-to-itch-is-configured - release-wasm - release-linux - - release-windows - - release-macos - if: ${{ needs.check-upload-to-itch.outputs.should-upload == 'yes' }} + # [CHANGE]: Uncomment this if you also want to push windows and macos builds to itch automatically + # It is disabled by default because these take a looong time (specially macos) + # - release-windows + # - release-macos + if: ${{ needs.check-if-upload-to-itch-is-configured.outputs.should-upload == 'yes' }} env: version: ${{needs.get-version.outputs.version}} diff --git a/examples/dvd.rs b/examples/dvd.rs index 0e15667..dec640b 100644 --- a/examples/dvd.rs +++ b/examples/dvd.rs @@ -1,6 +1,8 @@ use bevy::{prelude::*, window::WindowResolution}; use bevy_kira_audio::prelude::*; +use bevy_persistent::Persistent; use hello_bevy::{ + config::GameOptions, load::{GameAssets, SampleAssets}, GamePlugin, GameState, }; @@ -75,7 +77,12 @@ struct CollisionEvent(Entity); // Systems // ······· -fn init_sample(mut cmd: Commands, assets: Res, info: Option>) { +fn init_sample( + mut cmd: Commands, + assets: Res, + opts: Res>, + info: Option>, +) { cmd.spawn((Camera2dBundle::default(), GameCamera)); if info.is_some() { @@ -110,7 +117,7 @@ fn init_sample(mut cmd: Commands, assets: Res, info: Option, info: Option>) { +fn init_sample( + mut cmd: Commands, + assets: Res, + opts: Res>, + info: Option>, +) { cmd.spawn((Camera2dBundle::default(), GameCamera)); if info.is_some() { @@ -104,7 +114,7 @@ fn init_sample(mut cmd: Commands, assets: Res, info: Option String { +impl ToString for Bind { + fn to_string(&self) -> String { match self { Bind::Key(key) => format!("{:?}", key), Bind::Mouse(button) => format!("{:?}", button), diff --git a/src/menu.rs b/src/menu.rs index f1a3b3b..54e43c1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -9,9 +9,7 @@ use crate::{ use bevy::prelude::*; use bevy::reflect::Struct; -// TODO: Extract styles into external functions (maybe create ui package) -// TODO: Change the create functions to be more modular -// TODO: Single UI camera (for debug fps as well +// TODO: Single UI camera (for debug fps as well) // TODO: Tweening and animation // ······ @@ -37,12 +35,16 @@ impl Plugin for MenuPlugin { clean_menu.run_if(in_state(GameState::Menu)), ) .add_systems( - OnEnter(MenuState::Options), + OnEnter(MenuState::Settings), clean_menu.run_if(in_state(GameState::Menu)), ) .add_systems( OnEnter(MenuState::Keybinds), clean_menu.run_if(in_state(GameState::Menu)), + ) + .add_systems( + OnEnter(MenuState::Visual), + clean_menu.run_if(in_state(GameState::Menu)), ); } } @@ -51,8 +53,9 @@ impl Plugin for MenuPlugin { pub enum MenuState { #[default] Main, - Options, + Settings, Keybinds, + Visual, } // ·········· @@ -72,10 +75,12 @@ struct MenuText; enum MenuButton { Play, GoMain, - GoOptions, + GoSettings, GoKeybinds, - OptionsColor(String), + GoVisual, RemapKeybind(String, Vec), + ChangeFont(String), + ChangeColor(String, String), } // ······· @@ -94,7 +99,7 @@ fn init_menu(mut cmd: Commands, opts: Res>, style: Res { menu_state.set(MenuState::Main); } - MenuButton::GoOptions => { - menu_state.set(MenuState::Options); + MenuButton::GoSettings => { + menu_state.set(MenuState::Settings); } MenuButton::GoKeybinds => { menu_state.set(MenuState::Keybinds); } - MenuButton::OptionsColor(field) => { - // TODO: Add color picker - info!("color {}", field); + MenuButton::GoVisual => { + menu_state.set(MenuState::Visual); } - MenuButton::RemapKeybind(field, _) => { + MenuButton::RemapKeybind(_, _) => { // TODO: Remap keymaps - info!("remap {}", field); + } + MenuButton::ChangeFont(_) => { + // TODO: Change font size + } + MenuButton::ChangeColor(_, _) => { + // TODO: Change color } } } @@ -175,8 +184,9 @@ fn clean_menu( match state.get() { MenuState::Main => layout_main(cmd, node, &style), - MenuState::Options => layout_options(cmd, node, &style, &opts), + MenuState::Settings => layout_options(cmd, node, &style), MenuState::Keybinds => layout_keybinds(cmd, node, &style, &keybinds), + MenuState::Visual => layout_visual(cmd, node, &style, &opts), } } @@ -203,8 +213,11 @@ fn return_to_menu( let input = InputState::new(&keyboard, &mouse, &gamepad); if input.just_pressed(&keybinds.pause).unwrap_or(false) { - if *current_menu_state.get() != MenuState::Main { - next_menu_state.set(MenuState::Main); + match *current_menu_state.get() { + MenuState::Settings => next_menu_state.set(MenuState::Main), + MenuState::Keybinds => next_menu_state.set(MenuState::Settings), + MenuState::Visual => next_menu_state.set(MenuState::Settings), + _ => {} } game_state.set(GameState::Menu); } @@ -216,63 +229,107 @@ fn return_to_menu( fn layout_main(mut cmd: Commands, node: Entity, style: &UIStyle) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Hello Bevy"); + UIText::new(style, "Hello Bevy").with_title().add(parent); - create_button(parent, style, "Play", MenuButton::Play); - create_button(parent, style, "Options", MenuButton::GoOptions); + UIButton::new(style, "Play", MenuButton::Play).add(parent); + UIButton::new(style, "Settings", MenuButton::GoSettings).add(parent); }); } -fn layout_options(mut cmd: Commands, node: Entity, style: &UIStyle, opts: &GameOptions) { +fn layout_options(mut cmd: Commands, node: Entity, style: &UIStyle) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Options"); + UIText::new(style, "Settings").with_title().add(parent); - create_button(parent, style, "Keybinds", MenuButton::GoKeybinds); + UIButton::new(style, "Keybinds", MenuButton::GoKeybinds).add(parent); + UIButton::new(style, "Visual", MenuButton::GoVisual).add(parent); - for (i, value) in opts.color.iter_fields().enumerate() { - let field_name = opts.color.name_at(i).unwrap(); - if let Some(value) = value.downcast_ref::() { - let r = value.r(); - let g = value.g(); - let b = value.b(); - create_button( - parent, - style, - &format!( - "{}: {:.0},{:.0},{:.0}", - field_name, - r * 255., - g * 255., - b * 255. - ), - MenuButton::OptionsColor(field_name.to_string()), - ); - } - } - - create_button(parent, style, "Back", MenuButton::GoMain); + UIButton::new(style, "Back", MenuButton::GoMain).add(parent); }); } fn layout_keybinds(mut cmd: Commands, node: Entity, style: &UIStyle, keybinds: &Keybinds) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Keybinds"); + UIText::new(style, "Options").with_title().add(parent); - // TODO: Scrollable section + // TODO: Scrollable section (Requires #8104 to be merged in 0.13) for (i, value) in keybinds.iter_fields().enumerate() { let field_name = keybinds.name_at(i).unwrap(); if let Some(value) = value.downcast_ref::>() { - create_keybind_remap( - parent, - style, - field_name, - MenuButton::RemapKeybind(field_name.to_string(), value.clone()), - value, - ); + UIOption::new(style, field_name).add(parent, |row| { + let keys = value + .iter() + .map(|bind| bind.to_string()) + .collect::>() + .join(" "); + + UIButton::new( + style, + &keys, + MenuButton::RemapKeybind(field_name.to_string(), value.clone()), + ) + .with_width(Val::Px(64.)) + .add(row); + }); + } + } + + UIButton::new(style, "Back", MenuButton::GoSettings).add(parent); + }); +} + +fn layout_visual(mut cmd: Commands, node: Entity, style: &UIStyle, opts: &GameOptions) { + cmd.entity(node).with_children(|parent| { + UIText::new(style, "Visual settings") + .with_title() + .add(parent); + + for (i, value) in opts.font_size.iter_fields().enumerate() { + let field_name = opts.font_size.name_at(i).unwrap().to_string(); + if let Some(value) = value.downcast_ref::() { + UIOption::new(style, &format!("font_{}", field_name)).add(parent, |row| { + UIButton::new( + style, + &format!("{}", value), + MenuButton::ChangeFont(field_name), + ) + .with_width(Val::Px(40.)) + .add(row); + }); + } + } + + for (i, value) in opts.color.iter_fields().enumerate() { + let field_name = format!("color_{}", opts.color.name_at(i).unwrap()); + if let Some(value) = value.downcast_ref::() { + UIOption::new(style, &field_name).add(parent, |row| { + UIButton::new( + style, + &format!("{:.0}", value.r() * 255.), + MenuButton::ChangeColor(field_name.clone(), "r".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + + UIButton::new( + style, + &format!("{:.0}", value.g() * 255.), + MenuButton::ChangeColor(field_name.clone(), "g".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + + UIButton::new( + style, + &format!("{:.0}", value.b() * 255.), + MenuButton::ChangeColor(field_name.clone(), "b".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + }); } } - create_button(parent, style, "Back", MenuButton::GoOptions); + UIButton::new(style, "Back", MenuButton::GoSettings).add(parent); }); } diff --git a/src/ui.rs b/src/ui.rs index f994386..24c93e0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,11 @@ use bevy::prelude::*; use bevy_persistent::Persistent; -use crate::{config::GameOptions, input::Bind, load::GameAssets}; +use crate::{config::GameOptions, load::GameAssets}; -// TODO: Input field -// TODO: Color picker +const MENU_WIDTH: Val = Val::Px(300.); +const MENU_ITEM_HEIGHT: Val = Val::Px(40.); +const MENU_ITEM_GAP: Val = Val::Px(10.); // ······ // Plugin @@ -29,8 +30,9 @@ impl Plugin for UIPlugin { pub struct UIStyle { pub title: TextStyle, pub text: TextStyle, - pub button: Style, pub button_text: TextStyle, + + pub button: Style, pub button_bg: BackgroundColor, } @@ -55,20 +57,20 @@ pub fn change_style( color: opts.color.mid, }; + style.button_text = TextStyle { + font: assets.font.clone(), + font_size: opts.font_size.button_text, + color: opts.color.dark, + }; + style.button = Style { - width: Val::Px(196.), - height: Val::Px(48.), + width: MENU_WIDTH, + height: MENU_ITEM_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }; - style.button_text = TextStyle { - font: assets.font.clone(), - font_size: 24., - color: opts.color.dark, - }; - style.button_bg = opts.color.light.into(); } @@ -76,98 +78,118 @@ pub fn change_style( // Extra // ····· -pub fn create_title(parent: &mut ChildBuilder, style: &UIStyle, text: &str) { - parent.spawn(TextBundle::from_section(text, style.title.clone())); +// Text + +pub struct UIText<'a> { + text: TextBundle, + style: &'a UIStyle, +} + +impl<'a> UIText<'a> { + pub fn new(style: &'a UIStyle, text: &str) -> Self { + Self { + text: TextBundle::from_section(text, style.text.clone()), + style, + } + } + + pub fn with_title(mut self) -> Self { + self.text.text.sections[0].style = self.style.title.clone(); + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.text.style = style; + self + } + + pub fn add(self, parent: &mut ChildBuilder) { + parent.spawn(self.text); + } } -pub fn create_button( - parent: &mut ChildBuilder, - style: &UIStyle, - text: &str, +// Button + +pub struct UIButton { + button: ButtonBundle, + text: TextBundle, action: T, -) { - parent - .spawn(( - ButtonBundle { +} + +impl UIButton { + pub fn new(style: &UIStyle, text: &str, action: T) -> Self { + Self { + button: ButtonBundle { style: style.button.clone(), background_color: style.button_bg, ..default() }, + text: TextBundle::from_section(text, style.button_text.clone()), action, - )) - .with_children(|parent| { - parent.spawn(TextBundle::from_section(text, style.button_text.clone())); - }); + } + } + + pub fn with_width(mut self, width: Val) -> Self { + self.button.style.width = width; + self + } + + pub fn add(self, parent: &mut ChildBuilder) { + parent + .spawn((self.button, self.action)) + .with_children(|button| { + button.spawn(self.text); + }); + } } -pub fn create_keybind_remap( - parent: &mut ChildBuilder, - style: &UIStyle, - text: &str, - action: T, - bind: &[Bind], -) { - parent - .spawn(NodeBundle { - style: Style { - min_width: Val::Px(196.), - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - column_gap: Val::Px(12.), +// Option row (label text + widget) + +pub struct UIOption<'a> { + row: NodeBundle, + label: UIText<'a>, +} + +impl<'a> UIOption<'a> { + pub fn new(style: &'a UIStyle, label: &str) -> Self { + Self { + row: NodeBundle { + style: Style { + width: MENU_WIDTH, + column_gap: MENU_ITEM_GAP, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, ..default() }, - ..default() - }) - .with_children(|parent| { - let name = text - .chars() - .enumerate() - .map(|(i, c)| { - if i == 0 { - c.to_uppercase().next().unwrap() - } else if c == '_' { - ' ' - } else { - c - } - }) - .collect::(); - - parent.spawn( - TextBundle::from_section(name, style.text.clone()).with_style(Style { - flex_grow: 1., - ..default() - }), - ); - - parent - .spawn(( - ButtonBundle { - style: Style { - width: Val::Px(96.), - ..style.button.clone() - }, - background_color: style.button_bg, - ..default() - }, - action, - )) - .with_children(|parent| { - let name = bind - .iter() - .map(|bind| bind.name()) - .collect::>() - .join(", "); - let font_size = if name.len() > 1 { 16. } else { 24. }; - parent.spawn(TextBundle::from_section( - name, - TextStyle { - font: style.button_text.font.clone(), - font_size, - color: style.button_text.color, - }, - )); - }); + label: UIText::new(style, &snake_to_upper(label)).with_style(Style { + flex_grow: 1., + ..default() + }), + } + } + + pub fn add(self, parent: &mut ChildBuilder, children: impl FnOnce(&mut ChildBuilder)) { + parent.spawn(self.row).with_children(|row| { + self.label.add(row); + children(row); }); + } +} + +pub fn snake_to_upper(text: &str) -> String { + text.chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().next().unwrap() + } else if c == '_' { + ' ' + } else { + c + } + }) + .collect::() }