Files
Basalt2/docs/guides/responsive-system.md

219 lines
6.5 KiB
Markdown

# 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 run
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.