DEVELOPMENT ENVIRONMENT

~liljamo/deck-builder

8d3ea6fb5549f0af96f594bd4302f8fc7e37c17f — Jonni Liljamo 1 year, 10 months ago 3a56c8b
Basic menu layout, missing functionality still
M sdbclient/src/main.rs => sdbclient/src/main.rs +70 -7
@@ 7,14 7,50 @@
 */

use bevy::{
    app::{App, PluginGroup},
    window::{
        CompositeAlphaMode, CursorGrabMode, MonitorSelection, PresentMode, WindowDescriptor,
        WindowMode, WindowPlugin, WindowPosition, WindowResizeConstraints,
    },
    DefaultPlugins,
    prelude::*,
    window::{CompositeAlphaMode, CursorGrabMode, PresentMode, WindowResizeConstraints},
};

use bevy_egui::EguiPlugin;
use bevy_inspector_egui::WorldInspectorPlugin;

mod menu;
mod splash;
mod util;

/// Used to control the state of the game
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum GameState {
    Splash,
    MainMenu,
    Game,
}

// Various settings that can be changed from the... Settings.
/// Volume
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
pub struct CfgVolume(u32);
/// Fullscreen
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
pub struct CfgFullscreen(bool);
/// Resolution
#[derive(Resource, Debug, Component, PartialEq, Clone, Copy)]
pub struct CfgResolution(f32, f32);

// Settings that the user has no access to, or can only access through developer settings
/// API Server
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct CfgAPIServer(String);
/// User logged in status
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
pub struct CfgLoggedIn(bool);
/// Login username inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct CfgUserLoginInputs(String, String);
/// User Token
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct CfgUserToken(String);

fn main() {
    let mut app = App::new();



@@ 33,7 69,7 @@ fn main() {
            scale_factor_override: Some(1.),
            title: "Deck Builder".to_string(),
            present_mode: PresentMode::Fifo,
            resizable: true,
            resizable: false,
            decorations: true,
            cursor_visible: true,
            cursor_grab_mode: CursorGrabMode::None,


@@ 46,5 82,32 @@ fn main() {
        ..Default::default()
    }));

    app.add_plugin(EguiPlugin);
    app.add_plugin(WorldInspectorPlugin::new());

    app.insert_resource(CfgVolume(7))
        .insert_resource(CfgFullscreen(false))
        .insert_resource(CfgResolution(1280., 720.))
        .insert_resource(CfgAPIServer("http://localhost:8080".to_string()))
        .insert_resource(CfgUserLoginInputs("".to_string(), "".to_string()))
        .insert_resource(CfgLoggedIn(false))
        .insert_resource(CfgUserToken("".to_string()));

    app.add_startup_system(setup).add_state(GameState::Splash);

    app.add_plugin(splash::SplashPlugin)
        .add_plugin(menu::MenuPlugin);

    app.run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera3dBundle::default());
}

/// Utility function do despawn an entity and all its children
pub fn despawn_screen<T: Component>(to_despawn: Query<Entity, With<T>>, mut commands: Commands) {
    for entity in &to_despawn {
        commands.entity(entity).despawn_recursive();
    }
}

A sdbclient/src/menu/accountloginui.rs => sdbclient/src/menu/accountloginui.rs +43 -0
@@ 0,0 1,43 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::prelude::*;
use bevy_egui::{egui, EguiContext};

use crate::{util::eguipwd, CfgUserLoginInputs};

use super::MenuState;

pub fn account_login_ui(
    mut egui_context: ResMut<EguiContext>,
    mut menu_state: ResMut<State<MenuState>>,
    mut inputs: ResMut<CfgUserLoginInputs>,
) {
    egui::Window::new("Login")
        .collapsible(false)
        .show(egui_context.ctx_mut(), |ui| {
            ui.horizontal(|ui| {
                ui.label("Username: ");
                ui.text_edit_singleline(&mut inputs.0);
            });

            ui.horizontal(|ui| {
                ui.label("Password: ");
                ui.add(eguipwd::password(&mut inputs.1));
            });

            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
                if ui.button("Cancel").clicked() {
                    menu_state.set(MenuState::Main).unwrap();
                }
                if ui.button("Login").clicked() {
                    info!("todo");
                }
            })
        });
}

A sdbclient/src/menu/accountscreenloggedin.rs => sdbclient/src/menu/accountscreenloggedin.rs +99 -0
@@ 0,0 1,99 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings menu screen
#[derive(Component)]
pub struct OnAccountLoggedInScreen;

pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let font = asset_server.load("fonts/FiraMono-Regular.ttf");

    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: font.clone(),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    flex_direction: FlexDirection::Column,
                    ..default()
                },
                ..default()
            },
            OnAccountLoggedInScreen,
        ))
        .with_children(|parent| {
            parent.spawn(
                TextBundle::from_section(
                    "Account",
                    TextStyle {
                        font: font.clone(),
                        font_size: 60.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [
                        (MenuButtonAction::AccountLogin, "Logout"),
                        (MenuButtonAction::BackToMainMenu, "Back"),
                    ] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/menu/accountscreenloggedout.rs => sdbclient/src/menu/accountscreenloggedout.rs +100 -0
@@ 0,0 1,100 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings menu screen
#[derive(Component)]
pub struct OnAccountLoggedOutScreen;

pub fn account_loggedout_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let font = asset_server.load("fonts/FiraMono-Regular.ttf");

    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: font.clone(),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    flex_direction: FlexDirection::Column,
                    ..default()
                },
                ..default()
            },
            OnAccountLoggedOutScreen,
        ))
        .with_children(|parent| {
            parent.spawn(
                TextBundle::from_section(
                    "Account",
                    TextStyle {
                        font: font.clone(),
                        font_size: 60.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [
                        (MenuButtonAction::AccountLogin, "Login"),
                        (MenuButtonAction::AccountRegister, "Register"),
                        (MenuButtonAction::BackToMainMenu, "Back"),
                    ] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/menu/mainmenuscreen.rs => sdbclient/src/menu/mainmenuscreen.rs +90 -0
@@ 0,0 1,90 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on menu screen
#[derive(Component)]
pub struct OnMainMenuScreen;

pub fn main_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let font = asset_server.load("fonts/FiraMono-Regular.ttf");

    let button_style = Style {
        size: Size::new(Val::Px(250.), Val::Px(65.)),
        margin: UiRect::all(Val::Px(20.)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..Default::default()
    };

    let button_text_style = TextStyle {
        font: font.clone(),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.), Val::Percent(100.)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    flex_direction: FlexDirection::Column,
                    ..Default::default()
                },
                ..Default::default()
            },
            OnMainMenuScreen,
        ))
        .with_children(|parent| {
            // Game title, currently text
            // TODO: Change to a fancy image logo
            parent.spawn(
                TextBundle::from_section(
                    "Deck Builder",
                    TextStyle {
                        font: font.clone(),
                        font_size: 80.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );

            // Main menu buttons
            for (action, text) in [
                (MenuButtonAction::Play, "Play"),
                (MenuButtonAction::Account, "Account"),
                (MenuButtonAction::Settings, "Settings"),
                (MenuButtonAction::Exit, "Exit"),
            ] {
                parent
                    .spawn((
                        ButtonBundle {
                            style: button_style.clone(),
                            background_color: NORMAL_BUTTON.into(),
                            ..Default::default()
                        },
                        action,
                    ))
                    .with_children(|parent| {
                        parent.spawn(TextBundle::from_section(text, button_text_style.clone()));
                    });
            }
        });
}

A sdbclient/src/menu/mod.rs => sdbclient/src/menu/mod.rs +193 -0
@@ 0,0 1,193 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{app::AppExit, prelude::*};

use crate::CfgLoggedIn;

use super::{despawn_screen, GameState};

mod mainmenuscreen;
use mainmenuscreen::*;

mod settingsmenuscreen;
use settingsmenuscreen::*;

mod settingsdisplayscreen;
use settingsdisplayscreen::*;

mod settingsaudioscreen;
use settingsaudioscreen::*;

mod settingsmiscscreen;
use settingsmiscscreen::*;

mod accountscreenloggedout;
use accountscreenloggedout::*;

mod accountscreenloggedin;
use accountscreenloggedin::*;

mod accountloginui;
use accountloginui::*;

const TEXT_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);

pub struct MenuPlugin;

impl Plugin for MenuPlugin {
    fn build(&self, app: &mut App) {
        app.
            // Start with no menu. The menu is loaded when the GameState::MainMenu is entered.
            add_state(MenuState::None)
            .add_system_set(SystemSet::on_enter(GameState::MainMenu).with_system(menu_setup))
            // Systems for main menu screen
            .add_system_set(SystemSet::on_enter(MenuState::Main).with_system(main_menu_setup))
            .add_system_set(SystemSet::on_exit(MenuState::Main).with_system(despawn_screen::<OnMainMenuScreen>))
            // Systems for settings menu screen
            .add_system_set(SystemSet::on_enter(MenuState::Settings).with_system(settings_menu_setup))
            .add_system_set(SystemSet::on_exit(MenuState::Settings).with_system(despawn_screen::<OnSettingsMenuScreen>))
            // Systems for settings display screen
            .add_system_set(SystemSet::on_enter(MenuState::SettingsDisplay).with_system(settings_display_setup))
            .add_system_set(SystemSet::on_exit(MenuState::SettingsDisplay).with_system(despawn_screen::<OnSettingsDisplayScreen>))
            // Systems for settings audio screen
            .add_system_set(SystemSet::on_enter(MenuState::SettingsAudio).with_system(settings_audio_setup))
            .add_system_set(SystemSet::on_exit(MenuState::SettingsAudio).with_system(despawn_screen::<OnSettingsAudioScreen>))
            // Systems for settings misc screen
            .add_system_set(SystemSet::on_enter(MenuState::SettingsMisc).with_system(settings_misc_setup))
            .add_system_set(SystemSet::on_exit(MenuState::SettingsMisc).with_system(despawn_screen::<OnSettingsMiscScreen>))
            // Systems for account loggedout screen
            .add_system_set(SystemSet::on_enter(MenuState::AccountLoggedOut).with_system(account_loggedout_setup))
            .add_system_set(SystemSet::on_exit(MenuState::AccountLoggedOut).with_system(despawn_screen::<OnAccountLoggedOutScreen>))
            // Systems for account loggedin screen
            .add_system_set(SystemSet::on_enter(MenuState::AccountLoggedIn).with_system(account_loggedin_setup))
            .add_system_set(SystemSet::on_exit(MenuState::AccountLoggedIn).with_system(despawn_screen::<OnAccountLoggedInScreen>))
            // Account login and register systems
            .add_system_set(SystemSet::on_update(MenuState::AccountLogin).with_system(account_login_ui))
            // Common systems
            .add_system_set(SystemSet::on_update(GameState::MainMenu).with_system(menu_action).with_system(button_system));
    }
}

/// Menu State
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub enum MenuState {
    None,
    Main,
    Settings,
    SettingsDisplay,
    SettingsAudio,
    SettingsMisc,
    AccountLoggedIn,
    AccountLoggedOut,
    AccountLogin,
    AccountRegister,
}

/// Tag component for tagging entities on account screen
#[derive(Component)]
struct OnAccountScreen;

/// Tag component for tagging entities on login screen
#[derive(Component)]
struct OnLoginScreen;

/// Tag component for tagging entities on register screen
#[derive(Component)]
struct OnRegisterScreen;

const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25);
const HOVERED_PRESSED_BUTTON: Color = Color::rgb(0.25, 0.65, 0.25);
const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.75, 0.35);

/// Tag component for tagging currently selected settings tab
#[derive(Component)]
struct SelectedSettingsTab;

/// All button actions
#[derive(Component)]
enum MenuButtonAction {
    Play,
    Settings,
    SettingsDisplay,
    SettingsAudio,
    SettingsMisc,
    Account,
    AccountLogin,
    AccountRegister,
    BackToMainMenu,
    BackToSettings,
    Exit,
}

fn menu_action(
    mut interaction_query: Query<
        (&Interaction, &MenuButtonAction),
        (Changed<Interaction>, With<Button>),
    >,
    mut app_exit_events: EventWriter<AppExit>,
    mut menu_state: ResMut<State<MenuState>>,
    mut game_state: ResMut<State<GameState>>,
    mut logged_in: Res<CfgLoggedIn>,
) {
    for (interaction, menu_button_action) in &interaction_query {
        if *interaction == Interaction::Clicked {
            match menu_button_action {
                MenuButtonAction::Exit => app_exit_events.send(AppExit),
                MenuButtonAction::Play => println!("todo"),
                MenuButtonAction::Settings => menu_state.set(MenuState::Settings).unwrap(),
                MenuButtonAction::SettingsDisplay => {
                    menu_state.set(MenuState::SettingsDisplay).unwrap()
                }
                MenuButtonAction::SettingsAudio => {
                    menu_state.set(MenuState::SettingsAudio).unwrap()
                }
                MenuButtonAction::SettingsMisc => menu_state.set(MenuState::SettingsMisc).unwrap(),
                MenuButtonAction::Account => {
                    if logged_in.0 {
                        menu_state.set(MenuState::AccountLoggedIn).unwrap()
                    } else {
                        menu_state.set(MenuState::AccountLoggedOut).unwrap()
                    }
                }
                MenuButtonAction::AccountLogin => menu_state.set(MenuState::AccountLogin).unwrap(),
                MenuButtonAction::AccountRegister => {
                    menu_state.set(MenuState::AccountRegister).unwrap()
                }
                MenuButtonAction::BackToSettings => menu_state.set(MenuState::Settings).unwrap(),
                MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main).unwrap(),
            }
        }
    }
}

/// System for handling button hovering
fn button_system(
    mut interaction_query: Query<
        (
            &Interaction,
            &mut BackgroundColor,
            Option<&SelectedSettingsTab>,
        ),
        (Changed<Interaction>, With<Button>),
    >,
) {
    for (interaction, mut color, selected) in &mut interaction_query {
        *color = match (*interaction, selected) {
            (Interaction::Clicked, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
            (Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
            (Interaction::Hovered, None) => HOVERED_BUTTON.into(),
            (Interaction::None, None) => NORMAL_BUTTON.into(),
        }
    }
}

fn menu_setup(mut menu_state: ResMut<State<MenuState>>) {
    let _ = menu_state.set(MenuState::Main);
}

A sdbclient/src/menu/settingsaudioscreen.rs => sdbclient/src/menu/settingsaudioscreen.rs +79 -0
@@ 0,0 1,79 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings volume screen
#[derive(Component)]
pub struct OnSettingsAudioScreen;

pub fn settings_audio_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: asset_server.load("fonts/FiraMono-Regular.ttf"),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    ..default()
                },
                ..default()
            },
            OnSettingsAudioScreen,
        ))
        .with_children(|parent| {
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [(MenuButtonAction::BackToSettings, "Back")] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/menu/settingsdisplayscreen.rs => sdbclient/src/menu/settingsdisplayscreen.rs +79 -0
@@ 0,0 1,79 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings display screen
#[derive(Component)]
pub struct OnSettingsDisplayScreen;

pub fn settings_display_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: asset_server.load("fonts/FiraMono-Regular.ttf"),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    ..default()
                },
                ..default()
            },
            OnSettingsDisplayScreen,
        ))
        .with_children(|parent| {
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [(MenuButtonAction::BackToSettings, "Back")] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/menu/settingsmenuscreen.rs => sdbclient/src/menu/settingsmenuscreen.rs +101 -0
@@ 0,0 1,101 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings menu screen
#[derive(Component)]
pub struct OnSettingsMenuScreen;

pub fn settings_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let font = asset_server.load("fonts/FiraMono-Regular.ttf");

    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: font.clone(),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    flex_direction: FlexDirection::Column,
                    ..default()
                },
                ..default()
            },
            OnSettingsMenuScreen,
        ))
        .with_children(|parent| {
            parent.spawn(
                TextBundle::from_section(
                    "Settings",
                    TextStyle {
                        font: font.clone(),
                        font_size: 60.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [
                        (MenuButtonAction::SettingsDisplay, "Display"),
                        (MenuButtonAction::SettingsAudio, "Audio"),
                        (MenuButtonAction::SettingsMisc, "Misc"),
                        (MenuButtonAction::BackToMainMenu, "Back"),
                    ] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/menu/settingsmiscscreen.rs => sdbclient/src/menu/settingsmiscscreen.rs +79 -0
@@ 0,0 1,79 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings misc screen
#[derive(Component)]
pub struct OnSettingsMiscScreen;

pub fn settings_misc_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),
        margin: UiRect::all(Val::Px(20.0)),
        justify_content: JustifyContent::Center,
        align_items: AlignItems::Center,
        ..default()
    };

    let button_text_style = TextStyle {
        font: asset_server.load("fonts/FiraMono-Regular.ttf"),
        font_size: 40.0,
        color: TEXT_COLOR,
    };

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    ..default()
                },
                ..default()
            },
            OnSettingsMiscScreen,
        ))
        .with_children(|parent| {
            parent
                .spawn(NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    background_color: Color::GRAY.into(),
                    ..default()
                })
                .with_children(|parent| {
                    for (action, text) in [(MenuButtonAction::BackToSettings, "Back")] {
                        parent
                            .spawn((
                                ButtonBundle {
                                    style: button_style.clone(),
                                    background_color: NORMAL_BUTTON.into(),
                                    ..default()
                                },
                                action,
                            ))
                            .with_children(|parent| {
                                parent.spawn(TextBundle::from_section(
                                    text,
                                    button_text_style.clone(),
                                ));
                            });
                    }
                });
        });
}

A sdbclient/src/splash/mod.rs => sdbclient/src/splash/mod.rs +80 -0
@@ 0,0 1,80 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    ui::{JustifyContent, Size, Style, Val},
};

use super::{despawn_screen, GameState};

/// This plugin will display a splsh logo on startup
pub struct SplashPlugin;

impl Plugin for SplashPlugin {
    fn build(&self, app: &mut App) {
        app
            // Load the splash when we enter the Splash state
            .add_system_set(SystemSet::on_enter(GameState::Splash).with_system(splash_setup))
            // Run a timer for a few seconds
            .add_system_set(SystemSet::on_update(GameState::Splash).with_system(splash_timer))
            // Despawn when we exit the Splash state
            .add_system_set(
                SystemSet::on_exit(GameState::Splash).with_system(despawn_screen::<OnSplashScreen>),
            );
    }
}

/// Tag component for tagging entities on the splash screen
#[derive(Component)]
struct OnSplashScreen;

/// Timer resource
#[derive(Resource, Deref, DerefMut)]
struct SplashTimer(Timer);

fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let logo = asset_server.load("branding/logo.png");

    commands
        .spawn((
            NodeBundle {
                style: Style {
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    size: Size::new(Val::Percent(100.), Val::Percent(100.)),
                    ..Default::default()
                },
                ..Default::default()
            },
            OnSplashScreen,
        ))
        .with_children(|parent| {
            parent.spawn(ImageBundle {
                style: Style {
                    size: Size::new(Val::Px(1000.), Val::Auto),
                    ..Default::default()
                },
                image: UiImage(logo),
                ..Default::default()
            });
        });

    // Insert the timer resource
    commands.insert_resource(SplashTimer(Timer::from_seconds(2., TimerMode::Once)));
}

fn splash_timer(
    mut game_state: ResMut<State<GameState>>,
    time: Res<Time>,
    mut timer: ResMut<SplashTimer>,
) {
    if timer.tick(time.delta()).finished() {
        game_state.set(GameState::MainMenu).unwrap()
    }
}

A sdbclient/src/util/eguipwd.rs => sdbclient/src/util/eguipwd.rs +38 -0
@@ 0,0 1,38 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy_egui::egui;

fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
    let state_id = ui.id().with("show_plaintext");

    let mut show_plaintext = ui.data().get_temp::<bool>(state_id).unwrap_or(false);

    let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
        let response = ui
            .add(egui::SelectableLabel::new(show_plaintext, "👁"))
            .on_hover_text("Show/hide password");

        if response.clicked() {
            show_plaintext = !show_plaintext;
        }

        ui.add_sized(
            ui.available_size(),
            egui::TextEdit::singleline(password).password(!show_plaintext),
        );
    });

    ui.data().insert_temp(state_id, show_plaintext);

    result.response
}

pub fn password(password: &mut String) -> impl egui::Widget + '_ {
    move |ui: &mut egui::Ui| password_ui(ui, password)
}

A sdbclient/src/util/mod.rs => sdbclient/src/util/mod.rs +9 -0
@@ 0,0 1,9 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

pub mod eguipwd;