From aca8a297bbb2370c56d3d7a887eaeadefc1657dc Mon Sep 17 00:00:00 2001 From: Matthias Richter Date: Sun, 3 Jan 2016 18:33:39 +0100 Subject: [PATCH] Fix #17: Support multiple instances --- button.lua | 15 ++-- checkbox.lua | 15 ++-- core.lua | 191 ++++++++++++++++++++-------------------- docs/core.rst | 49 +++++++++-- docs/gettingstarted.rst | 100 ++++++++++++++++----- docs/index.rst | 42 ++++----- docs/layout.rst | 30 +++++-- docs/widgets.rst | 8 +- imagebutton.lua | 27 +++--- init.lua | 66 +++++++++++--- input.lua | 23 +++-- label.lua | 15 ++-- slider.lua | 23 +++-- theme.lua | 25 ++---- 14 files changed, 385 insertions(+), 244 deletions(-) diff --git a/button.lua b/button.lua index 8094c60..e985f06 100644 --- a/button.lua +++ b/button.lua @@ -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 diff --git a/checkbox.lua b/checkbox.lua index 3f634e5..ce6b357 100644 --- a/checkbox.lua +++ b/checkbox.lua @@ -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 diff --git a/core.lua b/core.lua index 466eaec..e1f1254 100644 --- a/core.lua +++ b/core.lua @@ -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 diff --git a/docs/core.rst b/docs/core.rst index 799a554..d2f7622 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -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 ` subsection in the + :doc:`gettingstarted` guide. diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index 19334fb..2d87691 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -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 `) 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 ` 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 +`_ 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. diff --git a/docs/index.rst b/docs/index.rst index 5ae8f06..830b19f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/docs/layout.rst b/docs/layout.rst index 5333923..c82f0a1 100644 --- a/docs/layout.rst +++ b/docs/layout.rst @@ -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 `_:: + + 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 ^^^^^^^^^^^^ diff --git a/docs/widgets.rst b/docs/widgets.rst index 1fe6180..bac5bc9 100644 --- a/docs/widgets.rst +++ b/docs/widgets.rst @@ -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 --------------- diff --git a/imagebutton.lua b/imagebutton.lua index dffd5a1..f34ca83 100644 --- a/imagebutton.lua +++ b/imagebutton.lua @@ -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 diff --git a/init.lua b/init.lua index c43d45e..9542735 100644 --- a/init.lua +++ b/init.lua @@ -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, +}) diff --git a/input.lua b/input.lua index b52f33e..cc8b921 100644 --- a/input.lua +++ b/input.lua @@ -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 diff --git a/label.lua b/label.lua index 6628664..57ba5c1 100644 --- a/label.lua +++ b/label.lua @@ -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 diff --git a/slider.lua b/slider.lua index 519cc78..db25faf 100644 --- a/slider.lua +++ b/slider.lua @@ -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 diff --git a/theme.lua b/theme.lua index 989fbcc..5cf46c3 100644 --- a/theme.lua +++ b/theme.lua @@ -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)