[*] Initial commit.
This commit is contained in:
commit
6dd7691d71
|
@ -0,0 +1,48 @@
|
|||
# ---> Lua
|
||||
# Compiled Lua sources
|
||||
luac.out
|
||||
|
||||
# luarocks build files
|
||||
*.src.rock
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.os
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
*.def
|
||||
*.exp
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# ldoc output directory
|
||||
doc/
|
||||
# crush library directory
|
||||
lib/
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
gear = "https://git.doublefourteen.io/lua/gear",
|
||||
moonspeak = "https://git.doublefourteen.io/lua/moonspeak"
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
Copyright (c) 2022 The DoubleFourteen Code Forge
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
|
@ -0,0 +1,29 @@
|
|||
Portions of Yui code are based on SUIT - Simple User Interface Toolkit for LÖVE,
|
||||
by Mathias Richter, available at:
|
||||
https://github.com/vrld/SUIT
|
||||
|
||||
Under the following license:
|
||||
|
||||
> 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.
|
|
@ -0,0 +1,29 @@
|
|||
Yui - A Declarative UI library for LÖVE
|
||||
=======================================
|
||||
|
||||
**Yui** - Yet another User Interface, is an attempt to ease the process of assembling trivial menu-like GUIs for your game using [LÖVE](https://love2d.org).
|
||||
|
||||
## Why is that?
|
||||
|
||||
Because I'm spending so much time tweaking and customizing existing libraries,
|
||||
I might as well make my own.
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Yui** depends on:
|
||||
|
||||
* [gear](https://git.doublefourteen.io/lua/gear) for general algorithms.
|
||||
* [moonspeak](https://git.doublefourteen.io/lua/moonspeak) for its localization functionality.
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Portions of this library's widget rendering code are taken from the
|
||||
Simple User Interface Toolkit (**SUIT**) for LÖVE by Matthias Richter.
|
||||
|
||||
SUIT's source code is available at: [vrld/SUIT](https://github.com/vrld/SUIT).
|
||||
SUIT is licensed under the [MIT license](https://github.com/vrld/suit/blob/master/license.txt).
|
||||
|
||||
Widgets offered by **yui** are also inspired by **SUIT**.
|
||||
|
||||
See [ACKNOWLEDGEMENT](README.ACKNOWLEDGEMENT) for full SUIT license
|
||||
information and copyright notice.
|
|
@ -0,0 +1,59 @@
|
|||
local BASE = (...):gsub('button', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local shadowtext = require 'lib.gear.shadowtext'
|
||||
local T = require('lib.moonspeak').translate
|
||||
|
||||
local Button = setmetatable({}, Widget)
|
||||
Button.__index = Button
|
||||
|
||||
|
||||
function Button.new(args)
|
||||
local self = setmetatable(args, Button)
|
||||
|
||||
self.text = self.text or ""
|
||||
self.align = self.align or 'center'
|
||||
self.valign = self.valign or 'center'
|
||||
self.color = self.color or core.theme.color
|
||||
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
|
||||
self.active = false
|
||||
if not self.notranslate then
|
||||
self.text = T(self.text)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
local function hit(button)
|
||||
if not button.active then
|
||||
button.active = true
|
||||
button:onHit()
|
||||
|
||||
button.ui.timer:after(0.15, function() button.active = false end)
|
||||
end
|
||||
end
|
||||
|
||||
function Button:onPointerInput(px,py, clicked)
|
||||
self:grabFocus()
|
||||
if clicked then hit(self) end
|
||||
end
|
||||
|
||||
function Button:onActionInput(action)
|
||||
if action.confirm then hit(self) end
|
||||
end
|
||||
|
||||
function Button:draw()
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local font = self.font or love.graphics.getFont()
|
||||
local c = self:colorForState()
|
||||
|
||||
core.drawBox(x,y,w,h, c, self.cornerRadius)
|
||||
love.graphics.setColor(c.fg)
|
||||
love.graphics.setFont(font)
|
||||
|
||||
y = y + core.verticalOffsetForAlign(self.valign, font, h)
|
||||
shadowtext.printf(self.text, x+2, y, w-4, self.align)
|
||||
end
|
||||
|
||||
return Button
|
|
@ -0,0 +1,64 @@
|
|||
local BASE = (...):gsub('checkbox', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local shadowtext = require 'lib.gear.shadowtext'
|
||||
local T = require('lib.moonspeak').translate
|
||||
|
||||
local Checkbox = setmetatable({}, Widget)
|
||||
Checkbox.__index = Checkbox
|
||||
|
||||
|
||||
function Checkbox.new(args)
|
||||
local self = setmetatable(args, Checkbox)
|
||||
|
||||
self.text = self.text or ""
|
||||
self.text = self.notranslate and self.text or T(self.text)
|
||||
self.align = self.align or 'left'
|
||||
self.valign = self.valign or 'center'
|
||||
self.color = self.color or core.theme.color
|
||||
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
|
||||
self.checked = self.checked or false
|
||||
return self
|
||||
end
|
||||
|
||||
function Checkbox:onPointerInput(px,py, clicked)
|
||||
self:grabFocus()
|
||||
if clicked then
|
||||
self.checked = not self.checked
|
||||
self:onChange(self.checked)
|
||||
end
|
||||
end
|
||||
|
||||
function Checkbox:onActionInput(action)
|
||||
if action.confirm then
|
||||
self.checked = not self.checked
|
||||
self:onChange(self.checked)
|
||||
end
|
||||
end
|
||||
|
||||
function Checkbox:draw()
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local c = self:colorForState()
|
||||
local font = self.font or love.graphics.getFont()
|
||||
|
||||
-- Draw checkbox
|
||||
core.drawBox(x+h/10,y+h/10,h*.8,h*.8, c, self.cornerRadius)
|
||||
love.graphics.setColor(c.fg)
|
||||
if self.checked then
|
||||
love.graphics.setLineStyle('smooth')
|
||||
love.graphics.setLineWidth(5)
|
||||
love.graphics.setLineJoin('bevel')
|
||||
love.graphics.line(x+h*.2,y+h*.55, x+h*.45,y+h*.75, x+h*.8,y+h*.2)
|
||||
end
|
||||
|
||||
-- Most of the times checkboxes have no text, so test for performance
|
||||
if self.text ~= "" then
|
||||
love.graphics.setFont(font)
|
||||
y = y + core.verticalOffsetForAlign(self.valign, font, self.h)
|
||||
shadowtext.printf(self.text, x + h, y, w - h, self.align)
|
||||
end
|
||||
end
|
||||
|
||||
return Checkbox
|
|
@ -0,0 +1,148 @@
|
|||
local BASE = (...):gsub('choice', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local shadowtext = require 'lib.gear.shadowtext'
|
||||
local T = require('lib.moonspeak').translate
|
||||
|
||||
local Choice = setmetatable({}, Widget)
|
||||
Choice.__index = Choice
|
||||
|
||||
|
||||
function Choice.new(args)
|
||||
local self = setmetatable(args, Choice)
|
||||
|
||||
self.align = self.align or 'center'
|
||||
self.valign = self.valign or 'center'
|
||||
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
|
||||
self.color = self.color or core.theme.color
|
||||
self.hovered = false
|
||||
self.choices = self.choices or { "" }
|
||||
self.nowrap = self.nowrap or #self.choices == 0
|
||||
self.index = 1 -- by default
|
||||
|
||||
for i,choice in ipairs(self.choices) do
|
||||
-- Expand shorthands
|
||||
if type(choice) ~= 'table' then
|
||||
choice = {
|
||||
text = tostring(choice),
|
||||
notranslate = type(choice) ~= 'string',
|
||||
value = choice
|
||||
}
|
||||
|
||||
self.choices[i] = choice
|
||||
end
|
||||
-- Mark default choice if needed
|
||||
if choice.value == self.default then
|
||||
self.index = i
|
||||
end
|
||||
-- Translate choice
|
||||
if not (self.notranslate or choice.notranslate) then
|
||||
choice.text = T(choice.text)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Choice:checkIndex()
|
||||
if self.nowrap then
|
||||
self.index = math.min(math.max(self.index, 1), #self.choices)
|
||||
else
|
||||
if self.index < 1 then
|
||||
self.index = #self.choices
|
||||
end
|
||||
if self.index > #self.choices then
|
||||
self.index = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Choice:onActionInput(action)
|
||||
local oldindex = self.index
|
||||
local handled = false
|
||||
|
||||
-- Change choice
|
||||
if action.left then
|
||||
self.index = oldindex - 1
|
||||
handled = true
|
||||
end
|
||||
if action.right then
|
||||
self.index = oldindex + 1
|
||||
handled = true
|
||||
end
|
||||
if not handled then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Apply wrapping
|
||||
self:checkIndex()
|
||||
|
||||
-- Fire event if necessary
|
||||
if oldindex ~= self.index then
|
||||
self:onChange(self.choices[self.index])
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Choice:onPointerInput(px,py, clicked)
|
||||
self:grabFocus()
|
||||
if not clicked then
|
||||
return
|
||||
end
|
||||
|
||||
local mx = px - self.x
|
||||
local oldindex = self.index
|
||||
|
||||
-- Test whether arrows are hit
|
||||
-- NOTE: don't care about arrows being disabled, checkIndex() will fix that.
|
||||
if mx <= self.h+2 then
|
||||
self.index = self.index - 1
|
||||
elseif mx >= self.w - self.h-2 then
|
||||
self.index = self.index + 1
|
||||
end
|
||||
|
||||
self:checkIndex()
|
||||
if oldindex ~= self.index then
|
||||
self:onChange(self.choices[self.index])
|
||||
end
|
||||
end
|
||||
|
||||
function Choice:draw()
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local font = self.font or love.graphics.getFont()
|
||||
local c = self:colorForState()
|
||||
|
||||
core.drawBox(x,y,w,h, c, self.cornerRadius)
|
||||
|
||||
if self.ui.focused == self then
|
||||
-- draw < and > arrows, desaturate color if arrow is disabled
|
||||
local cc = self.color.hovered
|
||||
|
||||
love.graphics.setLineStyle('smooth')
|
||||
love.graphics.setLineWidth(3)
|
||||
love.graphics.setLineJoin('bevel')
|
||||
|
||||
local r, g, b = cc.fg[1], cc.fg[2], cc.fg[3]
|
||||
local a = (self.nowrap and self.index == 1) and 0.4 or 1
|
||||
|
||||
love.graphics.setColor(r,g,b,a)
|
||||
love.graphics.line(x+h*.8,y+h*.2, x+h*.5,y+h*.5, x+h*.8,y+h*.8)
|
||||
|
||||
a = (self.nowrap and self.index == #self.choices) and 0.4 or 1
|
||||
|
||||
love.graphics.setColor(r,g,b,a)
|
||||
love.graphics.line(x+w-h*.8,y+h*.2, x+w-h*.5,y+h*.5, x+w-h*.8,y+h*.8)
|
||||
end
|
||||
|
||||
-- draw text
|
||||
local text = self.choices[self.index].text
|
||||
|
||||
y = y + core.verticalOffsetForAlign(self.valign, font, h)
|
||||
|
||||
love.graphics.setColor(c.fg)
|
||||
love.graphics.setFont(font)
|
||||
shadowtext.printf(text, x+h+2, y, w-2*(h + 2), self.align)
|
||||
end
|
||||
|
||||
return Choice
|
|
@ -0,0 +1,24 @@
|
|||
local BASE = (...):gsub('columns', '')
|
||||
|
||||
local Layout = require BASE..'layout'
|
||||
|
||||
local Columns = setmetatable({}, Layout)
|
||||
Columns.__index = Columns
|
||||
|
||||
|
||||
-- Advance position to next column,
|
||||
-- given current position, widget dimensions and padding.
|
||||
local function columnadvance(x,y, ww,wh, padding)
|
||||
return x + ww + padding, y
|
||||
end
|
||||
|
||||
function Columns.new(args)
|
||||
local self = setmetatable(Layout.new(args), Columns)
|
||||
|
||||
self.advance = columnadvance
|
||||
self.prev = 'left'
|
||||
self.next = 'right'
|
||||
return self
|
||||
end
|
||||
|
||||
return Columns
|
|
@ -0,0 +1,27 @@
|
|||
local BASE = (...):gsub('core', '')
|
||||
|
||||
local core = { theme = require(BASE..'theme') }
|
||||
core.__index = core
|
||||
|
||||
-- Helpers for drawing
|
||||
function core.verticalOffsetForAlign(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
|
||||
|
||||
function core.drawBox(x,y,w,h, color, cornerRadius)
|
||||
w = math.max(cornerRadius/2, w)
|
||||
if h < cornerRadius/2 then
|
||||
y,h = y - (cornerRadius - h), cornerRadius/2
|
||||
end
|
||||
|
||||
love.graphics.setColor(color.bg)
|
||||
love.graphics.rectangle('fill', x,y, w,h, cornerRadius)
|
||||
end
|
||||
|
||||
return core
|
|
@ -0,0 +1,284 @@
|
|||
--- crush - The uncomplicated dependency system for LÖVE.
|
||||
--
|
||||
-- Author: Lorenzo Cogotti
|
||||
-- Copyright: 2022 The DoubleFourteen Code Forge
|
||||
-- License: MIT (see LICENSE file for details)
|
||||
|
||||
local io = require 'io'
|
||||
local os = require 'os'
|
||||
|
||||
-- System specific functions
|
||||
--
|
||||
-- Portions of this code are based on work from the LuaRocks project.
|
||||
-- LuaRocks is free software and uses the MIT license.
|
||||
--
|
||||
-- LuaRocks website: https://luarocks.org
|
||||
-- LuaRocks sources: https://github.com/luarocks/luarocks
|
||||
|
||||
local is_windows = package.config:sub(1,1) == "\\"
|
||||
|
||||
local is_directory
|
||||
local Q
|
||||
local quiet
|
||||
local chdir
|
||||
local mkdir
|
||||
|
||||
if is_windows then
|
||||
-- ---------------
|
||||
-- NOTE: untested!
|
||||
-- ---------------
|
||||
|
||||
-- local
|
||||
function is_directory(path)
|
||||
local fh, _, code = io.open(path, 'r')
|
||||
|
||||
if code == 13 then -- directories return "Permission denied"
|
||||
fh, _, code = io.open(path.."\\", 'r')
|
||||
if code == 2 then -- directories return 2, files return 22
|
||||
return true
|
||||
end
|
||||
end
|
||||
if fh then
|
||||
fh:close()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function split_path(s)
|
||||
local drive = ""
|
||||
local root = ""
|
||||
local rest
|
||||
|
||||
local unquoted = s:match("^['\"](.*)['\"]$")
|
||||
if unquoted then
|
||||
s = unquoted
|
||||
end
|
||||
if s:match("^.:") then
|
||||
drive = s:sub(1, 2)
|
||||
s = s:sub(3)
|
||||
end
|
||||
if s:match("^[\\/]") then
|
||||
root = s:sub(1, 1)
|
||||
rest = s:sub(2)
|
||||
else
|
||||
rest = s
|
||||
end
|
||||
return drive, root, rest
|
||||
end
|
||||
|
||||
-- local
|
||||
function Q(s)
|
||||
local drive, root, rest = split_path(s)
|
||||
if root ~= "" then
|
||||
s = s:gsub("/", "\\")
|
||||
end
|
||||
if s == "\\" then
|
||||
return '\\' -- CHDIR needs special handling for root dir
|
||||
end
|
||||
|
||||
-- URLs and anything else
|
||||
s = s:gsub('\\(\\*)"', '\\%1%1"')
|
||||
s = s:gsub('\\+$', '%0%0')
|
||||
s = s:gsub('"', '\\"')
|
||||
s = s:gsub('(\\*)%%', '%1%1"%%"')
|
||||
return '"'..s..'"'
|
||||
end
|
||||
|
||||
-- local
|
||||
function quiet(cmd)
|
||||
return cmd.." 2> NUL 1> NUL"
|
||||
end
|
||||
|
||||
-- local
|
||||
function chdir(newdir, cmd)
|
||||
local drive = newdir:match("^([A-Za-z]:)")
|
||||
|
||||
cmd = "cd "..Q(newdir).." & "..cmd
|
||||
if drive then
|
||||
cmd = drive.." & "..cmd
|
||||
end
|
||||
return cmd
|
||||
end
|
||||
|
||||
-- local
|
||||
function mkdir(path)
|
||||
local cmd = "mkdir "..Q(path).." 2> NUL 1> NUL"
|
||||
|
||||
os.execute(cmd)
|
||||
if not is_directory(path) then
|
||||
error("Couldn't create directory '"..path.."'.")
|
||||
end
|
||||
end
|
||||
else
|
||||
-- local
|
||||
function is_directory(path)
|
||||
local fh, _, code = io.open(path.."/.", 'r')
|
||||
|
||||
if code == 2 then -- "No such file or directory"
|
||||
return false
|
||||
end
|
||||
if code == 20 then -- "Not a directory", regardless of permissions
|
||||
return false
|
||||
end
|
||||
if code == 13 then -- "Permission denied", but is a directory
|
||||
return true
|
||||
end
|
||||
if fh then
|
||||
_, _, code = fh:read(1)
|
||||
fh:close()
|
||||
if code == 21 then -- "Is a directory"
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- local
|
||||
function Q(s)
|
||||
return "'"..s:gsub("'", "'\\''").."'"
|
||||
end
|
||||
|
||||
-- local
|
||||
function quiet(cmd)
|
||||
return cmd.." >/dev/null 2>&1"
|
||||
end
|
||||
|
||||
-- local
|
||||
function chdir(newdir, cmd)
|
||||
return "cd "..Q(newdir).." && "..cmd
|
||||
end
|
||||
|
||||
-- local
|
||||
function mkdir(path)
|
||||
local cmd = "mkdir "..Q(path).." >/dev/null 2>&1"
|
||||
|
||||
os.execute(cmd)
|
||||
if not is_directory(path) then
|
||||
error("Couldn't create directory '"..path.."'.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Dependency fetch
|
||||
|
||||
local function fetch(dep)
|
||||
local dest = 'lib/'..dep.name
|
||||
|
||||
print(("Dependency %s -> %s (%s)"):format(dep.name, dest, dep.url))
|
||||
|
||||
local cmd, fullcmd
|
||||
|
||||
if is_directory(dest) then
|
||||
-- Directory exists, pull operation
|
||||
cmd = "git pull"
|
||||
fullcmd = chdir(dest, quiet("git pull"))
|
||||
else
|
||||
-- Directory doesn't exist, clone operation
|
||||
cmd = "git clone "..Q(dep.url).." "..Q(dep.name)
|
||||
fullcmd = chdir("lib", quiet(cmd))
|
||||
end
|
||||
|
||||
if not os.execute(fullcmd) then
|
||||
error(name..": Dependency fetch failed ("..cmd..").")
|
||||
end
|
||||
end
|
||||
|
||||
-- .lovedeps file scan
|
||||
|
||||
local function map_file(name)
|
||||
local fh = io.open(name, 'r')
|
||||
if fh == nil then
|
||||
error(name..": can't read file.")
|
||||
end
|
||||
|
||||
local contents = fh:read('*all')
|
||||
fh:close()
|
||||
|
||||
return contents
|
||||
end
|
||||
|
||||
local function scandeps(manifest, mode, deps)
|
||||
mode = mode or 'nodups'
|
||||
deps = deps or {}
|
||||
|
||||
local contents = map_file(manifest)
|
||||
contents = "return "..contents
|
||||
|
||||
local fun, res = load(contents, manifest, 't', {})
|
||||
if not fun then
|
||||
error(res)
|
||||
end
|
||||
|
||||
local ok, def = pcall(fun)
|
||||
if not ok then
|
||||
error(def) -- def is now pcall()'s error message
|
||||
end
|
||||
if type(def) ~= 'table' then
|
||||
error("[string \""..manifest.."\"]: Loading resulted in a '"..type(def).."', while 'table' was expected.")
|
||||
end
|
||||
|
||||
for name,url in pairs(def) do
|
||||
if type(url) == 'function' then
|
||||
goto skip -- ignore functions
|
||||
end
|
||||
|
||||
if type(url) ~= 'string' then
|
||||
error("[string \""..manifest.."\"]: "..name..": git repository URL must be a 'string'.")
|
||||
end
|
||||
|
||||
for i in ipairs(deps) do
|
||||
if name == deps[i].name then
|
||||
if mode == 'skipdups' then
|
||||
goto skip
|
||||
end
|
||||
|
||||
error("[string \""..manifest.."\"]: "..name..": Duplicate dependency.")
|
||||
end
|
||||
end
|
||||
|
||||
deps[#deps+1] = { name = name, url = url }
|
||||
|
||||
::skip::
|
||||
end
|
||||
|
||||
return deps
|
||||
end
|
||||
|
||||
-- Entry point
|
||||
|
||||
local function file_exists(name)
|
||||
local fh = io.open(name, 'r')
|
||||
if fh ~= nil then
|
||||
fh:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function run()
|
||||
local deps = scandeps(".lovedeps")
|
||||
|
||||
mkdir("lib")
|
||||
|
||||
-- NOTE: deps array may grow while scanning
|
||||
local i = 1
|
||||
while i <= #deps do
|
||||
local dep = deps[i]
|
||||
|
||||
-- Fetch dependency
|
||||
fetch(dep)
|
||||
|
||||
-- Resolve dependency's dependencies
|
||||
local depmanifest = "lib/"..dep.name.."/.lovedeps"
|
||||
|
||||
if file_exists(depmanifest) then
|
||||
scandeps(depmanifest, 'skipdups', deps)
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
run()
|
|
@ -0,0 +1,60 @@
|
|||
-- Simple Device driver for Yui, only depends on LÖVE 2D, listens to
|
||||
-- keyboard and mouse pointer.
|
||||
|
||||
local Device = {}
|
||||
Device.__index = Device
|
||||
|
||||
|
||||
function Device.new()
|
||||
return setmetatable({
|
||||
px = nil, py = nil,
|
||||
clicking = nil,
|
||||
|
||||
confirm = nil,
|
||||
cancel = nil,
|
||||
up = nil,
|
||||
left = nil,
|
||||
down = nil,
|
||||
right = nil,
|
||||
}, Device)
|
||||
end
|
||||
|
||||
function Device:snapshot()
|
||||
local snap = {}
|
||||
|
||||
-- Mouse pointer
|
||||
local px,py = love.mouse.getPosition()
|
||||
local clicking = love.mouse.isDown(1)
|
||||
|
||||
snap.px,snap.py = px,py
|
||||
snap.pointing = clicking
|
||||
snap.clicked = self.clicking and not clicking
|
||||
snap.pointer = px ~= self.px or py ~= self.py or snap.clicked or snap.pointing
|
||||
|
||||
-- Keyboard input
|
||||
local confirm = love.keyboard.isDown('return', 'space')
|
||||
local cancel = love.keyboard.isDown('escape')
|
||||
local up = love.keyboard.isDown('up', 'w')
|
||||
local left = love.keyboard.isDown('left', 'a')
|
||||
local down = love.keyboard.isDown('down', 's')
|
||||
local right = love.keyboard.isDown('right', 'd')
|
||||
|
||||
snap.confirm = self.confirm and not confirm
|
||||
snap.cancel = self.cancel and not cancel
|
||||
snap.up = self.up and not up
|
||||
snap.left = self.left and not left
|
||||
snap.down = self.down and not down
|
||||
snap.right = self.right and not right
|
||||
snap.action = snap.confirm or snap.cancel or
|
||||
snap.up or snap.left or snap.down or snap.right
|
||||
|
||||
-- Update old state
|
||||
self.px,self.py = px,py
|
||||
self.clicking = clicking
|
||||
self.confirm,self.cancel = confirm,cancel
|
||||
self.left,self.up,self.right,self.down = left,up,right,down
|
||||
|
||||
return snap
|
||||
end
|
||||
|
||||
return Device
|
|
@ -0,0 +1,18 @@
|
|||
local BASE = (...)..'.'
|
||||
|
||||
return {
|
||||
Button = require(BASE..'button'),
|
||||
Checkbox = require(BASE..'checkbox'),
|
||||
Choice = require(BASE..'choice'),
|
||||
Columns = require(BASE..'columns'),
|
||||
Input = require(BASE..'input'),
|
||||
Label = require(BASE..'label'),
|
||||
Layout = require(BASE..'layout'),
|
||||
Rows = require(BASE..'rows'),
|
||||
Slider = require(BASE..'slider'),
|
||||
Spacer = require(BASE..'spacer'),
|
||||
Ui = require(BASE..'ui'),
|
||||
Widget = require(BASE..'widget'),
|
||||
|
||||
theme = require(BASE..'theme')
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
local BASE = (...):gsub('input', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local utf8 = require 'utf8'
|
||||
|
||||
-- Grabs keyboard on focus
|
||||
local Input = setmetatable({ grabkeyboard = true }, Widget)
|
||||
Input.__index = Input
|
||||
|
||||
|
||||
local function split(str, pos)
|
||||
local ofs = utf8.offset(str, pos) or 0
|
||||
return str:sub(1, ofs-1), str:sub(ofs)
|
||||
end
|
||||
|
||||
function Input.new(args)
|
||||
local self = setmetatable(args, Input)
|
||||
|
||||
self.text = self.text or ""
|
||||
self.color = self.color or core.theme.color
|
||||
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
|
||||
self.cursor = math.max(1, math.min(utf8.len(self.text)+1,
|
||||
self.cursor or utf8.len(self.text)+1))
|
||||
self.candidate = { text = "", start = 0, length = 0 }
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Input:onPointerInput(px,py, clicked)
|
||||
if clicked then
|
||||
self:grabFocus()
|
||||
|
||||
-- Schedule cursor reposition for next draw()
|
||||
self.px = px
|
||||
self.py = py
|
||||
end
|
||||
end
|
||||
|
||||
function Input:textedited(text, start, length)
|
||||
self.candidate.text = text
|
||||
self.candidate.start = start
|
||||
self.candidate.length = length
|
||||
end
|
||||
|
||||
function Input:textinput(text)
|
||||
if text ~= "" then
|
||||
local a,b = split(self.text, self.cursor)
|
||||
|
||||
self.text = table.concat {a, text, b}
|
||||
self.cursor = self.cursor + utf8.len(text)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:keypressed(key, code, isrepeat)
|
||||
if isrepeat == nil then
|
||||
-- LOVE sends 3 types of keypressed() events,
|
||||
-- 1. with isrepeat = true
|
||||
-- 2. with isrepeat = false
|
||||
-- 3. with isrepeat = nil
|
||||
--
|
||||
-- We're only interested in the first 2.
|
||||
return
|
||||
end
|
||||
|
||||
if self.candidate.length == 0 then
|
||||
if key == 'backspace' then
|
||||
local a,b = split(self.text, self.cursor)
|
||||
|
||||
self.text = table.concat { split(a, utf8.len(a)), b }
|
||||
self.cursor = math.max(1, self.cursor-1)
|
||||
|
||||
elseif key == 'delete' then
|
||||
local a,b = split(self.text, self.cursor)
|
||||
local _,b = split(b, 2)
|
||||
|
||||
self.text = table.concat { a, b }
|
||||
|
||||
elseif key == 'left' then
|
||||
self.cursor = math.max(0, self.cursor-1)
|
||||
|
||||
elseif key == 'right' then
|
||||
self.cursor = math.min(utf8.len(self.text)+1, self.cursor+1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Input:keyreleased(key)
|
||||
if self.candidate.length == 0 then
|
||||
if key == 'home' then
|
||||
self.cursor = 1
|
||||
elseif key == 'end' then
|
||||
self.cursor = utf8.len(self.text)+1
|
||||
elseif key == 'return' then
|
||||
self:onChange(self.text)
|
||||
self.cursor = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Input:draw()
|
||||
-- Cursor position is before the char (including EOS) i.e. in "hello":
|
||||
-- position 1: |hello
|
||||
-- position 2: h|ello
|
||||
-- ...
|
||||
-- position 6: hello|
|
||||
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local font = self.font or love.graphics.getFont()
|
||||
local th = font:getHeight()
|
||||
local tw = font:getWidth(self.text)
|
||||
|
||||
-- Get size of text and cursor position
|
||||
local cursor_pos = 0
|
||||
if self.cursor > 1 then
|
||||
local s = self.text:sub(1, utf8.offset(self.text, self.cursor)-1)
|
||||
cursor_pos = font:getWidth(s)
|
||||
end
|
||||
|
||||
-- Compute drawing offset
|
||||
if self.px ~= nil then
|
||||
-- Mouse movement
|
||||
local mx = self.px - self.x
|
||||
|
||||
self.cursor = utf8.len(self.text) + 1
|
||||
for c = 1,self.cursor do
|
||||
local s = self.text:sub(0, utf8.offset(self.text, c)-1)
|
||||
if font:getWidth(s) >= mx then
|
||||
self.cursor = c-1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
self.px,self.py = nil,nil
|
||||
end
|
||||
|
||||
core.drawBox(x,y,w,h, self.color.normal, self.cornerRadius)
|
||||
|
||||
-- Apply text margins
|
||||
w = math.max(w - 6, 0)
|
||||
x = math.min(x + 3, x + w)
|
||||
|
||||
-- Set scissors
|
||||
local sx, sy, sw, sh = love.graphics.getScissor()
|
||||
love.graphics.setScissor(x-1,y,w+2,h)
|
||||
|
||||
-- Text
|
||||
love.graphics.setColor(self.color.normal.fg)
|
||||
love.graphics.setFont(font)
|
||||
love.graphics.print(self.text, x, y + (h-th)/2)
|
||||
|
||||
if self.candidate.length > 0 then
|
||||
-- Candidate text
|
||||
local ctw = font:getWidth(self.candidate.text)
|
||||
|
||||
love.graphics.setColor(self.color.normal.fg)
|
||||
love.graphics.print(self.candidate.text, x + tw, y + (h-th)/2)
|
||||
|
||||
-- Candidate text rectangle box
|
||||
love.graphics.rectangle('line', x + tw, y + (h-th)/2, ctw, th)
|
||||
|
||||
self.candidate.text = ""
|
||||
self.candidate.start = 0
|
||||
self.candidate.length = 0
|
||||
end
|
||||
|
||||
-- Cursor
|
||||
if self:isFocused() and (love.timer.getTime() % 1) > .5 then
|
||||
local ct = self.candidate
|
||||
local ss = ct.text:sub(1, utf8.offset(ct.text, ct.start))
|
||||
local ws = ct.start > 0 and font:getWidth(ss) or 0
|
||||
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.setLineStyle('rough')
|
||||
love.graphics.line(x + cursor_pos + ws, y + (h-th)/2,
|
||||
x + cursor_pos + ws, y + (h+th)/2)
|
||||
end
|
||||
|
||||
-- Reset scissor
|
||||
love.graphics.setScissor(sx,sy,sw,sh)
|
||||
end
|
||||
|
||||
return Input
|
|
@ -0,0 +1,37 @@
|
|||
local BASE = (...):gsub('label', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local shadowtext = require 'lib.gear.shadowtext'
|
||||
local T = require('lib.moonspeak').translate
|
||||
|
||||
-- Labels don't accept focus
|
||||
local Label = setmetatable({ nofocus = true }, Widget)
|
||||
Label.__index = Label
|
||||
|
||||
|
||||
function Label.new(args)
|
||||
local self = setmetatable(args, Label)
|
||||
|
||||
self.text = self.text or ""
|
||||
self.text = self.notranslate and self.text or T(self.text)
|
||||
self.align = self.align or 'center'
|
||||
self.valign = self.valign or 'center'
|
||||
self.color = self.color or core.theme.color
|
||||
return self
|
||||
end
|
||||
|
||||
function Label:draw()
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local font = self.font or love.graphics.getFont()
|
||||
local c = self.color.normal
|
||||
|
||||
y = y + core.verticalOffsetForAlign(self.valign, font, h)
|
||||
|
||||
love.graphics.setColor(c.fg)
|
||||
love.graphics.setFont(font)
|
||||
shadowtext.printf(self.text, x+2, y, w-4, self.align)
|
||||
end
|
||||
|
||||
return Label
|
|
@ -0,0 +1,251 @@
|
|||
local BASE = (...):gsub('layout', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local utils = require 'lib.gear'
|
||||
|
||||
local isinstance = gear.meta.isinstance
|
||||
local rectunion = gear.rect.union
|
||||
local pointinrect = gear.rect.pointinside
|
||||
|
||||
local Layout = setmetatable({}, Widget)
|
||||
Layout.__index = Layout
|
||||
|
||||
|
||||
-- Calculate initial widget size.
|
||||
local function calcsize(sizes, widget)
|
||||
local w, h = widget.w, widget.h
|
||||
if w == nil then
|
||||
assert(#sizes > 0, "Default width is undefined!")
|
||||
w = sizes[#sizes].w
|
||||
end
|
||||
if h == nil then
|
||||
assert(#sizes > 0, "Default height is undefined!")
|
||||
h = sizes[#sizes].h
|
||||
end
|
||||
|
||||
if w == 'max' then
|
||||
w = 0
|
||||
for _,v in ipairs(sizes) do
|
||||
if v.w > w then
|
||||
w = v.w
|
||||
end
|
||||
end
|
||||
elseif w == 'median' then
|
||||
w = 0
|
||||
for _,v in ipairs(sizes) do
|
||||
w = w + v.w
|
||||
end
|
||||
w = math.ceil(w / #sizes)
|
||||
elseif w == 'min' then
|
||||
w = math.huge
|
||||
for _,v in ipairs(sizes) do
|
||||
if v.w < w then
|
||||
w = v.w
|
||||
end
|
||||
end
|
||||
else
|
||||
assert(type(w) == 'number')
|
||||
end
|
||||
|
||||
if h == 'max' then
|
||||
h = 0
|
||||
for _,v in ipairs(sizes) do
|
||||
if v.h > h then
|
||||
h = v.h
|
||||
end
|
||||
end
|
||||
elseif h == 'median' then
|
||||
h = 0
|
||||
for _,v in ipairs(sizes) do
|
||||
h = h + v.h
|
||||
end
|
||||
h = math.ceil(h / #sizes)
|
||||
elseif h == 'min' then
|
||||
h = math.huge
|
||||
for _,v in ipairs(sizes) do
|
||||
if v.h < h then
|
||||
h = v.h
|
||||
end
|
||||
end
|
||||
else
|
||||
assert(type(h) == 'number')
|
||||
end
|
||||
|
||||
sizes[#sizes+1] = { w = w, h = h }
|
||||
|
||||
widget.w, widget.h = w, h
|
||||
return w, h
|
||||
end
|
||||
|
||||
-- Lay down container widgets according to Layout type.
|
||||
function Layout:layoutWidgets()
|
||||
local nx,ny = self.x,self.y
|
||||
local sizes = {}
|
||||
local stack = self.stack
|
||||
local pad = self.padding
|
||||
|
||||
-- Container bounds, empty
|
||||
local rx,ry,rw,rh = nx,ny,-1,-1
|
||||
|
||||
-- Layout container children
|
||||
for _,widget in ipairs(self) do
|
||||
widget.x, widget.y = nx, ny
|
||||
widget.ui = self.ui
|
||||
widget.parent = self
|
||||
|
||||
if isinstance(widget, Layout) then
|
||||
widget:layoutWidgets()
|
||||
end
|
||||
|
||||
local w,h = calcsize(sizes, widget)
|
||||
rx,ry,rw,rh = rectunion(rx,ry,rw,rh, nx,ny,w,h)
|
||||
|
||||
nx,ny = self.advance(nx,ny, w,h, pad)
|
||||
|
||||
stack[#stack+1] = widget
|
||||
end
|
||||
|
||||
self.x = rx
|
||||
self.y = ry
|
||||
self.w = math.max(rw, 0)
|
||||
self.h = math.max(rh, 0)
|
||||
end
|
||||
|
||||
function Layout:onPointerInput(px,py, clicked, down)
|
||||
local stack = self.stack
|
||||
|
||||
-- Propagate pointer event from topmost widget to bottom
|
||||
for i = #stack,1,-1 do
|
||||
local widget = stack[i]
|
||||
local x,y,w,h = widget.x,widget.y,widget.w,widget.h
|
||||
|
||||
if pointinrect(px,py, x,y,w,h) then
|
||||
widget:handlePointer(px,py, clicked, down)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Find layout's child containing the provided widget.
|
||||
local function childof(layout, widget)
|
||||
local parent = widget.parent
|
||||
while parent ~= layout do
|
||||
widget = parent
|
||||
parent = widget.parent
|
||||
end
|
||||
return widget
|
||||
end
|
||||
local function findfirst(widget)
|
||||
while isinstance(widget, Layout) do
|
||||
-- Find first element accepting focus
|
||||
for i = 1,#widget do
|
||||
if not widget[i].nofocus then
|
||||
widget = widget[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return widget
|
||||
end
|
||||
local function findnext(layout, widget)
|
||||
local child = childof(layout, widget)
|
||||
|
||||
for i,w in ipairs(layout) do
|
||||
if w == child then
|
||||
-- Search to the right, wraparound to the left
|
||||
for j = i+1,#layout do
|
||||
if not layout[j].nofocus then
|
||||
return findfirst(layout[j])
|
||||
end
|
||||
end
|
||||
for j = 1,i-1 do
|
||||
if not layout[j].nofocus then
|
||||
return findfirst(layout[j])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return widget
|
||||
end
|
||||
local function findprev(layout, widget)
|
||||
local child = childof(layout, widget)
|
||||
|
||||
for i,w in ipairs(layout) do
|
||||
if w == child then
|
||||
-- Search to the left, wraparound to the right
|
||||
for j = i-1,1,-1 do
|
||||
if not layout[j].nofocus then
|
||||
return findfirst(layout[j])
|
||||
end
|
||||
end
|
||||
for j = #layout,i+1,-1 do
|
||||
if not layout[j].nofocus then
|
||||
return findfirst(layout[j])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return widget
|
||||
end
|
||||
|
||||
function Layout:firstFocusableWidget()
|
||||
return findfirst(self)
|
||||
end
|
||||
function Layout:nextFocusableWidget()
|
||||
return findnext(self, self.ui.focused)
|
||||
end
|
||||
function Layout:previousFocusableWidget()
|
||||
return findprev(self, self.ui.focused)
|
||||
end
|
||||
|
||||
function Layout:onActionInput(action)
|
||||
local handled = false
|
||||
|
||||
if action[self.next] then
|
||||
local n = self:nextFocusableWidget()
|
||||
|
||||
n:grabFocus()
|
||||
handled = true
|
||||
end
|
||||
if action[self.prev] then
|
||||
local p = self:previousFocusableWidget()
|
||||
|
||||
p:grabFocus()
|
||||
handled = true
|
||||
end
|
||||
return handled
|
||||
end
|
||||
|
||||
function Layout:update(dt)
|
||||
for _,widget in ipairs(self.stack) do
|
||||
widget:update(dt)
|
||||
end
|
||||
end
|
||||
|
||||
function Layout:draw()
|
||||
-- Draw all children according to their order (topmost last)
|
||||
for _,widget in ipairs(self.stack) do
|
||||
widget:draw()
|
||||
end
|
||||
end
|
||||
|
||||
function Layout.new(args)
|
||||
local self = setmetatable(args, Layout)
|
||||
|
||||
self.padding = self.padding or 0
|
||||
self.stack = {}
|
||||
|
||||
-- A Layout ignores focus if empty or containing only nofocus widgets
|
||||
self.nofocus = true
|
||||
for _,w in ipairs(self) do
|
||||
if not w.nofocus then
|
||||
self.nofocus = false
|
||||
break
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return Layout
|
|
@ -0,0 +1,24 @@
|
|||
local BASE = (...):gsub('rows')
|
||||
|
||||
local Layout = require BASE..'layout'
|
||||
|
||||
local Rows = setmetatable({}, Layout)
|
||||
Rows.__index = Rows
|
||||
|
||||
|
||||
-- Advance position to next row,
|
||||
-- given current position, widget dimensions and padding.
|
||||
local function rowadvance(x,y, ww,wh, padding)
|
||||
return x, y + wh + padding
|
||||
end
|
||||
|
||||
function Rows.new(args)
|
||||
local self = setmetatable(Layout.new(args), Rows)
|
||||
|
||||
self.advance = rowadvance
|
||||
self.prev = 'up'
|
||||
self.next = 'down'
|
||||
return self
|
||||
end
|
||||
|
||||
return Rows
|
|
@ -0,0 +1,92 @@
|
|||
local BASE = (...):gsub('slider', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local core = require BASE..'core'
|
||||
|
||||
local Slider = setmetatable({}, Widget)
|
||||
Slider.__index = Slider
|
||||
|
||||
|
||||
function Slider.new(args)
|
||||
local self = setmetatable(args, Slider)
|
||||
|
||||
self.color = self.color or core.theme.color
|
||||
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
|
||||
self.vertical = self.vertical or false
|
||||
self.min = self.min or 0
|
||||
self.max = self.max or 1
|
||||
self.value = self.value or self.min
|
||||
self.step = self.step or (self.max - self.min) / 10
|
||||
return self
|
||||
end
|
||||
|
||||
function Slider:onPointerInput(px,py, clicked, down)
|
||||
self:grabFocus()
|
||||
if not down then
|
||||
return
|
||||
end
|
||||
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
|
||||
local fraction
|
||||
if self.vertical then
|
||||
fraction = math.min(1, math.max(0, (y+h - py) / h))
|
||||
else
|
||||
fraction = math.min(1, math.max(0, (px - x) / w))
|
||||
end
|
||||
|
||||
local v = fraction*(self.max - self.min) + self.min
|
||||
if v ~= self.value then
|
||||
self.value = v
|
||||
self:onChange(v)
|
||||
end
|
||||
end
|
||||
|
||||
function Slider:onActionInput(action)
|
||||
local up = self.vertical and 'up' or 'right'
|
||||
local down = self.vertical and 'down' or 'left'
|
||||
|
||||
local handled = false
|
||||
if action[up] then
|
||||
self.value = math.min(self.max, self.value + self.step)
|
||||
handled = true
|
||||
elseif action[down] then
|
||||
self.value = math.max(self.min, self.value - self.step)
|
||||
handled = true
|
||||
end
|
||||
if handled then
|
||||
self:onChange(self.value)
|
||||
end
|
||||
|
||||
return handled
|
||||
end
|
||||
|
||||
function Slider:draw()
|
||||
local x,y,w,h = self.x,self.y,self.w,self.h
|
||||
local r = math.min(w,h) / 2.1
|
||||
local c = self:colorForState()
|
||||
local fraction = (self.value - self.min) / (self.max - self.min)
|
||||
|
||||
local xb, yb, wb, hb -- size of the progress bar
|
||||
if self.vertical then
|
||||
x, w = x + w*.25, w*.5
|
||||
xb, yb, wb, hb = x, y+h*(1-fraction), w, h*fraction
|
||||
else
|
||||
y, h = y + h*.25, h*.5
|
||||
xb, yb, wb, hb = x,y, w*fraction, h
|
||||
end
|
||||
|
||||
core.drawBox(x,y,w,h, c, self.cornerRadius)
|
||||
core.drawBox(xb,yb,wb,hb, {bg=c.fg}, self.cornerRadius)
|
||||
|
||||
if self:isFocused() then
|
||||
love.graphics.setColor(c.fg)
|
||||
if self.vertical then
|
||||
love.graphics.circle('fill', x+wb/2, yb, r)
|
||||
else
|
||||
love.graphics.circle('fill', x+wb, yb+hb/2, r)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Slider
|
|
@ -0,0 +1,14 @@
|
|||
local BASE = (...):gsub('spacer', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
|
||||
-- Spacers don't accept focus
|
||||
local Spacer = setmetatable({ nofocus = true }, Widget)
|
||||
Spacer.__index = Spacer
|
||||
|
||||
|
||||
function Spacer.new(args)
|
||||
return setmetatable(args, Spacer)
|
||||
end
|
||||
|
||||
return Spacer
|
|
@ -0,0 +1,11 @@
|
|||
local theme = {
|
||||
cornerRadius = 4,
|
||||
|
||||
color = {
|
||||
normal = {bg = { 0.25, 0.25, 0.25}, fg = {0.73, 0.73, 0.73}},
|
||||
hovered = {bg = { 0.19, 0.6, 0.73}, fg = {1, 1, 1}},
|
||||
active = {bg = { 1, 0.6, 0}, fg = {1, 1, 1}}
|
||||
}
|
||||
}
|
||||
|
||||
return theme
|
|
@ -0,0 +1,176 @@
|
|||
local BASE = (...):gsub('ui', '')
|
||||
|
||||
local Widget = require BASE..'widget'
|
||||
local Layout = require BASE..'layout'
|
||||
|
||||
local gear = require 'lib.gear'
|
||||
|
||||
local Timer = gear.Timer
|
||||
local isinstance = gear.meta.isinstance
|
||||
local pointinrect = gear.rect.pointinside
|
||||
|
||||
local Ui = {}
|
||||
Ui.__index = Ui
|
||||
|
||||
|
||||
-- Scan UI for the LAST widgets with 'cancelfocus' or 'firstfocus' flags
|
||||
local function resolveautofocus(widget)
|
||||
local firstfocus, cancelfocus
|
||||
|
||||
if isinstance(widget, Layout) then
|
||||
for _,w in ipairs(widget) do
|
||||
local firstf, cancelf
|
||||
|
||||
if not w.nofocus then
|
||||
if isinstance(w, Layout) then
|
||||
firstf, cancelf = resolveautofocus(w)
|
||||
else
|
||||
if w.firstfocus then
|
||||
firstf = w
|
||||
end
|
||||
if w.cancelfocus then
|
||||
cancelf = w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
firstfocus = firstf or firstfocus
|
||||
cancelfocus = cancelf or cancelfocus
|
||||
end
|
||||
|
||||
elseif not widget.nofocus then
|
||||
if widget.firstfocus then
|
||||
firstfocus = widget
|
||||
end
|
||||
if widget.cancelfocus then
|
||||
cancelfocus = widget
|
||||
end
|
||||
end
|
||||
|
||||
return firstfocus, cancelfocus
|
||||
end
|
||||
|
||||
local function propagateaction(ui, action)
|
||||
local focused = ui.focused
|
||||
if focused.grabkeyboard then
|
||||
-- A widget stealing input
|
||||
-- explicitly consumes any action.
|
||||
return true
|
||||
end
|
||||
while focused ~= nil do
|
||||
if focused:onActionInput(action) then
|
||||
return true -- action consumed
|
||||
end
|
||||
|
||||
focused = focused.parent
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Ui.new(args)
|
||||
local self = setmetatable(args, Ui)
|
||||
assert(#self == 1, "Ui.new() must have exactly one root widget.")
|
||||
|
||||
self.device = self.device or require('device.love').new()
|
||||
self.x = self.x or 0
|
||||
self.y = self.y or 0
|
||||
self.pointerActive = true
|
||||
self.timer = Timer.new()
|
||||
|
||||
local root = self[1]
|
||||
if not isinstance(root, Widget) then
|
||||
error("Ui.new() bad root Widget type: "..type(root)..".")
|
||||
end
|
||||
|
||||
root.x,root.y = self.x,self.y
|
||||
root.ui = self
|
||||
if isinstance(root, Layout) then
|
||||
root:layoutWidgets()
|
||||
else
|
||||
assert(type(root.w) == 'number', "Ui.new() root Widget must have a numeric width.")
|
||||
assert(type(root.h) == 'number', "Ui.new() root Widget must have a numeric height.")
|
||||
assert(not root.nofocus, "Ui.new() single root Widget can't be nofocus.")
|
||||
end
|
||||
|
||||
self.w,self.h = root.w,root.h
|
||||
|
||||
local firstfocus, cancelfocus = resolveautofocus(root)
|
||||
if firstfocus == nil then
|
||||
firstfocus = isinstance(root, Layout) and
|
||||
root:firstFocusableWidget() or
|
||||
root
|
||||
end
|
||||
|
||||
self.cancelfocus = cancelfocus
|
||||
firstfocus:grabFocus()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Event propagators for widgets listening to keyboard input
|
||||
function Ui:keypressed(key, scancode, isrepeat)
|
||||
local focused = self.focused
|
||||
|
||||
if focused ~= nil and focused.grabkeyboard then
|
||||
focused:keypressed(key, scancode, isrepeat)
|
||||
end
|
||||
end
|
||||
function Ui:keyreleased(key, scancode)
|
||||
local focused = self.focused
|
||||
|
||||
if focused ~= nil and focused.grabkeyboard then
|
||||
focused:keyreleased(key, scancode)
|
||||
end
|
||||
end
|
||||
function Ui:textinput(text)
|
||||
local focused = self.focused
|
||||
|
||||
if focused ~= nil and focused.grabkeyboard then
|
||||
focused:textinput(text)
|
||||
end
|
||||
end
|
||||
function Ui:textedited(text, start, length)
|
||||
local focused = self.focused
|
||||
|
||||
if focused ~= nil and focused.grabkeyboard then
|
||||
focused:textedited(text, start, length)
|
||||
end
|
||||
end
|
||||
|
||||
function Ui:update(dt)
|
||||
local root = self[1]
|
||||
local x,y,w,h = root.x,root.y,root.w,root.h
|
||||
local snap = self.device:snapshot()
|
||||
|
||||
self.timer:update(dt)
|
||||
|
||||
-- Propagate pointer events in focus order
|
||||
if self.pointerActive then
|
||||
if snap.pointer and pointinrect(snap.px,snap.py, x,y,w,h) then
|
||||
root:onPointerInput(snap.px,snap.py, snap.clicked, snap.pointing)
|
||||
end
|
||||
end
|
||||
|
||||
-- Propagate actions from focused widget up
|
||||
if snap.action and not propagateaction(self, snap) then
|
||||
-- Take global actions if nobody consumed the event
|
||||
if snap.cancel and self.cancelfocus then
|
||||
-- Focus on the last widget with 'cancelfocus'
|
||||
self.cancelfocus:grabFocus()
|
||||
end
|
||||
end
|
||||
|
||||
-- Perform regular lifetime updates
|
||||
root:update(dt)
|
||||
end
|
||||
|
||||
function Ui:draw()
|
||||
local root = self[1]
|
||||
|
||||
love.graphics.push('all')
|
||||
root:draw()
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
return Ui
|
|
@ -0,0 +1,117 @@
|
|||
local rectunion = require('lib.gear.rect').union
|
||||
|
||||
local Widget = {
|
||||
__call = function(cls, args)
|
||||
return cls.new(args)
|
||||
end
|
||||
}
|
||||
Widget.__index = Widget
|
||||
|
||||
|
||||
local function raise(widget)
|
||||
local parent = widget.parent
|
||||
|
||||
-- A parent of a widget is necessarily a Layout
|
||||
while parent ~= nil do
|
||||
local stack = parent.stack
|
||||
|
||||
-- Move widget at the end of the stack, so it is rendered last.
|
||||
for i,w in ipairs(stack) do
|
||||
if w == widget then
|
||||
table.remove(stack, i)
|
||||
stack[#stack+1] = widget
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Focus widget's container, if any
|
||||
widget = parent
|
||||
parent = widget.parent
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:grabFocus()
|
||||
local ui = self.ui
|
||||
local focused = ui.focused
|
||||
|
||||
if focused == self then
|
||||
return
|
||||
end
|
||||
if focused ~= nil then
|
||||
-- Notify leave
|
||||
focused.hovered = false
|
||||
if focused.grabkeyboard then
|
||||
if love.system.getOS() == 'Android' or love.system.getOS() == 'iOS' then
|
||||
love.keyboard.setTextInput(false)
|
||||
end
|
||||
love.keyboard.setKeyRepeat(false)
|
||||
end
|
||||
|
||||
focused:onLeave()
|
||||
end
|
||||
|
||||
local wasHovered = self.hovered
|
||||
|
||||
self.hovered = true
|
||||
if self.grabkeyboard then
|
||||
love.keyboard.setTextInput(true, self.x,self.y,self.w,self.h)
|
||||
love.keyboard.setKeyRepeat(true)
|
||||
end
|
||||
if not wasHovered then
|
||||
-- First time hovered, notify enter
|
||||
self:onEnter()
|
||||
end
|
||||
|
||||
-- Raise widget
|
||||
ui.focused = self
|
||||
raise(self)
|
||||
end
|
||||
|
||||
function Widget:isFocused()
|
||||
return self.ui.focused == self
|
||||
end
|
||||
|
||||
function Widget.recalculateBounds()
|
||||
local widget = self.parent
|
||||
while widget ~= nil do
|
||||
local rx,ry,rw,rh = widget.x,widget.y,-1,-1
|
||||
|
||||
for _,w in ipairs(widget) do
|
||||
rx,ry,rw,rh = rectunion(rx,ry,rw,rh, w.x,w.y,w.w,w.h)
|
||||
end
|
||||
|
||||
widget.x = rx
|
||||
widget.y = ry
|
||||
widget.w = rw
|
||||
widget.h = rh
|
||||
|
||||
widget = widget.parent
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper for drawing
|
||||
function Widget:colorForState()
|
||||
if self.active then
|
||||
return self.color.active
|
||||
elseif self:isFocused() then
|
||||
return self.color.hovered
|
||||
else
|
||||
return self.color.normal
|
||||
end
|
||||
end
|
||||
|
||||
-- Common NOP event handlers
|
||||
function Widget:onHit() end
|
||||
function Widget:onEnter() end
|
||||
function Widget:onLeave() end
|
||||
function Widget:onChange() end
|
||||
|
||||
-- NOP input event handlers
|
||||
function Widget:onActionInput(action) end
|
||||
function Widget:onPointerInput(x,y, clicked) end
|
||||
|
||||
-- NOP UI event handlers
|
||||
function Widget:update(dt) end
|
||||
function Widget:draw() end
|
||||
|
||||
return Widget
|
Loading…
Reference in New Issue