kitgit

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.

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-03-07 02:49

view full diff

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index a1925fc..ec651e8 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -553,3 +553,5 @@ impl AnimationComponent {
         }
     }
 }
+
+pub const MAX_MORPH_WEIGHTS: usize = 4096;
\ No newline at end of file
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index d91d577..60e2573 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -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
diff --git a/crates/eucalyptus-core/src/billboard.rs b/crates/eucalyptus-core/src/billboard.rs
index 60d5405..757be2a 100644
--- a/crates/eucalyptus-core/src/billboard.rs
+++ b/crates/eucalyptus-core/src/billboard.rs
@@ -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| {
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index f548639..4469730 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -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 {
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index 2f00943..56fa85c 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -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>();
 }
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index eb203e0..994ceb7 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -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
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
new file mode 100644
index 0000000..288c4c5
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -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));
+                });
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index 433c7a3..c7128b0 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -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(),
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/build_console.rs b/crates/eucalyptus-editor/src/editor/build_console.rs
index 6e2daa8..99d4a10 100644
--- a/crates/eucalyptus-editor/src/editor/build_console.rs
+++ b/crates/eucalyptus-editor/src/editor/build_console.rs
@@ -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
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/console.rs b/crates/eucalyptus-editor/src/editor/console.rs
index 484a9d8..980d3c2 100644
--- a/crates/eucalyptus-editor/src/editor/console.rs
+++ b/crates/eucalyptus-editor/src/editor/console.rs
@@ -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>,
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/console_error.rs b/crates/eucalyptus-editor/src/editor/console_error.rs
deleted file mode 100644
index edf6c8f..0000000
--- a/crates/eucalyptus-editor/src/editor/console_error.rs
+++ /dev/null
@@ -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>,
-}
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index 270d112..d9cc38e 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -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(),
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index e1ea33c..70bcfe5 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -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,
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/input.rs b/crates/eucalyptus-editor/src/editor/input.rs
index d9681b8..f1f9df2 100644
--- a/crates/eucalyptus-editor/src/editor/input.rs
+++ b/crates/eucalyptus-editor/src/editor/input.rs
@@ -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")
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index ba8942b..0f149e8 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -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,
                     },
                 );
+
         });
 
         {
diff --git a/crates/eucalyptus-editor/src/editor/page.rs b/crates/eucalyptus-editor/src/editor/page.rs
new file mode 100644
index 0000000..a553af5
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/page.rs
@@ -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;
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
index f77fafb..489b1e1 100644
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ b/crates/eucalyptus-editor/src/editor/resource.rs
@@ -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,
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 523f214..339ff4a 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -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();
+                }
             }
         }
 
diff --git a/crates/eucalyptus-editor/src/editor/settings.rs b/crates/eucalyptus-editor/src/editor/settings.rs
index 10ba47b..3227070 100644
--- a/crates/eucalyptus-editor/src/editor/settings.rs
+++ b/crates/eucalyptus-editor/src/editor/settings.rs
@@ -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");
             }
diff --git a/crates/eucalyptus-editor/src/editor/settings/editor.rs b/crates/eucalyptus-editor/src/editor/settings/editor.rs
index 54f64d6..23c053d 100644
--- a/crates/eucalyptus-editor/src/editor/settings/editor.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/editor.rs
@@ -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,
diff --git a/crates/eucalyptus-editor/src/editor/ui/mod.rs b/crates/eucalyptus-editor/src/editor/ui/mod.rs
new file mode 100644
index 0000000..f7d8c49
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/ui/mod.rs
@@ -0,0 +1,13 @@
+pub struct UiEditor {
+    pub active_entity: Option<hecs::Entity>,
+}
+
+impl UiEditor {
+    pub fn new() -> Self {
+        Self {
+            active_entity: None,
+        }
+    }
+    
+    
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/viewport.rs b/crates/eucalyptus-editor/src/editor/viewport.rs
index 9c98624..2d22d10 100644
--- a/crates/eucalyptus-editor/src/editor/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/viewport.rs
@@ -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,
         }
     }
 
diff --git a/crates/kino-ui/Cargo.toml b/crates/kino-ui/Cargo.toml
index 550ecc7..6c4f824 100644
--- a/crates/kino-ui/Cargo.toml
+++ b/crates/kino-ui/Cargo.toml
@@ -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"]
diff --git a/crates/kino-ui/README.md b/crates/kino-ui/README.md
index c13900a..c236d7c 100644
--- a/crates/kino-ui/README.md
+++ b/crates/kino-ui/README.md
@@ -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) {
     
     // ...
 }
-```
\ No newline at end of file
+```
+
+## Features
+- ser: allows for serialization of widgets with serde. 
\ No newline at end of file
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
index 7c0dc2a..8a652ea 100644
--- a/crates/kino-ui/src/lib.rs
+++ b/crates/kino-ui/src/lib.rs
@@ -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);
 
diff --git a/crates/kino-ui/src/tree.rs b/crates/kino-ui/src/tree.rs
new file mode 100644
index 0000000..7cd5f14
--- /dev/null
+++ b/crates/kino-ui/src/tree.rs
@@ -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);
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/layout.rs b/crates/kino-ui/src/widgets/layout.rs
index 23bfed6..07a122a 100644
--- a/crates/kino-ui/src/widgets/layout.rs
+++ b/crates/kino-ui/src/widgets/layout.rs
@@ -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())
+    }
+}
diff --git a/crates/kino-ui/src/widgets/mod.rs b/crates/kino-ui/src/widgets/mod.rs
index e40d81b..77dc816 100644
--- a/crates/kino-ui/src/widgets/mod.rs
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -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],
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
index d3fd8da..57175f2 100644
--- a/crates/kino-ui/src/widgets/rect.rs
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -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())
+    }
+}
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
index 3980963..4497fe6 100644
--- a/crates/kino-ui/src/widgets/text.rs
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -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))),
+            )
+        }
+    }
+}
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index abff776..77873ae 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -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");
                 }