Inky is an unopinonated GUI framework for the LÖVE game framework, though it should work with anything Lua(JIT) based. It is heavily inspired by Helium and React.
Inky aims to solve LÖVE's problem of having no generic GUI framework that can work everywhere for anything. Most of LÖVE's GUI frameworks provide a (limited) set of widgets, and/or constrain itself to only a single input system. Inky gives complete freedom in both these aspects: Mouse, Mobile, Gamepad, Retro, Modern, Windowed. Everything is possible with Inky.
Inky does not provide any out of the box widgets for you to use. If you want a button, you'll have to program it. However! Inky does provide everything to make this process streamlined and easy. Making a widget means settings up 'hooks' for the widget's logic, and providing a draw function. Inky provides hooks to respond to events, interact with pointers, manage state, perform side effects, and much more.
To get started, create a local copy of Inky in your game's directory by downloading it, or cloning the repository.
Use Inky.defineElement
to define a new UI element:
-- button.lua
local Inky = require("inky")
return Inky.defineElement(function(self)
self.props.count = 0
self:onPointer("release", function()
self.props.count = self.props.count + 1
end)
return function(_, x, y, w, h)
love.graphics.rectangle("line", x, y, w, h)
love.graphics.printf("I have been clicked " .. self.props.count .. " times", x, y, w, "center")
end
end)
Then, use Inky.scene
to set up a scene with your element, and use Inky.pointer
to delegate the events:
-- main.lua
local Inky = require("inky")
local Button = require("examples.getting_started.button")
local scene = Inky.scene()
local pointer = Inky.pointer(scene)
local button_1 = Button(scene)
local button_2 = Button(scene)
love.window.setMode(220, 66)
love.window.setTitle("Example: Getting Started")
function love.update(dt)
local mx, my = love.mouse.getX(), love.mouse.getY()
pointer:setPosition(mx, my)
end
function love.draw()
scene:beginFrame()
button_1:render(10, 10, 200, 16)
button_2:render(10, 40, 200, 16)
scene:finishFrame()
end
function love.mousereleased(x, y, button)
if (button == 1) then
pointer:raise("release")
end
end
The above is also available as an example at https://github.com/Keyslam/Inky/tree/develop/examples/getting_started.
With Inky you'll be interacting with 3 kinds of objects: Elements, Scenes, Pointers.
Elements encapsulate a single UI widget, like a button, a label, or a list.
You define your widgets using Inky.defineElement
. For each Element you can configure it's variables, how to respond to events, how to draw, and much more.
Element = Inky.defineElement(initializer)
element = Element(scene)
element.props.foo = "bar"
x, y, w, h = element:getView()
element:on(eventName, callback)
element:onPointer(eventName, callback)
element:onPointerInHierarchy(eventName, callback)
element:onPointerEnter(callback)
element:onPointerExit(callback)
element:onEnable(callback)
element:onDisable(callback)
element:useOverlapCheck(predicate)
element:useEffect(effect, ...)
element:render(x, y, w, h, depth)
Scenes manage Elements. 1 Scene can contain many Elements, and an Element can only be in 1 Scene.
scene = Inky.scene(spatialHashSize)
scene:beginFrame()
scene:finishFrame()
didBeginFrame = scene:didBeginFrame()
scene:raise(eventName, ...)
Pointers represent the cursor in the GUI system, but are flexible to also support touch controls, d-pad controls, keyboard controls, and much more. Pointers can have a (x, y) position, in which case they interact with Elements at that location. Pointers can also have a target Element, in which case they only interact with that Element.
pointer = Inky.pointer(scene)
pointer:setPosition(x, y)
x, y = pointer:getPosition()
pointer:setTarget(target)
target = pointer:getTarget()
mode = pointer:getMode()
pointer:setActive(active)
active = pointer:isActive()
doesOverlapElement = pointer:doesOverlapElement(element)
doesOverlapAnyElement = pointer:doesOverlapAnyElement()
consumed = pointer:raise(eventName, ...)
pointer:captureElement(element, shouldCapture)
doesCaptureElement = pointer:doesCaptureElement(element)
Elements can be defined by providing a initializer
function, which can optionally return a draw
function.
local MyElement = Inky.defineElement(function(element)
-- Optional draw function
return function(self, x, y, w, h, depth)
end
end)
The result can then be called to create an instance of the Element
local myElement = MyElement(scene)
Elements can be rendered, meaning it will respond to events and be drawn.
If no depth is provided, the depth of the parent Element + 1
is used instead.
NOTE: A Scene Frame has to be started before an Element can be rendered.
myElement:render(x, y, w, h, depth?)
Elements contain a props
field which can be used to send and read arguments.
local myElement = MyElement(scene)
myElement.props.foo = "bar"
local MyElement = Inky.defineElement(function(element)
print(element.props.foo) -- "bar"
end)
NOTE: It is encouraged to also use the
props
field to store variables that define the state of the Element.
local MyElement = Inky.defineElement(function(element)
-- Bad
local hovered = false
-- Good
element.props.hovered = false
end)
Elements can listen to the change of a prop.
NOTE: Effects are executed right before the next draw of the Element
local MyElement = Inky.defineElement(function(element)
-- Listen to 'foo' changing
element:useEffect(function()
print("Foo changed")
end, "foo")
-- Listen to 'foo' or 'bar' changing
element:useEffect(function()
print("foo or bar changed")
end, "foo", "bar")
end)
Elements can listen to Pointers Events and Scene Events
local MyElement = Inky.defineElement(function(element)
-- Listen to Scene Event raised
element:on(eventName, function(self, ...)
print("Scene event")
end)
-- Listen to Pointer Event raised
element:onPointer(eventName, function(self, pointer, ...)
print("Pointer event")
end)
-- Listen to Pointer event in Hierarchy
-- That is, any (grand)child of this Element accepted the Pointer Event
element:onPointerInHierarchy(eventName, function(self, pointer, ...)
print("Pointer event in hierarchy")
end)
end)
Elements can listen to know when a Pointer starts or stops hovering over it.
local MyElement = Inky.defineElement(function(element)
-- Listen to Pointer started hovering Element
element:onPointerEnter(function(self, pointer, ...)
print("Enter")
end)
-- Listen to Pointer stopped hovering this Element
element:onPointerExit(function(self, pointer, ...)
print("Exit")
end)
end)
When an Element is rendered this frame, but wasn't rendered last frame, it is effectively enabled. Similarly, if an Element isn't rendered this frame, but was rendered last frame, it is effectively disabled. Elements can listen to know when this occurs.
local MyElement = Inky.defineElement(function(element)
-- Listen to Element enabled
element:onEnable(function(self)
print("Enabled")
end)
-- Listen to Element disabled
element:onDisable(function(self)
print("Disabled")
end)
end)
Elements can provide a custom function for overlapping checks with Pointers. This can be useful for rounded buttons, for example.
NOTE: Pointers always need to overlap with bounding box of the Element
local MyElement = Inky.defineElement(function(element)
-- Define a overlap check
element:useOverlapCheck(function(self, px, py, x, y, w, h)
-- Only if the pointer's x position is less than 200
return px < 200
end)
end)
Elements know the position they were last rendered at
local MyElement = Inky.defineElement(function(element)
element:on(eventName, function()
local x, y, w, h = element:getView()
end)
end)
Pointers need to be attached to a Scene
local pointer = Inky.pointer(scene)
The position of a Pointer can be set and got.
Setting the position changes the mode of the Pointer to POSITION
.
pointer:setPosition(x, y)
local x, y = pointer:getPosition()
The target of a Pointer can be set and got.
Setting the target changes the mode of the Pointer to TARGET
. See
pointer:setTarget(element)
local target = pointer:getTarget()
Pointers can be in 2 modes: POSITION
and TARGET
.
In POSITION
mode the Pointer interacts with any Elements it overlaps with. This is useful for your standard mouse cursor.
In TARGET
mode the Pointer interacts only with the target Element. This can be useful for keyboard navigation and programmatically interacting with Elements.
local mode = pointer:getMode()
Pointers can be made (in)active. When a Pointer is inactive it doesn't interact with any Elements.
pointer:setActive(boolean)
local isActive = pointer:isActive()
Pointer Events can be raised to be caught by Elements.
Pointer Events are sent to the Element with the highest depth first. When an Element listens to the Event it is consumed, and won't be sent to any other Elements.
If the event was consumed by any element, this function returns true
. Otherwise it returns false
.
consumed = pointer:raise(eventName, ...)
When a Pointer 'captures' an Element all the Pointer Events will be sent to it, regardless of if the Pointer overlaps the Element.
-- Start capturing
pointer:captureElement(element, true)
-- Start capturing
pointer:captureElement(element, false)
local doesCaptureElement = pointer:doesCaptureElement(element)
Pointers know which Elements it is overlapping
local doesOverlapElement = pointer:doesOverlapElement(element)
local doesOverlapAnyElement = pointer:doesOverlapAnyElement()
Scenes use a SpatialHash under the hood which cell size can be configured.
-- Create a Scene with the default SpatialHash size (32)
local scene = Scene()
-- Create a Scene with a SpatialHash size of (64)
local scene = Scene(64)
Elements need to be rendered within a Scene Frame, which needs to be started and finished.
function love.draw()
scene:beginFrame()
-- Render elements
scene:finishFrame()
end
local didBeginFrame = scene:didBeginFrame()
Scene Events can be raised to be caught by Elements. Scene Events are sent to all Elements active in the Scene.
scene:raise(eventName, ...)