tirbofish/dropbear · commit
8235d5cf136ac9978375ef6bdaa38a20e266b6ab
wip: ui editor
refactor: prepared kino_ui for serialization
refactor: integrated kino_ui WidgetTree into the components and redback-runtime.
feature: ability to switch between UI and editor
possible now
feature: implemented categorisation to adding a component
feature: scene settings related to overlaying.
Signature present but could not be verified.
Unverified
@@ -553,3 +553,5 @@ impl AnimationComponent { } } } + +pub const MAX_MORPH_WEIGHTS: usize = 4096; @@ -12,6 +12,7 @@ crate-type = ["rlib", "cdylib"] [dependencies] dropbear-macro = { path = "../dropbear-macro" } magna-carta = { path = "../magna-carta" } +kino-ui = { path = "../kino-ui", features = ["ser"]} anyhow.workspace = true postcard.workspace = true @@ -23,6 +23,8 @@ pub struct BillboardComponent { pub world_size: glam::Vec2, #[serde(default = "default_enabled")] pub enabled: bool, + #[serde(default)] + pub ui_tree: kino_ui::WidgetTree, } impl Default for BillboardComponent { @@ -32,6 +34,7 @@ impl Default for BillboardComponent { rotation: None, world_size: glam::Vec2::ONE, enabled: true, + ui_tree: Default::default(), } } } @@ -47,7 +50,7 @@ impl Component for BillboardComponent { ComponentDescriptor { fqtn: "eucalyptus_core::billboard::BillboardComponent".to_string(), type_name: "Billboard".to_string(), - category: Some("Rendering".to_string()), + category: Some("UI".to_string()), description: Some("Renders a camera-facing textured quad".to_string()), } } @@ -70,13 +73,19 @@ impl InspectableComponent for BillboardComponent { fn inspect( &mut self, _world: &World, - _entity: Entity, + entity: Entity, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>, ) { CollapsingHeader::new("Billboard") .default_open(true) .show(ui, |ui| { + if ui.button("Edit in UI Editor").clicked() { + ui.ctx().data_mut(|d| { + d.insert_temp::<Option<Entity>>(egui::Id::new("open_ui_editor"), Some(entity)); + }); + } + ui.checkbox(&mut self.enabled, "Enabled"); ui.horizontal(|ui| { @@ -474,7 +474,7 @@ pub trait Component: Sync + Send { /// `hecs::EntityBuilder` during scene initialisation. fn init( ser: &'_ Self::SerializedForm, - graphics: Arc<SharedGraphicsContext>, + _graphics: Arc<SharedGraphicsContext>, ) -> ComponentInitFuture<'_, Self>; /// Called every frame to update the component's state. @@ -489,7 +489,7 @@ pub trait Component: Sync + Send { /// Called when saving the scene to disk. Returns the [`Self::SerializedForm`] of the component that can be /// saved to disk. - fn save(&self, world: &hecs::World, entity: hecs::Entity) -> Box<dyn SerializedComponent>; + fn save(&self, _world: &hecs::World, _entity: hecs::Entity) -> Box<dyn SerializedComponent>; } pub trait InspectableComponent: Send + Sync { @@ -22,6 +22,7 @@ pub mod transform; pub mod types; pub mod utils; pub mod billboard; +pub mod ui; pub use dropbear_macro as macros; @@ -31,6 +32,7 @@ use crate::physics::kcc::KCC; use crate::physics::rigidbody::RigidBody; use crate::billboard::BillboardComponent; use crate::states::Script; +use crate::ui::HUDComponent; use dropbear_engine::animation::AnimationComponent; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, MeshRenderer}; @@ -60,4 +62,5 @@ pub fn register_components(component_registry: &mut ComponentRegistry) { component_registry.register::<KCC>(); component_registry.register::<AnimationComponent>(); component_registry.register::<BillboardComponent>(); + component_registry.register::<HUDComponent>(); } @@ -73,6 +73,14 @@ pub struct SceneSettings { /// Toggles rendering of collider hitboxes / wireframes. #[serde(default)] pub show_hitboxes: bool, + + /// Overlays the HUD on top of the viewport if it exists as an entity. + #[serde(default = "SceneSettings::default_overlay_hud")] + pub overlay_hud: bool, + + /// Overlays all billboard ui for all entities if it exists as a component. + #[serde(default = "SceneSettings::default_overlay_billboard")] + pub overlay_billboard: bool, } impl SceneSettings { @@ -81,8 +89,18 @@ impl SceneSettings { Self { preloaded: false, show_hitboxes: false, + overlay_hud: false, + overlay_billboard: true, } } + + pub(crate) const fn default_overlay_hud() -> bool { + false + } + + pub(crate) const fn default_overlay_billboard() -> bool { + true + } } /// Specifies the configuration of a scene, such as its entities, hierarchies and any settings that @@ -0,0 +1,62 @@ +use std::sync::Arc; +use egui::{CollapsingHeader, Ui}; +use hecs::{Entity, World}; +use dropbear_engine::graphics::SharedGraphicsContext; +use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent}; +use crate::physics::PhysicsState; + +#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct HUDComponent { + pub ui_tree: kino_ui::WidgetTree, +} + +impl HUDComponent { + pub fn tree(&self) -> &kino_ui::WidgetTree { + &self.ui_tree + } + + pub fn tree_mut(&mut self) -> &mut kino_ui::WidgetTree { + &mut self.ui_tree + } +} + +#[typetag::serde] +impl SerializedComponent for HUDComponent {} + +impl Component for HUDComponent { + type SerializedForm = Self; + type RequiredComponentTypes = (Self, ); + + fn descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "eucalyptus_core::ui::HUDComponent".to_string(), + type_name: "HUD".to_string(), + category: Some("UI".to_string()), + description: Some("Renders a camera-facing textured quad, typically used for a HUD or 2D context".to_string()), + } + } + + fn init(ser: &'_ Self::SerializedForm, _graphics: Arc<SharedGraphicsContext>) -> ComponentInitFuture<'_, Self> { + Box::pin(async move { Ok((ser.clone(),)) }) + } + + fn update_component(&mut self, _world: &World, _physics: &mut PhysicsState, _entity: Entity, _dt: f32, _graphics: Arc<SharedGraphicsContext>) { + + } + + fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> { + Box::new(self.clone()) + } +} + +impl InspectableComponent for HUDComponent { + fn inspect(&mut self, _world: &World, entity: Entity, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) { + CollapsingHeader::new("HUD").show(ui, |ui| { + if ui.button("Edit in UI Editor").clicked() { + ui.ctx().data_mut(|d| { + d.insert_temp::<Option<Entity>>(egui::Id::new("open_ui_editor"), Some(entity)); + }); + } + }); + } +} @@ -16,6 +16,7 @@ use crate::editor::{ SceneDivision, ScriptDivision, Signal, StaticallyKept, TABS_GLOBAL, }; use eucalyptus_core::component::DRAGGED_ASSET_ID; +use crate::editor::page::EditorTabVisibility; impl<'a> EditorTabViewer<'a> { pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) { @@ -1319,6 +1320,7 @@ impl<'a> EditorTabViewer<'a> { *self.selected_entity = None; } else if let Some(selection) = cfg.component_selection(node_id) { cfg.root_node_selected = false; + *self.selected_entity = selection.entity(); self.inspect_component_selection(cfg, selection); } else if let Some(entity) = Self::entity_from_node_id(node_id) { cfg.root_node_selected = false; @@ -1374,6 +1376,7 @@ impl EditorTabDock for AssetViewerDock { EditorTabDockDescriptor { id: "asset_viewer", title: "Asset Viewer".to_string(), + visibility: EditorTabVisibility::all(), } } @@ -3,9 +3,11 @@ use std::path::PathBuf; use egui::{Margin, RichText}; use crate::editor::{ - EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, - console_error::{ConsoleItem, ErrorLevel}, + EditorTabDock, EditorTabDockDescriptor, EditorTabViewer + , }; +use crate::editor::console::{ConsoleItem, ErrorLevel}; +use crate::editor::page::EditorTabVisibility; impl<'a> EditorTabViewer<'a> { pub fn build_console(&mut self, ui: &mut egui::Ui) { @@ -147,7 +149,10 @@ pub struct BuildConsoleDock; impl EditorTabDock for BuildConsoleDock { fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { id: "build_console", title: "Build Output".to_string(), + EditorTabDockDescriptor { + id: "build_console", + title: "Build Output".to_string(), + visibility: EditorTabVisibility::all(), // idk about this one } } @@ -1,6 +1,7 @@ use parking_lot::Mutex; use std::io::{BufRead, BufReader}; use std::net::{TcpListener, TcpStream}; +use std::path::PathBuf; use std::sync::Arc; pub struct EucalyptusConsole { @@ -87,3 +88,17 @@ impl EucalyptusConsole { buf } } + +pub enum ErrorLevel { + Info, + Warn, + Error, +} + +pub struct ConsoleItem { + pub id: u64, + pub error_level: ErrorLevel, + pub msg: String, + pub file_location: Option<PathBuf>, + pub line_ref: Option<String>, +} @@ -1,15 +0,0 @@ -use std::path::PathBuf; - -pub enum ErrorLevel { - Info, - Warn, - Error, -} - -pub struct ConsoleItem { - pub id: u64, - pub error_level: ErrorLevel, - pub msg: String, - pub file_location: Option<PathBuf>, - pub line_ref: Option<String>, -} @@ -11,6 +11,7 @@ use egui_dock::TabViewer; use hecs::{Entity, World}; use parking_lot::Mutex; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; +use crate::editor::page::EditorTabVisibility; pub struct EditorTabViewer<'a> { pub view: egui::TextureId, @@ -32,6 +33,7 @@ pub struct EditorTabViewer<'a> { pub build_logs: &'a mut Vec<String>, pub eucalyptus_console: &'a mut EucalyptusConsole, pub current_scene_name: &'a mut Option<String>, + pub ui_editor: &'a mut UiEditor, } pub type EditorTabId = u64; @@ -225,6 +227,7 @@ impl Default for EditorTabRegistry { pub struct EditorTabDockDescriptor { pub id: &'static str, pub title: String, + pub visibility: EditorTabVisibility, } pub trait EditorTabDock { @@ -347,6 +350,7 @@ impl EditorTabDock for ConsoleDock { EditorTabDockDescriptor { id: "console", title: "Console".to_string(), + visibility: EditorTabVisibility::all(), } } @@ -6,8 +6,10 @@ use eucalyptus_core::{ states::{Label, PROJECT}, }; use hecs::{Entity, World}; +use std::collections::BTreeMap; use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL}; +use crate::editor::page::EditorTabVisibility; impl<'a> EditorTabViewer<'a> { pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) { @@ -69,16 +71,42 @@ impl<'a> EditorTabViewer<'a> { } }); ui.menu_button("Add", |ui| { - log_once::debug_once!("Available components: "); + let mut grouped_components: BTreeMap<String, Vec<(u64, &eucalyptus_core::component::ComponentDescriptor)>> = + BTreeMap::new(); + for (id, desc) in registry.iter_available_components() { - log_once::debug_once!("id: {}, name: {}", id, desc.fqtn); + let category = desc + .category + .clone() + .unwrap_or_else(|| "Uncategorised".to_string()); + grouped_components + .entry(category) + .or_default() + .push((id, desc)); + } - if ui.button(desc.type_name.as_str()).clicked() { - if let Some(component) = registry.create_default_component(id) { - *signal = Signal::AddComponent(entity, component); + for (category, components) in grouped_components.iter_mut() { + components.sort_by(|a, b| a.1.type_name.cmp(&b.1.type_name)); + + ui.menu_button(category, |ui| { + for (id, desc) in components.iter() { + let response = ui.button(desc.type_name.as_str()); + + if let Some(description) = desc.description.as_ref() { + response.clone().on_hover_text(description); + } + + if response.clicked() { + if let Some(component) = + registry.create_default_component(*id) + { + *signal = + Signal::AddComponent(entity, component); + } + ui.close(); + } } - ui.close(); - } + }); } }); }), @@ -209,7 +237,10 @@ pub struct EntityListDock; impl EditorTabDock for EntityListDock { fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { id: "entity_list", title: "Model/Entity List".to_string(), + EditorTabDockDescriptor { + id: "entity_list", + title: "Model/Entity List".to_string(), + visibility: EditorTabVisibility::GameEditor, } } @@ -74,8 +74,9 @@ impl Keyboard for Editor { } } KeyCode::Delete => { + // todo: fix this up if !is_playing { - if let Some((_, tab)) = self.dock_state.find_active_focused() + if let Some((_, tab)) = self.game_editor_dock_state.find_active_focused() && self .tab_registry .id_for_title("Model/Entity List") @@ -163,8 +164,9 @@ impl Keyboard for Editor { } } KeyCode::KeyC => { + // todo: fix this if ctrl_pressed && !is_playing { - if let Some((_, tab)) = self.dock_state.find_active_focused() + if let Some((_, tab)) = self.game_editor_dock_state.find_active_focused() && self .tab_registry .id_for_title("Model/Entity List") @@ -1,8 +1,6 @@ pub mod asset_viewer; pub mod build_console; -// pub mod component; pub mod console; -pub mod console_error; pub mod dock; pub mod entity_list; pub mod input; @@ -10,20 +8,20 @@ pub mod resource; pub mod scene; pub mod settings; pub mod viewport; +pub mod page; +pub mod ui; pub(crate) use crate::editor::dock::*; -const MAX_MORPH_WEIGHTS: usize = 4096; - use crate::about::AboutWindow; use crate::build::build; use crate::debug; use crate::editor::console::EucalyptusConsole; -use crate::editor::settings::editor::{EDITOR_SETTINGS, EditorSettingsWindow}; +use crate::editor::settings::editor::{EditorSettingsWindow, EDITOR_SETTINGS}; use crate::editor::settings::project::ProjectSettingsWindow; use crate::plugin::PluginRegistry; use crate::stats::NerdStats; -use crossbeam_channel::{Receiver, Sender, unbounded}; +use crossbeam_channel::{unbounded, Receiver, Sender}; use dropbear_engine::billboarding::BillboardPipeline; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::EntityTransform; @@ -33,10 +31,10 @@ use dropbear_engine::pipelines::DropbearShaderPipeline; use dropbear_engine::pipelines::GlobalsUniform; use dropbear_engine::pipelines::light_cube::LightCubePipeline; use dropbear_engine::pipelines::shader::MainRenderPipeline; -use dropbear_engine::sky::{DEFAULT_SKY_TEXTURE, HdrLoader, SkyPipeline}; +use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE}; use dropbear_engine::{ - DropbearWindowBuilder, WindowData, camera::Camera, entity::Transform, future::FutureHandle, - graphics::SharedGraphicsContext, scene::SceneCommand, + camera::Camera, entity::Transform, future::FutureHandle, graphics::SharedGraphicsContext, scene::SceneCommand, + DropbearWindowBuilder, WindowData, }; use egui::{self, Context}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; @@ -48,14 +46,14 @@ use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use eucalyptus_core::scene::{SceneConfig, SceneEntity}; use eucalyptus_core::states::Label; -use eucalyptus_core::{APP_INFO, register_components}; +use eucalyptus_core::{register_components, APP_INFO}; use eucalyptus_core::{ camera::{CameraComponent, CameraType, DebugCamera}, fatal, info, input::InputState, scripting::BuildStatus, states, - states::{PROJECT, SCENES, WorldLoadingStatus}, + states::{WorldLoadingStatus, PROJECT, SCENES}, success, utils::ViewportMode, warn, @@ -71,6 +69,7 @@ use rfd::FileDialog; use std::collections::HashSet; use std::rc::Rc; use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, time::Instant}; +use std::cmp::PartialEq; use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; use wgpu::{Color, Extent3d}; @@ -79,13 +78,17 @@ use winit::dpi::PhysicalSize; use dropbear_engine::multisampling::AntiAliasingMode; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; +use dropbear_engine::animation::MAX_MORPH_WEIGHTS; +use crate::editor::page::EditorTabVisibility; +use crate::editor::ui::UiEditor; pub struct Editor { pub dt: f32, pub scene_command: SceneCommand, pub world: Box<World>, pub physics_state: PhysicsState, - pub dock_state: DockState<EditorTabId>, + pub game_editor_dock_state: DockState<EditorTabId>, + pub ui_editor_dock_state: DockState<EditorTabId>, pub texture_id: Option<egui::TextureId>, pub size: Extent3d, pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, @@ -94,6 +97,10 @@ pub struct Editor { pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, pub color: Color, + pub ui_editor: UiEditor, + + pub current_page: EditorTabVisibility, + // rendering pub light_cube_pipeline: Option<LightCubePipeline>, pub main_render_pipeline: Option<MainRenderPipeline>, @@ -170,7 +177,8 @@ pub struct Editor { // plugins pub plugin_registry: PluginRegistry, - pub dock_state_shared: Option<Arc<Mutex<DockState<EditorTabId>>>>, + pub game_dock_state_shared: Option<Arc<Mutex<DockState<EditorTabId>>>>, + pub ui_dock_state_shared: Option<Arc<Mutex<DockState<EditorTabId>>>>, // scene creation open_new_scene_window: bool, @@ -217,8 +225,8 @@ impl Editor { let surface = dock_state.main_surface_mut(); let [_old, right] = - surface.split_right(NodeIndex::root(), 0.25, vec![entity_list_tab]); - let [_old, _] = surface.split_left(NodeIndex::root(), 0.20, vec![resource_tab]); + surface.split_right(NodeIndex::root(), 0.25, vec![resource_tab]); + let [_old, _] = surface.split_left(NodeIndex::root(), 0.20, vec![entity_list_tab]); let [_old, _] = surface.split_below(right, 0.5, vec![asset_tab]); eucalyptus_core::utils::start_deadlock_detector(); @@ -249,7 +257,6 @@ impl Editor { Ok(Self { scene_command: SceneCommand::None, - dock_state, texture_id: None, size: Extent3d::default(), main_render_pipeline: None, @@ -260,6 +267,7 @@ impl Editor { window: None, world: Box::new(World::new()), physics_state: PhysicsState::new(), + game_editor_dock_state: dock_state, show_new_project: false, project_name: String::new(), project_path: Arc::new(Mutex::new(None)), @@ -295,7 +303,8 @@ impl Editor { last_build_error: None, show_build_error_window: false, plugin_registry, - dock_state_shared: None, + game_dock_state_shared: None, + ui_dock_state_shared: None, open_new_scene_window: false, new_scene_name: String::new(), current_scene_name: None, @@ -324,6 +333,9 @@ impl Editor { default_morph_info_buffer: None, default_animation_bind_group: None, dt: 60.0, + ui_editor_dock_state: DockState::new(vec![]), + current_page: EditorTabVisibility::GameEditor, + ui_editor: UiEditor::new(), }) } @@ -508,8 +520,8 @@ impl Editor { { let mut config = EDITOR_SETTINGS.write(); - let dock_state = self.dock_state.clone(); - config.dock_layout = Some(dock_state); + config.game_editor_dock_state = Some(self.game_editor_dock_state.clone()); + config.ui_editor_dock_state = Some(self.ui_editor_dock_state.clone()); config.save()?; } @@ -593,7 +605,8 @@ impl Editor { world_sender: Option<oneshot::Sender<hecs::World>>, active_camera: Arc<Mutex<Option<hecs::Entity>>>, project_path: Arc<Mutex<Option<PathBuf>>>, - dock_state: Arc<Mutex<DockState<EditorTabId>>>, + game_dock_state: Arc<Mutex<DockState<EditorTabId>>>, + ui_dock_state: Arc<Mutex<DockState<EditorTabId>>>, component_registry: Arc<ComponentRegistry>, ) -> anyhow::Result<()> { { @@ -603,8 +616,14 @@ impl Editor { let layout = EDITOR_SETTINGS.read(); - if let Some(layout) = &layout.dock_layout { - let mut dock = dock_state.lock(); + if let Some(layout) = &layout.game_editor_dock_state { + let mut dock = game_dock_state.lock(); + let layout = layout.clone(); + *dock = layout.clone(); + } + + if let Some(layout) = &layout.ui_editor_dock_state { + let mut dock = ui_dock_state.lock(); let layout = layout.clone(); *dock = layout.clone(); } @@ -903,8 +922,11 @@ impl Editor { } egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - ui.menu_button("File", |ui| { + let top_bar_rect = ui.max_rect(); + + ui.horizontal(|ui| { + egui::MenuBar::new().ui(ui, |ui| { + ui.menu_button("File", |ui| { if ui.button("New Scene").clicked() { self.open_new_scene_window = true; } @@ -1041,8 +1063,8 @@ impl Editor { } success!("Successfully saved project"); } - }); - ui.menu_button("Edit", |ui| { + }); + ui.menu_button("Edit", |ui| { if ui.button("Copy").clicked() { if let Some(entity) = &self.selected_entity { let Ok(label) = self.world.get::<&Label>(*entity) else { @@ -1092,17 +1114,55 @@ impl Editor { self.signal = Signal::Undo; } ui.label("Redo"); - }); + }); + + ui.menu_button("Window", |ui_window| { + let mut enabled_docks = Vec::new(); + let mut disabled_docks = Vec::new(); - ui.menu_button("Window", |ui_window| { for (k, v) in self.tab_registry.descriptors.iter() { + if v.visibility.intersects(self.current_page) { + enabled_docks.push((*k, v)); + } else { + disabled_docks.push((*k, v)); + } + } + + for (k, v) in &enabled_docks { if ui_window.button(v.title.as_str()).clicked() { - self.dock_state.push_to_focused_leaf(*k); + if self.current_page.contains(EditorTabVisibility::GameEditor) { + self.game_editor_dock_state.push_to_first_leaf(*k); + } else if self.current_page.contains(EditorTabVisibility::UIEditor) { + self.ui_editor_dock_state.push_to_first_leaf(*k); + } else { + fatal!("Unable to open dock: no page is open right now, likely editor bug"); + } } } - }); - ui.menu_button("Help", |ui| { + if !enabled_docks.is_empty() && !disabled_docks.is_empty() { + ui_window.separator(); + } + + for (_k, v) in &disabled_docks { + let mut available_locations = Vec::new(); + if v.visibility.contains(EditorTabVisibility::GameEditor) { + available_locations.push("Game"); + } + if v.visibility.contains(EditorTabVisibility::UIEditor) { + available_locations.push("UI"); + } + + let location_list = available_locations.join(", "); + let response = ui_window.add_enabled(false, egui::Button::new(v.title.as_str())); + response.on_hover_text(format!( + "Dock only available on [{}]", + location_list + )); + } + }); + + ui.menu_button("Help", |ui| { if ui.button("Show AppData folder").clicked() { match app_dirs2::app_root(app_dirs2::AppDataType::UserData, &APP_INFO) { Ok(val) => match open::that(&val) { @@ -1148,15 +1208,52 @@ impl Editor { .build(); self.scene_command = SceneCommand::RequestWindow(window); } - }); + }); - { - let cfg = EDITOR_SETTINGS.read(); - if cfg.is_debug_menu_shown { - debug::show_menu_bar(ui, &mut self.signal); + { + let cfg = EDITOR_SETTINGS.read(); + if cfg.is_debug_menu_shown { + debug::show_menu_bar(ui, &mut self.signal); + } } - } + }); + + }); + + let center_rect = egui::Rect::from_center_size( + top_bar_rect.center(), + egui::vec2(160.0, top_bar_rect.height()), + ); + ui.scope_builder(egui::UiBuilder::new().max_rect(center_rect), |ui| { + ui.with_layout( + egui::Layout::centered_and_justified(egui::Direction::LeftToRight), + |ui| { + ui.horizontal(|ui| { + let game_selected = + self.current_page.contains(EditorTabVisibility::GameEditor); + if ui.selectable_label(game_selected, "Game").clicked() { + self.current_page = EditorTabVisibility::GameEditor; + } + + ui.label("|"); + + let ui_selected = + self.current_page.contains(EditorTabVisibility::UIEditor); + if ui.selectable_label(ui_selected, "UI").clicked() { + self.current_page = EditorTabVisibility::UIEditor; + } + }); + }, + ); + }); + + let right_rect = egui::Rect::from_min_max( + egui::pos2(top_bar_rect.right() - 96.0, top_bar_rect.top()), + egui::pos2(top_bar_rect.right() - 8.0, top_bar_rect.bottom()), + ); + + ui.scope_builder(egui::UiBuilder::new().max_rect(right_rect), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let can_play = matches!(self.editor_state, EditorState::Playing); ui.group(|ui| { @@ -1171,7 +1268,11 @@ impl Editor { if ui.button("▶").clicked() { log::debug!("Menu Button Play button pressed"); let mut found_starting = false; - for (_, comp) in self.world.query::<(&Camera, &CameraComponent)>().iter() { + for (_, comp) in self + .world + .query::<(&Camera, &CameraComponent)>() + .iter() + { if comp.starting_camera { found_starting = true; } @@ -1257,7 +1358,13 @@ impl Editor { }; egui::CentralPanel::default().show(ctx, |ui| { - DockArea::new(&mut self.dock_state) + DockArea::new(if self.current_page.contains(EditorTabVisibility::GameEditor) { + &mut self.game_editor_dock_state + } else if self.current_page.contains(EditorTabVisibility::UIEditor) { + &mut self.ui_editor_dock_state + } else { + panic!("Unable to locate create dock area: current page not set, likely editor bug"); + }) .style(Style::from_egui(ui.style().as_ref())) .show_inside( ui, @@ -1281,8 +1388,10 @@ impl Editor { tab_registry: &self.tab_registry, eucalyptus_console: &mut self.console, current_scene_name: &mut self.current_scene_name, + ui_editor: &mut self.ui_editor, }, ); + }); { @@ -0,0 +1,11 @@ +use bitflags::bitflags; + +bitflags! { + #[derive(PartialEq, Copy, Clone)] + pub struct EditorTabVisibility : u8 { + /// The editor for the game design. + const GameEditor = 1; + /// The editor for UI + const UIEditor = 1 << 1; + } +} @@ -2,6 +2,7 @@ use hecs::Entity; use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL}; use dropbear_engine::camera::Camera; use eucalyptus_core::camera::{CameraComponent, CameraType}; +use crate::editor::page::EditorTabVisibility; impl<'a> EditorTabViewer<'a> { pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) { @@ -89,7 +90,10 @@ pub struct ResourceInspectorDock; impl EditorTabDock for ResourceInspectorDock { fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { id: "inspector", title: "Resource Inspector".to_string(), + EditorTabDockDescriptor { + id: "inspector", + title: "Resource Inspector".to_string(), + visibility: EditorTabVisibility::GameEditor, } } @@ -17,8 +17,8 @@ use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry}; use eucalyptus_core::properties::CustomProperties; -use eucalyptus_core::states::{Label, WorldLoadingStatus}; -use glam::vec2; +use eucalyptus_core::states::{Label, WorldLoadingStatus, SCENES}; +use eucalyptus_core::ui::HUDComponent; use hecs::Entity; use log; use parking_lot::Mutex; @@ -31,10 +31,9 @@ use std::{ use winit::{event::WindowEvent, event_loop::ActiveEventLoop, keyboard::KeyCode}; use winit::event::{MouseScrollDelta, TouchPhase}; use kino_ui::rendering::KinoRenderTargetId; -use kino_ui::widgets::Fill; impl Scene for Editor { - fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { self.current_scene_name = { let last_opened = { let project = PROJECT.read(); @@ -60,8 +59,11 @@ impl Scene for Editor { let active_camera_clone = self.active_camera.clone(); let project_path_clone = self.project_path.clone(); - let dock_state_shared = Arc::new(Mutex::new(self.dock_state.clone())); - let dock_state_for_loading = dock_state_shared.clone(); + let game_dock_state_shared = Arc::new(Mutex::new(self.game_editor_dock_state.clone())); + let game_dock_state_for_loading = game_dock_state_shared.clone(); + + let ui_dock_state_shared = Arc::new(Mutex::new(self.ui_editor_dock_state.clone())); + let ui_dock_state_for_loading = ui_dock_state_shared.clone(); let component_registry = self.component_registry.clone(); @@ -74,7 +76,8 @@ impl Scene for Editor { Some(tx2), active_camera_clone, project_path_clone, - dock_state_for_loading, + game_dock_state_for_loading, + ui_dock_state_for_loading, component_registry, ) .await @@ -86,7 +89,8 @@ impl Scene for Editor { self.world_load_handle = Some(handle); - self.dock_state_shared = Some(dock_state_shared); + self.game_dock_state_shared = Some(game_dock_state_shared); + self.ui_dock_state_shared = Some(ui_dock_state_shared); self.window = Some(graphics.window.clone()); self.is_world_loaded.mark_scene_loaded(); @@ -126,11 +130,18 @@ impl Scene for Editor { self.world = Box::new(loaded_world); self.is_world_loaded.mark_project_loaded(); - if let Some(dock_state_shared) = &self.dock_state_shared + if let Some(dock_state_shared) = &self.game_dock_state_shared + && let Some(loaded_dock_state) = dock_state_shared.try_lock() + { + self.game_editor_dock_state = loaded_dock_state.clone(); + log::debug!("Game dock state updated from loaded config"); + } + + if let Some(dock_state_shared) = &self.ui_dock_state_shared && let Some(loaded_dock_state) = dock_state_shared.try_lock() { - self.dock_state = loaded_dock_state.clone(); - log::debug!("Dock state updated from loaded config"); + self.ui_editor_dock_state = loaded_dock_state.clone(); + log::debug!("UI dock state updated from loaded config"); } log::debug!("World received"); @@ -243,7 +254,7 @@ impl Scene for Editor { } } - if let Some((_, tab)) = self.dock_state.find_active_focused() { + if let Some((_, tab)) = self.game_editor_dock_state.find_active_focused() { let viewport_tab = self.tab_registry.id_for_title("Viewport"); self.is_viewport_focused = viewport_tab.map_or(false, |id| *tab == id); } else { @@ -316,18 +327,65 @@ impl Scene for Editor { .record_stats(dt, self.world.len() as u32); } + let open_ui_editor = graphics.get_egui_context().data_mut(|d| { + d.get_temp::<Option<Entity>>(egui::Id::new("open_ui_editor")) + .flatten() + .inspect(|_| d.remove::<Option<Entity>>(egui::Id::new("open_ui_editor"))) + }); + + if let Some(entity) = open_ui_editor { + self.current_page = EditorTabVisibility::UIEditor; + self.ui_editor.active_entity = Some(entity); + } + + let (overlay_billboard, overlay_hud) = if let Some(scene_name) = &self.current_scene_name { + let scenes = SCENES.read(); + if let Some(scene) = scenes.iter().find(|s| s.scene_name == *scene_name) { + (scene.settings.overlay_billboard, scene.settings.overlay_hud) + } else { + (true, false) + } + } else { + (true, false) + }; + if let Some(kino) = &mut self.kino { - for (e, _) in self.world.query::<(Entity, &BillboardComponent)>().iter() { - kino.begin(KinoRenderTargetId::Billboard(e.to_bits().get())); - - // todo: remove after testing - kino_ui::rect(kino, "rect", |rect| { - rect.fill = Fill::new([1.0, 0.0, 0.0, 1.0]); - rect.anchor = kino_ui::widgets::Anchor::Center; - rect.size = vec2(500.0, 300.0); - }); + if overlay_billboard { + let billboard_trees: Vec<(u64, kino_ui::WidgetTree)> = self + .world + .query::<(Entity, &BillboardComponent)>() + .iter() + .filter_map(|(entity, billboard)| { + if billboard.enabled { + Some((entity.to_bits().get(), billboard.ui_tree.clone())) + } else { + None + } + }) + .collect(); - kino.flush(); + for (entity_id, tree) in billboard_trees { + kino.begin(KinoRenderTargetId::Billboard(entity_id)); + tree.submit(kino); + kino.flush(); + } + } + + if overlay_hud { + let hud_trees: Vec<kino_ui::WidgetTree> = self + .world + .query::<&HUDComponent>() + .iter() + .map(|hud| hud.tree().clone()) + .collect(); + + if !hud_trees.is_empty() { + kino.begin(KinoRenderTargetId::HUD); + for tree in hud_trees { + tree.submit(kino); + } + kino.flush(); + } } } @@ -27,6 +27,18 @@ impl<'a> EditorTabViewer<'a> { scene.settings.show_hitboxes = show_hitboxes; } ui.label("Renders collider wireframes for debugging"); + + let mut overlay_billboard = scene.settings.overlay_billboard; + if ui.checkbox(&mut overlay_billboard, "Overlay Billboard UI").changed() { + scene.settings.overlay_billboard = overlay_billboard; + } + ui.label("Renders billboard UI widgets for all entities"); + + let mut overlay_hud = scene.settings.overlay_hud; + if ui.checkbox(&mut overlay_hud, "Overlay HUD").changed() { + scene.settings.overlay_hud = overlay_hud; + } + ui.label("Renders the HUD UI overlay on top of the viewport"); } else { ui.label("Scene not found"); } @@ -29,9 +29,13 @@ pub static EDITOR_SETTINGS: Lazy<RwLock<EditorSettings>> = /// This is not related to a project, and is for each user who uses the editor. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct EditorSettings { - /// The layout of the dock. + /// The layout of the dock within the game editor page. #[serde(default)] - pub dock_layout: Option<DockState<EditorTabId>>, + pub game_editor_dock_state: Option<DockState<EditorTabId>>, + + /// The layout of the dock within the UI editor + #[serde(default)] + pub ui_editor_dock_state: Option<DockState<EditorTabId>>, #[serde(default)] pub target_fps: HistoricalOption<u32>, @@ -53,7 +57,8 @@ impl EditorSettings { /// Creates a new instance of [EditorSettings] pub fn new() -> Self { Self { - dock_layout: None, + game_editor_dock_state: None, + ui_editor_dock_state: None, is_debug_menu_shown: false, target_fps: HistoricalOption::none(), anti_aliasing_mode: AntiAliasingMode::MSAA4, @@ -0,0 +1,13 @@ +pub struct UiEditor { + pub active_entity: Option<hecs::Entity>, +} + +impl UiEditor { + pub fn new() -> Self { + Self { + active_entity: None, + } + } + + +} @@ -6,6 +6,7 @@ use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::utils::ViewportMode; use glam::DVec3; use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation}; +use crate::editor::page::EditorTabVisibility; impl<'a> EditorTabViewer<'a> { pub(crate) fn viewport_tab(&mut self, ui: &mut egui::Ui) { @@ -271,6 +272,7 @@ impl EditorTabDock for ViewportDock { EditorTabDockDescriptor { id: "viewport", title: "Viewport".to_string(), + visibility: EditorTabVisibility::GameEditor, } } @@ -18,5 +18,9 @@ image.workspace = true anyhow.workspace = true log.workspace = true winit.workspace = true - glyphon.workspace = true +serde = { workspace = true, optional = true} +typetag = { workspace = true, optional = true} + +[features] +ser = ["serde", "typetag"] @@ -3,7 +3,8 @@ Kino is a type of resin that is made by eucalyptus trees, and the name of the UI library. Built with wgpu and winit, this UI library is inspired by the ui crate [wick3dr0se/egor](https://github.com/wick3dr0se/egor) and uses -Assembly-like instructions to render different components, including standard and contained widgets. +Assembly-like instructions to render different components, including standard and containerised widgets. +It uses a FIFO (first in, first out) architecture for rendering. # Example @@ -42,4 +43,7 @@ pub fn render(kino: &mut KinoState) { // ... } -``` +``` + +## Features +- ser: allows for serialization of widgets with serde. @@ -7,6 +7,7 @@ pub mod rendering; pub mod resp; pub mod widgets; pub mod windowing; +mod tree; pub mod crates { pub use glyphon; @@ -15,6 +16,7 @@ pub mod crates { } pub use widgets::shorthand::*; +pub use tree::{WidgetDescriptor, WidgetNode, WidgetTree}; use crate::asset::{AssetServer, Handle}; use crate::camera::Camera2D; @@ -65,7 +67,7 @@ impl KinoState { pub fn renderer(&mut self) -> &mut KinoWGPURenderer { &mut self.renderer } - + /// Returns a mutable reference to the current [`KinoWinitWindowing`], used for handling events /// and windowing operations. pub fn windowing(&mut self) -> &mut KinoWinitWindowing { @@ -651,6 +653,7 @@ impl KinoState { } /// The id of the widget, often being a hash. +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub struct WidgetId(u64); @@ -0,0 +1,79 @@ +use crate::{KinoState, WidgetId}; + +#[cfg_attr(any(feature = "ser"), typetag::serde(tag = "type"))] +pub trait WidgetDescriptor: Send + Sync + 'static { + fn id(&self) -> WidgetId; + fn is_container(&self) -> bool { + false + } + fn label(&self) -> &'static str; + fn submit(self: Box<Self>, children: Vec<WidgetNode>, kino: &mut KinoState); + fn clone_boxed(&self) -> Box<dyn WidgetDescriptor>; +} + +impl Clone for Box<dyn WidgetDescriptor> { + fn clone(&self) -> Self { + self.clone_boxed() + } +} + +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct WidgetNode { + pub id: WidgetId, + pub widget: Box<dyn WidgetDescriptor>, + pub children: Vec<WidgetNode>, +} + +impl WidgetNode { + pub fn new(widget: impl WidgetDescriptor) -> Self { + let id = widget.id(); + Self { + id, + widget: Box::new(widget), + children: vec![], + } + } + + pub fn with_child(mut self, child: WidgetNode) -> Self { + self.children.push(child); + self + } + + pub fn with_children(mut self, children: impl IntoIterator<Item = WidgetNode>) -> Self { + self.children.extend(children); + self + } +} + +#[cfg_attr(feature = "ser", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct WidgetTree { + pub roots: Vec<WidgetNode>, +} + +impl Default for WidgetTree { + fn default() -> Self { + Self { roots: vec![] } + } +} + +impl WidgetTree { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, node: WidgetNode) { + self.roots.push(node); + } + + pub fn submit(self, kino: &mut KinoState) { + for node in self.roots { + submit_node(node, kino); + } + } +} + +pub(crate) fn submit_node(node: WidgetNode, kino: &mut KinoState) { + node.widget.submit(node.children, kino); +} @@ -63,6 +63,8 @@ fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 { vec2(max_width, total_height) } +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Row { pub id: WidgetId, pub anchor: Anchor, @@ -140,6 +142,36 @@ impl ContaineredWidget for Row { } } +#[cfg_attr(any(feature = "ser"), typetag::serde)] +impl crate::WidgetDescriptor for Row { + fn id(&self) -> crate::WidgetId { + self.id + } + + fn is_container(&self) -> bool { + true + } + + fn label(&self) -> &'static str { + "Row" + } + + fn submit(self: Box<Self>, children: Vec<crate::WidgetNode>, kino: &mut crate::KinoState) { + let id = self.id; + kino.add_container(self); + for child in children { + crate::tree::submit_node(child, kino); + } + kino.end_container(id); + } + + fn clone_boxed(&self) -> Box<dyn crate::WidgetDescriptor> { + Box::new(self.clone()) + } +} + +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Column { pub id: WidgetId, pub anchor: Anchor, @@ -216,3 +248,30 @@ impl ContaineredWidget for Column { self } } +#[cfg_attr(any(feature = "ser"), typetag::serde)] +impl crate::WidgetDescriptor for Column { + fn id(&self) -> crate::WidgetId { + self.id + } + + fn is_container(&self) -> bool { + true + } + + fn label(&self) -> &'static str { + "Column" + } + + fn submit(self: Box<Self>, children: Vec<crate::WidgetNode>, kino: &mut crate::KinoState) { + let id = self.id; + kino.add_container(self); + for child in children { + crate::tree::submit_node(child, kino); + } + kino.end_container(id); + } + + fn clone_boxed(&self) -> Box<dyn crate::WidgetDescriptor> { + Box::new(self.clone()) + } +} @@ -8,6 +8,8 @@ use glam::Vec2; use std::any::Any; /// Determines how the object is anchored. +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub enum Anchor { /// A center anchor is when the position is based on the center of the object (such as the /// center of a circle) @@ -37,6 +39,7 @@ pub trait ContaineredWidget: Send + Sync { /// Describes the colour that the widget will be filled in with. #[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Fill { /// The colour of the fill described as RGBA between the range `0.0` <-> `1.0`. /// @@ -61,6 +64,7 @@ impl Default for Fill { /// Describes the properties of the border/stroke of the widget. #[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Border { /// The colour of the border described as RGBA between the range `0.0` <-> `1.0`. pub colour: [f32; 4], @@ -12,6 +12,8 @@ use std::any::Any; use winit::event::{ElementState, MouseButton}; /// A simple and humble rectangle. +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Rectangle { /// The identifier of the widget. /// @@ -33,7 +35,8 @@ pub struct Rectangle { /// Default: [`vec2(64.0, 64.0)`] pub size: Vec2, - /// The texture that this + #[cfg_attr(any(feature = "ser"), serde(skip))] + /// The texture that this renders /// /// Default: [`None`] pub texture: Option<Handle<Texture>>, @@ -259,3 +262,54 @@ impl ContaineredWidget for Rectangle { self } } + +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] +pub struct RectContainer(pub Rectangle); + +#[cfg_attr(any(feature = "ser"), typetag::serde)] +impl crate::WidgetDescriptor for RectContainer { + fn id(&self) -> crate::WidgetId { + self.0.id + } + + fn is_container(&self) -> bool { + true + } + + fn label(&self) -> &'static str { + "RectContainer" + } + + fn submit(self: Box<Self>, children: Vec<crate::WidgetNode>, kino: &mut crate::KinoState) { + let id = self.0.id; + kino.add_container(Box::new(self.0)); + for child in children { + crate::tree::submit_node(child, kino); + } + kino.end_container(id); + } + + fn clone_boxed(&self) -> Box<dyn crate::WidgetDescriptor> { + Box::new(self.clone()) + } +} + +#[cfg_attr(any(feature = "ser"), typetag::serde)] +impl crate::WidgetDescriptor for Rectangle { + fn id(&self) -> crate::WidgetId { + self.id + } + + fn label(&self) -> &'static str { + "Rectangle" + } + + fn submit(self: Box<Self>, _children: Vec<crate::WidgetNode>, kino: &mut crate::KinoState) { + kino.add_widget(self); + } + + fn clone_boxed(&self) -> Box<dyn crate::WidgetDescriptor> { + Box::new(self.clone()) + } +} @@ -7,6 +7,8 @@ use glam::Vec2; use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping}; use std::any::Any; use winit::event::{ElementState, MouseButton}; +#[cfg(feature = "ser")] +use crate::widgets::text::ser::{SerializedAttrs, SerializedMetrics}; /// Creates a label with the specified text and properties. /// @@ -14,13 +16,23 @@ use winit::event::{ElementState, MouseButton}; /// Responses are weird for text, as it recognises the input when you touch the text itself. /// /// If you want an area, you might be interested in [`crate::rect_container`] (with a transparent colour). +#[derive(Clone)] +#[cfg_attr(any(feature = "ser"), derive(serde::Serialize, serde::Deserialize))] pub struct Text { pub id: WidgetId, pub text: String, pub position: Vec2, pub size: Vec2, + + #[cfg(not(feature = "ser"))] pub metrics: Metrics, + #[cfg(feature = "ser")] + pub metrics: SerializedMetrics, + + #[cfg(not(feature = "ser"))] pub attributes: AttrsOwned, + #[cfg(feature = "ser")] + pub attributes: SerializedAttrs, } impl Text { @@ -31,8 +43,8 @@ impl Text { text: text.to_string(), position: Vec2::new(10.0, 10.0), size: Vec2::ZERO, - metrics: Metrics::new(16.0, 1.0), - attributes: AttrsOwned::new(&Attrs::new().color(Color::rgb(0, 0, 0))), + metrics: Metrics::new(16.0, 1.0).into(), + attributes: AttrsOwned::new(&Attrs::new().color(Color::rgb(0, 0, 0))).into(), } } @@ -56,19 +68,25 @@ impl Text { } pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self { - self.attributes = attributes; + self.attributes = attributes.into(); self } pub fn with_metrics(mut self, metrics: Metrics) -> Self { - self.metrics = metrics; + self.metrics = metrics.into(); self } } impl NativeWidget for Text { fn render(self: Box<Self>, state: &mut KinoState) { - let mut buffer = Buffer::new(&mut state.renderer.text.font_system, self.metrics); + #[cfg(not(feature = "ser"))] + let (metrics, attributes) = (self.metrics, self.attributes); + #[cfg(feature = "ser")] + let (metrics, attributes): (Metrics, AttrsOwned) = + (self.metrics.into(), self.attributes.into()); + + let mut buffer = Buffer::new(&mut state.renderer.text.font_system, metrics); if self.size != Vec2::ZERO { buffer.set_size( &mut state.renderer.text.font_system, @@ -79,7 +97,7 @@ impl NativeWidget for Text { buffer.set_text( &mut state.renderer.text.font_system, &self.text, - &self.attributes.as_attrs(), + &attributes.as_attrs(), Shaping::Basic, ); @@ -91,7 +109,7 @@ impl NativeWidget for Text { } max_y = max_y.max(run.line_top + run.line_height); } - let intrinsic_size = Vec2::new(max_x.max(1.0), max_y.max(self.metrics.line_height)); + let intrinsic_size = Vec2::new(max_x.max(1.0), max_y.max(metrics.line_height)); let size = if self.size == Vec2::ZERO { intrinsic_size } else { @@ -135,3 +153,128 @@ impl NativeWidget for Text { self } } + +#[cfg_attr(any(feature = "ser"), typetag::serde)] +impl crate::WidgetDescriptor for Text { + fn id(&self) -> crate::WidgetId { + self.id + } + + fn label(&self) -> &'static str { + "Text" + } + + fn submit(self: Box<Self>, _children: Vec<crate::WidgetNode>, kino: &mut crate::KinoState) { + kino.add_widget(self); + } + + fn clone_boxed(&self) -> Box<dyn crate::WidgetDescriptor> { + Box::new(self.clone()) + } +} + +#[cfg(feature = "ser")] +pub mod ser { + use glyphon::{AttrsOwned, Color, FamilyOwned, Metrics, Style, Stretch, Weight}; + + #[derive(Clone, serde::Serialize, serde::Deserialize)] + pub struct SerializedMetrics { + pub font_size: f32, + pub line_height: f32, + } + + impl From<Metrics> for SerializedMetrics { + fn from(value: Metrics) -> Self { + Self { + font_size: value.font_size, + line_height: value.line_height, + } + } + } + + impl From<SerializedMetrics> for Metrics { + fn from(value: SerializedMetrics) -> Self { + Metrics::new(value.font_size, value.line_height) + } + } + + #[derive(Clone, serde::Serialize, serde::Deserialize)] + pub struct SerializedAttrs { + pub color: Option<u32>, + pub family: SerializedFamily, + pub weight: u16, + pub style: u8, + pub stretch: u8, + } + + #[derive(Clone, serde::Serialize, serde::Deserialize)] + pub enum SerializedFamily { + Name(String), + Serif, + SansSerif, + Cursive, + Fantasy, + Monospace, + } + + impl From<AttrsOwned> for SerializedAttrs { + fn from(a: AttrsOwned) -> Self { + Self { + color: a.color_opt.map(|c| c.0), + family: match a.family_owned { + FamilyOwned::Name(s) => SerializedFamily::Name(s.to_string()), + FamilyOwned::Serif => SerializedFamily::Serif, + FamilyOwned::SansSerif => SerializedFamily::SansSerif, + FamilyOwned::Cursive => SerializedFamily::Cursive, + FamilyOwned::Fantasy => SerializedFamily::Fantasy, + FamilyOwned::Monospace => SerializedFamily::Monospace, + }, + weight: a.weight.0, + style: match a.style { + Style::Normal => 0, + Style::Italic => 1, + Style::Oblique => 2, + }, + stretch: a.stretch.to_number() as u8, + } + } + } + + impl From<SerializedAttrs> for AttrsOwned { + fn from(s: SerializedAttrs) -> Self { + use glyphon::Attrs; + let family_owned = match s.family { + SerializedFamily::Name(n) => FamilyOwned::Name(n.into()), + SerializedFamily::Serif => FamilyOwned::Serif, + SerializedFamily::SansSerif => FamilyOwned::SansSerif, + SerializedFamily::Cursive => FamilyOwned::Cursive, + SerializedFamily::Fantasy => FamilyOwned::Fantasy, + SerializedFamily::Monospace => FamilyOwned::Monospace, + }; + let style = match s.style { + 1 => Style::Italic, + 2 => Style::Oblique, + _ => Style::Normal, + }; + let stretch = match s.stretch { + 1 => Stretch::UltraCondensed, + 2 => Stretch::ExtraCondensed, + 3 => Stretch::Condensed, + 4 => Stretch::SemiCondensed, + 6 => Stretch::SemiExpanded, + 7 => Stretch::Expanded, + 8 => Stretch::ExtraExpanded, + 9 => Stretch::UltraExpanded, + _ => Stretch::Normal, + }; + AttrsOwned::new( + &Attrs::new() + .family(family_owned.as_family()) + .weight(Weight(s.weight)) + .style(style) + .stretch(stretch) + .color(Color(s.color.unwrap_or(0))), + ) + } + } +} @@ -26,16 +26,14 @@ use eucalyptus_core::rapier3d::prelude::QueryFilter; use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResult}; use eucalyptus_core::states::SCENES; use eucalyptus_core::states::{Label, PROJECT}; -use glam::{vec2, DVec3, Mat3, Mat4, Quat, Vec2, Vec3}; +use eucalyptus_core::ui::HUDComponent; +use glam::{DVec3, Mat3, Mat4, Quat, Vec2, Vec3}; use hecs::Entity; -// use kino_ui::widgets::rect::Rectangle; -// use kino_ui::widgets::{Border, Fill}; use std::collections::HashMap; use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use kino_ui::rendering::KinoRenderTargetId; -use kino_ui::widgets::{Border, Fill}; -use kino_ui::widgets::rect::Rectangle; +use kino_ui::{WidgetTree}; impl Scene for PlayMode { fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { @@ -546,120 +544,42 @@ impl Scene for PlayMode { })); }); - if let Some(kino) = &mut self.kino { - // kino.begin(KinoRenderTargetId::HUD); - kino.begin(KinoRenderTargetId::Billboard(0)); - - // todo: clean this up after testing - let no_texture = kino.add_texture_from_bytes( - &graphics.device, - &graphics.queue, - "no texture", - include_bytes!("../../../resources/textures/no-texture.png"), - 256, - 256, - ); - - kino_ui::rect_container( - kino, - Rectangle::new("parent") - .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) - .size(vec2(400.0, 400.0)), - |kino| { - kino.add_widget( - Rectangle::new("rect") - .texture(no_texture) - .size(vec2(128.0, 100.0)) - .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0)) - .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) - .texture(no_texture) - .build(), - ); - }, - ); - - kino_ui::label(kino, "Hello World!", |l| { - l.position = vec2( - graphics.viewport_texture.size.width as f32 / 2.0, - graphics.viewport_texture.size.height as f32 / 2.0, - ); - l.metrics.font_size = 30.0; - }); - - kino.flush(); + let billboard_trees: Vec<(u64, WidgetTree)> = self + .world + .query::<(Entity, &BillboardComponent)>() + .iter() + .filter_map(|(entity, billboard)| { + if billboard.enabled { + Some((entity.to_bits().get(), billboard.ui_tree.clone())) + } else { + None + } + }) + .collect(); - kino.begin(KinoRenderTargetId::HUD); + let hud_trees: Vec<WidgetTree> = self + .world + .query::<&HUDComponent>() + .iter() + .map(|hud| hud.tree().clone()) + .collect(); - kino_ui::rect(kino, "rect", |rect| { - rect.fill = Fill::new([1.0, 0.0, 0.0, 1.0]); - }); + if let Some(kino) = &mut self.kino { + for (entity_id, tree) in billboard_trees { + kino.begin(KinoRenderTargetId::Billboard(entity_id)); + tree.submit(kino); + kino.flush(); + } - kino.flush(); + if !hud_trees.is_empty() { + kino.begin(KinoRenderTargetId::HUD); + for tree in hud_trees { + // there can only be one. + tree.submit(kino); + } + kino.flush(); + } } - - // if let Some(kino) = &mut self.kino { - // // #[allow(dead_code)] - // let no_texture = kino.add_texture_from_bytes( - // &graphics.device, - // &graphics.queue, - // "no texture", - // include_bytes!("../../../resources/textures/no-texture.png"), - // 256, - // 256, - // ); - - // let parent = kino_ui::rect_container( - // kino, - // Rectangle::new("parent") - // .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) - // .size(vec2(400.0, 400.0)), - // |kino| { - // kino.add_widget( - // Rectangle::new("rect") - // .texture(no_texture) - // .size(vec2(128.0, 100.0)) - // .border(Border::new([1.0, 0.0, 0.0, 1.0], 3.0)) - // .fill(Fill::new([1.0, 1.0, 1.0, 1.0])) - // .texture(no_texture) - // .build(), - // ); - // }, - // ); - - // kino_ui::label(kino, "Hello World!", |l| { - // l.position = vec2( - // graphics.viewport_texture.size.width as f32 / 2.0, - // graphics.viewport_texture.size.height as f32 / 2.0, - // ); - // l.metrics.font_size = 30.0; - // }); - - // kino.poll(); - - // if kino.response(parent).clicked { - // println!("Parent clicked!"); - // }; - - // // if kino.response(parent).hovering { - // // println!("Parent hovering"); - // // }; - - // if kino.response("rect").clicked { - // println!("child clicked!"); - // }; - - // // if kino.response("rect").hovering { - // // println!("child hovering"); - // // }; - - // if kino.response("Hello World!").clicked { - // println!("text clicked") - // } - - // if kino.response("Hello World!").hovering { - // println!("text hovering"); - // }; - // } } else { log::warn!("No such camera exists in the world"); }