Files
love-luigi/luigi/layout.lua
2015-11-10 17:01:17 -05:00

344 lines
8.3 KiB
Lua

--[[--
A Layout contains a tree of widgets with a single `root` widget.
Layouts will resize to fit the window unless a `top` or `left`
property is found in the root widget.
Layouts are drawn in the order that they were shown, so the
most recently shown layout shown will always appear on top.
Other events are sent to layouts in the opposite direction,
and are trapped by the first layout that can handle the event
(for example, the topmost layer that is focused or hovered).
@classmod Layout
--]]--
local ROOT = (...):gsub('[^.]*$', '')
local Base = require(ROOT .. 'base')
local Event = require(ROOT .. 'event')
local Widget = require(ROOT .. 'widget')
local Input = require(ROOT .. 'input')
local Style = require(ROOT .. 'style')
local Hooker = require(ROOT .. 'hooker')
local Layout = Base:extend()
Layout.isLayout = true
--[[--
Layout constructor.
@function Luigi.Layout
@tparam table data
A tree of widget data.
@treturn Layout
A Layout instance.
--]]--
function Layout:constructor (data)
data = data or {}
self.accelerators = {}
self:addDefaultHandlers()
self:setStyle()
self:setTheme(require(ROOT .. 'theme.light'))
self.isShown = false
self.hooks = {}
self.root = data
Widget(self, data)
end
--[[--
Set the style from a definition table or function.
@tparam table|function rules
Style definition.
--]]--
function Layout:setStyle (rules)
if type(rules) == 'function' then
rules = rules()
end
self.style = Style(rules or {}, { 'id', 'style' })
end
--[[--
Set the theme from a definition table or function.
@tparam table|function rules
Theme definition.
--]]--
function Layout:setTheme (rules)
if type(rules) == 'function' then
rules = rules()
end
self.theme = Style(rules or {}, { 'type' })
end
--[[--
Show the layout.
Hooks all appropriate Love events and callbacks.
--]]--
function Layout:show ()
if self.isShown then
self:unhook() -- return
self.isShown = nil
end
if not self.input then
self.input = Input.default -- Input(self)
end
self:manageInput()
self.root:reshape()
end
--[[--
Hide the layout.
Unhooks Love events and callbacks.
--]]--
function Layout:hide ()
if not self.isShown then
return
end
self.isShown = nil
self:unhook()
end
--[[--
Focus next focusable widget.
Traverses widgets using Widget:getNextNeighbor until a focusable widget is
found, and focuses that widget.
@treturn Widget
The widget that was focused, or nil
--]]--
function Layout:focusNextWidget ()
local widget = self.focusedWidget or self.root
local nextWidget = widget:getNextNeighbor()
while nextWidget ~= widget do
if nextWidget:focus() then return nextWidget end
nextWidget = nextWidget:getNextNeighbor()
end
end
--[[--
Focus previous focusable widget.
Traverses widgets using Widget:getPreviousNeighbor until a focusable widget is
found, and focuses that widget.
@treturn Widget
The widget that was focused, or nil
--]]--
function Layout:focusPreviousWidget ()
local widget = self.focusedWidget or self.root
local previousWidget = widget:getPreviousNeighbor()
while previousWidget ~= widget do
if previousWidget:focus() then return previousWidget end
previousWidget = previousWidget:getPreviousNeighbor()
end
end
--[[--
Get the innermost widget at given coordinates.
@tparam number x
Number of pixels from window's left edge.
@tparam number y
Number of pixels from window's top edge.
@tparam[opt] Widget root
Widget to search within, defaults to layout root.
--]]--
function Layout:getWidgetAt (x, y, root)
local widget = root or self.root
-- Loop through in reverse, because siblings defined later in the tree
-- will overdraw earlier siblings.
local childCount = #widget
for i = childCount, 1, -1 do
local child = widget[i]
local inner = self:getWidgetAt(x, y, child)
if inner then return inner end
end
if widget:isAt(x, y) then return widget end
end
-- Internal, called from Widget:new
function Layout:addWidget (widget)
if widget.id then
self[widget.id] = widget
end
if widget.key then
self.accelerators[widget.key] = widget
end
end
-- Add handlers for keyboard accelerators and tab focus
function Layout:addDefaultHandlers ()
self:onKeyPress(function (event)
-- tab / shift-tab cycles focused widget
if event.key == 'tab' then
if love.keyboard.isDown('lshift', 'rshift') then
self:focusPreviousWidget()
else
self:focusNextWidget()
end
return
end
-- space / enter presses focused widget
local widget = self.focusedWidget
if widget and event.key == 'space' or event.key == ' '
or event.key == 'return' then
self.input:handlePressStart(self, event.key, event.x, event.y,
widget, event.key)
return
end
-- accelerators
local acceleratedWidget = self.accelerators[event.key]
if acceleratedWidget then
acceleratedWidget.hovered = true
self.input:handlePressStart(self, event.key, event.x, event.y,
acceleratedWidget, event.key)
end
end)
self:onKeyRelease(function (event)
-- space / enter presses focused widget
local widget = self.focusedWidget
if widget and event.key == 'space' or event.key == ' '
or event.key == 'return' then
self.input:handlePressEnd(self, event.key, event.x, event.y,
widget, event.key)
return
end
-- accelerators
local acceleratedWidget = self.accelerators[event.key]
if acceleratedWidget then
acceleratedWidget.hovered = false
self.input:handlePressEnd(self, event.key, event.x, event.y,
acceleratedWidget, event.key)
end
end)
end
-- event stuff
function Layout:hook (key, method, hookLast)
self.hooks[#self.hooks + 1] = Hooker.hook(love, key, method, hookLast)
end
function Layout:unhook ()
for _, item in ipairs(self.hooks) do
Hooker.unhook(item)
end
self.hooks = {}
end
local getMouseButtonId, isMouseDown
if love._version_minor < 10 then
getMouseButtonId = function (value)
return value == 'l' and 1
or value == 'r' and 2
or value == 'm' and 3
end
isMouseDown = function ()
return love.mouse.isDown('l', 'r', 'm')
end
else
getMouseButtonId = function (value)
return value
end
isMouseDown = function ()
return love.mouse.isDown(1, 2, 3)
end
end
function Layout:manageInput ()
if self.isShown then
return
end
self.isShown = true
local input = self.input
self:hook('draw', function ()
input:handleDisplay(self)
end, true)
self:hook('resize', function (width, height)
return input:handleReshape(self, width, height)
end)
self:hook('mousepressed', function (x, y, button)
return input:handlePressStart(self, getMouseButtonId(button), x, y)
end)
self:hook('mousereleased', function (x, y, button)
return input:handlePressEnd(self, getMouseButtonId(button), x, y)
end)
self:hook('mousemoved', function (x, y, dx, dy)
if isMouseDown() then
return input:handlePressedMove(self, x, y)
else
return input:handleMove(self, x, y)
end
end)
self:hook('keypressed', function (key, isRepeat)
return input:handleKeyPress(
self, key, love.mouse.getX(), love.mouse.getY())
end)
self:hook('keyreleased', function (key)
return input:handleKeyRelease(
self, key, love.mouse.getX(), love.mouse.getY())
end)
self:hook('textinput', function (text)
return input:handleTextInput(
self, text, love.mouse.getX(), love.mouse.getY())
end)
end
-- event binders
Event.injectBinders(Layout)
-- ffi
function Layout:showMessage () end
local _, ffi = pcall(require, 'ffi')
if ffi then
ffi.cdef [[
int SDL_ShowSimpleMessageBox(
uint32_t flags,
const char* title,
const char* message,
void* window
);
]]
function Layout:showMessage (title, message)
ffi.C.SDL_ShowSimpleMessageBox(0, title, message, nil)
end
end
return Layout