Switch from bevy_ui to bevy_egui

Generic widgets done with inspiration from:
https://github.com/bevyengine/bevy/discussions/5522
This commit is contained in:
Tobias Berger 2022-10-11 15:19:36 +02:00
parent 2a1a8855d8
commit 91018d1d73
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
12 changed files with 508 additions and 584 deletions

75
Cargo.lock generated
View file

@ -29,6 +29,18 @@ dependencies = [
"version_check", "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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.19" version = "0.7.19"
@ -126,6 +138,12 @@ version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
[[package]]
name = "atomic_refcell"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -277,6 +295,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "bevy_encase_derive" name = "bevy_encase_derive"
version = "0.8.1" version = "0.8.1"
@ -678,7 +706,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e9aa1866c1cf7ee000f281ce9e90d02d701f5c7380a107252017e58e2f5246" checksum = "f6e9aa1866c1cf7ee000f281ce9e90d02d701f5c7380a107252017e58e2f5246"
dependencies = [ dependencies = [
"ahash", "ahash 0.7.6",
"getrandom", "getrandom",
"hashbrown", "hashbrown",
"instant", "instant",
@ -1072,6 +1100,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 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]] [[package]]
name = "encase" name = "encase"
version = "0.3.0" version = "0.3.0"
@ -1114,6 +1162,21 @@ dependencies = [
"regex", "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]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.3.23" version = "0.3.23"
@ -1366,7 +1429,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [ dependencies = [
"ahash", "ahash 0.7.6",
"serde", "serde",
] ]
@ -1702,6 +1765,12 @@ dependencies = [
"memoffset", "memoffset",
] ]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"
@ -2768,6 +2837,8 @@ name = "worlds-history-sim-rs"
version = "0.1.1" version = "0.1.1"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_egui",
"fxhash",
"planet", "planet",
] ]

View file

@ -10,13 +10,23 @@ release = { strip = "symbols", lto = "thin", opt-level = "z" }
[features] [features]
logging = ["planet/logging"] logging = ["planet/logging"]
globe_view = ["planet/globe_view", "render"] globe_view = ["planet/globe_view", "render"]
render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/x11", "bevy/wayland", "bevy/render", "planet/render"] render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/x11", "bevy/wayland", "bevy/render", "planet/render", "dep:fxhash", "dep:bevy_egui"]
default = ["render", "logging"] default = ["render", "logging", "globe_view"]
[dependencies.planet] [dependencies.planet]
path = "planet" path = "planet"
default-features = false default-features = false
[dependencies.bevy] [dependencies.bevy]
version = "0.8" version = "0.8.1"
default-features = false 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.

View file

@ -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<ToolbarButton> 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<ToolbarButton> 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;

View file

@ -1,3 +1,2 @@
pub(crate) mod markers;
#[cfg(feature = "render")] #[cfg(feature = "render")]
pub(crate) mod panning; pub(crate) mod panning;

3
src/gui/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub(crate) mod widget;
pub(crate) mod widgets;
pub(crate) use widget::*;

73
src/gui/widget.rs Normal file
View file

@ -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<Self>, ui: &mut Ui, id: WidgetId);
}
pub(crate) fn widget<S: 'static + WidgetSystem>(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::<StateInstances<S>>() {
// 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::<S>());
world.insert_resource(StateInstances::<S> {
instances: HashMap::new(),
});
}
world.resource_scope(|world, mut states: Mut<'_, StateInstances<S>>| {
if !states.instances.contains_key(&id) {
debug!(
"Registering system state for widget {id:?} of type {}",
std::any::type_name::<S>()
);
_ = 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<T: WidgetSystem> {
instances: HashMap<WidgetId, SystemState<T>>,
}
#[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)
}
}

View file

@ -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<Self>, 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::<Diagnostics>();
_ = 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::<CursorMapPosition>().to_string());
ui.end_row()
});
}
}

4
src/gui/widgets/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub(crate) mod info_panel;
pub(crate) use info_panel::InfoPanel;
pub(crate) mod toolbar;
pub(crate) use toolbar::ToolbarWidget;

181
src/gui/widgets/toolbar.rs Normal file
View file

@ -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<WorldManager>| {
let mut images = world.resource_mut::<Assets<Image>>();
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::<WorldManager>().save_world("planet.ron") {
eprintln!("Failed to save planet.ron: {}", err);
}
},
ToolbarButton::LoadWorld => {
world.resource_scope(|world, mut images: Mut<'_, Assets<Image>>| {
if let Err(err) = world
.resource_mut::<WorldManager>()
.load_world("planet.ron", &mut images)
{
eprintln!("Failed to save planet.ron: {}", err);
} else {
update_textures(world);
}
});
},
ToolbarButton::Rainfall => {
world.resource_mut::<WorldManager>().toggle_rainfall();
update_textures(world);
},
ToolbarButton::Temperature => {
world.resource_mut::<WorldManager>().toggle_temperature();
update_textures(world);
},
ToolbarButton::PlanetView => {
world.resource_mut::<WorldManager>().cycle_view();
update_textures(world);
},
ToolbarButton::Contours => {
world.resource_mut::<WorldManager>().toggle_contours();
update_textures(world);
},
#[cfg(feature = "globe_view")]
ToolbarButton::GlobeView => {
let mut camera_3d = world
.query_filtered::<&mut Camera, (With<Camera3d>, Without<Camera2d>)>()
.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<Camera2d>, Without<Camera3d>)>()
.single_mut(world);
camera_2d.is_active = !camera_2d.is_active;
pancam.enabled = camera_2d.is_active;
},
};
}
}
impl From<ToolbarButton> 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<ToolbarButton> 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<Self>, 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);
}
}
},
);
}
}

View file

@ -1,64 +1,36 @@
#![warn(absolute_paths_not_starting_with_crate)] use gui::widgets::{InfoPanel, ToolbarWidget};
// #![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)]
pub(crate) mod components; pub(crate) mod components;
#[cfg(feature = "render")]
pub(crate) mod gui;
pub(crate) mod macros; pub(crate) mod macros;
pub(crate) mod plugins; pub(crate) mod plugins;
pub(crate) mod resources; pub(crate) mod resources;
pub(crate) mod ui_helpers;
#[cfg(all(feature = "logging", feature = "render"))] use {
use bevy::{ bevy::{
diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin}, app::App,
log::debug, log::LogSettings,
prelude::{IntoExclusiveSystem, World},
utils::{default, tracing::Level},
},
bevy_egui::egui::{FontData, FontDefinitions, FontFamily},
planet::WorldManager,
plugins::WorldPlugins,
}; };
#[cfg(feature = "render")] #[cfg(feature = "render")]
use { use {
bevy::text::Font,
bevy::{ bevy::{
asset::Assets, asset::Assets,
core_pipeline::core_2d::{Camera2d, Camera2dBundle}, core_pipeline::core_2d::{Camera2d, Camera2dBundle},
ecs::{ ecs::{
change_detection::ResMut, change_detection::{Mut, ResMut},
query::{Changed, With}, query::With,
system::{Commands, Query, Res}, system::{Commands, Query, Res},
}, },
hierarchy::BuildChildren,
prelude::Vec2, prelude::Vec2,
render::{ render::{
camera::{Camera, RenderTarget}, camera::{Camera, RenderTarget},
color::Color,
render_resource::{ render_resource::{
Extent3d, Extent3d,
TextureDescriptor, TextureDescriptor,
@ -69,47 +41,20 @@ use {
texture::{Image, ImageSettings}, texture::{Image, ImageSettings},
}, },
sprite::{Sprite, SpriteBundle}, sprite::{Sprite, SpriteBundle},
text::Text,
transform::components::GlobalTransform, transform::components::GlobalTransform,
ui::{ window::{WindowDescriptor, Windows},
entity::{NodeBundle, TextBundle},
AlignSelf,
FocusPolicy,
Interaction,
JustifyContent,
PositionType,
Size,
Style,
UiColor,
UiRect,
Val,
},
window::{CursorIcon, WindowDescriptor, Windows},
winit::WinitSettings, winit::WinitSettings,
}, },
components::{ bevy_egui::EguiContext,
markers::{InfoPanel, ToolbarButton}, components::panning::Pan2d,
panning::Pan2d, gui::widget,
},
planet::BiomeStats,
resources::CursorMapPosition, 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"))] #[cfg(all(feature = "render", feature = "globe_view"))]
use { use {
bevy::{ bevy::{
asset::Handle, asset::Handle,
core_pipeline::core_3d::{Camera3d, Camera3dBundle}, core_pipeline::core_3d::Camera3dBundle,
ecs::query::Without,
pbr::{PbrBundle, PointLight, PointLightBundle, StandardMaterial}, pbr::{PbrBundle, PointLight, PointLightBundle, StandardMaterial},
prelude::{Quat, Vec3}, prelude::{Quat, Vec3},
render::camera::OrthographicProjection, render::camera::OrthographicProjection,
@ -119,198 +64,6 @@ use {
std::f32::consts::FRAC_PI_2, std::f32::consts::FRAC_PI_2,
}; };
#[cfg(feature = "render")]
fn refresh_map_texture(
images: &mut Assets<Image>,
#[cfg(feature = "globe_view")] materials: &mut Assets<StandardMaterial>,
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<Interaction>,
>,
mut windows: ResMut<'_, Windows>,
mut images: ResMut<'_, Assets<Image>>,
mut world_manager: ResMut<'_, WorldManager>,
#[cfg(feature = "globe_view")] mut camera_3d_query: Query<
'_,
'_,
&mut Camera,
(With<Camera3d>, Without<Camera2d>),
>,
#[cfg(feature = "globe_view")] mut camera_2d_query: Query<
'_,
'_,
(&mut Camera, &mut Pan2d),
(With<Camera2d>, Without<Camera3d>),
>,
#[cfg(feature = "globe_view")] mut materials: ResMut<'_, Assets<StandardMaterial>>,
) {
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")] #[cfg(feature = "render")]
fn update_cursor_map_position( fn update_cursor_map_position(
mut cursor_map_position: ResMut<'_, CursorMapPosition>, mut cursor_map_position: ResMut<'_, CursorMapPosition>,
@ -350,107 +103,44 @@ fn rotate_globe(mut globe_transform: Query<'_, '_, &mut Transform, With<Handle<M
globe_transform.single_mut().rotate_y(ROTATION_SPEED); globe_transform.single_mut().rotate_y(ROTATION_SPEED);
} }
#[cfg(feature = "render")]
fn update_info_panel(
#[cfg(feature = "logging")] diagnostics: Res<'_, Diagnostics>,
cursor_position: Res<'_, CursorMapPosition>,
world_manager: Res<'_, WorldManager>,
mut text: Query<'_, '_, &mut Text, With<InfoPanel>>,
) {
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}%)",
(<BiomeStats>::from(biome_type).name),
presence * 100.0
)
})
.collect::<Vec<String>>()
.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}%)",
(<BiomeStats>::from(biome_type).name),
presence * 100.0
)
})
.collect::<Vec<String>>()
.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")] #[cfg(feature = "render")]
fn generate_graphics( fn generate_graphics(
mut commands: Commands<'_, '_>, mut commands: Commands<'_, '_>,
mut world_manager: ResMut<'_, WorldManager>, mut world_manager: ResMut<'_, WorldManager>,
mut images: ResMut<'_, Assets<Image>>, mut images: ResMut<'_, Assets<Image>>,
mut fonts: ResMut<'_, Assets<Font>>, mut egui_context: ResMut<'_, EguiContext>,
#[cfg(feature = "globe_view")] mut materials: ResMut<'_, Assets<StandardMaterial>>, #[cfg(feature = "globe_view")] mut materials: ResMut<'_, Assets<StandardMaterial>>,
#[cfg(feature = "globe_view")] mut meshes: ResMut<'_, Assets<Mesh>>, #[cfg(feature = "globe_view")] mut meshes: ResMut<'_, Assets<Mesh>>,
) { ) {
let julia_mono_handle = fonts.add( // Add Julia-Mono font to egui
Font::try_from_bytes(include_bytes!("../assets/JuliaMono.ttf").to_vec()) {
.expect("Failed to create JuliaMono font!"), 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 world = world_manager.world();
let custom_sprite_size = Vec2 { let custom_sprite_size = Vec2 {
x: (WORLD_SCALE * world.width as i32) as f32, x: (WORLD_SCALE * world.width as i32) as f32,
y: (WORLD_SCALE * world.height 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 { let map_image_handle = images.add(Image {
data: world_manager.map_color_bytes(), data: world_manager.map_color_bytes(),
texture_descriptor: TextureDescriptor { texture_descriptor: TextureDescriptor {
@ -469,6 +159,20 @@ fn generate_graphics(
..default() ..default()
}); });
world_manager.map_image_handle_id = Some(map_image_handle.id); world_manager.map_image_handle_id = Some(map_image_handle.id);
_ = commands
.spawn_bundle(Camera2dBundle::default())
.insert(Pan2d::new());
// 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()
},
..default()
});
}
#[cfg(feature = "globe_view")] #[cfg(feature = "globe_view")]
{ {
@ -533,80 +237,22 @@ fn generate_graphics(
..default() ..default()
}); });
} }
}
_ = commands fn update_gui(world: &mut World) {
.spawn_bundle(Camera2dBundle::default()) world.resource_scope(|world, mut ctx: Mut<'_, EguiContext>| {
.insert(Pan2d::new()); let ctx = ctx.ctx_mut();
_ = commands.spawn_bundle(SpriteBundle { _ = bevy_egui::egui::Window::new("Info panel")
texture: images.get_handle(world_manager.map_image_handle_id.unwrap()), .resizable(false)
sprite: Sprite { .show(ctx, |ui| {
custom_size: Some(custom_sprite_size), widget::<InfoPanel<'_, '_>>(world, ui, "Map Info Panel".into());
..default()
},
..default()
}); });
_ = commands _ = bevy_egui::egui::TopBottomPanel::bottom("Toolbar")
.spawn_bundle(NodeBundle { .resizable(false)
style: Style { .default_height(30.0)
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), .show(ctx, |ui| {
..default() widget::<ToolbarWidget<'_, '_>>(world, ui, "Toolbar".into());
},
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)
});
}); });
}); });
} }
@ -632,9 +278,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}) })
.insert_resource(CursorMapPosition::default()) .insert_resource(CursorMapPosition::default())
.add_startup_system(generate_graphics) .add_startup_system(generate_graphics)
.add_system(handle_toolbar_button) .add_system(update_gui.exclusive_system())
.add_system(update_cursor_map_position) .add_system(update_cursor_map_position);
.add_system(update_info_panel);
#[cfg(all(feature = "render", feature = "globe_view"))] #[cfg(all(feature = "render", feature = "globe_view"))]
{ {
_ = app.add_system(rotate_globe); _ = app.add_system(rotate_globe);

View file

@ -10,10 +10,7 @@ use bevy::{
impl PluginGroup for WorldPlugins { impl PluginGroup for WorldPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) { fn build(&mut self, group: &mut PluginGroupBuilder) {
_ = group _ = group.add(LogPlugin).add(CorePlugin).add(TimePlugin);
.add(LogPlugin::default())
.add(CorePlugin::default())
.add(TimePlugin::default());
#[cfg(feature = "render")] #[cfg(feature = "render")]
{ {
@ -32,39 +29,41 @@ impl PluginGroup for WorldPlugins {
window::WindowPlugin, window::WindowPlugin,
winit::WinitPlugin, winit::WinitPlugin,
}, },
bevy_egui::EguiPlugin,
}; };
_ = group _ = group
.add(TransformPlugin::default()) .add(TransformPlugin)
// hierarchy // hierarchy
.add(InputPlugin::default()) .add(InputPlugin)
.add(WindowPlugin::default()) .add(WindowPlugin)
.add(AssetPlugin::default()) .add(AssetPlugin)
.add(HierarchyPlugin::default()) .add(HierarchyPlugin)
.add(WinitPlugin::default()) .add(WinitPlugin)
.add(RenderPlugin::default()) .add(RenderPlugin)
.add(CorePipelinePlugin::default()) .add(CorePipelinePlugin)
.add(SpritePlugin::default()) .add(SpritePlugin)
.add(TextPlugin::default()) .add(TextPlugin)
.add(UiPlugin::default()) .add(UiPlugin)
.add(PanningPlugin::default()); .add(PanningPlugin)
.add(EguiPlugin);
#[cfg(feature = "globe_view")] #[cfg(feature = "globe_view")]
{ {
use bevy::pbr::PbrPlugin; use bevy::pbr::PbrPlugin;
_ = group.add(PbrPlugin::default()) _ = group.add(PbrPlugin)
} }
} }
#[cfg(not(feature = "render"))] #[cfg(not(feature = "render"))]
{ {
use bevy::app::ScheduleRunnerPlugin; use bevy::app::ScheduleRunnerPlugin;
_ = group.add(ScheduleRunnerPlugin::default()); _ = group.add(ScheduleRunnerPlugin);
} }
_ = group.add(DiagnosticsPlugin::default()); _ = group.add(DiagnosticsPlugin);
#[cfg(all(feature = "logging"))] #[cfg(all(feature = "logging"))]
{ {
use bevy::diagnostic::FrameTimeDiagnosticsPlugin; use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
_ = group.add(FrameTimeDiagnosticsPlugin::default()); _ = group.add(FrameTimeDiagnosticsPlugin);
} }
_ = group.add(LogDiagnosticsPlugin::default()); _ = group.add(LogDiagnosticsPlugin::default());
} }

View file

@ -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<Font>, which: ToolbarButton) -> TextBundle {
TextBundle {
text: Text::from_section(
which,
TextStyle {
font,
font_size: 20.0,
color: Color::WHITE,
},
),
..default()
}
}