kitgit

tirbofish/dropbear · commit

2f038d7e62aeb33bdc2766943bd0729ba3f0ed39

feature: rows and columns + scripting feature: scripting for rectangles fix: dpi implemented

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-02-11 11:05

view full diff

diff --git a/crates/kino-ui/src/camera.rs b/crates/kino-ui/src/camera.rs
index f1b9753..4b1c675 100644
--- a/crates/kino-ui/src/camera.rs
+++ b/crates/kino-ui/src/camera.rs
@@ -1,5 +1,5 @@
-use bytemuck::{Pod, Zeroable};
-use glam::{Mat4, Vec2};
+    use bytemuck::{Pod, Zeroable};
+use glam::{Mat4, Vec2, Vec3};
 use crate::math::Rect;
 
 pub struct Camera2D {
@@ -19,15 +19,24 @@ impl Default for Camera2D {
 impl Camera2D {
     /// Returns the orthographic view-projection matrix for the current camera state
     pub fn view_proj(&self, screen_size: Vec2) -> Mat4 {
-        let width = screen_size.x / self.zoom;
-        let height = screen_size.y / self.zoom;
+        let (width, height) = (screen_size.x, screen_size.y);
 
-        let left = self.position.x;
-        let right = self.position.x + width;
-        let top = self.position.y;
-        let bottom = self.position.y + height;
+        let view = Mat4::look_at_rh(
+            self.position.extend(1.0),
+            self.position.extend(0.0),
+            Vec3::Y,
+        );
 
-        Mat4::orthographic_lh(left, right, bottom, top, -1.0, 1.0)
+        let proj = Mat4::orthographic_rh(
+            0.0,
+            width,
+            height,
+            0.0,
+            -1.0,
+            1.0,
+        );
+
+        proj * view
     }
 
     /// Set the camera's position (top-left corner of view)
diff --git a/crates/kino-ui/src/widgets/layout.rs b/crates/kino-ui/src/widgets/layout.rs
new file mode 100644
index 0000000..5f17de2
--- /dev/null
+++ b/crates/kino-ui/src/widgets/layout.rs
@@ -0,0 +1,218 @@
+// In widgets/layout.rs
+
+use std::any::Any;
+use glam::{vec2, Vec2};
+use crate::{KinoState, UiNode, UiInstructionType, ContaineredWidgetType, WidgetId};
+use crate::widgets::{Anchor, ContaineredWidget};
+
+fn calculate_node_size(node: &UiNode) -> Vec2 {
+    match &node.instruction {
+        UiInstructionType::Widget(widget) => widget.size(),
+        UiInstructionType::Containered(ContaineredWidgetType::Start { widget, .. }) => {
+            if let Some(row) = widget.as_any().downcast_ref::<Row>() {
+                calculate_row_size(&node.children, row.spacing)
+            } else if let Some(column) = widget.as_any().downcast_ref::<Column>() {
+                calculate_column_size(&node.children, column.spacing)
+            } else {
+                Vec2::ZERO
+            }
+        }
+        _ => Vec2::ZERO,
+    }
+}
+
+fn calculate_row_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_width = 0.0;
+    let mut max_height: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_width += child_size.x;
+        max_height = max_height.max(child_size.y);
+
+        if i < children.len() - 1 {
+            total_width += spacing;
+        }
+    }
+
+    vec2(total_width, max_height)
+}
+
+fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 {
+    if children.is_empty() {
+        return Vec2::ZERO;
+    }
+
+    let mut total_height = 0.0;
+    let mut max_width: f32 = 0.0;
+
+    for (i, child) in children.iter().enumerate() {
+        let child_size = calculate_node_size(child);
+        total_height += child_size.y;
+        max_width = max_width.max(child_size.x);
+
+        if i < children.len() - 1 {
+            total_height += spacing;
+        }
+    }
+
+    vec2(max_width, total_height)
+}
+
+pub struct Row {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Row {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Row {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_row_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(-total_size.x / 2.0, 0.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut x_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(x_offset, 0.0),
+                vec2(child_size.x, total_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            x_offset += child_size.x + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_row_size(children, self.spacing)
+    }
+
+    fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+pub struct Column {
+    pub id: WidgetId,
+    pub anchor: Anchor,
+    pub position: Vec2,
+    pub spacing: f32,
+}
+
+impl Column {
+    pub fn new(id: impl Into<WidgetId>) -> Self {
+        Self {
+            id: id.into(),
+            anchor: Anchor::TopLeft,
+            position: Vec2::ZERO,
+            spacing: 8.0,
+        }
+    }
+
+    pub fn at(mut self, position: impl Into<Vec2>) -> Self {
+        self.position = position.into();
+        self
+    }
+
+    pub fn spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
+    pub fn anchor(mut self, anchor: Anchor) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    pub fn build(self) -> Box<Self> {
+        Box::new(self)
+    }
+}
+
+impl ContaineredWidget for Column {
+    fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) {
+        let total_size = calculate_column_size(&children, self.spacing);
+
+        let offset = match self.anchor {
+            Anchor::TopLeft => Vec2::ZERO,
+            Anchor::Center => vec2(0.0, -total_size.y / 2.0),
+        };
+
+        let start_pos = self.position + offset;
+        let mut y_offset = 0.0;
+
+        for child in children {
+            let child_size = calculate_node_size(&child);
+
+            state.push_container(crate::math::Rect::new(
+                start_pos + vec2(0.0, y_offset),
+                vec2(total_size.x, child_size.y)
+            ));
+
+            state.render_tree(vec![child]);
+            state.pop_container();
+
+            y_offset += child_size.y + self.spacing;
+        }
+    }
+
+    fn size(&self, children: &[UiNode]) -> Vec2 {
+        calculate_column_size(children, self.spacing)
+    }
+
+    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/widgets/mod.rs b/crates/kino-ui/src/widgets/mod.rs
index 20c1dfe..0f23c9a 100644
--- a/crates/kino-ui/src/widgets/mod.rs
+++ b/crates/kino-ui/src/widgets/mod.rs
@@ -1,8 +1,10 @@
 pub mod rect;
 pub mod shorthand;
 pub mod text;
+pub mod layout;
 
 use std::any::Any;
+use glam::Vec2;
 use crate::{KinoState, UiNode, WidgetId};
 
 /// Determines how the object is anchored.
@@ -21,12 +23,14 @@ pub trait NativeWidget: Send + Sync {
     /// The state is provided for you to manipulate, such as adding a new response based on the
     /// [`WidgetId`]. 
     fn render(self: Box<Self>, state: &mut KinoState);
+    fn size(&self) -> Vec2;
     fn id(&self) -> WidgetId;
     fn as_any(&self) -> &dyn Any;
 }
 
 pub trait ContaineredWidget: Send + Sync {
     fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState);
+    fn size(&self, children: &[UiNode]) -> Vec2;
     fn id(&self) -> WidgetId;
     fn as_any(&self) -> &dyn Any;
 }
diff --git a/crates/kino-ui/src/widgets/rect.rs b/crates/kino-ui/src/widgets/rect.rs
index b547784..e1917d7 100644
--- a/crates/kino-ui/src/widgets/rect.rs
+++ b/crates/kino-ui/src/widgets/rect.rs
@@ -65,7 +65,7 @@ impl Rectangle {
             id: id.into(),
             anchor: Anchor::TopLeft,
             position: Vec2::ZERO,
-            size: vec2(64.0, 64.0),
+            size: vec2(64.0, 128.0),
             rotation: 0.0,
             uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
             fill: Fill::default(),
@@ -220,6 +220,10 @@ impl NativeWidget for Rectangle {
         self.render_body(state, &rect, rot);
     }
 
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
@@ -238,6 +242,10 @@ impl ContaineredWidget for Rectangle {
         state.pop_container();
     }
 
+    fn size(&self, _children: &[UiNode]) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
diff --git a/crates/kino-ui/src/widgets/shorthand.rs b/crates/kino-ui/src/widgets/shorthand.rs
index e84b96c..75f5262 100644
--- a/crates/kino-ui/src/widgets/shorthand.rs
+++ b/crates/kino-ui/src/widgets/shorthand.rs
@@ -1,4 +1,5 @@
 use crate::{KinoState, WidgetId};
+use crate::widgets::layout::{Column, Row};
 use crate::widgets::rect::Rectangle;
 use crate::widgets::text::Text;
 
@@ -42,4 +43,38 @@ where
     let mut text = Text::new(text);
     configure(&mut text);
     kino.add_widget(Box::new(text))
+}
+
+/// Shorthand for [`Row`], used for displaying items that are displayed horizontally.
+pub fn row<C>(
+    kino: &mut KinoState,
+    row: Row,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = row.id;
+
+    kino.add_container(Box::new(row));
+    contents(kino);
+    kino.end_container(id);
+    id
+}
+
+/// Shorthand for [`Column`], used for displaying items that are displayed vertically.
+pub fn column<C>(
+    kino: &mut KinoState,
+    column: Column,
+    contents: C,
+) -> WidgetId
+where
+    C: FnOnce(&mut KinoState),
+{
+    let id = column.id;
+
+    kino.add_container(Box::new(column));
+    contents(kino);
+    kino.end_container(id);
+    id
 }
\ No newline at end of file
diff --git a/crates/kino-ui/src/widgets/text.rs b/crates/kino-ui/src/widgets/text.rs
index 857c6ed..06a7dcb 100644
--- a/crates/kino-ui/src/widgets/text.rs
+++ b/crates/kino-ui/src/widgets/text.rs
@@ -126,6 +126,10 @@ impl NativeWidget for Text {
         });
     }
 
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+
     fn id(&self) -> WidgetId {
         self.id
     }
diff --git a/crates/kino-ui/src/windowing.rs b/crates/kino-ui/src/windowing.rs
index 3f70541..2731bdd 100644
--- a/crates/kino-ui/src/windowing.rs
+++ b/crates/kino-ui/src/windowing.rs
@@ -2,6 +2,7 @@ use std::sync::Arc;
 use glam::Vec2;
 use winit::event::{ElementState, MouseButton, WindowEvent};
 use winit::window::Window;
+use crate::KinoState;
 
 pub struct KinoWinitWindowing {
     // mouse
@@ -9,29 +10,46 @@ pub struct KinoWinitWindowing {
     pub mouse_button: MouseButton,
     pub mouse_press_state: ElementState,
 
-    // keyboard
-
     // windowing
-    _window: Arc<Window>,
+    window: Arc<Window>,
     /// The top-left most pixel
     pub viewport_offset: Vec2,
     /// Scale from screen-space to viewport texture space
     pub viewport_scale: Vec2,
+
+    pub scale_factor: f32,
+    pub auto_scale: bool,
 }
 
 impl KinoWinitWindowing {
     /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset.
-    pub fn new(window: Arc<Window>) -> Self {
+    pub fn new(window: Arc<Window>, scale_factor: Option<f32>) -> Self {
+        let auto_scale = scale_factor.is_none();
+        let scale_factor = scale_factor.unwrap_or(window.scale_factor() as f32);
         Self {
             mouse_position: Default::default(),
             mouse_button: MouseButton::Left,
             mouse_press_state: ElementState::Released,
-            _window: window,
+            window,
             viewport_offset: Default::default(),
             viewport_scale: Vec2::ONE,
+            scale_factor,
+            auto_scale,
         }
     }
 
+    /// Get the physical size of the window in pixels
+    pub fn physical_size(&self) -> (u32, u32) {
+        let size = self.window.inner_size();
+        (size.width, size.height)
+    }
+
+    /// Get the logical size of the window (physical size / scale_factor)
+    pub fn logical_size(&self) -> Vec2 {
+        let (w, h) = self.physical_size();
+        Vec2::new(w as f32, h as f32) / self.scale_factor
+    }
+
     pub(crate) fn handle_event(&mut self, event: &WindowEvent) {
         match event {
             WindowEvent::CursorMoved { position, .. } => {
@@ -43,11 +61,50 @@ impl KinoWinitWindowing {
                 self.mouse_button = *button;
                 self.mouse_press_state = *state;
             }
+            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
+                if self.auto_scale { return; }
+                self.scale_factor = *scale_factor as f32;
+            }
             WindowEvent::Resized(_) => {}
             WindowEvent::KeyboardInput { .. } => {}
             WindowEvent::MouseWheel { .. } => {}
-            WindowEvent::ScaleFactorChanged { .. } => {}
             _ => {}
         }
     }
+}
+
+impl KinoState {
+    /// Get the current DPI scale factor
+    pub fn scale_factor(&self) -> f32 {
+        self.windowing.scale_factor
+    }
+
+    /// Scale a logical size to physical pixels
+    pub fn to_physical(&self, logical: f32) -> f32 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale a logical Vec2 to physical pixels
+    pub fn to_physical_vec(&self, logical: Vec2) -> Vec2 {
+        logical * self.windowing.scale_factor
+    }
+
+    /// Scale physical pixels to logical size
+    pub fn to_logical(&self, physical: f32) -> f32 {
+        physical / self.windowing.scale_factor
+    }
+
+    /// Scale physical Vec2 to logical size
+    pub fn to_logical_vec(&self, physical: Vec2) -> Vec2 {
+        physical / self.windowing.scale_factor
+    }
+
+    pub fn set_scale_factor(&mut self, factor: Option<f32>) {
+        if let Some(factor) = factor {
+            self.windowing.scale_factor = factor;
+            self.windowing.auto_scale = false;
+        } else {
+            self.windowing.auto_scale = true;
+        }
+    }
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index 9ed9e3e..ced3eed 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -1,9 +1,9 @@
 package com.dropbear
 
-import com.dropbear.asset.AssetHandle
+import com.dropbear.asset.AssetType
+import com.dropbear.asset.Handle
 import com.dropbear.ffi.NativeEngine
 import com.dropbear.input.InputState
-import com.dropbear.logging.Logger
 import com.dropbear.scene.SceneManager
 import com.dropbear.ui.UIInstruction
 import com.dropbear.ui.UIInstructionSet
@@ -32,17 +32,6 @@ class DropbearEngine(val native: NativeEngine) {
         fun getLastErrMsg(): String? {
             return lastErrorMessage
         }
-
-        /**
-         * Globally sets whether exceptions should be thrown when an error occurs.
-         *
-         * This can be run in your update loop without consequences.
-         */
-        @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-                "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-        fun callExceptionOnError(toggle: Boolean) {
-            exceptionOnError = toggle
-        }
     }
 
     /**
@@ -61,19 +50,10 @@ class DropbearEngine(val native: NativeEngine) {
      * ## Warning
      * The eucalyptus asset URI (or `euca://`) is case-sensitive.
      */
-    fun getAsset(eucaURI: String): AssetHandle? {
+    fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? {
         val id = com.dropbear.getAsset(eucaURI)
-        return if (id != null) AssetHandle(id) else null
-    }
-
-    /**
-     * Globally sets whether exceptions should be thrown when an error occurs.
-     *
-     * This can be run in your update loop without consequences.
-     */
-    @Deprecated("Currently not supported anymore, automatically throws exception on error. " +
-            "Better to catch the exception instead", level = DeprecationLevel.HIDDEN)
-    fun callExceptionOnError(toggle: Boolean) {
+        if (id == null || id <= 0L) return null
+        return Handle(id)
     }
 
     /**
diff --git a/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
index 4119d09..a0f7dcf 100644
--- a/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/animation/AnimationComponent.kt
@@ -41,6 +41,8 @@ class AnimationComponent(parentEntity: EntityId) : Component(parentEntity, "Anim
     fun reset() {
         time = 0f
     }
+
+    fun setAnimation(index: Int, speed: Float = 1f) = setActiveAnimationIndex(index).let { setSpeed(speed) }
 }
 
 expect fun AnimationComponent.getActiveAnimationIndex(): Int?
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
index 79b5b50..ac80dcd 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetType.kt
@@ -1,4 +1,3 @@
 package com.dropbear.asset
 
-interface AssetType {
-}
\ No newline at end of file
+open class AssetType internal constructor(open val id: Long)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
index a4c69da..fb70859 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Model.kt
@@ -6,7 +6,7 @@ import com.dropbear.asset.model.Skin
 import com.dropbear.asset.model.Animation
 import com.dropbear.asset.model.Node
 
-class Model(val id: Long): AssetType {
+class Model(override val id: Long): AssetType(id) {
     val label: String
         get() = getLabel()
 
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
index 4ffe7ca..a6b7f70 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/Texture.kt
@@ -1,6 +1,6 @@
 package com.dropbear.asset
 
-class Texture(val id: Long): AssetType {
+class Texture(override val id: Long): AssetType(id) {
     var label: String?
         get() = getLabel()
         set(value) = setLabel(value)
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
index 226e577..fe9649a 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Animation.kt
@@ -14,12 +14,34 @@ data class AnimationChannel(
     val times: DoubleArray,
     val values: ChannelValues,
     val interpolation: AnimationInterpolation
-)
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as AnimationChannel
+
+        if (targetNode != other.targetNode) return false
+        if (!times.contentEquals(other.times)) return false
+        if (values != other.values) return false
+        if (interpolation != other.interpolation) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = targetNode
+        result = 31 * result + times.contentHashCode()
+        result = 31 * result + values.hashCode()
+        result = 31 * result + interpolation.hashCode()
+        return result
+    }
+}
 
 enum class AnimationInterpolation {
     LINEAR,
     STEP,
-    CUBICSPLINE
+    CUBIC_SPLINE
 }
 
 sealed class ChannelValues {
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
new file mode 100644
index 0000000..5e9d853
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Anchor.kt
@@ -0,0 +1,14 @@
+package com.dropbear.ui.styling
+
+enum class Anchor {
+    /**
+     * A center anchor is when the position is based on the center of the object (such as the
+     * center of a circle)
+     */
+    Center,
+
+    /**
+     * A top left anchor is when the position is based on the top left corner of the object.
+     */
+    TopLeft,
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
new file mode 100644
index 0000000..c564aad
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/styling/Fill.kt
@@ -0,0 +1,5 @@
+package com.dropbear.ui.styling
+
+import com.dropbear.utils.Colour
+
+data class Fill(var colour: Colour)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
new file mode 100644
index 0000000..8a0bab7
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Column.kt
@@ -0,0 +1,70 @@
+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.styling.Anchor
+
+class Column(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class ColumnInstruction: UIInstruction {
+		data class StartColumnBlock(val id: WidgetId, val column: com.dropbear.ui.widgets.Column) : ColumnInstruction()
+		data class EndColumnBlock(val id: WidgetId) : ColumnInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this), ColumnInstruction.EndColumnBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(ColumnInstruction.StartColumnBlock(id, this)) +
+			children +
+			ColumnInstruction.EndColumnBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = ColumnInstruction.StartColumnBlock(id, this)
+
+	fun endInstruction(): UIInstruction = ColumnInstruction.EndColumnBlock(id)
+
+	fun at(position: Vector2d): Column {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Column {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Column {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.column(id: WidgetId = WidgetId(generateId().toLong()), block: Column.() -> Unit = {}): Column {
+	val column = Column(id = id).apply(block)
+	column.toInstruction().forEach { instructions.add(it) }
+	return column
+}
+
+fun UIBuilder.column(id: WidgetId, content: UIBuilder.(Column) -> Unit) {
+	column(id, columnBlock = {}, content = content)
+}
+
+fun UIBuilder.column(
+	id: WidgetId,
+	columnBlock: Column.() -> Unit = {},
+	content: UIBuilder.(Column) -> Unit
+) {
+	val column = Column(id = id).apply(columnBlock)
+	instructions.add(column.startInstruction())
+	this.content(column)
+	instructions.add(column.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
new file mode 100644
index 0000000..fd1efd4
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Rectangle.kt
@@ -0,0 +1,118 @@
+package com.dropbear.ui.widgets
+
+import com.dropbear.asset.Handle
+import com.dropbear.asset.Texture
+import com.dropbear.math.Angle
+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.styling.Anchor
+import com.dropbear.ui.styling.Border
+import com.dropbear.ui.styling.Fill
+import com.dropbear.utils.Colour
+
+class Rectangle(
+    override var id: WidgetId,
+    var anchor: Anchor = Anchor.TopLeft,
+    var position: Vector2d = Vector2d.zero(),
+    var size: Vector2d = Vector2d(64.0, 128.0),
+    var texture: Handle<Texture>? = null,
+    var rotation: Angle = Angle.ZERO,
+    var uv: List<Vector2d> = listOf(Vector2d.zero(), Vector2d(1.0, 0.0), Vector2d.one(), Vector2d(0.0, 1.0)),
+    var fill: Fill = Fill(Colour.WHITE),
+    var border: Border? = null
+): Widget() {
+    init {
+        require(uv.size == 4) { "Rectangle uv must have 4 coordinates." }
+    }
+
+    sealed class RectangleInstruction: UIInstruction {
+        data class Rectangle(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class StartRectangleBlock(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction()
+        data class EndRectangleBlock(val id: WidgetId) : RectangleInstruction()
+    }
+
+    override fun toInstruction(): List<UIInstruction> {
+        return listOf(RectangleInstruction.Rectangle(id, this))
+    }
+
+    fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+        return listOf(RectangleInstruction.StartRectangleBlock(id, this)) +
+            children +
+            RectangleInstruction.EndRectangleBlock(id)
+    }
+
+    fun startInstruction(): UIInstruction = RectangleInstruction.StartRectangleBlock(id, this)
+
+    fun endInstruction(): UIInstruction = RectangleInstruction.EndRectangleBlock(id)
+
+    fun with(position: Vector2d, size: Vector2d): Rectangle {
+        this.position = position
+        this.size = size
+        return this
+    }
+
+    fun withAnchor(anchor: Anchor): Rectangle {
+        this.anchor = anchor
+        return this
+    }
+
+    fun at(position: Vector2d): Rectangle {
+        this.position = position
+        return this
+    }
+
+    fun size(size: Vector2d): Rectangle {
+        this.size = size
+        return this
+    }
+
+    fun fill(fill: Fill): Rectangle {
+        this.fill = fill
+        return this
+    }
+
+    fun border(border: Border?): Rectangle {
+        this.border = border
+        return this
+    }
+
+    fun rotate(angle: Angle): Rectangle {
+        this.rotation = angle
+        return this
+    }
+
+    fun texture(texture: Handle<Texture>?): Rectangle {
+        this.texture = texture
+        return this
+    }
+
+    fun uv(coords: List<Vector2d>): Rectangle {
+        require(coords.size == 4) { "Rectangle uv must have 4 coordinates." }
+        this.uv = coords
+        return this
+    }
+}
+
+fun UIBuilder.rectangle(id: WidgetId = WidgetId(generateId().toLong()), block: Rectangle.() -> Unit = {}): Rectangle {
+    val rect = Rectangle(id = id).apply(block)
+    rect.toInstruction().forEach { instructions.add(it) }
+    return rect
+}
+
+fun UIBuilder.container(id: WidgetId, block: UIBuilder.(Rectangle) -> Unit) {
+    container(id, rectBlock = {}, content = block)
+}
+
+fun UIBuilder.container(
+    id: WidgetId,
+    rectBlock: Rectangle.() -> Unit = {},
+    content: UIBuilder.(Rectangle) -> Unit
+) {
+    val rect = Rectangle(id = id).apply(rectBlock)
+    instructions.add(rect.startInstruction())
+    this.content(rect)
+    instructions.add(rect.endInstruction())
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
new file mode 100644
index 0000000..955ee08
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/widgets/Row.kt
@@ -0,0 +1,70 @@
+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.styling.Anchor
+
+class Row(
+	override var id: WidgetId,
+	var anchor: Anchor = Anchor.TopLeft,
+	var position: Vector2d = Vector2d.zero(),
+	var spacing: Double = 8.0,
+): Widget() {
+	sealed class RowInstruction: UIInstruction {
+		data class StartRowBlock(val id: WidgetId, val row: com.dropbear.ui.widgets.Row) : RowInstruction()
+		data class EndRowBlock(val id: WidgetId) : RowInstruction()
+	}
+
+	override fun toInstruction(): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this), RowInstruction.EndRowBlock(id))
+	}
+
+	fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> {
+		return listOf(RowInstruction.StartRowBlock(id, this)) +
+			children +
+			RowInstruction.EndRowBlock(id)
+	}
+
+	fun startInstruction(): UIInstruction = RowInstruction.StartRowBlock(id, this)
+
+	fun endInstruction(): UIInstruction = RowInstruction.EndRowBlock(id)
+
+	fun at(position: Vector2d): Row {
+		this.position = position
+		return this
+	}
+
+	fun spacing(spacing: Double): Row {
+		this.spacing = spacing
+		return this
+	}
+
+	fun withAnchor(anchor: Anchor): Row {
+		this.anchor = anchor
+		return this
+	}
+}
+
+fun UIBuilder.row(id: WidgetId = WidgetId(generateId().toLong()), block: Row.() -> Unit = {}): Row {
+	val row = Row(id = id).apply(block)
+	row.toInstruction().forEach { instructions.add(it) }
+	return row
+}
+
+fun UIBuilder.row(id: WidgetId, content: UIBuilder.(Row) -> Unit) {
+	row(id, rowBlock = {}, content = content)
+}
+
+fun UIBuilder.row(
+	id: WidgetId,
+	rowBlock: Row.() -> Unit = {},
+	content: UIBuilder.(Row) -> Unit
+) {
+	val row = Row(id = id).apply(rowBlock)
+	instructions.add(row.startInstruction())
+	this.content(row)
+	instructions.add(row.endInstruction())
+}
\ No newline at end of file