From b5137a4477ded384f3f44eb8afded847adcf3caa Mon Sep 17 00:00:00 2001 From: Matthias Richter Date: Thu, 31 Dec 2015 18:23:52 +0100 Subject: [PATCH] LET THERE BE SUIT! --- .gitignore | 1 - README.md | 331 +++++++++++++++++++++++----------------------- button.lua | 89 +++---------- checkbox.lua | 82 +++--------- core.lua | 265 +++++++++++++++++++++---------------- group.lua | 151 --------------------- imagebutton.lua | 42 ++++++ init.lua | 43 ++---- input.lua | 135 ++++++++++--------- keyboard.lua | 108 --------------- label.lua | 73 +++------- layout.lua | 286 +++++++++++++++++++++++++++++++++++++++ license.txt | 23 ++++ mouse.lua | 110 --------------- slider.lua | 108 ++++++--------- slider2d.lua | 99 -------------- style-default.lua | 204 ---------------------------- theme.lua | 150 +++++++++++++++++++++ utf8.lua | 168 ----------------------- 19 files changed, 987 insertions(+), 1481 deletions(-) delete mode 100644 group.lua create mode 100644 imagebutton.lua delete mode 100644 keyboard.lua create mode 100644 layout.lua create mode 100644 license.txt delete mode 100644 mouse.lua delete mode 100644 slider2d.lua delete mode 100644 style-default.lua create mode 100644 theme.lua delete mode 100644 utf8.lua 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 -}