LET THERE BE SUIT!
This commit is contained in:
parent
44be6169e3
commit
b5137a4477
19 changed files with 987 additions and 1481 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
|||
main.lua
|
||||
quickie
|
||||
*.love
|
||||
|
|
299
README.md
299
README.md
|
@ -1,184 +1,177 @@
|
|||
# QUICKIE
|
||||
# SUIT
|
||||
|
||||
Quickie is an [immediate mode gui][IMGUI] library for [LÖVE][LOVE]. Initial inspiration came from the article [Sol on Immediate Mode GUIs (IMGUI)][Sol]. You should check it out to understand how Quickie works.
|
||||
Simple User Interface Toolkit for LÖVE.
|
||||
|
||||
## Immediate mode GUI
|
||||
|
||||
SUIT is an immediate mode GUI library.
|
||||
|
||||
With classical (retained) mode libraries you typically have a stage where you
|
||||
create the whole UI when the program initializes. After that point, the GUI
|
||||
does not change much.
|
||||
|
||||
With immediate mode libraries, on the other hand, the GUI is created every
|
||||
frame from scratch. There are no widget objects, only functions that draw the
|
||||
widget and update internal GUI state. This allows to put the widgets in their
|
||||
immediate conceptual context (instead of a construction stage). It also makes
|
||||
the UI very flexible: Don't want to draw a widget? Simply remove the call.
|
||||
Handling the mutable data (e.g., text of an input box) of each widget is your
|
||||
responsibility. This separation of behaviour and data
|
||||
|
||||
## What SUIT is
|
||||
|
||||
SUIT is simple: It provides only the most important widgets for games:
|
||||
|
||||
- Buttons (including Image Buttons)
|
||||
- Checkboxes
|
||||
- Text Input
|
||||
- Value Sliders
|
||||
|
||||
SUIT is comfortable: It features a simple, yet effective row/column-based
|
||||
layouting engine.
|
||||
|
||||
SUIT is adaptable: You can easily alter the color scheme, change how widgets
|
||||
are drawn or swap the whole theme.
|
||||
|
||||
SUIT is hackable: The core library can be used to construct new widgets with
|
||||
relative ease.
|
||||
|
||||
**SUIT is good at games**
|
||||
|
||||
## What SUIT is not
|
||||
|
||||
SUIT is not a complete GUI library: It does not provide dropdowns, subwindows,
|
||||
radio buttons, menu bars, ribbons, etc.
|
||||
|
||||
SUIT is not a complete GUI library: SUIT spits MVC and other good OO practices
|
||||
in the face.
|
||||
|
||||
SUIT is not a complete GUI library: There is no markup language to generate or
|
||||
style the GUI.
|
||||
|
||||
**SUIT is not good at "serious" applications**
|
||||
|
||||
## Example code
|
||||
|
||||
## Documentation
|
||||
|
||||
To be done.
|
||||
|
||||
## Example code
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
local gui = require "Quickie"
|
||||
```lua
|
||||
suit = require 'suit'
|
||||
|
||||
function love.load()
|
||||
-- preload fonts
|
||||
fonts = {
|
||||
[12] = love.graphics.newFont(12),
|
||||
[20] = love.graphics.newFont(20),
|
||||
}
|
||||
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
|
||||
-- generate some assets
|
||||
snd = generateClickySound()
|
||||
normal, hot = generateImageButton()
|
||||
smallerFont = love.graphics.newFont(10)
|
||||
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}}
|
||||
-- mutable widget data
|
||||
local slider= {value = .5, max = 2}
|
||||
local input = {text = "Hello"}
|
||||
local chk = {text = "Check me out"}
|
||||
|
||||
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
|
||||
if gui.Checkbox{checked = menu_open.main, text = "Show Menu"} then
|
||||
menu_open.main = not menu_open.main
|
||||
-- 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
|
||||
|
||||
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
|
||||
-- Input box
|
||||
if suit.Input(input, suit.layout.row()).submitted then
|
||||
print(input.text)
|
||||
end
|
||||
|
||||
if menu_open.right then
|
||||
gui.group.push{grow = "up"}
|
||||
if gui.Button{text = "Foo"} then
|
||||
menu_open.foo = not menu_open.foo
|
||||
end
|
||||
if menu_open.foo then
|
||||
gui.Button{text = "???"}
|
||||
end
|
||||
gui.group.pop{}
|
||||
|
||||
gui.Button{text = "Bar"}
|
||||
gui.Button{text = "Baz"}
|
||||
end
|
||||
gui.group.pop{}
|
||||
|
||||
if gui.Button{text = "Widget demo"} then
|
||||
menu_open.demo = not menu_open.open
|
||||
-- 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
|
||||
|
||||
end
|
||||
gui.group.pop{}
|
||||
-- Checkbox
|
||||
suit.Checkbox(chk, {align='right'}, suit.layout.row())
|
||||
|
||||
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}
|
||||
-- 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()
|
||||
|
||||
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}
|
||||
-- image buttons
|
||||
suit.ImageButton({normal, hot = hot}, suit.layout.row(200,100))
|
||||
|
||||
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{}
|
||||
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},
|
||||
}
|
||||
suit.Label("You uncovered the secret!", {align="left", font = smallerFont}, rows.item(1))
|
||||
suit.Label(slider.value, {align='left'}, rows.item(3))
|
||||
|
||||
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}}
|
||||
-- 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
|
||||
|
||||
function love.draw()
|
||||
gui.core.draw()
|
||||
-- draw the gui
|
||||
suit.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
|
||||
-- forward keyboard events
|
||||
function love.textinput(t)
|
||||
suit.core.textinput(t)
|
||||
end
|
||||
|
||||
-- LÖVE 0.9
|
||||
function love.textinput(str)
|
||||
gui.keyboard.textinput(str)
|
||||
function love.keypressed(key)
|
||||
suit.core.keypressed(key)
|
||||
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...
|
||||
|
||||
|
||||
# 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/
|
||||
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
|
||||
return 0,0,0,0
|
||||
end)
|
||||
hot:mapPixel(function(x,y)
|
||||
local d = (x/200-.5)^2 + (y/100-.5)^2
|
||||
if d < .13 then
|
||||
return 255,255,255,255
|
||||
end
|
||||
return 0,0,0,0
|
||||
end)
|
||||
return love.graphics.newImage(normal), love.graphics.newImage(hot)
|
||||
end
|
||||
```
|
||||
|
|
91
button.lua
91
button.lua
|
@ -1,77 +1,24 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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
|
||||
w = w or opt.font:getWidth(text) + 4
|
||||
h = h or opt.font:getHeight() + 4
|
||||
|
||||
core.registerHitbox(opt.id, x,y,w,h)
|
||||
core.registerDraw(core.theme.Button, text, opt, x,y,w,h)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
|
84
checkbox.lua
84
checkbox.lua
|
@ -1,68 +1,28 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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)
|
||||
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
|
||||
if w.size[2] == 'tight' then
|
||||
w.size[2] = f:getHeight(w.text)
|
||||
core.registerDraw(core.theme.Checkbox, checkbox, opt, x,y,w,h)
|
||||
|
||||
return {
|
||||
id = opt.id,
|
||||
hit = hit,
|
||||
hovered = core.isHot(opt.id),
|
||||
entered = core.isHot(opt.id) and not core.wasHot(opt.id),
|
||||
left = not core.isHot(opt.id) and core.wasHot(opt.id)
|
||||
}
|
||||
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
251
core.lua
|
@ -1,127 +1,162 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
local BASE = (...):match('(.-)[^%.]+$')
|
||||
local theme = require(BASE..'theme')
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local BASE = (...):match("(.-)[^%.]+$")
|
||||
local group = require(BASE .. 'group')
|
||||
local mouse = require(BASE .. 'mouse')
|
||||
local keyboard = require(BASE .. 'keyboard')
|
||||
|
||||
--
|
||||
-- Helper functions
|
||||
--
|
||||
|
||||
-- evaluates all arguments
|
||||
local function strictAnd(...)
|
||||
local n = select("#", ...)
|
||||
local ret = true
|
||||
for i = 1,n do ret = select(i, ...) and ret end
|
||||
return ret
|
||||
-- helper
|
||||
local function getOptionsAndSize(opt, ...)
|
||||
if type(opt) == "table" then
|
||||
return opt, ...
|
||||
end
|
||||
return {}, opt, ...
|
||||
end
|
||||
|
||||
local function strictOr(...)
|
||||
local n = select("#", ...)
|
||||
local ret = false
|
||||
for i = 1,n do ret = select(i, ...) or ret end
|
||||
return ret
|
||||
-- gui state
|
||||
local hot, hot_last, active
|
||||
local NONE = {}
|
||||
|
||||
local function anyHot()
|
||||
return hot ~= nil
|
||||
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]
|
||||
local function isHot(id)
|
||||
return id == hot
|
||||
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'
|
||||
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
151
group.lua
|
@ -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
42
imagebutton.lua
Normal 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
|
33
init.lua
33
init.lua
|
@ -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')
|
||||
|
|
145
input.lua
145
input.lua
|
@ -1,80 +1,79 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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 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
|
||||
local function split(str, pos)
|
||||
local offset = utf8.offset(str, pos)
|
||||
return str:sub(1, offset-1), str:sub(offset)
|
||||
end
|
||||
|
||||
core.registerDraw(id, w.draw or core.style.Input,
|
||||
w.info.text, w.info.cursor, pos[1],pos[2], size[1],size[2])
|
||||
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()
|
||||
|
||||
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
|
||||
|
|
108
keyboard.lua
108
keyboard.lua
|
@ -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})
|
75
label.lua
75
label.lua
|
@ -1,61 +1,24 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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)
|
||||
w = w or opt.font:getWidth(text) + 4
|
||||
h = h or opt.font:getHeight() + 4
|
||||
|
||||
core.registerHitbox(opt.id, x,y,w,h)
|
||||
core.registerDraw(core.theme.Label, text, opt, x,y,w,h)
|
||||
|
||||
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
|
||||
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
286
layout.lua
Normal 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
23
license.txt
Normal 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
110
mouse.lua
|
@ -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})
|
100
slider.lua
100
slider.lua
|
@ -1,79 +1,55 @@
|
|||
--[[
|
||||
Copyright (c) 2012 Matthias Richter
|
||||
-- This file is part of QUI, copyright (c) 2016 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
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
|
||||
|
|
99
slider2d.lua
99
slider2d.lua
|
@ -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
|
|
@ -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
150
theme.lua
Normal 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
168
utf8.lua
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue