kitgit

tirbofish/dropbear · commit

09fa5f30eaf12226a7f845c2693f52d179518a70

wip: new library called kino-gui doesnt work (yet), thats tomorrow's problem. Unverified

Thribhu K <4tkbytes@pm.me> · 2026-01-21 16:46

view full diff

diff --git a/.cargo/config.toml b/.cargo/config.toml
index 5c0c649..d2312c6 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,3 @@
 [target.x86_64-unknown-linux-gnu]
 linker = "clang"
-rustflags = ["-C", "link-arg=-fuse-ld=mold"]
\ No newline at end of file
+rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"]
diff --git a/README.md b/README.md
index 581fb3d..212f748 100644
--- a/README.md
+++ b/README.md
@@ -11,20 +11,19 @@ If you might have not realised, all the crates/projects names are after Australi
 
 ## Projects
 
-- [dropbear-engine](https://github.com/tirbofish/dropbear/tree/main/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
-- [dropbear-shader](https://github.com/tirbofish/dropbear/tree/main/dropbear-shader) contains WESL shaders for users to import
-- [eucalyptus-editor](https://github.com/tirbofish/dropbear/tree/main/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
-- [eucalyptus-core](https://github.com/tirbofish/dropbear/tree/main/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/redback-runtime) is the runtime used to load .eupak files and run the game loaded on them.
+- [dropbear-engine](https://github.com/tirbofish/dropbear/tree/main/crates/dropbear-engine) is the rendering engine that uses wgpu and the main name of the project.
+- [eucalyptus-editor](https://github.com/tirbofish/dropbear/tree/main/crates/eucalyptus-editor) is the visual editor used to create games visually, taking inspiration from Unity, Unreal, Roblox Studio and other engines.
+- [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.
+- [kino-gui](https://github.com/tirbofish/dropbear/tree/main/crates/kino-gui) is a wgpu primary UI renderer used for displaying shapes, text, and images in a simple immediate-mode way. 
 
 [//]: # (- [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`)
 
 ### Related Projects
 
-- [magna-carta](https://github.com/tirbofish/dropbear/tree/main/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching. 
-- [magna-carta-plugin](https://github.com/tirbofish/dropbear/tree/main/magna-carta-plugin) is a Gradle plugin for generating metadata during compile time with the help of the magna-carta cli tool. 
 - [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 rasteriser. _(very crude but usable)_
+- [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)_
 
 ## Build
 
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 2d3af5b..e1aa092 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -327,11 +327,14 @@ Hardware:
             ],
         });
 
+        let device = Arc::new(device);
+        let queue = Arc::new(queue);
+
         let result = Self {
             surface: Arc::new(surface),
             surface_format: Texture::TEXTURE_FORMAT,
-            device: Arc::new(device),
-            queue: Arc::new(queue),
+            device,
+            queue,
             config,
             is_surface_configured,
             depth_texture,
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index b45ebbb..86be779 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -13,6 +13,7 @@ crate-type = ["rlib", "dylib"]
 dropbear-traits = { path = "../dropbear-traits" }
 dropbear-macro = { path = "../dropbear-macro" }
 magna-carta = { path = "../magna-carta" }
+kino-gui = { path = "../kino-gui" }
 
 anyhow.workspace = true
 postcard.workspace = true
diff --git a/crates/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
index 9b01965..cf6d021 100644
--- a/crates/eucalyptus-core/src/ui.rs
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -1,88 +1,68 @@
 pub mod rect;
 
-use egui::{Rect, Response, Sense};
 use once_cell::sync::Lazy;
 use parking_lot::Mutex;
-use crate::utils::hashmap::StaleTracker;
+use kino_gui::prelude::*;
+use std::collections::HashMap;
 
 pub static UI_COMMAND_BUFFER: Lazy<UiContext> = Lazy::new(|| UiContext::new());
 
+#[derive(Clone, Copy, Debug)]
+pub struct UiResponse {
+    pub id: u64,
+    pub was_clicked: bool,
+}
+
+impl UiResponse {
+    pub fn clicked(&self) -> bool {
+        self.was_clicked
+    }
+}
+
 pub enum UiCommand {
     Rect {
         id: u64,
-        initial: (f32, f32),
-        size: (f32, f32),
+        initial: Vector2,
+        size: Size,
         corner_radius: f32,
-        stroke: egui::Stroke,
-        fill: egui::Color32,
-        stroke_kind: egui::StrokeKind,
+        stroke: f32,
+        fill: Colour,
+        stroke_kind: String,
     },
     Circle {
         id: u64,
-        center_x: f64,
-        center_y: f64,
-        radius: f64,
+        center: Vector2,
+        radius: f32,
+        fill: Colour,
+        stroke: f32,
     },
 }
 
 pub struct UiContext {
-    command_buffer: Mutex<Vec<UiCommand>>,
-    currently_rendering: Mutex<StaleTracker<u64, Response>>,
+    commands: Mutex<Vec<UiCommand>>,
+    pub currently_rendering: Mutex<HashMap<u64, UiResponse>>,
 }
 
 impl UiContext {
     pub fn new() -> Self {
         Self {
-            command_buffer: Mutex::new(Vec::new()),
-            currently_rendering: Mutex::new(StaleTracker::new()),
+            commands: Mutex::new(Vec::new()),
+            currently_rendering: Mutex::new(HashMap::new()),
         }
     }
 
     pub fn push(&self, command: UiCommand) {
-        self.command_buffer.lock().push(command);
+        self.commands.lock().push(command);
     }
-}
-
-pub fn poll(ui: &mut egui::Ui) -> anyhow::Result<()> {
-    let mut buffer = UI_COMMAND_BUFFER.command_buffer.lock();
-    let mut rendering = UI_COMMAND_BUFFER.currently_rendering.lock();
-    rendering.tick();
-    for cmd in buffer.drain(..) {
-        match cmd {
-            UiCommand::Rect {
-                id,
-                initial,
-                size,
-                corner_radius,
-                stroke,
-                fill,
-                stroke_kind
-            } => {
-                let (resp, painter) = ui.allocate_painter(size.into(), Sense::hover());
-
-                painter.rect(
-                    Rect {
-                        min: initial.into(),
-                        max: [initial.0 + size.0, initial.1 + size.1].into(),
-                    },
-                    corner_radius,
-                    fill,
-                    stroke,
-                    stroke_kind
-                );
-
-                rendering.insert(id, resp);
-            }
-            UiCommand::Circle { .. } => {
 
-            }
-        }
+    pub fn drain_commands(&self) -> Vec<UiCommand> {
+        self.commands.lock().drain(..).collect()
     }
 
-    // remove anything past 3 gen
-    rendering.remove_stale(3);
-
-    Ok(())
+    pub fn update_responses(&self, responses: HashMap<u64, UiResponse>) {
+        let mut current = self.currently_rendering.lock();
+        *current = responses;
+    }
 }
 
 pub mod jni {
@@ -91,10 +71,11 @@ pub mod jni {
     use jni::JNIEnv;
     use jni::sys::{jboolean, jlong};
     use jni::objects::JObject;
+    use kino_gui::prelude::shapes::Rectangle;
+    use kino_gui::Widget;
     use crate::{convert_ptr};
     use crate::scripting::jni::utils::FromJObject;
     use crate::ui::{UiCommand, UiContext};
-    use crate::ui::rect::Rect;
 
     #[unsafe(no_mangle)]
     pub extern "system" fn Java_com_dropbear_ui_UINative_pushRect(
@@ -105,22 +86,22 @@ pub mod jni {
     ) {
         let ui = convert_ptr!(ui_buffer_handle => UiContext);
 
-        let rect: Rect = match Rect::from_jobject(&mut env, &rect) {
+        let rect = match Rectangle::from_jobject(&mut env, &rect) {
             Ok(v) => v,
             Err(e) => {
-                let _ = env.throw_new("java/lang/RuntimeException", format!("Unable to convert Rectangle->Rect: {:?}", e));
+                let _ = env.throw_new("java/lang/RuntimeException", format!("Unable to convert scripting::Rectangle->kino::Rectangle: {:?}", e));
                 return;
             }
         };
 
         ui.push(UiCommand::Rect {
-            id: rect.id,
-            initial: rect.initial_pos,
+            id: rect.id().as_u64(),
+            initial: rect.initial,
             size: rect.size,
-            corner_radius: rect.corner_radius,
-            stroke: rect.stroke,
-            fill: rect.fill,
-            stroke_kind: rect.stroke_kind,
+            corner_radius: 0.0, // rect.corner_radius when available
+            stroke: 0.0, // rect.stroke when available
+            fill: rect.fill_colour,
+            stroke_kind: "Middle".to_string(), // rect.stroke_kind when available
         });
     }
     
@@ -131,7 +112,7 @@ pub mod jni {
         ui_buffer_handle: jlong,
         circle: JObject,
     ) {
-        let ui = convert_ptr!(ui_buffer_handle => UiContext);
+        let _ui = convert_ptr!(ui_buffer_handle => UiContext);
 
         // Extract Circle fields
         let id_obj = match env
@@ -145,7 +126,7 @@ pub mod jni {
             }
         };
 
-        let id = match env.call_method(&id_obj, "getId", "()J", &[]).and_then(|v| v.j()) {
+        let _id = match env.call_method(&id_obj, "getId", "()J", &[]).and_then(|v| v.j()) {
             Ok(val) => val as u64,
             Err(e) => {
                 let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get id value: {}", e));
@@ -164,7 +145,7 @@ pub mod jni {
             }
         };
 
-        let center_x = match env.get_field(&center_obj, "x", "D").and_then(|v| v.d()) {
+        let _center_x = match env.get_field(&center_obj, "x", "D").and_then(|v| v.d()) {
             Ok(val) => val,
             Err(e) => {
                 let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get center.x: {}", e));
@@ -172,7 +153,7 @@ pub mod jni {
             }
         };
 
-        let center_y = match env.get_field(&center_obj, "y", "D").and_then(|v| v.d()) {
+        let _center_y = match env.get_field(&center_obj, "y", "D").and_then(|v| v.d()) {
             Ok(val) => val,
             Err(e) => {
                 let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get center.y: {}", e));
@@ -180,7 +161,7 @@ pub mod jni {
             }
         };
 
-        let radius = match env.get_field(&circle, "radius", "D").and_then(|v| v.d()) {
+        let _radius = match env.get_field(&circle, "radius", "D").and_then(|v| v.d()) {
             Ok(val) => val,
             Err(e) => {
                 let _ = env.throw_new("java/lang/RuntimeException", format!("Failed to get radius: {}", e));
@@ -188,12 +169,7 @@ pub mod jni {
             }
         };
 
-        ui.push(UiCommand::Circle {
-            id,
-            center_x,
-            center_y,
-            radius,
-        });
+        panic!("this is not implemented yet :(")
     }
 
     #[unsafe(no_mangle)]
diff --git a/crates/eucalyptus-core/src/ui/rect.rs b/crates/eucalyptus-core/src/ui/rect.rs
index d31c48c..d9eaeea 100644
--- a/crates/eucalyptus-core/src/ui/rect.rs
+++ b/crates/eucalyptus-core/src/ui/rect.rs
@@ -1,22 +1,13 @@
-use egui::{Stroke, StrokeKind};
 use jni::JNIEnv;
 use jni::objects::JObject;
 use crate::scripting::jni::utils::{FromJObject, ToJObject};
 use crate::scripting::result::DropbearNativeResult;
 use crate::scripting::native::DropbearNativeError;
+use kino_gui::prelude::*;
+use kino_gui::prelude::shapes::Rectangle;
+use kino_gui::WidgetId;
 
-/// Maps directly to a `com.dropbear.ui.primitive.Rectangle` Kotlin class
-pub struct Rect {
-    pub id: u64,
-    pub initial_pos: (f32, f32),
-    pub size: (f32, f32),
-    pub corner_radius: f32,
-    pub stroke: Stroke,
-    pub fill: egui::Color32,
-    pub stroke_kind: StrokeKind,
-}
-
-impl FromJObject for Rect {
+impl FromJObject for Rectangle {
     fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self>
     where
         Self: Sized
@@ -122,8 +113,14 @@ impl FromJObject for Rect {
             .b()
             .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;
 
-        let fill = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
-
+        // let fill = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
+        let fill = Colour {
+            r: r as f32 / 255.0,
+            g: g as f32 / 255.0,
+            b: b as f32 / 255.0,
+            a: a as f32 / 255.0,
+        };
+        
         // Get stroke colour
         let stroke_colour_obj = env
             .get_field(obj, "strokeColour", "Lcom/dropbear/utils/Colour;")
@@ -156,7 +153,7 @@ impl FromJObject for Rect {
             .map_err(|_| DropbearNativeError::JNIUnwrapFailed)? as u8;
 
         let stroke_color = egui::Color32::from_rgba_unmultiplied(stroke_r, stroke_g, stroke_b, stroke_a);
-        let stroke = Stroke::new(stroke_width, stroke_color);
+        // let stroke = Stroke::new(stroke_width, stroke_color);
 
         // Get stroke kind (enum)
         let stroke_kind_obj = env
@@ -176,26 +173,25 @@ impl FromJObject for Rect {
             .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?
             .into();
 
-        let stroke_kind = match stroke_kind_str.as_str() {
-            "Inside" => StrokeKind::Inside,
-            "Middle" => StrokeKind::Middle,
-            "Outside" => StrokeKind::Outside,
-            _ => StrokeKind::Middle, // default
-        };
-
-        Ok(Rect {
-            id,
-            initial_pos: (initial_x, initial_y),
-            size: (width, height),
-            corner_radius,
-            stroke,
-            fill,
-            stroke_kind,
-        })
+        // let stroke_kind = match stroke_kind_str.as_str() {
+        //     "Inside" => StrokeKind::Inside,
+        //     "Middle" => StrokeKind::Middle,
+        //     "Outside" => StrokeKind::Outside,
+        //     _ => StrokeKind::Middle, // default
+        // };
+
+        let rect = Rectangle::new(
+            WidgetId::new(id),
+            [initial_x, initial_y].into(),
+            [width, height].into(),
+            fill
+        );
+        
+        Ok(rect)
     }
 }
 
-impl ToJObject for Rect {
+impl ToJObject for Rectangle {
     fn to_jobject<'a>(&self, _env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {
         todo!()
     }
diff --git a/crates/kino-gui/Cargo.toml b/crates/kino-gui/Cargo.toml
new file mode 100644
index 0000000..d5ef34c
--- /dev/null
+++ b/crates/kino-gui/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "kino-gui"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme.workspace = true
+
+[dependencies]
+wgpu.workspace = true
+anyhow.workspace = true
+bytemuck.workspace = true
+parking_lot.workspace = true
diff --git a/crates/kino-gui/README.md b/crates/kino-gui/README.md
new file mode 100644
index 0000000..0eed566
--- /dev/null
+++ b/crates/kino-gui/README.md
@@ -0,0 +1,9 @@
+# kino-gui
+
+_kino-gui_ is a 2D UI immediate mode library that is wgpu native and game design oriented. Named after the sap that comes out of a 
+eucalyptus tree, this library can be used to create 2D designs, create HUDs for a 3D map or create menus for 2D contexts. 
+
+This library is simple to use, and designs itself to be "injected" as an overlay with your surface. 
+
+## Note
+It is a heavy WIP, and there are many features still not done yet. 
\ No newline at end of file
diff --git a/crates/kino-gui/src/lib.rs b/crates/kino-gui/src/lib.rs
new file mode 100644
index 0000000..4714806
--- /dev/null
+++ b/crates/kino-gui/src/lib.rs
@@ -0,0 +1,77 @@
+use std::hash::{DefaultHasher, Hash, Hasher};
+use parking_lot::Mutex;
+use crate::math::Size;
+use crate::rendering::KinoRenderer;
+
+pub mod rendering;
+pub(crate) mod math;
+pub(crate) mod primitives;
+pub(crate) mod utils;
+
+pub mod prelude {
+    pub use crate::{
+        rendering::KinoRenderer,
+        math::*,
+        primitives::*,
+        WidgetId,
+        Widget,
+    };
+}
+
+pub struct GumContext {
+    screen_size: Size,
+}
+
+impl GumContext {
+    pub fn new() -> Self {
+        Self { screen_size: Size::default() }
+    }
+}
+
+pub struct KinoUICommandBuffer {
+    contents: Mutex<Vec<Box<dyn Widget>>>,
+}
+
+impl KinoUICommandBuffer {
+    pub(crate) fn new() -> Self {
+        Self {
+            contents: Mutex::new(Vec::new()),
+        }
+    }
+
+    /// Drains the [self.contents] and sends back to the renderer
+    pub(crate) fn process<'a>(&self) -> Vec<Box<dyn Widget>> {
+        let mut contents = self.contents.lock();
+        contents.drain(..).collect()
+    }
+
+    pub fn add<T: Widget + 'static>(&self, widget: T) {
+        self.contents.lock().push(Box::new(widget));
+    }
+}
+
+pub trait Widget {
+    fn id(&self) -> WidgetId;
+    fn draw<'a>(&mut self, renderer: &KinoRenderer, pass: &mut wgpu::RenderPass<'a>);
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct WidgetId(u64);
+
+impl WidgetId {
+    pub fn new(id: u64) -> Self {
+        Self(id)
+    }
+
+    pub fn as_u64(&self) -> u64 {
+        self.0
+    }
+}
+
+impl Into<WidgetId> for String {
+    fn into(self) -> WidgetId {
+        let mut hasher = DefaultHasher::new();
+        self.hash(&mut hasher);
+        WidgetId(hasher.finish())
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-gui/src/math.rs b/crates/kino-gui/src/math.rs
new file mode 100644
index 0000000..8c2b8ee
--- /dev/null
+++ b/crates/kino-gui/src/math.rs
@@ -0,0 +1,106 @@
+#[derive(Debug, Clone, PartialEq)]
+pub struct Rect {
+    /// States the left most corner of the rectangle in reference to the window
+    pub initial: Vector2,
+    /// The width and height that extends from the initial point, with the width extending
+    /// left and the height extending down. 
+    pub size: Size,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct Vector2 {
+    pub x: f32,
+    pub y: f32,
+}
+
+impl Into<Size> for Vector2 {
+    fn into(self) -> Size {
+        Size {
+            width: self.x,
+            height: self.y,
+        }
+    }
+}
+
+impl Into<[f32; 2]> for Vector2 {
+    fn into(self) -> [f32; 2] {
+        [self.x, self.y]
+    }
+}
+
+impl Into<Vector2> for [f32; 2] {
+    fn into(self) -> Vector2 {
+        Vector2 {
+            x: self[0],
+            y: self[1],
+        }
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct Size {
+    pub width: f32,
+    pub height: f32
+}
+
+impl Default for Size {
+    fn default() -> Self {
+        Self {
+            // cant be zero otherwise wgpu panics
+            width: 800.0,
+            height: 600.0,
+        }
+    }
+}
+
+impl Into<Vector2> for Size {
+    fn into(self) -> Vector2 {
+        Vector2 {
+            x: self.width,
+            y: self.height,
+        }
+    }
+}
+
+impl Into<[f32; 2]> for Size {
+    fn into(self) -> [f32; 2] {
+        [self.width, self.height]
+    }
+}
+
+impl Into<Size> for [f32; 2] {
+    fn into(self) -> Size {
+        Size {
+            width: self[0],
+            height: self[1],
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Colour {
+    pub r: f32,
+    pub g: f32,
+    pub b: f32,
+    pub a: f32,
+}
+
+pub fn create_orthographic_projection(
+    left: f32,
+    right: f32,
+    bottom: f32,
+    top: f32,
+    near: f32,
+    far: f32,
+) -> [[f32; 4]; 4] {
+    let width = right - left;
+    let height = top - bottom;
+    let depth = far - near;
+
+    [
+        [2.0 / width, 0.0, 0.0, 0.0],
+        [0.0, 2.0 / height, 0.0, 0.0],
+        [0.0, 0.0, -2.0 / depth, 0.0],
+        [-(right + left) / width, -(bottom + top) / height, -(far + near) / depth, 1.0],
+    ]
+}
\ No newline at end of file
diff --git a/crates/kino-gui/src/primitives/mod.rs b/crates/kino-gui/src/primitives/mod.rs
new file mode 100644
index 0000000..4f68ece
--- /dev/null
+++ b/crates/kino-gui/src/primitives/mod.rs
@@ -0,0 +1 @@
+pub mod shapes;
\ No newline at end of file
diff --git a/crates/kino-gui/src/primitives/shapes.rs b/crates/kino-gui/src/primitives/shapes.rs
new file mode 100644
index 0000000..70d681b
--- /dev/null
+++ b/crates/kino-gui/src/primitives/shapes.rs
@@ -0,0 +1,120 @@
+use wgpu::RenderPass;
+use wgpu::util::DeviceExt;
+use crate::math::{create_orthographic_projection, Colour, Size, Vector2};
+use crate::{Widget, WidgetId};
+use crate::rendering::{Globals, KinoRenderer};
+use crate::utils::UniformBuffer;
+
+pub struct Rectangle {
+    pub id: WidgetId,
+    pub initial: Vector2,
+    pub size: Size,
+    pub fill_colour: Colour,
+
+    globals_uniform: Option<UniformBuffer<Globals>>,
+    vertex_buffer: Option<wgpu::Buffer>,
+}
+
+impl Rectangle {
+    pub fn new(id: WidgetId, initial: Vector2, size: Size, fill_colour: Colour) -> Self {
+        Self {
+            id,
+            initial,
+            size,
+            fill_colour,
+            globals_uniform: None,
+            vertex_buffer: None,
+        }
+    }
+
+    pub(crate) fn create_vertex_buffer(&self, device: &wgpu::Device) -> wgpu::Buffer {
+        let x = self.initial.x;
+        let y = self.initial.y;
+        let w = self.size.width;
+        let h = self.size.height;
+        let c = [
+            self.fill_colour.r,
+            self.fill_colour.g,
+            self.fill_colour.b,
+            self.fill_colour.a,
+        ];
+
+        #[repr(C)]
+        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+        struct Vertex {
+            position: [f32; 3],
+            fill_colour: [f32; 4],
+        }
+
+        let vertices: [Vertex; 6] = [
+            Vertex { position: [x, y, 0.0], fill_colour: c },
+            Vertex { position: [x + w, y, 0.0], fill_colour: c },
+            Vertex { position: [x + w, y + h, 0.0], fill_colour: c },
+            Vertex { position: [x, y, 0.0], fill_colour: c },
+            Vertex { position: [x + w, y + h, 0.0], fill_colour: c },
+            Vertex { position: [x, y + h, 0.0], fill_colour: c },
+        ];
+
+        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+            label: Some("rectangle vertex buffer"),
+            contents: bytemuck::cast_slice(&vertices),
+            usage: wgpu::BufferUsages::VERTEX,
+        })
+    }
+}
+
+impl Widget for Rectangle {
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn draw<'a>(&mut self, renderer: &KinoRenderer, pass: &mut RenderPass<'a>) {
+        if self.globals_uniform.is_none() {
+            let globals = Globals::new(
+                create_orthographic_projection(
+                    0.0,
+                    renderer.context.screen_size.width,
+                    renderer.context.screen_size.height,
+                    0.0,
+                    -1.0,
+                    1.0
+                ),
+            [
+                renderer.context.screen_size.width,
+                renderer.context.screen_size.height,
+                ],
+            );
+
+            self.globals_uniform = Some(UniformBuffer::new(
+                &renderer.render.device,
+                Some(globals),
+                Some("rectangle globals uniform"),
+            ));
+        }
+
+        if self.vertex_buffer.is_none() {
+            self.vertex_buffer = Some(self.create_vertex_buffer(&renderer.render.device));
+        }
+
+        let globals_bind_group = renderer.render.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("rectangle globals bind group"),
+            layout: &renderer.render.globals_uniform_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+                        buffer: self.globals_uniform.as_ref().unwrap().buffer(),
+                        offset: 0,
+                        size: None,
+                    }),
+                }
+            ],
+        });
+
+        pass.set_pipeline(&renderer.render.pipeline);
+        pass.set_bind_group(0, &globals_bind_group, &[]);
+        pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
+
+        pass.draw(0..6, 0..1);
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-gui/src/rendering.rs b/crates/kino-gui/src/rendering.rs
new file mode 100644
index 0000000..bbb9ce3
--- /dev/null
+++ b/crates/kino-gui/src/rendering.rs
@@ -0,0 +1,195 @@
+use std::num::NonZeroU64;
+use std::sync::Arc;
+use anyhow::Context;
+use wgpu::{include_wgsl, BindGroupLayoutEntry, ShaderStages, VertexAttribute, VertexStepMode};
+use crate::{GumContext, KinoUICommandBuffer};
+use crate::math::Size;
+
+pub struct KinoRenderer {
+    pub(crate) context: GumContext,
+    pub(crate) render: KinoRenderPipeline,
+    pub(crate) ui: Arc<KinoUICommandBuffer>,
+}
+
+pub(crate) struct KinoRenderPipeline {
+    pub(crate) globals_uniform_layout: wgpu::BindGroupLayout,
+    pub(crate) pipeline: wgpu::RenderPipeline,
+    pub(crate) device: Arc<wgpu::Device>,
+    pub(crate) queue: Arc<wgpu::Queue>,
+}
+
+impl KinoRenderer {
+    pub fn new(
+        device: Arc<wgpu::Device>,
+        queue: Arc<wgpu::Queue>,
+        texture_format: wgpu::TextureFormat,
+    ) -> anyhow::Result<Self> {
+        let shader = device.create_shader_module(include_wgsl!("shaders/primitive.wgsl"));
+
+        let globals_uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("kino globals uniform"),
+            entries: &[
+                BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: None,
+                    },
+                    count: None,
+                }
+            ],
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("kino render pipeline layout"),
+            bind_group_layouts: &[
+                &globals_uniform_layout
+            ],
+            push_constant_ranges: &[],
+        });
+
+        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("kino render pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                compilation_options: Default::default(),
+                buffers: &[
+                    VertexInput::desc()
+                ],
+            },
+            primitive: Default::default(),
+            depth_stencil: None,
+            multisample: Default::default(),
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                compilation_options: Default::default(),
+                targets: &[
+                    Some(wgpu::ColorTargetState {
+                        format: texture_format,
+                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+                        write_mask: Default::default(),
+                    })
+                ],
+            }),
+            multiview: None,
+            cache: None,
+        });
+
+        Ok(Self {
+            context: GumContext::new(),
+            render: KinoRenderPipeline {
+                globals_uniform_layout,
+                pipeline,
+                device,
+                queue,
+            },
+            ui: Arc::new(KinoUICommandBuffer::new()),
+        })
+    }
+
+    pub fn get_ui(&self) -> Arc<KinoUICommandBuffer> {
+        self.ui.clone()
+    }
+
+    /// Uses [self.render] in the backend, creates an encoder with the device provided, and
+    /// renders the content.
+    pub fn render_without_encoder(
+        &mut self,
+        view: &wgpu::TextureView,
+        size_to_render: Size,
+    ) {
+        let mut encoder = self.render.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("kino render encoder"),
+        });
+
+        self.render(&mut encoder, view, size_to_render);
+
+        self.render.queue.submit(std::iter::once(encoder.finish()));
+    }
+
+    /// Renders the content onto the provided texture/view.
+    pub fn render(
+        &mut self,
+        encoder: &mut wgpu::CommandEncoder,
+        view: &wgpu::TextureView,
+        size_to_render: Size,
+    ) {
+        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+            label: Some("kino render pass"),
+            color_attachments: &[
+                Some(wgpu::RenderPassColorAttachment {
+                    view,
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                })
+            ],
+            depth_stencil_attachment: None,
+            timestamp_writes: None,
+            occlusion_query_set: None,
+        });
+
+        self.context.screen_size = size_to_render;
+        
+        let mut contents = self.ui.process();
+        for mut widget in contents.drain(..) {
+            widget.draw(&self, &mut pass);
+        }
+    }
+}
+
+pub struct VertexInput {
+    position: [f32; 3],
+    fill_colour: [f32; 4],
+}
+
+impl VertexInput {
+    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
+        wgpu::VertexBufferLayout {
+            array_stride: 0,
+            step_mode: VertexStepMode::Vertex,
+            attributes: &[
+                // position
+                VertexAttribute {
+                    format: wgpu::VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                },
+
+                // fill_colour
+                VertexAttribute {
+                    format: wgpu::VertexFormat::Float32x4,
+                    offset: size_of::<[f32; 3]>() as u64,
+                    shader_location: 1,
+                }
+            ],
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+#[derive(Default)]
+pub struct Globals {
+    pub proj: [[f32; 4]; 4],
+    pub screen_size: [f32; 2],
+    pub _padding: [f32; 2],
+}
+
+impl Globals {
+    pub fn new(proj: [[f32; 4]; 4], screen_size: [f32; 2]) -> Self {
+        Self {
+            proj,
+            screen_size,
+            _padding: [0.0; 2],
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/kino-gui/src/shaders/primitive.wgsl b/crates/kino-gui/src/shaders/primitive.wgsl
new file mode 100644
index 0000000..3d250e3
--- /dev/null
+++ b/crates/kino-gui/src/shaders/primitive.wgsl
@@ -0,0 +1,30 @@
+struct Globals {
+    proj: mat4x4<f32>,
+    screen_size: vec2<f32>,
+}
+
+@group(0) @binding(0)
+var<uniform> globals: Globals;
+
+struct VertexInput {
+    @location(0) position: vec3<f32>,
+    @location(1) fill_colour: vec4<f32>,
+}
+
+struct VertexOutput {
+    @builtin(position) position: vec4<f32>,
+    @location(0) colour: vec4<f32>,
+}
+
+@vertex
+fn vs_main(in: VertexInput) -> VertexOutput {
+    var out: VertexOutput;
+    out.position = globals.proj * vec4<f32>(in.position, 1.0);
+    out.colour = in.fill_colour;
+    return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    return in.colour;
+}
\ No newline at end of file
diff --git a/crates/kino-gui/src/utils.rs b/crates/kino-gui/src/utils.rs
new file mode 100644
index 0000000..9415989
--- /dev/null
+++ b/crates/kino-gui/src/utils.rs
@@ -0,0 +1,35 @@
+use wgpu::util::{BufferInitDescriptor, DeviceExt};
+
+pub struct UniformBuffer<T> {
+    pub inner: T,
+    buffer: wgpu::Buffer,
+}
+
+impl<T: bytemuck::Pod + Default> UniformBuffer<T> {
+    pub fn new(device: &wgpu::Device, uniform: Option<T>, label: Option<&str>) -> Self {
+        let uniform = if let Some(uniform) = uniform {
+            uniform
+        } else {
+            T::default()
+        };
+
+        let buffer = device.create_buffer_init(&BufferInitDescriptor {
+            label,
+            contents: bytemuck::cast_slice(&[uniform]),
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+        });
+
+        Self {
+            inner: uniform,
+            buffer,
+        }
+    }
+
+    pub fn write(&self, queue: &wgpu::Queue, uniform: T) {
+        queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[uniform]));
+    }
+    
+    pub fn buffer(&self) -> &wgpu::Buffer {
+        &self.buffer
+    }
+}
diff --git a/crates/redback-runtime/Cargo.toml b/crates/redback-runtime/Cargo.toml
index 83de71d..acbc5b7 100644
--- a/crates/redback-runtime/Cargo.toml
+++ b/crates/redback-runtime/Cargo.toml
@@ -9,6 +9,7 @@ readme = "README.md"
 [dependencies]
 dropbear-engine = { path = "../dropbear-engine" }
 eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false }
+kino-gui = { path = "../kino-gui" }
 
 anyhow.workspace = true
 log.workspace = true
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 816d008..a857bd8 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -85,6 +85,7 @@ pub struct PlayMode {
     main_pipeline: Option<MainRenderPipeline>,
     shader_globals: Option<GlobalsUniform>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
+    kino_renderer: Option<kino_gui::prelude::KinoRenderer>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -152,6 +153,7 @@ impl PlayMode {
             collision_event_receiver: Some(ce_r),
             collision_force_event_receiver: Some(cfe_r),
             event_collector,
+            kino_renderer: None,
         };
 
         log::debug!("Created new play mode instance");
@@ -164,6 +166,20 @@ impl PlayMode {
         self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone()));
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
+        
+        match kino_gui::prelude::KinoRenderer::new(
+            graphics.device.clone(),
+            graphics.queue.clone(),
+            graphics.surface_format,
+        ) {
+            Ok(renderer) => {
+                self.kino_renderer = Some(renderer);
+                log::debug!("KinoRenderer initialized successfully");
+            }
+            Err(e) => {
+                log::error!("Failed to initialize KinoRenderer: {}", e);
+            }
+        }
     }
 
     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 ad37b95..39eb8d2 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -446,16 +446,6 @@ impl Scene for PlayMode {
                             size: egui::vec2(display_width, display_height),
                         }));
                     });
-
-                    // render overlay
-                    egui::Area::new("overlay".into())
-                        .fixed_pos(egui::pos2(center_x, center_y))
-                        .show(&graphics.get_egui_context(), |o_ui| {
-                            // render the scripting overlay
-                            if let Err(e) = eucalyptus_core::ui::poll(o_ui) {
-                                log_once::error_once!("Unable to poll the UI: {}", e);
-                            }
-                        });
                 } else {
                     log::warn!("No such camera exists in the world");
                 }
@@ -830,6 +820,43 @@ impl Scene for PlayMode {
                 log_once::error_once!("{}", e);
             }
         }
+
+        if let Some(kino_renderer) = &mut self.kino_renderer {
+            let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino ui render encoder"));
+            
+            let commands = eucalyptus_core::ui::UI_COMMAND_BUFFER.drain_commands();
+            let ui_buffer = kino_renderer.get_ui();
+
+            for command in commands {
+                match command {
+                    eucalyptus_core::ui::UiCommand::Rect { id, initial, size, fill, .. } => {
+                        let rect = kino_gui::prelude::shapes::Rectangle::new(
+                            kino_gui::prelude::WidgetId::new(id),
+                            initial,
+                            size,
+                            fill
+                        );
+                        ui_buffer.add(rect);
+                    }
+                    _ => {}
+                }
+            }
+
+            let screen_size = kino_gui::prelude::Size {
+                width: graphics.viewport_texture.size.width as f32,
+                height: graphics.viewport_texture.size.height as f32,
+            };
+
+            kino_renderer.render(
+                &mut *encoder,
+                &graphics.viewport_texture.view,
+                screen_size,
+            );
+
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("Failed to submit kino renderer: {}", e);
+            }
+        }
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}