LET THERE BE SUIT!

This commit is contained in:
Matthias Richter 2015-12-31 18:23:52 +01:00
parent 44be6169e3
commit b5137a4477
19 changed files with 987 additions and 1481 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
main.lua main.lua
quickie
*.love *.love

299
README.md
View file

@ -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() function love.load()
-- preload fonts -- generate some assets
fonts = { snd = generateClickySound()
[12] = love.graphics.newFont(12), normal, hot = generateImageButton()
[20] = love.graphics.newFont(20), smallerFont = love.graphics.newFont(10)
}
love.graphics.setBackgroundColor(17,17,17)
love.graphics.setFont(fonts[12])
-- group defaults
gui.group.default.size[1] = 150
gui.group.default.size[2] = 25
gui.group.default.spacing = 5
end end
local menu_open = { -- mutable widget data
main = false, local slider= {value = .5, max = 2}
right = false, local input = {text = "Hello"}
foo = false, local chk = {text = "Check me out"}
demo = false
}
local check1 = false
local check2 = false
local input = {text = ""}
local slider = {value = .5}
local slider2d = {value = {.5,.5}}
function love.update(dt) function love.update(dt)
gui.group.push{grow = "down", pos = {5,5}} -- new layout at 100,100 with a padding of 20x20 px
suit.layout.reset(100,100, 20,20)
-- all widgets return true if they are clicked on/activated -- Button
if gui.Checkbox{checked = menu_open.main, text = "Show Menu"} then state = suit.Button("Hover me!", suit.layout.row(200,30))
menu_open.main = not menu_open.main if state.entered then
love.audio.play(snd)
end
if state.hit then
print("Ouch!")
end end
if menu_open.main then -- Input box
gui.group.push{grow = "right"} if suit.Input(input, suit.layout.row()).submitted then
print(input.text)
-- 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 end
if menu_open.right then -- dynamically add widgets
gui.group.push{grow = "up"} if suit.Button("test2", suit.layout.row(nil,40)).hovered then
if gui.Button{text = "Foo"} then -- drawing options can be provided for each widget ... optionally
menu_open.foo = not menu_open.foo suit.Button("You can see", {align='left', valign='top'}, suit.layout.row(nil,30))
end suit.Button("...but you can't touch!", {align='right', valign='bottom'}, suit.layout.row(nil,30))
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
end -- Checkbox
gui.group.pop{} suit.Checkbox(chk, {align='right'}, suit.layout.row())
if menu_open.demo then -- nested layouts
gui.group{grow = "down", pos = {200, 80}, function() suit.layout.push(suit.layout.row())
love.graphics.setFont(fonts[20]) suit.Slider(slider, suit.layout.col(160, 20))
gui.Label{text = "Widgets"} suit.Label(("%.02f"):format(slider.value), suit.layout.col(40))
love.graphics.setFont(fonts[12]) suit.layout.pop()
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() -- image buttons
gui.Button{text = "", size = {2}} -- acts as separator suit.ImageButton({normal, hot = hot}, suit.layout.row(200,100))
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 chk.checked then
if gui.Checkbox{checked = check1, text = "Checkbox", size = {"tight"}} then -- precomputed layout can fill up available space
check1 = not check1 suit.layout.reset()
print(check1) rows = suit.layout.rows{pos = {400,100},
end min_height = 300,
if gui.Checkbox{checked = check2, text = "Another Checkbox"} then {200, 30},
check2 = not check2 {30, 'fill'},
end {200, 30},
if gui.Checkbox{checked = check2, text = "Linked Checkbox"} then }
check2 = not check2 suit.Label("You uncovered the secret!", {align="left", font = smallerFont}, rows.item(1))
end suit.Label(slider.value, {align='left'}, rows.item(3))
gui.group.pop{}
gui.group{grow = "right", function() -- give different id to slider on same object so they don't grab
gui.Label{text = "Input", size = {70}} -- each others user interaction
gui.Input{info = input, size = {300}} suit.Slider(slider, {id = 'vs', vertical=true}, rows.item(2))
end} print(rows.item(3))
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 end
end end
function love.draw() function love.draw()
gui.core.draw() -- draw the gui
suit.core.draw()
end end
function love.keypressed(key, code) -- forward keyboard events
gui.keyboard.pressed(key) function love.textinput(t)
-- LÖVE 0.8: see if this code can be converted in a character suit.core.textinput(t)
if pcall(string.char, code) and code > 0 then
gui.keyboard.textinput(string.char(code))
end
end end
-- LÖVE 0.9 function love.keypressed(key)
function love.textinput(str) suit.core.keypressed(key)
gui.keyboard.textinput(str)
end end
# Documentation -- 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
To be done... function generateImageButton()
local normal, hot = love.image.newImageData(200,100), love.image.newImageData(200,100)
normal:mapPixel(function(x,y)
# License local d = (x/200-.5)^2 + (y/100-.5)^2
if d < .12 then
Copyright (c) 2012 Matthias Richter return 200,160,20,255
end
Permission is hereby granted, free of charge, to any person obtaining a copy return 0,0,0,0
of this software and associated documentation files (the "Software"), to deal end)
in the Software without restriction, including without limitation the rights hot:mapPixel(function(x,y)
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell local d = (x/200-.5)^2 + (y/100-.5)^2
copies of the Software, and to permit persons to whom the Software is if d < .13 then
furnished to do so, subject to the following conditions: return 255,255,255,255
end
The above copyright notice and this permission notice shall be included in return 0,0,0,0
all copies or substantial portions of the Software. end)
return love.graphics.newImage(normal), love.graphics.newImage(hot)
Except as contained in this notice, the name(s) of the above copyright holders end
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/

View file

@ -1,77 +1,24 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
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 core = require(BASE .. 'core')
local group = require(BASE .. 'group')
local mouse = require(BASE .. 'mouse')
local keyboard = require(BASE .. 'keyboard')
-- the widget return function(text, ...)
-- {text = text, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} local opt, x,y,w,h = core.getOptionsAndSize(...)
return function(w) opt.id = opt.id or text
assert(type(w) == "table" and w.text, "Invalid argument") opt.font = opt.font or love.graphics.getFont()
-- if tight fit requested, compute the size according to text size w = w or opt.font:getWidth(text) + 4
-- have a 2px margin around the text h = h or opt.font:getHeight() + 4
local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight')
if tight then core.registerHitbox(opt.id, x,y,w,h)
local f = assert(love.graphics.getFont()) core.registerDraw(core.theme.Button, text, opt, x,y,w,h)
if w.size[1] == 'tight' then
w.size[1] = f:getWidth(w.text) + 4 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 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')
end

View file

@ -1,68 +1,28 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
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 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(checkbox, ...)
return function(w) local opt, x,y,w,h = core.getOptionsAndSize(...)
assert(type(w) == "table") opt.id = opt.id or checkbox
w.text = w.text or "" opt.font = opt.font or love.graphics.getFont()
local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight') w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4)
if tight then h = h or opt.font:getHeight() + 4
local f = assert(love.graphics.getFont())
if w.size[1] == 'tight' then core.registerHitbox(opt.id, x,y,w,h)
w.size[1] = f:getWidth(w.text) local hit = core.mouseReleasedOn(opt.id)
if hit then
checkbox.checked = not checkbox.checked
end end
if w.size[2] == 'tight' then core.registerDraw(core.theme.Checkbox, checkbox, opt, x,y,w,h)
w.size[2] = f:getHeight(w.text)
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 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
end
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
end

251
core.lua
View file

@ -1,127 +1,162 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
of this software and associated documentation files (the "Software"), to deal local theme = require(BASE..'theme')
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 -- helper
all copies or substantial portions of the Software. local function getOptionsAndSize(opt, ...)
if type(opt) == "table" then
Except as contained in this notice, the name(s) of the above copyright holders return opt, ...
shall not be used in advertising or otherwise to promote the sale, use or end
other dealings in this Software without prior written authorization. return {}, opt, ...
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 end
local function strictOr(...) -- gui state
local n = select("#", ...) local hot, hot_last, active
local ret = false local NONE = {}
for i = 1,n do ret = select(i, ...) or ret end
return ret local function anyHot()
return hot ~= nil
end end
-- local function isHot(id)
-- Widget ID return id == hot
--
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 end
-- local function wasHot(id)
-- Drawing / Frame update return id == hot_last
-- end
local draw_items = {n = 0}
local function registerDraw(id, f, ...) local function isActive(id)
assert(type(f) == 'function' or (getmetatable(f) or {}).__call, return id == active
'Drawing function is not a callable type!') end
local font = love.graphics.getFont() -- mouse handling
local mouse_x, mouse_y, mouse_button_down = 0,0, false
local state = 'normal' local function mouseInRect(x,y,w,h)
if mouse.isHot(id) or keyboard.hasFocus(id) then return not (mouse_x < x or mouse_y < y or mouse_x > x+w or mouse_y > y+h)
state = mouse.isActive(id) and 'active' or 'hot' 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
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))
end end
end end
-- actually update-and-draw 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
local function draw() local function draw()
keyboard.endFrame() exitFrame()
mouse.endFrame() for i = 1,draw_queue.n do
group.endFrame() draw_queue[i]()
end
-- save graphics state draw_queue.n = 0
local c = {love.graphics.getColor()} enterFrame()
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()
end end
-- local module = {
-- The Module getOptionsAndSize = getOptionsAndSize,
--
return { 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, generateID = generateID,
enterFrame = enterFrame,
style = require((...):match("(.-)[^%.]+$") .. 'style-default'), exitFrame = exitFrame,
registerDraw = registerDraw, registerDraw = registerDraw,
theme = theme,
draw = draw, draw = draw,
strictAnd = strictAnd,
strictOr = strictOr,
} }
theme.core = module
return module

151
group.lua
View file

@ -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,
})

42
imagebutton.lua Normal file
View file

@ -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

View file

@ -1,40 +1,13 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
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 = (...) .. '.' local BASE = (...) .. '.'
assert(not BASE:match('%.init%.$'), "Invalid require path `"..(...).."' (drop the `.init').")
return { return {
core = require(BASE .. 'core'), core = require(BASE .. 'core'),
group = require(BASE .. 'group'), layout = require(BASE .. 'layout'),
mouse = require(BASE .. 'mouse'),
keyboard = require(BASE .. 'keyboard'),
Button = require(BASE .. 'button'), Button = require(BASE .. 'button'),
ImageButton = require(BASE .. 'imagebutton'),
Slider = require(BASE .. 'slider'), Slider = require(BASE .. 'slider'),
Slider2D = require(BASE .. 'slider2d'),
Label = require(BASE .. 'label'), Label = require(BASE .. 'label'),
Input = require(BASE .. 'input'), Input = require(BASE .. 'input'),
Checkbox = require(BASE .. 'checkbox') Checkbox = require(BASE .. 'checkbox')

145
input.lua
View file

@ -1,80 +1,79 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
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 core = require(BASE .. 'core')
local group = require(BASE .. 'group') local utf8 = require 'utf8'
local mouse = require(BASE .. 'mouse')
local keyboard = require(BASE .. 'keyboard')
local utf8 = require(BASE .. 'utf8')
-- {info = {text = "", cursor = text:len()}, pos = {x, y}, size={w, h}, widgetHit=widgetHit, draw=draw} local function split(str, pos)
return function(w) local offset = utf8.offset(str, pos)
assert(type(w) == "table" and type(w.info) == "table", "Invalid argument") return str:sub(1, offset-1), str:sub(offset)
w.info.text = w.info.text or ""
w.info.cursor = math.min(w.info.cursor or w.info.text:len(), w.info.text:len())
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
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
end end
core.registerDraw(id, w.draw or core.style.Input, return function(input, ...)
w.info.text, w.info.cursor, pos[1],pos[2], size[1],size[2]) 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()
return mouse.releasedOn(id) or keyboard.pressedOn(id, 'return') w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4
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|
core.registerHitbox(opt.id, x,y,w,h)
core.grabKeyboardFocus(opt.id)
opt.hasKeyboardFocus = core.hasKeyboardFocus(opt.id)
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(core.theme.Input, input, opt, x,y,w,h)
return {
id = opt.id,
hit = core.mouseReleasedOn(opt.id),
submitted = core.keyPressedOn(opt.id, "return"),
hovered = core.isHot(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id)
}
end end

View file

@ -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})

View file

@ -1,61 +1,24 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
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 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(text, ...)
return function(w) local opt, x,y,w,h = core.getOptionsAndSize(...)
assert(type(w) == "table" and w.text, "Invalid argument") opt.id = opt.id or text
w.align = w.align or 'left' opt.font = opt.font or love.graphics.getFont()
local tight = w.size and (w.size[1] == 'tight' or w.size[2] == 'tight') w = w or opt.font:getWidth(text) + 4
if tight then h = h or opt.font:getHeight() + 4
local f = assert(love.graphics.getFont())
if w.size[1] == 'tight' then core.registerHitbox(opt.id, x,y,w,h)
w.size[1] = f:getWidth(w.text) core.registerDraw(core.theme.Label, text, opt, x,y,w,h)
return {
id = opt.id,
hit = core.mouseReleasedOn(opt.id),
hovered = core.isHot(opt.id),
entered = core.isHot(opt.id) and not core.wasHot(opt.id),
left = not core.isHot(opt.id) and core.wasHot(opt.id)
}
end 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)
end

286
layout.lua Normal file
View file

@ -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
--]]

23
license.txt Normal file
View file

@ -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.

110
mouse.lua
View file

@ -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})

View file

@ -1,79 +1,55 @@
--[[ -- This file is part of QUI, copyright (c) 2016 Matthias Richter
Copyright (c) 2012 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy local BASE = (...):match('(.-)[^%.]+$')
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 core = require(BASE .. 'core')
local group = require(BASE .. 'group')
local mouse = require(BASE .. 'mouse')
local keyboard = require(BASE .. 'keyboard')
-- {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(info, ...)
return function(w) local opt, x,y,w,h = core.getOptionsAndSize(...)
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() opt.id = opt.id or info
local pos, size = group.getRect(w.pos, w.size)
mouse.updateWidget(id, pos, size, w.widgetHit) info.min = info.min or math.min(info.value, 0)
keyboard.makeCyclable(id) 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
core.registerHitbox(opt.id, x,y,w,h)
if core.isActive(opt.id) then
-- mouse update -- mouse update
local changed = false local mx,my = core.getMousePosition()
if mouse.isActive(id) then if opt.vertical then
keyboard.setFocus(id) fraction = math.min(1, math.max(0, (y+h - my) / h))
if w.vertical then
fraction = math.min(1, math.max(0, (pos[2] - mouse.y + size[2]) / size[2]))
else 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
end end
local v = fraction * (info.max - info.min) + info.min
if v ~= info.value then
info.value = v
value_changed = true
end end
-- keyboard update -- keyboard update
if keyboard.hasFocus(id) then local key_up = opt.vertical and 'up' or 'right'
local keys = w.vertical and {'up', 'down'} or {'right', 'left'} local key_down = opt.vertical and 'down' or 'left'
if keyboard.key == keys[1] then if core.getPressedKey() == key_up then
w.info.value = math.min(w.info.max, w.info.value + w.info.step) info.value = math.min(info.max, info.value + info.step)
changed = true value_changed = true
elseif keyboard.key == keys[2] then elseif core.getPressedKey() == key_down then
w.info.value = math.max(w.info.min, w.info.value - w.info.step) info.value = math.max(info.min, info.value - info.step)
changed = true value_changed = true
end end
end end
core.registerDraw(id, w.draw or core.style.Slider, core.registerDraw(core.theme.Slider, fraction, opt, x,y,w,h)
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 end

View file

@ -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

View file

@ -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,
}

150
theme.lua Normal file
View file

@ -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

168
utf8.lua
View file

@ -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
}