kitgit

tirbofish/dropbear · commit

3a818442b7a234263bb51a63e0ae43bffb4d16b7

feature: created a checkbox refactor: changed up my UI structure Unverified

Thribhu K <4tkbytes@pm.me> · 2026-01-29 14:28

view full diff

diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 423883a..4de9409 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -80,6 +80,14 @@ pub struct Camera {
     pub view_mat: DMat4,
     /// Projection Matrix
     pub proj_mat: DMat4,
+    
+    /// Smooth damping factor for camera position updates (0.0 = no smoothing, higher = more smoothing)
+    /// This helps prevent jittering when camera follows physics-driven objects
+    pub position_smoothing: f64,
+    /// Internal: previous eye position for smoothing
+    smooth_eye: DVec3,
+    /// Internal: previous target position for smoothing  
+    smooth_target: DVec3,
 }
 
 /// A simple builder/struct that allows you to build a [`Camera`]
@@ -165,6 +173,9 @@ impl Camera {
             },
             view_mat,
             proj_mat,
+            position_smoothing: 10.0,  // Default smoothing value
+            smooth_eye: builder.eye,
+            smooth_target: builder.target,
         };
 
         log::debug!("Created new camera{}", if let Some(l) = label { format!(" with the label {}", l) } else { String::new() } );
@@ -217,7 +228,17 @@ impl Camera {
     }
 
     fn build_vp(&mut self) -> DMat4 {
-        let view = DMat4::look_at_lh(self.eye, self.target, self.up);
+        // Apply smooth interpolation to camera position if smoothing is enabled
+        if self.position_smoothing > 0.0 {
+            let lerp_factor = (1.0 / (1.0 + self.position_smoothing)).clamp(0.0, 1.0);
+            self.smooth_eye = self.smooth_eye.lerp(self.eye, lerp_factor);
+            self.smooth_target = self.smooth_target.lerp(self.target, lerp_factor);
+        } else {
+            self.smooth_eye = self.eye;
+            self.smooth_target = self.target;
+        }
+        
+        let view = DMat4::look_at_lh(self.smooth_eye, self.smooth_target, self.up);
         let proj = DMat4::perspective_infinite_reverse_lh(
             self.settings.fov_y.to_radians(),
             self.aspect,
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 3e49d4e..015d73d 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -23,12 +23,14 @@ 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 }
+        Self { local, world, previous_world: None }
     }
 
     /// Creates a new [EntityTransform] from a world [Transform] and a default local transform.
@@ -38,6 +40,7 @@ impl EntityTransform {
         Self {
             world,
             local: Transform::default(),
+            previous_world: None,
         }
     }
 
@@ -74,6 +77,26 @@ 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/eucalyptus-core/src/ui.rs b/crates/eucalyptus-core/src/ui.rs
index 6a594ca..242ae77 100644
--- a/crates/eucalyptus-core/src/ui.rs
+++ b/crates/eucalyptus-core/src/ui.rs
@@ -1,21 +1,26 @@
 mod button;
 mod utils;
 mod text;
+mod align;
+mod checkbox;
 
+use std::any::Any;
 use std::cell::RefCell;
 use std::collections::HashMap;
+use std::fmt::Debug;
 use ::jni::JNIEnv;
 use ::jni::objects::JObject;
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
 use yakui::{Alignment, MainAxisSize, Yakui};
-use yakui::font::Fonts;
 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;
 
 thread_local! {
@@ -38,32 +43,17 @@ pub struct UIComponent {
 pub struct WidgetState {
     pub clicked: bool,
     pub hovering: bool,
-}
-
-pub trait NativeWidget: Send + std::fmt::Debug {
-    fn build(self: Box<Self>, states: &mut HashMap<i64, WidgetState>);
-}
-
-#[derive(Debug)]
-pub struct WrapperWidget<T> {
-    pub id: i64,
-    pub widget: T,
+    pub checked: bool,
 }
 
 pub trait WidgetParser: Send + Sync {
-    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>>;
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>>;
     fn name(&self) -> String;
 }
 
-pub fn register_widget_parser<P: WidgetParser + 'static>(parser: P) {
-    UI_CONTEXT.with(|v| {
-        v.borrow().parsers.lock().push(Box::new(parser));
-    });
-}
-
 pub struct UiContext {
     pub yakui_state: Mutex<Yakui>,
-    pub instruction_set: Mutex<Vec<Box<dyn NativeWidget>>>,
+    pub instruction_set: Mutex<Vec<UiInstructionType>>,
     pub widget_states: Mutex<HashMap<i64, WidgetState>>,
     pub parsers: Mutex<Vec<Box<dyn WidgetParser>>>,
 }
@@ -75,34 +65,127 @@ pub fn poll() {
         let mut widget_states = ctx.widget_states.lock();
 
         widget_states.clear();
-        
-        let current_instructions = instructions.drain(..).collect::<Vec<Box<dyn NativeWidget>>>();
+
+        let current_instructions = instructions.drain(..).collect::<Vec<UiInstructionType>>();
+
+        let tree = build_tree(current_instructions);
+
         yakui::widgets::Align::new(Alignment::TOP_LEFT).show(|| {
             yakui::widgets::List::column()
-                .main_axis_size(MainAxisSize::Min)
+                .main_axis_size(MainAxisSize::Max)
                 .show(|| {
-                    for i in current_instructions {
-                        i.build(&mut widget_states);
-                    }
+                    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),
+    Widget(Box<dyn NativeWidget>),
+}
+
+#[derive(Debug)]
+pub enum ContaineredWidgetType {
+    Start {
+        id: i64,
+        widget: Box<dyn ContaineredWidget>,
+    },
+    End {
+        id: i64,
+    }
+}
+
+pub trait NativeWidget: Send + Sync + Debug {
+    fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>);
+    fn id(&self) -> i64;
+    fn as_any(&self) -> &dyn Any;
+}
+
+pub trait ContaineredWidget: Send + Sync + Debug {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>);
+    fn as_any(&self) -> &dyn Any;
+}
+
+pub struct UiNode {
+    pub instruction: UiInstructionType,
+    pub children: Vec<UiNode>,
+}
+
 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));
 
         let yakui = Yakui::new();
-        let fonts = yakui.dom().get_global_or_init(Fonts::default);
-        fonts.set_sans_serif_family("Roboto");
-        fonts.set_serif_family("Roboto");
-        fonts.set_cursive_family("Roboto");
-        fonts.set_fantasy_family("Roboto");
-        fonts.set_monospace_family("Roboto");
 
         Self {
             yakui_state: Mutex::new(yakui),
@@ -123,7 +206,7 @@ pub trait UiWidgetType: FromJObject {
 pub mod jni {
     #![allow(non_snake_case)]
 
-    use jni::sys::{jboolean, jlong};
+    use jni::sys::{jlong};
     use jni::objects::{JClass, JObjectArray};
     use jni::JNIEnv;
     use crate::convert_ptr;
@@ -165,30 +248,8 @@ pub mod jni {
             }
         }
 
-        ui.instruction_set.lock().extend(rust_instructions);
-    }
-    
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getClicked(
-        _env: JNIEnv,
-        _class: JClass,
-        ui_buf_ptr: jlong,
-        id: jlong,
-    ) -> jboolean {
-        let ui = convert_ptr!(ui_buf_ptr => UiContext);
-        let states = ui.widget_states.lock();
-        states.get(&id).map(|s| s.clicked).unwrap_or(false).into()
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getHovering(
-        _env: JNIEnv,
-        _class: JClass,
-        ui_buf_ptr: jlong,
-        id: jlong,
-    ) -> jboolean {
-        let ui = convert_ptr!(ui_buf_ptr => UiContext);
-        let states = ui.widget_states.lock();
-        states.get(&id).map(|s| s.hovering).unwrap_or(false).into()
+        let mut instruction_set = ui.instruction_set.lock();
+        instruction_set.clear();
+        instruction_set.extend(rust_instructions);
     }
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/align.rs b/crates/eucalyptus-core/src/ui/align.rs
new file mode 100644
index 0000000..51fc973
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/align.rs
@@ -0,0 +1,81 @@
+use std::any::Any;
+use std::collections::HashMap;
+use jni::JNIEnv;
+use jni::objects::JObject;
+use yakui::Alignment;
+use crate::scripting::jni::utils::FromJObject;
+use crate::scripting::result::DropbearNativeResult;
+use crate::ui::{ContaineredWidget, ContaineredWidgetType, UiInstructionType, UiNode, WidgetParser, WidgetState};
+
+pub(crate) struct AlignParser;
+
+#[derive(Debug, Clone)]
+pub(crate) struct AlignWidget {
+    pub alignment: Alignment,
+}
+
+impl WidgetParser for AlignParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+
+        if name_string.contains("AlignmentInstruction$StartAlignmentBlock") {
+            let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/widgets/Align;")?.l()?;
+            let align = yakui::widgets::Align::from_jobject(env, &align_obj)?;
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Containered(
+                ContaineredWidgetType::Start {
+                    id,
+                    widget: Box::new(AlignWidget {
+                        alignment: align.alignment,
+                    }),
+                }
+            )));
+        }
+
+        if name_string.contains("AlignmentInstruction$EndAlignmentBlock") {
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Containered(
+                ContaineredWidgetType::End { id }
+            )));
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("AlignParser")
+    }
+}
+
+impl ContaineredWidget for AlignWidget {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut HashMap<i64, WidgetState>) {
+        yakui::widgets::Align::new(self.alignment).show(|| {
+            super::render_tree(children, state);
+        });
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+impl FromJObject for yakui::widgets::Align {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self>
+    where
+        Self: Sized
+    {
+        let align_obj = env.get_field(obj, "align", "Lcom/dropbear/ui/styling/Alignment;")?.l()?;
+        let alignment = Alignment::from_jobject(env, &align_obj)?;
+
+        Ok(Self {
+            alignment,
+        })
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/button.rs b/crates/eucalyptus-core/src/ui/button.rs
index dc7ecb6..f4bb622 100644
--- a/crates/eucalyptus-core/src/ui/button.rs
+++ b/crates/eucalyptus-core/src/ui/button.rs
@@ -1,33 +1,39 @@
-use jni::JNIEnv;
-use jni::objects::JObject;
+use std::any::Any;
+use std::collections::HashMap;
 use yakui::{Alignment, BorderRadius};
 use yakui::widgets::{Button, Pad, DynamicButtonStyle};
 use crate::scripting::jni::utils::FromJObject;
 use crate::scripting::result::DropbearNativeResult;
 use std::borrow::Cow;
-use std::collections::HashMap;
-use crate::ui::{NativeWidget, WidgetParser, WidgetState, WrapperWidget};
+use ::jni::JNIEnv;
+use ::jni::objects::JObject;
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
 
 pub(crate) struct ButtonParser;
 
+#[derive(Debug)]
+pub(crate) struct ButtonWidget {
+    pub id: i64,
+    pub button: yakui::widgets::Button,
+}
+
 impl WidgetParser for ButtonParser {
-    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>> {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
         let class = env.get_object_class(obj)?;
         let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
         let name_string: String = env.get_string(&name_str_obj.into())?.into();
-        // println!("ButtonParser obj get_string result: {}", name_string);
 
         if name_string.contains("ButtonInstruction$Button") {
             let button_obj = env.get_field(obj, "button", "Lcom/dropbear/ui/widgets/Button;")?.l()?;
-            let btn = yakui::widgets::Button::from_jobject(env, &button_obj)?;
+            let button = yakui::widgets::Button::from_jobject(env, &button_obj)?;
 
             let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
             let id = env.get_field(id_obj, "id", "J")?.j()?;
 
-            return Ok(Some(Box::new(WrapperWidget {
+            return Ok(Some(UiInstructionType::Widget(Box::new(ButtonWidget {
                 id,
-                widget: btn,
-            })));
+                button,
+            }))));
         }
 
         Ok(None)
@@ -38,14 +44,23 @@ impl WidgetParser for ButtonParser {
     }
 }
 
-impl NativeWidget for WrapperWidget<yakui::widgets::Button> {
-    fn build(self: Box<Self>, states: &mut HashMap<i64, WidgetState>) {
-        let res = self.widget.show();
+impl NativeWidget for ButtonWidget {
+    fn render(self: Box<Self>, states: &mut HashMap<i64, WidgetState>) {
+        let res = self.button.show();
         states.insert(self.id, WidgetState {
             clicked: res.clicked,
             hovering: res.hovering,
+            checked: false, // always be false because it is not a checkbox, obv
         });
     }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
 }
 
 impl FromJObject for Button {
@@ -55,40 +70,22 @@ impl FromJObject for Button {
         let text: String = env.get_string(&text_jstring)?.into();
 
         let f = env.get_field(obj, "alignment", "Lcom/dropbear/ui/styling/Alignment;")?.l()?;
-        let alignment = Alignment::from_jobject(
-            env,
-            &f
-        )?;
+        let alignment = Alignment::from_jobject(env, &f)?;
 
         let f = env.get_field(obj, "padding", "Lcom/dropbear/ui/styling/Padding;")?.l()?;
-        let padding = Pad::from_jobject(
-            env,
-            &f
-        )?;
+        let padding = Pad::from_jobject(env, &f)?;
 
         let f = env.get_field(obj, "borderRadius", "Lcom/dropbear/ui/styling/BorderRadius;")?.l()?;
-        let border_radius = BorderRadius::from_jobject(
-            env,
-            &f
-        )?;
-        
+        let border_radius = BorderRadius::from_jobject(env, &f)?;
+
         let f = env.get_field(obj, "style", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
-        let style = DynamicButtonStyle::from_jobject(
-            env,
-            &f
-        )?;
+        let style = DynamicButtonStyle::from_jobject(env, &f)?;
 
         let f = env.get_field(obj, "hoverStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
-        let hover_style = DynamicButtonStyle::from_jobject(
-            env,
-            &f
-        )?;
+        let hover_style = DynamicButtonStyle::from_jobject(env, &f)?;
 
         let f = env.get_field(obj, "downStyle", "Lcom/dropbear/ui/styling/DynamicButtonStyle;")?.l()?;
-        let down_style = DynamicButtonStyle::from_jobject(
-            env,
-            &f
-        )?;
+        let down_style = DynamicButtonStyle::from_jobject(env, &f)?;
 
         Ok(Self {
             text: Cow::Owned(text),
@@ -101,3 +98,37 @@ impl FromJObject for Button {
         })
     }
 }
+
+pub mod jni {
+    #![allow(non_snake_case)]
+    
+    use jni::JNIEnv;
+    use jni::objects::JClass;
+    use jni::sys::{jboolean, jlong};
+    use crate::convert_ptr;
+    use crate::ui::UiContext;
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getClicked(
+        _env: JNIEnv,
+        _class: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+        let states = ui.widget_states.lock();
+        states.get(&id).map(|s| s.clicked).unwrap_or(false).into()
+    }
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_ButtonNative_getHovering(
+        _env: JNIEnv,
+        _class: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+        let states = ui.widget_states.lock();
+        states.get(&id).map(|s| s.hovering).unwrap_or(false).into()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/checkbox.rs b/crates/eucalyptus-core/src/ui/checkbox.rs
new file mode 100644
index 0000000..17ff8ec
--- /dev/null
+++ b/crates/eucalyptus-core/src/ui/checkbox.rs
@@ -0,0 +1,99 @@
+use std::any::Any;
+use std::collections::HashMap;
+use ::jni::JNIEnv;
+use ::jni::objects::JObject;
+use yakui::widgets::Checkbox;
+use crate::scripting::result::DropbearNativeResult;
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
+
+pub(crate) struct CheckboxParser;
+
+#[derive(Debug)]
+pub(crate) struct CheckboxWidget {
+    pub id: i64,
+    pub checkbox: Checkbox,
+}
+
+impl WidgetParser for CheckboxParser {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
+        let class = env.get_object_class(obj)?;
+        let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
+        let name_string: String = env.get_string(&name_str_obj.into())?.into();
+
+        if name_string.contains("CheckboxInstruction$Checkbox") {
+            let checked = env.get_field(obj, "checked", "Z")?.z()?;
+            let checkbox = Checkbox::new(checked);
+
+            let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
+            let id = env.get_field(id_obj, "id", "J")?.j()?;
+
+            return Ok(Some(UiInstructionType::Widget(Box::new(CheckboxWidget {
+                id,
+                checkbox,
+            }))));
+        }
+
+        Ok(None)
+    }
+
+    fn name(&self) -> String {
+        String::from("CheckboxParser")
+    }
+}
+
+impl NativeWidget for CheckboxWidget {
+    fn render(self: Box<Self>, state: &mut HashMap<i64, WidgetState>) {
+        let resp = self.checkbox.show();
+        state.insert(self.id, WidgetState {
+            clicked: false,
+            hovering: false,
+            checked: resp.checked,
+        });
+
+    }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+pub mod jni {
+    #![allow(non_snake_case)]
+
+    use jni::JNIEnv;
+    use jni::objects::JClass;
+    use jni::sys::{jboolean, jlong};
+    use crate::convert_ptr;
+    use crate::ui::UiContext;
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_getChecked(
+        _env: JNIEnv,
+        _: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+
+        if let Some(v) = ui.widget_states.lock().get(&(id as i64)) {
+            return v.checked.into();
+        }
+        false.into()
+    }
+
+    #[unsafe(no_mangle)]
+    pub extern "system" fn Java_com_dropbear_ui_widgets_CheckboxNative_hasCheckedState(
+        _env: JNIEnv,
+        _: JClass,
+        ui_buf_ptr: jlong,
+        id: jlong,
+    ) -> jboolean {
+        let ui = convert_ptr!(ui_buf_ptr => UiContext);
+
+        ui.widget_states.lock().contains_key(&(id as i64)).into()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ui/text.rs b/crates/eucalyptus-core/src/ui/text.rs
index e7c084e..1c5fce3 100644
--- a/crates/eucalyptus-core/src/ui/text.rs
+++ b/crates/eucalyptus-core/src/ui/text.rs
@@ -1,3 +1,4 @@
+use std::any::Any;
 use std::borrow::Cow;
 use std::collections::HashMap;
 use jni::JNIEnv;
@@ -6,12 +7,18 @@ use yakui::style::TextStyle;
 use yakui::widgets::Pad;
 use crate::scripting::jni::utils::FromJObject;
 use crate::scripting::result::DropbearNativeResult;
-use crate::ui::{NativeWidget, WidgetParser, WidgetState, WrapperWidget};
+use crate::ui::{NativeWidget, UiInstructionType, WidgetParser, WidgetState};
 
 pub(crate) struct TextParser;
 
+#[derive(Debug)]
+pub(crate) struct TextWidget {
+    pub id: i64,
+    pub text: yakui::widgets::Text,
+}
+
 impl WidgetParser for TextParser {
-    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<Box<dyn NativeWidget>>> {
+    fn parse(&self, env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Option<UiInstructionType>> {
         let class = env.get_object_class(obj)?;
         let name_str_obj = env.call_method(class, "getName", "()Ljava/lang/String;", &[])?.l()?;
         let name_string: String = env.get_string(&name_str_obj.into())?.into();
@@ -24,10 +31,10 @@ impl WidgetParser for TextParser {
             let id_obj = env.get_field(obj, "id", "Lcom/dropbear/ui/WidgetId;")?.l()?;
             let id = env.get_field(id_obj, "id", "J")?.j()?;
 
-            return Ok(Some(Box::new(WrapperWidget {
+            return Ok(Some(UiInstructionType::Widget(Box::new(TextWidget {
                 id,
-                widget: text,
-            })));
+                text,
+            }))))
         }
 
         Ok(None)
@@ -38,9 +45,17 @@ impl WidgetParser for TextParser {
     }
 }
 
-impl NativeWidget for WrapperWidget<yakui::widgets::Text> {
-    fn build(self: Box<Self>, _states: &mut HashMap<i64, WidgetState>) {
-        self.widget.show();
+impl NativeWidget for TextWidget {
+    fn render(self: Box<Self>, _state: &mut HashMap<i64, WidgetState>) {
+        let _ = self.text.show(); // no response
+    }
+
+    fn id(&self) -> i64 {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
     }
 }
 
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 903169b..491891b 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -24,7 +24,6 @@ use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, Wor
 use eucalyptus_core::command::COMMAND_BUFFER;
 use eucalyptus_core::scene::loading::IsSceneLoaded;
 use std::collections::HashMap;
-use std::env::current_exe;
 use std::path::PathBuf;
 use wgpu::SurfaceConfiguration;
 use winit::window::Fullscreen;
@@ -81,7 +80,7 @@ fn find_jvm_library_path() -> PathBuf {
 fn find_jvm_library_path() -> PathBuf {
     let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
 
-    for entry in std::fs::read_dir(current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") {
+    for entry in std::fs::read_dir(std::env::current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") {
         let entry = entry.expect("Unable to get directory entry");
         let path = entry.path();
 
@@ -105,6 +104,7 @@ fn find_jvm_library_path() -> PathBuf {
         .map(|(path, _)| path)
         .expect("No suitable candidate for a JVM targeted play mode session available")
 }
+
 pub struct PlayMode {
     scene_command: SceneCommand,
     input_state: InputState,
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 8002c8f..24d4e35 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -186,6 +186,11 @@ 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() {
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt
index fbd1fe1..c197f9a 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/Widget.kt
@@ -5,5 +5,16 @@ package com.dropbear.ui
  * describing a widget such as a button, or even a column.
  */
 abstract class Widget {
+    /**
+     * The [WidgetId] used to differentiate between two different widgets.
+     *
+     * It is typically derived from the name of the widget.
+     */
     abstract val id: WidgetId
+
+    /**
+     * Converts the [Widget] to a [UIInstruction] list, allowing to be added
+     * by a [UIBuilder].
+     */
+    abstract fun toInstruction(): List<UIInstruction>
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt
index e555608..391bb2e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/WidgetId.kt
@@ -2,4 +2,6 @@ package com.dropbear.ui
 
 import com.dropbear.utils.ID
 
-class WidgetId(val id: Long) : ID(id)
\ No newline at end of file
+class WidgetId(val id: Long) : ID(id) {
+    constructor(id: String) : this(id.hashCode().toLong())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt
index 28c64cb..c6756a1 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Align.kt
@@ -7,32 +7,42 @@ import com.dropbear.ui.WidgetId
 import com.dropbear.ui.styling.Alignment
 
 class Align(
+    override var id: WidgetId,
     var align: Alignment,
-): Widget()  {
-    override lateinit var id: WidgetId
-
+    var widgets: MutableList<Widget> = mutableListOf(),
+): Widget() {
     companion object {
-        fun center() = Align(align = Alignment.CENTER)
+        fun center(id: WidgetId) = Align(align = Alignment.CENTER, id = id)
     }
 
     sealed class AlignmentInstruction: UIInstruction {
-        data class Center(val id: WidgetId, val align: Align) : AlignmentInstruction()
+        data class StartAlignmentBlock(val id: WidgetId, val align: Align) : AlignmentInstruction()
+        data class EndAlignmentBlock(val id: WidgetId) : AlignmentInstruction()
     }
 
-    fun toInstruction(): AlignmentInstruction.Center {
-        return AlignmentInstruction.Center(this.id, this)
+    override fun toInstruction(): List<UIInstruction> {
+        val instructions = mutableListOf<UIInstruction>()
+        instructions.add(AlignmentInstruction.StartAlignmentBlock(id, this))
+        widgets.forEach { widget ->
+            widget.toInstruction().forEach { instruction ->
+                instructions.add(instruction)
+            }
+        }
+        instructions.add(AlignmentInstruction.EndAlignmentBlock(id))
+        return instructions.toList()
     }
 }
 
-
-fun UIBuilder.center(block: UIBuilder.() -> Unit = {}) {
-    val align = Align.center()
-    instructions.add(align.toInstruction())
-    block()
+fun UIBuilder.center(id: WidgetId, block: UIBuilder.() -> Unit = {}) {
+    val align = Align.center(id)
+    instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align))
+    block(this)
+    instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id))
 }
 
-fun UIBuilder.align(alignment: Alignment, block: UIBuilder.() -> Unit = {}) {
-    val align = Align(alignment)
-    instructions.add(align.toInstruction())
-    block()
+fun UIBuilder.align(alignment: Alignment, id: WidgetId, block: UIBuilder.() -> Unit = {}) {
+    val align = Align(align = alignment, id = id)
+    instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align))
+    block(this)
+    instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id))
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt
index f729063..d3bd5ec 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Button.kt
@@ -96,8 +96,8 @@ class Button(
         data class Button(val id: WidgetId, val button: com.dropbear.ui.widgets.Button) : ButtonInstruction()
     }
 
-    fun toInstruction(): ButtonInstruction.Button {
-        return ButtonInstruction.Button(this.id, this)
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(ButtonInstruction.Button(this.id, this))
     }
 }
 
@@ -107,6 +107,8 @@ expect fun Button.getHovering(): Boolean
 // fits that of yakui_widgets::shorthand::button
 fun UIBuilder.button(text: String, block: Button.() -> Unit = {}): Button {
     val btn = Button.styled(text).apply(block)
-    instructions.add(btn.toInstruction())
+    btn.toInstruction().forEach {
+        instructions.add(it)
+    }
     return btn
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt
new file mode 100644
index 0000000..0c682fb
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Checkbox.kt
@@ -0,0 +1,48 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+
+class Checkbox(
+    override val id: WidgetId,
+    initiallyChecked: Boolean,
+) : Widget() {
+    private var cachedChecked: Boolean = initiallyChecked
+
+    /**
+     * To set a new checkbox value, make it so the class is different next frame.
+     */
+    var checked: Boolean
+        get() {
+            if (hasCheckedState()) {
+                cachedChecked = getChecked()
+            }
+            return cachedChecked
+        }
+        private set(value) {
+            cachedChecked = value
+        }
+
+    init {
+        checked = initiallyChecked
+    }
+
+    sealed class CheckboxInstruction: UIInstruction {
+        data class Checkbox(val id: WidgetId, val checked: Boolean) : CheckboxInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(CheckboxInstruction.Checkbox(id, checked))
+    }
+}
+
+expect fun Checkbox.getChecked(): Boolean
+expect fun Checkbox.hasCheckedState(): Boolean
+
+fun UIBuilder.checkbox(checked: Boolean, id: WidgetId, block: Checkbox.() -> Unit = {}): Checkbox {
+    val cb = Checkbox(id, checked).apply(block)
+    cb.toInstruction().forEach { instruction -> instructions.add(instruction) }
+    return cb
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt
new file mode 100644
index 0000000..1e4377a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/ColouredBox.kt
@@ -0,0 +1,69 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.math.Vector2d
+import com.dropbear.ui.UIBuilder
+import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.Widget
+import com.dropbear.ui.WidgetId
+import com.dropbear.ui.buildUI
+import com.dropbear.utils.Colour
+
+class ColouredBox(
+    override val id: WidgetId,
+    var colour: Colour = Colour.WHITE,
+    var minSize: Vector2d = Vector2d.zero(),
+) : Widget() {
+    var widgets: MutableList<UIInstruction> = mutableListOf()
+
+    companion object {
+        fun sized(id: WidgetId, colour: Colour, size: Vector2d): ColouredBox {
+            return ColouredBox(id, colour, size)
+        }
+
+        fun container(id: WidgetId, colour: Colour): ColouredBox {
+            return ColouredBox(id, colour, Vector2d.zero())
+        }
+    }
+
+    sealed class ColouredBoxInstruction: UIInstruction {
+        data class StartColouredBoxInstruction(val id: WidgetId, val box: ColouredBox): ColouredBoxInstruction()
+        data class EndColouredBoxInstruction(val id: WidgetId): ColouredBoxInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        val list = mutableListOf<UIInstruction>()
+        list.add(ColouredBoxInstruction.StartColouredBoxInstruction(this.id, this))
+        list.addAll(widgets)
+        list.add(ColouredBoxInstruction.EndColouredBoxInstruction(this.id))
+        return list.toList()
+    }
+
+    fun addChild(widget: Widget) {
+        widgets.addAll(widget.toInstruction())
+    }
+}
+
+fun UIBuilder.colouredBox(id: WidgetId, colour: Colour, minSize: Vector2d, block: ColouredBox.() -> Unit = {}): ColouredBox {
+    val box = ColouredBox(id, colour, minSize).apply(block)
+    instructions.addAll(box.toInstruction())
+    return box
+}
+
+fun UIBuilder.colouredBoxContainer(id: WidgetId, colour: Colour, children: UIBuilder.() -> Unit): ColouredBox {
+    val box = ColouredBox(id = id, colour = colour)
+    val newUI = UIBuilder()
+    children(newUI) // execute
+    box.widgets.addAll(newUI.build())
+    instructions.addAll(box.toInstruction())
+    return box
+}
+
+fun dummy() {
+    val instructions = buildUI {
+        colouredBox(WidgetId("something"), Colour.TRANSPARENT, Vector2d.zero())
+
+        colouredBoxContainer(WidgetId("something"), Colour.TRANSPARENT) {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt
index c02043e..7beb2e1 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Text.kt
@@ -37,8 +37,8 @@ class Text(
         data class Text(val id: WidgetId, val text: com.dropbear.ui.widgets.Text) : TextInstruction()
     }
 
-    fun toInstruction(): TextInstruction.Text {
-        return TextInstruction.Text(this.id, this)
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(TextInstruction.Text(this.id, this))
     }
 }
 
@@ -49,7 +49,9 @@ fun UIBuilder.label(text: String, block: Text.() -> Unit = {}): Text {
         style = style,
         padding = Padding.zero()
     ).apply(block)
-    instructions.add(text.toInstruction())
+    text.toInstruction().forEach {
+        instructions.add(it)
+    }
     return text
 }
 
@@ -61,6 +63,8 @@ fun UIBuilder.text(size: Double, text: String, block: Text.() -> Unit = {}): Tex
         style = style,
         padding = Padding.zero()
     ).apply(block)
-    instructions.add(text.toInstruction())
+    text.toInstruction().forEach {
+        instructions.add(it)
+    }
     return text
 }
\ No newline at end of file
diff --git a/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java b/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
new file mode 100644
index 0000000..e32abf2
--- /dev/null
+++ b/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
@@ -0,0 +1,12 @@
+package com.dropbear.ui.widgets;
+
+import com.dropbear.EucalyptusCoreLoader;
+
+public class CheckboxNative {
+    static {
+        new EucalyptusCoreLoader().ensureLoaded();
+    }
+
+    public static native boolean getChecked(long uiBufferHandle, long id);
+    public static native boolean hasCheckedState(long uiBufferHandle, long id);
+}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt
new file mode 100644
index 0000000..528277d
--- /dev/null
+++ b/scripting/jvmMain/kotlin/com/dropbear/ui/widgets/Checkbox.jvm.kt
@@ -0,0 +1,11 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.DropbearEngine
+
+actual fun Checkbox.getChecked(): Boolean {
+    return CheckboxNative.getChecked(DropbearEngine.native.uiBufferHandle, id.id)
+}
+
+actual fun Checkbox.hasCheckedState(): Boolean {
+    return CheckboxNative.hasCheckedState(DropbearEngine.native.uiBufferHandle, id.id)
+}
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt
new file mode 100644
index 0000000..aaaf684
--- /dev/null
+++ b/scripting/nativeMain/kotlin/com/dropbear/ui/widgets/Checkbox.native.kt
@@ -0,0 +1,9 @@
+package com.dropbear.ui.widgets
+
+actual fun Checkbox.getChecked(): Boolean {
+    TODO("Not yet implemented")
+}
+
+actual fun Checkbox.hasCheckedState(): Boolean {
+    return false
+}
\ No newline at end of file