FlexLöve v0.2.2
A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.
FlexLöve is a lightweight, flexible GUI library for Löve2D that implements a flexbox-based layout system. It provides a simple way to create and manage UI elements with automatic layout calculations, animations, theming, and responsive design. Immediate mode support is now included (retained is default).
⚠️ Development Status
This library is under active development. While many features are functional, some aspects may change or have incomplete/broken implementations.
Coming Soon
The following features are currently being actively developed:
- Animations: Simple to use animations for UI transitions and effects
- Generic Image Support: Enhanced image rendering capabilities and utilities
Features
- Flexbox Layout: Modern flexbox layouts for UI elements with full flex properties
- Grid Layout: CSS-like (but simplified) grid system for structured layouts
- Element Management: Hierarchical element structures with automatic sizing
- Interactive Elements: Buttons with click detection, event system, and callbacks
- Theme System: 9-patch (NinePatch) theming with state support (normal, hover, pressed, disabled)
- Android 9-Patch Auto-Parsing: Automatic parsing of *.9.png files with multi-region support
- Animations: Built-in animation support for transitions and effects
- Responsive Design: Automatic resizing with viewport units (vw, vh, %)
- Color Handling: Utility classes for managing colors in various formats
- Text Rendering: Flexible text display with alignment and auto-scaling
- Corner Radius: Rounded corners with individual corner control
- Advanced Positioning: Absolute, relative, flex, and grid positioning modes
Quick Start
Add the modules directory and FlexLove.lua into your project. I recommend going to the
RELEASES page, but you can clone the repo if you want the latest features, but bugs are more
likely.
local FlexLove = require("FlexLove")
-- Initialize with base scaling and theme
FlexLove.init({
baseScale = { width = 1920, height = 1080 },
theme = "space"
immediateMode = true -- Optional: enable immediate mode (default: false)
})
-- Create a button with flexbox layout
local button = FlexLove.new({
width = "20vw",
height = "10vh",
backgroundColor = Color.new(0.2, 0.2, 0.8, 1),
text = "Click Me",
textSize = "md",
themeComponent = "button",
onEvent = function(element, event)
print("Button clicked!")
end
})
-- In your love.update and love.draw:
function love.update(dt)
FlexLove.update(dt)
end
function love.draw()
FlexLove.draw()
end
Documentation
Complete API reference with all classes, methods, and properties is available on GitHub Pages. The documentation includes:
- Searchable sidebar navigation
- Syntax-highlighted code examples
- Version selector (access docs for previous versions)
- Detailed parameter and return value descriptions
Documentation Versions
Access documentation for specific versions:
- Latest: https://mikefreno.github.io/FlexLove/api.html
- Specific version:
https://mikefreno.github.io/FlexLove/versions/v0.2.0/api.html
Use the version dropdown in the documentation header to switch between versions.
API Conventions
Method Patterns
- Constructors:
ClassName.new(props)→ instance - Static Methods:
ClassName.methodName(args)→ result - Instance Methods:
instance:methodName(args)→ result - Getters:
instance:getPropertyName()→ value - Internal Fields:
_fieldName(private, do not access directly) - Error Handling: Constructors throw errors, utility functions return nil + error string
Return Value Patterns
- Single Success: return value
- Success/Failure: return result, errorMessage (nil on success for error)
- Multiple Values: return value1, value2 (documented in @return)
- Constructors: Always return instance (never nil)
Core Concepts
Immediate Mode vs Retained Mode
FlexLöve supports both immediate mode and retained mode UI paradigms, giving you flexibility in how you structure your UI code:
Retained Mode (Default)
In retained mode, you create elements once and they persist across frames. The library manages the element hierarchy, but you must manage changes in element state by .
local someGameState = true
-- Create elements once (e.g., in love.load)
local button1 = FlexLove.new({
text = "Button 1",
disabled = someGameState,
onEvent = function() print("Clicked!") end
})
-- ... other things ... --
local someGameState = false -- button1 will not change its disabled state, you need a way to update the element
local button2 = FlexLove.new({
text = "Click to activate button 1",
onEvent = function(_, event)
if event.type == "release" then -- only fire on mouse release
button1.disabled = false -- this will actual update the element
end
end
})
-- ... other things ... --
local someGameState = false -- button1 will not change its disabled state, you need a way to update the element
local button2 = FlexLove.new({
text = "Click to activate button 1",
onEvent = function(_, event)
if event.type == "release" then -- only fire on mouse release
button1.disabled = false -- this will actual update the element
end
end
})
Best for:
- Complex UIs with many persistent elements
- Elements that maintain state over time
- UIs that don't change frequently
Immediate Mode
In immediate mode, you recreate UI elements every frame based on your application state. This approach can be simpler for dynamic UIs that change frequently. There is of course some overhead for this, which is why it is not the default behavior.
-- Recreate UI every frame
local someGameState = true
-- Create elements once (e.g., in love.load)
local button1 = FlexLove.new({
text = "Button 1",
disabled = someGameState,
onEvent = function() print("Clicked!") end
})
-- ... other things ... --
local someGameState = false -- button1 in immediate mode will have its state updated
local button2 = FlexLove.new({
text = "Click to activate button 1",
onEvent = function(_, event)
if event.type == "release" then -- only fire on mouse release
button1.disabled = false -- this will also update the element
end
end
})
Best for:
- Simple UIs that change frequently
- Procedurally generated interfaces
- Debugging and development tools
- UIs driven directly by application state
You should be able to mix both modes in the same application - use retained mode for your main UI and immediate mode for debug overlays or dynamic elements, though this hasn't been tested.
Element Properties
Common properties for all elements:
{
-- Positioning & Size
x = 0, -- X position (number or string with units)
y = 0, -- Y position
width = 100, -- Width (number, string, or "auto")
height = 100, -- Height
z = 0, -- Z-index for layering
-- Visual Styling
backgroundColor = Color.new(0, 0, 0, 0), -- Background color
cornerRadius = 0, -- Uniform radius or {topLeft, topRight, bottomLeft, bottomRight}
border = {}, -- {top, right, bottom, left} boolean flags
borderColor = Color.new(0, 0, 0, 1),
opacity = 1, -- 0 to 1
-- Layout
positioning = "flex", -- "absolute", "relative", "flex", or "grid"
padding = {}, -- {top, right, bottom, left} or shortcuts
margin = {}, -- {top, right, bottom, left} or shortcuts
gap = 10, -- Space between children
-- Flexbox Properties
flexDirection = "horizontal", -- "horizontal" or "vertical"
justifyContent = "flex-start", -- Main axis alignment
alignItems = "stretch", -- Cross axis alignment
flexWrap = "nowrap", -- "nowrap" or "wrap"
-- Grid Properties
gridRows = 1,
gridColumns = 1,
rowGap = 10,
columnGap = 10,
-- Text
text = "Hello",
textColor = Color.new(1, 1, 1, 1),
textAlign = "start", -- "start", "center", "end"
textSize = "md", -- Number or preset ("xs", "sm", "md", "lg", "xl", etc.)
-- Theming
theme = "space", -- Theme name
themeComponent = "button", -- Component type from theme
-- Interaction
onEvent = function(element, event) end,
disabled = false,
disableHighlight = false, -- Disable pressed overlay (auto-true for themed elements)
-- Hierarchy
parent = nil, -- Parent element
}
Layout Modes
Absolute Positioning
local element = FlexLove.new({
positioning = "absolute",
x = 100,
y = 50,
width = 200,
height = 100
})
Flexbox Layout
local container = FlexLove.new({
positioning = "flex",
flexDirection = "horizontal",
justifyContent = "center",
alignItems = "center",
gap = 10
})
Grid Layout
local grid = FlexLove.new({
positioning = "grid",
gridRows = 3,
gridColumns = 3,
rowGap = 10,
columnGap = 10
})
Corner Radius
Supports uniform or individual corner radii:
-- Uniform radius
cornerRadius = 15
-- Individual corners
cornerRadius = {
topLeft = 20,
topRight = 10,
bottomLeft = 10,
bottomRight = 20
}
Theme System
To create a theme explore themes/space.lua as a reference
Load and apply themes for consistent styling:
FlexLove.init({
theme = "space" -- will use this as the initial theme
})
-- and if you need dynamic themes
local Theme = FlexLove.Theme
Theme.load("metal")
Theme.setActive("metal")
-- Use theme on elements
local button = Flexlove.new({
width = 200,
height = 60,
text = "Themed Button",
themeComponent = "button", -- Uses "button" component from active theme
backgroundColor = Color.new(0.5, 0.5, 1, 0.3) -- Renders behind theme
})
Android 9-Patch Support
FlexLove automatically parses Android 9-patch (*.9.png) files:
-- Theme definition with auto-parsed 9-patch
{
name = "My Theme",
components = {
button = {
atlas = "themes/mytheme/button.9.png"
-- insets automatically extracted from 9-patch borders
-- supports multiple stretch regions for complex scaling
},
panel = {
atlas = "themes/mytheme/panel.png",
insets = { left = 20, top = 20, right = 20, bottom = 20 }
-- manual insets still supported (overrides auto-parsing)
}
}
}
9-Patch Format:
- Files ending in
.9.pngare automatically detected and parsed - Guide pixels are automatically removed - the 1px border is stripped during loading
- Top/left borders define stretchable regions (black pixels)
- Bottom/right borders define content padding (optional) - automatically applied to child positioning
- Supports multiple non-contiguous stretch regions
- Manual insets override auto-parsing when specified
Scaling Corners:
{
button = {
atlas = "themes/mytheme/button.9.png",
scaleCorners = 2 -- Scale corners by 2x (number = direct multiplier)
}
}
scaleCornersaccepts a number (e.g., 2 = 2x size, 0.5 = half size)- Default:
nil(no scaling, 1:1 pixel perfect) - Corners scale uniformly while edges stretch as defined by guides
Themes support state-based rendering:
normal- Default statehover- Mouse over elementpressed- Element being clickeddisabled- Element is disabledactive- Element is active/focused
Event System
Enhanced event handling with detailed event information:
onEvent = function(element, event)
-- event.type: "click", "press", "release", "rightclick", "middleclick"
-- event.button: 1 (left), 2 (right), 3 (middle)
-- event.x, event.y: Mouse position
-- event.clickCount: Number of clicks (for double-click detection)
-- event.modifiers: { shift, ctrl, alt, gui }
if event.type == "click" and event.modifiers.shift then
print("Shift-clicked!")
end
end
Input Fields
FlexLöve provides text input support with single-line (and multi-line coming soon) fields:
-- Create a text input field
local input = FlexLove.new({
x = 10,
y = 10,
width = 200,
height = 30,
editable = true,
text = "Type here...",
placeholder = "Enter text",
textColor = Color.new(1, 1, 1, 1),
onTextChange = function(element, newText, oldText)
print("Text changed:", newText)
end
})
local textArea = FlexLove.new({
x = 10,
y = 50,
width = 300,
height = 150,
editable = true,
multiline = true,
text = "",
placeholder = "Enter multiple lines..."
})
Important: To enable key repeat for navigation keys (arrows, backspace, delete), add this to your love.load():
function love.load()
love.keyboard.setKeyRepeat(true)
end
Input Properties:
editable- Enable text input (default: false)multiline- Allow multiple lines (default: false)placeholder- Placeholder text when emptymaxLength- Maximum character countpasswordMode- Hide text with bulletsselectOnFocus- Select all text when focused
Input Callbacks:
onTextChange(element, newText, oldText)- Called when text changesonTextInput(element, text)- Called for each character inputonEnter(element)- Called when Enter is pressed (single-line only)onFocus(element)- Called when input gains focusonBlur(element)- Called when input loses focus
Features:
- Cursor positioning and blinking
- Text selection (mouse and keyboard)
- Copy/Cut/Paste (Ctrl+C/X/V)
- Word navigation (Ctrl+Arrow keys)
- Select all (Ctrl+A)
- Automatic text scrolling to keep cursor visible
- UTF-8 support
Responsive Units
Support for viewport-relative units:
local element = FlexLove.new({
width = "50vw", -- 50% of viewport width
height = "30vh", -- 30% of viewport height
x = "25%", -- 25% of parent width
textSize = "3vh" -- 3% of viewport height
})
Animations
Create smooth transitions:
local Animation = FlexLove.Animation
-- Fade animation
local fadeIn = Animation.fade(1.0, 0, 1)
fadeIn:apply(element)
-- Scale animation
local scaleUp = Animation.scale(0.5,
{ width = 100, height = 50 },
{ width = 200, height = 100 }
)
scaleUp:apply(element)
-- Custom animation with easing
local customAnim = Animation.new({
duration = 1.0,
start = { opacity = 0, width = 100 },
final = { opacity = 1, width = 200 },
easing = "easeInOutCubic"
})
customAnim:apply(element)
Creating Colors
local Color = FlexLove.Color
-- From RGB values (0-1 range)
local red = Color.new(1, 0, 0, 1)
-- From hex string
local blue = Color.fromHex("#0000FF")
local semiTransparent = Color.fromHex("#FF000080")
API Reference
Flexlove (Main Module)
Flexlove.init(props)- Initialize GUI system with base scaleFlexlove.new(props)- Create a new elementFlexlove.update(dt)- Update all elementsFlexlove.draw()- Draw all elementsFlexlove.resize()- Handle window resize
Color
Color.new(r, g, b, a)- Create color (values 0-1)Color.fromHex(hex)- Create from hex stringColor:toHex()- Convert to hex stringColor:toRGBA()- Get RGBA values
Theme (only needed for dynamic changes)
Theme.load(name)- Load theme by nameTheme.setActive(name)- Set active themeTheme.getActive()- Get current active theme
Animation
Animation.new(props)- Create custom animationAnimation.fade(duration, from, to)- Fade animationAnimation.scale(duration, from, to)- Scale animation
Enums
TextAlign
START- Align to startCENTER- Center alignEND- Align to endJUSTIFY- Justify text
Positioning
ABSOLUTE- Absolute positioningRELATIVE- Relative positioningFLEX- Flexbox layoutGRID- Grid layout
FlexDirection
HORIZONTAL- Horizontal flexVERTICAL- Vertical flex
JustifyContent
FLEX_START- Align to startCENTER- Center alignFLEX_END- Align to endSPACE_AROUND- Space around itemsSPACE_BETWEEN- Space between itemsSPACE_EVENLY- Even spacing
AlignItems / AlignSelf
STRETCH- Stretch to fillFLEX_START- Align to startFLEX_END- Align to endCENTER- Center alignBASELINE- Baseline align
FlexWrap
NOWRAP- No wrappingWRAP- Wrap items
Examples
The examples/ directory contains comprehensive demos:
EventSystemDemo.lua- Event handling and callbacksCornerRadiusDemo.lua- Rounded corners showcaseThemeLayeringDemo.lua- Theme system with layeringDisableHighlightDemo.lua- Highlight controlSimpleGrid.lua- Grid layout examplesTextSizePresets.lua- Text sizing optionsOnClickAnimations.lua- Animation examplesZIndexDemo.lua- Layering demonstration
Testing
Run tests with:
lua testing/runAll.lua
# or a specific test:
lua testing/__tests__/<specific_test>
Compatibility
Compatibility:
- Lua: 5.1+
- LÖVE: 11.x (tested)
- LuaJIT: Compatible
License
MIT License - see LICENSE file for details.
Contributing
This library is under active development. Contributions, bug reports, and feature requests are welcome!