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
quickie
*.love

331
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()
-- 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
function love.keypressed(key)
suit.core.keypressed(key)
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
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
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
if menu_open.right then
gui.group.push{grow = "up"}
if gui.Button{text = "Foo"} then
menu_open.foo = not menu_open.foo
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
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
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))
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)
return love.graphics.newImage(normal), love.graphics.newImage(hot)
end
```

View file

@ -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:
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 BASE = (...):match('(.-)[^%.]+$')
local core = require(BASE .. 'core')
local group = require(BASE .. 'group')
local mouse = require(BASE .. 'mouse')
local keyboard = require(BASE .. 'keyboard')
-- 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")
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()
-- 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
w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4
-- Generate unique identifier for gui state update and querying.
local id = w.id or core.generateID()
core.registerHitbox(opt.id, x,y,w,h)
core.registerDraw(core.theme.Button, text, opt, x,y,w,h)
-- 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

View file

@ -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:
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 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 ""
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()
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
w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4)
h = h or opt.font:getHeight() + 4
core.registerHitbox(opt.id, x,y,w,h)
local hit = core.mouseReleasedOn(opt.id)
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

263
core.lua
View file

@ -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
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
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
-- 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()
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 {
local module = {
getOptionsAndSize = getOptionsAndSize,
anyHot = anyHot,
isHot = isHot,
wasHot = wasHot,
isActive = isActive,
mouseInRect = mouseInRect,
registerHitbox = registerHitbox,
registerMouseHit = registerMouseHit,
mouseReleasedOn = mouseReleasedOn,
updateMouse = updateMouse,
getMousePosition = getMousePosition,
getPressedKey = getPressedKey,
keypressed = keypressed,
textinput = textinput,
grabKeyboardFocus = grabKeyboardFocus,
hasKeyboardFocus = hasKeyboardFocus,
keyPressedOn = keyPressedOn,
generateID = generateID,
style = require((...):match("(.-)[^%.]+$") .. 'style-default'),
enterFrame = enterFrame,
exitFrame = exitFrame,
registerDraw = registerDraw,
theme = theme,
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 @@
--[[
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'),
layout = require(BASE .. 'layout'),
Button = require(BASE .. 'button'),
ImageButton = require(BASE .. 'imagebutton'),
Slider = require(BASE .. 'slider'),
Slider2D = require(BASE .. 'slider2d'),
Label = require(BASE .. 'label'),
Input = require(BASE .. 'input'),
Checkbox = require(BASE .. 'checkbox')

141
input.lua
View file

@ -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:
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 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')
local utf8 = require 'utf8'
-- {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())
local function split(str, pos)
local offset = utf8.offset(str, pos)
return str:sub(1, offset-1), str:sub(offset)
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)
if mouse.isActive(id) then keyboard.setFocus(id) end
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()
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
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
core.registerDraw(id, w.draw or core.style.Input,
w.info.text, w.info.cursor, pos[1],pos[2], size[1],size[2])
-- 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
return mouse.releasedOn(id) or keyboard.pressedOn(id, 'return')
-- 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

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 @@
--[[
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:
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 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'
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()
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
w = w or opt.font:getWidth(text) + 4
h = h or opt.font:getHeight() + 4
local id = w.id or core.generateID()
local pos, size = group.getRect(w.pos, w.size)
core.registerHitbox(opt.id, x,y,w,h)
core.registerDraw(core.theme.Label, text, opt, x,y,w,h)
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

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 @@
--[[
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:
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 BASE = (...):match('(.-)[^%.]+$')
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(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)
return function(info, ...)
local opt, x,y,w,h = core.getOptionsAndSize(...)
local id = w.id or core.generateID()
local pos, size = group.getRect(w.pos, w.size)
opt.id = opt.id or info
mouse.updateWidget(id, pos, size, w.widgetHit)
keyboard.makeCyclable(id)
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
core.registerHitbox(opt.id, x,y,w,h)
if core.isActive(opt.id) then
-- 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]))
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]))
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
fraction = math.min(1, math.max(0, (mx - x) / w))
end
local v = fraction * (info.max - info.min) + info.min
if v ~= info.value then
info.value = v
value_changed = true
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
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
core.registerDraw(id, w.draw or core.style.Slider,
fraction, w.vertical, pos[1],pos[2], size[1],size[2])
core.registerDraw(core.theme.Slider, fraction, opt, x,y,w,h)
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

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
}