mirror of
https://github.com/airstruck/luigi.git
synced 2026-01-10 16:28:23 +00:00
initial commit
This commit is contained in:
12
luigi/base.lua
Normal file
12
luigi/base.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
return {
|
||||
extend = function (self, subtype)
|
||||
return setmetatable(subtype or {}, {
|
||||
__index = self,
|
||||
__call = function (self, ...)
|
||||
local instance = setmetatable({}, { __index = self })
|
||||
return instance, instance:constructor(...)
|
||||
end
|
||||
})
|
||||
end,
|
||||
constructor = function () end,
|
||||
}
|
||||
49
luigi/event.lua
Normal file
49
luigi/event.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
|
||||
local Event = Base:extend({ name = 'Event' })
|
||||
|
||||
function Event:emit (observer, data, defaultAction)
|
||||
local callbacks = self.registry[observer]
|
||||
if not callbacks then
|
||||
if defaultAction then defaultAction() end
|
||||
return
|
||||
end
|
||||
for i, callback in ipairs(callbacks) do
|
||||
local result = callback(data or {})
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
if defaultAction then defaultAction() end
|
||||
end
|
||||
|
||||
function Event:bind (observer, callback)
|
||||
local registry = self.registry
|
||||
if not registry[observer] then
|
||||
registry[observer] = {}
|
||||
end
|
||||
table.insert(registry[observer], callback)
|
||||
end
|
||||
|
||||
local eventNames = {
|
||||
'Display', 'Keyboard', 'Motion', 'Mouse', 'Reshape', 'Enter', 'Leave',
|
||||
'Press', 'PressStart', 'PressDrag', 'PressMove', 'PressLeave', 'PressEnter',
|
||||
'PressEnd'
|
||||
}
|
||||
|
||||
local weakKeyMeta = { __mode = 'k' }
|
||||
|
||||
for i, name in ipairs(eventNames) do
|
||||
Event[name] = Event:extend({
|
||||
name = name,
|
||||
registry = setmetatable({}, weakKeyMeta),
|
||||
})
|
||||
end
|
||||
|
||||
function Event.injectBinders (t)
|
||||
for i, name in ipairs(eventNames) do
|
||||
t['on' .. name] = function (...) return Event[name]:bind(...) end
|
||||
end
|
||||
end
|
||||
|
||||
return Event
|
||||
60
luigi/font.lua
Normal file
60
luigi/font.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
|
||||
local Font = Base:extend()
|
||||
|
||||
local cache = {}
|
||||
|
||||
function Font:constructor (path, size, color)
|
||||
if not size then
|
||||
size = 12
|
||||
end
|
||||
if not color then
|
||||
color = { 0, 0, 0 }
|
||||
end
|
||||
local key = (path or '') .. '_' .. size
|
||||
|
||||
if not cache[key] then
|
||||
if path then
|
||||
cache[key] = love.graphics.newFont(path, size)
|
||||
else
|
||||
cache[key] = love.graphics.newFont(size)
|
||||
end
|
||||
end
|
||||
|
||||
self.layout = {}
|
||||
self.font = cache[key]
|
||||
self.color = color
|
||||
end
|
||||
|
||||
function Font:setAlignment (align)
|
||||
self.layout.align = align
|
||||
end
|
||||
|
||||
function Font:setWidth (width)
|
||||
self.layout.width = width
|
||||
end
|
||||
|
||||
function Font:getLineHeight ()
|
||||
return self.font:getLineHeight()
|
||||
end
|
||||
|
||||
function Font:getAscender ()
|
||||
return self.font:getAscent()
|
||||
end
|
||||
|
||||
function Font:getDescender ()
|
||||
return self.font:getDescent()
|
||||
end
|
||||
|
||||
function Font:getAdvance (text)
|
||||
return (self.font:getWidth(text))
|
||||
end
|
||||
|
||||
function Font:getWrappedHeight (text)
|
||||
local _, lines = self.font:getWrap(text, self.layout.width)
|
||||
return #lines * self.font:getHeight()
|
||||
end
|
||||
|
||||
return Font
|
||||
65
luigi/hooker.lua
Normal file
65
luigi/hooker.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
local Hooker = {}
|
||||
|
||||
local wrapped = {}
|
||||
|
||||
local hooks = {}
|
||||
|
||||
local function hook (key, func)
|
||||
if not func then
|
||||
return
|
||||
end
|
||||
|
||||
local next = hooks[key]
|
||||
local item = { next = next, unhook = unhook, key = key, func = func }
|
||||
|
||||
if next then
|
||||
next.prev = item
|
||||
end
|
||||
|
||||
hooks[key] = item
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
local function unhook (item)
|
||||
if item.prev then
|
||||
item.prev.next = item.next
|
||||
end
|
||||
|
||||
if item.next then
|
||||
item.next.prev = item.prev
|
||||
end
|
||||
|
||||
if hooks[item.key] == item then
|
||||
hooks[item.key] = item.next
|
||||
end
|
||||
|
||||
item.prev = nil
|
||||
item.next = nil
|
||||
item.func = nil
|
||||
end
|
||||
|
||||
function Hooker.hook (key, func)
|
||||
if not wrapped[key] then
|
||||
wrapped[key] = true
|
||||
|
||||
hook(key, love[key])
|
||||
|
||||
love[key] = function (...)
|
||||
local item = hooks[key]
|
||||
|
||||
while item do
|
||||
item.func(...)
|
||||
item = item.next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return hook(key, func)
|
||||
end
|
||||
|
||||
function Hooker.unhook (item)
|
||||
return unhook(item)
|
||||
end
|
||||
|
||||
return Hooker
|
||||
167
luigi/input.lua
Normal file
167
luigi/input.lua
Normal file
@@ -0,0 +1,167 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Renderer = require(ROOT .. 'renderer')
|
||||
|
||||
local Input = Base:extend()
|
||||
|
||||
local weakValueMeta = { __mode = 'v' }
|
||||
|
||||
function Input:constructor (layout)
|
||||
self.layout = layout
|
||||
self.pressedWidgets = setmetatable({}, weakValueMeta)
|
||||
self.passedWidgets = setmetatable({}, weakValueMeta)
|
||||
end
|
||||
|
||||
function Input:bubbleEvent (eventName, widget, data)
|
||||
local event = Event[eventName]
|
||||
for ancestor in widget:getAncestors(true) do
|
||||
local result = event:emit(ancestor, data)
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
return event:emit(self.layout, data)
|
||||
end
|
||||
|
||||
function Input:handleDisplay ()
|
||||
local root = self.layout.root
|
||||
if root then Renderer:render(root) end
|
||||
Event.Display:emit(self.layout)
|
||||
end
|
||||
|
||||
function Input:handleKeyboard (key, x, y)
|
||||
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
|
||||
self:bubbleEvent('Keyboard', widget, {
|
||||
target = widget,
|
||||
key = key, x = x, y = y
|
||||
})
|
||||
end
|
||||
|
||||
function Input:handleMotion (x, y)
|
||||
local widget = self.layout:getWidgetAt(x, y)
|
||||
local previousWidget = self.previousMotionWidget
|
||||
if not widget.hovered then
|
||||
if previousWidget then
|
||||
previousWidget.hovered = nil
|
||||
end
|
||||
widget.hovered = true
|
||||
-- self.layout:update()
|
||||
widget:update()
|
||||
end
|
||||
self:bubbleEvent('Motion', widget, {
|
||||
target = widget,
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
if widget ~= previousWidget then
|
||||
if previousWidget then
|
||||
self:bubbleEvent('Leave', previousWidget, {
|
||||
target = previousWidget,
|
||||
newTarget = widget,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
self:bubbleEvent('Enter', widget, {
|
||||
target = widget,
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
self.previousMotionWidget = widget
|
||||
end
|
||||
end
|
||||
|
||||
function Input:handlePressedMotion (x, y)
|
||||
local widget = self.layout:getWidgetAt(x, y)
|
||||
for button = 0, 2 do
|
||||
local originWidget = self.pressedWidgets[button]
|
||||
local passedWidget = self.passedWidgets[button]
|
||||
if originWidget then
|
||||
self:bubbleEvent('PressDrag', originWidget, {
|
||||
target = originWidget,
|
||||
newTarget = widget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
if (widget == passedWidget) then
|
||||
self:bubbleEvent('PressMove', widget, {
|
||||
target = widget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
else
|
||||
originWidget.pressed = (widget == originWidget) or nil
|
||||
originWidget:update()
|
||||
-- self.layout:update()
|
||||
if passedWidget then
|
||||
self:bubbleEvent('PressLeave', passedWidget, {
|
||||
target = passedWidget,
|
||||
newTarget = widget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
self:bubbleEvent('PressEnter', widget, {
|
||||
target = widget,
|
||||
oldTarget = passedWidget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
self.passedWidgets[button] = widget
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Input:handlePressStart (button, x, y)
|
||||
local widget = self.layout:getWidgetAt(x, y)
|
||||
widget.pressed = true
|
||||
-- self.layout:update()
|
||||
widget:update()
|
||||
self.pressedWidgets[button] = widget
|
||||
self.passedWidgets[button] = widget
|
||||
self:bubbleEvent('PressStart', widget, {
|
||||
target = widget,
|
||||
buton = button, x = x, y = y
|
||||
})
|
||||
end
|
||||
|
||||
function Input:handlePressEnd (button, x, y)
|
||||
local widget = self.layout:getWidgetAt(x, y)
|
||||
local originWidget = self.pressedWidgets[button]
|
||||
originWidget.pressed = nil
|
||||
-- self.layout:update()
|
||||
originWidget:update()
|
||||
self:bubbleEvent('PressEnd', widget, {
|
||||
target = widget,
|
||||
origin = originWidget,
|
||||
buton = button, x = x, y = y
|
||||
})
|
||||
if (widget == originWidget) then
|
||||
self:bubbleEvent('Press', widget, {
|
||||
target = widget,
|
||||
buton = button, x = x, y = y
|
||||
})
|
||||
end
|
||||
self.pressedWidgets[button] = nil
|
||||
self.passedWidgets[button] = nil
|
||||
end
|
||||
|
||||
function Input:handleReshape (width, height)
|
||||
local layout = self.layout
|
||||
local root = layout.root
|
||||
for i, widget in ipairs(layout.widgets) do
|
||||
widget.position = {}
|
||||
widget.dimensions = {}
|
||||
end
|
||||
root.width = width
|
||||
root.height = height
|
||||
Event.Reshape:emit(root, {
|
||||
target = root,
|
||||
width = width, height = height
|
||||
})
|
||||
end
|
||||
|
||||
return Input
|
||||
83
luigi/layout.lua
Normal file
83
luigi/layout.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Window = require(ROOT .. 'window')
|
||||
local Widget = require(ROOT .. 'widget')
|
||||
local Input = require(ROOT .. 'input')
|
||||
local Style = require(ROOT .. 'style')
|
||||
|
||||
local Layout = Base:extend()
|
||||
|
||||
local weakValueMeta = { __mode = 'v' }
|
||||
|
||||
function Layout:constructor (data)
|
||||
self.widgets = setmetatable({}, weakValueMeta)
|
||||
self.root = Widget.create(self, data or {})
|
||||
self:setStyle()
|
||||
self:setTheme()
|
||||
end
|
||||
|
||||
function Layout:setStyle (rules)
|
||||
self.style = Style(rules or {}, 'id', 'style')
|
||||
end
|
||||
|
||||
function Layout:setTheme (rules)
|
||||
self.theme = Style(rules or {}, 'type')
|
||||
end
|
||||
|
||||
function Layout:show ()
|
||||
local root = self.root
|
||||
local width = root.width
|
||||
local height = root.height
|
||||
local title = root.title
|
||||
if not self.input then
|
||||
self.input = Input(self)
|
||||
end
|
||||
if not self.window then
|
||||
self.window = Window(self.input)
|
||||
end
|
||||
self.window:show(width, height, title)
|
||||
end
|
||||
|
||||
function Layout:hide ()
|
||||
self.window:hide()
|
||||
end
|
||||
|
||||
-- Update the display. Call this after you change widget properties
|
||||
-- that affect display.
|
||||
function Layout:update (reshape)
|
||||
self.window:update(reshape)
|
||||
end
|
||||
|
||||
-- Get the innermost widget at a position, within a root widget.
|
||||
-- Should always return a widget since all positions are within
|
||||
-- the layout's root widget.
|
||||
function Layout:getWidgetAt (x, y, root)
|
||||
local widget = root or self.root
|
||||
local children = widget.children
|
||||
local childCount = #children
|
||||
-- Loop through in reverse, because siblings defined later in the tree
|
||||
-- will overdraw earlier siblings.
|
||||
for i = childCount, 1, -1 do
|
||||
local child = children[i]
|
||||
local inner = self:getWidgetAt(x, y, child)
|
||||
if inner then return inner end
|
||||
end
|
||||
if widget:isAt(x, y) then return widget end
|
||||
if widget == self.root then return widget end
|
||||
end
|
||||
|
||||
-- Internal, called from Widget:new
|
||||
function Layout:addWidget (widget)
|
||||
if widget.id then
|
||||
self[widget.id] = widget
|
||||
end
|
||||
table.insert(self.widgets, widget)
|
||||
end
|
||||
|
||||
-- event binders
|
||||
|
||||
Event.injectBinders(Layout)
|
||||
|
||||
return Layout
|
||||
144
luigi/renderer.lua
Normal file
144
luigi/renderer.lua
Normal file
@@ -0,0 +1,144 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Font = require(ROOT .. 'font')
|
||||
|
||||
local Renderer = Base:extend()
|
||||
|
||||
function Renderer:renderBackground (widget, window)
|
||||
local bg = widget.background
|
||||
if not bg then return end
|
||||
local bend = widget.bend
|
||||
local x1, y1, x2, y2 = widget:getRectangle(true)
|
||||
window:fill(x1, y1, x2, y2, bg, bend)
|
||||
end
|
||||
|
||||
function Renderer:renderOutline (widget, window)
|
||||
if not widget.outline then return end
|
||||
local x1, y1, x2, y2 = widget:getRectangle(true)
|
||||
window:outline(x1, y1, x2, y2, widget.outline)
|
||||
end
|
||||
|
||||
local imageCache = {}
|
||||
|
||||
local function loadImage (path)
|
||||
if not imageCache[path] then
|
||||
imageCache[path] = love.graphics.newImage(path)
|
||||
end
|
||||
|
||||
return imageCache[path]
|
||||
end
|
||||
|
||||
-- TODO: this function is a monster, fix it somehow
|
||||
function Renderer:renderIconAndText (widget, window)
|
||||
local x1, y1, x2, y2 = widget:getRectangle(true, true)
|
||||
local icon = widget.icon and loadImage(widget.icon)
|
||||
local align = widget.align or ''
|
||||
local padding = widget.padding or 0
|
||||
local text = widget.text
|
||||
local x, y, iconWidth, iconHeight
|
||||
|
||||
if icon then
|
||||
iconWidth, iconHeight = icon:getWidth(), icon:getHeight()
|
||||
-- horizontal alignment
|
||||
if align:find('right') then
|
||||
x = x2 - iconWidth
|
||||
elseif align:find('center') then
|
||||
x = x1 + (x2 - x1) / 2 - iconWidth / 2
|
||||
else -- if align:find('left') then
|
||||
x = x1
|
||||
end
|
||||
|
||||
-- vertical alignment
|
||||
if align:find('bottom') then
|
||||
y = y2 - iconHeight
|
||||
elseif align:find('middle') then
|
||||
y = y1 + (y2 - y1) / 2 - iconHeight / 2
|
||||
else -- if align:find('top') then
|
||||
y = y1
|
||||
end
|
||||
|
||||
--[[
|
||||
if text and align:find('center') then
|
||||
if align:find('bottom') then
|
||||
y = y - textHeight - padding
|
||||
elseif align:find('middle') then
|
||||
y = y - (textHeight + padding) / 2
|
||||
end
|
||||
end
|
||||
--]]
|
||||
|
||||
love.graphics.draw(icon, x, y)
|
||||
end
|
||||
|
||||
-- render text
|
||||
if not text then return end
|
||||
|
||||
if not widget.fontData then
|
||||
widget.fontData = Font(widget.font, widget.fontSize, widget.textColor)
|
||||
end
|
||||
|
||||
local font = widget.fontData
|
||||
|
||||
if icon then
|
||||
if align:find('center') then
|
||||
-- y1 = y1 + iconHeight + padding
|
||||
elseif align:find('right') then
|
||||
x2 = x2 - iconWidth - padding
|
||||
else
|
||||
x1 = x1 + iconWidth + padding
|
||||
end
|
||||
end
|
||||
|
||||
font:setWidth(x2 - x1)
|
||||
if align:find('right') then
|
||||
font:setAlignment('right')
|
||||
elseif align:find('center') then
|
||||
font:setAlignment('center')
|
||||
elseif align:find('justify') then
|
||||
font:setAlignment('justify')
|
||||
else -- if align:find('left') then
|
||||
font:setAlignment('left')
|
||||
end
|
||||
|
||||
local textHeight = font:getWrappedHeight(text)
|
||||
|
||||
local x, y
|
||||
|
||||
-- vertical alignment
|
||||
if align:find('bottom') then
|
||||
y = y2 - textHeight
|
||||
elseif align:find('middle') then
|
||||
y = y2 - (y2 - y1) / 2 - textHeight / 2
|
||||
if icon and align:find('center') then
|
||||
y = y1 + (iconHeight + padding) / 2
|
||||
end
|
||||
else -- if align:find('top') then
|
||||
y = y1
|
||||
if icon and align:find('center') then
|
||||
y = y1 + iconHeight + padding
|
||||
end
|
||||
end
|
||||
|
||||
x = math.floor(x1)
|
||||
y = math.floor(y)
|
||||
|
||||
window:write(x, y, x1, y1, x2, y2, text, font)
|
||||
end
|
||||
|
||||
function Renderer:renderChildren (widget)
|
||||
for i, child in ipairs(widget.children) do self:render(child) end
|
||||
end
|
||||
|
||||
function Renderer:render (widget)
|
||||
Event.Display:emit(widget, {}, function()
|
||||
local window = widget.layout.window
|
||||
self:renderBackground(widget, window)
|
||||
self:renderOutline(widget, window)
|
||||
self:renderIconAndText(widget, window)
|
||||
return self:renderChildren(widget)
|
||||
end)
|
||||
end
|
||||
|
||||
return Renderer
|
||||
79
luigi/style.lua
Normal file
79
luigi/style.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
|
||||
local Style = Base:extend()
|
||||
|
||||
function Style:constructor (rules, ...)
|
||||
self.rules = rules
|
||||
self.lookupNames = { ... }
|
||||
end
|
||||
|
||||
function Style:getProperty (object, property)
|
||||
local ownProperty = rawget(object, property)
|
||||
if ownProperty ~= nil then return ownProperty end
|
||||
for styleDef in self:each(object) do
|
||||
local result = self:getProperty(styleDef, property)
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
end
|
||||
|
||||
function Style:each (object)
|
||||
local rules = self.rules
|
||||
local nextStyleName = self:eachName(object)
|
||||
return function ()
|
||||
local styleName = nextStyleName()
|
||||
while styleName do
|
||||
local styleDef = rules[styleName]
|
||||
if styleDef then return styleDef end
|
||||
styleName = nextStyleName()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Style:eachName (object)
|
||||
local lookupNames = self.lookupNames
|
||||
local lookupNameIndex = 0
|
||||
local lookupPropIndex = 0
|
||||
local lookupProp
|
||||
|
||||
local returnedSpecialName = {}
|
||||
|
||||
local function checkLookupProp()
|
||||
if type(lookupProp) == 'table' and lookupPropIndex >= #lookupProp then
|
||||
lookupProp = nil
|
||||
end
|
||||
while not lookupProp do
|
||||
returnedSpecialName = {}
|
||||
lookupPropIndex = 0
|
||||
lookupNameIndex = lookupNameIndex + 1
|
||||
if lookupNameIndex > #lookupNames then return end
|
||||
lookupProp = rawget(object, lookupNames[lookupNameIndex])
|
||||
if type(lookupProp) == 'string' then
|
||||
lookupProp = { lookupProp }
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function getSpecialName (...)
|
||||
for k, name in ipairs({ ... }) do
|
||||
if not returnedSpecialName[name] then
|
||||
returnedSpecialName[name] = true
|
||||
if rawget(object, name) then
|
||||
return lookupProp[lookupPropIndex + 1] .. '_' .. name
|
||||
else
|
||||
return lookupProp[lookupPropIndex + 1] .. '_not_' .. name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return function ()
|
||||
if not checkLookupProp() then return end
|
||||
local specialName = getSpecialName('pressed', 'hovered')
|
||||
if specialName then return specialName end
|
||||
lookupPropIndex = lookupPropIndex + 1
|
||||
return lookupProp[lookupPropIndex]
|
||||
end
|
||||
end
|
||||
|
||||
return Style
|
||||
39
luigi/theme/light.lua
Normal file
39
luigi/theme/light.lua
Normal file
@@ -0,0 +1,39 @@
|
||||
local backColor = { 240, 240, 240 }
|
||||
local lineColor = { 220, 220, 220 }
|
||||
local highlightColor = { 220, 220, 240 }
|
||||
|
||||
return {
|
||||
panel = {
|
||||
background = backColor,
|
||||
padding = 4,
|
||||
},
|
||||
button = {
|
||||
type = 'panel',
|
||||
align = 'center middle',
|
||||
outline = lineColor,
|
||||
bend = 0.1,
|
||||
margin = 4,
|
||||
},
|
||||
button_hovered = {
|
||||
bend = 0.2,
|
||||
},
|
||||
button_pressed = {
|
||||
bend = -0.1,
|
||||
},
|
||||
text = {
|
||||
align = 'left middle',
|
||||
background = { 255, 255, 255 },
|
||||
outline = lineColor,
|
||||
bend = -0.1,
|
||||
margin = 4,
|
||||
padding = 4,
|
||||
},
|
||||
sash = {
|
||||
background = highlightColor
|
||||
},
|
||||
slider = {
|
||||
type = 'panel',
|
||||
outline = lineColor,
|
||||
bend = 0.1,
|
||||
},
|
||||
}
|
||||
246
luigi/widget.lua
Normal file
246
luigi/widget.lua
Normal file
@@ -0,0 +1,246 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
|
||||
local Widget = Base:extend()
|
||||
|
||||
Widget.isWidget = true
|
||||
|
||||
Widget.registeredTypes = {
|
||||
sash = ROOT .. 'widget.sash',
|
||||
slider = ROOT .. 'widget.slider',
|
||||
text = ROOT .. 'widget.text',
|
||||
}
|
||||
|
||||
function Widget.create (layout, data)
|
||||
local path = data.type and Widget.registeredTypes[data.type]
|
||||
|
||||
if path then
|
||||
return require(path)(layout, data)
|
||||
end
|
||||
|
||||
return Widget(layout, data)
|
||||
end
|
||||
|
||||
function Widget:constructor (layout, data)
|
||||
self.type = 'generic'
|
||||
self.layout = layout
|
||||
self.children = {}
|
||||
self.position = { x = nil, y = nil }
|
||||
self.dimensions = { width = nil, height = nil }
|
||||
self:extract(data)
|
||||
layout:addWidget(self)
|
||||
local widget = self
|
||||
local meta = getmetatable(self)
|
||||
local metaIndex = meta.__index
|
||||
function meta:__index(property)
|
||||
local value = metaIndex[property]
|
||||
local style = widget.layout.style
|
||||
local theme = widget.layout.theme
|
||||
if value ~= nil then return value end
|
||||
value = style and style:getProperty(self, property)
|
||||
if value ~= nil then return value end
|
||||
return theme and theme:getProperty(self, property)
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:extract (data)
|
||||
function toWidget(t)
|
||||
if t.isWidget then return t end
|
||||
return Widget.create(self.layout, t)
|
||||
end
|
||||
|
||||
for k, v in pairs(data) do
|
||||
if type(k) == 'number' then
|
||||
self.children[k] = toWidget(v)
|
||||
self.children[k].parent = self
|
||||
else
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:getPrevious ()
|
||||
local siblings = self.parent.children
|
||||
for i, widget in ipairs(siblings) do
|
||||
if widget == self then return siblings[i - 1] end
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:getNext ()
|
||||
local siblings = self.parent.children
|
||||
for i, widget in ipairs(siblings) do
|
||||
if widget == self then return siblings[i + 1] end
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:addChild (data)
|
||||
local layout = self.layout
|
||||
local child = Widget.create(layout, data)
|
||||
|
||||
table.insert(self.children, child)
|
||||
child.parent = self
|
||||
layout:addWidget(child)
|
||||
end
|
||||
|
||||
function Widget:calculateDimension (name)
|
||||
function clamp(value, min, max)
|
||||
if value < min then
|
||||
value = min
|
||||
elseif value > max then
|
||||
value = max
|
||||
end
|
||||
return value
|
||||
end
|
||||
if self[name] then
|
||||
self.dimensions[name] = clamp(self[name], 0, self.layout.root[name])
|
||||
end
|
||||
if self.dimensions[name] then
|
||||
return self.dimensions[name]
|
||||
end
|
||||
local parent = self.parent
|
||||
if not parent then
|
||||
return self.layout
|
||||
end
|
||||
local parentDimension = parent:calculateDimension(name)
|
||||
local parentFlow = parent.flow or 'y'
|
||||
if (parentFlow == 'y' and name == 'width') or
|
||||
(parentFlow == 'x' and name == 'height')
|
||||
then
|
||||
return parentDimension
|
||||
end
|
||||
local claimed = 0
|
||||
local unsized = 1
|
||||
for i, widget in ipairs(self.parent.children) do
|
||||
if widget ~= self then
|
||||
if widget[name] then
|
||||
claimed = claimed + widget:calculateDimension(name)
|
||||
if claimed > parentDimension then
|
||||
claimed = parentDimension
|
||||
end
|
||||
else
|
||||
unsized = unsized + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local size = (self.parent:calculateDimension(name) - claimed) / unsized
|
||||
self.dimensions[name] = clamp(size, 0, self.layout.root[name])
|
||||
return size
|
||||
end
|
||||
|
||||
function Widget:calculatePosition (axis)
|
||||
if self.position[axis] then
|
||||
return self.position[axis]
|
||||
end
|
||||
local parent = self.parent
|
||||
if not parent then
|
||||
self.position[axis] = 0
|
||||
return 0
|
||||
end
|
||||
local parentPos = parent:calculatePosition(axis)
|
||||
local p = parentPos
|
||||
local parentFlow = parent.flow or 'y'
|
||||
for i, widget in ipairs(parent.children) do
|
||||
if widget == self then
|
||||
self.position[axis] = p
|
||||
return p
|
||||
end
|
||||
if parentFlow == axis then
|
||||
local dimension = (axis == 'x') and 'width' or 'height'
|
||||
p = p + widget:calculateDimension(dimension)
|
||||
end
|
||||
end
|
||||
self.position[axis] = 0
|
||||
return 0
|
||||
end
|
||||
|
||||
function Widget:getX ()
|
||||
return self:calculatePosition('x')
|
||||
end
|
||||
|
||||
function Widget:getY ()
|
||||
return self:calculatePosition('y')
|
||||
end
|
||||
|
||||
function Widget:getWidth ()
|
||||
return self:calculateDimension('width')
|
||||
end
|
||||
|
||||
function Widget:getHeight ()
|
||||
return self:calculateDimension('height')
|
||||
end
|
||||
|
||||
function Widget:setDimension (name, size)
|
||||
local parentDimension = self.parent:calculateDimension(name)
|
||||
local claimed = 0
|
||||
for i, widget in ipairs(self.parent.children) do
|
||||
if widget ~= self and widget[name] then
|
||||
claimed = claimed + widget[name]
|
||||
end
|
||||
end
|
||||
if claimed + size > parentDimension then
|
||||
size = parentDimension - claimed
|
||||
end
|
||||
self[name] = size
|
||||
end
|
||||
|
||||
function Widget:setWidth (size)
|
||||
return self:setDimension('width', size)
|
||||
end
|
||||
|
||||
function Widget:setHeight (size)
|
||||
return self:setDimension('height', size)
|
||||
end
|
||||
|
||||
function Widget:getOrigin ()
|
||||
return self:getX(), self:getY()
|
||||
end
|
||||
|
||||
function Widget:getExtent ()
|
||||
local x, y = self:getX(), self:getY()
|
||||
return x + self:getWidth(), y + self:getHeight()
|
||||
end
|
||||
|
||||
function Widget:getRectangle (useMargin, usePadding)
|
||||
local x1, y1 = self:getOrigin()
|
||||
local x2, y2 = self:getExtent()
|
||||
local function shrink(amount)
|
||||
x1 = x1 + amount
|
||||
y1 = y1 + amount
|
||||
x2 = x2 - amount
|
||||
y2 = y2 - amount
|
||||
end
|
||||
if useMargin then
|
||||
shrink(self.margin or 0)
|
||||
end
|
||||
if usePadding then
|
||||
shrink(self.padding or 0)
|
||||
end
|
||||
return x1, y1, x2, y2
|
||||
end
|
||||
|
||||
function Widget:isAt (x, y)
|
||||
local x1, y1, x2, y2 = self:getRectangle()
|
||||
return (x1 < x) and (x2 > x) and (y1 < y) and (y2 > y)
|
||||
end
|
||||
|
||||
function Widget:getAncestors (includeSelf)
|
||||
local instance = includeSelf and self or self.parent
|
||||
return function()
|
||||
local widget = instance
|
||||
if not widget then return end
|
||||
instance = widget.parent
|
||||
return widget
|
||||
end
|
||||
end
|
||||
|
||||
function Widget:update ()
|
||||
self.layout:update()
|
||||
end
|
||||
|
||||
-- event binders
|
||||
|
||||
Event.injectBinders(Widget)
|
||||
|
||||
return Widget
|
||||
35
luigi/widget/sash.lua
Normal file
35
luigi/widget/sash.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local Widget = require((...):gsub('%.[^.]*$', ''))
|
||||
|
||||
local Sash = Widget:extend()
|
||||
|
||||
function Sash:constructor(layout, data)
|
||||
Widget.constructor(self, layout, data)
|
||||
|
||||
self:onPressDrag(function(event)
|
||||
local axis = self.parent.flow
|
||||
if axis == 'x' then
|
||||
dimension = 'width'
|
||||
else
|
||||
axis = 'y'
|
||||
dimension = 'height'
|
||||
end
|
||||
local prevSibling = self:getPrevious()
|
||||
local nextSibling = self:getNext()
|
||||
local prevSize = prevSibling and prevSibling[dimension]
|
||||
local nextSize = nextSibling and nextSibling[dimension]
|
||||
if prevSize then
|
||||
prevSibling:setDimension(dimension,
|
||||
event[axis] - prevSibling:calculatePosition(axis))
|
||||
end
|
||||
if nextSize then
|
||||
nextSibling:setDimension(dimension,
|
||||
nextSibling:calculatePosition(axis) +
|
||||
nextSibling[dimension] - event[axis])
|
||||
end
|
||||
|
||||
layout:update(true)
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
return Sash
|
||||
52
luigi/widget/slider.lua
Normal file
52
luigi/widget/slider.lua
Normal file
@@ -0,0 +1,52 @@
|
||||
local Widget = require((...):gsub('%.[^.]*$', ''))
|
||||
|
||||
local Slider = Widget:extend()
|
||||
|
||||
function Slider:constructor(layout, data)
|
||||
Widget.constructor(self, layout, data)
|
||||
|
||||
local function getCenter()
|
||||
return self:getX() + self:getWidth() / 2
|
||||
end
|
||||
|
||||
local position = 0.5
|
||||
|
||||
self:onPressDrag(function(event)
|
||||
local x1, y1, x2, y2 = self:getRectangle(true, true)
|
||||
position = (event.x - x1) / (x2 - x1)
|
||||
if position < 0 then position = 0 end
|
||||
if position > 1 then position = 1 end
|
||||
self:update()
|
||||
end)
|
||||
|
||||
self:onDisplay(function(event)
|
||||
-- event:yield()
|
||||
local x1, y1, x2, y2 = self:getRectangle(true, true)
|
||||
local padding = self.padding or 0
|
||||
self.layout.window:fill(
|
||||
x1,
|
||||
y1 + (y2 - y1) / 2 - padding / 2,
|
||||
x2,
|
||||
y1 + (y2 - y1) / 2 + padding / 2,
|
||||
self.background, -(self.bend or 0)
|
||||
)
|
||||
self.layout.window:fill(
|
||||
x1 + position * (x2 - x1) - padding,
|
||||
y1 + padding,
|
||||
x1 + position * (x2 - x1) + padding,
|
||||
y2 - padding,
|
||||
self.background, self.bend
|
||||
)
|
||||
self.layout.window:outline(
|
||||
x1 + position * (x2 - x1) - padding,
|
||||
y1 + padding,
|
||||
x1 + position * (x2 - x1) + padding,
|
||||
y2 - padding,
|
||||
self.outline
|
||||
)
|
||||
return false
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
return Slider
|
||||
9
luigi/widget/text.lua
Normal file
9
luigi/widget/text.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local Widget = require((...):gsub('%.[^.]*$', ''))
|
||||
|
||||
local Text = Widget:extend()
|
||||
|
||||
function Text:constructor(layout, data)
|
||||
Widget.constructor(self, layout, data)
|
||||
end
|
||||
|
||||
return Text
|
||||
125
luigi/window.lua
Normal file
125
luigi/window.lua
Normal file
@@ -0,0 +1,125 @@
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Hooker = require(ROOT .. 'hooker')
|
||||
|
||||
local Window = Base:extend()
|
||||
|
||||
local unpack = table.unpack or _G.unpack
|
||||
|
||||
function Window:constructor (input)
|
||||
self.input = input
|
||||
self.isMousePressed = false
|
||||
self.isManagingInput = false
|
||||
self.hooked = {}
|
||||
self.hooks = {}
|
||||
end
|
||||
|
||||
function Window:hook (key, method)
|
||||
self.hooks[#self.hooks + 1] = Hooker.hook(key, method)
|
||||
end
|
||||
|
||||
function Window:unhook ()
|
||||
for _, item in ipairs(self.hooks) do
|
||||
Hooker.unhook(item)
|
||||
end
|
||||
self.hooks = {}
|
||||
end
|
||||
|
||||
function Window:manageInput (input)
|
||||
if self.isManagingInput then
|
||||
return
|
||||
end
|
||||
self.isManagingInput = true
|
||||
|
||||
self:hook('draw', function ()
|
||||
input:handleDisplay()
|
||||
end)
|
||||
self:hook('resize', function (width, height)
|
||||
return input:handleReshape(width, height)
|
||||
end)
|
||||
self:hook('mousepressed', function (x, y, button)
|
||||
self.isMousePressed = true
|
||||
return input:handlePressStart(button, x, y)
|
||||
end)
|
||||
self:hook('mousereleased', function (x, y, button)
|
||||
self.isMousePressed = false
|
||||
return input:handlePressEnd(button, x, y)
|
||||
end)
|
||||
self:hook('mousemoved', function (x, y, dx, dy)
|
||||
if self.isMousePressed then
|
||||
return input:handlePressedMotion(x, y)
|
||||
else
|
||||
return input:handleMotion(x, y)
|
||||
end
|
||||
end)
|
||||
self:hook('keypressed', function (key, isRepeat)
|
||||
return input:handleKeyboard(key, love.mouse.getX(), love.mouse.getY())
|
||||
end)
|
||||
end
|
||||
|
||||
function Window:show (width, height, title)
|
||||
local currentWidth, currentHeight, flags = love.window.getMode()
|
||||
love.window.setMode(width or currentWidth, height or currentHeight, flags)
|
||||
if title then
|
||||
love.window.setTitle(title)
|
||||
end
|
||||
self:manageInput(self.input)
|
||||
end
|
||||
|
||||
function Window:hide ()
|
||||
if not self.isManagingInput then
|
||||
return
|
||||
end
|
||||
self.isManagingInput = false
|
||||
self:unhook()
|
||||
end
|
||||
|
||||
local function setColor (color)
|
||||
love.graphics.setColor(color)
|
||||
end
|
||||
|
||||
function Window:fill (x1, y1, x2, y2, color)
|
||||
setColor(color)
|
||||
love.graphics.rectangle('fill', x1, y1, x2 - x1, y2 - y1)
|
||||
end
|
||||
|
||||
function Window:outline (x1, y1, x2, y2, color)
|
||||
setColor(color)
|
||||
love.graphics.rectangle('line', x1, y1, x2 - x1, y2 - y1)
|
||||
end
|
||||
|
||||
function Window:write (x, y, x1, y1, x2, y2, text, font)
|
||||
|
||||
local width, height = x2 - x1, y2 - y1
|
||||
|
||||
if width < 1 or height < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local sx, sy, sw, sh = love.graphics.getScissor()
|
||||
|
||||
love.graphics.setScissor(x1, y1, width, height)
|
||||
local oldFont = love.graphics.getFont()
|
||||
love.graphics.setFont(font.font)
|
||||
|
||||
setColor(font.color)
|
||||
|
||||
local layout = font.layout
|
||||
love.graphics.printf(text, x, y, layout.width or width, layout.align)
|
||||
|
||||
love.graphics.setScissor(sx, sy, sw, sh)
|
||||
love.graphics.setFont(oldFont)
|
||||
end
|
||||
|
||||
function Window:update (reshape)
|
||||
if reshape then
|
||||
for i, widget in ipairs(self.input.layout.widgets) do
|
||||
widget.position = {}
|
||||
widget.dimensions = {}
|
||||
widget.fontData = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Window
|
||||
Reference in New Issue
Block a user