Initial work on menus

This commit is contained in:
2026-09-10 23:26:17 +02:00
parent 71820a25dd
commit 3dbda96d31
22 changed files with 1093 additions and 44 deletions
+81
View File
@@ -0,0 +1,81 @@
use bevy::{input_focus::InputFocus, prelude::*, text::FontSourceTemplate, ui::auto_directional_navigation::AutoDirectionalNavigation};
use crate::{game_settings::CommonAssets, menus::style::{FGUiButtonType, FGUiStyle}};
#[derive(Component, Default, Clone)]
#[require(Button)]
pub struct FGUiButton {
pub button_type: FGUiButtonType
}
pub fn menu_button(label: &str) -> impl Scene {
bsn! {
FGUiButton
Node {
width: px(150),
height: px(65),
border: px(5),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
margin: UiRect { left: px(0), right: px(0), top: px(20), bottom: px(20) }
}
BoxShadow::new(Color::WHITE, px(0.0), px(0.0), px(10.0), px(10.0))
BorderColor::from(Color::BLACK)
BackgroundColor(Color::srgb(0.15, 0.15, 0.15))
AutoDirectionalNavigation::default()
Children [(
Text(label)
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(super::style::H3_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
TextShadow
),
]
}
}
pub fn button_setup(query: Query<(Entity, &FGUiButton, &mut Node), Added<FGUiButton>>, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>, mut commands: Commands) {
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
for (entity, button, mut node) in query {
styles.button_styles[&button.button_type].normal.apply(&mut commands, entity, &mut node);
}
}
pub fn button_style_system(query: Query<(Entity, &FGUiButton, &Interaction, &mut Node), Changed<Interaction>>, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>, mut commands: Commands) {
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
for (entity, button, interaction, mut node) in query {
let button_style_to_use = match interaction {
Interaction::Pressed => &styles.button_styles[&button.button_type].press,
Interaction::Hovered => &styles.button_styles[&button.button_type].hover,
Interaction::None => &styles.button_styles[&button.button_type].normal,
};
button_style_to_use.apply(&mut commands, entity, &mut node);
}
}
pub fn button_focus_style_system(query: Query<(Entity, &FGUiButton)>, focus: Res<InputFocus>, mut commands: Commands, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>) {
if let Some(focused_entity) = focus.get() {
if !query.contains(focused_entity) {
return;
}
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
for (entity, button) in query {
if focused_entity == entity {
let style = &styles.button_styles[&button.button_type];
commands.entity(focused_entity).insert(Outline {
width: px(style.focus_outline_width_px),
offset: px(style.focus_outline_offset_px),
color: style.focus_outline_color
});
} else {
commands.entity(entity).remove::<Outline>();
}
}
}
}
@@ -0,0 +1,41 @@
use bevy::{prelude::*, text::FontSourceTemplate};
use crate::{fighter::manifest::FighterManifest, menus::{button::FGUiButton, style::FGUiButtonType}};
#[derive(Component, Clone, Default, FromTemplate)]
pub struct CharacterBox {
fighter_manifest: Handle<FighterManifest>,
}
impl CharacterBox {
pub fn scene(manifest_handle: Handle<FighterManifest>, manifests: &Res<Assets<FighterManifest>>) -> impl Scene {
let manifest = manifests.get(&manifest_handle).expect("Fighter manifest should be valid");
bsn! {
Node {
width: px(128),
height: px(128),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::FlexEnd
}
FGUiButton {
button_type: FGUiButtonType::CharacterSelectCharacter
}
CharacterBox {
fighter_manifest: manifest_handle
}
BackgroundColor(Color::srgb(1.0, 1.0, 1.0))
Children [
Node {
width: percent(100)
}
Text("El Grande Padre")
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(crate::menus::style::TEXT_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
BackgroundColor(Color::srgb(0.0, 0.0, 1.0))
]
}
}
}
@@ -0,0 +1,18 @@
use bevy::prelude::*;
pub struct FighterPortrait {
pub player_slot: usize
}
impl FighterPortrait {
pub fn create(player_slot: usize) -> impl Scene {
bsn![
Node {
max_width: px(300),
height: px(400),
flex_grow: 1.0
}
BackgroundColor(bevy::color::palettes::css::ORANGE)
]
}
}
+151
View File
@@ -0,0 +1,151 @@
use bevy::prelude::*;
use crate::{
fighter::manifest::{FighterManifest, FighterManifestRegistry},
input::{LocalInputAssignments, LocalInputSource},
menus::{MenuMarker, build_menu_with_props, cursor::FGMenuCursor, spawn_menu, wrap_menu},
};
pub mod character_box;
pub mod fighter_portrait;
#[derive(Component, FromTemplate)]
pub struct CharacterSelectScreen {
pub fighter_portait_container: Entity,
}
pub fn setup_character_select(
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
commands: Commands,
manifest_registry: Res<FighterManifestRegistry>,
assignments: Res<LocalInputAssignments>,
manifests: Res<Assets<FighterManifest>>,
) {
let character_boxes = manifest_registry
.iter()
.map(|(_, v)| character_box::CharacterBox::scene(v.clone(), &manifests))
.collect::<Vec<_>>();
// Create existing portraits
let fighter_portraits = assignments
.iter()
.map(|(slot, _)| fighter_portrait::FighterPortrait::create(*slot))
.collect::<Vec<_>>();
let menu = bsn! {
// Characters
CharacterSelectScreen {
fighter_portait_container: #PortraitContainer
}
Node {
flex_direction: FlexDirection::Column
}
Children [
(Node {
flex_direction: FlexDirection::Column,
width: percent(100),
height: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center
}
Children [
{character_boxes}
]
),
(
#PortraitContainer
Node {
flex_direction: FlexDirection::Row,
width: percent(100),
justify_content: JustifyContent::Center
}
Children [
{fighter_portraits}
]
)
]
};
spawn_menu(existing_menu, commands, menu);
}
pub fn create_portrait(
player_slot: usize,
container: Entity,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mapping: Res<LocalInputAssignments>,
) {
let entity = commands
.spawn_scene(fighter_portrait::FighterPortrait::create(player_slot))
.id();
commands.entity(container).add_child(entity);
commands.spawn(
FGMenuCursor::create_cursor(player_slot, meshes, materials, mapping)
);
}
#[derive(Event)]
pub struct FighterSlotAssigned {
pub slot: usize,
}
pub fn handle_fighter_slot_addition(
addition: On<FighterSlotAssigned>,
query: Single<&CharacterSelectScreen>,
commands: Commands,
meshes: ResMut<Assets<Mesh>>,
materials: ResMut<Assets<ColorMaterial>>,
mapping: Res<LocalInputAssignments>,
) {
let slot = addition.slot;
create_portrait(
slot,
query.fighter_portait_container,
commands,
meshes,
materials,
mapping,
);
}
pub fn css_input(
key: Res<ButtonInput<KeyCode>>,
pads: Query<(Entity, &Gamepad)>,
mut assignments: ResMut<LocalInputAssignments>,
mut commands: Commands,
) {
if key.get_pressed().len() != 0 {
if let None = assignments
.iter()
.find(|(_, source)| matches!(source, LocalInputSource::Keyboard))
{
if let Some(free_idx) = assignments.get_free_player_index() {
assignments.push((free_idx, LocalInputSource::Keyboard));
info!("New keyboard assignment");
commands.trigger(FighterSlotAssigned { slot: free_idx });
}
}
}
for (entity, pad) in pads {
if pad.any_pressed(GamepadButton::all()) {
let existed = assignments
.iter()
.find(|(_, assignment)| {
if let LocalInputSource::Gamepad(assignment_entity) = assignment {
return entity == *assignment_entity;
}
false
})
.is_some();
if !existed && let Some(free_idx) = assignments.get_free_player_index() {
assignments.push((free_idx, LocalInputSource::Gamepad(entity)));
commands.trigger(FighterSlotAssigned { slot: free_idx });
}
}
}
}
+152
View File
@@ -0,0 +1,152 @@
use bevy::{
asset::uuid::Uuid,
math::VectorSpace,
picking::pointer::{Location, PointerId, PointerInput, PointerLocation},
prelude::*,
window::{NormalizedWindowRef, WindowRef},
};
use crate::{game_settings::GameSettings, input::LocalInputAssignments, menus::input::MenuInput};
#[derive(Default)]
pub enum MenuCursorInputSource {
#[default]
Any,
Player(usize),
}
#[derive(Component, Default)]
pub struct FGMenuCursor {
pub input_source: MenuCursorInputSource,
pub velocity: Vec2,
}
impl FGMenuCursor {
pub fn create_cursor(
player_slot: usize,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mapping: Res<LocalInputAssignments>,
) -> impl Bundle {
let (_, mapping) = mapping
.iter()
.find(|(slot, _)| *slot == player_slot)
.unwrap();
let material = materials.add(ColorMaterial::from_color(
bevy::color::palettes::tailwind::ROSE_600,
));
let circle_mesh = meshes.add(Circle::new(25.0));
let pointer_id = match mapping {
crate::input::LocalInputSource::Keyboard => PointerId::Mouse,
crate::input::LocalInputSource::Gamepad(_) => PointerId::Custom(Uuid::new_v4()),
};
(
FGMenuCursor {
input_source: super::cursor::MenuCursorInputSource::Player(player_slot),
..default()
},
pointer_id,
MeshMaterial2d(material),
Mesh2d(circle_mesh),
Transform::IDENTITY,
GlobalZIndex(1000),
)
}
}
pub fn cursor_input(
window: Single<Entity, With<bevy::window::PrimaryWindow>>,
query: Query<(
&mut FGMenuCursor,
&PointerLocation,
&PointerId,
)>,
key: Res<ButtonInput<KeyCode>>,
assignments: Res<LocalInputAssignments>,
game_settings: Res<GameSettings>,
time: Res<Time>,
pads: Query<(Entity, &Gamepad)>,
mut pointer_writer: MessageWriter<PointerInput>,
) {
for (mut cursor, location, pointer_id) in query {
let input = match cursor.input_source {
MenuCursorInputSource::Any => {
let mut input = MenuInput::default();
for (_, source) in assignments.iter() {
input.accumulate(&MenuInput::from_source(
source,
&key,
&pads,
&assignments,
&game_settings,
));
}
input
}
MenuCursorInputSource::Player(slot) => {
if let Some((_, source)) = assignments.iter().find(|(idx, _)| *idx == slot) {
MenuInput::from_source(source, &key, &pads, &assignments, &game_settings)
} else {
MenuInput::default()
}
}
};
let movement_screen_space = input.movement * Vec2::new(1.0, -1.0);
cursor.velocity = movement_screen_space * 500.0;
if cursor.velocity == Vec2::ZERO {
continue;
}
let old_cursor_pos = location
.location
.clone()
.map(|loc| loc.position)
.unwrap_or(Vec2::ZERO);
let cursor_pos = old_cursor_pos + cursor.velocity * time.delta_secs();
dbg!(cursor_pos);
pointer_writer.write(PointerInput {
pointer_id: *pointer_id,
location: Location {
target: bevy::camera::NormalizedRenderTarget::Window(
WindowRef::Primary
.normalize(Some(window.entity()))
.expect("Primary window should be valid"),
),
position: cursor_pos,
},
action: bevy::picking::pointer::PointerAction::Move {
delta: cursor_pos - old_cursor_pos,
},
});
//cursor.position
}
}
pub fn update_cursor_transform(
camera: Single<(&Camera, &GlobalTransform), With<IsDefaultUiCamera>>,
query: Query<(&mut Transform, &PointerLocation), With<FGMenuCursor>>,
) {
let (camera, camera_transform) = camera.into_inner();
for (mut trf, location) in query {
let cursor_pos_viewport = camera
.viewport_to_world_2d(
camera_transform,
location
.clone()
.location
.map(|d| d.position)
.unwrap_or_default(),
)
.ok()
.unwrap_or_default();
trf.translation = Vec3::new(cursor_pos_viewport.x, cursor_pos_viewport.y, 0.0);
}
}
+65
View File
@@ -0,0 +1,65 @@
use bevy::prelude::*;
use crate::{game_settings::GameSettings, input::{LocalInputAssignments, LocalInputSource}};
#[derive(Default)]
pub struct MenuInput {
pub movement: Vec2,
pub accept: bool
}
impl MenuInput {
pub fn accumulate(&mut self, other: &Self) {
self.movement += other.movement;
self.movement = self.movement.normalize();
self.accept = self.accept || other.accept;
}
pub fn from_gamepad(gamepad: &Gamepad, game_settings: &GameSettings) -> Self {
let mut x = gamepad.left_stick().x;
let mut y = gamepad.left_stick().y;
let deadzone = game_settings.input_common.stick_deadzone.to_num::<f32>();
if x.abs() < deadzone {
x = 0.0;
}
if y.abs() < deadzone {
y = 0.0;
}
MenuInput {
movement: Vec2::new(x, y),
accept: gamepad.pressed(GamepadButton::South)
}
}
pub fn from_keyboard(keeb: &ButtonInput<KeyCode>) -> Self {
let x = if keeb.pressed(KeyCode::KeyA) { -1.0 } else { 0.0 };
let x = x + if keeb.pressed(KeyCode::KeyD) { 1.0 } else { 0.0 };
let y = if keeb.pressed(KeyCode::KeyS) { -1.0 } else { 0.0 };
let y = y + if keeb.pressed(KeyCode::KeyW) { 1.0 } else { 0.0 };
let movement = Vec2::new(x, y);
MenuInput {
movement: movement,
accept: keeb.pressed(KeyCode::Enter)
}
}
pub fn from_source(source: &LocalInputSource, key: &ButtonInput<KeyCode>, gamepads: &Query<(Entity, &Gamepad)>, assignments: &LocalInputAssignments, game_settings: &GameSettings) -> Self {
match source {
LocalInputSource::Keyboard => Self::from_keyboard(key),
LocalInputSource::Gamepad(entity) => {
let gamepad = gamepads.get(*entity);
if let Ok((_, gamepad)) = gamepad {
Self::from_gamepad(gamepad, game_settings)
} else {
Self::default()
}
},
}
}
}
+55
View File
@@ -0,0 +1,55 @@
use bevy::{
prelude::*, text::FontSourceTemplate,
ui::auto_directional_navigation::AutoDirectionalNavigation,
};
use crate::{AppState, menus::{MenuMarker, button::menu_button, navigation::NavigationDefaultFocus}};
#[derive(SceneComponent, Default, Clone)]
pub struct MainMenu;
impl MainMenu {
fn scene() -> impl Scene {
bsn! {
BackgroundColor(Color::srgb(0.15, 0.0, 0.0))
Children [
(
Text("Project Shinespark")
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(super::style::H1_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
),
(
Text("This is a beta, kuro owes me 5€")
TextFont {
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
font_size: px(super::style::H2_SIZE),
}
TextColor(Color::srgb(0.9, 0.9, 0.9))
),
(
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
column_gap: px(5),
flex_direction: FlexDirection::Column,
}
Children [
(
menu_button("Versus")
on(|_event: On<Pointer<Press>>, mut next_state: ResMut<NextState<AppState>>,| next_state.set(AppState::CharacterSelect))
NavigationDefaultFocus
),
]
),
]
}
}
}
pub fn setup_main_menu(mut commands: Commands) {
}
+168
View File
@@ -0,0 +1,168 @@
use bevy::{
input_focus::{
InputFocus, InputFocusVisible,
directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},
},
prelude::*,
};
use bevy::prelude::*;
use crate::{
AppState,
game_settings::CommonAssets,
menus::{character_select::CharacterSelectScreen, main_menu::MainMenu, style::FGUiStyle},
};
pub mod button;
pub mod character_select;
pub mod cursor;
pub mod input;
pub mod main_menu;
pub mod navigation;
pub mod scaling;
pub mod style;
#[derive(Component, Clone, Default)]
pub struct MenuMarker {}
pub struct MenuPlugin;
pub fn build_menu_scene<S>(existing_menu: Option<Single<Entity, With<MenuMarker>>>) -> impl Scene
where
S: SceneComponent + Default + Clone + Send + Sync,
{
}
pub fn wrap_menu(menu: impl Scene) -> impl Scene {
bsn! {
#MenuRoot
MenuMarker
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
}
Children [
(
#MenuContainer
Node {
width: px(1920),
height: px(1080),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
column_gap: vmin(5.0),
flex_direction: FlexDirection::Column,
padding: UiRect { left: vmin(5.0), right: vmin(5.0), top: vmin(5.0), bottom: vmin(5.0) }
}
menu
)
]
}
}
pub fn spawn_menu(
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
mut commands: Commands,
menu: impl Scene,
) {
if let Some(entity) = existing_menu {
commands
.entity(entity.into_inner())
.remove::<Camera2d>()
.remove::<Camera>()
.despawn();
}
commands.queue_spawn_scene(wrap_menu(menu));
}
pub fn build_menu_with_props<S>(props: S::Props) -> impl Scene
where
S: SceneComponent + Default + Clone + Send + Sync,
{
S::scene(props)
}
pub fn build_menu<S>(
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
mut commands: Commands,
) where
S: SceneComponent + Default + Clone + Send + Sync,
{
spawn_menu(
existing_menu,
commands,
build_menu_with_props::<S>(S::Props::default()),
);
}
impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
OnEnter(AppState::MainMenu),
(build_menu::<MainMenu>, |mut commands: Commands| {
commands.spawn((Camera2d, IsDefaultUiCamera));
commands.spawn((
Camera2d,
Camera {
order: 1,
..default()
},
));
}),
)
.add_systems(
OnEnter(AppState::CharacterSelect),
character_select::setup_character_select,
)
.init_resource::<navigation::UINavigationActionState>()
.insert_resource(InputFocusVisible(true))
.insert_resource(GlobalUiDebugOptions {
enabled: true,
..default()
})
// Configure auto-navigation behavior
.insert_resource(AutoNavigationConfig {
// Require at least 10% overlap in perpendicular axis for cardinal directions
min_alignment_factor: 0.1,
// Don't connect nodes more than 500 pixels apart between their closest edges
max_search_distance: Some(500.0),
// Prefer nodes that are well-aligned
prefer_aligned: true,
})
.add_plugins(DirectionalNavigationPlugin)
.add_systems(
PreUpdate,
(
navigation::give_default_focus,
scaling::ui_scaling_system,
button::button_setup,
button::button_style_system,
button::button_focus_style_system,
)
.run_if(resource_exists::<CommonAssets>),
)
.add_systems(Startup, scaling::ui_update_scale_on_startup)
.add_systems(
PostUpdate,
(character_select::css_input, (cursor::cursor_input, cursor::update_cursor_transform).chain())
.run_if(in_state(AppState::CharacterSelect))
.run_if(any_with_component::<CharacterSelectScreen>),
)
.add_observer(character_select::handle_fighter_slot_addition)
.add_plugins(bevy_common_assets::ron::RonAssetPlugin::<FGUiStyle>::new(
&["stylesheet.ron"],
))
.add_systems(
Update,
(
navigation::process_inputs,
navigation::navigate,
navigation::interact_with_focused_button,
)
.chain(),
)
.init_resource::<InputFocus>();
}
}
+166
View File
@@ -0,0 +1,166 @@
use std::{collections::HashSet, time::Duration};
use bevy::{camera::NormalizedRenderTarget, input_focus::{InputFocus, InputFocusVisible}, math::CompassOctant, picking::{backend::HitData, pointer::{Location, PointerId}}, prelude::*, ui::auto_directional_navigation::AutoDirectionalNavigator};
// Action state and input handling
#[derive(Debug, PartialEq, Eq, Hash)]
enum DirectionalNavigationAction {
Up,
Down,
Left,
Right,
Select,
}
impl DirectionalNavigationAction {
fn variants() -> Vec<Self> {
vec![
DirectionalNavigationAction::Up,
DirectionalNavigationAction::Down,
DirectionalNavigationAction::Left,
DirectionalNavigationAction::Right,
DirectionalNavigationAction::Select,
]
}
fn keycode(&self) -> KeyCode {
match self {
DirectionalNavigationAction::Up => KeyCode::ArrowUp,
DirectionalNavigationAction::Down => KeyCode::ArrowDown,
DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
DirectionalNavigationAction::Right => KeyCode::ArrowRight,
DirectionalNavigationAction::Select => KeyCode::Enter,
}
}
fn gamepad_button(&self) -> GamepadButton {
match self {
DirectionalNavigationAction::Up => GamepadButton::DPadUp,
DirectionalNavigationAction::Down => GamepadButton::DPadDown,
DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
DirectionalNavigationAction::Right => GamepadButton::DPadRight,
DirectionalNavigationAction::Select => GamepadButton::South,
}
}
}
#[derive(Default, Resource)]
pub struct UINavigationActionState {
pressed_actions: HashSet<DirectionalNavigationAction>,
}
#[derive(Default, Component, Clone)]
pub struct NavigationDefaultFocus;
pub fn process_inputs(
mut action_state: ResMut<UINavigationActionState>,
keyboard_input: Res<ButtonInput<KeyCode>>,
gamepad_input: Query<&Gamepad>,
) {
action_state.pressed_actions.clear();
for action in DirectionalNavigationAction::variants() {
if keyboard_input.just_pressed(action.keycode()) {
action_state.pressed_actions.insert(action);
}
}
for gamepad in gamepad_input.iter() {
for action in DirectionalNavigationAction::variants() {
if gamepad.just_pressed(action.gamepad_button()) {
action_state.pressed_actions.insert(action);
}
}
}
}
const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
pub fn highlight_focused_element(
input_focus: Res<InputFocus>,
input_focus_visible: Res<InputFocusVisible>,
mut query: Query<(Entity, &mut BorderColor)>,
) {
for (entity, mut border_color) in query.iter_mut() {
if input_focus.get() == Some(entity) && input_focus_visible.0 {
*border_color = BorderColor::all(FOCUSED_BORDER);
} else {
*border_color = BorderColor::DEFAULT;
}
}
}
pub fn navigate(
action_state: Res<UINavigationActionState>,
mut auto_directional_navigator: AutoDirectionalNavigator,
) {
let net_east_west = action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Right) as i8
- action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Left) as i8;
let net_north_south = action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Up) as i8
- action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Down) as i8;
// Use Dir2::from_xy to convert input to direction, then convert to CompassOctant
let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
.ok()
.map(CompassOctant::from);
if let Some(direction) = maybe_direction {
match auto_directional_navigator.navigate(direction) {
Ok(_entity) => {
// Successfully navigated
}
Err(e) => {
error!("Navigation error: {}", e)
}
}
}
}
pub fn give_default_focus(query: Query<Entity, Added<NavigationDefaultFocus>>, mut input_focus: ResMut<InputFocus>) {
for entity in query {
input_focus.set(entity, bevy::input_focus::FocusCause::Navigated);
}
}
pub fn interact_with_focused_button(
action_state: Res<UINavigationActionState>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
if action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Select)
&& let Some(focused_entity) = input_focus.get()
{
commands.trigger(Pointer::new(
PointerId::Mouse,
Location {
target: NormalizedRenderTarget::None {
width: 0,
height: 0,
},
position: Vec2::ZERO,
},
Press {
button: PointerButton::Primary,
hit: HitData {
camera: Entity::PLACEHOLDER,
depth: 0.0,
position: None,
normal: None,
extra: None,
},
count: 1,
},
focused_entity,
));
}
}
+29
View File
@@ -0,0 +1,29 @@
use bevy::camera::CameraProjection;
use bevy::camera::Projection::Orthographic;
use bevy::prelude::*;
use bevy::window::WindowResized;
fn compute_ui_scale(window_size: Vec2) -> f32 {
const BASE_SIZE: Vec2 = Vec2::new(1920.0, 1080.0);
const BASE_ASPECT_RATIO: f32 = BASE_SIZE.x / BASE_SIZE.y;
let new_aspect_ratio = window_size.x / window_size.y;
if new_aspect_ratio > BASE_ASPECT_RATIO {
window_size.y / BASE_SIZE.y
} else {
window_size.x / BASE_SIZE.x
}
}
pub fn ui_update_scale_on_startup(window: Single<&Window>, mut ui_scale: ResMut<UiScale>) {
ui_scale.0 = compute_ui_scale(window.size());
}
pub fn ui_scaling_system(mut resize_reader: MessageReader<WindowResized>, mut ui_scale: ResMut<UiScale>) {
for event in resize_reader.read() {
let dimensions = Vec2::new(event.width, event.height);
*ui_scale = UiScale(compute_ui_scale(dimensions));
break
}
}
+62
View File
@@ -0,0 +1,62 @@
use std::collections::HashMap;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
pub const H1_SIZE: i32 = 72;
pub const H2_SIZE: i32 = 54;
pub const H3_SIZE: i32 = 40;
pub const TEXT_SIZE: i32 = 24;
#[derive(Serialize, Deserialize, Reflect, Default)]
#[serde(default)]
pub struct BoxShadowStyle {
color: Color,
offset_px_x: i32,
offset_px_y: i32,
spread_radius: f32,
blur_radius: f32
}
#[derive(Serialize, Deserialize, Reflect, Default)]
#[serde(default)]
pub struct ButtonStyle {
background_color: Color,
border_color: Color,
text_color: Color,
box_shadow: BoxShadowStyle,
border_size: i32
}
#[derive(Serialize, Deserialize, Reflect)]
pub struct ButtonStyles {
pub normal: ButtonStyle,
pub hover: ButtonStyle,
pub press: ButtonStyle,
pub focus_outline_width_px: i32,
pub focus_outline_offset_px: i32,
pub focus_outline_color: Color,
}
impl ButtonStyle {
pub fn apply(&self, commands: &mut Commands, entity: Entity, node: &mut Node) {
node.border = UiRect::all(px(self.border_size));
commands.entity(entity).insert((
BackgroundColor(self.background_color),
BorderColor::all(self.background_color),
BoxShadow::new(self.box_shadow.color, px(self.box_shadow.offset_px_x), px(self.box_shadow.offset_px_y), px(self.box_shadow.spread_radius), px(self.box_shadow.blur_radius))
));
}
}
#[derive(Reflect, Eq, PartialEq, Hash, Deserialize, Serialize, Default, Clone)]
pub enum FGUiButtonType {
#[default]
MainMenu,
CharacterSelectCharacter
}
#[derive(Asset, Resource, Reflect, Deserialize, Serialize)]
pub struct FGUiStyle {
pub button_styles: HashMap<FGUiButtonType, ButtonStyles>
}