kitgit

tirbofish/dropbear · diff

c4d1e16 · Thribhu K

feature: implemented text (but still on wgpu 27.0) :( feature: implemented basic input checking docs: new readme.md feature: got texturing working

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index 1aecd11..5908bf2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -80,6 +80,7 @@ yakui-winit = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f
 thiserror = "2.0"
 tempfile = "3.24"
 combine = "4.6"
+glyphon = "0.8"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/README.md b/README.md
index c1410ff..221bda4 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,6 @@
   <h1 align="center">dropbear</h1>
 </p>
 
----
-
 dropbear is a game engine used to create games, made in Rust and scripted with the Kotlin Language.
 
 It's name is a double entendre, with it being the nickname of koalas but also fits in nicely with the theme of rust utilising memory management with "drops".
@@ -18,7 +16,7 @@ If you might have not realised, all the crates/projects names are after Australi
 - [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-core) is the library used by both `redback-runtime` and `eucalyptus-editor` to share configs and metadata between each other.
 - [redback-runtime](https://github.com/tirbofish/dropbear/tree/main/crates/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
 - [magna-carta](https://github.com/tirbofish/dropbear/tree/main/crates/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching.
-- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time. 
+- [kino_ui](https://github.com/tirbofish/dropbear/tree/main/crates/kino_ui) is the main runtime-side UI system to render widgets and elements. 
 
 [//]: # (- [eucalyptus-sdk]&#40;https://github.com/tirbofish/dropbear/tree/main/eucalyptus-sdk&#41; is used to develop plugins to be used with the `eucalyptus-editor`)
 
@@ -26,6 +24,7 @@ If you might have not realised, all the crates/projects names are after Australi
 
 - [dropbear_future-queue](https://github.com/tirbofish/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context
 - [model_to_image](https://github.com/tirbofish/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasterizer. _(very crude but usable)_
+- [slank](https://github.com/tirbofish/dropbear/tree/main/crates/slank) is a slang compiler that compiles .slang files into shaders during build.rs compilation time.
 
 ## Build
 
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 015d73d..3e49d4e 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -23,14 +23,12 @@ use dropbear_macro::SerializableComponent;
 pub struct EntityTransform {
     local: Transform,
     world: Transform,
-    #[serde(skip)]
-    previous_world: Option<Transform>,
 }
 
 impl EntityTransform {
     /// Creates a new [EntityTransform] from a local and world [Transform]
     pub fn new(local: Transform, world: Transform) -> Self {
-        Self { local, world, previous_world: None }
+        Self { local, world }
     }
 
     /// Creates a new [EntityTransform] from a world [Transform] and a default local transform.
@@ -40,7 +38,6 @@ impl EntityTransform {
         Self {
             world,
             local: Transform::default(),
-            previous_world: None,
         }
     }
 
@@ -77,26 +74,6 @@ impl EntityTransform {
             scale: self.world.scale * self.local.scale,
         }
     }
-
-    /// Returns an interpolated transform between previous and current world transforms
-    /// This is used to smooth out rendering between physics updates
-    pub fn interpolate(&self, alpha: f64) -> Transform {
-        if let Some(prev) = self.previous_world {
-            let current = self.sync();
-            Transform {
-                position: prev.position.lerp(current.position, alpha),
-                rotation: prev.rotation.slerp(current.rotation, alpha),
-                scale: prev.scale.lerp(current.scale, alpha),
-            }
-        } else {
-            self.sync()
-        }
-    }
-
-    /// Stores the current world transform as the previous state for interpolation
-    pub fn store_previous(&mut self) {
-        self.previous_world = Some(self.world);
-    }
 }
 
 /// A type that represents a position, rotation and scale of an entity
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index e53d544..00c51c5 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -227,11 +227,11 @@ impl CommandEncoder {
     /// Submits the command encoder for execution.
     ///
     /// Panics if an unwinding error is caught, or just returns the error as normal.
-    pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
+    pub fn submit(self) -> anyhow::Result<()> {
         let command_buffer = self.inner.finish();
 
         match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
-            graphics.queue.submit(std::iter::once(command_buffer));
+            self.queue.submit(std::iter::once(command_buffer));
         })) {
             Ok(_) => {Ok(())}
             Err(_) => {
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 6da1a98..20acab3 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -480,7 +480,7 @@ Hardware:
                 });
             }
 
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
index 23ceec6..e5b18cf 100644
--- a/crates/euca-runner/src/main.rs
+++ b/crates/euca-runner/src/main.rs
@@ -1,4 +1,4 @@
-#![windows_subsystem = "windows"]
+#![cfg_attr(not(debug), windows_subsystem = "windows")]
 
 use app_dirs2::AppInfo;
 use dropbear_engine::future::FutureQueue;
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 32d158a..f525312 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -413,7 +413,7 @@ impl Scene for Editor {
                 });
             }
 
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
@@ -717,7 +717,7 @@ impl Scene for Editor {
                     }
                 }
             }
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
diff --git a/crates/eucalyptus-editor/src/main.rs b/crates/eucalyptus-editor/src/main.rs
index 5a8ee55..b5fbfa1 100644
--- a/crates/eucalyptus-editor/src/main.rs
+++ b/crates/eucalyptus-editor/src/main.rs
@@ -101,7 +101,7 @@ async fn main() -> anyhow::Result<()> {
             )
             .filter(Some("eucalyptus_core"), LevelFilter::Debug)
             .filter(Some("dropbear_traits"), LevelFilter::Debug)
-            .filter(Some("kino_ui"), LevelFilter::Trace)
+            .filter(Some("kino_ui"), LevelFilter::Debug)
             .init();
         log::info!("Initialised logger");
     }
diff --git a/crates/kino-ui/Cargo.toml b/crates/kino-ui/Cargo.toml
index 5bf8cd9..b59a6ce 100644
--- a/crates/kino-ui/Cargo.toml
+++ b/crates/kino-ui/Cargo.toml
@@ -15,3 +15,6 @@ bytemuck.workspace = true
 image.workspace = true
 anyhow.workspace = true
 log.workspace = true
+winit.workspace = true
+
+glyphon.workspace = true
diff --git a/crates/kino-ui/README.md b/crates/kino-ui/README.md
new file mode 100644
index 0000000..171d1c1
--- /dev/null
+++ b/crates/kino-ui/README.md
@@ -0,0 +1,45 @@
+# kino-ui
+
+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.  
+
+# Example
+
+```rust
+pub fn init() {
+    let renderer = KinoWGPURenderer::new(/*yadda yadda doo*/);
+    let windowing = KinoWinitWindowing::new(window.clone());
+    
+    // keep this with you this is important. the rest are owned by KinoState
+    let kino = KinoState::new(renderer, windowing);
+}
+
+pub fn update(kino: &mut KinoState) {
+    // creating new widget
+    let new_rect = kino.add_widget(Box::new(
+        Rectangle::new("red patch".into())
+            .with(&Rect::new([50.0, 100.0].into(), [128.0, 100.0].into()))
+            .colour([255.0, 0.0, 0.0, 255.0]).into()
+    ));
+
+    // polling to build the widget tree
+    kino.poll();
+
+    // checking input response
+    if kino.response(new_rect).clicked {
+        println!("I have been clicked!");
+    }
+}
+
+pub fn render(kino: &mut KinoState) {
+    // ...
+    
+    // generally the last thing to render on your viewport if you want
+    // an overlay
+    kino.render(/**/);
+    
+    // ...
+}
+```
\ No newline at end of file
diff --git a/crates/kino-ui/src/asset.rs b/crates/kino-ui/src/asset.rs
index c92ee70..a0f0ace 100644
--- a/crates/kino-ui/src/asset.rs
+++ b/crates/kino-ui/src/asset.rs
@@ -1,14 +1,24 @@
 use std::collections::HashMap;
+use std::hash::{Hash, Hasher};
+use std::collections::hash_map::DefaultHasher;
 use std::marker::PhantomData;
 use crate::rendering::texture::Texture;
 
 /// A handle with type [`T`] that provides an index to the [AssetServer] contents.
-#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
+#[derive(Hash, Eq, PartialEq, Debug)]
 pub struct Handle<T> {
     pub id: u64,
     _phanton: PhantomData<T>
 }
 
+impl<T> Copy for Handle<T> {}
+
+impl<T> Clone for Handle<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
 impl<T> Handle<T> {
     pub fn new(id: u64) -> Self {
         Self { id, _phanton: Default::default() }
@@ -19,6 +29,7 @@ impl<T> Handle<T> {
 #[derive(Default)]
 pub struct AssetServer {
     textures: HashMap<u64, Texture>,
+    texture_labels: HashMap<String, Handle<Texture>>,
 }
 
 impl AssetServer {
@@ -26,6 +37,7 @@ impl AssetServer {
     pub fn new() -> AssetServer {
         AssetServer {
             textures: Default::default(),
+            texture_labels: Default::default(),
         }
     }
 
@@ -35,17 +47,50 @@ impl AssetServer {
     /// you can use [`Texture::from_bytes`].
     pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> {
         let handle = Handle::new(texture.hash);
-        self.textures.insert(handle.id, texture);
+        self.textures.entry(handle.id).or_insert(texture);
         handle
     }
 
+    /// Adds a texture with a label. If the texture already exists (by hash),
+    /// returns the existing handle and updates the label to point at it.
+    pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> {
+        let handle = self.add_texture(texture);
+        self.texture_labels.insert(label.into(), handle.clone());
+        handle
+    }
+
+    /// Maps a label to an existing texture handle.
+    pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) {
+        self.texture_labels.insert(label.into(), handle.clone());
+    }
+
     /// Updates the asset server by inserting the texture provided at the location of the handle,
     /// and removing the old texture (by returning it back to you).
     pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> {
         self.textures.insert(handle.id, texture)
     }
 
-    pub fn get_texture(&mut self, handle: Handle<Texture>) -> Option<&Texture> {
+    pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> {
         self.textures.get(&handle.id)
     }
+
+    pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> {
+        self.texture_labels
+            .get(label)
+            .and_then(|handle| self.textures.get(&handle.id))
+    }
+
+    pub fn get_texture_handle(&self, label: &str) -> Option<Handle<Texture>> {
+        self.texture_labels.get(label).cloned()
+    }
+
+    pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> {
+        self.textures.contains_key(&hash).then(|| Handle::new(hash))
+    }
+
+    pub fn hash_bytes(data: &[u8]) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        data.hash(&mut hasher);
+        hasher.finish()
+    }
 }
\ No newline at end of file
diff --git a/crates/kino-ui/src/camera.rs b/crates/kino-ui/src/camera.rs
index 9c6d083..f1b9753 100644
--- a/crates/kino-ui/src/camera.rs
+++ b/crates/kino-ui/src/camera.rs
@@ -18,7 +18,7 @@ impl Default for Camera2D {
 
 impl Camera2D {
     /// Returns the orthographic view-projection matrix for the current camera state
-    pub(crate) fn view_proj(&self, screen_size: Vec2) -> Mat4 {
+    pub fn view_proj(&self, screen_size: Vec2) -> Mat4 {
         let width = screen_size.x / self.zoom;
         let height = screen_size.y / self.zoom;
 
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
index 5f2d531..8354703 100644
--- a/crates/kino-ui/src/lib.rs
+++ b/crates/kino-ui/src/lib.rs
@@ -1,6 +1,4 @@
-//! kino-ui, a UI library with instruction set UI rendering.
-//!
-//! Uses wgpu for rendering and winit for input management.
+#![doc = include_str!("../README.md")]
 
 pub mod resp;
 pub mod widgets;
@@ -8,23 +6,29 @@ pub mod rendering;
 pub mod camera;
 pub mod asset;
 pub mod math;
+pub mod windowing;
 
 use crate::asset::{AssetServer, Handle};
 use crate::camera::Camera2D;
 use crate::rendering::texture::Texture;
 use crate::rendering::vertex::Vertex;
-use crate::rendering::{KinoWGPURenderer, VertexBatch};
+use crate::rendering::{KinoWGPURenderer};
 use crate::resp::WidgetResponse;
 use crate::widgets::{ContaineredWidget, NativeWidget};
+use std::borrow::Cow;
 use std::collections::{HashMap, VecDeque};
 use std::fmt::Debug;
 use std::hash::{DefaultHasher, Hash, Hasher};
 use wgpu::{LoadOp, StoreOp};
+use rendering::batching::VertexBatch;
+use crate::windowing::KinoWinitWindowing;
+use glam::Vec2;
 
 /// Holds the state of all the instructions, and the vertices+indices for rendering as well
 /// as the responses.
 pub struct KinoState {
     renderer: KinoWGPURenderer,
+    windowing: KinoWinitWindowing,
     instruction_set: VecDeque<UiInstructionType>,
     widget_states: HashMap<WidgetId, WidgetResponse>,
     assets: AssetServer,
@@ -36,10 +40,11 @@ impl KinoState {
     /// Creates a new instance of a [KinoState].
     ///
     /// This sits inside your `init()` function.
-    pub fn new(renderer: KinoWGPURenderer) -> KinoState {
+    pub fn new(renderer: KinoWGPURenderer, windowing: KinoWinitWindowing) -> Self {
         log::debug!("Created KinoState");
         KinoState {
             renderer,
+            windowing,
             instruction_set: Default::default(),
             widget_states: Default::default(),
             assets: Default::default(),
@@ -48,11 +53,19 @@ impl KinoState {
         }
     }
 
-    pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) {
+    /// Adds a widget (a [`NativeWidget`]) to the instruction set as a
+    /// [`UiInstructionType::Widget`] and returns back the associated [`WidgetId`] for response
+    /// checking.
+    pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) -> WidgetId {
+        let id = widget.id();
         self.instruction_set.push_back(UiInstructionType::Widget(widget));
+        id
     }
 
-    pub fn add_container(&mut self, _container: Box<dyn ContaineredWidget>) {
+    /// 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 {
@@ -62,11 +75,17 @@ impl KinoState {
         // ))
     }
 
+    /// Adds a [UiInstructionType] to the instruction set.
     pub fn add_instruction(&mut self, ui_instruction_type: UiInstructionType) {
         self.instruction_set.push_back(ui_instruction_type);
     }
 
-    /// Polls for changes, builds the tree and prepares them for rendering.
+    /// Polls for changes by clearing the current instruction set, build the tree and
+    /// preparing them for rendering.
+    ///
+    /// If you create a widget and then check for a response before polling, you will not receive
+    /// back a response. You are required to poll/prepare the contents before being given access
+    /// to the response information.
     ///
     /// This sits inside your `update()` loop.
     pub fn poll(&mut self) {
@@ -82,8 +101,23 @@ impl KinoState {
         self.render_tree(tree);
     }
 
+    /// Fetches the [`WidgetResponse`] from an associated [`WidgetId`].
+    ///
+    /// 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 set_viewport_offset(&mut self, offset: Vec2) {
+        self.windowing.viewport_offset = offset;
+    }
+
     /// Pushes the vertices and indices to the renderer.
     ///
+    /// This is the recommended `render()` function and is used when you want
+    /// `kino_ui` to create the render pass and submit to the queue.
+    ///
     /// This sits inside your `render()` loop.
     pub fn render(
         &mut self,
@@ -119,13 +153,130 @@ impl KinoState {
             });
 
             for mut tg in batch {
-                log::debug!("Rendering textured geometry: {:?}", tg);
+                // log::debug!("Rendering textured geometry: {:?}", tg);
                 let texture = tg.texture_id.and_then(|v| {
                     self.assets.get_texture(v)
                 });
                 self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture);
             }
+
+            // self.renderer.text.render(&mut pass);
+        }
+    }
+
+    /// Pushes the vertices and indices to the renderer.
+    ///
+    /// This is used when you want control on the [`wgpu::RenderPass`] and you want
+    /// `kino_ui` to only draw the widgets.
+    ///
+    /// This sits inside your `render()` loop.
+    pub fn render_into_pass(
+        &mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        pass: &mut wgpu::RenderPass<'_>,
+    ) {
+        self.renderer.upload_camera_matrix(
+            queue,
+            self.camera
+                .view_proj(self.renderer.size)
+                .to_cols_array_2d(),
+        );
+        let batch = self.batch.take();
+
+        for mut tg in batch {
+            // log::debug!("Rendering textured geometry: {:?}", tg);
+            let texture = tg.texture_id.and_then(|v| {
+                self.assets.get_texture(v)
+            });
+            self.renderer.draw_batch(pass, device, queue, &mut tg.batch, texture);
+        }
+
+        // self.renderer.text.render(&mut pass);
+    }
+
+    /// Handles the event into the internal input state.
+    ///
+    /// This is not required, however if you want reactivity, include it into your WindowEvent code.
+    pub fn handle_event(&mut self, event: &winit::event::WindowEvent) {
+        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
+    /// and just updates the label mapping.
+    pub fn add_texture_from_bytes(
+        &mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        label: impl Into<String>,
+        data: &[u8],
+        width: u32,
+        height: u32,
+    ) -> Handle<Texture> {
+        let mut raw: Cow<[u8]> = Cow::Borrowed(data);
+        let mut width = width;
+        let mut height = height;
+        let expected_len = (width as usize)
+            .saturating_mul(height as usize)
+            .saturating_mul(4);
+
+        if data.len() != expected_len {
+            match image::load_from_memory(data) {
+                Ok(img) => {
+                    let rgba = img.to_rgba8();
+                    let (w, h) = rgba.dimensions();
+                    width = w;
+                    height = h;
+                    raw = Cow::Owned(rgba.into_raw());
+                }
+                Err(e) => {
+                    log::error!("Failed to decode texture bytes: {}", e);
+                    let fallback = Texture::create_default(
+                        device,
+                        queue,
+                        self.renderer.texture_bind_group_layout(),
+                    );
+                    return self.assets.add_texture_with_label(label, fallback);
+                }
+            }
         }
+
+        let hash = AssetServer::hash_bytes(raw.as_ref());
+        if let Some(handle) = self.assets.texture_handle_by_hash(hash) {
+            self.assets.label_texture(label, handle.clone());
+            return handle;
+        }
+
+        let texture = Texture::from_bytes(
+            device,
+            queue,
+            self.renderer.texture_bind_group_layout(),
+            raw.as_ref(),
+            width,
+            height,
+        );
+        self.assets.add_texture_with_label(label, texture)
+    }
+
+    /// Fetch a texture handle by label.
+    pub fn texture_handle(&self, label: &str) -> Option<Handle<Texture>> {
+        self.assets.get_texture_handle(label)
+    }
+
+    pub(crate) fn input(&self) -> &KinoWinitWindowing {
+        &self.windowing
+    }
+
+    pub(crate) fn set_response(&mut self, id: WidgetId, response: WidgetResponse) {
+        self.widget_states.insert(id, response);
     }
 
     fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> {
@@ -195,9 +346,16 @@ impl KinoState {
     }
 }
 
+/// The id of the widget, often being a hash.
 #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
 pub struct WidgetId(pub u64);
 
+impl WidgetId {
+    pub fn from_str(str: &str) -> Self {
+        str.into()
+    }
+}
+
 impl Into<WidgetId> for &str {
     fn into(self) -> WidgetId {
         let mut hasher = DefaultHasher::new();
diff --git a/crates/kino-ui/src/math.rs b/crates/kino-ui/src/math.rs
index 83d50b2..1f8561a 100644
--- a/crates/kino-ui/src/math.rs
+++ b/crates/kino-ui/src/math.rs
@@ -1,5 +1,6 @@
 use glam::{vec2, Vec2};
 
+/// A rectangular shape consisting of a `position` and a `size`. 
 #[derive(Copy, Clone, PartialEq, Debug)]
 pub struct Rect {
     pub position: Vec2,
@@ -27,7 +28,7 @@ impl Rect {
         self.position + self.size * 0.5
     }
 
-    // Move the rectangle by the given delta vector
+    /// Move the rectangle by the given delta vector
     pub fn translate(&mut self, delta: Vec2) {
         self.position += delta;
     }
diff --git a/crates/kino-ui/src/rendering.rs b/crates/kino-ui/src/rendering.rs
index 9c799de..156fc72 100644
--- a/crates/kino-ui/src/rendering.rs
+++ b/crates/kino-ui/src/rendering.rs
@@ -1,18 +1,21 @@
 use glam::Vec2;
-use wgpu::{BufferUsages, IndexFormat};
 use wgpu::util::{BufferInitDescriptor, DeviceExt};
+use batching::VertexBatch;
 use crate::camera::{CameraRendering, CameraUniform};
 use crate::rendering::pipeline::KinoRendererPipeline;
-use crate::rendering::vertex::Vertex;
+// use crate::rendering::text::KinoTextRenderer;
 
 pub mod pipeline;
 pub mod texture;
 pub mod vertex;
+pub mod batching;
+// pub mod text;
 
 pub struct KinoWGPURenderer {
     pipeline: KinoRendererPipeline,
     default_texture: texture::Texture,
     pub size: Vec2,
+    // pub text: KinoTextRenderer,
 
     camera: CameraRendering,
 }
@@ -51,11 +54,14 @@ impl KinoWGPURenderer {
         });
 
         let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout);
+        // let text = KinoTextRenderer::new(&device, &queue, surface_format);
+
         log::debug!("Created KinoWGPURenderer");
         Self {
             pipeline,
             default_texture,
             size: Vec2::from_array(size),
+            // text,
             camera: CameraRendering {
                 buffer: camera_buffer,
                 bind_group: camera_bind_group,
@@ -94,122 +100,9 @@ impl KinoWGPURenderer {
         batch.draw(r_pass);
         batch.clear();
     }
-}
 
-/// Describes a primitive shape.
-#[derive(Debug)]
-pub struct VertexBatch {
-    vertices: Vec<Vertex>,
-    indices: Vec<u16>,
-    vertex_buffer: Option<wgpu::Buffer>,
-    index_buffer: Option<wgpu::Buffer>,
-    vertices_dirty: bool,
-    indices_dirty: bool,
-}
-
-impl Default for VertexBatch {
-    fn default() -> Self {
-        Self {
-            vertices: Vec::with_capacity(Self::MAX_VERTICES),
-            indices: Vec::with_capacity(Self::MAX_INDICES),
-            vertex_buffer: None,
-            index_buffer: None,
-            vertices_dirty: false,
-            indices_dirty: false,
-        }
+    pub(crate) fn texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
+        &self.pipeline.texture_bind_group_layout
     }
 }
 
-impl VertexBatch {
-    const MAX_VERTICES: usize = u16::MAX as usize;
-    const MAX_INDICES: usize = Self::MAX_VERTICES * 6;
-
-    /// Returns true if adding verts/indices would exceed max allowed
-    fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool {
-        self.vertices.len() + vert_count > Self::MAX_VERTICES
-            || self.indices.len() + idx_count > Self::MAX_INDICES
-    }
-
-    /// Adds vertices/indices, returns false if it would overflow
-    pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool {
-        if self.would_overflow(verts.len(), indices.len()) {
-            return false;
-        }
-
-        let idx_offset = self.vertices.len() as u16;
-        self.vertices.extend_from_slice(verts);
-        self.indices.extend(indices.iter().map(|i| *i + idx_offset));
-
-        self.vertices_dirty = true;
-        self.indices_dirty = true;
-
-        true
-    }
-
-    fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
-        if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) {
-            return;
-        }
-
-        if self.vertex_buffer.is_none() {
-            self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("kino vertex buffer"),
-                size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
-                usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
-                mapped_at_creation: false,
-            }));
-        }
-        if self.index_buffer.is_none() {
-            self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("kino index buffer"),
-                size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64,
-                usage: BufferUsages::INDEX | BufferUsages::COPY_DST,
-                mapped_at_creation: false,
-            }));
-        }
-
-        if self.vertices_dirty {
-            queue.write_buffer(
-                self.vertex_buffer.as_ref().unwrap(),
-                0,
-                bytemuck::cast_slice(&self.vertices),
-            );
-            self.vertices_dirty = false;
-        }
-        if self.indices_dirty {
-            let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec();
-            let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize;
-            if remainder != 0 {
-                let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder;
-                indices_bytes.extend_from_slice(&vec![0u8; pad_len]);
-            }
-
-            queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes);
-            self.indices_dirty = false;
-        }
-    }
-
-    fn is_empty(&self) -> bool {
-        self.vertices.is_empty() || self.indices.is_empty()
-    }
-
-    fn clear(&mut self) {
-        self.vertices.clear();
-        self.indices.clear();
-        self.vertices_dirty = true;
-        self.indices_dirty = true;
-    }
-
-    fn draw(&self, pass: &mut wgpu::RenderPass) {
-        if self.is_empty() {
-            return;
-        }
-
-        pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
-        pass.set_index_buffer(
-            self.index_buffer.as_ref().unwrap().slice(..),
-            IndexFormat::Uint16,
-        );
-        pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
-    }
-}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering/batching.rs b/crates/kino-ui/src/rendering/batching.rs
new file mode 100644
index 0000000..9abca52
--- /dev/null
+++ b/crates/kino-ui/src/rendering/batching.rs
@@ -0,0 +1,120 @@
+use wgpu::{BufferUsages, IndexFormat};
+use crate::rendering::vertex::Vertex;
+
+/// Describes a primitive shape.
+#[derive(Debug)]
+pub struct VertexBatch {
+    vertices: Vec<Vertex>,
+    indices: Vec<u16>,
+    vertex_buffer: Option<wgpu::Buffer>,
+    index_buffer: Option<wgpu::Buffer>,
+    vertices_dirty: bool,
+    indices_dirty: bool,
+}
+
+impl Default for VertexBatch {
+    fn default() -> Self {
+        Self {
+            vertices: Vec::with_capacity(Self::MAX_VERTICES),
+            indices: Vec::with_capacity(Self::MAX_INDICES),
+            vertex_buffer: None,
+            index_buffer: None,
+            vertices_dirty: false,
+            indices_dirty: false,
+        }
+    }
+}
+
+impl VertexBatch {
+    const MAX_VERTICES: usize = u16::MAX as usize;
+    const MAX_INDICES: usize = Self::MAX_VERTICES * 6;
+
+    /// Returns true if adding verts/indices would exceed max allowed
+    fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool {
+        self.vertices.len() + vert_count > Self::MAX_VERTICES
+            || self.indices.len() + idx_count > Self::MAX_INDICES
+    }
+
+    /// Adds vertices/indices, returns false if it would overflow
+    pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool {
+        if self.would_overflow(verts.len(), indices.len()) {
+            return false;
+        }
+
+        let idx_offset = self.vertices.len() as u16;
+        self.vertices.extend_from_slice(verts);
+        self.indices.extend(indices.iter().map(|i| *i + idx_offset));
+
+        self.vertices_dirty = true;
+        self.indices_dirty = true;
+
+        true
+    }
+
+    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
+        if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) {
+            return;
+        }
+
+        if self.vertex_buffer.is_none() {
+            self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("kino vertex buffer"),
+                size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
+                usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            }));
+        }
+        if self.index_buffer.is_none() {
+            self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some("kino index buffer"),
+                size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64,
+                usage: BufferUsages::INDEX | BufferUsages::COPY_DST,
+                mapped_at_creation: false,
+            }));
+        }
+
+        if self.vertices_dirty {
+            queue.write_buffer(
+                self.vertex_buffer.as_ref().unwrap(),
+                0,
+                bytemuck::cast_slice(&self.vertices),
+            );
+            self.vertices_dirty = false;
+        }
+        if self.indices_dirty {
+            let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec();
+            let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize;
+            if remainder != 0 {
+                let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder;
+                indices_bytes.extend_from_slice(&vec![0u8; pad_len]);
+            }
+
+            queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes);
+            self.indices_dirty = false;
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.vertices.is_empty() || self.indices.is_empty()
+    }
+
+    pub fn clear(&mut self) {
+        self.vertices.clear();
+        self.indices.clear();
+        self.vertices_dirty = true;
+        self.indices_dirty = true;
+    }
+
+    pub fn draw(&self, pass: &mut wgpu::RenderPass) {
+        if self.is_empty() {
+            return;
+        }
+
+        pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
+        pass.set_index_buffer(
+            self.index_buffer.as_ref().unwrap().slice(..),
+            IndexFormat::Uint16,
+        );
+        pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/rendering/text.rs b/crates/kino-ui/src/rendering/text.rs
new file mode 100644
index 0000000..384c08a
--- /dev/null
+++ b/crates/kino-ui/src/rendering/text.rs
@@ -0,0 +1,97 @@
+use glam::Vec2;
+use glyphon::{Buffer, Cache, Color, FontSystem, Resolution, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport};
+
+pub struct TextEntry {
+    pub buffer: Buffer,
+    pub position: Vec2,
+}
+
+pub struct KinoTextRenderer {
+    pub font_system:
+        FontSystem,
+    pub swash_cache: SwashCache,
+    pub atlas: TextAtlas,
+    pub renderer: TextRenderer,
+    pub viewport: Viewport,
+    pub entries: Vec<TextEntry>,
+}
+
+impl KinoTextRenderer {
+    pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
+        log::debug!("Creating KinoTextRenderer");
+        log::debug!("Loading system fonts");
+        let mut font_system = FontSystem::new();
+        font_system
+            .db_mut()
+            .load_system_fonts();
+        log::debug!("Loaded system fonts");
+
+        log::debug!("Loading \"Roboto-Regular.ttf\" as fallback");
+        font_system.db_mut().load_font_data(include_bytes!("../../../../resources/fonts/Roboto-Regular.ttf").to_vec());
+        log::debug!("Loaded fallback");
+        let swash_cache = SwashCache::new();
+        let cache = Cache::new(device);
+        let viewport = Viewport::new(device, &cache);
+        let mut atlas = TextAtlas::new(device, queue, &cache, format);
+        let renderer = TextRenderer::new(&mut atlas, device, Default::default(), None);
+
+        log::debug!("Created new KinoTextRenderer");
+
+        Self {
+            font_system,
+            swash_cache,
+            atlas,
+            renderer,
+            viewport,
+            entries: Vec::new(),
+        }
+    }
+
+    /// Prepare the text renderer for drawing
+    pub(crate) fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) {
+        if self.entries.is_empty() {
+            return;
+        }
+
+        let text_areas: Vec<TextArea> = self
+            .entries
+            .iter()
+            .map(|entry| TextArea {
+                buffer: &entry.buffer,
+                left: entry.position.x,
+                top: entry.position.y,
+                bounds: TextBounds {
+                    right: width as i32,
+                    bottom: height as i32,
+                    ..Default::default()
+                },
+                scale: 1.0,
+                default_color: Color::rgb(255, 255, 255),
+                custom_glyphs: &[],
+            })
+            .collect();
+        self.renderer
+            .prepare(
+                device,
+                queue,
+                &mut self.font_system,
+                &mut self.atlas,
+                &self.viewport,
+                text_areas,
+                &mut self.swash_cache,
+            )
+            .unwrap();
+
+        self.entries.clear();
+    }
+
+    pub(crate) 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) {
+        self.viewport.update(queue, Resolution { width, height });
+    }
+}
diff --git a/crates/kino-ui/src/resp.rs b/crates/kino-ui/src/resp.rs
index c136d3e..4e46ea3 100644
--- a/crates/kino-ui/src/resp.rs
+++ b/crates/kino-ui/src/resp.rs
@@ -2,5 +2,4 @@
 pub struct WidgetResponse {
     pub clicked: bool,
     pub hovering: bool,
-    pub checked: 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 ec5ccce..b63ccfa 100644
--- a/crates/kino-ui/src/widgets/mod.rs
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -1,4 +1,5 @@
 pub mod rect;
+// pub mod text;
 
 use std::any::Any;
 use crate::{KinoState, UiNode, WidgetId};
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
index 062eebe..bd7736a 100644
--- a/crates/kino-ui/src/widgets/rect.rs
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -6,6 +6,8 @@ use crate::math::Rect;
 use crate::rendering::texture::Texture;
 use crate::rendering::vertex::Vertex;
 use crate::widgets::{Anchor, NativeWidget};
+use crate::resp::WidgetResponse;
+use winit::event::{ElementState, MouseButton};
 
 pub struct Rectangle {
     /// The identifier of the widget.
@@ -44,7 +46,7 @@ impl Rectangle {
             position: Vec2::ZERO,
             size: vec2(64.0, 64.0),
             rotation: 0.0,
-            colour: [255.0, 255.0, 255.0, 255.0],
+            colour: [1.0, 1.0, 1.0, 1.0],
             uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
             texture: None,
         }
@@ -72,8 +74,8 @@ impl Rectangle {
         self.size = size;
         self
     }
-    /// Sets the color of the rectangle
-    pub fn color(mut self, colour: [f32; 4]) -> Self {
+    /// Sets the colour of the rectangle
+    pub fn colour(mut self, colour: [f32; 4]) -> Self {
         self.colour = colour;
         self
     }
@@ -104,6 +106,18 @@ impl NativeWidget for Rectangle {
         };
         let top_left = self.position + offset;
         let rect = Rect::new(top_left, self.size);
+        let input = state.input();
+        let hovering = rect.contains(input.mouse_position);
+        let clicked = hovering
+            && input.mouse_button == MouseButton::Left
+            && input.mouse_press_state == ElementState::Pressed;
+        state.set_response(
+            self.id,
+            WidgetResponse {
+                clicked,
+                hovering,
+            },
+        );
         let rot = Mat2::from_angle(self.rotation);
         let verts: Vec<_> = rect
             .corners()
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
new file mode 100644
index 0000000..637efbd
--- /dev/null
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -0,0 +1,74 @@
+use std::any::Any;
+use glam::Vec2;
+use glyphon::{Attrs, AttrsOwned, Buffer, Color, Metrics, Shaping};
+use crate::{KinoState, WidgetId};
+use crate::rendering::text::TextEntry;
+use crate::widgets::NativeWidget;
+
+pub struct Text {
+    pub id: WidgetId,
+    pub text: String,
+    pub position: 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 {
+        Self {
+            id: WidgetId::from_str(&text),
+            text,
+            position: Vec2::new(10.0, 10.0),
+            metrics: Metrics::new(16.0, 1.0),
+            attributes: AttrsOwned::new(Attrs::new()),
+        }
+    }
+
+    /// Set a custom ID before sending the widget off
+    pub fn with_id(mut self, id: WidgetId) -> Self {
+        self.id = id;
+        self
+    }
+
+    /// Set the position of text in screen space
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn with_attrs(mut self, attributes: AttrsOwned) -> Self {
+        self.attributes = attributes;
+        self
+    }
+
+    pub fn with_metrics(mut self, metrics: Metrics) -> Self {
+        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);
+        buffer.set_text(
+            &mut state.renderer.text.font_system,
+            &self.text,
+            self.attributes.as_attrs(),
+            Shaping::Basic,
+        );
+
+        state.renderer.text.entries.push(TextEntry {
+            buffer,
+            position: self.position,
+        });
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-ui/src/windowing.rs b/crates/kino-ui/src/windowing.rs
new file mode 100644
index 0000000..bfbe72d
--- /dev/null
+++ b/crates/kino-ui/src/windowing.rs
@@ -0,0 +1,49 @@
+use std::sync::Arc;
+use glam::Vec2;
+use winit::event::{ElementState, MouseButton, WindowEvent};
+use winit::window::Window;
+
+pub struct KinoWinitWindowing {
+    // mouse
+    pub mouse_position: Vec2,
+    pub mouse_button: MouseButton,
+    pub mouse_press_state: ElementState,
+
+    // keyboard
+
+    // windowing
+    _window: Arc<Window>,
+    /// The top-left most pixel
+    pub viewport_offset: Vec2,
+}
+
+impl KinoWinitWindowing {
+    /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset.
+    pub fn new(window: Arc<Window>) -> Self {
+        Self {
+            mouse_position: Default::default(),
+            mouse_button: MouseButton::Left,
+            mouse_press_state: ElementState::Released,
+            _window: window,
+            viewport_offset: Default::default(),
+        }
+    }
+
+    pub(crate) fn handle_event(&mut self, event: &WindowEvent) {
+        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;
+            }
+            WindowEvent::MouseInput { state, button, .. } => {
+                self.mouse_button = *button;
+                self.mouse_press_state = *state;
+            }
+            WindowEvent::Resized(_) => {}
+            WindowEvent::KeyboardInput { .. } => {}
+            WindowEvent::MouseWheel { .. } => {}
+            WindowEvent::ScaleFactorChanged { .. } => {}
+            _ => {}
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 8c57aa0..70679c6 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -33,6 +33,7 @@ use eucalyptus_core::physics::PhysicsState;
 use eucalyptus_core::rapier3d::prelude::*;
 use eucalyptus_core::register_components;
 use kino_ui::KinoState;
+use kino_ui::windowing::KinoWinitWindowing;
 use kino_ui::rendering::KinoWGPURenderer;
 
 mod scene;
@@ -219,7 +220,18 @@ impl PlayMode {
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
         
-        self.kino = Some(KinoState::new(KinoWGPURenderer::new(&graphics.device, &graphics.queue, Texture::TEXTURE_FORMAT, [graphics.viewport_texture.size.width as f32, graphics.viewport_texture.size.height as f32])))
+        self.kino = Some(KinoState::new(
+            KinoWGPURenderer::new(
+                &graphics.device,
+                &graphics.queue,
+                Texture::TEXTURE_FORMAT,
+                [
+                    graphics.viewport_texture.size.width as f32,
+                    graphics.viewport_texture.size.height as f32,
+                ],
+            ),
+            KinoWinitWindowing::new(graphics.window.clone()),
+        ))
     }
 
     fn reload_scripts_for_current_world(&mut self) {
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 48e39d5..295ad58 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -190,11 +190,6 @@ impl Scene for PlayMode {
             }
         }
 
-        // Store previous transform states for interpolation
-        for transform in self.world.query::<&mut EntityTransform>().iter() {
-            transform.store_previous();
-        }
-
         let mut sync_updates = Vec::new();
 
         for (entity, label, _) in self.world.query::<(Entity, &Label, &EntityTransform)>().iter() {
@@ -456,7 +451,10 @@ impl Scene for PlayMode {
                 if !p.is_everything_loaded() && p.is_first_scene {
                     ui.centered_and_justified(|ui| {
                         egui_extras::install_image_loaders(&graphics.get_egui_context());
-                        ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png"))
+                        ui.add(
+                            egui::Image::new(egui::include_image!("../../../resources/eucalyptus-editor.png"))
+                                .max_width(128.0)
+                        )
                     });
                     return;
                 }
@@ -498,6 +496,9 @@ 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));
+                    }
 
                     ui.allocate_exact_size(available_size, egui::Sense::hover());
 
@@ -533,11 +534,22 @@ impl Scene for PlayMode {
                     });
 
                     if let Some(kino) = &mut self.kino {
-                        kino.add_widget(Box::new(
+                        let peter_griffen = kino.add_texture_from_bytes(&graphics.device, &graphics.queue, "theres nothing", include_bytes!("../../../resources/textures/no-texture.png"), 256, 256);
+
+                        let rect = kino.add_widget(Box::new(
                             Rectangle::new("rect".into())
+                                .texture(peter_griffen)
                         ));
 
                         kino.poll();
+
+                        if kino.response(rect).clicked {
+                            println!("Clicked!");
+                        };
+
+                        if kino.response(rect).hovering {
+                            println!("Hovering...");
+                        };
                     }
                 } else {
                     log::warn!("No such camera exists in the world");
@@ -605,7 +617,7 @@ impl Scene for PlayMode {
                 });
             }
 
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
         }
@@ -910,7 +922,7 @@ impl Scene for PlayMode {
                 }
             }
 
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("{}", e);
             }
 
@@ -935,7 +947,7 @@ impl Scene for PlayMode {
         if let Some(kino) = &mut self.kino {
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
             kino.render(&graphics.device, &graphics.queue, &mut encoder, &graphics.viewport_texture.view);
-            if let Err(e) = encoder.submit(graphics.clone()) {
+            if let Err(e) = encoder.submit() {
                 log_once::error_once!("Unable to submit kino: {}", e);
             }
         }
@@ -949,6 +961,10 @@ impl Scene for PlayMode {
                 yak.handle_window_event(&mut yakui, event);
             }
         });
+
+        if let Some(kino) = &mut self.kino {
+            kino.handle_event(event);
+        }
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}