diff --git a/Cargo.toml b/Cargo.toml index 9c378d18..9625d346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ opt-level = 3 [features] bevy_xpbd_3d = ["dep:bevy_xpbd_3d", "bevy_xpbd_3d/debug-plugin", "bevy_xpbd_3d/3d", "bevy_xpbd_3d/collider-from-mesh", "bevy_xpbd_3d/f32"] -default = ["bevy_xpbd_3d"] +default = ["bevy_xpbd_3d", "persistance_editor"] persistance_editor = [] [[example]] diff --git a/src/editor/core/hotkeys.rs b/src/editor/core/hotkeys.rs new file mode 100644 index 00000000..18816502 --- /dev/null +++ b/src/editor/core/hotkeys.rs @@ -0,0 +1,175 @@ +use bevy::prelude::*; +use bevy::reflect::GetTypeRegistration; +use bevy::utils::HashMap; + +#[cfg(feature = "persistance_editor")] +use super::AppPersistanceExt; + +//TODO: I think this must be a derive macro in future +pub trait Hotkey: + Send + + Sync + + Reflect + + FromReflect + + GetTypeRegistration + + TypePath + + PartialEq + + Eq + + Copy + + std::hash::Hash + + 'static +{ + fn name(&self) -> String; +} + +#[derive(Resource, Reflect)] +pub struct HotkeySet { + pub bindings: HashMap>, + pub name: String, +} + +impl Default for HotkeySet +where + T: Hotkey, +{ + fn default() -> Self { + Self { + bindings: HashMap::new(), + name: T::short_type_path().to_string(), + } + } +} + +#[derive(Resource, Default)] +pub struct AllHotkeys { + pub mappers: Vec< + Box< + dyn Fn(&mut World, &mut dyn FnMut(&mut World, String, &mut Vec)) + Send + Sync, + >, + >, + pub global_mapper: Vec< + Box< + dyn Fn(&mut World, &mut dyn FnMut(&mut World, &mut dyn UntypedHotkeySet)) + Send + Sync, + >, + >, +} + +impl AllHotkeys { + pub fn map( + &self, + world: &mut World, + map_fun: &mut dyn FnMut(&mut World, String, &mut Vec), + ) { + for mapper in &self.mappers { + mapper(world, map_fun); + } + } + + pub fn global_map( + &self, + world: &mut World, + map_fun: &mut dyn FnMut(&mut World, &mut dyn UntypedHotkeySet), + ) { + for mapper in &self.global_mapper { + mapper(world, map_fun); + } + } +} + +pub trait UntypedHotkeySet { + fn get_flat_bindings(&mut self) -> Vec<(String, &mut Vec)>; + fn get_name(&self) -> &str; +} + +impl UntypedHotkeySet for HotkeySet { + fn get_flat_bindings(&mut self) -> Vec<(String, &mut Vec)> { + let mut res = self + .bindings + .iter_mut() + .map(|(k, v)| (k.name(), v)) + .collect::>(); + + res.sort_by(|a, b| a.0.cmp(&b.0)); + res + } + + fn get_name(&self) -> &str { + &self.name + } +} + +pub trait HotkeyAppExt { + fn editor_hotkey(&mut self, key: T, binding: Vec) -> &mut Self; +} + +impl HotkeyAppExt for App { + fn editor_hotkey(&mut self, key: T, binding: Vec) -> &mut Self { + if !self.world.contains_resource::() { + self.insert_resource(AllHotkeys::default()); + } + + if !self.world.contains_resource::>() { + self.insert_resource(HotkeySet::::default()); + self.init_resource::>(); + #[cfg(feature = "persistance_editor")] + { + self.persistance_resource_with_fn::>(Box::new( + |dst: &mut HotkeySet, src: HotkeySet| { + dst.bindings.extend(src.bindings); + }, + )); + } + self.add_systems(PreUpdate, hotkey_mapper::); + self.register_type::>(); + self.register_type::>(); + self.register_type::>>(); + self.register_type::(); + self.world + .resource_mut::() + .mappers + .push(Box::new(|w, map_fun| { + w.resource_scope::, _>(|world, mut set| { + for (name, binding) in set.get_flat_bindings() { + map_fun(world, name, binding); + } + }); + })); + + self.world + .resource_mut::() + .global_mapper + .push(Box::new(|w, map_fun| { + w.resource_scope::, _>(|world, mut set| { + map_fun(world, set.as_mut()); + }) + })) + } + + let mut set = self.world.get_resource_mut::>().unwrap(); + set.bindings.insert(key, binding); + self + } +} + +fn hotkey_mapper( + bindings: Res>, + mut hotkeys: ResMut>, + input: Res>, +) where + T: Hotkey, +{ + hotkeys.clear(); + for (key, binding) in bindings.bindings.iter() { + let mut pressed = true; + for code in binding { + if !input.pressed(*code) { + pressed = false; + } + } + if pressed { + hotkeys.press(*key); + } else { + hotkeys.release(*key); + } + } +} diff --git a/src/editor/core/mod.rs b/src/editor/core/mod.rs index 11981e13..61a5ee6a 100644 --- a/src/editor/core/mod.rs +++ b/src/editor/core/mod.rs @@ -13,6 +13,9 @@ pub use task_storage::*; pub mod undo; pub use undo::*; +pub mod hotkeys; +pub use hotkeys::*; + #[cfg(feature = "persistance_editor")] pub mod persistance; #[cfg(feature = "persistance_editor")] diff --git a/src/editor/core/persistance.rs b/src/editor/core/persistance.rs index 2defeda6..772cdbc1 100644 --- a/src/editor/core/persistance.rs +++ b/src/editor/core/persistance.rs @@ -135,7 +135,7 @@ fn persistance_end(mut persistance: ResMut) { } PersistanceMode::Loading => { persistance.mode = PersistanceMode::None; - if persistance.load_counter == persistance.target_count { + if persistance.load_counter != persistance.target_count { error!( "Persistance loading error: {} of {} resources were loaded", persistance.load_counter, persistance.target_count @@ -215,11 +215,32 @@ impl Default for PersistanceDataSource { Self::File("editor.ron".to_string()) } } +#[derive(Resource)] +struct PersistanceLoadPipeline { + pub load_fn: Box, +} + +impl Default for PersistanceLoadPipeline { + fn default() -> Self { + Self { + load_fn: Box::new(|dst, src| { + *dst = src; + }), + } + } +} pub trait AppPersistanceExt { fn persistance_resource( &mut self, ) -> &mut Self; + + fn persistance_resource_with_fn< + T: Default + Reflect + FromReflect + Resource + GetTypeRegistration, + >( + &mut self, + load_function: Box, + ) -> &mut Self; } impl AppPersistanceExt for App { @@ -233,6 +254,33 @@ impl AppPersistanceExt for App { self.register_type::(); self.add_event::>(); + self.init_resource::>(); + + self.add_systems( + Update, + persistance_resource_system::.in_set(PersistanceSet::ResourceProcess), + ); + + self + } + + fn persistance_resource_with_fn< + T: Default + Reflect + FromReflect + Resource + GetTypeRegistration, + >( + &mut self, + load_function: Box, + ) -> &mut Self { + self.world + .resource_mut::() + .target_count += 1; + + self.register_type::(); + self.add_event::>(); + + self.insert_resource(PersistanceLoadPipeline { + load_fn: load_function, + }); + self.add_systems( Update, persistance_resource_system::.in_set(PersistanceSet::ResourceProcess), @@ -250,6 +298,7 @@ fn persistance_resource_system< mut resource: ResMut, registry: Res, mut persistance_loaded: EventWriter>, + pipeline: ResMut>, ) { for event in events.read() { match event { @@ -283,8 +332,14 @@ fn persistance_resource_system< .deserialize(&mut ron::Deserializer::from_str(data).unwrap()) .unwrap(); - let converted = ::from_reflect(&*reflected_value).unwrap(); - *resource = converted; + let Some(converted) = ::from_reflect(&*reflected_value) else { + warn!( + "Persistance resource {} could not be converted", + T::get_type_registration().type_info().type_path() + ); + continue; + }; + (pipeline.load_fn)(resource.as_mut(), converted); resource.set_changed(); persistance_loaded.send(PersistanceLoaded::::default()); diff --git a/src/editor/ui/mod.rs b/src/editor/ui/mod.rs index 830e84ab..eeff583e 100644 --- a/src/editor/ui/mod.rs +++ b/src/editor/ui/mod.rs @@ -38,7 +38,7 @@ use crate::{EditorSet, EditorState}; use self::{ mouse_check::{pointer_context_check, MouseCheck}, - tools::gizmo::GizmoTool, + tools::gizmo::{GizmoTool, GizmoToolPlugin}, }; use super::{ @@ -104,6 +104,7 @@ impl Plugin for EditorUiPlugin { app.add_plugins(SpaceInspectorPlugin); app.editor_tool(GizmoTool::default()); + app.add_plugins(GizmoToolPlugin); app.world.resource_mut::().active_tool = Some(0); app.add_plugins(settings::SettingsWindowPlugin); diff --git a/src/editor/ui/settings.rs b/src/editor/ui/settings.rs index 52621acf..adf3beac 100644 --- a/src/editor/ui/settings.rs +++ b/src/editor/ui/settings.rs @@ -1,7 +1,10 @@ -use bevy::prelude::*; +use bevy::{prelude::*, utils::HashSet}; use bevy_egui::*; -use crate::prelude::{EditorTab, EditorTabName}; +use crate::{ + editor::core::AllHotkeys, + prelude::{EditorTab, EditorTabName}, +}; #[cfg(feature = "persistance_editor")] use crate::prelude::editor::core::AppPersistanceExt; @@ -50,6 +53,8 @@ impl ToString for NewTabBehaviour { #[derive(Default, Resource, Clone)] pub struct SettingsWindow { pub new_tab: NewTabBehaviour, + read_input_for_hotkey: Option, + all_pressed_hotkeys: HashSet, } impl EditorTab for SettingsWindow { @@ -94,39 +99,107 @@ impl EditorTab for SettingsWindow { ui.spacing(); ui.heading("Hotkeys in Game view tab"); + if world.contains_resource::() { + egui::Grid::new("hotkeys_grid") + .num_columns(2) + .show(ui, |ui| { + world.resource_scope::(|world, all_hotkeys| { + all_hotkeys.global_map(world, &mut |world, set| { + ui.heading(set.get_name()); + ui.end_row(); + let all_bindings = set.get_flat_bindings(); + for (hotkey_name, bindings) in all_bindings { + ui.label(&hotkey_name); + + if let Some(read_input_for_hotkey) = &self.read_input_for_hotkey { + if hotkey_name == *read_input_for_hotkey { + let mut key_text = String::new(); + + world.resource_scope::, _>( + |_world, input| { + let all_pressed = input + .get_pressed() + .copied() + .collect::>(); + self.all_pressed_hotkeys.extend(all_pressed.iter()); + let all_pressed = self + .all_pressed_hotkeys + .iter() + .copied() + .collect::>(); + + if all_pressed.is_empty() { + key_text = "Wait for input".to_string(); + } else { + key_text = format!("{:?}", all_pressed[0]); + for key in all_pressed.iter().skip(1) { + key_text = + format!("{} + {:?}", key_text, key); + } + } + + if input.get_just_released().len() > 0 { + bindings.clear(); + *bindings = all_pressed; + self.read_input_for_hotkey = None; + self.all_pressed_hotkeys.clear(); + } + + ui.add(egui::Button::new( + egui::RichText::new(&key_text).strong(), + )); + }, + ); + } else { + let binding_text = if bindings.len() == 1 { + format!("{:?}", &bindings[0]) + } else { + format!("{:?}", bindings) + }; + + if ui.button(binding_text).clicked() { + self.read_input_for_hotkey = Some(hotkey_name); + } + } + } else { + let binding_text = if bindings.len() == 1 { + format!("{:?}", &bindings[0]) + } else { + format!("{:?}", bindings) + }; + + if ui.button(binding_text).clicked() { + self.read_input_for_hotkey = Some(hotkey_name); + } + } + + ui.end_row(); + } + }); + }); + }); + } - egui::Grid::new("hotkeys") - .num_columns(2) - .striped(true) - .show(ui, |ui| { - ui.label("Select object"); - ui.label("Left mouse button"); - ui.end_row(); - - ui.label("Move object"); - ui.label("G"); - ui.end_row(); - - ui.label("Rotate object"); - ui.label("R"); - ui.end_row(); - - ui.label("Scale object"); - ui.label("S"); - ui.end_row(); - - ui.label("Move/rotate/scale/clone \nmany objects simultaneously"); - ui.label("Shift"); - ui.end_row(); - - ui.label("Clone object"); - ui.label("Alt"); - ui.end_row(); - - ui.label("Delete object"); - ui.label("Delete or X"); - ui.end_row(); - }); + // egui::Grid::new("hotkeys") + // .num_columns(2) + // .striped(true) + // .show(ui, |ui| { + // ui.label("Select object"); + // ui.label("Left mouse button"); + // ui.end_row(); + + // ui.label("Move/rotate/scale/clone \nmany objects simultaneously"); + // ui.label("Shift"); + // ui.end_row(); + + // ui.label("Clone object"); + // ui.label("Alt"); + // ui.end_row(); + + // ui.label("Delete object"); + // ui.label("Delete or X"); + // ui.end_row(); + // }); } fn title(&self) -> egui::WidgetText { diff --git a/src/editor/ui/tools/gizmo.rs b/src/editor/ui/tools/gizmo.rs index 89cf710e..00b35573 100644 --- a/src/editor/ui/tools/gizmo.rs +++ b/src/editor/ui/tools/gizmo.rs @@ -3,11 +3,47 @@ use bevy_egui::egui::{self, Key}; use egui_gizmo::*; use crate::{ - editor::core::{EditorTool, Selected}, + editor::core::{EditorTool, Hotkey, HotkeyAppExt, Selected}, prelude::CloneEvent, EditorCameraMarker, }; +pub struct GizmoToolPlugin; + +impl Plugin for GizmoToolPlugin { + fn build(&self, app: &mut App) { + app.editor_hotkey(GizmoHotkey::Translate, vec![KeyCode::G]); + app.editor_hotkey(GizmoHotkey::Rotate, vec![KeyCode::R]); + app.editor_hotkey(GizmoHotkey::Scale, vec![KeyCode::S]); + app.editor_hotkey(GizmoHotkey::Delete, vec![KeyCode::X]); + app.editor_hotkey(GizmoHotkey::Multiple, vec![KeyCode::ShiftLeft]); + app.editor_hotkey(GizmoHotkey::Clone, vec![KeyCode::AltLeft]); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +pub enum GizmoHotkey { + Translate, + Rotate, + Scale, + Delete, + Multiple, + Clone, +} + +impl Hotkey for GizmoHotkey { + fn name<'a>(&self) -> String { + match self { + GizmoHotkey::Translate => "Translate entity".to_string(), + GizmoHotkey::Rotate => "Rotate entity".to_string(), + GizmoHotkey::Scale => "Scale entity".to_string(), + GizmoHotkey::Delete => "Delete entity".to_string(), + GizmoHotkey::Multiple => "Change multiple entities".to_string(), + GizmoHotkey::Clone => "Clone entity".to_string(), + } + } +} + pub struct GizmoTool { pub gizmo_mode: GizmoMode, pub is_move_cloned_entities: bool, @@ -33,6 +69,7 @@ impl EditorTool for GizmoTool { // If SHIFT pressed draw "mean" gizmo to move all selected entities together // If ALT pressed, then entity will be cloned at interact // If SHIFT+ALT pressed, then all selected entities will be cloned at interact + // All hotkeys can be changes in editor ui let mode2name = vec![ (GizmoMode::Translate, "⬌", "Translate"), @@ -60,24 +97,38 @@ impl EditorTool for GizmoTool { }); let mut del = false; + let mut clone_pressed = false; + let mut multiple_pressed = false; if ui.ui_contains_pointer() && !ui.ctx().wants_keyboard_input() { //hot keys. Blender keys preffer let mode2key = vec![ - (GizmoMode::Translate, Key::G), - (GizmoMode::Rotate, Key::R), - (GizmoMode::Scale, Key::S), + (GizmoMode::Translate, GizmoHotkey::Translate), + (GizmoMode::Rotate, GizmoHotkey::Rotate), + (GizmoMode::Scale, GizmoHotkey::Scale), ]; + let input = world.resource::>(); + for (mode, key) in mode2key { - if ui.input(|s| s.key_pressed(key)) { + if input.just_pressed(key) { self.gizmo_mode = mode; } } - if ui.input(|s| s.key_pressed(Key::Delete) || s.key_pressed(Key::X)) { + if ui.input(|s| s.key_pressed(Key::Delete) || input.just_pressed(GizmoHotkey::Delete)) { del = true; } + + if !input.pressed(GizmoHotkey::Clone) { + self.is_move_cloned_entities = false; + } else { + clone_pressed = true; + } + + if input.pressed(GizmoHotkey::Multiple) { + multiple_pressed = true; + } } if del { @@ -97,10 +148,6 @@ impl EditorTool for GizmoTool { (*ref_tr, ref_cam.clone()) }; - if ui.input(|s| !s.modifiers.alt) { - self.is_move_cloned_entities = false; - } - let selected = world .query_filtered::>() .iter(world) @@ -111,7 +158,7 @@ impl EditorTool for GizmoTool { let cell = world.as_unsafe_world_cell(); let view_matrix = Mat4::from(cam_transform.affine().inverse()); - if ui.input(|s| s.modifiers.shift) { + if multiple_pressed { let mut mean_transform = Transform::IDENTITY; for e in &selected { let Some(ecell) = cell.get_entity(*e) else { @@ -160,7 +207,7 @@ impl EditorTool for GizmoTool { global_mean = GlobalTransform::from(mean_transform); - if gizmo_interacted && ui.input(|s| s.modifiers.alt) { + if gizmo_interacted && clone_pressed { if self.is_move_cloned_entities { } else { for e in selected.iter() { @@ -223,7 +270,7 @@ impl EditorTool for GizmoTool { scale: Vec3::from(<[f32; 3]>::from(result.scale)), }; - if ui.input(|s| s.modifiers.alt) { + if clone_pressed { if self.is_move_cloned_entities { let new_transform = GlobalTransform::from(new_transform); @@ -255,7 +302,7 @@ impl EditorTool for GizmoTool { .mode(self.gizmo_mode) .interact(ui) { - if ui.input(|s| s.modifiers.alt) { + if clone_pressed { if self.is_move_cloned_entities { *transform = Transform { translation: Vec3::from(<[f32; 3]>::from(result.translation)),