Log In  


PicoUI

PicoUI is a declarative UI framework inspired by Apple's SwiftUI, designed to simplify UI development for Picotron. It provides a reactive state management system and a set of view components for constructing interfaces in a straightforward and maintainable way.

By using PicoUI, you can build complex user interfaces without dealing with the complexities of custom UI code based on raw offsets. PicoUI makes it easier to refactor and modify your codebase by automatically updating the UI when the underlying state changes.

Views

PicoUI provides several view components, each designed for a specific purpose:

TextView

Displays text with a specified color.

Arguments:

  • text (string or Observable): The text to display.
  • color (number or Observable): The color of the text (default: 7).

SprView

Displays a sprite image.

Arguments:

  • sprite (number or Observable): The index of the sprite to display.

RectView

Displays a filled rectangle with specified dimensions and color.

Arguments:

  • w (number or Observable): The width of the rectangle.
  • h (number or Observable): The height of the rectangle.
  • color (number or Observable): The color of the rectangle (default: 7).

VStack

Arranges child views vertically with optional spacing.

Arguments:

  • spacing (number or Observable): The vertical spacing between child views (default: 0).

Use the append(child) method to add child views.


HStack

Arranges child views horizontally with optional spacing.

Arguments:

  • spacing (number or Observable): The horizontal spacing between child views (default: 0).

Use the append(child) method to add child views.


PaddingView

Adds padding around a single child view.

Arguments:

  • padding_x (number or Observable): The horizontal padding (default: 0).
  • padding_y (number or Observable): The vertical padding (default: 0).

Usage:

local padding_view = PaddingView:new(padding_x, padding_y)
padding_view:set_child(child_view)

BackgroundView

Draws a background color behind a single child view.

Arguments:

  • color (number or Observable): The background color (default: 7).

Usage:

local background_view = BackgroundView:new(color)
background_view:set_child(child_view)

BorderView

Draws a border around a single child view with optional padding.

Arguments:

  • border_color (number or Observable): The color of the border (default: 7).
  • padding_x (number or Observable): The horizontal padding inside the border (default: 0).
  • padding_y (number or Observable): The vertical padding inside the border (default: 0).

Usage:

local border_view = BorderView:new(border_color, padding_x, padding_y)
border_view:set_child(child_view)

Usage

State Management

Any view argument can be either a static value or an Observable value. Observables should be stored in a state table accessible to the _update() function. For example:

state = {
    mem_text = Observable:new("0 KiB"),
    fps_text = Observable:new("0 FPS"),
    fps_color = Observable:new(perf_colors.low),
    cpu_text = Observable:new("0.0 CPU"),
}

Setting Up the Interface

Include PicoUI in your main script:

include "PicoUI.lua"

Build your interface by creating the innermost views first and then composing them into outer views. For example:

-- Create text views for performance counters
local fps_counter = TextView:new(state.fps_text, state.fps_color)
local cpu_counter = TextView:new(state.cpu_text, 7)
local mem_counter = TextView:new(state.mem_text, 12)

-- Arrange the counters vertically with spacing
local perf_container = VStack:new(2)
perf_container:append(fps_counter)
perf_container:append(cpu_counter)
perf_container:append(mem_counter)

-- Add padding around the container
local perf_container_padding = PaddingView:new(4, 4)
perf_container_padding:set_child(perf_container)

-- Add a background to the container
local perf_bg = BackgroundView:new(32)
perf_bg:set_child(perf_container_padding)

-- Add a border around the entire view
root = BorderView:new(17, 1, 1)
root:set_child(perf_bg)

Use the append(child) method for views that can contain multiple children (like VStack, HStack), and set_child(child) for views that hold a single child (like PaddingView, BackgroundView, BorderView). Concrete views like TextView or SprView cannot hold children.

Initialize the view hierarchy in the _init() function, and ensure the root view is accessible globally.

Updating and Drawing

In the _update() function, update your state values and then call root:update() to refresh the views:

function _update()
    -- Update state values
    state.fps_text:set(string.format("%.4f FPS", stat(7)))
    state.cpu_text:set(string.format("%.5f CPU", stat(1)))
    state.mem_text:set(string.format("%7i KiB", stat(0)))
    -- Update the root view
    root:update()
end

In the _draw() function, clear the screen and draw the root view at the desired position:

function _draw()
    cls(0)
    root:draw()(12, 12)
end

Full Example

Here's a complete example demonstrating the usage of PicoUI. Ensure that you have a file named PicoUI.lua in the same directory with the PicoUI code.

-- Include the PicoUI framework
include "PicoUI.lua"

-- Define performance colors
perf_colors = {
    low = 8,
    med = 9,
    high = 10
}

-- Initialize the observable state
state = {
    mem_text = Observable:new("0 KiB"),
    fps_text = Observable:new("0 FPS"),
    fps_color = Observable:new(perf_colors.low),
    cpu_text = Observable:new("0.0 CPU"),
}

function _init()
    -- Create text views for performance counters
    local fps_counter = TextView:new(state.fps_text, state.fps_color)
    local cpu_counter = TextView:new(state.cpu_text, 7)
    local mem_counter = TextView:new(state.mem_text, 12)

    -- Arrange the counters vertically with spacing
    local perf_container = VStack:new(2)
    perf_container:append(fps_counter)
    perf_container:append(cpu_counter)
    perf_container:append(mem_counter)

    -- Add padding around the container
    local perf_container_padding = PaddingView:new(4, 4)
    perf_container_padding:set_child(perf_container)

    -- Add a background to the container
    local perf_bg = BackgroundView:new(32)
    perf_bg:set_child(perf_container_padding)

    -- Add a border around the entire view
    root = BorderView:new(17, 1, 1)
    root:set_child(perf_bg)
end

function _update()
    local fps = stat(7)
    local cpu = stat(1)
    local mem = stat(0)
    local perf = "low"

    if fps > 10 then
        if fps < 30 then
            perf = "med"
        else
            perf = "high"
        end
    end

    state.fps_text:set(string.format("%.4f FPS", fps))
    state.fps_color:set(perf_colors[perf] or 7)
    state.cpu_text:set(string.format("%.5f CPU", cpu))
    state.mem_text:set(string.format("%7i KiB", mem))

    -- Update the root view
    root:update()
end

function _draw()
    cls(0)
    root:draw()(12, 12)
end

PicoUI Code

The full PicoUI framework code is provided below.

-- Observable class: Manages a value and notifies observers when it changes
Observable = {}
Observable.__index = Observable

function Observable:new(value)
    local o = {
        value = value,
        observers = {}
    }
    setmetatable(o, self)
    return o
end

function Observable:set(value)
    self.value = value
    self:notify()
end

function Observable:get()
    return self.value
end

function Observable:observe(observer)
    table.insert(self.observers, observer)
end

function Observable:notify()
    for _, observer in ipairs(self.observers) do
        observer(self.value)
    end
end

-- Base class for concrete view components
ConcreteView = {}
ConcreteView.__index = ConcreteView

function ConcreteView:new()
    local o = {}
    setmetatable(o, self)
    o._boundProperties = {}
    return o
end

function ConcreteView:bindProperty(propName, value)
    if type(value) == "table" and value.observe then
        self[propName] = value:get()
        value:observe(function(new_value)
            self[propName] = new_value
            self:update()
        end)
        table.insert(self._boundProperties, propName)
    else
        self[propName] = value
    end
end

function ConcreteView:update()
    -- Placeholder
end

function ConcreteView:draw()
    -- Placeholder
end

function ConcreteView:width()
    return self._width or 0
end

function ConcreteView:height()
    return self._height or 0
end

-- TextView class: Displays text with a given color
TextView = {}
TextView.__index = TextView
setmetatable(TextView, {__index = ConcreteView})

function TextView:new(text, color)
    local o = ConcreteView:new()
    setmetatable(o, self)
    o:bindProperty("text", text or "")
    o:bindProperty("color", color or 7)
    o:update()
    return o
end

function TextView:update()
    self._width = #(self.text or "") * 5
    self._height = 9
end

function TextView:draw()
    return function(x, y)
        clip(x, y, x + self:width(), y + self:height())
        print(self.text, x, y, self.color)
        clip()
    end
end

-- SprView class: Displays a sprite
SprView = {}
SprView.__index = SprView
setmetatable(SprView, {__index = ConcreteView})

function SprView:new(sprite)
    local o = ConcreteView:new()
    setmetatable(o, self)
    o:bindProperty("sprite", sprite)
    o:update()
    return o
end

function SprView:update()
    local tex = get_spr(self.sprite)
    self._width = tex:width()
    self._height = tex:height()
end

function SprView:draw()
    return function(x, y)
        clip(x, y, x + self:width(), y + self:height())
        spr(self.sprite, x, y)
        clip()
    end
end

-- RectView class: Displays a filled rectangle
RectView = {}
RectView.__index = RectView
setmetatable(RectView, {__index = ConcreteView})

function RectView:new(w, h, color)
    local o = ConcreteView:new()
    setmetatable(o, self)
    o:bindProperty("_width", w or 0)
    o:bindProperty("_height", h or 0)
    o:bindProperty("color", color or 7)
    o:update()
    return o
end

function RectView:update()
    -- Width and height are set via bindProperty
end

function RectView:draw()
    return function(x, y)
        clip(x, y, x + self:width(), y + self:height())
        rectfill(x, y, x + self:width() - 1, y + self:height() - 1, self.color)
        clip()
    end
end

-- Base class for composite views that can contain child views
View = {}
View.__index = View

function View:new()
    local o = {}
    setmetatable(o, self)
    o.children = {}
    o.spacing = 0
    o._boundProperties = {}
    return o
end

function View:bindProperty(propName, value)
    if type(value) == "table" and value.observe then
        self[propName] = value:get()
        value:observe(function(new_value)
            self[propName] = new_value
            self:update()
        end)
        table.insert(self._boundProperties, propName)
    else
        self[propName] = value
    end
end

function View:append(v)
    table.insert(self.children, v)
end

function View:update()
    -- Placeholder
end

function View:width()
    return self._width or 0
end

function View:height()
    return self._height or 0
end

-- VStack class: Arranges child views vertically with optional spacing
VStack = {}
VStack.__index = VStack
setmetatable(VStack, {__index = View})

function VStack:new(spacing)
    local o = View:new()
    setmetatable(o, self)
    o:bindProperty("spacing", spacing or 0)
    o:update()
    return o
end

function VStack:append(v)
    table.insert(self.children, v)
end

function VStack:update()
    self._width = 0
    self._height = 0
    local total_spacing = self.spacing * math.max(0, #self.children - 1)
    for _, child in ipairs(self.children) do
        child:update()
        self._width = math.max(self._width, child:width())
        self._height = self._height + child:height()
    end
    self._height = self._height + total_spacing
end

function VStack:draw()
    return function(x, y)
        local current_y = y
        for _, child in ipairs(self.children) do
            local draw_func = child:draw()
            draw_func(x, current_y)
            current_y = current_y + child:height() + self.spacing
        end
    end
end

-- HStack class: Arranges child views horizontally with optional spacing
HStack = {}
HStack.__index = HStack
setmetatable(HStack, {__index = View})

function HStack:new(spacing)
    local o = View:new()
    setmetatable(o, self)
    o:bindProperty("spacing", spacing or 0)
    o:update()
    return o
end

function HStack:append(v)
    table.insert(self.children, v)
end

function HStack:update()
    self._width = 0
    self._height = 0
    local total_spacing = self.spacing * math.max(0, #self.children - 1)
    for _, child in ipairs(self.children) do
        child:update()
        self._width = self._width + child:width()
        self._height = math.max(self._height, child:height())
    end
    self._width = self._width + total_spacing
end

function HStack:draw()
    return function(x, y)
        local current_x = x
        for _, child in ipairs(self.children) do
            local draw_func = child:draw()
            draw_func(current_x, y)
            current_x = current_x + child:width() + self.spacing
        end
    end
end

-- PaddingView class: Adds padding around a single child view
PaddingView = {}
PaddingView.__index = PaddingView
setmetatable(PaddingView, {__index = View})

function PaddingView:new(padding_x, padding_y)
    local o = View:new()
    setmetatable(o, self)
    o:bindProperty("padding_x", padding_x or 0)
    o:bindProperty("padding_y", padding_y or 0)
    o.child = nil
    o:update()
    return o
end

function PaddingView:set_child(child)
    self.child = child
end

function PaddingView:update()
    if self.child then
        self.child:update()
        self._width = self.child:width() + 2 * self.padding_x
        self._height = self.child:height() + 2 * self.padding_y
    else
        self._width = 0
        self._height = 0
    end
end

function PaddingView:draw()
    return function(x, y)
        if self.child then
            local draw_func = self.child:draw()
            draw_func(x + self.padding_x, y + self.padding_y)
        end
    end
end

-- BackgroundView class: Draws a background color behind a single child view
BackgroundView = {}
BackgroundView.__index = BackgroundView
setmetatable(BackgroundView, {__index = View})

function BackgroundView:new(color)
    local o = View:new()
    setmetatable(o, self)
    o:bindProperty("color", color or 7)
    o.child = nil
    o:update()
    return o
end

function BackgroundView:set_child(child)
    self.child = child
end

function BackgroundView:update()
    if self.child then
        self.child:update()
        self._width = self.child:width()
        self._height = self.child:height()
    else
        self._width = 0
        self._height = 0
    end
end

function BackgroundView:draw()
    return function(x, y)
        rectfill(x, y, x + self:width() - 1, y + self:height() - 1, self.color)
        if self.child then
            local draw_func = self.child:draw()
            draw_func(x, y)
        end
    end
end

-- BorderView class: Draws a border around a single child view, with optional padding
BorderView = {}
BorderView.__index = BorderView
setmetatable(BorderView, {__index = View})

function BorderView:new(border_color, padding_x, padding_y)
    local o = View:new()
    setmetatable(o, self)
    o:bindProperty("border_color", border_color or 7)
    o:bindProperty("padding_x", padding_x or 0)
    o:bindProperty("padding_y", padding_y or 0)
    o.child = nil
    o:update()
    return o
end

function BorderView:set_child(child)
    self.child = child
end

function BorderView:update()
    if self.child then
        self.child:update()
        self._width = self.child:width() + 2 * self.padding_x
        self._height = self.child:height() + 2 * self.padding_y
    else
        self._width = 0
        self._height = 0
    end
end

function BorderView:draw()
    return function(x, y)
        rectfill(x, y, x + self:width() - 1, y + self:height() - 1, self.border_color)
        if self.child then
            local draw_func = self.child:draw()
            draw_func(x + self.padding_x, y + self.padding_y)
        end
    end
end


I plan to add more views and make things less complex (eg needing to nest 3 views to add background, padding, and border). I'd love any feedback or suggestions!

8


PicoUI Update 1.1

New features

  • More declarative interface structuring
  • Properties!!! (No more background or padding views)

Example code

include "view.lua"

--  @Mark[[ Constants ]]
w = 480
h = 270

-- Define performance colors
perf_colors = {
    low = 8,
    med = 9,
    high = 10
}

-- Initialize the observable state
state = {
    mem_text = Observable:new("0 KiB"),
    fps_text = Observable:new("0 FPS"),
    fps_color = Observable:new(perf_colors.low),
    cpu_text = Observable:new("0.0 CPU"),
}

function _init()
    -- Build the UI using the new declarative syntax with universal properties
    root = VStack{
        Text{state.fps_text}{color = state.fps_color},
        Text{state.cpu_text}{color = 7},
        Text{state.mem_text}{color = 12},
    }{
        align = "center",
        spacing = 2,
        padding_x = 4,
        padding_y = 4,
        background_color = 32,
        border_color = 17,
        border_x = 1,
        border_y = 1
    }
end

function _update()
    local fps = stat(7)
    local cpu = stat(1)
    local mem = stat(0)
    local perf = "low"

    if fps > 10 then
        if fps < 30 then
            perf = "med"
        else
            perf = "high"
        end
    end

    state.fps_text:set(string.format("%s FPS", fps))
    state.fps_color:set(perf_colors[perf] or 7)
    state.cpu_text:set(string.format("%.2f", cpu*100) .. " CPU")
    state.mem_text:set(string.format("%7i KiB", mem))

    -- Update the root view
    root:update()
end

function _draw()
    cls(0)
    root:draw()(12, 12)
end

I would love to add usage, but I am somewhat busy right now. Take a look at the example for reference.

PicoUI code

-- Observable class: Manages a value and notifies observers when it changes
Observable = {}
Observable.__index = Observable

-- Creates a new Observable instance with an initial value
function Observable:new(value)
    local o = {
        value = value,   -- Stores the current value
        observers = {}   -- List of observers to notify on value change
    }
    setmetatable(o, self)
    return o
end

-- Sets a new value and notifies all observers of the change
function Observable:set(value)
    self.value = value  -- Update the internal value
    self:notify()       -- Notify observers that the value has changed
end

-- Gets the current value of the observable
function Observable:get()
    return self.value   -- Return the stored value
end

-- Registers an observer function to be notified when the value changes
function Observable:observe(observer)
    table.insert(self.observers, observer) -- Add the observer to the list
end

-- Notifies all registered observers by calling their functions with the updated value
function Observable:notify()
    for _, observer in ipairs(self.observers) do
        observer(self.value)  -- Call each observer with the current value
    end
end

-- Base class for concrete view components, managing view properties and behavior
ConcreteViewClass = {}
ConcreteViewClass.__index = ConcreteViewClass

-- Creates a new view component with optional properties
function ConcreteViewClass:new(props)
    local o = {}
    setmetatable(o, self)
    o._boundProperties = {}  -- Keeps track of properties bound to observables
    props = props or {}
    o:bindUniversalProperties(props) -- Bind universal view properties
    o:update()                       -- Trigger update to initialize view
    return o
end

-- Binds universal properties (padding, border, background color, etc.)
function ConcreteViewClass:bindUniversalProperties(props)
    -- Universal properties that apply to all view components
    self:bindProperty("padding_x", props.padding_x or 0)
    self:bindProperty("padding_y", props.padding_y or 0)
    self:bindProperty("border_x", props.border_x or 0)
    self:bindProperty("border_y", props.border_y or 0)
    self:bindProperty("background_color", props.background_color)
    self:bindProperty("border_color", props.border_color)
end

-- Binds a property to an observable or sets a static value
function ConcreteViewClass:bindProperty(propName, value)
    if type(value) == "table" and value.observe then
        -- If the value is observable, bind and update the property when it changes
        self[propName] = value:get() -- Initialize with the current value
        value:observe(function(new_value)
            self[propName] = new_value -- Update property with new value
            self:update()              -- Redraw the component
        end)
        table.insert(self._boundProperties, propName) -- Track bound properties
    else
        -- Static value: set the property directly
        self[propName] = value
    end
end

-- Placeholder function to be implemented by subclasses for updating the view
function ConcreteViewClass:update()
    -- To be implemented by subclasses
end

-- Calculates and returns the width of the view including padding and border
function ConcreteViewClass:width()
    local width = self._width or 0
    width = width + 2 * (self.padding_x or 0) + 2 * (self.border_x or 0) -- Add padding and border
    return width
end

-- Calculates and returns the height of the view including padding and border
function ConcreteViewClass:height()
    local height = self._height or 0
    height = height + 2 * (self.padding_y or 0) + 2 * (self.border_y or 0) -- Add padding and border
    return height
end

-- Draws the view, including the border and background, and calls drawContent for specific content
function ConcreteViewClass:draw()
    return function(x, y)
        local bx = self.border_x or 0
        local by = self.border_y or 0
        local px = self.padding_x or 0
        local py = self.padding_y or 0

        -- Draw the border if defined
        if self.border_color then
            rectfill(x, y, x + self:width() - 1, y + self:height() - 1, self.border_color)
        end

        -- Draw the background if defined
        if self.background_color then
            local bg_x = x + bx
            local bg_y = y + by
            local bg_w = self:width() - 2 * bx
            local bg_h = self:height() - 2 * by
            rectfill(bg_x, bg_y, bg_x + bg_w - 1, bg_y + bg_h - 1, self.background_color)
        end

        -- Draw the content inside the view
        local content_x = x + bx + px
        local content_y = y + by + py
        self:drawContent()(content_x, content_y) -- Call drawContent to render specific content
    end
end

-- Placeholder function to be implemented by subclasses for drawing content
function ConcreteViewClass:drawContent()
    return function(x, y)
        -- Placeholder function for content
    end
end

-- ViewClass serves as a base for composite views that can hold child views
ViewClass = {}
ViewClass.__index = ViewClass

-- Creates a new composite view with optional properties
function ViewClass:new(props)
    local o = {}
    setmetatable(o, self)
    o.children = {}          -- List to store child views
    o._boundProperties = {}   -- Keeps track of bound properties
    props = props or {}
    o:bindUniversalProperties(props) -- Bind universal properties
    o:update()                      -- Trigger update to layout the view
    return o
end

-- Binds universal properties for composite views
function ViewClass:bindUniversalProperties(props)
    self:bindProperty("padding_x", props.padding_x or 0)
    self:bindProperty("padding_y", props.padding_y or 0)
    self:bindProperty("border_x", props.border_x or 0)
    self:bindProperty("border_y", props.border_y or 0)
    self:bindProperty("background_color", props.background_color)
    self:bindProperty("border_color", props.border_color)
    self:bindProperty("spacing", props.spacing or 0) -- Spacing between child views
end

-- Binds a property, similar to ConcreteViewClass
function ViewClass:bindProperty(propName, value)
    if type(value) == "table" and value.observe then
        self[propName] = value:get()
        value:observe(function(new_value)
            self[propName] = new_value
            self:update()
        end)
        table.insert(self._boundProperties, propName)
    else
        self[propName] = value
    end
end

-- Appends a child view to the composite view
function ViewClass:append(v)
    table.insert(self.children, v) -- Add child view to the list of children
end

-- Placeholder function to be implemented by subclasses for updating the view layout
function ViewClass:update()
    -- To be implemented by subclasses
end

-- Calculates and returns the width of the composite view, accounting for children
function ViewClass:width()
    local width = self._width or 0
    width = width + 2 * (self.padding_x or 0) + 2 * (self.border_x or 0) -- Add padding and border
    return width
end

-- Calculates and returns the height of the composite view, accounting for children
function ViewClass:height()
    local height = self._height or 0
    height = height + 2 * (self.padding_y or 0) + 2 * (self.border_y or 0) -- Add padding and border
    return height
end

-- Draws the composite view and its child views
function ViewClass:draw()
    return function(x, y)
        local bx = self.border_x or 0
        local by = self.border_y or 0
        local px = self.padding_x or 0
        local py = self.padding_y or 0

        -- Draw the border if defined
        if self.border_color then
            rectfill(x, y, x + self:width() - 1, y + self:height() - 1, self.border_color)
        end

        -- Draw the background if defined
        if self.background_color then
            local bg_x = x + bx
            local bg_y = y + by
            local bg_w = self:width() - 2 * bx
            local bg_h = self:height() - 2 * by
            rectfill(bg_x, bg_y, bg_x + bg_w - 1, bg_y + bg_h - 1, self.background_color)
        end

        -- Draw content, including children
        local content_x = x + bx + px
        local content_y = y + by + py
        self:drawContent()(content_x, content_y) -- Draw child views or content
    end
end

-- Placeholder function for drawing content, implemented by subclasses
function ViewClass:drawContent()
    return function(x, y)
        -- Placeholder for subclasses
    end
end

-- TextViewClass displays text within a view
TextViewClass = {}
TextViewClass.__index = TextViewClass
setmetatable(TextViewClass, {__index = ConcreteViewClass})

-- Creates a new TextViewClass with text and color properties
function TextViewClass:new(props)
    local o = ConcreteViewClass:new(props) -- Call base class constructor
    setmetatable(o, self)
    props = props or {}
    o:bindProperty("text", props.text or "")   -- Bind text property
    o:bindProperty("color", props.color or 7)  -- Bind color property
    -- Bind any additional properties passed in props
    for k, v in pairs(props) do
        if k ~= "text" and k ~= "color" then
            o:bindProperty(k, v)
        end
    end
    o:update() -- Trigger update to adjust dimensions
    return o
end

-- Updates text view dimensions based on text length
function TextViewClass:update()
    self._width = #(self.text or "") * 5  -- Assume 5 pixels per character
    self._height = 8  -- Assume fixed height of 8 pixels for text
end

-- Draws the text content inside the view
function TextViewClass:drawContent()
    return function(x, y)
        clip(x, y, x + self._width, y + self._height) -- Clip to view dimensions
        print(self.text, x, y, self.color)            -- Draw the text
        clip()                                        -- Reset clipping
    end
end

-- Text component helper function to create TextViewClass instances
function Text(children)
    return function(props)
        local text = children[1] -- Use the first child as the text
        props = props or {}
        props.text = text         -- Set the text property
        local o = TextViewClass:new(props) -- Create new TextViewClass instance
        return o
    end
end

-- VStackClass: Arranges child views vertically with optional spacing and alignment
VStackClass = {}
VStackClass.__index = VStackClass
setmetatable(VStackClass, {__index = ViewClass})

function VStackClass:new(props)
    local o = ViewClass:new(props)
    setmetatable(o, self)
    -- Accept 'align' property with default 'left'
    o:bindProperty("align", props.align or "left")
    o:update()
    return o
end

function VStackClass:update()
    self._width = 0
    self._height = 0
    local total_spacing = self.spacing * math.max(0, #self.children - 1)

    -- Update children and determine stack dimensions
    for _, child in ipairs(self.children) do
        child:update()
        self._width = math.max(self._width, child:width())
        self._height = self._height + child:height()
    end
    self._height = self._height + total_spacing
end

function VStackClass:drawContent()
    return function(x, y)
        local current_y = y
        for _, child in ipairs(self.children) do
            local draw_func = child:draw()
            local child_width = child:width()
            local offset_x = 0

            -- Adjust x-position based on alignment
            if self.align == "center" then
                offset_x = (self._width - child_width) / 2
            elseif self.align == "right" then
                offset_x = self._width - child_width
            elseif self.align == "left" then
                offset_x = 0
            else
                -- Default to 'left' alignment
                offset_x = 0
            end

            draw_func(x + offset_x, current_y)
            current_y = current_y + child:height() + self.spacing
        end
    end
end

function VStack(children)
    return function(props)
        local o = VStackClass:new(props)
        for _, child in ipairs(children) do
            o:append(child)
        end
        return o
    end
end

-- HStackClass: Arranges child views horizontally with optional spacing and alignment
HStackClass = {}
HStackClass.__index = HStackClass
setmetatable(HStackClass, {__index = ViewClass})

function HStackClass:new(props)
    local o = ViewClass:new(props)
    setmetatable(o, self)
    -- Accept 'align' property with default 'top'
    o:bindProperty("align", props.align or "top")
    o:update()
    return o
end

function HStackClass:update()
    self._width = 0
    self._height = 0
    local total_spacing = self.spacing * math.max(0, #self.children - 1)

    -- Update children and determine stack dimensions
    for _, child in ipairs(self.children) do
        child:update()
        self._width = self._width + child:width()
        self._height = math.max(self._height, child:height())
    end
    self._width = self._width + total_spacing
end

function HStackClass:drawContent()
    return function(x, y)
        local current_x = x
        for _, child in ipairs(self.children) do
            local draw_func = child:draw()
            local child_height = child:height()
            local offset_y = 0

            -- Adjust y-position based on alignment
            if self.align == "center" then
                offset_y = (self._height - child_height) / 2
            elseif self.align == "bottom" then
                offset_y = self._height - child_height
            elseif self.align == "top" then
                offset_y = 0
            else
                -- Default to 'top' alignment
                offset_y = 0
            end

            draw_func(current_x, y + offset_y)
            current_x = current_x + child:width() + self.spacing
        end
    end
end

function HStack(children)
    return function(props)
        local o = HStackClass:new(props)
        for _, child in ipairs(children) do
            o:append(child)
        end
        return o
    end
end


I plan to add more features soon:

  • Easier assignments to reactive state
  • Reactive state predicates (eg. if v >= 100 then 8)
  • More layout like
    • Defined dimensions


[Please log in to post a comment]