kitgit

tirbofish/dropbear · diff

d27053d · Thribhu K

feature: text rendering now works (workaround for wgpu 27) + input feature: contained widgets now work. feature: shorthand for UI

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index 5908bf2..4aa4d1e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,13 +74,13 @@ rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] }
 cbindgen = { version = "0.29.2" }
 postcard = { version = "1.1", features = ["use-std"]}
 pollster = "0.4"
-yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] }
-yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
-yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
+#yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] }
+#yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
+#yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
 thiserror = "2.0"
 tempfile = "3.24"
 combine = "4.6"
-glyphon = "0.8"
+glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" }
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 5a68677..bce2147 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -44,8 +44,8 @@ dashmap.workspace = true
 typetag.workspace = true
 postcard.workspace = true
 pollster.workspace = true
-yakui-wgpu.workspace = true
-yakui.workspace = true
+#yakui-wgpu.workspace = true
+#yakui.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index 00c51c5..f5072e1 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -32,8 +32,8 @@ pub struct SharedGraphicsContext {
     pub future_queue: Arc<FutureQueue>,
     pub supports_storage: bool,
     pub mipmapper: Arc<MipMapper>,
-    pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
-    pub yakui_texture: yakui::TextureId,
+    // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
+    // pub yakui_texture: yakui::TextureId,
 }
 
 impl SharedGraphicsContext {
@@ -76,8 +76,8 @@ impl SharedGraphicsContext {
             surface_format: state.surface_format,
             supports_storage: state.supports_storage,
             mipmapper: state.mipmapper.clone(),
-            yakui_renderer: state.yakui_renderer.clone(),
-            yakui_texture: state.yakui_texture.clone(),
+            // yakui_renderer: state.yakui_renderer.clone(),
+            // yakui_texture: state.yakui_texture.clone(),
             surface_config: state.config.clone(),
         }
     }
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 20acab3..f8b38cc 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -98,8 +98,8 @@ pub struct State {
     physics_accumulator: Duration,
 
     pub scene_manager: scene::Manager,
-    pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
-    pub yakui_texture: yakui::TextureId,
+    // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>,
+    // pub yakui_texture: yakui::TextureId,
 }
 
 impl State {
@@ -333,18 +333,18 @@ Hardware:
         let device = Arc::new(device);
         let queue = Arc::new(queue);
 
-        let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new(
-            &device,
-            &queue
-        )));
+        // let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new(
+        //     &device,
+        //     &queue
+        // )));
 
-        let yakui_texture = yakui_renderer.lock().add_texture(
-            viewport_texture.view.clone(),
-            wgpu::FilterMode::default(),
-            wgpu::FilterMode::default(),
-            wgpu::FilterMode::default(),
-            wgpu::AddressMode::default(),
-        );
+        // let yakui_texture = yakui_renderer.lock().add_texture(
+        //     viewport_texture.view.clone(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::FilterMode::default(),
+        //     wgpu::AddressMode::default(),
+        // );
 
         let result = Self {
             surface: Arc::new(surface),
@@ -373,8 +373,8 @@ Hardware:
                 light_cube_bind_group_layout,
             }),
             supports_storage: supports_storage_resources,
-            yakui_renderer,
-            yakui_texture,
+            // yakui_renderer,
+            // yakui_texture,
         };
 
         Ok(result)
diff --git a/crates/dropbear-engine/src/pipelines/hdr.rs b/crates/dropbear-engine/src/pipelines/hdr.rs
new file mode 100644
index 0000000..23d53bb
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/hdr.rs
@@ -0,0 +1,158 @@
+// use crate::texture::Texture;
+//
+// pub struct HdrPipeline {
+//     pipeline: wgpu::RenderPipeline,
+//     bind_group: wgpu::BindGroup,
+//     texture: Texture,
+//     width: u32,
+//     height: u32,
+//     format: wgpu::TextureFormat,
+//     layout: wgpu::BindGroupLayout,
+// }
+//
+// impl HdrPipeline {
+//     pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> Self {
+//         let width = config.width;
+//         let height = config.height;
+//
+//         // We could use `Rgba32Float`, but that requires some extra
+//         // features to be enabled for rendering.
+//         let format = wgpu::TextureFormat::Rgba16Float;
+//
+//         let texture = Texture(
+//             device,
+//             width,
+//             height,
+//             format,
+//             wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
+//             wgpu::FilterMode::Nearest,
+//             Some("Hdr::texture"),
+//         );
+//
+//         let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+//             label: Some("Hdr::layout"),
+//             entries: &[
+//                 // This is the HDR texture
+//                 wgpu::BindGroupLayoutEntry {
+//                     binding: 0,
+//                     visibility: wgpu::ShaderStages::FRAGMENT,
+//                     ty: wgpu::BindingType::Texture {
+//                         sample_type: wgpu::TextureSampleType::Float { filterable: true },
+//                         view_dimension: wgpu::TextureViewDimension::D2,
+//                         multisampled: false,
+//                     },
+//                     count: None,
+//                 },
+//                 wgpu::BindGroupLayoutEntry {
+//                     binding: 1,
+//                     visibility: wgpu::ShaderStages::FRAGMENT,
+//                     ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+//                     count: None,
+//                 },
+//             ],
+//         });
+//         let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+//             label: Some("Hdr::bind_group"),
+//             layout: &layout,
+//             entries: &[
+//                 wgpu::BindGroupEntry {
+//                     binding: 0,
+//                     resource: wgpu::BindingResource::TextureView(&texture.view),
+//                 },
+//                 wgpu::BindGroupEntry {
+//                     binding: 1,
+//                     resource: wgpu::BindingResource::Sampler(&texture.sampler),
+//                 },
+//             ],
+//         });
+//
+//         // We'll cover the shader next
+//         let shader = wgpu::include_wgsl!("hdr.wgsl");
+//         let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+//             label: None,
+//             bind_group_layouts: &[&layout],
+//             push_constant_ranges: &[],
+//         });
+//
+//         let pipeline = create_render_pipeline(
+//             device,
+//             &pipeline_layout,
+//             config.format.add_srgb_suffix(),
+//             None,
+//             // We'll use some math to generate the vertex data in
+//             // the shader, so we don't need any vertex buffers
+//             &[],
+//             wgpu::PrimitiveTopology::TriangleList,
+//             shader,
+//         );
+//
+//         Self {
+//             pipeline,
+//             bind_group,
+//             layout,
+//             texture,
+//             width,
+//             height,
+//             format,
+//         }
+//     }
+//
+//     /// Resize the HDR texture
+//     pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
+//         self.texture = texture::Texture::create_2d_texture(
+//             device,
+//             width,
+//             height,
+//             wgpu::TextureFormat::Rgba16Float,
+//             wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
+//             wgpu::FilterMode::Nearest,
+//             Some("Hdr::texture"),
+//         );
+//         self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+//             label: Some("Hdr::bind_group"),
+//             layout: &self.layout,
+//             entries: &[
+//                 wgpu::BindGroupEntry {
+//                     binding: 0,
+//                     resource: wgpu::BindingResource::TextureView(&self.texture.view),
+//                 },
+//                 wgpu::BindGroupEntry {
+//                     binding: 1,
+//                     resource: wgpu::BindingResource::Sampler(&self.texture.sampler),
+//                 },
+//             ],
+//         });
+//         self.width = width;
+//         self.height = height;
+//     }
+//
+//     /// Exposes the HDR texture
+//     pub fn view(&self) -> &wgpu::TextureView {
+//         &self.texture.view
+//     }
+//
+//     /// The format of the HDR texture
+//     pub fn format(&self) -> wgpu::TextureFormat {
+//         self.format
+//     }
+//
+//     /// This renders the internal HDR texture to the [TextureView]
+//     /// supplied as parameter.
+//     pub fn process(&self, encoder: &mut wgpu::CommandEncoder, output: &wgpu::TextureView) {
+//         let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+//             label: Some("Hdr::process"),
+//             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+//                 view: &output,
+//                 resolve_target: None,
+//                 ops: Operations {
+//                     load: wgpu::LoadOp::Load,
+//                     store: wgpu::StoreOp::Store,
+//                 },
+//             })],
+//             depth_stencil_attachment: None,
+//         });
+//         pass.set_pipeline(&self.pipeline);
+//         pass.set_bind_group(0, &self.bind_group, &[]);
+//         pass.draw(0..3, 0..1);
+//     }
+// }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/mod.rs b/crates/dropbear-engine/src/pipelines/mod.rs
index f151c40..9689e3e 100644
--- a/crates/dropbear-engine/src/pipelines/mod.rs
+++ b/crates/dropbear-engine/src/pipelines/mod.rs
@@ -5,6 +5,7 @@ use crate::shader::Shader;
 pub mod shader;
 pub mod light_cube;
 pub mod globals;
+mod hdr;
 
 pub use globals::{Globals, GlobalsUniform};
 
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
index e5b18cf..0761329 100644
--- a/crates/euca-runner/src/main.rs
+++ b/crates/euca-runner/src/main.rs
@@ -1,4 +1,4 @@
-#![cfg_attr(not(debug), windows_subsystem = "windows")]
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use app_dirs2::AppInfo;
 use dropbear_engine::future::FutureQueue;
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index 8c7901a..cdcdfbb 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -43,7 +43,7 @@ semver.workspace = true
 rustc_version_runtime.workspace = true
 rapier3d.workspace = true
 bytemuck.workspace = true
-yakui.workspace = true
+#yakui.workspace = true
 thiserror.workspace = true
 combine.workspace = true
 
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
index 242ae77..608ee11 100644
--- a/crates/eucalyptus-core/src/ui.rs
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -1,8 +1,8 @@
-mod button;
-mod utils;
-mod text;
-mod align;
-mod checkbox;
+// mod button;
+// mod utils;
+// mod text;
+// mod align;
+// mod checkbox;
 
 use std::any::Any;
 use std::cell::RefCell;
@@ -12,16 +12,16 @@ use ::jni::JNIEnv;
 use ::jni::objects::JObject;
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
-use yakui::{Alignment, MainAxisSize, Yakui};
+// use yakui::{Alignment, MainAxisSize, Yakui};
 use dropbear_engine::utils::ResourceReference;
 use dropbear_macro::SerializableComponent;
 use dropbear_traits::SerializableComponent;
 use crate::scripting::jni::utils::{FromJObject};
 use crate::scripting::result::DropbearNativeResult;
-use crate::ui::align::{AlignParser};
-use crate::ui::button::ButtonParser;
-use crate::ui::checkbox::CheckboxParser;
-use crate::ui::text::TextParser;
+// use crate::ui::align::{AlignParser};
+// use crate::ui::button::ButtonParser;
+// use crate::ui::checkbox::CheckboxParser;
+// use crate::ui::text::TextParser;
 
 thread_local! {
     pub static UI_CONTEXT: RefCell<UiContext> = RefCell::new(UiContext::new());
@@ -52,7 +52,7 @@ pub trait WidgetParser: Send + Sync {
 }
 
 pub struct UiContext {
-    pub yakui_state: Mutex<Yakui>,
+    // pub yakui_state: Mutex<Yakui>,
     pub instruction_set: Mutex<Vec<UiInstructionType>>,
     pub widget_states: Mutex<HashMap<i64, WidgetState>>,
     pub parsers: Mutex<Vec<Box<dyn WidgetParser>>>,
@@ -61,88 +61,25 @@ pub struct UiContext {
 pub fn poll() {
     UI_CONTEXT.with(|v| {
         let ctx = v.borrow();
-        let mut instructions = ctx.instruction_set.lock();
+        // let mut instructions = ctx.instruction_set.lock();
         let mut widget_states = ctx.widget_states.lock();
 
         widget_states.clear();
 
-        let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>();
+        // let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>();
 
-        let tree = build_tree(current_instructions);
+        // let tree = build_tree(current_instructions);
 
-        yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| {
-            yakui::widgets::List::column()
-                .main_axis_size(MainAxisSize::Max)
-                .show(|| {
-                    render_tree(tree, &mut widget_states);
-                });
-        });
+        // yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| {
+        //     yakui::widgets::List::column()
+        //         .main_axis_size(MainAxisSize::Max)
+        //         .show(|| {
+        //             render_tree(tree, &mut widget_states);
+        //         });
+        // });
     });
 }
 
-fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> {
-    let mut stack: Vec<UiNode> = Vec::new();
-    let mut root = Vec::new();
-
-    for instruction in instructions {
-        match &instruction {
-            UiInstructionType::Containered(container_ty) => {
-                match container_ty {
-                    ContaineredWidgetType::Start { .. } => {
-                        stack.push(UiNode {
-                            instruction,
-                            children: Vec::new(),
-                        });
-                    }
-                    ContaineredWidgetType::End { .. } => {
-                        if let Some(node) = stack.pop() {
-                            if let Some(parent) = stack.last_mut() {
-                                parent.children.push(node);
-                            } else {
-                                root.push(node);
-                            }
-                        }
-                    }
-                }
-            }
-            UiInstructionType::Widget(_) => {
-                let node = UiNode {
-                    instruction,
-                    children: Vec::new(),
-                };
-
-                if let Some(parent) = stack.last_mut() {
-                    parent.children.push(node);
-                } else {
-                    root.push(node);
-                }
-            }
-        }
-    }
-
-    root
-}
-
-pub fn render_tree(nodes: Vec<UiNode>, widget_state: &mut HashMap<i64, WidgetState>) {
-    for node in nodes {
-        match node.instruction {
-            UiInstructionType::Containered(container_ty) => {
-                match container_ty {
-                    ContaineredWidgetType::Start { widget, .. } => {
-                        widget.render(node.children, widget_state);
-                    }
-                    ContaineredWidgetType::End { .. } => {
-                        // already handled in tree building
-                    }
-                }
-            }
-            UiInstructionType::Widget(widget) => {
-                widget.render(widget_state);
-            }
-        }
-    }
-}
-
 #[derive(Debug)]
 pub enum UiInstructionType {
     Containered(ContaineredWidgetType),
@@ -180,15 +117,15 @@ impl UiContext {
     pub fn new() -> Self {
         let mut parsers: Vec<Box<dyn WidgetParser>> = Vec::new();
 
-        parsers.push(Box::new(ButtonParser));
-        parsers.push(Box::new(TextParser));
-        parsers.push(Box::new(AlignParser));
-        parsers.push(Box::new(CheckboxParser));
+        // parsers.push(Box::new(ButtonParser));
+        // parsers.push(Box::new(TextParser));
+        // parsers.push(Box::new(AlignParser));
+        // parsers.push(Box::new(CheckboxParser));
 
-        let yakui = Yakui::new();
+        // let yakui = Yakui::new();
 
         Self {
-            yakui_state: Mutex::new(yakui),
+            // yakui_state: Mutex::new(yakui),
             instruction_set: Default::default(),
             widget_states: Default::default(),
             parsers: Mutex::new(parsers),
diff --git a/crates/eucalyptus-editor/README.md b/crates/eucalyptus-editor/README.md
index 4d2c94f..d62f03c 100644
--- a/crates/eucalyptus-editor/README.md
+++ b/crates/eucalyptus-editor/README.md
@@ -5,8 +5,7 @@ supported.
 
 ## Other functions
 
-To run a local play test, run `eucalyptus-editor.exe play {project_dir}` with optional jvm args specified through
-`--jvm-args {jvm_args}`. 
+To run a local play test, run `eucalyptus-editor.exe play {project_dir}`. 
 
-eucalyptus-editor does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do
+eucalyptus-editor (the editor itself) does not have a scripting interface, therefore can be run without the Java Virtual Machine. If you do
 wish to enter into "Play Mode" or start testing your game, you will require a form of Java Development Kit (JDK).
\ No newline at end of file
diff --git a/crates/kino-ui/README.md b/crates/kino-ui/README.md
index c4235a8..c13900a 100644
--- a/crates/kino-ui/README.md
+++ b/crates/kino-ui/README.md
@@ -3,7 +3,7 @@
 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 contained widgets.
 
 # Example
 
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
index febd76d..62ac460 100644
--- a/crates/kino-ui/src/lib.rs
+++ b/crates/kino-ui/src/lib.rs
@@ -8,6 +8,14 @@ pub mod asset;
 pub mod math;
 pub mod windowing;
 
+pub mod crates {
+    pub use wgpu;
+    pub use winit;
+    pub use glyphon;
+}
+
+pub use widgets::shorthand::*;
+
 use crate::asset::{AssetServer, Handle};
 use crate::camera::Camera2D;
 use crate::rendering::texture::Texture;
@@ -15,6 +23,7 @@ use crate::rendering::vertex::Vertex;
 use crate::rendering::{KinoWGPURenderer};
 use crate::resp::WidgetResponse;
 use crate::widgets::{ContaineredWidget, NativeWidget};
+use crate::math::Rect;
 use std::borrow::Cow;
 use std::collections::{HashMap, VecDeque};
 use std::fmt::Debug;
@@ -23,6 +32,7 @@ use wgpu::{LoadOp, StoreOp};
 use rendering::batching::VertexBatch;
 use crate::windowing::KinoWinitWindowing;
 use glam::Vec2;
+use crate::rendering::text::KinoTextRenderer;
 
 /// Holds the state of all the instructions, and the vertices+indices for rendering as well
 /// as the responses.
@@ -34,6 +44,40 @@ pub struct KinoState {
     assets: AssetServer,
     batch: PrimitiveBatch,
     camera: Camera2D,
+    container_stack: Vec<ContainerContext>,
+}
+
+// public stuff
+impl KinoState {
+    /// Returns a mutable reference to the current [`KinoTextRenderer`], used for text and font
+    /// management. 
+    pub fn text(&mut self) -> &mut KinoTextRenderer {
+        &mut self.renderer.text
+    }
+    
+    /// Returns a mutable reference to the current [`KinoWGPURenderer`], used for rendering and 
+    /// pipelines. 
+    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 {
+        &mut self.windowing
+    }
+    
+    /// Returns a mutable reference to the current [`Camera2D`], used for displaying the current 
+    /// viewport. 
+    pub fn camera(&mut self) -> &mut Camera2D {
+        &mut self.camera
+    }
+    
+    /// Returns a mutable reference to the [`AssetServer`], used for storing textures and
+    /// other assets. 
+    pub fn assets(&mut self) -> &mut AssetServer {
+        &mut self.assets
+    }
 }
 
 impl KinoState {
@@ -50,6 +94,7 @@ impl KinoState {
             assets: Default::default(),
             batch: Default::default(),
             camera: Camera2D::default(),
+            container_stack: Default::default(),
         }
     }
 
@@ -65,14 +110,22 @@ impl KinoState {
     /// Adds a widget (a [`ContaineredWidget`]) to the instruction set as a
     /// [`UiInstructionType::Containered`] and returns the associated [`WidgetId`] for response
     /// checking.
-    pub fn add_container(&mut self, _container: Box<dyn ContaineredWidget>) -> WidgetId {
-        todo!("This is broken rn and idk how to implement it")
-        // self.instruction_set.push_back(UiInstructionType::Containered(
-        //     ContaineredWidgetType::Start {
-        //         id: container.id(),
-        //         widget: container,
-        //     }
-        // ))
+    pub fn add_container(&mut self, container: Box<dyn ContaineredWidget>) -> WidgetId {
+        let id = container.id();
+        self.instruction_set.push_back(UiInstructionType::Containered(
+            ContaineredWidgetType::Start {
+                id,
+                widget: container,
+            }
+        ));
+        id
+    }
+
+    /// Ends the current container block.
+    pub fn end_container(&mut self, id: WidgetId) {
+        self.instruction_set.push_back(UiInstructionType::Containered(
+            ContaineredWidgetType::End { id }
+        ));
     }
 
     /// Adds a [UiInstructionType] to the instruction set.
@@ -105,14 +158,20 @@ impl KinoState {
     ///
     /// This will only provide the proper information **after** you have
     /// polled with [`KinoState::poll`].
-    pub fn response(&self, id: WidgetId) -> WidgetResponse {
-        self.widget_states.get(&id).copied().unwrap_or_default()
+    pub fn response(&self, id: impl Into<WidgetId>) -> WidgetResponse {
+        self.widget_states.get(&id.into()).copied().unwrap_or_default()
     }
 
     pub fn set_viewport_offset(&mut self, offset: Vec2) {
         self.windowing.viewport_offset = offset;
     }
 
+    /// Sets both the viewport offset (top-left in screen space) and scale (screen->viewport).
+    pub fn set_viewport_transform(&mut self, offset: Vec2, scale: Vec2) {
+        self.windowing.viewport_offset = offset;
+        self.windowing.viewport_scale = scale;
+    }
+
     /// Pushes the vertices and indices to the renderer.
     ///
     /// This is the recommended `render()` function and is used when you want
@@ -135,6 +194,13 @@ impl KinoState {
         );
         let batch = self.batch.take();
 
+        let (width, height) = {
+            let tex = view.texture();
+            (tex.width(), tex.height())
+        };
+
+        self.renderer.text.prepare(&device, &queue, width, height);
+
         {
             let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("kino render pass"),
@@ -160,7 +226,7 @@ impl KinoState {
                 self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture);
             }
 
-            // self.renderer.text.render(&mut pass);
+            self.renderer.text.render(&mut pass);
         }
     }
 
@@ -170,11 +236,11 @@ impl KinoState {
     /// `kino_ui` to only draw the widgets.
     ///
     /// This sits inside your `render()` loop.
-    pub fn render_into_pass(
-        &mut self,
+    pub fn render_into_pass<'a>(
+        &'a mut self,
         device: &wgpu::Device,
         queue: &wgpu::Queue,
-        pass: &mut wgpu::RenderPass<'_>,
+        pass: &mut wgpu::RenderPass<'a>,
     ) {
         self.renderer.upload_camera_matrix(
             queue,
@@ -192,7 +258,7 @@ impl KinoState {
             self.renderer.draw_batch(pass, device, queue, &mut tg.batch, texture);
         }
 
-        // self.renderer.text.render(&mut pass);
+        self.renderer.text.render(pass);
     }
 
     /// Handles the event into the internal input state.
@@ -202,12 +268,6 @@ impl KinoState {
         self.windowing.handle_event(event);
     }
 
-    /// Returns a mutable reference to the internal [`AssetServer`], used
-    /// for storing textures.
-    pub fn assets(&mut self) -> &mut AssetServer {
-        &mut self.assets
-    }
-
     /// Creates (or reuses) a texture from raw RGBA bytes or encoded image bytes
     /// (e.g. `include_bytes!()` PNG/JPEG) and stores it by label.
     /// If a texture with the same content hash already exists, it reuses the handle
@@ -274,7 +334,7 @@ impl KinoState {
     }
 
     /// Returns a reference to the windowing. Used for input detection.
-    pub fn input(&self) -> &KinoWinitWindowing {
+    pub(crate) fn input(&self) -> &KinoWinitWindowing {
         &self.windowing
     }
 
@@ -282,6 +342,37 @@ impl KinoState {
         self.widget_states.insert(id, response);
     }
 
+    pub(crate) fn layout_offset(&self) -> Vec2 {
+        self.container_stack
+            .last()
+            .map(|ctx| ctx.offset)
+            .unwrap_or(Vec2::ZERO)
+    }
+
+    pub(crate) fn clip_contains(&self, point: Vec2) -> bool {
+        match self.container_stack.last().and_then(|ctx| ctx.clip) {
+            Some(rect) => rect.contains(point),
+            None => self.container_stack.is_empty(),
+        }
+    }
+
+    pub(crate) fn push_container(&mut self, rect: Rect) {
+        let parent_offset = self.layout_offset();
+        let world_rect = Rect::new(rect.position + parent_offset, rect.size);
+        let clip = match self.container_stack.last().and_then(|ctx| ctx.clip) {
+            Some(parent_clip) => intersect_rects(parent_clip, world_rect),
+            None => Some(world_rect),
+        };
+        self.container_stack.push(ContainerContext {
+            offset: world_rect.position,
+            clip,
+        });
+    }
+
+    pub(crate) fn pop_container(&mut self) {
+        self.container_stack.pop();
+    }
+
     fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> {
         let mut stack: Vec<UiNode> = Vec::new();
         let mut root = Vec::new();
@@ -325,7 +416,7 @@ impl KinoState {
         root
     }
 
-    fn render_tree(&mut self, nodes: Vec<UiNode>) {
+    pub(crate) fn render_tree(&mut self, nodes: Vec<UiNode>) {
         for node in nodes {
             match node.instruction {
                 UiInstructionType::Containered(container_ty) => {
@@ -353,6 +444,12 @@ impl KinoState {
 #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
 pub struct WidgetId(u64);
 
+impl Default for WidgetId {
+    fn default() -> Self {
+        WidgetId(0) // dummy value
+    }
+}
+
 impl WidgetId {
     /// Creates a new [`WidgetId`] from an object that can be hashed.
     pub fn new<H: Hash>(value: H) -> Self {
@@ -422,6 +519,12 @@ pub struct UiNode {
     pub children: Vec<UiNode>,
 }
 
+#[derive(Clone, Copy, Debug)]
+struct ContainerContext {
+    offset: Vec2,
+    clip: Option<Rect>,
+}
+
 #[derive(Debug, Default)]
 pub struct TexturedGeometry {
     pub texture_id: Option<Handle<Texture>>,
@@ -451,4 +554,14 @@ impl PrimitiveBatch {
     pub(crate) fn take(&mut self) -> Vec<TexturedGeometry> {
         std::mem::take(&mut self.geometry)
     }
+}
+
+fn intersect_rects(a: Rect, b: Rect) -> Option<Rect> {
+    let min = a.min().max(b.min());
+    let max = a.max().min(b.max());
+    if max.cmpge(min).all() {
+        Some(Rect::new(min, max - min))
+    } else {
+        None
+    }
 }
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering.rs b/crates/kino-ui/src/rendering.rs
index 236ee72..90c9fbd 100644
--- a/crates/kino-ui/src/rendering.rs
+++ b/crates/kino-ui/src/rendering.rs
@@ -3,22 +3,22 @@ use wgpu::util::{BufferInitDescriptor, DeviceExt};
 use batching::VertexBatch;
 use crate::camera::{CameraRendering, CameraUniform};
 use crate::rendering::pipeline::KinoRendererPipeline;
-// use crate::rendering::text::KinoTextRenderer;
+use crate::rendering::text::KinoTextRenderer;
 
 pub mod pipeline;
 pub mod texture;
 pub mod vertex;
 pub mod batching;
-// pub mod text;
+pub mod text;
 
 pub struct KinoWGPURenderer {
     pipeline: KinoRendererPipeline,
     default_texture: texture::Texture,
     pub format: wgpu::TextureFormat,
     pub size: Vec2,
-    // pub text: KinoTextRenderer,
+    pub text: KinoTextRenderer,
 
-    camera: CameraRendering,
+    pub camera: CameraRendering,
 }
 
 impl KinoWGPURenderer {
@@ -55,7 +55,7 @@ impl KinoWGPURenderer {
         });
 
         let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout, surface_format);
-        // let text = KinoTextRenderer::new(&device, &queue, surface_format);
+        let text = KinoTextRenderer::new(&device, &queue, surface_format);
 
         log::debug!("Created KinoWGPURenderer");
         Self {
@@ -63,7 +63,7 @@ impl KinoWGPURenderer {
             default_texture,
             format: surface_format,
             size: Vec2::from_array(size),
-            // text,
+            text,
             camera: CameraRendering {
                 buffer: camera_buffer,
                 bind_group: camera_bind_group,
diff --git a/crates/kino-ui/src/rendering/text.rs b/crates/kino-ui/src/rendering/text.rs
index 384c08a..dd5291d 100644
--- a/crates/kino-ui/src/rendering/text.rs
+++ b/crates/kino-ui/src/rendering/text.rs
@@ -7,8 +7,7 @@ pub struct TextEntry {
 }
 
 pub struct KinoTextRenderer {
-    pub font_system:
-        FontSystem,
+    pub font_system: FontSystem,
     pub swash_cache: SwashCache,
     pub atlas: TextAtlas,
     pub renderer: TextRenderer,
@@ -48,7 +47,8 @@ impl KinoTextRenderer {
     }
 
     /// Prepare the text renderer for drawing
-    pub(crate) fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) {
+    pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) {
+        self.viewport.update(queue, Resolution { width, height });
         if self.entries.is_empty() {
             return;
         }
@@ -85,13 +85,13 @@ impl KinoTextRenderer {
         self.entries.clear();
     }
 
-    pub(crate) fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
+    pub fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
         self.renderer
             .render(&self.atlas, &self.viewport, pass)
             .unwrap();
     }
 
-    pub(crate) fn resize(&mut self, width: u32, height: u32, queue: &wgpu::Queue) {
+    pub fn resize(&mut self, width: u32, height: u32, queue: &wgpu::Queue) {
         self.viewport.update(queue, Resolution { width, height });
     }
 }
diff --git a/crates/kino-ui/src/resp.rs b/crates/kino-ui/src/resp.rs
index 4e46ea3..dad460d 100644
--- a/crates/kino-ui/src/resp.rs
+++ b/crates/kino-ui/src/resp.rs
@@ -1,5 +1,8 @@
+use crate::{WidgetId};
+
 #[derive(Clone, Copy, Debug, Default)]
 pub struct WidgetResponse {
+    pub queried: WidgetId,
     pub clicked: bool,
     pub hovering: bool,
 }
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/mod.rs b/crates/kino-ui/src/widgets/mod.rs
index 6544624..20c1dfe 100644
--- a/crates/kino-ui/src/widgets/mod.rs
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -1,11 +1,16 @@
 pub mod rect;
-// pub mod text;
+pub mod shorthand;
+pub mod text;
 
 use std::any::Any;
 use crate::{KinoState, UiNode, WidgetId};
 
+/// Determines how the object is anchored.
 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)
     Center,
+    /// A top left anchor is when the position is based on the top left corner of the rectangle.
     TopLeft,
 }
 
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
index 12407af..b547784 100644
--- a/crates/kino-ui/src/widgets/rect.rs
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -2,12 +2,12 @@
 
 use std::any::Any;
 use glam::{vec2, Mat2, Vec2};
-use crate::{KinoState, WidgetId};
+use crate::{KinoState, UiNode, WidgetId};
 use crate::asset::Handle;
 use crate::math::Rect;
 use crate::rendering::texture::Texture;
 use crate::rendering::vertex::Vertex;
-use crate::widgets::{Anchor, Border, Fill, NativeWidget};
+use crate::widgets::{Anchor, Border, ContaineredWidget, Fill, NativeWidget};
 use crate::resp::WidgetResponse;
 use winit::event::{ElementState, MouseButton};
 
@@ -82,16 +82,19 @@ impl Rectangle {
     }
 
     /// Sets the anchor point of the rectangle
+    ///
     /// Defaults to [`Anchor::TopLeft`].
     pub fn anchor(mut self, anchor: Anchor) -> Self {
         self.anchor = anchor;
         self
     }
+
     /// Sets the world-space position of the rectangle
     pub fn at(mut self, position: impl Into<Vec2>) -> Self {
         self.position = position.into();
         self
     }
+
     /// Sets the size of the rectangle
     pub fn size(mut self, size: Vec2) -> Self {
         self.size = size;
@@ -124,37 +127,46 @@ impl Rectangle {
     }
 
     /// Custom UV coordinates
+    ///
     /// Defaults to covering the full texture ((0,0) - (1,1))
     pub fn uv(mut self, coords: [[f32; 2]; 4]) -> Self {
         self.uvs = coords;
         self
     }
-}
 
-impl NativeWidget for Rectangle {
-    fn render(self: Box<Self>, state: &mut KinoState) {
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+
+    fn compute_rect(&self, state: &KinoState) -> (Vec2, Rect, Mat2) {
+        let container_offset = state.layout_offset();
         let offset = match self.anchor {
             Anchor::TopLeft => Vec2::ZERO,
             Anchor::Center => -self.size / 2.0,
         };
-        let top_left = self.position + offset;
+        let local_top_left = self.position + offset;
+        let top_left = local_top_left + container_offset;
         let rect = Rect::new(top_left, self.size);
+        let rot = Mat2::from_angle(self.rotation);
+        (local_top_left, rect, rot)
+    }
 
+    fn render_body(&self, state: &mut KinoState, rect: &Rect, rot: Mat2) {
         let input = state.input();
-        let hovering = rect.contains(input.mouse_position);
+        let hovering = rect.contains(input.mouse_position)
+            && state.clip_contains(input.mouse_position);
         let clicked = hovering
             && input.mouse_button == MouseButton::Left
             && input.mouse_press_state == ElementState::Pressed;
         state.set_response(
             self.id,
             WidgetResponse {
+                queried: self.id,
                 clicked,
                 hovering,
             },
         );
 
-        let rot = Mat2::from_angle(self.rotation);
-
         let fill_verts: Vec<_> = rect
             .corners()
             .iter()
@@ -170,12 +182,12 @@ impl NativeWidget for Rectangle {
         if let Some(border) = self.border {
             let half_width = border.width / 2.0;
             let outer_rect = Rect::new(
-                top_left - Vec2::splat(half_width),
-                self.size + Vec2::splat(border.width)
+                rect.position - Vec2::splat(half_width),
+                rect.size + Vec2::splat(border.width)
             );
             let inner_rect = Rect::new(
-                top_left + Vec2::splat(half_width),
-                self.size - Vec2::splat(border.width)
+                rect.position + Vec2::splat(half_width),
+                rect.size - Vec2::splat(border.width)
             );
 
             let outer_corners = outer_rect.corners();
@@ -200,6 +212,31 @@ impl NativeWidget for Rectangle {
             state.batch.push(&border_verts, &border_indices, None);
         }
     }
+}
+
+impl NativeWidget for Rectangle {
+    fn render(self: Box<Self>, state: &mut KinoState) {
+        let (_local_top_left, rect, rot) = self.compute_rect(state);
+        self.render_body(state, &rect, rot);
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl ContaineredWidget for Rectangle {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let (local_top_left, rect, rot) = self.compute_rect(state);
+        self.render_body(state, &rect, rot);
+        state.push_container(Rect::new(local_top_left, self.size));
+        state.render_tree(children);
+        state.pop_container();
+    }
 
     fn id(&self) -> WidgetId {
         self.id
diff --git a/crates/kino-ui/src/widgets/shorthand.rs b/crates/kino-ui/src/widgets/shorthand.rs
new file mode 100644
index 0000000..e84b96c
--- /dev/null
+++ b/crates/kino-ui/src/widgets/shorthand.rs
@@ -0,0 +1,45 @@
+use crate::{KinoState, WidgetId};
+use crate::widgets::rect::Rectangle;
+use crate::widgets::text::Text;
+
+/// Shorthand for a standard rectangle widget.
+pub fn rect<F>(kino: &mut KinoState, id: impl Into<WidgetId>, configure: F) -> WidgetId
+where
+    F: FnOnce(&mut Rectangle),
+{
+    let id = id.into();
+    let mut rect = Rectangle::new(id);
+    configure(&mut rect);
+    kino.add_widget(Box::new(rect));
+    id
+}
+
+/// Shorthand for a rectangle container.
+///
+/// `configure` sets up the rectangle, and `contents` emits child widgets between
+/// start/end instructions.
+pub fn rect_container<C>(
+    kino: &mut KinoState,
+    rect: Rectangle,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = rect.id;
+
+    kino.add_container(Box::new(rect));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
+
+/// Shorthand for a standard label. 
+pub fn label<F>(kino: &mut KinoState, text: impl ToString, configure: F) -> WidgetId
+where
+    F: FnOnce(&mut Text),
+{
+    let mut text = Text::new(text);
+    configure(&mut text);
+    kino.add_widget(Box::new(text))
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
index 637efbd..857c6ed 100644
--- a/crates/kino-ui/src/widgets/text.rs
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -2,26 +2,37 @@ use std::any::Any;
 use glam::Vec2;
 use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping};
 use crate::{KinoState, WidgetId};
+use crate::math::Rect;
 use crate::rendering::text::TextEntry;
+use crate::resp::WidgetResponse;
 use crate::widgets::NativeWidget;
+use winit::event::{ElementState, MouseButton};
 
+/// Creates a label with the specified text and properties. 
+/// 
+/// # Input
+/// 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). 
 pub struct Text {
     pub id: WidgetId,
     pub text: String,
     pub position: Vec2,
+    pub size: Vec2,
     pub metrics: Metrics,
     pub attributes: AttrsOwned,
 }
 
 impl Text {
     /// Create a new text builder that will push text to the renderer
-    pub fn new(text: String) -> Self {
+    pub fn new(text: impl ToString) -> Self {
         Self {
-            id: WidgetId::from_str(&text),
-            text,
+            id: text.to_string().into(),
+            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()),
+            attributes: AttrsOwned::new(&Attrs::new().color(Color::rgb(0, 0, 0))),
         }
     }
 
@@ -37,6 +48,13 @@ impl Text {
         self
     }
 
+    /// Sets the position & size from a [`Rect`].
+    pub fn with(mut self, rect: &Rect) -> Self {
+        self.position = rect.position;
+        self.size = rect.size;
+        self
+    }
+
     pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self {
         self.attributes = attributes;
         self
@@ -46,21 +64,65 @@ impl Text {
         self.metrics = metrics;
         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);
+        if self.size != Vec2::ZERO {
+            buffer.set_size(
+                &mut state.renderer.text.font_system,
+                Some(self.size.x),
+                Some(self.size.y),
+            );
+        }
         buffer.set_text(
             &mut state.renderer.text.font_system,
             &self.text,
-            self.attributes.as_attrs(),
+            &self.attributes.as_attrs(),
             Shaping::Basic,
         );
 
+        let mut max_x = 0.0f32;
+        let mut max_y = 0.0f32;
+        for run in buffer.layout_runs() {
+            if let Some(last) = run.glyphs.last() {
+                max_x = max_x.max(last.x + last.w);
+            }
+            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 size = if self.size == Vec2::ZERO {
+            intrinsic_size
+        } else {
+            self.size
+        };
+
+        let top_left = self.position + state.layout_offset();
+        let rect = Rect::new(top_left, size);
+
+        let input = state.input();
+        let hovering = rect.contains(input.mouse_position)
+            && state.clip_contains(input.mouse_position);
+        let clicked = hovering
+            && input.mouse_button == MouseButton::Left
+            && input.mouse_press_state == ElementState::Pressed;
+        state.set_response(
+            self.id,
+            WidgetResponse {
+                queried: self.id,
+                clicked,
+                hovering,
+            },
+        );
+
         state.renderer.text.entries.push(TextEntry {
             buffer,
-            position: self.position,
+            position: top_left,
         });
     }
 
diff --git a/crates/kino-ui/src/windowing.rs b/crates/kino-ui/src/windowing.rs
index bfbe72d..3f70541 100644
--- a/crates/kino-ui/src/windowing.rs
+++ b/crates/kino-ui/src/windowing.rs
@@ -15,6 +15,8 @@ pub struct KinoWinitWindowing {
     _window: Arc<Window>,
     /// The top-left most pixel
     pub viewport_offset: Vec2,
+    /// Scale from screen-space to viewport texture space
+    pub viewport_scale: Vec2,
 }
 
 impl KinoWinitWindowing {
@@ -26,6 +28,7 @@ impl KinoWinitWindowing {
             mouse_press_state: ElementState::Released,
             _window: window,
             viewport_offset: Default::default(),
+            viewport_scale: Vec2::ONE,
         }
     }
 
@@ -33,7 +36,8 @@ impl KinoWinitWindowing {
         match event {
             WindowEvent::CursorMoved { position, .. } => {
                 let screen_pos = Vec2::new(position.x as f32, position.y as f32);
-                self.mouse_position = screen_pos - self.viewport_offset;
+                let local = screen_pos - self.viewport_offset;
+                self.mouse_position = local * self.viewport_scale;
             }
             WindowEvent::MouseInput { state, button, .. } => {
                 self.mouse_button = *button;
diff --git a/crates/redback-runtime/Cargo.toml b/crates/redback-runtime/Cargo.toml
index e2642bc..6e4bd89 100644
--- a/crates/redback-runtime/Cargo.toml
+++ b/crates/redback-runtime/Cargo.toml
@@ -29,13 +29,13 @@ serde.workspace = true
 crossbeam-channel.workspace = true
 gilrs.workspace = true
 futures.workspace = true
-yakui-winit.workspace = true
-yakui.workspace = true
 egui.workspace = true
 egui_extras.workspace = true
 glam.workspace = true
 wgpu.workspace = true
-yakui-wgpu.workspace = true
+#yakui-winit.workspace = true
+#yakui.workspace = true
+#yakui-wgpu.workspace = true
 
 [features]
 debug = []
diff --git a/crates/redback-runtime/src/input.rs b/crates/redback-runtime/src/input.rs
index 4f2e8a4..d127717 100644
--- a/crates/redback-runtime/src/input.rs
+++ b/crates/redback-runtime/src/input.rs
@@ -5,7 +5,6 @@ use winit::event_loop::ActiveEventLoop;
 use winit::keyboard::KeyCode;
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
 use crate::PlayMode;
-use eucalyptus_core::ui::UI_CONTEXT;
 
 impl Keyboard for PlayMode {
     fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) {
@@ -32,57 +31,14 @@ impl Mouse for PlayMode {
         self.input_state.mouse_delta = delta;
         self.input_state.mouse_pos = (position.x, position.y);
         self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position));
-
-        UI_CONTEXT.with(|ctx| {
-            let yak = ctx.borrow();
-            let mut yakui = yak.yakui_state.lock();
-            let relative_x = (position.x as f32) - self.viewport_offset.0;
-            let relative_y = (position.y as f32) - self.viewport_offset.1;
-            
-            yakui.handle_event(yakui::event::Event::CursorMoved(Some(yakui::geometry::Vec2::new(relative_x, relative_y))));
-        });
     }
 
     fn mouse_down(&mut self, button: MouseButton) {
         self.input_state.mouse_button.insert(button);
-        
-        UI_CONTEXT.with(|ctx| {
-            let yak = ctx.borrow();
-            let mut yakui = yak.yakui_state.lock();
-            let btn = match button {
-                MouseButton::Left => Some(yakui::input::MouseButton::One),
-                MouseButton::Right => Some(yakui::input::MouseButton::Two),
-                MouseButton::Middle => Some(yakui::input::MouseButton::Three),
-                _ => None,
-            };
-            if let Some(b) = btn {
-                yakui.handle_event(yakui::event::Event::MouseButtonChanged {
-                    button: b,
-                    down: true,
-                });
-            }
-        });
     }
 
     fn mouse_up(&mut self, button: MouseButton) {
         self.input_state.mouse_button.remove(&button);
-        
-        UI_CONTEXT.with(|ctx| {
-            let yak = ctx.borrow();
-            let mut yakui = yak.yakui_state.lock();
-            let btn = match button {
-                MouseButton::Left => Some(yakui::input::MouseButton::One),
-                MouseButton::Right => Some(yakui::input::MouseButton::Two),
-                MouseButton::Middle => Some(yakui::input::MouseButton::Three),
-                _ => None,
-            };
-            if let Some(b) = btn {
-                yakui.handle_event(yakui::event::Event::MouseButtonChanged {
-                    button: b,
-                    down: false,
-                });
-            }
-        });
     }
 }
 
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 70679c6..864cc46 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -27,7 +27,7 @@ use std::collections::HashMap;
 use std::path::PathBuf;
 use wgpu::SurfaceConfiguration;
 use winit::window::Fullscreen;
-use yakui_winit::YakuiWinit;
+// use yakui_winit::YakuiWinit;
 use dropbear_engine::texture::Texture;
 use eucalyptus_core::physics::PhysicsState;
 use eucalyptus_core::rapier3d::prelude::*;
@@ -149,7 +149,7 @@ pub struct PlayMode {
     viewport_offset: (f32, f32),
 
     // ui
-    yakui_winit: Option<YakuiWinit>,
+    // yakui_winit: Option<YakuiWinit>,
     kino: Option<kino_ui::KinoState>,
 }
 
@@ -197,7 +197,7 @@ impl PlayMode {
             collision_event_receiver: Some(ce_r),
             collision_force_event_receiver: Some(cfe_r),
             event_collector,
-            yakui_winit: None,
+            // yakui_winit: None,
             display_settings: DisplaySettings {
                 window_mode: WindowMode::Windowed,
                 maintain_aspect_ratio: true,
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 5506bc0..1a4bdd9 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -33,15 +33,15 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOAD
 use crate::PlayMode;
 use eucalyptus_core::physics::collider::shader::create_wireframe_geometry;
 use eucalyptus_core::ui::UI_CONTEXT;
-use kino_ui::widgets::{Border, Fill};
+use kino_ui::widgets::{Anchor, Border, Fill};
 use kino_ui::widgets::rect::Rectangle;
 
 impl Scene for PlayMode {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>) {
-        let mut yak = yakui_winit::YakuiWinit::new(&graphics.window);
-        yak.set_automatic_viewport(false);
-        yak.set_automatic_scale_factor(false);
-        self.yakui_winit = Some(yak);
+        // let mut yak = yakui_winit::YakuiWinit::new(&graphics.window);
+        // yak.set_automatic_viewport(false);
+        // yak.set_automatic_scale_factor(false);
+        // self.yakui_winit = Some(yak);
         
         if self.current_scene.is_none() {
             let initial_scene = if let Some(s) = &self.initial_scene {
@@ -493,7 +493,20 @@ impl Scene for PlayMode {
 
                     self.viewport_offset = (image_rect.min.x, image_rect.min.y);
                     if let Some(kino) = &mut self.kino {
-                        kino.set_viewport_offset(Vec2::new(image_rect.min.x, image_rect.min.y));
+                        let scale_x = if display_width > 0.0 {
+                            graphics.viewport_texture.size.width as f32 / display_width
+                        } else {
+                            1.0
+                        };
+                        let scale_y = if display_height > 0.0 {
+                            graphics.viewport_texture.size.height as f32 / display_height
+                        } else {
+                            1.0
+                        };
+                        kino.set_viewport_transform(
+                            Vec2::new(image_rect.min.x, image_rect.min.y),
+                            Vec2::new(scale_x, scale_y),
+                        );
                     }
 
                     ui.allocate_exact_size(available_size, egui::Sense::hover());
@@ -506,31 +519,31 @@ impl Scene for PlayMode {
                     });
 
                     // overlay
-                    UI_CONTEXT.with(|yakui_cell| {
-                        let yak = yakui_cell.borrow();
-                        let mut yakui = yak.yakui_state.lock();
-
-                        let tex_size = graphics.viewport_texture.size;
-                        let viewport_size = yakui::geometry::Vec2::new(
-                            tex_size.width as f32,
-                            tex_size.height as f32,
-                        );
-                        yakui.set_surface_size(viewport_size);
-                        yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size(
-                            yakui::geometry::Vec2::ZERO,
-                            viewport_size,
-                        ));
-                        yakui.set_scale_factor(graphics.window.scale_factor() as f32);
-
-                        yakui.start();
-
-                        // eucalyptus_core::ui::poll();
-
-                        yakui.finish();
-                    });
+                    // UI_CONTEXT.with(|yakui_cell| {
+                    //     let yak = yakui_cell.borrow();
+                    //     let mut yakui = yak.yakui_state.lock();
+                    // 
+                    //     let tex_size = graphics.viewport_texture.size;
+                    //     let viewport_size = yakui::geometry::Vec2::new(
+                    //         tex_size.width as f32,
+                    //         tex_size.height as f32,
+                    //     );
+                    //     yakui.set_surface_size(viewport_size);
+                    //     yakui.set_unscaled_viewport(yakui::geometry::Rect::from_pos_size(
+                    //         yakui::geometry::Vec2::ZERO,
+                    //         viewport_size,
+                    //     ));
+                    //     yakui.set_scale_factor(graphics.window.scale_factor() as f32);
+                    // 
+                    //     yakui.start();
+                    // 
+                    //     // eucalyptus_core::ui::poll();
+                    // 
+                    //     yakui.finish();
+                    // });
 
                     if let Some(kino) = &mut self.kino {
-                        #[allow(dead_code)]
+                        // #[allow(dead_code)]
                         let no_texture = kino.add_texture_from_bytes(
                             &graphics.device, &graphics.queue,
                             "no texture",
@@ -538,22 +551,52 @@ impl Scene for PlayMode {
                             256, 256
                         );
 
-                        let rect = kino.add_widget(Box::new(
-                            Rectangle::new("rect")
-                                .texture(no_texture)
-                                .size(vec2(128.0, 100.0))
-                                .border(Border::new([1.0, 1.0, 1.0, 1.0], 3.0))
-                                .fill(Fill::new([1.0, 1.0, 1.0, 0.5])),
-                        ));
+                        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(rect).clicked {
-                            println!("Clicked!");
+                        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!("Hovering...");
+                        // 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 {
@@ -959,13 +1002,13 @@ impl Scene for PlayMode {
     }
 
     fn handle_event(&mut self, event: &WindowEvent) {
-        UI_CONTEXT.with(|yakui_cell| {
-            let yak = yakui_cell.borrow();
-            let mut yakui = yak.yakui_state.lock();
-            if let Some(yak) = &mut self.yakui_winit {
-                yak.handle_window_event(&mut yakui, event);
-            }
-        });
+        // UI_CONTEXT.with(|yakui_cell| {
+        //     let yak = yakui_cell.borrow();
+        //     let mut yakui = yak.yakui_state.lock();
+        //     if let Some(yak) = &mut self.yakui_winit {
+        //         yak.handle_window_event(&mut yakui, event);
+        //     }
+        // });
 
         if let Some(kino) = &mut self.kino {
             kino.handle_event(event);
diff --git a/crates/slank/README.md b/crates/slank/README.md
index e178cd9..090bfad 100644
--- a/crates/slank/README.md
+++ b/crates/slank/README.md
@@ -40,7 +40,8 @@ slank::CompiledSlangShader::from_bytes("shader_label", shader_bytes);
 There are two main features currently available:
 
 - `download-slang` - Slank downloads the latest slangc compiler from the GitHub releases and stores it 
-                     in the user cache. The SLANG_DIR will be set to the directory of the slangc compiler. 
+                     in the user cache. The SLANG_DIR will be set to the directory of the slangc compiler.
+                     Note: Using this in CI will require you to add a `GITHUB_TOKEN` env var to avoid the rate limits. Locally, you are fine. 
 - `use-wgpu` - Enables wgpu as a dependency and unlocks utility traits for wgpu. 
 
 # Contribution
diff --git a/crates/slank/build.rs b/crates/slank/build.rs
index 21f597b..4819473 100644
--- a/crates/slank/build.rs
+++ b/crates/slank/build.rs
@@ -1,8 +1,5 @@
-use std::fs::File;
 use std::path::PathBuf;
 use std::process::Command;
-use flate2::read::GzDecoder;
-use tar::Archive;
 
 fn main() -> anyhow::Result<()> {
     println!("cargo:rerun-if-changed=build.rs");
@@ -254,7 +251,7 @@ fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> {
         return Err(anyhow::anyhow!("Download failed with status: {}", response.status()));
     }
 
-    let mut file = File::create(dest)
+    let mut file = std::fs::File::create(dest)
         .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?;
 
     response.copy_to(&mut file)
@@ -265,7 +262,7 @@ fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> {
 
 #[cfg(feature = "download-slang")]
 fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> {
-    let file = File::open(archive)
+    let file = std::fs::File::open(archive)
         .map_err(|e| anyhow::anyhow!("Failed to open archive: {}", e))?;
 
     if archive.extension().and_then(|s| s.to_str()) == Some("zip") {
@@ -275,8 +272,8 @@ fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> {
         archive.extract(dest)
             .map_err(|e| anyhow::anyhow!("Failed to extract zip: {}", e))?;
     } else {
-        let tar = GzDecoder::new(file);
-        let mut archive = Archive::new(tar);
+        let tar = flate2::read::GzDecoder::new(file);
+        let mut archive = tar::Archive::new(tar);
 
         archive.unpack(dest)
             .map_err(|e| anyhow::anyhow!("Failed to extract tar.gz: {}", e))?;
diff --git a/crates/slank/src/lib.rs b/crates/slank/src/lib.rs
index 89c5f1a..a31ccc9 100644
--- a/crates/slank/src/lib.rs
+++ b/crates/slank/src/lib.rs
@@ -578,6 +578,6 @@ mod tests {
         let bytes = vec![1, 2, 3, 4];
         let shader = CompiledSlangShader::from_bytes("idk", &bytes);
         assert_eq!(shader.source, bytes);
-        assert_eq!(shader.label(), "unknown");
+        assert_eq!(shader.label(), "idk");
     }
 }
\ No newline at end of file