diff --git a/.gitignore b/.gitignore index dc5c077..b26399e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ main.lua -quickie *.love diff --git a/README.md b/README.md index ba9d88e..94b666d 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,177 @@ -# QUICKIE +# SUIT -Quickie is an [immediate mode gui][IMGUI] library for [LÖVE][LOVE]. Initial inspiration came from the article [Sol on Immediate Mode GUIs (IMGUI)][Sol]. You should check it out to understand how Quickie works. +Simple User Interface Toolkit for LÖVE. + +## Immediate mode GUI + +SUIT is an immediate mode GUI library. + +With classical (retained) mode libraries you typically have a stage where you +create the whole UI when the program initializes. After that point, the GUI +does not change much. + +With immediate mode libraries, on the other hand, the GUI is created every +frame from scratch. There are no widget objects, only functions that draw the +widget and update internal GUI state. This allows to put the widgets in their +immediate conceptual context (instead of a construction stage). It also makes +the UI very flexible: Don't want to draw a widget? Simply remove the call. +Handling the mutable data (e.g., text of an input box) of each widget is your +responsibility. This separation of behaviour and data + +## What SUIT is + +SUIT is simple: It provides only the most important widgets for games: + +- Buttons (including Image Buttons) +- Checkboxes +- Text Input +- Value Sliders + +SUIT is comfortable: It features a simple, yet effective row/column-based +layouting engine. + +SUIT is adaptable: You can easily alter the color scheme, change how widgets +are drawn or swap the whole theme. + +SUIT is hackable: The core library can be used to construct new widgets with +relative ease. + +**SUIT is good at games** + +## What SUIT is not + +SUIT is not a complete GUI library: It does not provide dropdowns, subwindows, +radio buttons, menu bars, ribbons, etc. + +SUIT is not a complete GUI library: SUIT spits MVC and other good OO practices +in the face. + +SUIT is not a complete GUI library: There is no markup language to generate or +style the GUI. + +**SUIT is not good at "serious" applications** + +## Example code + +## Documentation + +To be done. + +## Example code -# Example +```lua +suit = require 'suit' - local gui = require "Quickie" +function love.load() + -- generate some assets + snd = generateClickySound() + normal, hot = generateImageButton() + smallerFont = love.graphics.newFont(10) +end - function love.load() - -- preload fonts - fonts = { - [12] = love.graphics.newFont(12), - [20] = love.graphics.newFont(20), +-- mutable widget data +local slider= {value = .5, max = 2} +local input = {text = "Hello"} +local chk = {text = "Check me out"} + +function love.update(dt) + -- new layout at 100,100 with a padding of 20x20 px + suit.layout.reset(100,100, 20,20) + + -- Button + state = suit.Button("Hover me!", suit.layout.row(200,30)) + if state.entered then + love.audio.play(snd) + end + if state.hit then + print("Ouch!") + end + + -- Input box + if suit.Input(input, suit.layout.row()).submitted then + print(input.text) + end + + -- dynamically add widgets + if suit.Button("test2", suit.layout.row(nil,40)).hovered then + -- drawing options can be provided for each widget ... optionally + suit.Button("You can see", {align='left', valign='top'}, suit.layout.row(nil,30)) + suit.Button("...but you can't touch!", {align='right', valign='bottom'}, suit.layout.row(nil,30)) + end + + -- Checkbox + suit.Checkbox(chk, {align='right'}, suit.layout.row()) + + -- nested layouts + suit.layout.push(suit.layout.row()) + suit.Slider(slider, suit.layout.col(160, 20)) + suit.Label(("%.02f"):format(slider.value), suit.layout.col(40)) + suit.layout.pop() + + -- image buttons + suit.ImageButton({normal, hot = hot}, suit.layout.row(200,100)) + + if chk.checked then + -- precomputed layout can fill up available space + suit.layout.reset() + rows = suit.layout.rows{pos = {400,100}, + min_height = 300, + {200, 30}, + {30, 'fill'}, + {200, 30}, } - love.graphics.setBackgroundColor(17,17,17) - love.graphics.setFont(fonts[12]) + suit.Label("You uncovered the secret!", {align="left", font = smallerFont}, rows.item(1)) + suit.Label(slider.value, {align='left'}, rows.item(3)) - -- group defaults - gui.group.default.size[1] = 150 - gui.group.default.size[2] = 25 - gui.group.default.spacing = 5 + -- give different id to slider on same object so they don't grab + -- each others user interaction + suit.Slider(slider, {id = 'vs', vertical=true}, rows.item(2)) + print(rows.item(3)) end +end - local menu_open = { - main = false, - right = false, - foo = false, - demo = false - } - local check1 = false - local check2 = false - local input = {text = ""} - local slider = {value = .5} - local slider2d = {value = {.5,.5}} +function love.draw() + -- draw the gui + suit.core.draw() +end - function love.update(dt) - gui.group.push{grow = "down", pos = {5,5}} +-- forward keyboard events +function love.textinput(t) + suit.core.textinput(t) +end - -- all widgets return true if they are clicked on/activated - if gui.Checkbox{checked = menu_open.main, text = "Show Menu"} then - menu_open.main = not menu_open.main - end +function love.keypressed(key) + suit.core.keypressed(key) +end - if menu_open.main then - gui.group.push{grow = "right"} - - -- widgets can have custom ID's for tooltips etc (see below) - if gui.Button{id = "group stacking", text = "Group stacking"} then - menu_open.right = not menu_open.right - end - - if menu_open.right then - gui.group.push{grow = "up"} - if gui.Button{text = "Foo"} then - menu_open.foo = not menu_open.foo - end - if menu_open.foo then - gui.Button{text = "???"} - end - gui.group.pop{} - - gui.Button{text = "Bar"} - gui.Button{text = "Baz"} - end - gui.group.pop{} - - if gui.Button{text = "Widget demo"} then - menu_open.demo = not menu_open.open - end - - end - gui.group.pop{} - - if menu_open.demo then - gui.group{grow = "down", pos = {200, 80}, function() - love.graphics.setFont(fonts[20]) - gui.Label{text = "Widgets"} - love.graphics.setFont(fonts[12]) - gui.group{grow = "right", function() - gui.Button{text = "Button"} - gui.Button{text = "Tight Button", size = {"tight"}} - gui.Button{text = "Tight² Button", size = {"tight", "tight"}} - end} - - gui.group{grow = "right", function() - gui.Button{text = "", size = {2}} -- acts as separator - gui.Label{text = "Tight Label", size = {"tight"}} - gui.Button{text = "", size = {2}} - gui.Label{text = "Center Label", align = "center"} - gui.Button{text = "", size = {2}} - gui.Label{text = "Another Label"} - gui.Button{text = "", size = {2}} - end} - - gui.group.push{grow = "right"} - if gui.Checkbox{checked = check1, text = "Checkbox", size = {"tight"}} then - check1 = not check1 - print(check1) - end - if gui.Checkbox{checked = check2, text = "Another Checkbox"} then - check2 = not check2 - end - if gui.Checkbox{checked = check2, text = "Linked Checkbox"} then - check2 = not check2 - end - gui.group.pop{} - - gui.group{grow = "right", function() - gui.Label{text = "Input", size = {70}} - gui.Input{info = input, size = {300}} - end} - - gui.group{grow = "right", function() - gui.Label{text = "Slider", size = {70}} - gui.Slider{info = slider} - gui.Label{text = ("Value: %.2f"):format(slider.value), size = {70}} - end} - - gui.Label{text = "2D Slider", pos = {nil,10}} - gui.Slider2D{info = slider2d, size = {250, 250}} - gui.Label{text = ("Value: %.2f, %.2f"):format(slider2d.value[1], slider2d.value[2])} - end} - end - - -- tooltip (see above) - if gui.mouse.isHot('group stacking') then - local mx,my = love.mouse.getPosition() - gui.Label{text = 'Demonstrates group stacking', pos = {mx+10,my-20}} - end +-- generate assets (see love.load) +function generateClickySound() + local snd = love.sound.newSoundData(512, 44100, 16, 1) + for i = 0,snd:getSampleCount()-1 do + local t = i / 44100 + local s = i / snd:getSampleCount() + snd:setSample(i, (.7*(2*love.math.random()-1) + .3*math.sin(t*9000*math.pi)) * (1-s)^1.2 * .3) end + return love.audio.newSource(snd) +end - function love.draw() - gui.core.draw() - end - - function love.keypressed(key, code) - gui.keyboard.pressed(key) - -- LÖVE 0.8: see if this code can be converted in a character - if pcall(string.char, code) and code > 0 then - gui.keyboard.textinput(string.char(code)) +function generateImageButton() + local normal, hot = love.image.newImageData(200,100), love.image.newImageData(200,100) + normal:mapPixel(function(x,y) + local d = (x/200-.5)^2 + (y/100-.5)^2 + if d < .12 then + return 200,160,20,255 end - end - - -- LÖVE 0.9 - function love.textinput(str) - gui.keyboard.textinput(str) - end - -# Documentation - -To be done... - - -# License - -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -[LOVE]: http://love2d.org -[IMGUI]: http://www.mollyrocket.com/forums/viewforum.php?f=10 -[Sol]: http://sol.gfxile.net/imgui/ + return 0,0,0,0 + end) + hot:mapPixel(function(x,y) + local d = (x/200-.5)^2 + (y/100-.5)^2 + if d < .13 then + return 255,255,255,255 + end + return 0,0,0,0 + end) + return love.graphics.newImage(normal), love.graphics.newImage(hot) +end +``` diff --git a/button.lua b/button.lua index a1f0589..6131477 100644 --- a/button.lua +++ b/button.lua @@ -1,77 +1,24 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +return function(text, ...) + local opt, x,y,w,h = core.getOptionsAndSize(...) + opt.id = opt.id or text + opt.font = opt.font or love.graphics.getFont() -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. + w = w or opt.font:getWidth(text) + 4 + h = h or opt.font:getHeight() + 4 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') + core.registerHitbox(opt.id, x,y,w,h) + core.registerDraw(core.theme.Button, text, opt, x,y,w,h) --- the widget --- {text = text, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == "table" and w.text, "Invalid argument") - - -- if tight fit requested, compute the size according to text size - -- have a 2px margin around the text - local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight') - if tight then - local f = assert(love.graphics.getFont()) - if w.size[1] == 'tight' then - w.size[1] = f:getWidth(w.text) + 4 - end - if w.size[2] == 'tight' then - w.size[2] = f:getHeight(w.text) + 4 - end - end - - -- Generate unique identifier for gui state update and querying. - local id = w.id or core.generateID() - - -- group.getRect determines the position and size of the widget according - -- to the currently active group. Both arguments may be omitted. - local pos, size = group.getRect(w.pos, w.size) - - -- mouse.updateWidget(id, {x,y}, {w,h}, widgetHit) updates the state for this widget. - -- widgetHit may be nil, in which case mouse.widgetHit will be used. - -- The widget mouse-state can be: - -- hot (mouse over widget), - -- active (mouse pressed on widget) or - -- normal (mouse not on widget and not pressed on widget). - mouse.updateWidget(id, pos, size, w.widgetHit) - - -- keyboard.makeCyclable makes the item focus on tab or whatever binding is - -- in place (see core.keyboard.cycle). Cycle order is determied by the - -- order you call the widget functions. - keyboard.makeCyclable(id) - - -- core.registerDraw(id, drawfunction, drawfunction-arguments...) - -- shows widget when core.draw() is called. - core.registerDraw(id, w.draw or core.style.Button, - w.text, pos[1],pos[2], size[1],size[2]) - - return mouse.releasedOn(id) or keyboard.pressedOn(id, 'return') + 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) + } end - diff --git a/checkbox.lua b/checkbox.lua index 5137bb0..aac2044 100644 --- a/checkbox.lua +++ b/checkbox.lua @@ -1,68 +1,28 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +return function(checkbox, ...) + local opt, x,y,w,h = core.getOptionsAndSize(...) + opt.id = opt.id or checkbox + opt.font = opt.font or love.graphics.getFont() -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. + w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4) + h = h or opt.font:getHeight() + 4 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') - --- {checked = status, text = "", algin = "left", pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == "table") - w.text = w.text or "" - - local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight') - if tight then - local f = assert(love.graphics.getFont()) - if w.size[1] == 'tight' then - w.size[1] = f:getWidth(w.text) - end - if w.size[2] == 'tight' then - w.size[2] = f:getHeight(w.text) - end - -- account for the checkbox - local bw = math.min(w.size[1] or group.size[1], w.size[2] or group.size[2]) - w.size[1] = w.size[1] + bw + 4 + 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) - local id = w.id or core.generateID() - local pos, size = group.getRect(w.pos, w.size) - - mouse.updateWidget(id, pos, size, w.widgetHit) - keyboard.makeCyclable(id) - - local checked = w.checked - local key = keyboard.key - if mouse.releasedOn(id) or ((key == 'return' or key == ' ') and keyboard.hasFocus(id)) then - w.checked = not w.checked - end - - core.registerDraw(id, w.draw or core.style.Checkbox, - w.checked, w.text, w.align or 'left', pos[1], pos[2], size[1], size[2]) - - return w.checked ~= checked + 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) + } end - diff --git a/core.lua b/core.lua index 42b1ae1..fbb1bb5 100644 --- a/core.lua +++ b/core.lua @@ -1,127 +1,162 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local theme = require(BASE..'theme') -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local BASE = (...):match("(.-)[^%.]+$") -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') - --- --- Helper functions --- - --- evaluates all arguments -local function strictAnd(...) - local n = select("#", ...) - local ret = true - for i = 1,n do ret = select(i, ...) and ret end - return ret -end - -local function strictOr(...) - local n = select("#", ...) - local ret = false - for i = 1,n do ret = select(i, ...) or ret end - return ret -end - --- --- Widget ID --- -local maxid, uids = 0, {} -setmetatable(uids, {__index = function(t, i) - t[i] = {} - return t[i] -end}) - -local function generateID() - maxid = maxid + 1 - return uids[maxid] -end - --- --- Drawing / Frame update --- -local draw_items = {n = 0} -local function registerDraw(id, f, ...) - assert(type(f) == 'function' or (getmetatable(f) or {}).__call, - 'Drawing function is not a callable type!') - - local font = love.graphics.getFont() - - local state = 'normal' - if mouse.isHot(id) or keyboard.hasFocus(id) then - state = mouse.isActive(id) and 'active' or 'hot' +-- helper +local function getOptionsAndSize(opt, ...) + if type(opt) == "table" then + return opt, ... end - local rest = {n = select('#', ...), ...} - draw_items.n = draw_items.n + 1 - draw_items[draw_items.n] = function() - if font then love.graphics.setFont(font) end - f(state, unpack(rest, 1, rest.n)) + return {}, opt, ... +end + +-- gui state +local hot, hot_last, active +local NONE = {} + +local function anyHot() + return hot ~= nil +end + +local function isHot(id) + return id == hot +end + +local function wasHot(id) + return id == hot_last +end + +local function isActive(id) + return id == active +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) +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 + end + end +end + +local function registerHitbox(id, x,y,w,h) + return 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) +end + +local function updateMouse(x, y, button_down) + mouse_x, mouse_y, mouse_button_down = x,y, button_down +end + +local function getMousePosition() + return mouse_x, mouse_y +end + +-- keyboard handling +local key_down, textchar, keyboardFocus +local function getPressedKey() + return key_down, textchar +end + +local function keypressed(key) + key_down = key +end + +local function textinput(char) + textchar = char +end + +local function grabKeyboardFocus(id) + if isActive(id) then + keyboardFocus = id + end +end + +local function hasKeyboardFocus(id) + return keyboardFocus == id +end + +local function keyPressedOn(id, key) + return hasKeyboardFocus(id) and 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) +end + +local function exitFrame() + if not mouse_button_down then + active = nil + elseif active == nil then + active = NONE + end +end + +-- draw +local draw_queue = {n = 0} + +local function registerDraw(f, ...) + local args = {...} + local nargs = select('#', ...) + draw_queue.n = draw_queue.n + 1 + draw_queue[draw_queue.n] = function() + f(unpack(args, 1, nargs)) end end --- actually update-and-draw local function draw() - keyboard.endFrame() - mouse.endFrame() - group.endFrame() - - -- save graphics state - local c = {love.graphics.getColor()} - local f = love.graphics.getFont() - local lw = love.graphics.getLineWidth() - local ls = love.graphics.getLineStyle() - - for i = 1,draw_items.n do draw_items[i]() end - - -- restore graphics state - love.graphics.setLineWidth(lw) - love.graphics.setLineStyle(ls) - if f then love.graphics.setFont(f) end - love.graphics.setColor(c) - - draw_items.n = 0 - maxid = 0 - - group.beginFrame() - mouse.beginFrame() - keyboard.beginFrame() + exitFrame() + for i = 1,draw_queue.n do + draw_queue[i]() + end + draw_queue.n = 0 + enterFrame() end --- --- The Module --- -return { - generateID = generateID, +local module = { + getOptionsAndSize = getOptionsAndSize, - style = require((...):match("(.-)[^%.]+$") .. 'style-default'), + 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, + + generateID = generateID, + enterFrame = enterFrame, + exitFrame = exitFrame, registerDraw = registerDraw, - draw = draw, - - strictAnd = strictAnd, - strictOr = strictOr, + theme = theme, + draw = draw, } +theme.core = module +return module diff --git a/group.lua b/group.lua deleted file mode 100644 index 4889cd4..0000000 --- a/group.lua +++ /dev/null @@ -1,151 +0,0 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local stack = {n = 0} -local default = { - pos = {0,0}, - grow = {0,0}, - spacing = 2, - size = {100, 30}, - upper_left = {0,0}, - lower_right = {0,0}, -} -local current = default - -local Grow = { - none = { 0, 0}, - up = { 0, -1}, - down = { 0, 1}, - left = {-1, 0}, - right = { 1, 0} -} - --- {grow = grow, spacing = spacing, size = size, pos = pos} -local function push(info) - local grow = info.grow or "none" - local spacing = info.spacing or default.spacing - - local size = { - info.size and info.size[1] or current.size[1], - info.size and info.size[2] or current.size[2] - } - - local pos = {current.pos[1], current.pos[2]} - if info.pos then - pos[1] = pos[1] + (info.pos[1] or 0) - pos[2] = pos[2] + (info.pos[2] or 0) - end - - assert(size, "Size neither specified nor derivable from parent group.") - assert(pos, "Position neither specified nor derivable from parent group.") - grow = assert(Grow[grow], "Invalid grow: " .. tostring(grow)) - - current = { - pos = pos, - grow = grow, - size = size, - spacing = spacing, - upper_left = { math.huge, math.huge}, - lower_right = {-math.huge, -math.huge}, - } - stack.n = stack.n + 1 - stack[stack.n] = current -end - -local function advance(pos, size) - current.upper_left[1] = math.min(current.upper_left[1], pos[1]) - current.upper_left[2] = math.min(current.upper_left[2], pos[2]) - current.lower_right[1] = math.max(current.lower_right[1], pos[1] + size[1]) - current.lower_right[2] = math.max(current.lower_right[2], pos[2] + size[2]) - - if current.grow[1] ~= 0 then - current.pos[1] = pos[1] + current.grow[1] * (size[1] + current.spacing) - end - if current.grow[2] ~= 0 then - current.pos[2] = pos[2] + current.grow[2] * (size[2] + current.spacing) - end - return pos, size -end - -local function getRect(pos, size) - pos = {pos and pos[1] or 0, pos and pos[2] or 0} - size = {size and size[1] or current.size[1], size and size[2] or current.size[2]} - - -- growing left/up: update current position to account for differnt size - if current.grow[1] < 0 and current.size[1] ~= size[1] then - current.pos[1] = current.pos[1] + (current.size[1] - size[1]) - end - if current.grow[2] < 0 and current.size[2] ~= size[2] then - current.pos[2] = current.pos[2] - (current.size[2] - size[2]) - end - - pos[1] = pos[1] + current.pos[1] - pos[2] = pos[2] + current.pos[2] - - return advance(pos, size) -end - -local function pop() - assert(stack.n > 0, "Group stack is empty.") - stack.n = stack.n - 1 - local child = current - current = stack[stack.n] or default - - local size = { - child.lower_right[1] - math.max(child.upper_left[1], current.pos[1]), - child.lower_right[2] - math.max(child.upper_left[2], current.pos[2]) - } - advance(current.pos, size) -end - -local function beginFrame() - current = default - stack.n = 0 -end - -local function endFrame() - -- future use? -end - -return setmetatable({ - push = push, - pop = pop, - getRect = getRect, - advance = advance, - beginFrame = beginFrame, - endFrame = endFrame, - default = default, -}, { - __index = function(_,k) - return ({size = current.size, pos = current.pos})[k] - end, - __call = function(_, info) - assert(type(info) == 'table' and type(info[1]) == 'function') - push(info) - info[1]() - pop() - end, -}) diff --git a/imagebutton.lua b/imagebutton.lua new file mode 100644 index 0000000..cb256db --- /dev/null +++ b/imagebutton.lua @@ -0,0 +1,42 @@ +-- This file is part of QUI, copyright (c) 2016 Matthias Richter + +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') + +return function(...) + local opt, x,y,w,h = core.getOptionsAndSize(...) + opt.normal = opt.normal or opt[1] + opt.hot = opt.hot or opt[2] or opt.normal + opt.active = opt.active or opt[3] or opt.hot + 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) + local id = opt.normal:getData() + assert(id:typeOf("ImageData"), "Can only use uncompressed images") + u, v = math.floor(u+.5), math.floor(v+.5) + if u < 0 or u >= opt.normal:getWidth() or v < 0 or v >= opt.normal:getHeight() then + return false + end + local _,_,_,a = id:getPixel(u,v) + return a > 0 + end) + + local img = opt.normal + if core.isHot(opt.id) then + img = opt.hot + elseif core.isActive(opt.id) then + img = opt.active + end + + 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), + 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) + } +end diff --git a/init.lua b/init.lua index 11c5e92..0e1dcb7 100644 --- a/init.lua +++ b/init.lua @@ -1,41 +1,14 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- +-- This file is part of QUI, copyright (c) 2016 Matthias Richter local BASE = (...) .. '.' -assert(not BASE:match('%.init%.$'), "Invalid require path `"..(...).."' (drop the `.init').") return { - core = require(BASE .. 'core'), - group = require(BASE .. 'group'), - mouse = require(BASE .. 'mouse'), - keyboard = require(BASE .. 'keyboard'), - Button = require(BASE .. 'button'), - Slider = require(BASE .. 'slider'), - Slider2D = require(BASE .. 'slider2d'), - Label = require(BASE .. 'label'), - Input = require(BASE .. 'input'), + 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') } diff --git a/input.lua b/input.lua index b161993..6a06c9c 100644 --- a/input.lua +++ b/input.lua @@ -1,80 +1,79 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') +local utf8 = require 'utf8' -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +local function split(str, pos) + local offset = utf8.offset(str, pos) + return str:sub(1, offset-1), str:sub(offset) +end -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. +return function(input, ...) + local font = love.graphics.getFont() + local opt, x,y,w,h = core.getOptionsAndSize(...) + opt.id = opt.id or input + opt.font = opt.font or love.graphics.getFont() -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- + w = w or opt.font:getWidth(text) + 4 + h = h or opt.font:getHeight() + 4 -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') -local utf8 = require(BASE .. 'utf8') + input.text = input.text or "" + input.cursor = math.max(1, math.min(utf8.len(input.text)+1, input.cursor or utf8.len(input.text)+1)) + -- cursor is position *before* the character (including EOS) i.e. in "hello": + -- position 1: |hello + -- position 2: h|ello + -- ... + -- position 6: hello| --- {info = {text = "", cursor = text:len()}, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == "table" and type(w.info) == "table", "Invalid argument") - w.info.text = w.info.text or "" - w.info.cursor = math.min(w.info.cursor or w.info.text:len(), w.info.text:len()) + core.registerHitbox(opt.id, x,y,w,h) - local id = w.id or core.generateID() - local pos, size = group.getRect(w.pos, w.size) - mouse.updateWidget(id, pos, size, w.widgetHit) - keyboard.makeCyclable(id) - if mouse.isActive(id) then keyboard.setFocus(id) end + core.grabKeyboardFocus(opt.id) + opt.hasKeyboardFocus = core.hasKeyboardFocus(opt.id) - if not keyboard.hasFocus(id) then - --[[nothing]] - -- editing - elseif keyboard.key == 'backspace' and w.info.cursor > 0 then - w.info.cursor = math.max(0, w.info.cursor-1) - local left, right = utf8.split(w.info.text, w.info.cursor) - w.info.text = left .. utf8.sub(right, 2) - elseif keyboard.key == 'delete' then - local left, right = utf8.split(w.info.text, w.info.cursor) - w.info.text = left .. utf8.sub(right, 2) - w.info.cursor = math.min(w.info.text:len(), w.info.cursor) - -- movement - elseif keyboard.key == 'left' then - w.info.cursor = math.max(0, w.info.cursor-1) - elseif keyboard.key == 'right' then - w.info.cursor = math.min(w.info.text:len(), w.info.cursor+1) - elseif keyboard.key == 'home' then - w.info.cursor = 0 - elseif keyboard.key == 'end' then - w.info.cursor = w.info.text:len() - -- info - elseif keyboard.key == 'return' then - keyboard.clearFocus() - keyboard.pressed('', -1) - elseif keyboard.str then - local left, right = utf8.split(w.info.text, w.info.cursor) - w.info.text = left .. keyboard.str .. right - w.info.cursor = w.info.cursor + 1 + if opt.hasKeyboardFocus then + local keycode,char = core.getPressedKey() + -- text input + if char ~= "" then + local a,b = split(input.text, input.cursor) + input.text = table.concat{a, char, b} + input.cursor = input.cursor + 1 + end + + -- text editing + if keycode == 'backspace' then + local a,b = split(input.text, input.cursor) + input.text = table.concat{split(a,utf8.len(a)), b} + input.cursor = math.max(1, input.cursor-1) + elseif keycode == 'delete' then + local a,b = split(input.text, input.cursor) + local _,b = split(b, 2) + input.text = table.concat{a, b} + end + + -- cursor movement + if keycode =='left' then + input.cursor = math.max(0, input.cursor-1) + elseif keycode =='right' then -- cursor movement + input.cursor = math.min(utf8.len(input.text)+1, input.cursor+1) + elseif keycode =='home' then -- cursor movement + input.cursor = 1 + elseif keycode =='end' then -- cursor movement + input.cursor = utf8.len(input.text)+1 + end + + -- mouse cursor position + -- TODO end - core.registerDraw(id, w.draw or core.style.Input, - w.info.text, w.info.cursor, pos[1],pos[2], size[1],size[2]) + core.registerDraw(core.theme.Input, input, opt, x,y,w,h) - return mouse.releasedOn(id) or keyboard.pressedOn(id, 'return') + 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) + } end diff --git a/keyboard.lua b/keyboard.lua deleted file mode 100644 index 0e52eb3..0000000 --- a/keyboard.lua +++ /dev/null @@ -1,108 +0,0 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local key,str = nil, nil -local focus, lastwidget -local NO_WIDGET = {} - -local cycle = { - -- binding = {key = key, modifier1, modifier2, ...} XXX: modifiers are OR-ed! - prev = {key = 'tab', 'lshift', 'rshift'}, - next = {key = 'tab'}, -} - -local function pressed(k) - assert(type(k) == 'string', 'Invalid argument `key`. Expected string, got ' .. type(k)) - key = k -end - -local function textinput(s) - assert(type(s) == 'string', 'Invalid argument `key`. Expected string, got ' .. type(s)) - str = s -end - -local function setFocus(id) focus = id end -local function disable() focus = NO_WIDGET end -local function clearFocus() focus = nil end -local function hasFocus(id) return id == focus end -local function getFocus() return focus end - -local function tryGrab(id) - if not focus then - setFocus(id) - end -end - -local function isBindingDown(bind) - local modifiersDown = #bind == 0 or love.keyboard.isDown(unpack(bind)) - return key == bind.key and modifiersDown -end - -local function makeCyclable(id) - tryGrab(id) - if hasFocus(id) then - if isBindingDown(cycle.prev) then - setFocus(lastwidget) - key = nil - elseif isBindingDown(cycle.next) then - setFocus(nil) - key = nil - end - end - lastwidget = id -end - -local function pressedOn(id, k) - return (k or 'return') == key and hasFocus(id) and k -end - -local function beginFrame() - -- for future use? -end - -local function endFrame() - key, str = nil, nil -end - -return setmetatable({ - cycle = cycle, - pressed = pressed, - textinput = textinput, - tryGrab = tryGrab, - isBindingDown = isBindingDown, - setFocus = setFocus, - getFocus = getFocus, - clearFocus = clearFocus, - hasFocus = hasFocus, - makeCyclable = makeCyclable, - pressedOn = pressedOn, - - disable = disable, - enable = clearFocus, - - beginFrame = beginFrame, - endFrame = endFrame, -}, {__index = function(_,k) return ({key = key, str = str})[k] end}) diff --git a/label.lua b/label.lua index 345516c..d4c8202 100644 --- a/label.lua +++ b/label.lua @@ -1,61 +1,24 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +return function(text, ...) + local opt, x,y,w,h = core.getOptionsAndSize(...) + opt.id = opt.id or text + opt.font = opt.font or love.graphics.getFont() -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. + w = w or opt.font:getWidth(text) + 4 + h = h or opt.font:getHeight() + 4 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- + core.registerHitbox(opt.id, x,y,w,h) + core.registerDraw(core.theme.Label, text, opt, x,y,w,h) -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') - --- {text = text, align = align, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == "table" and w.text, "Invalid argument") - w.align = w.align or 'left' - - local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight') - if tight then - local f = assert(love.graphics.getFont()) - if w.size[1] == 'tight' then - w.size[1] = f:getWidth(w.text) - end - if w.size[2] == 'tight' then - w.size[2] = f:getHeight(w.text) - end - end - - local id = w.id or core.generateID() - local pos, size = group.getRect(w.pos, w.size) - - if keyboard.hasFocus(id) then - keyboard.clearFocus() - end - - core.registerDraw(id, w.draw or core.style.Label, - w.text, w.align, pos[1],pos[2], size[1],size[2]) - - return mouse.releasedOn(id) + 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) + } end - diff --git a/layout.lua b/layout.lua new file mode 100644 index 0000000..a993d22 --- /dev/null +++ b/layout.lua @@ -0,0 +1,286 @@ +-- This file is part of QUI, copyright (c) 2016 Matthias Richter + +local Layout = {} +function Layout.new() + return setmetatable({_stack = {}}, {__index = Layout}):reset() +end + +function Layout:reset(x,y, padx,pady) + self._x = x or 0 + self._y = y or 0 + self._padx = padx or 0 + self._pady = pady or 0 + self._w = 0 + self._h = 0 + self._widths = {min=math.huge,max=-math.huge} + self._heights = {min=math.huge,max=-math.huge} + + return self +end + +function Layout:padding(padx,pady) + self._padx = padx + self._pady = pady +end + +function Layout:push(x,y) + self._stack[#self._stack+1] = { + self._x, self._y, + self._padx, self._pady, + self._w, self._h, + self._widths, + self._heights, + } + + return self:reset(x,y) +end + +function Layout:pop() + assert(#self._stack > 0, "Nothing to pop") + local w,h = self._w, self._h + self._x, self._y, + self._padx,self._pady, + self._w, self._h, + self._widths, self._heights = unpack(self._stack[#self._stack]) + + self._w, self._h = math.max(w, self._w), math.max(h, self._h) + + return self +end + +--- recursive binary search for position of v +local function insert_sorted_helper(t, i0, i1, v) + if i1 <= i0 then + table.insert(t, i0, v) + return + end + + local i = i0 + math.floor((i1-i0)/2) + if t[i] < v then + return insert_sorted_helper(t, i+1, i1, v) + elseif t[i] > v then + return insert_sorted_helper(t, i0, i-1, v) + else + table.insert(t, i, v) + end +end + +local function insert_sorted(t, v) + if v <= 0 then return end + insert_sorted_helper(t, 1, #t, v) + t.min = math.min(t.min, v) + t.max = math.max(t.max, v) +end + +local function calc_width_height(self, w, h) + if w == "" or w == nil then + w = self._w + elseif w == "max" then + w = self._widths.max + elseif w == "min" then + w = self._widths.min + elseif w == "median" then + w = self._widths[math.ceil(#self._widths/2)] or 0 + elseif type(w) ~= "number" then + error("width: invalid value (" .. tostring(w) .. ")", 2) + end + + if h == "" or h == nil then + h = self._h + elseif h == "max" then + h = self._heights.max + elseif h == "min" then + h = self._heights.min + elseif h == "median" then + h = self._heights[math.ceil(#self._heights/2)] or 0 + elseif type(h) ~= "number" then + error("width: invalid value (" .. tostring(w) .. ")",2) + end + + insert_sorted(self._widths, w) + insert_sorted(self._heights, h) + return w,h +end + +function Layout:row(w, h) + self._y = self._y + self._pady + w,h = calc_width_height(self, w, h) + + local x,y = self._x, self._y + self._h + self._y, self._w, self._h = y, w, h + + return x,y,w,h +end + +function Layout:col(w, h) + self._x = self._x + self._padx + w,h = calc_width_height(self, w, h) + + local x,y = self._x + self._w, self._y + self._x, self._w, self._h = x, w, h + + return x,y,w,h +end + + +local function layout_iterator(t, idx) + idx = (idx or 1) + 1 + if t[idx] == nil then return nil end + return idx, unpack(t[idx]) +end + +local function layout_retained_mode(self, t, constructor, string_argument_to_table, fill_width, fill_height) + -- sanity check + local p = t.pos or {0,0} + assert(type(p) == "table", "Invalid argument `pos' (table expected, got "..type(p)..")",2) + + self:push(p[1] or 0, p[2] or 0) + + -- first pass: get dimensions, add layout info + local layout = {n_fill_w = 0, n_fill_h = 0} + for i,v in ipairs(t) do + if type(v) == "string" then + v = string_argument_to_table(v) + end + local x,y,w,h = 0,0, v[1], v[2] + if v[1] == "fill" then w = 0 end + if v[2] == "fill" then h = 0 end + + x,y, w,h = constructor(self, w,h) + + if v[1] == "fill" then + w = "fill" + layout.n_fill_w = layout.n_fill_w + 1 + end + if v[2] == "fill" then + h = "fill" + layout.n_fill_h = layout.n_fill_h + 1 + end + layout[i] = {x,y,w,h, unpack(v,3)} + end + + -- second pass: extend "fill" cells and shift others accordingly + local fill_w = fill_width(layout, t.min_width or 0, self._x + self._w - p[1]) + local fill_h = fill_height(layout, t.min_height or 0, self._y + self._h - p[2]) + local dx,dy = 0,0 + for _,v in ipairs(layout) do + v[1], v[2] = v[1] + dx, v[2] + dy + if v[3] == "fill" then + v[3] = fill_w + dx = dx + v[3] + end + if v[4] == "fill" then + v[4] = fill_h + dy = dy + v[4] + end + end + + -- finally: return layout with iterator + self:pop() + layout.item = function(self, i) + if self ~= layout then -- allow either colon or dot syntax + i = self + end + return unpack(layout[i]) + end + return setmetatable(layout, {__call = function() + return layout_iterator, layout, 0 + end}) +end + +function Layout:rows(t) + return layout_retained_mode(self, t, self.row, + function(v) return {nil, v} end, + function() return self._widths.max end, -- fill width + function(l,mh,h) return (mh - h) / l.n_fill_h end) -- fill height +end + +function Layout:cols(t) + return layout_retained_mode(self, t, self.col, + function(v) return {v} end, + function(l,mw,w) return (mw - w) / l.n_fill_w end, -- fill width + function() return self._heights.max end) -- fill height +end + +-- TODO: nesting a la rows{..., cols{...} } ? + +local instance = Layout.new() +return setmetatable({ + new = Layout.new, + reset = function(...) return instance:reset(...) end, + push = function(...) return instance:push(...) end, + pop = function(...) return instance:pop(...) end, + row = function(...) return instance:row(...) end, + col = function(...) return instance:col(...) end, + rows = function(...) return instance:rows(...) end, + cols = function(...) return instance:cols(...) end, +}, {__call = function(_,...) return Layout.new(...) end}) + +--[[do + +L = Layout.new() + + + +print("immediate mode") +print("--------------") +x,y,w,h = L:row(100,20) -- x,y,w,h = x0,y0, 100,20 +print(1,x,y,w,h) +x,y,w,h = L:row() -- x,y,w,h = x0, y0+20, 100,20 (default: reuse last dimensions) +print(2,x,y,w,h) +x,y,w,h = L:col(20) -- x,y,w,h = x0+100, y0+20, 20, 20 +print(3,x,y,w,h) +x,y,w,h = L:row(nil,30) -- x,y,w,h = x0+100, y0+40, 20, 30 +print(4,x,y,w,h) +print() + +L:reset() + +local layout = L:rows{ + pos = {10,10}, -- optional, default {0,0} + + {100, 10}, + {nil, 10}, -- {100, 10} + {100, 20}, -- {100, 20} + {}, -- {100, 20} -- default = last value + {nil, "median"}, -- {100, 20} + "median", -- {100, 20} + "max", -- {100, 20} + "min", -- {100, 10} + "" -- {100, 10} -- default = last value +} + +print("rows") +print("----") +for i,x,y,w,h in layout() do + print(i,x,y,w,h) +end +print() + +-- +-------+-------+----------------+-------+ +-- | | | | | +-- 70 {100, | "max" | "fill" | "min" | +-- | 70} | | | | +-- +--100--+--100--+------220-------+--100--+ +-- +-- `-------------------,--------------------' +-- 520 +local layout = L:cols{ + pos = {10,10}, + min_width = 520, + + {100, 70}, + "max", -- {100, 70} + "fill", -- {min_width - width_of_items, 70} = {420, 70} + "min", -- {100,70} +} + +print("cols") +print("----") +for i,x,y,w,h in layout() do + print(i,x,y,w,h) +end +print() + +end +--]] diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..d879371 --- /dev/null +++ b/license.txt @@ -0,0 +1,23 @@ +Copyright (c) 2016 Matthias Richter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +Except as contained in this notice, the name(s) of the above copyright holders +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/mouse.lua b/mouse.lua deleted file mode 100644 index 85db24b..0000000 --- a/mouse.lua +++ /dev/null @@ -1,110 +0,0 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local _M -- holds the module. needed to make widgetHit overridable - -local x,y = 0,0 -local down, downLast = false, false -local hot, active = nil, nil -local NO_WIDGET = {} -local function _NOP_() end - -local function widgetHit(mouse, pos, size) - return mouse[1] >= pos[1] and mouse[1] <= pos[1] + size[1] and - mouse[2] >= pos[2] and mouse[2] <= pos[2] + size[2] -end - -local function setHot(id) hot = id end -local function setActive(id) active = id end -local function isHot(id) return id == hot end -local function isActive(id) return id == active end -local function getHot() return hot end - -local function updateWidget(id, pos, size, hit) - hit = hit or _M.widgetHit - - if hit({x,y}, pos, size) then - setHot(id) - if not active and down then - setActive(id) - end - end -end - -local function releasedOn(id) - return not down and isHot(id) and isActive(id) and downLast -end - -local function beginFrame() - hot = nil - x,y = _M.getMousePosition() - downLast = down - down = false - for _,btn in ipairs{'l', 'm', 'r'} do - down = down or (love.mouse.isDown(btn) and btn) - end -end - -local function endFrame() - if not down then -- released - setActive(nil) - elseif not active then -- clicked outside - setActive(NO_WIDGET) - end -end - -local function disable() - _M.beginFrame = _NOP_ - _M.endFrame = _NOP_ - _M.updateWidget = _NOP_ -end - -local function enable() - _M.beginFrame = beginFrame - _M.endFrame = endFrame - _M.updateWidget = updateWidget -end - -_M = { - widgetHit = widgetHit, - setHot = setHot, - getHot = getHot, - setActive = setActive, - isHot = isHot, - isActive = isActive, - updateWidget = updateWidget, - releasedOn = releasedOn, - - beginFrame = beginFrame, - endFrame = endFrame, - - disable = disable, - enable = enable, - getMousePosition = love.mouse.getPosition -} - --- metatable provides getters to x, y and down -return setmetatable(_M, {__index = function(_,k) return ({x = x, y = y, down = down})[k] end}) diff --git a/slider.lua b/slider.lua index 4700ae5..5157d63 100644 --- a/slider.lua +++ b/slider.lua @@ -1,79 +1,55 @@ ---[[ -Copyright (c) 2012 Matthias Richter +-- This file is part of QUI, copyright (c) 2016 Matthias Richter -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +local BASE = (...):match('(.-)[^%.]+$') +local core = require(BASE .. 'core') -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +return function(info, ...) + local opt, x,y,w,h = core.getOptionsAndSize(...) -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. + opt.id = opt.id or info -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- + info.min = info.min or math.min(info.value, 0) + info.max = info.max or math.max(info.value, 1) + info.step = info.step or (info.max - info.min) / 10 + local fraction = (info.value - info.min) / (info.max - info.min) + local value_changed = false -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') + core.registerHitbox(opt.id, x,y,w,h) --- {info = {value = v, min = 0, max = 1, step = (max-min)/20}, vertical = boolean, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == 'table' and type(w.info) == "table" and w.info.value, "Invalid argument.") - w.info.min = w.info.min or 0 - w.info.max = w.info.max or math.max(w.info.value, 1) - w.info.step = w.info.step or (w.info.max - w.info.min) / 20 - local fraction = (w.info.value - w.info.min) / (w.info.max - w.info.min) - - local id = w.id or core.generateID() - local pos, size = group.getRect(w.pos, w.size) - - mouse.updateWidget(id, pos, size, w.widgetHit) - keyboard.makeCyclable(id) - - -- mouse update - local changed = false - if mouse.isActive(id) then - keyboard.setFocus(id) - if w.vertical then - fraction = math.min(1, math.max(0, (pos[2] - mouse.y + size[2]) / size[2])) + if core.isActive(opt.id) then + -- mouse update + local mx,my = core.getMousePosition() + if opt.vertical then + fraction = math.min(1, math.max(0, (y+h - my) / h)) else - fraction = math.min(1, math.max(0, (mouse.x - pos[1]) / size[1])) + fraction = math.min(1, math.max(0, (mx - x) / w)) end - local v = fraction * (w.info.max - w.info.min) + w.info.min - if v ~= w.info.value then - w.info.value = v - changed = true + local v = fraction * (info.max - info.min) + info.min + if v ~= info.value then + info.value = v + value_changed = true + end + + -- 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 + info.value = math.min(info.max, info.value + info.step) + value_changed = true + elseif core.getPressedKey() == key_down then + info.value = math.max(info.min, info.value - info.step) + value_changed = true end end - -- keyboard update - if keyboard.hasFocus(id) then - local keys = w.vertical and {'up', 'down'} or {'right', 'left'} - if keyboard.key == keys[1] then - w.info.value = math.min(w.info.max, w.info.value + w.info.step) - changed = true - elseif keyboard.key == keys[2] then - w.info.value = math.max(w.info.min, w.info.value - w.info.step) - changed = true - end - end + core.registerDraw(core.theme.Slider, fraction, opt, x,y,w,h) - core.registerDraw(id, w.draw or core.style.Slider, - fraction, w.vertical, pos[1],pos[2], size[1],size[2]) - - return changed + return { + id = 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) + } end diff --git a/slider2d.lua b/slider2d.lua deleted file mode 100644 index 19e264c..0000000 --- a/slider2d.lua +++ /dev/null @@ -1,99 +0,0 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local BASE = (...):match("(.-)[^%.]+$") -local core = require(BASE .. 'core') -local group = require(BASE .. 'group') -local mouse = require(BASE .. 'mouse') -local keyboard = require(BASE .. 'keyboard') - --- {info = {value = {x,y}, min = {0,0}, max = {1,1}, step = (max-min)/20}, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} -return function(w) - assert(type(w) == 'table' and type(w.info) == "table" and w.info.value, "Invalid argument.") - w.info.min = { - w.info.min and w.info.min[1] or 0, - w.info.min and w.info.min[2] or 0, - } - w.info.max = { - w.info.max and w.info.max[1] or math.max(1, w.info.value[1]), - w.info.max and w.info.max[2] or math.max(1, w.info.value[2]), - } - w.info.step = { - w.info.step and w.info.step[1] or (w.info.max[1] - w.info.min[1]) / 20, - w.info.step and w.info.step[2] or (w.info.max[2] - w.info.min[2]) / 20, - } - - local fraction = { - (w.info.value[1] - w.info.min[1]) / (w.info.max[1] - w.info.min[1]), - (w.info.value[2] - w.info.min[2]) / (w.info.max[2] - w.info.min[2]), - } - - local id = w.id or core.generateID() - local pos, size = group.getRect(w.pos, w.size) - - mouse.updateWidget(id, pos, size, w.widgetHit) - keyboard.makeCyclable(id) - - -- update value - local changed = false - if mouse.isActive(id) then - keyboard.setFocus(id) - fraction = { - math.min(1, math.max(0, (mouse.x - pos[1]) / size[1])), - math.min(1, math.max(0, (mouse.y - pos[2]) / size[2])), - } - local v = { - fraction[1] * (w.info.max[1] - w.info.min[1]) + w.info.min[1], - fraction[2] * (w.info.max[2] - w.info.min[2]) + w.info.min[2], - } - if v[1] ~= w.info.value[1] or v[2] ~= w.info.value[2] then - w.info.value = v - changed = true - end - end - - if keyboard.hasFocus(id) then - if keyboard.key == 'down' then - w.info.value[2] = math.min(w.info.max[2], w.info.value[2] + w.info.step[2]) - changed = true - elseif keyboard.key == 'up' then - w.info.value[2] = math.max(w.info.min[2], w.info.value[2] - w.info.step[2]) - changed = true - end - if keyboard.key == 'right' then - w.info.value[1] = math.min(w.info.max[1], w.info.value[1] + w.info.step[1]) - changed = true - elseif keyboard.key == 'left' then - w.info.value[1] = math.max(w.info.min[1], w.info.value[1] - w.info.step[1]) - changed = true - end - end - - core.registerDraw(id, w.draw or core.style.Slider2D, - fraction, pos[1],pos[2], size[1],size[2]) - - return changed -end diff --git a/style-default.lua b/style-default.lua deleted file mode 100644 index 71f412a..0000000 --- a/style-default.lua +++ /dev/null @@ -1,204 +0,0 @@ ---[[ -Copyright (c) 2012 Matthias Richter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]]-- - -local BASE = (...):match("(.-)[^%.]+$") -local utf8 = require(BASE .. 'utf8') - --- default style -local color = { - normal = {bg = {78,78,78}, fg = {200,200,200}, border={20,20,20}}, - hot = {bg = {98,98,98}, fg = {69,201,84}, border={30,30,30}}, - active = {bg = {88,88,88}, fg = {49,181,64}, border={10,10,10}} -} - --- box drawing -local gradient = {} -function gradient:set(from, to) - local id = love.image.newImageData(1,2) - id:setPixel(0,0, to,to,to,255) - id:setPixel(0,1, from,from,from,255) - gradient.img = love.graphics.newImage(id) - gradient.img:setFilter('linear', 'linear') -end -gradient:set(200,255) - -local function box(x,y,w,h, bg, border, flip) - love.graphics.setLineWidth(1) - love.graphics.setLineStyle('rough') - - love.graphics.setColor(bg) - local sy = flip and -h/2 or h/2 - love.graphics.draw(gradient.img, x,y+h/2, 0,w,sy, 0,1) - love.graphics.setColor(border) - love.graphics.rectangle('line', x,y,w,h) -end - --- load default font -if not love.graphics.getFont() then - love.graphics.setFont(love.graphics.newFont(12)) -end - -local function Button(state, title, x,y,w,h) - local c = color[state] - box(x,y,w,h, c.bg, c.border, state == 'active') - local f = assert(love.graphics.getFont()) - x,y = x + (w-f:getWidth(title))/2, y + (h-f:getHeight(title))/2 - love.graphics.setColor(c.fg) - love.graphics.print(title, x,y) -end - -local function Label(state, text, align, x,y,w,h) - local c = color[state] - love.graphics.setColor(c.fg) - local f = assert(love.graphics.getFont()) - y = y + (h - f:getHeight(text))/2 - if align == 'center' then - x = x + (w - f:getWidth(text))/2 - elseif align == 'right' then - x = x + w - f:getWidth(text) - end - love.graphics.print(text, x,y) -end - -local function Slider(state, fraction, vertical, x,y,w,h) - local c = color[state] - - love.graphics.setLineWidth(1) - love.graphics.setLineStyle('rough') - love.graphics.setColor(c.bg) - if vertical then - love.graphics.rectangle('fill', x+w/2-2,y,4,h) - love.graphics.setColor(c.border) - love.graphics.rectangle('line', x+w/2-2,y,4,h) - y = math.floor(y + h - h * fraction - 5) - h = 10 - else - love.graphics.rectangle('fill', x,y+h/2-2,w,4) - love.graphics.setColor(c.border) - love.graphics.rectangle('line', x,y+h/2-2,w,4) - x = math.floor(x + w * fraction - 5) - w = 10 - end - box(x,y,w,h, c.bg,c.border) -end - -local function Slider2D(state, fraction, x,y,w,h) - local c = color[state] - box(x,y,w,h, c.bg, c.border) - - -- draw quadrants - love.graphics.setLineWidth(1) - love.graphics.setLineStyle('rough') - love.graphics.setColor(c.fg[1], c.fg[2], c.fg[3], math.min(127,c.fg[4] or 255)) - love.graphics.line(x+w/2,y, x+w/2,y+h) - love.graphics.line(x,y+h/2, x+w,y+h/2) - - -- draw cursor - local xx = math.ceil(x + fraction[1] * w) - local yy = math.ceil(y + fraction[2] * h) - love.graphics.setColor(c.fg) - love.graphics.line(xx-3,yy,xx+2.5,yy) - love.graphics.line(xx,yy-2.5,xx,yy+2.5) -end - -local function Input(state, text, cursor, x,y,w,h) - local c = color[state] - box(x,y,w,h, c.bg, c.border, state ~= 'active') - - local f = love.graphics.getFont() - local th = f:getHeight(text) - local cursorPos = x + 2 + f:getWidth(utf8.sub(text, 1,cursor)) - local offset = 2 - math.floor((cursorPos-x) / (w-4)) * (w-4) - - local tsx,tsy,tsw,tsh = x+1, y, w-2, h - local sx,sy,sw,sh = love.graphics.getScissor() - if sx then -- intersect current scissors with our's - local l,r = math.max(sx, tsx), math.min(sx+sw, tsx+tsw) - local t,b = math.max(sy, tsy), math.min(sy+sh, tsy+tsh) - if l > r or t > b then -- there is no intersection - return - end - tsx, tsy, tsw, tsh = l, t, r-l, b-t - end - - love.graphics.setScissor(tsx, tsy, tsw, tsh) - love.graphics.setLineWidth(1) - love.graphics.setLineStyle('rough') - love.graphics.setColor(color.normal.fg) - love.graphics.print(text, x+offset,y+(h-th)/2) - if state ~= 'normal' then - love.graphics.setColor(color.active.fg) - love.graphics.line(cursorPos+offset, y+4, cursorPos+offset, y+h-4) - end - if sx then - love.graphics.setScissor(sx,sy,sw,sh) - else - love.graphics.setScissor() - end -end - -local function Checkbox(state, checked, label, align, x,y,w,h) - local c = color[state] - local bw, bx, by = math.min(w,h)*.7, x, y - by = y + (h-bw)/2 - - local f = assert(love.graphics.getFont()) - local tw,th = f:getWidth(label), f:getHeight(label) - local tx, ty = x, y + (h-th)/2 - if align == 'left' then - -- [ ] LABEL - bx, tx = x, x+bw+4 - else - -- LABEL [ ] - tx, bx = x, x+4+tw - end - - box(bx,by,bw,bw, c.bg, c.border) - - if checked then - bx,by = bx+bw*.25, by+bw*.25 - bw = bw * .5 - love.graphics.setColor(color.active.fg) - box(bx,by,bw,bw, color.hot.fg, {0,0,0,0}, true) - end - - love.graphics.setColor(c.fg) - love.graphics.print(label, tx, ty) -end - - --- the style -return { - color = color, - gradient = gradient, - - Button = Button, - Label = Label, - Slider = Slider, - Slider2D = Slider2D, - Input = Input, - Checkbox = Checkbox, -} diff --git a/theme.lua b/theme.lua new file mode 100644 index 0000000..1dca9d7 --- /dev/null +++ b/theme.lua @@ -0,0 +1,150 @@ +-- This file is part of QUI, copyright (c) 2016 Matthias Richter + +local BASE = (...):match('(.-)[^%.]+$') + +local theme = {} + +theme.color = { + normal = {bg = {78,78,78}, fg = {200,200,200}, border={20,20,20}}, + hot = {bg = {98,98,98}, fg = {69,201,84}, border={30,30,30}}, + active = {bg = {88,88,88}, fg = {49,181,64}, border={10,10,10}} +} + + +-- HELPER +function theme.getStateName(id) + if theme.core.isHot(id) then + return 'hot' + end + if theme.core.isActive(id) then + return 'active' + 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] +end + +function theme.drawBox(x,y,w,h, colors) + love.graphics.setColor(colors.bg) + love.graphics.rectangle('fill', x,y, w,h) + + love.graphics.setColor(colors.border) + love.graphics.rectangle('line', x,y, w,h) +end + +function theme.getVerticalOffsetForAlign(valign, font, h) + if valign == "top" then + return 0 + elseif valign == "bottom" then + return h - font:getHeight() + end + -- else: "middle" + return (h - font:getHeight()) / 2 +end + +-- WIDGET VIEWS +function theme.Label(text, opt, x,y,w,h) + y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) + + love.graphics.setColor((opt.color and opt.color.normal or {}).fg or theme.color.normal.fg) + love.graphics.setFont(opt.font) + love.graphics.printf(text, x+2, y, w-4, opt.align or "center") +end + +function theme.Button(text, opt, x,y,w,h) + local c = theme.getColorForState(opt) + + theme.drawBox(x,y,w,h, c) + love.graphics.setColor(c.fg) + love.graphics.setFont(opt.font) + + y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) + love.graphics.printf(text, x+2, y, w-4, opt.align or "center") +end + +function theme.Checkbox(chk, opt, x,y,w,h) + local c = theme.getColorForState(opt) + local th = opt.font:getHeight() + + theme.drawBox(x,y+(h-th)/2,th,th, c) + love.graphics.setColor(c.fg) + if chk.checked then + love.graphics.rectangle('fill', x+3,y+(h-th)/2+3,th-6,th-6) + end + + if chk.text then + love.graphics.setFont(opt.font) + y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) + love.graphics.printf(chk.text, x + th+2, y, w - th+2, opt.align or "left") + end +end + +function theme.Slider(fraction, opt, x,y,w,h) + local c = theme.getColorForState(opt) + love.graphics.setColor(c.bg) + + if opt.vertical then + love.graphics.rectangle('fill', x+w/2-2,y, 4,h) + y = math.floor(y + h * (1 - fraction)) + theme.drawBox(x,y-2,w,4, c) + else + love.graphics.rectangle('fill', x,y+h/2-2, w,4) + x = math.floor(x + w * fraction) + theme.drawBox(x-2,y,4,h, c) + end +end + +function theme.Input(input, opt, x,y,w,h) + local utf8 = require 'utf8' + theme.drawBox(x,y,w,h, (opt.color and opt.color.normal) or theme.color.normal) + x = x + 3 + w = w - 6 + + -- get size of text and cursor position + local th = opt.font:getHeight() + local tw = opt.font:getWidth(input.text) + local cursor_pos = 0 + if input.cursor > 1 then + local s = input.text:sub(0, utf8.offset(input.text, input.cursor-1)) + cursor_pos = opt.font:getWidth(s) + end + + -- compute drawing offset + input.drawoffset = input.drawoffset or 0 + if cursor_pos - input.drawoffset < 0 then + -- cursor left of input box + input.drawoffset = cursor_pos + end + if cursor_pos - input.drawoffset > w then + -- cursor right of input box + input.drawoffset = cursor_pos - w + end + if tw - input.drawoffset < w and tw > w then + -- text bigger than input box, but does not fill it + input.drawoffset = tw - w + end + + -- set scissors + local sx, sy, sw, sh = love.graphics.getScissor() + love.graphics.setScissor(x-1,y,w+2,h) + x = x - input.drawoffset + + -- text + love.graphics.setColor(opt.color and opt.color.normal or theme.color.normal.fg) + love.graphics.setFont(opt.font) + love.graphics.print(input.text, x, y+(h-th)/2) + + -- cursor + if opt.hasFocus then + love.graphics.line(x + cursor_pos, y + (h-th)/2, + x + cursor_pos, y + (h+th)/2) + end + + -- reset scissor + love.graphics.setScissor(sx,sy,sw,sh) +end + +return theme diff --git a/utf8.lua b/utf8.lua deleted file mode 100644 index 90a4ea0..0000000 --- a/utf8.lua +++ /dev/null @@ -1,168 +0,0 @@ --- utf8.lua - Basic (and unsafe) utf8 string support in plain Lua - public domain --- --- Written in 2013 by Matthias Richter (vrld@vrld.org) --- --- This software is in the public domain. Where that dedication is not --- recognized, you are granted a perpetual, irrevokable license to copy and --- modify this file as you see fit. This software is distributed without any --- warranty. - --- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --- ALL FUNCTIONS ARE UNSAFE: THEY ASSUME VALID UTF8 INPUT --- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - --- Generic for iterator. --- --- Arguments: --- s ... The utf8 string. --- i ... Last byte of the previous codepoint. --- --- Returns: --- k ... Number of the *last* byte of the codepoint. --- c ... The utf8 codepoint (character). --- n ... Width/number of bytes of the codepoint. -local function iter(s, i) - if i >= #s then return end - local b, nbytes = s:byte(i+1,i+1), 1 - - -- determine width of the codepoint by counting the number of set bits in the first byte - -- warning: there is no validation of the following bytes! - if b >= 0xc0 and b <= 0xdf then nbytes = 2 -- 1100 0000 to 1101 1111 - elseif b >= 0xe0 and b <= 0xef then nbytes = 3 -- 1110 0000 to 1110 1111 - elseif b >= 0xf0 and b <= 0xf7 then nbytes = 4 -- 1111 0000 to 1111 0111 - elseif b >= 0xf8 and b <= 0xfb then nbytes = 5 -- 1111 1000 to 1111 1011 - elseif b >= 0xfc and b <= 0xfd then nbytes = 6 -- 1111 1100 to 1111 1101 - elseif b < 0x00 or b > 0x7f then error(("Invalid codepoint: 0x%02x"):format(b)) - end - return i+nbytes, s:sub(i+1,i+nbytes), nbytes -end - --- Shortcut to the generic for iterator. --- --- Usage: --- for k, c, n in chars(s) do --- ... --- end --- --- Meaning of k, c, and n is the same as in iter(s, i). -local function chars(s) - return iter, s, 0 -end - --- Get length in characters of an utf8 string. --- --- Arguments: --- s ... The utf8 string. --- --- Returns: --- n ... Number of utf8 characters in s. -local function len(s) - -- assumes sane utf8 string: count the number of bytes that is *not* 10xxxxxx - local _, c = s:gsub('[^\128-\191]', '') - return c -end - --- Get substring, same semantics as string.sub(s,i,j). --- --- Arguments: --- s ... The utf8 string. --- i ... Starting position, may be negative. --- j ... (optional) Ending position, may be negative. --- --- Returns: --- t ... The substring. -local function sub(s, i, j) - local l = len(s) - j = j or l - if i < 0 then i = l + i + 1 end - if j < 0 then j = l + j + 1 end - if j < i then return '' end - - local k, t = 1, {} - for _, c in chars(s) do - if k >= i then t[#t+1] = c end - if k >= j then break end - k = k + 1 - end - return table.concat(t) -end - --- Split utf8 string in two substrings --- --- Arguments: --- s ... The utf8 string. --- i ... The position to split, may be negative. --- --- Returns: --- left ... Substring before i. --- right ... Substring after i. -local function split(s, i) - local l = len(s) - if i < 0 then i = l + i + 1 end - - local k, pos = 1, 0 - for byte in chars(s) do - if k > i then break end - pos, k = byte, k + 1 - end - return s:sub(1, pos), s:sub(pos+1, -1) -end - --- Reverses order of characters in an utf8 string. --- --- Arguments: --- s ... The utf8 string. --- --- Returns: --- t ... The revered string. -local function reverse(s) - local t = {} - for _, c in chars(s) do - table.insert(t, 1, c) - end - return table.concat(t) -end - --- Convert a Unicode code point to a UTF-8 byte sequence --- Logic stolen from this page: --- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa --- --- Arguments: --- Number representing the Unicode code point (e.g. 0x265c). --- --- Returns: --- UTF-8 encoded string of the given character. --- Numbers out of range produce a blank string. -local function encode(code) - if code < 0 then - error('Code point must not be negative.') - elseif code <= 0x7f then - return string.char(code) - elseif code <= 0x7ff then - local c1 = code / 64 + 192 - local c2 = code % 64 + 128 - return string.char(c1, c2) - elseif code <= 0xffff then - local c1 = code / 4096 + 224 - local c2 = code % 4096 / 64 + 128 - local c3 = code % 64 + 128 - return string.char(c1, c2, c3) - elseif code <= 0x10ffff then - local c1 = code / 262144 + 240 - local c2 = code % 262144 / 4096 + 128 - local c3 = code % 4096 / 64 + 128 - local c4 = code % 64 + 128 - return string.char(c1, c2, c3, c4) - end - return '' -end - -return { - iter = iter, - chars = chars, - len = len, - sub = sub, - split = split, - reverse = reverse, - encode = encode -}