diff --git a/Cargo.lock b/Cargo.lock index 1d8406d..4de28e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e6e951cfbb2db8de1828d49073a113a29fd7117b1596caa781a258c7e38d72" +dependencies = [ + "cfg-if 1.0.0", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.19" @@ -126,6 +138,12 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + [[package]] name = "autocfg" version = "1.1.0" @@ -277,6 +295,16 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy_egui" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d365761fd6a5c227b1f88f38b560287334accb69cfe938443e27615464edc897" +dependencies = [ + "bevy", + "egui", +] + [[package]] name = "bevy_encase_derive" version = "0.8.1" @@ -678,7 +706,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e9aa1866c1cf7ee000f281ce9e90d02d701f5c7380a107252017e58e2f5246" dependencies = [ - "ahash", + "ahash 0.7.6", "getrandom", "hashbrown", "instant", @@ -1072,6 +1100,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "egui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9fcd393c3daaaf5909008a1d948319d538b79c51871e4df0993260260a94e4" +dependencies = [ + "ahash 0.8.0", + "epaint", + "nohash-hasher", +] + +[[package]] +name = "emath" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9542a40106fdba943a055f418d1746a050e1a903a049b030c2b097d4686a33cf" +dependencies = [ + "bytemuck", +] + [[package]] name = "encase" version = "0.3.0" @@ -1114,6 +1162,21 @@ dependencies = [ "regex", ] +[[package]] +name = "epaint" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba04741be7f6602b1a1b28f1082cce45948a7032961c52814f8946b28493300" +dependencies = [ + "ab_glyph", + "ahash 0.8.0", + "atomic_refcell", + "bytemuck", + "emath", + "nohash-hasher", + "parking_lot 0.12.1", +] + [[package]] name = "erased-serde" version = "0.3.23" @@ -1366,7 +1429,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", "serde", ] @@ -1702,6 +1765,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.1" @@ -2768,6 +2837,8 @@ name = "worlds-history-sim-rs" version = "0.1.1" dependencies = [ "bevy", + "bevy_egui", + "fxhash", "planet", ] diff --git a/Cargo.toml b/Cargo.toml index e6e050b..d4a72e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,23 @@ release = { strip = "symbols", lto = "thin", opt-level = "z" } [features] logging = ["planet/logging"] globe_view = ["planet/globe_view", "render"] -render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/x11", "bevy/wayland", "bevy/render", "planet/render"] -default = ["render", "logging"] +render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/x11", "bevy/wayland", "bevy/render", "planet/render", "dep:fxhash", "dep:bevy_egui"] +default = ["render", "logging", "globe_view"] [dependencies.planet] path = "planet" default-features = false [dependencies.bevy] -version = "0.8" +version = "0.8.1" default-features = false + +[dependencies.fxhash] +version = "0.2.1" +optional = true + +[dependencies.bevy_egui] +version = "0.16.1" +optional = true +default-features = false +# features = ["manage_clipboard"] # In the future, when I add text input. \ No newline at end of file diff --git a/src/components/markers.rs b/src/components/markers.rs deleted file mode 100644 index eb2195c..0000000 --- a/src/components/markers.rs +++ /dev/null @@ -1,65 +0,0 @@ -#[cfg(feature = "render")] -use {crate::macros::iterable_enum, bevy::ecs::component::Component}; - -#[cfg(all(feature = "render", not(feature = "globe_view")))] -iterable_enum!(ToolbarButton { - GenerateWorld, - SaveWorld, - LoadWorld, - Rainfall, - Temperature, - PlanetView, - Contours, -}); -#[cfg(all(feature = "render", feature = "globe_view"))] -iterable_enum!(ToolbarButton { - GenerateWorld, - SaveWorld, - LoadWorld, - Rainfall, - Temperature, - PlanetView, - Contours, - GlobeView, -}); - -#[cfg(feature = "render")] -impl From for &'static str { - fn from(button: ToolbarButton) -> Self { - match button { - ToolbarButton::Rainfall => "Toggle rainfall", - ToolbarButton::Temperature => "Toggle temperature", - ToolbarButton::Contours => "Toggle contours", - ToolbarButton::PlanetView => "Cycle view", - ToolbarButton::GenerateWorld => "Generate new world", - ToolbarButton::SaveWorld => "Save", - ToolbarButton::LoadWorld => "Load", - #[cfg(feature = "globe_view")] - ToolbarButton::GlobeView => "Toggle globe", - } - } -} -#[cfg(feature = "render")] -impl From<&ToolbarButton> for &'static str { - fn from(button: &ToolbarButton) -> Self { - (*button).into() - } -} - -#[cfg(feature = "render")] -impl From for String { - fn from(button: ToolbarButton) -> Self { - <&'static str>::from(button).into() - } -} - -#[cfg(feature = "render")] -impl From<&ToolbarButton> for String { - fn from(button: &ToolbarButton) -> Self { - <&'static str>::from(button).into() - } -} - -#[cfg(feature = "render")] -#[derive(Component)] -pub(crate) struct InfoPanel; diff --git a/src/components/mod.rs b/src/components/mod.rs index e44bb60..80818b0 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod markers; #[cfg(feature = "render")] pub(crate) mod panning; diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..aae4e4c --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod widget; +pub(crate) mod widgets; +pub(crate) use widget::*; diff --git a/src/gui/widget.rs b/src/gui/widget.rs new file mode 100644 index 0000000..a2d197e --- /dev/null +++ b/src/gui/widget.rs @@ -0,0 +1,73 @@ +use { + bevy::{ + ecs::{ + change_detection::Mut, + system::{SystemParam, SystemState}, + world::World, + }, + log::debug, + utils::HashMap, + }, + bevy_egui::egui::Ui, + fxhash::FxHasher32, + std::hash::Hasher, +}; + +pub(crate) trait WidgetSystem: SystemParam { + fn system(world: &mut World, state: &mut SystemState, ui: &mut Ui, id: WidgetId); +} + +pub(crate) fn widget(world: &mut World, ui: &mut Ui, id: WidgetId) { + // We need to cache `SystemState` to allow for a system's locally tracked state + if !world.contains_resource::>() { + // Note, this message should only appear once! If you see it twice in the logs, + // the function may have been called recursively, and will panic. + debug!("Init system state {}", std::any::type_name::()); + world.insert_resource(StateInstances:: { + instances: HashMap::new(), + }); + } + world.resource_scope(|world, mut states: Mut<'_, StateInstances>| { + if !states.instances.contains_key(&id) { + debug!( + "Registering system state for widget {id:?} of type {}", + std::any::type_name::() + ); + _ = states.instances.insert(id, SystemState::new(world)); + } + let cached_state = states.instances.get_mut(&id).unwrap(); + S::system(world, cached_state, ui, id); + cached_state.apply(world); + }); +} + +/// A UI widget may have multiple instances. We need to ensure the local state +/// of these instances is not shared. This hashmap allows us to dynamically +/// store instance states. +#[derive(Default)] +struct StateInstances { + instances: HashMap>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct WidgetId(pub(crate) u64); +impl WidgetId { + #[must_use] + pub(crate) fn new(name: &str) -> Self { + let bytes = name.as_bytes(); + let mut hasher = FxHasher32::default(); + hasher.write(bytes); + WidgetId(hasher.finish()) + } + + // #[must_use] + // pub(crate) fn with(&self, name: &str) -> Self { + // Self::new(&format!("{}{name}", self.0)) + // } +} +impl From<&str> for WidgetId { + #[must_use] + fn from(str: &str) -> Self { + Self::new(str) + } +} diff --git a/src/gui/widgets/info_panel.rs b/src/gui/widgets/info_panel.rs new file mode 100644 index 0000000..9ebf7ef --- /dev/null +++ b/src/gui/widgets/info_panel.rs @@ -0,0 +1,50 @@ +#[cfg(feature = "logging")] +use bevy::diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin}; +use { + crate::{ + gui::{WidgetId, WidgetSystem}, + resources::CursorMapPosition, + }, + bevy::ecs::{ + system::{SystemParam, SystemState}, + world::World, + }, + bevy_egui::egui::{Grid, Ui}, + std::marker::PhantomData, +}; + +#[derive(SystemParam)] +pub(crate) struct InfoPanel<'w, 's> { + #[system_param(ignore)] + _phantom: PhantomData<(&'w (), &'s ())>, +} +impl WidgetSystem for InfoPanel<'_, '_> { + fn system(world: &mut World, _state: &mut SystemState, ui: &mut Ui, _id: WidgetId) { + // This will get everything our system/widget requested + // let mut params = state.get_mut(world); + + _ = Grid::new("info_panel") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + #[cfg(feature = "logging")] + { + let diagnostics = world.resource::(); + + _ = ui.label("Framerate"); + _ = ui.label( + match diagnostics.get_measurement(FrameTimeDiagnosticsPlugin::FPS) { + None => f64::NAN, + Some(fps) => fps.value.round(), + } + .to_string(), + ); + ui.end_row(); + } + + _ = ui.label("Cursor position"); + _ = ui.label(world.resource::().to_string()); + ui.end_row() + }); + } +} diff --git a/src/gui/widgets/mod.rs b/src/gui/widgets/mod.rs new file mode 100644 index 0000000..7e24c3c --- /dev/null +++ b/src/gui/widgets/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod info_panel; +pub(crate) use info_panel::InfoPanel; +pub(crate) mod toolbar; +pub(crate) use toolbar::ToolbarWidget; diff --git a/src/gui/widgets/toolbar.rs b/src/gui/widgets/toolbar.rs new file mode 100644 index 0000000..93eefc4 --- /dev/null +++ b/src/gui/widgets/toolbar.rs @@ -0,0 +1,181 @@ +use { + crate::{ + components::panning::Pan2d, + gui::{WidgetId, WidgetSystem}, + macros::iterable_enum, + }, + bevy::{ + ecs::{ + component::Component, + system::{SystemParam, SystemState}, + world::World, + }, + log::debug, + prelude::{Assets, Camera, Camera2d, Camera3d, Image, Mut, With, Without}, + render::render_resource::Extent3d, + }, + bevy_egui::egui::{Layout, Ui}, + planet::WorldManager, + std::marker::PhantomData, +}; + +#[cfg(not(feature = "globe_view"))] +iterable_enum!(ToolbarButton { + GenerateWorld, + SaveWorld, + LoadWorld, + Rainfall, + Temperature, + PlanetView, + Contours, +}); +#[cfg(feature = "globe_view")] +iterable_enum!(ToolbarButton { + GenerateWorld, + SaveWorld, + LoadWorld, + Rainfall, + Temperature, + PlanetView, + Contours, + GlobeView, +}); +fn update_textures(world: &mut World) { + debug!("refreshing world texture"); + world.resource_scope(|world, world_manager: Mut| { + let mut images = world.resource_mut::>(); + + let map_image_handle = images.get_handle( + world_manager + .map_image_handle_id + .expect("No map image handle"), + ); + let map_image = images + .get_mut(&map_image_handle) + .expect("Map image handle pointing to non-existing image"); + map_image.resize(Extent3d { + width: world_manager.world().width, + height: world_manager.world().height, + depth_or_array_layers: 1, + }); + map_image.data = world_manager.map_color_bytes(); + }); +} +impl ToolbarButton { + fn clicked(self, world: &mut World) { + match self { + ToolbarButton::GenerateWorld => { + world.resource_scope(|world, mut world_manager: Mut<'_, WorldManager>| { + match world_manager.new_world() { + Err(err) => { + eprintln!("Failed to generate world: {}", err); + }, + Ok(_) => { + update_textures(world); + }, + } + }) + }, + ToolbarButton::SaveWorld => { + if let Err(err) = world.resource::().save_world("planet.ron") { + eprintln!("Failed to save planet.ron: {}", err); + } + }, + ToolbarButton::LoadWorld => { + world.resource_scope(|world, mut images: Mut<'_, Assets>| { + if let Err(err) = world + .resource_mut::() + .load_world("planet.ron", &mut images) + { + eprintln!("Failed to save planet.ron: {}", err); + } else { + update_textures(world); + } + }); + }, + ToolbarButton::Rainfall => { + world.resource_mut::().toggle_rainfall(); + update_textures(world); + }, + ToolbarButton::Temperature => { + world.resource_mut::().toggle_temperature(); + update_textures(world); + }, + ToolbarButton::PlanetView => { + world.resource_mut::().cycle_view(); + update_textures(world); + }, + ToolbarButton::Contours => { + world.resource_mut::().toggle_contours(); + update_textures(world); + }, + #[cfg(feature = "globe_view")] + ToolbarButton::GlobeView => { + let mut camera_3d = world + .query_filtered::<&mut Camera, (With, Without)>() + .single_mut(world); + camera_3d.is_active = !camera_3d.is_active; + let (mut camera_2d, mut pancam) = world + .query_filtered::<(&mut Camera, &mut Pan2d), (With, Without)>() + .single_mut(world); + camera_2d.is_active = !camera_2d.is_active; + pancam.enabled = camera_2d.is_active; + }, + }; + } +} + +impl From for &'static str { + fn from(button: ToolbarButton) -> Self { + match button { + ToolbarButton::Rainfall => "Toggle rainfall", + ToolbarButton::Temperature => "Toggle temperature", + ToolbarButton::Contours => "Toggle contours", + ToolbarButton::PlanetView => "Cycle view", + ToolbarButton::GenerateWorld => "Generate new world", + ToolbarButton::SaveWorld => "Save", + ToolbarButton::LoadWorld => "Load", + #[cfg(feature = "globe_view")] + ToolbarButton::GlobeView => "Toggle globe", + } + } +} + +impl From<&ToolbarButton> for &'static str { + fn from(button: &ToolbarButton) -> Self { + (*button).into() + } +} + +impl From for String { + fn from(button: ToolbarButton) -> Self { + <&'static str>::from(button).into() + } +} + +impl From<&ToolbarButton> for String { + fn from(button: &ToolbarButton) -> Self { + <&'static str>::from(button).into() + } +} + +#[derive(SystemParam)] +pub(crate) struct ToolbarWidget<'w, 's> { + #[system_param(ignore)] + _phantom: PhantomData<(&'w (), &'s ())>, +} +impl WidgetSystem for ToolbarWidget<'_, '_> { + fn system(world: &mut World, _state: &mut SystemState, ui: &mut Ui, _id: WidgetId) { + ui.with_layout( + Layout::left_to_right(bevy_egui::egui::Align::Center), + |ui| { + for button in ToolbarButton::ITEMS { + if ui.button(<&'static str>::from(button)).clicked() { + debug!("Pressed button: {:#?}", button); + button.clicked(world); + } + } + }, + ); + } +} diff --git a/src/main.rs b/src/main.rs index 3a13980..25b8aaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,36 @@ -#![warn(absolute_paths_not_starting_with_crate)] -// #![warn(box_pointers)] -#![warn(elided_lifetimes_in_paths)] -#![warn(explicit_outlives_requirements)] -#![warn(keyword_idents)] -#![warn(macro_use_extern_crate)] -#![warn(meta_variable_misuse)] -#![warn(missing_abi)] -// #![warn(missing_copy_implementations)] -#![warn(missing_debug_implementations)] -// #![warn(missing_docs)] -#![warn(non_ascii_idents)] -#![warn(noop_method_call)] -#![warn(pointer_structural_match)] -#![warn(rust_2021_incompatible_closure_captures)] -#![warn(rust_2021_incompatible_or_patterns)] -#![warn(rust_2021_prefixes_incompatible_syntax)] -#![warn(rust_2021_prelude_collisions)] -#![warn(single_use_lifetimes)] -#![warn(trivial_casts)] -#![warn(trivial_numeric_casts)] -#![warn(unreachable_pub)] -#![warn(unsafe_code)] -#![warn(unsafe_op_in_unsafe_fn)] -#![warn(unstable_features)] -#![warn(unused_crate_dependencies)] -#![warn(unused_extern_crates)] -#![warn(unused_import_braces)] -#![warn(unused_lifetimes)] -#![warn(unused_macro_rules)] -#![warn(unused_qualifications)] -#![warn(unused_results)] -#![warn(variant_size_differences)] +use gui::widgets::{InfoPanel, ToolbarWidget}; pub(crate) mod components; +#[cfg(feature = "render")] +pub(crate) mod gui; pub(crate) mod macros; pub(crate) mod plugins; pub(crate) mod resources; -pub(crate) mod ui_helpers; -#[cfg(all(feature = "logging", feature = "render"))] -use bevy::{ - diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin}, - log::debug, +use { + bevy::{ + app::App, + log::LogSettings, + prelude::{IntoExclusiveSystem, World}, + utils::{default, tracing::Level}, + }, + bevy_egui::egui::{FontData, FontDefinitions, FontFamily}, + planet::WorldManager, + plugins::WorldPlugins, }; #[cfg(feature = "render")] use { - bevy::text::Font, bevy::{ asset::Assets, core_pipeline::core_2d::{Camera2d, Camera2dBundle}, ecs::{ - change_detection::ResMut, - query::{Changed, With}, + change_detection::{Mut, ResMut}, + query::With, system::{Commands, Query, Res}, }, - hierarchy::BuildChildren, prelude::Vec2, render::{ camera::{Camera, RenderTarget}, - color::Color, render_resource::{ Extent3d, TextureDescriptor, @@ -69,47 +41,20 @@ use { texture::{Image, ImageSettings}, }, sprite::{Sprite, SpriteBundle}, - text::Text, transform::components::GlobalTransform, - ui::{ - entity::{NodeBundle, TextBundle}, - AlignSelf, - FocusPolicy, - Interaction, - JustifyContent, - PositionType, - Size, - Style, - UiColor, - UiRect, - Val, - }, - window::{CursorIcon, WindowDescriptor, Windows}, + window::{WindowDescriptor, Windows}, winit::WinitSettings, }, - components::{ - markers::{InfoPanel, ToolbarButton}, - panning::Pan2d, - }, - planet::BiomeStats, + bevy_egui::EguiContext, + components::panning::Pan2d, + gui::widget, resources::CursorMapPosition, - ui_helpers::{toolbar_button, toolbar_button_text}, -}; -use { - bevy::{ - app::App, - log::LogSettings, - utils::{default, tracing::Level}, - }, - planet::WorldManager, - plugins::WorldPlugins, }; #[cfg(all(feature = "render", feature = "globe_view"))] use { bevy::{ asset::Handle, - core_pipeline::core_3d::{Camera3d, Camera3dBundle}, - ecs::query::Without, + core_pipeline::core_3d::Camera3dBundle, pbr::{PbrBundle, PointLight, PointLightBundle, StandardMaterial}, prelude::{Quat, Vec3}, render::camera::OrthographicProjection, @@ -119,198 +64,6 @@ use { std::f32::consts::FRAC_PI_2, }; -#[cfg(feature = "render")] -fn refresh_map_texture( - images: &mut Assets, - #[cfg(feature = "globe_view")] materials: &mut Assets, - world_manager: &WorldManager, -) { - let world = world_manager.world(); - #[cfg(feature = "logging")] - debug!("refreshing world texture"); - let map_image_handle = images.get_handle( - world_manager - .map_image_handle_id - .expect("No map image handle"), - ); - let map_image = images - .get_mut(&map_image_handle) - .expect("Map image handle pointing to non-existing image"); - map_image.resize(Extent3d { - width: world.width, - height: world.height, - depth_or_array_layers: 1, - }); - map_image.data = world_manager.map_color_bytes(); - - #[cfg(feature = "globe_view")] - { - let planet_image_handle = images.get_handle( - world_manager - .globe_image_handle_id - .expect("No planet image handle"), - ); - let planet_image = images - .get_mut(&planet_image_handle) - .expect("Planet image handle pointing to non-existing image"); - planet_image.resize(Extent3d { - width: world.width, - height: world.height, - depth_or_array_layers: 1, - }); - planet_image.data = world_manager.globe_color_bytes(); - - let planet_material_handle = materials.get_handle( - world_manager - .globe_material_handle_id - .expect("No planet material handle"), - ); - let planet_material = materials - .get_mut(&planet_material_handle) - .expect("Planet material handle pointing to non-existing material"); - planet_material.base_color_texture = Some(planet_image_handle); - } -} - -#[cfg(feature = "render")] -const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15); -#[cfg(feature = "render")] -const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25); -#[cfg(feature = "render")] -const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.60, 0.35); -#[cfg(feature = "render")] -fn handle_toolbar_button( - mut interaction_query: Query< - '_, - '_, - (&Interaction, &mut UiColor, &ToolbarButton), - Changed, - >, - mut windows: ResMut<'_, Windows>, - mut images: ResMut<'_, Assets>, - mut world_manager: ResMut<'_, WorldManager>, - #[cfg(feature = "globe_view")] mut camera_3d_query: Query< - '_, - '_, - &mut Camera, - (With, Without), - >, - #[cfg(feature = "globe_view")] mut camera_2d_query: Query< - '_, - '_, - (&mut Camera, &mut Pan2d), - (With, Without), - >, - #[cfg(feature = "globe_view")] mut materials: ResMut<'_, Assets>, -) { - for (interaction, mut color, toolbar_button) in &mut interaction_query { - match *interaction { - Interaction::Clicked => { - windows.primary_mut().set_cursor_icon(CursorIcon::Default); - *color = PRESSED_BUTTON.into(); - match toolbar_button { - ToolbarButton::Rainfall => { - #[cfg(feature = "logging")] - debug!("Toggling rainfall"); - world_manager.toggle_rainfall(); - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - }, - ToolbarButton::Temperature => { - #[cfg(feature = "logging")] - debug!("Toggling temperature"); - world_manager.toggle_temperature(); - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - }, - ToolbarButton::PlanetView => { - #[cfg(feature = "logging")] - debug!("Cycling planet view"); - world_manager.cycle_view(); - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - }, - ToolbarButton::Contours => { - #[cfg(feature = "logging")] - debug!("Toggling contours"); - world_manager.toggle_contours(); - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - }, - ToolbarButton::GenerateWorld => { - #[cfg(feature = "logging")] - debug!("Generating new world"); - _ = world_manager - .new_world() - .expect("Failed to generate new world"); - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - }, - ToolbarButton::SaveWorld => { - #[cfg(feature = "logging")] - debug!("Saving world"); - if let Err(err) = world_manager.save_world("planet.ron") { - eprintln!("Failed to save planet.ron: {}", err); - } - }, - ToolbarButton::LoadWorld => { - #[cfg(feature = "logging")] - debug!("Loading world"); - if let Err(err) = world_manager.load_world("planet.ron", &mut images) { - eprintln!("Failed to load planet.ron: {}", err); - } else { - refresh_map_texture( - &mut images, - #[cfg(feature = "globe_view")] - &mut materials, - &world_manager, - ); - } - }, - #[cfg(feature = "globe_view")] - ToolbarButton::GlobeView => { - #[cfg(feature = "logging")] - debug!("Toggling globe view"); - let mut camera_3d = camera_3d_query.single_mut(); - camera_3d.is_active = !camera_3d.is_active; - let (mut camera_2d, mut pancam) = camera_2d_query.single_mut(); - camera_2d.is_active = !camera_2d.is_active; - pancam.enabled = camera_2d.is_active; - }, - } - }, - Interaction::Hovered => { - windows.primary_mut().set_cursor_icon(CursorIcon::Hand); - *color = HOVERED_BUTTON.into(); - }, - Interaction::None => { - windows.primary_mut().set_cursor_icon(CursorIcon::Default); - *color = NORMAL_BUTTON.into(); - }, - } - } -} - #[cfg(feature = "render")] fn update_cursor_map_position( mut cursor_map_position: ResMut<'_, CursorMapPosition>, @@ -350,125 +103,76 @@ fn rotate_globe(mut globe_transform: Query<'_, '_, &mut Transform, With, - cursor_position: Res<'_, CursorMapPosition>, - world_manager: Res<'_, WorldManager>, - mut text: Query<'_, '_, &mut Text, With>, -) { - let world = world_manager.world(); - text.single_mut().sections[0].value = if cursor_position.x >= 0 - && cursor_position.x < world.width as i32 - && cursor_position.y >= 0 - && cursor_position.y < world.height as i32 - { - let cell = &world.terrain[cursor_position.y as usize][cursor_position.x as usize]; - - #[cfg(feature = "logging")] - { - format!( - "FPS: ~{}\nMouse position: {}\nAltitude: {}\nRainfall: {}\nTemperature: {}\n\n{}", - match diagnostics.get_measurement(FrameTimeDiagnosticsPlugin::FPS) { - None => f64::NAN, - Some(fps) => fps.value.round(), - }, - *cursor_position, - cell.altitude, - cell.rainfall, - cell.temperature, - cell.biome_presences - .iter() - .map(|(biome_type, presence)| { - format!( - "Biome: {} ({:.2}%)", - (::from(biome_type).name), - presence * 100.0 - ) - }) - .collect::>() - .join("\n") - ) - } - - #[cfg(not(feature = "logging"))] - { - format!( - "Mouse position: {}\nAltitude: {}\nRainfall: {}\nTemperature: {}\n{}", - *cursor_position, - cell.altitude, - cell.rainfall, - cell.temperature, - cell.biome_presences - .iter() - .map(|(biome_type, presence)| { - format!( - "Biome: {} ({:.2}%)", - (::from(biome_type).name), - presence * 100.0 - ) - }) - .collect::>() - .join("\n") - ) - } - } else { - #[cfg(feature = "logging")] - { - format!( - "FPS: ~{}\nMouse position: {}\nOut of bounds", - match diagnostics.get_measurement(FrameTimeDiagnosticsPlugin::FPS) { - None => f64::NAN, - Some(fps) => fps.value.round(), - }, - *cursor_position - ) - } - - #[cfg(not(feature = "logging"))] - { - format!("Mouse position: {}\nOut of bounds", *cursor_position) - } - }; -} - #[cfg(feature = "render")] fn generate_graphics( mut commands: Commands<'_, '_>, mut world_manager: ResMut<'_, WorldManager>, mut images: ResMut<'_, Assets>, - mut fonts: ResMut<'_, Assets>, + mut egui_context: ResMut<'_, EguiContext>, #[cfg(feature = "globe_view")] mut materials: ResMut<'_, Assets>, #[cfg(feature = "globe_view")] mut meshes: ResMut<'_, Assets>, ) { - let julia_mono_handle = fonts.add( - Font::try_from_bytes(include_bytes!("../assets/JuliaMono.ttf").to_vec()) - .expect("Failed to create JuliaMono font!"), - ); + // Add Julia-Mono font to egui + { + let ctx = egui_context.ctx_mut(); + let mut fonts = FontDefinitions::default(); + const FONT_NAME: &str = "Julia-Mono"; + _ = fonts.font_data.insert( + FONT_NAME.to_owned(), + FontData::from_static(include_bytes!("../assets/JuliaMono.ttf")), + ); + fonts + .families + .get_mut(&FontFamily::Monospace) + .expect("Failed to get 'Monospace' FontFamily") + .insert(0, FONT_NAME.to_owned()); + fonts + .families + .get_mut(&FontFamily::Proportional) + .expect("Failed to get 'Proportional' FontFamily") + .push(FONT_NAME.to_owned()); + ctx.set_fonts(fonts); + } + let world = world_manager.world(); let custom_sprite_size = Vec2 { x: (WORLD_SCALE * world.width as i32) as f32, y: (WORLD_SCALE * world.height as i32) as f32, }; + // Set up 2D map mode + { + let map_image_handle = images.add(Image { + data: world_manager.map_color_bytes(), + texture_descriptor: TextureDescriptor { + label: None, + size: Extent3d { + width: world.width, + height: world.height, + ..default() + }, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, + }, + ..default() + }); + world_manager.map_image_handle_id = Some(map_image_handle.id); + _ = commands + .spawn_bundle(Camera2dBundle::default()) + .insert(Pan2d::new()); - let map_image_handle = images.add(Image { - data: world_manager.map_color_bytes(), - texture_descriptor: TextureDescriptor { - label: None, - size: Extent3d { - width: world.width, - height: world.height, + // TODO: Switch to egui + _ = commands.spawn_bundle(SpriteBundle { + texture: images.get_handle(world_manager.map_image_handle_id.unwrap()), + sprite: Sprite { + custom_size: Some(custom_sprite_size), ..default() }, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba32Float, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - }, - ..default() - }); - world_manager.map_image_handle_id = Some(map_image_handle.id); + ..default() + }); + } #[cfg(feature = "globe_view")] { @@ -533,82 +237,24 @@ fn generate_graphics( ..default() }); } +} - _ = commands - .spawn_bundle(Camera2dBundle::default()) - .insert(Pan2d::new()); - _ = commands.spawn_bundle(SpriteBundle { - texture: images.get_handle(world_manager.map_image_handle_id.unwrap()), - sprite: Sprite { - custom_size: Some(custom_sprite_size), - ..default() - }, - ..default() +fn update_gui(world: &mut World) { + world.resource_scope(|world, mut ctx: Mut<'_, EguiContext>| { + let ctx = ctx.ctx_mut(); + _ = bevy_egui::egui::Window::new("Info panel") + .resizable(false) + .show(ctx, |ui| { + widget::>(world, ui, "Map Info Panel".into()); + }); + + _ = bevy_egui::egui::TopBottomPanel::bottom("Toolbar") + .resizable(false) + .default_height(30.0) + .show(ctx, |ui| { + widget::>(world, ui, "Toolbar".into()); + }); }); - - _ = commands - .spawn_bundle(NodeBundle { - style: Style { - size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), - ..default() - }, - color: Color::NONE.into(), - ..default() - }) - .with_children(|root_node| { - _ = root_node - .spawn_bundle(NodeBundle { - style: Style { - align_self: AlignSelf::FlexEnd, - padding: UiRect::all(Val::Px(2.0)), - ..default() - }, - color: Color::rgba(1.0, 1.0, 1.0, 0.05).into(), - focus_policy: FocusPolicy::Pass, - ..default() - }) - .with_children(|info_panel| { - _ = info_panel - .spawn_bundle(TextBundle { - text: Text::from_section( - "Info Panel", - bevy::text::TextStyle { - font: julia_mono_handle.clone(), - font_size: 15.0, - color: Color::WHITE, - }, - ), - ..default() - }) - .insert(InfoPanel); - }); - _ = root_node - .spawn_bundle(NodeBundle { - style: Style { - size: Size::new(Val::Percent(100.0), Val::Undefined), - padding: UiRect::all(Val::Px(3.0)), - justify_content: JustifyContent::SpaceAround, - position_type: PositionType::Absolute, - ..default() - }, - color: Color::NONE.into(), - focus_policy: FocusPolicy::Pass, - ..default() - }) - .with_children(|button_box| { - ToolbarButton::iterator().for_each(|&button_type| { - _ = button_box - .spawn_bundle(toolbar_button()) - .with_children(|button| { - _ = button.spawn_bundle(toolbar_button_text( - julia_mono_handle.clone(), - button_type, - )); - }) - .insert(button_type) - }); - }); - }); } #[cfg(feature = "render")] @@ -632,9 +278,8 @@ fn main() -> Result<(), Box> { }) .insert_resource(CursorMapPosition::default()) .add_startup_system(generate_graphics) - .add_system(handle_toolbar_button) - .add_system(update_cursor_map_position) - .add_system(update_info_panel); + .add_system(update_gui.exclusive_system()) + .add_system(update_cursor_map_position); #[cfg(all(feature = "render", feature = "globe_view"))] { _ = app.add_system(rotate_globe); diff --git a/src/plugins/world_plugins.rs b/src/plugins/world_plugins.rs index 5926f1b..9f75c7d 100644 --- a/src/plugins/world_plugins.rs +++ b/src/plugins/world_plugins.rs @@ -10,10 +10,7 @@ use bevy::{ impl PluginGroup for WorldPlugins { fn build(&mut self, group: &mut PluginGroupBuilder) { - _ = group - .add(LogPlugin::default()) - .add(CorePlugin::default()) - .add(TimePlugin::default()); + _ = group.add(LogPlugin).add(CorePlugin).add(TimePlugin); #[cfg(feature = "render")] { @@ -32,39 +29,41 @@ impl PluginGroup for WorldPlugins { window::WindowPlugin, winit::WinitPlugin, }, + bevy_egui::EguiPlugin, }; _ = group - .add(TransformPlugin::default()) + .add(TransformPlugin) // hierarchy - .add(InputPlugin::default()) - .add(WindowPlugin::default()) - .add(AssetPlugin::default()) - .add(HierarchyPlugin::default()) - .add(WinitPlugin::default()) - .add(RenderPlugin::default()) - .add(CorePipelinePlugin::default()) - .add(SpritePlugin::default()) - .add(TextPlugin::default()) - .add(UiPlugin::default()) - .add(PanningPlugin::default()); + .add(InputPlugin) + .add(WindowPlugin) + .add(AssetPlugin) + .add(HierarchyPlugin) + .add(WinitPlugin) + .add(RenderPlugin) + .add(CorePipelinePlugin) + .add(SpritePlugin) + .add(TextPlugin) + .add(UiPlugin) + .add(PanningPlugin) + .add(EguiPlugin); #[cfg(feature = "globe_view")] { use bevy::pbr::PbrPlugin; - _ = group.add(PbrPlugin::default()) + _ = group.add(PbrPlugin) } } #[cfg(not(feature = "render"))] { use bevy::app::ScheduleRunnerPlugin; - _ = group.add(ScheduleRunnerPlugin::default()); + _ = group.add(ScheduleRunnerPlugin); } - _ = group.add(DiagnosticsPlugin::default()); + _ = group.add(DiagnosticsPlugin); #[cfg(all(feature = "logging"))] { use bevy::diagnostic::FrameTimeDiagnosticsPlugin; - _ = group.add(FrameTimeDiagnosticsPlugin::default()); + _ = group.add(FrameTimeDiagnosticsPlugin); } _ = group.add(LogDiagnosticsPlugin::default()); } diff --git a/src/ui_helpers.rs b/src/ui_helpers.rs deleted file mode 100644 index 60ed4cd..0000000 --- a/src/ui_helpers.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[cfg(feature = "render")] -use { - crate::{components::markers::ToolbarButton, NORMAL_BUTTON}, - bevy::{ - asset::Handle, - render::color::Color, - text::{Font, Text, TextStyle}, - ui::{ - entity::{ButtonBundle, TextBundle}, - widget::Button, - AlignItems, - JustifyContent, - Style, - }, - utils::default, - }, -}; - -#[cfg(feature = "render")] -pub(crate) fn toolbar_button() -> ButtonBundle { - ButtonBundle { - button: Button, - style: Style { - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - color: NORMAL_BUTTON.into(), - ..default() - } -} - -#[cfg(feature = "render")] -pub(crate) fn toolbar_button_text(font: Handle, which: ToolbarButton) -> TextBundle { - TextBundle { - text: Text::from_section( - which, - TextStyle { - font, - font_size: 20.0, - color: Color::WHITE, - }, - ), - ..default() - } -}