Fix #17: Support multiple instances

This commit is contained in:
Matthias Richter 2016-01-03 18:33:39 +01:00
parent f77ab8e5e8
commit aca8a297bb
14 changed files with 385 additions and 244 deletions

View file

@ -1,9 +1,8 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(text, ...) return function(core, text, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...) local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or text opt.id = opt.id or text
opt.font = opt.font or love.graphics.getFont() opt.font = opt.font or love.graphics.getFont()
@ -11,14 +10,14 @@ return function(text, ...)
w = w or opt.font:getWidth(text) + 4 w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4 h = h or opt.font:getHeight() + 4
core.registerHitbox(opt.id, x,y,w,h) opt.state = core:registerHitbox(opt.id, x,y,w,h)
core.registerDraw(core.theme.Button, text, opt, x,y,w,h) core:registerDraw(core.theme.Button, text, opt, x,y,w,h)
return { return {
id = opt.id, id = opt.id,
hit = core.mouseReleasedOn(opt.id), hit = core:mouseReleasedOn(opt.id),
hovered = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

View file

@ -1,9 +1,8 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(checkbox, ...) return function(core, checkbox, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...) local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or checkbox opt.id = opt.id or checkbox
opt.font = opt.font or love.graphics.getFont() opt.font = opt.font or love.graphics.getFont()
@ -11,18 +10,18 @@ return function(checkbox, ...)
w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4) w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4)
h = h or opt.font:getHeight() + 4 h = h or opt.font:getHeight() + 4
core.registerHitbox(opt.id, x,y,w,h) opt.state = core:registerHitbox(opt.id, x,y,w,h)
local hit = core.mouseReleasedOn(opt.id) local hit = core:mouseReleasedOn(opt.id)
if hit then if hit then
checkbox.checked = not checkbox.checked checkbox.checked = not checkbox.checked
end end
core.registerDraw(core.theme.Checkbox, checkbox, opt, x,y,w,h) core:registerDraw(core.theme.Checkbox, checkbox, opt, x,y,w,h)
return { return {
id = opt.id, id = opt.id,
hit = hit, hit = hit,
hovered = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

191
core.lua
View file

@ -1,10 +1,34 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local NONE = {}
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local theme = require(BASE..'theme') local default_theme = require(BASE..'theme')
local suit = {}
suit.__index = suit
function suit.new(theme)
return setmetatable({
-- TODO: deep copy/copy on write? better to let user handle => documentation?
theme = theme or default_theme,
mouse_x = 0, mouse_y = 0,
mouse_button_down = false,
draw_queue = {n = 0},
Button = require(BASE.."button"),
ImageButton = require(BASE.."imagebutton"),
Label = require(BASE.."label"),
Checkbox = require(BASE.."checkbox"),
Input = require(BASE.."input"),
Slider = require(BASE.."slider"),
layout = require(BASE.."layout").new(),
}, suit)
end
-- helper -- helper
local function getOptionsAndSize(opt, ...) function suit.getOptionsAndSize(opt, ...)
if type(opt) == "table" then if type(opt) == "table" then
return opt, ... return opt, ...
end end
@ -12,75 +36,83 @@ local function getOptionsAndSize(opt, ...)
end end
-- gui state -- gui state
local hot, hot_last, active function suit:anyHovered()
local NONE = {} return self.hovered ~= nil
local function anyHot()
return hot ~= nil
end end
local function isHot(id) function suit:isHovered(id)
return id == hot return id == self.hovered
end end
local function wasHot(id) function suit:wasHovered(id)
return id == hot_last return id == self.hovered_last
end end
local function isActive(id) function suit:isActive(id)
return id == active return id == self.active
end
function suit:getStateName(id)
if self:isActive(id) then
return "active"
elseif self:isHovered(id) then
return "hovered"
end
return "normal"
end end
-- mouse handling -- mouse handling
local mouse_x, mouse_y, mouse_button_down = 0,0, false function suit:mouseInRect(x,y,w,h)
local function mouseInRect(x,y,w,h) return self.mouse_x >= x and self.mouse_y >= y and
return not (mouse_x < x or mouse_y < y or mouse_x > x+w or mouse_y > y+h) self.mouse_x <= x+w and self.mouse_y > y+h
end end
local function registerMouseHit(id, ul_x, ul_y, hit) function suit:registerMouseHit(id, ul_x, ul_y, hit)
if hit(mouse_x - ul_x, mouse_y - ul_y) then if hit(self.mouse_x - ul_x, self.mouse_y - ul_y) then
hot = id self.hovered = id
if active == nil and mouse_button_down then if self.active == nil and self.mouse_button_down then
active = id self.active = id
end end
end end
return self:getStateName(id)
end end
local function registerHitbox(id, x,y,w,h) function suit:registerHitbox(id, x,y,w,h)
return registerMouseHit(id, x,y, function(x,y) return self:registerMouseHit(id, x,y, function(x,y)
return x >= 0 and x <= w and y >= 0 and y <= h return x >= 0 and x <= w and y >= 0 and y <= h
end) end)
end end
local function mouseReleasedOn(id) function suit:mouseReleasedOn(id)
return not mouse_button_down and isActive(id) and isHot(id) return not self.mouse_button_down and self:isActive(id) and self:isHovered(id)
end end
local function updateMouse(x, y, button_down) function suit:updateMouse(x, y, button_down)
mouse_x, mouse_y, mouse_button_down = x,y, button_down self.mouse_x, self.mouse_y = x,y
if button_down ~= nil then
self.mouse_button_down = button_down
end
end end
local function getMousePosition() function suit:getMousePosition()
return mouse_x, mouse_y return self.mouse_x, self.mouse_y
end end
-- keyboard handling -- keyboard handling
local key_down, textchar, keyboardFocus function suit:getPressedKey()
local function getPressedKey() return self.key_down, self.textchar
return key_down, textchar
end end
local function keypressed(key) function suit:keypressed(key)
key_down = key self.key_down = key
end end
local function textinput(char) function suit:textinput(char)
textchar = char self.textchar = char
end end
local function grabKeyboardFocus(id) function suit:grabKeyboardFocus(id)
if isActive(id) then if self:isActive(id) then
keyboardFocus = id
if love.system.getOS() == "Android" or love.system.getOS() == "iOS" then if love.system.getOS() == "Android" or love.system.getOS() == "iOS" then
if id == NONE then if id == NONE then
love.keyboard.setTextInput( false ) love.keyboard.setTextInput( false )
@ -88,81 +120,52 @@ local function grabKeyboardFocus(id)
love.keyboard.setTextInput( true ) love.keyboard.setTextInput( true )
end end
end end
self.keyboardFocus = id
end end
return self:hasKeyboardFocus(id)
end end
local function hasKeyboardFocus(id) function suit:hasKeyboardFocus(id)
return keyboardFocus == id return self.keyboardFocus == id
end end
local function keyPressedOn(id, key) function suit:keyPressedOn(id, key)
return hasKeyboardFocus(id) and key_down == key return self:hasKeyboardFocus(id) and self.key_down == key
end end
-- state update -- state update
local function enterFrame() function suit:enterFrame()
hot_last, hot = hot, nil self.hovered_last, self.hovered = self.hovered, nil
updateMouse(love.mouse.getX(), love.mouse.getY(), love.mouse.isDown(1)) self:updateMouse(love.mouse.getX(), love.mouse.getY(), love.mouse.isDown(1))
key_down, textchar = nil, "" self.key_down, self.textchar = nil, ""
grabKeyboardFocus(NONE) self:grabKeyboardFocus(NONE)
end end
local function exitFrame() function suit:exitFrame()
if not mouse_button_down then if not self.mouse_button_down then
active = nil self.active = nil
elseif active == nil then elseif self.active == nil then
active = NONE self.active = NONE
end end
end end
-- draw -- draw
local draw_queue = {n = 0} function suit:registerDraw(f, ...)
local function registerDraw(f, ...)
local args = {...} local args = {...}
local nargs = select('#', ...) local nargs = select('#', ...)
draw_queue.n = draw_queue.n + 1 self.draw_queue.n = self.draw_queue.n + 1
draw_queue[draw_queue.n] = function() self.draw_queue[self.draw_queue.n] = function()
f(unpack(args, 1, nargs)) f(unpack(args, 1, nargs))
end end
end end
local function draw() function suit:draw()
exitFrame() self:exitFrame()
for i = 1,draw_queue.n do for i = 1,self.draw_queue.n do
draw_queue[i]() self.draw_queue[i]()
end end
draw_queue.n = 0 self.draw_queue.n = 0
enterFrame() self:enterFrame()
end end
local module = { return suit
getOptionsAndSize = getOptionsAndSize,
anyHot = anyHot,
isHot = isHot,
wasHot = wasHot,
isActive = isActive,
mouseInRect = mouseInRect,
registerHitbox = registerHitbox,
registerMouseHit = registerMouseHit,
mouseReleasedOn = mouseReleasedOn,
updateMouse = updateMouse,
getMousePosition = getMousePosition,
getPressedKey = getPressedKey,
keypressed = keypressed,
textinput = textinput,
grabKeyboardFocus = grabKeyboardFocus,
hasKeyboardFocus = hasKeyboardFocus,
keyPressedOn = keyPressedOn,
enterFrame = enterFrame,
exitFrame = exitFrame,
registerDraw = registerDraw,
theme = theme,
draw = draw,
}
theme.core = module
return module

View file

@ -76,23 +76,23 @@ Clears GUI state when exiting a frame.
GUI State GUI State
^^^^^^^^^ ^^^^^^^^^
.. function:: anyHot() .. function:: anyHovered()
:returns: ``true`` if any widget is in the ``hot`` state. :returns: ``true`` if any widget is hovered by the mouse.
Checks if any widget is in the hot state Checks if any widget is hovered by the mouse.
.. function:: isHot(id) .. function:: isHovered(id)
:param mixed id: Identifier of the widget. :param mixed id: Identifier of the widget.
:returns: ``true`` if the widget is in the ``hot`` state. :returns: ``true`` if the widget is hovered by the mouse.
Checks if the widget identified by ``id`` is hovered by the mouse. Checks if the widget identified by ``id`` is hovered by the mouse.
.. function:: wasHot(id) .. function:: wasHovered(id)
:param mixed id: Identifier of the widget. :param mixed id: Identifier of the widget.
:returns: ``true`` if the widget was in the ``hot`` state in the last frame. :returns: ``true`` if the widget was in the hovered by the mouse in the last frame.
Checks if the widget identified by ``id`` was hovered by the mouse in the last frame. Checks if the widget identified by ``id`` was hovered by the mouse in the last frame.
@ -121,7 +121,7 @@ Checks whether the mouse cursor is in the rectangle defined by ``x,y,w,h``.
:param function hit: Function to perform the hit test. :param function hit: Function to perform the hit test.
Registers a hit-test defined by the function ``hit`` for the widget identified Registers a hit-test defined by the function ``hit`` for the widget identified
by ``id``. Sets the widget to ``hot`` if th hit-test returns ``true``. Sets the by ``id``. Sets the widget to ``hovered`` if th hit-test returns ``true``. Sets the
widget to ``active`` if the hit-test returns ``true`` and the mouse button is widget to ``active`` if the hit-test returns ``true`` and the mouse button is
pressed. pressed.
@ -186,3 +186,36 @@ Checks whether the widget identified by ``id`` currently has keyboard focus.
Checks whether the key ``key`` was pressed while the widget identified by Checks whether the key ``key`` was pressed while the widget identified by
``id`` has keyboard focus. ``id`` has keyboard focus.
Instancing
----------
.. function:: new()
:returns: Separate UI state.
Create a separate UI and layout state. Everything that happens in the new
state will not affect any other state. You can use the new state like the
"global" state ``suit``, but call functions with the colon syntax instead of
the dot syntax, e.g.::
function love.load()
dress = suit.new()
end
function love.update()
dress.layout:reset()
dress:Label("Hello, World!", dress.layout:row(200,30))
dress:Input(input, dress.layout:row())
end
function love.draw()
dress:draw()
end
.. warning::
Unlike UI and layout state, the theme might be shared with other states.
Changes in a shared theme will be shared across all themes.
See the :ref:`Instance Theme <instance-theme>` subsection in the
:doc:`gettingstarted` guide.

View file

@ -93,7 +93,7 @@ draw it in ``love.draw()``::
end end
function love.draw() function love.draw()
suit.core.draw() suit.draw()
end end
This will produce this UI (after clicking the button): This will produce this UI (after clicking the button):
@ -159,11 +159,11 @@ and ``textinput`` events to SUIT::
-- forward keyboard events -- forward keyboard events
function love.textinput(t) function love.textinput(t)
suit.core.textinput(t) suit.textinput(t)
end end
function love.keypressed(key) function love.keypressed(key)
suit.core.keypressed(key) suit.keypressed(key)
end end
.. image:: _static/keyboard.gif .. image:: _static/keyboard.gif
@ -193,33 +193,34 @@ The first example can be written as follows::
function love.update(dt) function love.update(dt)
-- put the layout origin at position (100,100) -- put the layout origin at position (100,100)
-- cells will grow down and to the right of the origin -- cells will grow down and to the right of the origin
suit.layout.reset(100,100) -- note the colon syntax
suit.layout:reset(100,100)
-- put 10 extra pixels between cells in each direction -- put 10 extra pixels between cells in each direction
suit.layout.padding(10,10) suit.layout:padding(10,10)
-- construct a cell of size 300x30 px and put the button into it -- construct a cell of size 300x30 px and put the button into it
if suit.Button("Hello, World!", suit.layout.row(300,30)).hit then if suit.Button("Hello, World!", suit.layout:row(300,30)).hit then
show_message = true show_message = true
end end
-- add another cell below the first cell -- add another cell below the first cell
-- the size of the cell is the same as the first cell -- the size of the cell is the same as the first cell
if show_message then if show_message then
suit.Label("How are you today?", suit.layout.row()) suit.Label("How are you today?", suit.layout:row())
end end
end end
function love.draw() function love.draw()
suit.core.draw() suit.draw()
end end
.. image:: _static/layout.gif .. image:: _static/layout.gif
At the beginning of each frame, the layout origin (and some internal layout At the beginning of each frame, the layout origin (and some internal layout
state) has to be reset. You can also define optional padding between cells. state) has to be reset. You can also define optional padding between cells.
Cells are added using ``layout.row(w,h)`` (which puts the new cell below the Cells are added using ``layout:row(w,h)`` (which puts the new cell below the
old cell) and ``layout.col(w,h)`` (which puts the new cell to the right of the old cell) and ``layout:col(w,h)`` (which puts the new cell to the right of the
old cell). If omitted, the width and height of the new cell are copied from old cell). If omitted, the width and height of the new cell are copied from
the old cell. There are also special identifiers that calculate the size from the old cell. There are also special identifiers that calculate the size from
the sizes of all cells that were created since the last ``reset()``: ``max``, the sizes of all cells that were created since the last ``reset()``: ``max``,
@ -235,31 +236,90 @@ Themeing
SUIT lets you customize how any widget (except :func:`ImageButton`) is drawn. SUIT lets you customize how any widget (except :func:`ImageButton`) is drawn.
Each widget (except, :func:`you know <ImageButton>`) is drawn by a function in Each widget (except, :func:`you know <ImageButton>`) is drawn by a function in
the table ``suit.core.theme``. Conveniently, the name of the function the table ``suit.theme``. Conveniently, the name of the function
responsible for drawing a widget is named after it, so, a button is drawn by responsible for drawing a widget is named after it, so, a button is drawn by
the function ``suit.core.theme.Button``. If you want to change how a button is the function ``suit.theme.Button``. If you want to change how a button is
drawn, simply overwrite the function. If you want to redecorate completely, it drawn, simply overwrite the function. If you want to redecorate completely, it
might be easiest to start from scratch and swap the whole table. might be easiest to start from scratch and swap the whole table.
However, if you just don't like the colors, the default theme is open to change. However, if you just don't like the colors, the default theme is open to change.
It requires you to change the background (``bg``) and foreground (``fg``) color It requires you to change the background (``bg``) and foreground (``fg``) color
of three possible widget states: ``normal``, when nothing out of of three possible widget states: ``normal``, when nothing out of
the ordinary happened, ``hover``, when the mouse hovers above a widget, and the ordinary happened, ``hovered``, when the mouse hovers above a widget, and
``active``, when the mouse hovers above, and the mouse button is pressed (but ``active``, when the mouse hovers above, and the mouse button is pressed (but
not yet released) on the widget. The colors are saved in the table not yet released) on the widget. The colors are saved in the table
``suit.core.theme.color``. The default color scheme is this:: ``suit.theme.color``. The default color scheme is this::
suit.core.theme.color = { suit.theme.color = {
normal = {bg = { 66, 66, 66}, fg = {188,188,188}}, normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hover = {bg = { 50,153,187}, fg = {255,255,255}}, hovered = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}} active = {bg = {255,153, 0}, fg = {225,225,225}}
} }
You can also do minimally invasive surgery:: You can also do minimally invasive surgery::
function love.load() function love.load()
suit.core.theme.color.normal.fg = {255,255,255} suit.theme.color.normal.fg = {255,255,255}
suit.core.theme.color.hover = {bg = {200,230,255}, fg = {0,0,0}} suit.theme.color.hovered = {bg = {200,230,255}, fg = {0,0,0}}
end
GUI Instances
-------------
Sometimes you might feel the need to separate parts of the GUI. Maybe the
widgets should have a different theme, maybe certain should always be drawn
before or after other UI elements, or maybe you don't want the UI state to
"leak" (e.g., from a stacked pause gamestate to the main gamestate).
For this reason, SUIT allows you to create GUI instances::
local dress = suit.new()
The IO and layout state of ``dress`` is totally contained in the instance and
does not affect any other instances (including the "global" instance ``suit``).
In particular, ``suit.draw()`` will not draw anything from ``dress``. Luckily,
you can do that yourself::
dress:draw()
Notice that instances require that you use the colon syntax. This is true for
every `core <core>` function as well as the widgets. To create a button, for
example, you have to write::
dress:Button("Click?", dress.layout:row())
.. _instance-theme:
Instance Theme
^^^^^^^^^^^^^^
Unlike UI and layout state, themes **are** shared among instances. The reason
is that the ``suit.theme`` and ``dress.theme`` are **references**, and point to
the same table (unless you make either of them point somewhere else). Usually
this is a feature, but please still consider this
.. warning::
Changes in a shared theme will be shared across GUI instances.
If this is an issue---for example because you only want to change the color
scheme of an instance---you can either `deep-copy
<http://hump.readthedocs.org/en/latest/class.html#class:clone>`_ the theme
table or use some metatable magic::
dress.theme = setmetatable({}, {__index = suit.theme})
-- NOTE: you have to replace the whole color table. E.g., replacing only
-- dress.theme.color.normal will also change suit.theme.color.normal!
dress.theme.color = {
normal = {bg = {188,188,188}, fg = { 66, 66, 66}},
hovered = {bg = {255,255,255}, fg = { 50,153,187}},
active = {bg = {255,255,255}, fg = {225,153, 0}}
}
function dress.theme.Label(text, opt, x,y,w,h)
-- draw the label in a fancier way
end end
.. [1] But it thinks you can handle that. .. [1] But it thinks you can handle that.

View file

@ -49,7 +49,7 @@ The following code will create this UI:
-- generate some assets (below) -- generate some assets (below)
function love.load() function love.load()
snd = generateClickySound() snd = generateClickySound()
normal, hover, active = generateImageButton() normal, hovered, active = generateImageButton()
smallerFont = love.graphics.newFont(10) smallerFont = love.graphics.newFont(10)
end end
@ -63,11 +63,11 @@ The following code will create this UI:
-- put the layout origin at position (100,100) -- put the layout origin at position (100,100)
-- cells will grown down and to the right from this point -- cells will grown down and to the right from this point
-- also set cell padding to 20 pixels to the right and to the bottom -- also set cell padding to 20 pixels to the right and to the bottom
suit.layout.reset(100,100, 20,20) suit.layout:reset(100,100, 20,20)
-- put a button at the layout origin -- put a button at the layout origin
-- the cell of the button has a size of 200 by 30 pixels -- the cell of the button has a size of 200 by 30 pixels
state = suit.Button("Click?", suit.layout.row(200,30)) state = suit.Button("Click?", suit.layout:row(200,30))
-- if the button was entered, play a sound -- if the button was entered, play a sound
if state.entered then love.audio.play(snd) end if state.entered then love.audio.play(snd) end
@ -78,67 +78,67 @@ The following code will create this UI:
-- put an input box below the button -- put an input box below the button
-- the cell of the input box has the same size as the cell above -- the cell of the input box has the same size as the cell above
-- if the input cell is submitted, print the text -- if the input cell is submitted, print the text
if suit.Input(input, suit.layout.row()).submitted then if suit.Input(input, suit.layout:row()).submitted then
print(input.text) print(input.text)
end end
-- put a button below the input box -- put a button below the input box
-- the width of the cell will be the same as above, the height will be 40 px -- the width of the cell will be the same as above, the height will be 40 px
if suit.Button("Hover?", suit.layout.row(nil,40)).hovered then if suit.Button("Hover?", suit.layout:row(nil,40)).hovered then
-- if the button is hovered, show two other buttons -- if the button is hovered, show two other buttons
-- this will shift all other ui elements down -- this will shift all other ui elements down
-- put a button below the previous button -- put a button below the previous button
-- the cell height will be 30 px -- the cell height will be 30 px
-- the label of the button will be aligned top left -- the label of the button will be aligned top left
suit.Button("You can see", {align='left', valign='top'}, suit.layout.row(nil,30)) suit.Button("You can see", {align='left', valign='top'}, suit.layout:row(nil,30))
-- put a button below the previous button -- put a button below the previous button
-- the cell size will be the same as the one above -- the cell size will be the same as the one above
-- the label will be aligned bottom right -- the label will be aligned bottom right
suit.Button("...but you can't touch!", {align='right', valign='bottom'}, suit.Button("...but you can't touch!", {align='right', valign='bottom'},
suit.layout.row()) suit.layout:row())
end end
-- put a checkbox below the button -- put a checkbox below the button
-- the size will be the same as above -- the size will be the same as above
-- (NOTE: height depends on whether "Hover?" is hovered) -- (NOTE: height depends on whether "Hover?" is hovered)
-- the label "Check?" will be aligned right -- the label "Check?" will be aligned right
suit.Checkbox(chk, {align='right'}, suit.layout.row()) suit.Checkbox(chk, {align='right'}, suit.layout:row())
-- put a nested layout -- put a nested layout
-- the size of the cell will be as big as the cell above or as big as the -- the size of the cell will be as big as the cell above or as big as the
-- nested content, whichever is bigger -- nested content, whichever is bigger
suit.layout.push(suit.layout.row()) suit.layout:push(suit.layout:row())
-- put a slider in the cell -- put a slider in the cell
-- the inner cell will be 160 px wide and 20 px high -- the inner cell will be 160 px wide and 20 px high
suit.Slider(slider, suit.layout.col(160, 20)) suit.Slider(slider, suit.layout:col(160, 20))
-- put a label that shows the slider value to the right of the slider -- put a label that shows the slider value to the right of the slider
-- the width of the label will be 40 px -- the width of the label will be 40 px
suit.Label(("%.02f"):format(slider.value), suit.layout.col(40)) suit.Label(("%.02f"):format(slider.value), suit.layout:col(40))
-- close the nested layout -- close the nested layout
suit.layout.pop() suit.layout:pop()
-- put an image button below the nested cell -- put an image button below the nested cell
-- the size of the cell will be 200 by 100 px, -- the size of the cell will be 200 by 100 px,
-- but the image may be bigger or smaller -- but the image may be bigger or smaller
-- the button shows the image `normal' when the mouse is outside the image -- the button shows the image `normal' when the mouse is outside the image
-- or above a transparent pixel -- or above a transparent pixel
-- the button shows the image `hover` if the mouse is above an opaque pixel -- the button shows the image `hovered` if the mouse is above an opaque pixel
-- of the image `normal' -- of the image `normal'
-- the button shows the image `active` if the mouse is above an opaque pixel -- the button shows the image `active` if the mouse is above an opaque pixel
-- of the image `normal' and the mouse button is pressed -- of the image `normal' and the mouse button is pressed
suit.ImageButton(normal, {hover = hover, active = active}, suit.layout.row(200,100)) suit.ImageButton(normal, {hovered = hovered, active = active}, suit.layout:row(200,100))
-- if the checkbox is checked, display a precomputed layout -- if the checkbox is checked, display a precomputed layout
if chk.checked then if chk.checked then
-- the precomputed layout will be 3 rows below each other -- the precomputed layout will be 3 rows below each other
-- the origin of the layout will be at (400,100) -- the origin of the layout will be at (400,100)
-- the minimal height of the layout will be 300 px -- the minimal height of the layout will be 300 px
rows = suit.layout.rows{pos = {400,100}, min_height = 300, rows = suit.layout:rows{pos = {400,100}, min_height = 300,
{200, 30}, -- the first cell will measure 200 by 30 px {200, 30}, -- the first cell will measure 200 by 30 px
{30, 'fill'}, -- the second cell will be 30 px wide and fill the {30, 'fill'}, -- the second cell will be 30 px wide and fill the
-- remaining vertical space between the other cells -- remaining vertical space between the other cells
@ -165,17 +165,17 @@ The following code will create this UI:
function love.draw() function love.draw()
-- draw the gui -- draw the gui
suit.core.draw() suit.draw()
end end
function love.textinput(t) function love.textinput(t)
-- forward text input to SUIT -- forward text input to SUIT
suit.core.textinput(t) suit.textinput(t)
end end
function love.keypressed(key) function love.keypressed(key)
-- forward keypressed to SUIT -- forward keypressed to SUIT
suit.core.keypressed(key) suit.keypressed(key)
end end
-- generate assets (see love.load) -- generate assets (see love.load)
@ -203,11 +203,11 @@ The following code will create this UI:
end end
end end
local normal, hover, active = love.image.newImageData(200,100), love.image.newImageData(200,100), love.image.newImageData(200,100) local normal, hovered, active = love.image.newImageData(200,100), love.image.newImageData(200,100), love.image.newImageData(200,100)
normal:mapPixel(metaballs(.48, 188,188,188)) normal:mapPixel(metaballs(.48, 188,188,188))
hover:mapPixel(metaballs(.46, 50,153,187)) hovered:mapPixel(metaballs(.46, 50,153,187))
active:mapPixel(metaballs(.43, 255,153,0)) active:mapPixel(metaballs(.43, 255,153,0))
return love.graphics.newImage(normal), love.graphics.newImage(hover), love.graphics.newImage(active) return love.graphics.newImage(normal), love.graphics.newImage(hovered), love.graphics.newImage(active)
end end
Indices and tables Indices and tables

View file

@ -93,9 +93,9 @@ The specification is a table of tables, where each inner table follows the
convention of :func:`row` and :func:`col`. convention of :func:`row` and :func:`col`.
The result is a layout definition object that can be used to access the cells. The result is a layout definition object that can be used to access the cells.
There is almost only one reason to do so: You know the area of your layout in There are almost only two reasons to do so: (1) You know the area of your
advance (say, the screen size), and want certain cells to dynamically fill the layout in advance (say, the screen size), and want certain cells to dynamically
available space. fill the available space; (2) You want to animate the cells.
.. note:: .. note::
Unlike immediate mode layouts, predefined layouts **can not be nested**. Unlike immediate mode layouts, predefined layouts **can not be nested**.
@ -139,8 +139,7 @@ define the position (upper left corner) of the layout using the ``pos`` keyword:
Layout Definition Objects Layout Definition Objects
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
Once constructed, the layout can be executed using a layout definition object Once constructed, the cells can be accessed in two ways:
in two ways:
- Using iterators:: - Using iterators::
@ -154,6 +153,27 @@ in two ways:
suit.Button("Button 3", definition.cell(3)) suit.Button("Button 3", definition.cell(3))
suit.Button("Button 2", definition.cell(2)) suit.Button("Button 2", definition.cell(2))
There is actually a third way: Because layout definitions are just tables, you
can access the cells directly::
local cell = definition[1]
suit.Button("Button 1", cell[1], cell[2], cell[3], cell[4])
-- or suit.Button("Button 1", unpack(cell))
This is especially useful if you want to animate the cells, for example with a
`tween <http://hump.readthedocs.org/en/latest/timer.html#Timer.tween>`_::
for i,cell in ipairs(definition)
local destination = {[2] = cell[2]} -- save cell y position
cell[2] = -cell[4] -- move cell just outside of the screen
-- let the cells fall into the screen one after another
timer.after(i / 10, function()
timer.tween(0.7, cell, destination, 'bounce')
end)
end
Constructors Constructors
^^^^^^^^^^^^ ^^^^^^^^^^^^

View file

@ -41,7 +41,7 @@ theme.
The argument ``normal`` defines the image of the normal state as well as the The argument ``normal`` defines the image of the normal state as well as the
area of the widget: The button activates when the mouse is over a pixel with area of the widget: The button activates when the mouse is over a pixel with
non-zero alpha value. non-zero alpha value.
You can provide additional ``hover`` and ``active`` images, but the widget area You can provide additional ``hovered`` and ``active`` images, but the widget area
is always computed from the ``normal`` image. is always computed from the ``normal`` image.
Note that ``ImageButton`` does not recieve width and height parameters. As Note that ``ImageButton`` does not recieve width and height parameters. As
@ -52,11 +52,11 @@ such, it does not necessarily honor the cell size of a :doc:`layout`.
``normal`` ``normal``
Image for the normal state of the widget. Defaults to widget payload. Image for the normal state of the widget. Defaults to widget payload.
``hover`` ``hovered``
Image for the hot state of the widget. Defaults to ``normal`` if omitted. Image for the hovered state of the widget. Defaults to ``normal`` if omitted.
``active`` ``active``
Image for the active state of the widget. Defaults to ``hover`` if omitted. Image for the active state of the widget. Defaults to ``hovered`` if omitted.
Mutable Widgets Mutable Widgets
--------------- ---------------

View file

@ -1,17 +1,16 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(normal, ...) return function(core, normal, ...)
local opt, x,y = core.getOptionsAndSize(...) local opt, x,y = core.getOptionsAndSize(...)
opt.normal = normal or opt.normal or opt[1] opt.normal = normal or opt.normal or opt[1]
opt.hover = opt.hover or opt[2] or opt.normal opt.hovered = opt.hovered or opt[2] or opt.normal
opt.active = opt.active or opt[3] or opt.hover opt.active = opt.active or opt[3] or opt.hovered
assert(opt.normal, "Need at least `normal' state image") assert(opt.normal, "Need at least `normal' state image")
opt.id = opt.id or opt.normal opt.id = opt.id or opt.normal
core.registerMouseHit(opt.id, x,y, function(u,v) opt.state = core:registerMouseHit(opt.id, x,y, function(u,v)
local id = opt.normal:getData() local id = opt.normal:getData()
assert(id:typeOf("ImageData"), "Can only use uncompressed images") assert(id:typeOf("ImageData"), "Can only use uncompressed images")
u, v = math.floor(u+.5), math.floor(v+.5) u, v = math.floor(u+.5), math.floor(v+.5)
@ -23,20 +22,20 @@ return function(normal, ...)
end) end)
local img = opt.normal local img = opt.normal
if core.isActive(opt.id) then if core:isActive(opt.id) then
img = opt.active img = opt.active
elseif core.isHot(opt.id) then elseif core:isHovered(opt.id) then
img = opt.hover img = opt.hovered
end end
core.registerDraw(love.graphics.setColor, 255,255,255) core:registerDraw(love.graphics.setColor, 255,255,255)
core.registerDraw(love.graphics.draw, img, x,y) core:registerDraw(love.graphics.draw, img, x,y)
return { return {
id = opt.id, id = opt.id,
hit = core.mouseReleasedOn(opt.id), hit = core:mouseReleasedOn(opt.id),
hover = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

View file

@ -1,14 +1,58 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...) .. '.' local BASE = (...) .. "."
local suit = require(BASE .. "core")
return { local instance = suit.new()
core = require(BASE .. 'core'), return setmetatable({
layout = require(BASE .. 'layout'), new = suit.new,
Button = require(BASE .. 'button'), getOptionsAndSize = suit.getOptionsAndSize,
ImageButton = require(BASE .. 'imagebutton'),
Slider = require(BASE .. 'slider'), -- core functions
Label = require(BASE .. 'label'), anyHovered = function(...) return instance:anyHovered(...) end,
Input = require(BASE .. 'input'), isHovered = function(...) return instance:isHovered(...) end,
Checkbox = require(BASE .. 'checkbox') wasHovered = function(...) return instance:wasHovered(...) end,
} isActive = function(...) return instance:isActive(...) end,
mouseInRect = function(...) return instance:mouseInRect(...) end,
registerHitbox = function(...) return instance:registerHitbox(...) end,
registerMouseHit = function(...) return instance:registerMouseHit(...) end,
mouseReleasedOn = function(...) return instance:mouseReleasedOn(...) end,
updateMouse = function(...) return instance:updateMouse(...) end,
getMousePosition = function(...) return instance:getMousePosition(...) end,
getPressedKey = function(...) return instance:getPressedKey(...) end,
keypressed = function(...) return instance:keypressed(...) end,
textinput = function(...) return instance:textinput(...) end,
grabKeyboardFocus = function(...) return instance:grabKeyboardFocus(...) end,
hasKeyboardFocus = function(...) return instance:hasKeyboardFocus(...) end,
keyPressedOn = function(...) return instance:keyPressedOn(...) end,
enterFrame = function(...) return instance:enterFrame(...) end,
exitFrame = function(...) return instance:exitFrame(...) end,
registerDraw = function(...) return instance:registerDraw(...) end,
draw = function(...) return instance:draw(...) end,
-- widgets
Button = function(...) return instance:Button(...) end,
ImageButton = function(...) return instance:ImageButton(...) end,
Label = function(...) return instance:Label(...) end,
Checkbox = function(...) return instance:Checkbox(...) end,
Input = function(...) return instance:Input(...) end,
Slider = function(...) return instance:Slider(...) end,
-- layout
layout = instance.layout
}, {
-- theme
__newindex = function(t, k, v)
if k == "theme" then
instance.theme = v
else
rawset(t, k, v)
end
end,
__index = function(t, k)
return k == "theme" and instance.theme or rawget(t, k)
end,
})

View file

@ -1,7 +1,6 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
local utf8 = require 'utf8' local utf8 = require 'utf8'
local function split(str, pos) local function split(str, pos)
@ -9,7 +8,7 @@ local function split(str, pos)
return str:sub(1, offset-1), str:sub(offset) return str:sub(1, offset-1), str:sub(offset)
end end
return function(input, ...) return function(core, input, ...)
local font = love.graphics.getFont() local font = love.graphics.getFont()
local opt, x,y,w,h = core.getOptionsAndSize(...) local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or input opt.id = opt.id or input
@ -26,13 +25,11 @@ return function(input, ...)
-- ... -- ...
-- position 6: hello| -- position 6: hello|
core.registerHitbox(opt.id, x,y,w,h) opt.state = core:registerHitbox(opt.id, x,y,w,h)
opt.hasKeyboardFocus = core:grabKeyboardFocus(opt.id)
core.grabKeyboardFocus(opt.id)
opt.hasKeyboardFocus = core.hasKeyboardFocus(opt.id)
if opt.hasKeyboardFocus then if opt.hasKeyboardFocus then
local keycode,char = core.getPressedKey() local keycode,char = core:getPressedKey()
-- text input -- text input
if char ~= "" then if char ~= "" then
local a,b = split(input.text, input.cursor) local a,b = split(input.text, input.cursor)
@ -66,14 +63,14 @@ return function(input, ...)
-- TODO -- TODO
end end
core.registerDraw(core.theme.Input, input, opt, x,y,w,h) core:registerDraw(core.theme.Input, input, opt, x,y,w,h)
return { return {
id = opt.id, id = opt.id,
hit = core.mouseReleasedOn(opt.id), hit = core:mouseReleasedOn(opt.id),
submitted = core.keyPressedOn(opt.id, "return"), submitted = core:keyPressedOn(opt.id, "return"),
hovered = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

View file

@ -1,9 +1,8 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(text, ...) return function(core, text, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...) local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or text opt.id = opt.id or text
opt.font = opt.font or love.graphics.getFont() opt.font = opt.font or love.graphics.getFont()
@ -11,14 +10,14 @@ return function(text, ...)
w = w or opt.font:getWidth(text) + 4 w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4 h = h or opt.font:getHeight() + 4
core.registerHitbox(opt.id, x,y,w,h) opt.state = core:registerHitbox(opt.id, x,y,w,h)
core.registerDraw(core.theme.Label, text, opt, x,y,w,h) core:registerDraw(core.theme.Label, text, opt, x,y,w,h)
return { return {
id = opt.id, id = opt.id,
hit = core.mouseReleasedOn(opt.id), hit = core:mouseReleasedOn(opt.id),
hovered = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

View file

@ -1,9 +1,8 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter -- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local BASE = (...):match('(.-)[^%.]+$') local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(info, ...) return function(core, info, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...) local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or info opt.id = opt.id or info
@ -14,11 +13,11 @@ return function(info, ...)
local fraction = (info.value - info.min) / (info.max - info.min) local fraction = (info.value - info.min) / (info.max - info.min)
local value_changed = false local value_changed = false
core.registerHitbox(opt.id, x,y,w,h) opt.state = core:registerHitbox(opt.id, x,y,w,h)
if core.isActive(opt.id) then if core:isActive(opt.id) then
-- mouse update -- mouse update
local mx,my = core.getMousePosition() local mx,my = core:getMousePosition()
if opt.vertical then if opt.vertical then
fraction = math.min(1, math.max(0, (y+h - my) / h)) fraction = math.min(1, math.max(0, (y+h - my) / h))
else else
@ -33,23 +32,23 @@ return function(info, ...)
-- keyboard update -- keyboard update
local key_up = opt.vertical and 'up' or 'right' local key_up = opt.vertical and 'up' or 'right'
local key_down = opt.vertical and 'down' or 'left' local key_down = opt.vertical and 'down' or 'left'
if core.getPressedKey() == key_up then if core:getPressedKey() == key_up then
info.value = math.min(info.max, info.value + info.step) info.value = math.min(info.max, info.value + info.step)
value_changed = true value_changed = true
elseif core.getPressedKey() == key_down then elseif core:getPressedKey() == key_down then
info.value = math.max(info.min, info.value - info.step) info.value = math.max(info.min, info.value - info.step)
value_changed = true value_changed = true
end end
end end
core.registerDraw(core.theme.Slider, fraction, opt, x,y,w,h) core:registerDraw(core.theme.Slider, fraction, opt, x,y,w,h)
return { return {
id = opt.id, id = opt.id,
hit = core.mouseReleasedOn(opt.id), hit = core:mouseReleasedOn(opt.id),
changed = value_changed, changed = value_changed,
hovered = core.isHot(opt.id), hovered = core:isHovered(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id), entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id) left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
} }
end end

View file

@ -7,25 +7,15 @@ theme.cornerRadius = 4
theme.color = { theme.color = {
normal = {bg = { 66, 66, 66}, fg = {188,188,188}}, normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hover = {bg = { 50,153,187}, fg = {255,255,255}}, hovered = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}} active = {bg = {255,153, 0}, fg = {225,225,225}}
} }
-- HELPER -- HELPER
function theme.getStateName(id)
if theme.core.isActive(id) then
return 'active'
end
if theme.core.isHot(id) then
return 'hover'
end
return 'normal'
end
function theme.getColorForState(opt) function theme.getColorForState(opt)
local s = theme.getStateName(opt.id) local s = opt.state or "normal"
return (opt.color and opt.color[s]) or theme.color[s] return (opt.color and opt.color[opt.state]) or theme.color[s]
end end
function theme.drawBox(x,y,w,h, colors) function theme.drawBox(x,y,w,h, colors)
@ -96,10 +86,9 @@ function theme.Slider(fraction, opt, x,y,w,h)
local c = theme.getColorForState(opt) local c = theme.getColorForState(opt)
theme.drawBox(x,y,w,h, c) theme.drawBox(x,y,w,h, c)
love.graphics.setColor(c.fg) theme.drawBox(x,yb,wb,hb, {bg=c.fg})
love.graphics.rectangle('fill', x,yb,wb,hb, theme.cornerRadius)
if theme.getStateName(opt.id) ~= "normal" then if opt.state ~= nil and opt.state ~= "normal" then
love.graphics.setColor((opt.color and opt.color.active or {}).fg or theme.color.active.fg) love.graphics.setColor((opt.color and opt.color.active or {}).fg or theme.color.active.fg)
if opt.vertical then if opt.vertical then
love.graphics.circle('fill', x+wb/2, yb, r) love.graphics.circle('fill', x+wb/2, yb, r)