Plan is a super simple layout helper designed for use with Love2d.
Plan is in its early stages, may be full of bugs, and could easily change. Use with caution!
Plan is designed to sit all within a single file, and can easily be thrown
into your lib folder:
local Plan = require "path.to.libs.plan"Before jumping into the code, it'd be good to go over the basic ideas of the library, and how they fit together.
At the core of Plan there are two objects, Containers - which are your
layout blocks, and Rules which determine where your Containers are positioned.
Containers are able to contain other Containers, and these children use their own rules to determine their position relative to its parent. By themselves, Containers have no graphical component, hence the term "layout" helper, rather than UI - there's still a bit of work ahead of you.
Let's look at an example.
Any layout managed by Plan requires a root. Calling Plan.new() will create a
new root whose dimensions take up the entire screen at the point of calling.
We'll also hook into update and draw pre-emptively. For the root, and all
Containers really, these do nothing but call update and draw on its children
local Plan = require "lib.plan"
local uiRoot = nil
function love.load()
uiRoot = Plan.new()
end
function love.update(dt)
uiRoot:update(dt)
end
function love.draw()
uiRoot:draw()
endLets add a new Container. I want this Container to be centered horizontally, be 20 pixels from the top of the page, its height to be a third of the size of the screen, and its width to be the same as its height - wow. Thats a mouthful!
Thats where Rules come into play. The constructor for a Container requires a
Rules object to be passed in. These Rules are then used to compute the
position, and size, of the container.
Plan provides six rules out of the box:
PixelRulefor constant pixel values,RelativeRulefor values relative to its parent,CenterRulefor centering the position in its parent,AspectRulefor maintaining an aspect ratio with itselfParentRulefor taking the same value as its parentMaxRulefor taking the maximal value from its parent, ieparent.widthforx. Optionally, an offset can be added so that it isparent.width - offset.
more advanced users can add their own if they see fit, but we'll leave that for now.
Lets give the constraints listed out above a go in Plan:
local Plan = require "lib.plan"
local Container = Plan.Container
local Rules = Plan.Rules
local uiRoot = nil
function love.load()
-- Plan exposes its internal rules via functions, rather than objects for
-- ease of use.
local layoutRules = Rules.new()
:addX(Plan.center())
:addY(Plan.pixel(20))
:addWidth(Plan.aspect(1))
:addHeight(Plan.relative(0.33))
local container = Container:new(layoutRules)
uiRoot = Plan.new()
uiRoot:addChild(container)
end
function love.update(dt)
uiRoot:update(dt)
end
function love.draw()
uiRoot:draw()
endSweet! Lets run that and... nothing.
If you remember, Containers have no graphical component - we have to add
that ourselves. Luckily, Plan makes it easy to do so with Container:extend()
allowing us to override the base Container and add our own.
Lets create a Panel object that acts like a container, but draws a standard
box:
local Panel = Container:extend()Container:extend() returns an object that contains all the functions that
Container has, unless Panel chooses to override it - which we will. Because
we're adding a colour, and want to draw a coloured box, we'll need to override
the new function, and the draw function.
Plan makes this easy by exposing the super field on all extended objects.
If you've used classic.lua, then this syntax may look familiar
Lets take a look how this works:
local Panel = Container:extend()
function Panel:new(rules, colour)
-- initialises all the container fields
local panel = Panel.super.new(self, rules)
-- then we can add our own to it
panel.colour = colour
panel.r = 5
return panel
end
function Panel:draw()
love.graphics.push("all")
love.graphics.setColor(self.colour)
love.graphics.rectangle("fill", self.x, self.y, self.w, self.h, self.r, self.r)
love.graphics.pop()
-- then we want to draw our children containers:
Panel.super.draw(self)
endAnd then, lets modify our love callbacks:
local Plan = require "lib.plan"
local Rules = Plan.Rules
local Panel = require "examples.panel"
local uiRoot = nil
function love.load()
-- Plan exposes its internal rules via functions, rather than objects for
-- ease of use.
local layoutRules = Rules.new()
:addX(Plan.center())
:addY(Plan.pixel(20))
:addWidth(Plan.aspect(1))
:addHeight(Plan.relative(0.33))
local panel = Panel:new(layoutRules, { 0.133, 0.133, 0.133 })
uiRoot = Plan.new()
uiRoot:addChild(panel)
end
function love.update(dt)
uiRoot:update(dt)
end
function love.draw()
uiRoot:draw()
endRunning this, it should look something like this:
Great Success!
Although, are you ready for the fun part? Lets say we want this layout to keep
its position, no matter on the screen size - Plan can help with that!
Plan exposes a function called refresh which will trigger every child
component to recalculate its position based off of its rules. Lets tie this into
love.resize so that our layout changes with the screen size.
First, we must create a conf.lua file that will enable the ability to resize
the window:
function love.conf(t)
t.window.resizable = true
endThen, we can add this callback to the bottom of our example:
function love.resize()
uiRoot:refresh()
endWhen we run this, we should see no difference to before, right? But now try resizing the window:
Wahay! Our Panel now moves and scales depending on the Rules we set upon creation.
Congratulations, you've just written your first custom Plan component!
Plan is both the entry point, as well as the helper root container.
Returns a new Container with no parent (a root) with the dimensions x = 0,
y = 0, w = love.graphics.getWidth(), h = love.graphics.getHeight().
Plan doesn't extend Container, however you can access its underlying
container with Plan.new().root
Triggers a recalculation of the layout based off of its rules. Resets the
rules of its container to the screen size.
Adds a new child container. Behaves the same as Container:addChild(child)
Removes a child container. Behaves the same as Container:removeChild(child)
Updates all child containers. Behaves the same as Container:update(dt)
Draws all child containers. Behaves the same as Container:draw()
Containers are objects with locations determined by Rules. Generally it's
helpful to alias Plan.Container with Container
local Plan = require "lib.plan"
local Container = Plan.ContainerCreates a new Container. The created Container will not have been realised (its
x, y, w, h values all 0, its parent nil) until it has been either
added to a parent, or :refresh() called directly.
Returns a new object that inherits from Container. The new object can choose
to override certain functions (such as draw), otherwise it will fall back on
Container.
When overriding, you can call MyObj.super to refer to the parent Container
object. This is useful for still doing operations defined in Container such as
updating every child:
function MyObj:update(dt)
self.x = self.x + self.speed + dt -- update some local state
MyObj.super.update(self, dt) -- update all childen via `Container:update(dt)`
endWhen overriding :new then you have to call MyObj.super.new(self, rules) in
order to initialise the Container:
function MyObj:new(rules, otherParameter)
local obj = MyObj.super.new(self, rules)
obj.otherParameter = otherParameter
return obj
endAdds child as a child of the container. Sets the field of parent on child
to itself, and then calls :refresh on itself to realise its children.
Removes child as a child of the container.
Recalculates the container's dimensions based on the rules, then recalculates all child containers.
Updates the container. By default, Container does no sort of update, instead
just passes the call on to each child.
Draws the container. By default Container has no graphical component, instead
just passes the call onto each child.
Emits an event down the tree. This emits events in a depth-first manner. Useful for hooking into input events (ie. love.keypressed).
function myContainer:keypressed(key)
print("key pressed", key)
end
-- ...
function love.keypressed(key)
ui:emit("keypressed", key)
endTo prevent the event continuing down a branch, return false from the handler to stop. Note, this won't mean that the event will stop completely, only prevent it being passed to its children (and their children et al).
Rules are collections for rules for Containers. Generally it's helpful to
alias Plan.Rules with Rules:
local Plan = "lib.plan"
local Rules = Plan.RulesFor convenience, calling any add<X> function with a number (instead of a rule)
will treat the input as if you passed a PixelRule (more on individual rules
further below)
local myRules = Rules.new()
Rules:addX(4) -- equivalent to `:addX(Plan.pixel(4))`Retuns a new Rules object.
Sets the x rule for the rules collection. If a value is already set for x,
then it is overwritten.
Returns the Rule for x.
Sets the y rule for the rules collection. If a value is already set for y,
then it is overwritten.
Returns the Rule for y
Sets the width rule for the rules collection. If a value is already set for
width, then it is overwritten.
Returns the Rule for width
Sets the height rule for the rules collection. If a value is already set for
height, then it is overwritten.
Returns the Rule for Height
Triggers a calculation of the Rules' rules. Returns the resultant
x, y, width and height.
Updates the dimension rule with the provided update function fn. The
existing rule at the given dimension is provided as the first argument to the
update function, and any other args in ... are passed as well.
Under the hood, it'll look like.
self.rules[dimension] = fn(self.rules[dimension], ...)Each in-built rule provides a :set function that takes the same arguments as
its constructor to allow for easier updating of rules. For example
local oldRules = Rules.new()
:addX(Plan.pixel(100))
-- Some point later
oldRules:update("x", function(rule) rule:set(150) end)Remember: updating a rule will not update its layout until a :refresh()
call is made!
Returns a copy of the current Rules object. Also calls clone on all rules
in the collection.
Plan provides six rules out of the box, with the ability to add your own
custom ones (described in the Advanced Usage section).
Each rule exposes a :set function that takes the same number of arguments as
its constructor to allow for easy modification of rules. Note that rule value
updates do not recalculate until :refresh is called upon the container.
Returns a PixelRule object (internal) which describes a value in pixels.
rules:addX(Plan.pixel(10)) -- 10 pixels from the leftReturns a CenterRule object (internal) which centers the dimension. Calling
this on width or height will result in an error.
rules:addX(Plan.center()) -- centered horizontally
:addHeight(Plan.center()) -- blows up.Returns a RelativeRule object (internal) which sets the given dimension
relative to the same dimension on the parent.
rules:addX(Plan.relative(0.33)) -- positioned a third of the way from the leftReturns an AspectRule object (internal) which sets the given dimension as a
ratio to the opposite. Calling this on x or y will result in an error.
rules:addWidth(Plan.pixel(400)) -- 400 pixels wide
:addHeight(Plan.aspect(2)) -- 800 pixels high
:addX(Plan.aspect(1)) -- blows upReturns a ParentRule object (internal) which sets the given dimension the same
as the elements parent.
parentRules:addWidth(Plan.pixel(100)) -- parent width is 100 pixels
-- ...
rules:addWidth(Plan.parent()) -- width is 100 pixelsReturns a MaxRule object (internal) which sets the given dimension to be the
maximal value of its parent. For example, calling this on width or height
will result in the width and height of the parent, however calling this on
x or y will also result in width and height respectively. Optionally
takes an offset value that is subtracted from the result.
parentRules:addWidth(Plan.pixel(100)) -- parent width is 100 pixels
-- ...
rules:addX(Plan.max(20)) -- horizontal position is 80 pixelsPlan provides "factories" for some common base Rules objects. Currently
there are two, full and half.
The intenion is to use these base Rules objects, and overwrite the dimensions
you do not want. More will be added in the future.
Returns a Rules object with every dimension set to Plan.parent().
Returns a Rules object set to result in half of the parent container the
rules are given to. Takes a direction that describes which half you want.
Accepted directions are "top", "bottom", "left" and "right".
Returns a Rules object set to result in a container the specified relative
space from the edge of the parent on all four sides.
Returns a Rules object set to result in a container the specified pixel
distance from the edges of the parent on all four sides.
This section describes how you might take further advantage of some of the
features of Plan. They aren't necessarily essential to know, but if you find
the out-of-the-box features lacking, here's where to turn.
Plan.new() does not have to be the root of your layout. It's merely provided
as a helpful starting point that may cover most scenarios.
Personally, I recommend using Plan.new() as by creating your own root, you are
limited in which rules you can use on it to just pixel, as the remainder
require some parent existing.
However, in cases where you want a layout as a particular resolution,
you can forgo the Plan.new() layout, and instead create your own by creating
a Container directly. In order to delineate that it's the UI root, then you
must set the internal flag isUIRoot on the container to true:
local Plan = require "lib.plan"
local Container = Plan.Container
local Rules = Plan.Rules
local root = nil
function love.load()
local rules = Rules.new():addX(Plan.pixel(0))
:addY(Plan.pixel(0))
:addWidth(Plan.pixel(love.graphics.getWidth()))
:addHeight(Plan.pixel(love.graphics.getHeight()))
root = Container:new(rules)
root.isUIRoot = true
endSometimes Plan may not give you the rule you would like. Hopefully there are
more added in the future, but if you want your own secret sauce, you can provide
your own layout rules.
A Plan Rule requires only one function to be exposed, realise:
Returns the realised dimension.
dimension will be one of "x", "y", "w" for width, and "h" for height.
element is the Element you are calculating the rule for - a common use for
this is to fetch element.parent.
Sometimes you need to base your value off of another rule, which is why rules
is provided. The inbuilt rules assume that any other dimension has not been
realised, so it's often safer to just realise the dimension you want yourself -
rules.w:realise("w", element, rules) will return the realised width for the
element, without setting that value.
Optionally, you can implement a clone function, that should return a new copy
of the rule. This isn't required to run normally, but if you are making use of
Rules:clone then it is required.
All the internal rules with Plan implement a set function, which takes the
same arguments as its constructor. This is helpful as it can hide internals
from the users behind the same API as construcion. Again, not necessary
whatsoever, but a nice addition.
Through using :emit, it's possible to automagically hook into Love callbacks.
Since Plan is ideally only for UI components, it's probably best to only hook
into input events. It's not wise to emit draw and update events manually.
If using resize, you should not use it to resize an element, rather call
ui:refresh instead.
local callbacks = {
"directorydropped", "filedropped", "focus", "gamepadaxis", "gamepadpressed",
"gamepadreleased", "joystickaxis", "joystickhat", "joystickpressed",
"joystickreleased", "joystickremoved", "keypressed", "keyreleased",
"lowmemory", "mousefocus", "mousemoved", "mousepressed", "mousereleased",
"quit", "resize", "textedited", "textinput", "threaderror", "touchmoved",
"touchpressed", "touchreleased", "visible", "wheelmoved", "joystickadded"
}
function Plan.hook()
for i, callback in ipairs(callbacks) do
local actual = love[callback]
love[callback] = function(...)
if actual then
actual(...)
end
Plan.emit(callback, ...)
end
end
endFeel free to open issues and pull requests! Plan is in its early days, and
I'm adding to it when I come across a feature I would like to add while working
on other projects. If I'm missing anything you'd like, please, let me know!

