yui/ui.lua

269 lines
6.8 KiB
Lua

local BASE = (...):gsub('ui$', '')
local Widget = require(BASE..'widget')
local Layout = require(BASE..'layout')
local Columns = require(BASE..'columns')
local Rows = require(BASE..'rows')
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
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(BASE..'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:first() 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
local function actionpropagate(widget, action)
while widget ~= nil do
if widget:onActionInput(action) then
return true -- action consumed
end
widget = widget.parent
end
return false
end
local navs = { 'cancel', 'up', 'down', 'left', 'right' }
local function globalactions(ui, snap)
for _,nav in ipairs(navs) do
if snap[nav] then
ui:navigate(nav)
break -- discard other directions, if any
end
end
end
local function eventpropagate(ui, snap)
-- 1. Pointer events
if snap.pointer and not ui.grabpointer then
local root = ui[1]
local x,y,w,h = root.x,root.y,root.w,root.h
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
-- 2. Actions (keyboard/buttons)
if snap.action and not ui.grabkeyboard then
local consumed = actionpropagate(ui.focused, snap)
if not consumed then
-- 3. If no widget consumed action,
-- take global actions (e.g. navigation).
globalactions(ui, snap)
end
end
end
-- cls may be Rows, Columns or Layout in general
local function containerof(widget, cls)
repeat
widget = widget.parent
if isinstance(widget, cls) then
return widget
end
until widget == nil
end
local function findprev(ui, cls, widget)
local child = widget
while true do
local container = containerof(child, cls)
if container == nil then
-- All the way up, either return the original
-- widget or wraparound to the bottom
return isinstance(child, cls) and child:last() or widget
end
local w = container:before(child)
if w ~= nil then
return w
end
child = container -- move up into the hierarchy
end
end
-- Specular to findprev()
local function findnext(ui, cls, widget)
local child = widget
while true do
local container = containerof(child, cls)
if container == nil then
return isinstance(child, cls) and child:first() or widget
end
local w = container:after(child)
if w ~= nil then
return w
end
child = container
end
end
--- Move focus to the given direction.
--
-- @param where (string) Direction to move to,
-- one of: 'up', 'down', 'left', 'right', 'cancel'.
function Ui:navigate(where)
local nextfocus = nil
if where == 'cancel' then
nextfocus = self.cancelfocus
elseif where == 'up' then
nextfocus = findprev(self, Rows, self.focused)
elseif where == 'down' then
nextfocus = findnext(self, Rows, self.focused)
elseif where == 'left' then
nextfocus = findprev(self, Columns, self.focused)
elseif where == 'right' then
nextfocus = findnext(self, Columns, self.focused)
else
error("Bad direction: "..tostring(where))
end
if nextfocus ~= nil then
nextfocus:grabFocus()
end
end
function Ui:update(dt)
local root = self[1]
local snap = self.device:snapshot()
-- Update timer related effects
self.timer:update(dt)
-- Regular event propagation
eventpropagate(self, snap)
-- 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