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