Skip to content

Dynamic & Reactive Systems ​

Basalt provides three systems for creating dynamic, responsive UIs: the State System, the Responsive Plugin, and the Reactive Plugin. Each serves a different purpose and can be combined for maximum flexibility.


State System ​

The state system allows elements to have conditional states that automatically activate based on custom logic. This is the foundation that powers the responsive plugin.

Conditional States ​

Register states that evaluate conditions and automatically activate/deactivate:

lua
-- Using a function
-- When using functions, you need to manually specify which properties to observe
local label = main:addLabel()
    :registerResponsiveState("small", function(self)
        return self.parent:getWidth() < 25
    end, {
        observe = { 
            {main, "width"}  -- Format: {element, "property"}
        },
    })
    :setTextState("small", "Compact")
    :setText("Full Text")

-- Using a string expression (automatic dependency detection)
-- String expressions automatically detect and observe referenced properties
local sidebar = main:addFrame()
    :registerResponsiveState("collapsed", "parent.width < 30", 100)
    :setWidthState("collapsed", 5)
    :setWidthState("default", 15)
    :setY(15)

String Expressions ​

String expressions automatically parse and detect dependencies - no manual observe needed:

lua
-- Dependencies are auto-detected from the expression
element:registerResponsiveState("portrait", "parent.width < parent.height", 100)

You can optionally add extra dependencies that aren't in the expression:

lua
-- Auto-detects parent.width, but also observes otherElement.text
element:registerResponsiveState("complex", "parent.width < 30", {
    priority = 100,
    observe = {
        {otherElement, "text"}  -- Additional dependency
    }
})

Behind the scenes: The system parses expressions like "parent.width < 30", extracts property references (parent.width), and automatically sets up observers for reactive updates.

Manual State Control ​

You can also control states manually:

lua
element:setState("customState", 100)  -- Activate with priority 100
element:unsetState("customState")     -- Deactivate
element:hasState("customState")       -- Check if active
element:getActiveStates()             -- Get all active states sorted by priority

Responsive Plugin ​

The responsive plugin builds on the state system to create layouts that adapt to parent size or other conditions. It provides a fluent builder API.

Basic Responsive Layout ​

lua
local sidebar = main:addFrame()
    :responsive()
        :when("parent.width < 15")
            :apply({ width = 10, background = colors.gray })
        :when("parent.width >= 40")
            :apply({ width = 25, background = colors.lightGray })
        :otherwise({ width = 15 })

Advanced Conditions ​

String expressions support math operations and can reference any element property:

lua
local dynamicFrame = frame:addFrame()
    :responsive()
        :when("parent.width < parent.height")
            :apply({ width = "parent.width * 0.9", height = 10 })
        :otherwise({ width = 20, height = "parent.height * 0.9" })

Behind the scenes: The responsive plugin automatically detects dependencies from expressions (like parent.width) and sets up observers for reactive updates.


Reactive Plugin ​

The reactive plugin allows property values themselves to be dynamic expressions that automatically update when dependencies change.

Basic Usage ​

lua
-- Center a label horizontally
local label = frame:addLabel()
    :setText("Centered")
    :setX("{parent.width / 2 - self.width / 2}")

-- Progress bar that takes 80% of parent width
frame:addProgressBar()
    :setWidth("{parent.width * 0.8}")
    :setX("{parent.width * 0.1}")

Available Variables ​

  • self - The current element
  • parent - The parent element
  • elementName - Named elements (e.g., mySlider.value)

Linked Properties ​

lua
local slider = frame:addSlider("volumeSlider")
    :setPosition(2, 2)

local label = frame:addLabel()
    :setText("{volumeSlider.value}")  -- Text updates with slider
    :setX("{volumeSlider.x + volumeSlider.width + 2}")

Dynamic Sizing ​

lua
local label = frame:addLabel()
    :setText("Dynamic width")
    :setWidth("{#self.text + 2}")  -- Width = text length + padding

Combining Systems ​

You can combine all three systems for maximum flexibility:

lua
local frame = main:addFrame()
    :setWidth("{parent.width * 0.8}")  -- Reactive: 80% of parent
    :responsive()                       -- Responsive: breakpoints
        :when("parent.width < 30")
            :apply({ background = colors.gray })
        :otherwise({ background = colors.lightGray })
    :setPropertyState("background", "hover", colors.white)  -- State: hover effect

The responsive and reactive plugins work together - you can use reactive expressions within :apply():

lua
local element = main:addLabel()
    :responsive()
        :when("parent.width < 30")
            :apply({ text = "Small", x = "{parent.width - self.width}" })  -- Reactive expression
        :otherwise({ text = "Large", x = 5 })

Practical Example: Adaptive Layout ​

A common use case is creating a layout that adapts between side-by-side and stacked views based on screen width:

lua
local basalt = require("basalt")
local main = basalt.getMainFrame()

-- Left container
local rightContainer = main:addFrame()
    :setSize(20, 10)
    :setBackground(colors.green)
    :responsive()
        :when("parent.width >= 45")  -- Wide: positioned next to left
            :apply({
                x = 24,
                y = 2,
                width = 20
            })
        :otherwise({
                x = 2,
                y = 13,
                width = "{parent.width - 3}"
            })  -- Narrow: positioned below left
    :done()

rightContainer:addLabel()
    :setText("Right Panel")
    :setPosition(2, 2)

-- Status label showing current layout mode
local statusLabel = main:addLabel()
    :setPosition(2, 24)
    :responsive()
        :when("parent.width >= 45")
            :apply({ text = "Layout: Side by Side", foreground = colors.lime })
        :otherwise({ text = "Layout: Stacked", foreground = colors.orange })
    :done()
basalt.run()

When the main frame is wide (≥45 characters), the containers appear side by side. When it's narrow, they stack vertically - and the status label updates to reflect the current mode.

Released under the MIT License.