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
local BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
return function(text, ...)
return function(core, text, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...)
opt.id = opt.id or text
opt.font = opt.font or love.graphics.getFont()
@ -11,14 +10,14 @@ return function(text, ...)
w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4
core.registerHitbox(opt.id, x,y,w,h)
core.registerDraw(core.theme.Button, text, opt, 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)
return {
id = opt.id,
hit = core.mouseReleasedOn(opt.id),
hovered = core.isHot(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id)
hit = core:mouseReleasedOn(opt.id),
hovered = core:isHovered(opt.id),
entered = core:isHovered(opt.id) and not core:wasHovered(opt.id),
left = not core:isHovered(opt.id) and core:wasHovered(opt.id)
}
end

View file

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

191
core.lua
View file

@ -1,10 +1,34 @@
-- This file is part of SUIT, copyright (c) 2016 Matthias Richter
local NONE = {}
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
local function getOptionsAndSize(opt, ...)
function suit.getOptionsAndSize(opt, ...)
if type(opt) == "table" then
return opt, ...
end
@ -12,75 +36,83 @@ local function getOptionsAndSize(opt, ...)
end
-- gui state
local hot, hot_last, active
local NONE = {}
local function anyHot()
return hot ~= nil
function suit:anyHovered()
return self.hovered ~= nil
end
local function isHot(id)
return id == hot
function suit:isHovered(id)
return id == self.hovered
end
local function wasHot(id)
return id == hot_last
function suit:wasHovered(id)
return id == self.hovered_last
end
local function isActive(id)
return id == active
function suit:isActive(id)
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
-- mouse handling
local mouse_x, mouse_y, mouse_button_down = 0,0, false
local function mouseInRect(x,y,w,h)
return not (mouse_x < x or mouse_y < y or mouse_x > x+w or mouse_y > y+h)
function suit:mouseInRect(x,y,w,h)
return self.mouse_x >= x and self.mouse_y >= y and
self.mouse_x <= x+w and self.mouse_y > y+h
end
local function registerMouseHit(id, ul_x, ul_y, hit)
if hit(mouse_x - ul_x, mouse_y - ul_y) then
hot = id
if active == nil and mouse_button_down then
active = id
function suit:registerMouseHit(id, ul_x, ul_y, hit)
if hit(self.mouse_x - ul_x, self.mouse_y - ul_y) then
self.hovered = id
if self.active == nil and self.mouse_button_down then
self.active = id
end
end
return self:getStateName(id)
end
local function registerHitbox(id, x,y,w,h)
return registerMouseHit(id, x,y, function(x,y)
function suit:registerHitbox(id, x,y,w,h)
return self:registerMouseHit(id, x,y, function(x,y)
return x >= 0 and x <= w and y >= 0 and y <= h
end)
end
local function mouseReleasedOn(id)
return not mouse_button_down and isActive(id) and isHot(id)
function suit:mouseReleasedOn(id)
return not self.mouse_button_down and self:isActive(id) and self:isHovered(id)
end
local function updateMouse(x, y, button_down)
mouse_x, mouse_y, mouse_button_down = x,y, button_down
function suit:updateMouse(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
local function getMousePosition()
return mouse_x, mouse_y
function suit:getMousePosition()
return self.mouse_x, self.mouse_y
end
-- keyboard handling
local key_down, textchar, keyboardFocus
local function getPressedKey()
return key_down, textchar
function suit:getPressedKey()
return self.key_down, self.textchar
end
local function keypressed(key)
key_down = key
function suit:keypressed(key)
self.key_down = key
end
local function textinput(char)
textchar = char
function suit:textinput(char)
self.textchar = char
end
local function grabKeyboardFocus(id)
if isActive(id) then
keyboardFocus = id
function suit:grabKeyboardFocus(id)
if self:isActive(id) then
if love.system.getOS() == "Android" or love.system.getOS() == "iOS" then
if id == NONE then
love.keyboard.setTextInput( false )
@ -88,81 +120,52 @@ local function grabKeyboardFocus(id)
love.keyboard.setTextInput( true )
end
end
self.keyboardFocus = id
end
return self:hasKeyboardFocus(id)
end
local function hasKeyboardFocus(id)
return keyboardFocus == id
function suit:hasKeyboardFocus(id)
return self.keyboardFocus == id
end
local function keyPressedOn(id, key)
return hasKeyboardFocus(id) and key_down == key
function suit:keyPressedOn(id, key)
return self:hasKeyboardFocus(id) and self.key_down == key
end
-- state update
local function enterFrame()
hot_last, hot = hot, nil
updateMouse(love.mouse.getX(), love.mouse.getY(), love.mouse.isDown(1))
key_down, textchar = nil, ""
grabKeyboardFocus(NONE)
function suit:enterFrame()
self.hovered_last, self.hovered = self.hovered, nil
self:updateMouse(love.mouse.getX(), love.mouse.getY(), love.mouse.isDown(1))
self.key_down, self.textchar = nil, ""
self:grabKeyboardFocus(NONE)
end
local function exitFrame()
if not mouse_button_down then
active = nil
elseif active == nil then
active = NONE
function suit:exitFrame()
if not self.mouse_button_down then
self.active = nil
elseif self.active == nil then
self.active = NONE
end
end
-- draw
local draw_queue = {n = 0}
local function registerDraw(f, ...)
function suit:registerDraw(f, ...)
local args = {...}
local nargs = select('#', ...)
draw_queue.n = draw_queue.n + 1
draw_queue[draw_queue.n] = function()
self.draw_queue.n = self.draw_queue.n + 1
self.draw_queue[self.draw_queue.n] = function()
f(unpack(args, 1, nargs))
end
end
local function draw()
exitFrame()
for i = 1,draw_queue.n do
draw_queue[i]()
function suit:draw()
self:exitFrame()
for i = 1,self.draw_queue.n do
self.draw_queue[i]()
end
draw_queue.n = 0
enterFrame()
self.draw_queue.n = 0
self:enterFrame()
end
local module = {
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
return suit

View file

@ -76,23 +76,23 @@ Clears GUI state when exiting a frame.
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.
: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.
.. function:: wasHot(id)
.. function:: wasHovered(id)
: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.
@ -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.
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
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
``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
function love.draw()
suit.core.draw()
suit.draw()
end
This will produce this UI (after clicking the button):
@ -159,11 +159,11 @@ and ``textinput`` events to SUIT::
-- forward keyboard events
function love.textinput(t)
suit.core.textinput(t)
suit.textinput(t)
end
function love.keypressed(key)
suit.core.keypressed(key)
suit.keypressed(key)
end
.. image:: _static/keyboard.gif
@ -193,33 +193,34 @@ The first example can be written as follows::
function love.update(dt)
-- put the layout origin at position (100,100)
-- 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
suit.layout.padding(10,10)
suit.layout:padding(10,10)
-- 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
end
-- add another cell below the first cell
-- the size of the cell is the same as the first cell
if show_message then
suit.Label("How are you today?", suit.layout.row())
suit.Label("How are you today?", suit.layout:row())
end
end
function love.draw()
suit.core.draw()
suit.draw()
end
.. image:: _static/layout.gif
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.
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
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). 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 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.
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
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
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.
It requires you to change the background (``bg``) and foreground (``fg``) color
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
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 = {
normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hover = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}}
suit.theme.color = {
normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hovered = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}}
}
You can also do minimally invasive surgery::
function love.load()
suit.core.theme.color.normal.fg = {255,255,255}
suit.core.theme.color.hover = {bg = {200,230,255}, fg = {0,0,0}}
suit.theme.color.normal.fg = {255,255,255}
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
.. [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)
function love.load()
snd = generateClickySound()
normal, hover, active = generateImageButton()
normal, hovered, active = generateImageButton()
smallerFont = love.graphics.newFont(10)
end
@ -63,11 +63,11 @@ The following code will create this UI:
-- put the layout origin at position (100,100)
-- 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
suit.layout.reset(100,100, 20,20)
suit.layout:reset(100,100, 20,20)
-- put a button at the layout origin
-- 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 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
-- the cell of the input box has the same size as the cell above
-- 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)
end
-- put a button below the input box
-- 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
-- this will shift all other ui elements down
-- put a button below the previous button
-- the cell height will be 30 px
-- 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
-- the cell size will be the same as the one above
-- the label will be aligned bottom right
suit.Button("...but you can't touch!", {align='right', valign='bottom'},
suit.layout.row())
suit.layout:row())
end
-- put a checkbox below the button
-- the size will be the same as above
-- (NOTE: height depends on whether "Hover?" is hovered)
-- 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
-- the size of the cell will be as big as the cell above or as big as the
-- nested content, whichever is bigger
suit.layout.push(suit.layout.row())
suit.layout:push(suit.layout:row())
-- put a slider in the cell
-- 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
-- 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
suit.layout.pop()
suit.layout:pop()
-- put an image button below the nested cell
-- the size of the cell will be 200 by 100 px,
-- but the image may be bigger or smaller
-- the button shows the image `normal' when the mouse is outside the image
-- 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'
-- the button shows the image `active` if the mouse is above an opaque pixel
-- 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 chk.checked then
-- the precomputed layout will be 3 rows below each other
-- the origin of the layout will be at (400,100)
-- 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
{30, 'fill'}, -- the second cell will be 30 px wide and fill the
-- remaining vertical space between the other cells
@ -165,17 +165,17 @@ The following code will create this UI:
function love.draw()
-- draw the gui
suit.core.draw()
suit.draw()
end
function love.textinput(t)
-- forward text input to SUIT
suit.core.textinput(t)
suit.textinput(t)
end
function love.keypressed(key)
-- forward keypressed to SUIT
suit.core.keypressed(key)
suit.keypressed(key)
end
-- generate assets (see love.load)
@ -203,11 +203,11 @@ The following code will create this UI:
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))
hover:mapPixel(metaballs(.46, 50,153,187))
hovered:mapPixel(metaballs(.46, 50,153,187))
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
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`.
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
advance (say, the screen size), and want certain cells to dynamically fill the
available space.
There are almost only two reasons to do so: (1) You know the area of your
layout in advance (say, the screen size), and want certain cells to dynamically
fill the available space; (2) You want to animate the cells.
.. note::
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
^^^^^^^^^^^^^^^^^^^^^^^^^
Once constructed, the layout can be executed using a layout definition object
in two ways:
Once constructed, the cells can be accessed in two ways:
- Using iterators::
@ -154,6 +153,27 @@ in two ways:
suit.Button("Button 3", definition.cell(3))
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
^^^^^^^^^^^^

View file

@ -41,7 +41,7 @@ theme.
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
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.
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``
Image for the normal state of the widget. Defaults to widget payload.
``hover``
Image for the hot state of the widget. Defaults to ``normal`` if omitted.
``hovered``
Image for the hovered state of the widget. Defaults to ``normal`` if omitted.
``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
---------------

View file

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

View file

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

View file

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

View file

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

View file

@ -6,26 +6,16 @@ local theme = {}
theme.cornerRadius = 4
theme.color = {
normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hover = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}}
normal = {bg = { 66, 66, 66}, fg = {188,188,188}},
hovered = {bg = { 50,153,187}, fg = {255,255,255}},
active = {bg = {255,153, 0}, fg = {225,225,225}}
}
-- 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)
local s = theme.getStateName(opt.id)
return (opt.color and opt.color[s]) or theme.color[s]
local s = opt.state or "normal"
return (opt.color and opt.color[opt.state]) or theme.color[s]
end
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)
theme.drawBox(x,y,w,h, c)
love.graphics.setColor(c.fg)
love.graphics.rectangle('fill', x,yb,wb,hb, theme.cornerRadius)
theme.drawBox(x,yb,wb,hb, {bg=c.fg})
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)
if opt.vertical then
love.graphics.circle('fill', x+wb/2, yb, r)