add luigi graphics library
602
src/lib/luigi/attribute.lua
Normal file
|
@ -0,0 +1,602 @@
|
|||
--[[--
|
||||
Widget attributes.
|
||||
|
||||
This module defines "attributes" (special fields) that are
|
||||
recognized by all widgets. Their interpretation may vary
|
||||
depending on the `type` of widget. Some widget types may also
|
||||
recognize additional attributes.
|
||||
|
||||
Setting attributes can have side effects. For example, setting
|
||||
`height` or `width` causes the parent widget and its descendants
|
||||
to recalculate their size and position.
|
||||
--]]--
|
||||
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Shortcut = require(ROOT .. 'shortcut')
|
||||
|
||||
local Attribute = {}
|
||||
|
||||
local function cascade (widget, attribute)
|
||||
local value = rawget(widget, 'attributes')[attribute]
|
||||
if value ~= nil then return value end
|
||||
local parent = rawget(widget, 'parent')
|
||||
return parent and parent[attribute]
|
||||
end
|
||||
|
||||
--[[--
|
||||
Type of widget.
|
||||
|
||||
Should contain a string identifying the widget's type.
|
||||
After the layout is built, may be replaced by an array
|
||||
of strings identifying multiple types. This is used
|
||||
internally by some widgets to provide information about
|
||||
the widget's state to the theme (themes describe the
|
||||
appearance of widgets according to their type).
|
||||
|
||||
If a type is registered with the widget's layout, the registered
|
||||
type initializer function will run once when the widget is constructed.
|
||||
|
||||
@see Widget.register
|
||||
|
||||
@attrib type
|
||||
--]]--
|
||||
Attribute.type = {}
|
||||
|
||||
function Attribute.type.set (widget, value)
|
||||
local oldType = widget.attributes.type
|
||||
|
||||
widget.attributes.type = value
|
||||
|
||||
if value and not widget.hasType then
|
||||
widget.hasType = true
|
||||
local Widget = require(ROOT .. 'widget')
|
||||
local decorate = Widget.typeDecorators[value]
|
||||
|
||||
if decorate then
|
||||
decorate(widget)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Widget identifier.
|
||||
|
||||
Should contain a unique string identifying the widget, if present.
|
||||
|
||||
A reference to the widget will be stored in the associated layout
|
||||
in a property having the same name as the widget's id.
|
||||
|
||||
Setting this attribute re-registers the widget with its layout.
|
||||
|
||||
@attrib id
|
||||
--]]--
|
||||
Attribute.id = {}
|
||||
|
||||
function Attribute.id.set (widget, value)
|
||||
local layout = widget.layout.master or widget.layout
|
||||
local oldValue = widget.attributes.id
|
||||
|
||||
if oldValue then
|
||||
layout[oldValue] = nil
|
||||
end
|
||||
|
||||
if value then
|
||||
layout[value] = widget
|
||||
end
|
||||
|
||||
widget.attributes.id = value
|
||||
end
|
||||
|
||||
--[[--
|
||||
Widget value.
|
||||
|
||||
Some widget types expect the value to be of a specific type and
|
||||
within a specific range. For example, `slider` and `progress`
|
||||
widgets expect a normalized number, `text` widgets expect
|
||||
a string, and `check` and `radio` widgets expect a boolean.
|
||||
|
||||
Setting this attribute bubbles the `Change` event.
|
||||
|
||||
@attrib value
|
||||
--]]--
|
||||
Attribute.value = {}
|
||||
|
||||
function Attribute.value.set (widget, value)
|
||||
local oldValue = widget.value
|
||||
widget.attributes.value = value
|
||||
widget:bubbleEvent('Change', { value = value, oldValue = oldValue })
|
||||
end
|
||||
|
||||
--[[--
|
||||
Solidity.
|
||||
|
||||
Should true or false.
|
||||
|
||||
@attrib icon
|
||||
--]]--
|
||||
Attribute.solid = {}
|
||||
|
||||
function Attribute.solid.set (widget, value)
|
||||
widget.attributes.solid = value
|
||||
end
|
||||
|
||||
Attribute.solid.get = cascade
|
||||
|
||||
--[[--
|
||||
Context menu.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib context
|
||||
--]]--
|
||||
Attribute.context = {}
|
||||
|
||||
function Attribute.context.set (widget, value)
|
||||
widget.attributes.context = value
|
||||
if not value then return end
|
||||
value.isContextMenu = true
|
||||
widget.layout:createWidget { type = 'menu', value }
|
||||
end
|
||||
|
||||
Attribute.context.get = cascade
|
||||
|
||||
--[[--
|
||||
Widget style.
|
||||
|
||||
Should contain a string or array of strings identifying
|
||||
style rules to be applied to the widget. When resolving
|
||||
any attribute with a `nil` value, these style rules are
|
||||
searched for a corresponding attribute.
|
||||
|
||||
Setting this attribute resets the `Font` and `Text` object
|
||||
associated with this widget.
|
||||
|
||||
Setting this attribute recalculates the size and position
|
||||
of the parent widget and its descendants.
|
||||
|
||||
@attrib style
|
||||
--]]--
|
||||
Attribute.style = {}
|
||||
|
||||
function Attribute.style.set (widget, value)
|
||||
widget.attributes.style = value
|
||||
widget.fontData = nil
|
||||
widget.textData = nil
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Status message.
|
||||
|
||||
Should contain a string with a short message describing the
|
||||
purpose or state of the widget.
|
||||
|
||||
This message will appear in the last created `status` widget
|
||||
in the same layout, or in the master layout if one exists.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib status
|
||||
--]]--
|
||||
Attribute.status = {}
|
||||
|
||||
Attribute.status.get = cascade
|
||||
|
||||
--[[--
|
||||
Scroll ability.
|
||||
|
||||
Should contain `true` or `false` (or `nil`).
|
||||
|
||||
If set to `true`, moving the scroll wheel over the widget will adjust
|
||||
its scroll position when the widget's contents overflow its boundary.
|
||||
|
||||
@attrib scroll
|
||||
--]]--
|
||||
Attribute.scroll = {}
|
||||
|
||||
--[[--
|
||||
Keyboard Attributes.
|
||||
|
||||
@section keyboard
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Focusable.
|
||||
|
||||
Should contain `true` if the widget can be focused by pressing the tab key.
|
||||
|
||||
@attrib focusable
|
||||
--]]--
|
||||
Attribute.focusable = {}
|
||||
|
||||
--[[--
|
||||
Keyboard shortcut.
|
||||
|
||||
Should contain a string representing a key and optional modifiers,
|
||||
separated by dashes; for example `'ctrl-c'` or `'alt-shift-escape'`.
|
||||
|
||||
Pressing this key combination bubbles a `Press` event on the widget,
|
||||
as if it had been pressed with a mouse or touch interface.
|
||||
|
||||
Setting this attribute re-registers the widget with its layout.
|
||||
|
||||
@attrib shortcut
|
||||
--]]--
|
||||
Attribute.shortcut = {}
|
||||
|
||||
local function setShortcut (layout, shortcut, value)
|
||||
local mainKey, modifierFlags = Shortcut.parseKeyCombo(shortcut)
|
||||
if mainKey then
|
||||
layout.shortcuts[modifierFlags][mainKey] = value
|
||||
end
|
||||
end
|
||||
|
||||
function Attribute.shortcut.set (widget, value)
|
||||
local layout = widget.layout.master or widget.layout
|
||||
local oldValue = widget.attributes.shortcut
|
||||
|
||||
if oldValue then
|
||||
if type(oldValue) == 'table' then
|
||||
for _, v in ipairs(oldValue) do
|
||||
setShortcut(layout, v, nil)
|
||||
end
|
||||
else
|
||||
setShortcut(layout, oldValue, nil)
|
||||
end
|
||||
end
|
||||
|
||||
if value then
|
||||
if type(value) == 'table' then
|
||||
for _, v in ipairs(value) do
|
||||
setShortcut(layout, v, widget)
|
||||
end
|
||||
else
|
||||
setShortcut(layout, value, widget)
|
||||
end
|
||||
end
|
||||
|
||||
widget.attributes.shortcut = value
|
||||
end
|
||||
|
||||
--[[--
|
||||
Size Attributes.
|
||||
|
||||
Setting these attributes recalculates the size and position
|
||||
of the parent widget and its descendants.
|
||||
|
||||
@section size
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Flow axis.
|
||||
|
||||
Should equal either `'x'` or `'y'`. Defaults to `'y'`.
|
||||
|
||||
This attribute determines the placement and default dimensions
|
||||
of any child widgets.
|
||||
|
||||
When flow is `'x'`, the `height` of child widgets defaults
|
||||
to this widget's height, and each child is placed to the
|
||||
right of the previous child. When flow is `'y'`, the `width`
|
||||
of child widgets defaults to this widget's width, and each
|
||||
child is placed below the previous child.
|
||||
|
||||
Setting this attribute resets the `Text` object associated
|
||||
with this widget.
|
||||
|
||||
@attrib flow
|
||||
--]]--
|
||||
Attribute.flow = {}
|
||||
|
||||
function Attribute.flow.set (widget, value)
|
||||
widget.attributes.flow = value
|
||||
widget.textData = nil
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Width.
|
||||
|
||||
This attribute may not always hold a numeric value.
|
||||
To get the calculated width, use `Widget:getWidth`.
|
||||
|
||||
Setting this attribute when the `wrap` attribute is
|
||||
also present resets the `Text` object associated
|
||||
with this widget.
|
||||
|
||||
@attrib width
|
||||
--]]--
|
||||
Attribute.width = {}
|
||||
|
||||
function Attribute.width.set (widget, value)
|
||||
if value ~= 'auto' then
|
||||
value = value and math.max(value, widget.minwidth or 0)
|
||||
end
|
||||
widget.attributes.width = value
|
||||
if widget.wrap then
|
||||
widget.textData = nil
|
||||
end
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Height.
|
||||
|
||||
This attribute may not always hold a numeric value.
|
||||
To get the calculated height, use `Widget:getHeight`.
|
||||
|
||||
@attrib height
|
||||
--]]--
|
||||
Attribute.height = {}
|
||||
|
||||
function Attribute.height.set (widget, value)
|
||||
if value ~= 'auto' then
|
||||
value = value and math.max(value, widget.minheight or 0)
|
||||
end
|
||||
widget.attributes.height = value
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Minimum width.
|
||||
|
||||
@attrib minwidth
|
||||
--]]--
|
||||
Attribute.minwidth = {}
|
||||
|
||||
function Attribute.minwidth.set (widget, value)
|
||||
local attributes = widget.attributes
|
||||
attributes.minwidth = value
|
||||
if type(value) == 'number' then
|
||||
local current = attributes.width
|
||||
if type(current) == 'number' then
|
||||
attributes.width = math.max(current, value)
|
||||
end
|
||||
end
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Minimum height.
|
||||
|
||||
@attrib minheight
|
||||
--]]--
|
||||
Attribute.minheight = {}
|
||||
|
||||
function Attribute.minheight.set (widget, value)
|
||||
local attributes = widget.attributes
|
||||
attributes.minheight = value
|
||||
if type(value) == 'number' then
|
||||
local current = attributes.height
|
||||
if type(current) == 'number' then
|
||||
attributes.height = math.max(current, value)
|
||||
end
|
||||
end
|
||||
widget.reshape(widget.parent or widget)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Font Attributes.
|
||||
|
||||
Setting these attributes resets the Font and Text
|
||||
objects associated with the widget.
|
||||
|
||||
@section font
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Font path.
|
||||
|
||||
Should contain a path to a TrueType font to use for displaying
|
||||
this widget's `text`.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib font
|
||||
--]]--
|
||||
Attribute.font = {}
|
||||
|
||||
local function resetFont (widget)
|
||||
rawset(widget, 'fontData', nil)
|
||||
rawset(widget, 'textData', nil)
|
||||
for _, child in ipairs(widget) do
|
||||
resetFont(child)
|
||||
end
|
||||
local items = widget.items
|
||||
if items then
|
||||
for _, child in ipairs(items) do
|
||||
resetFont(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Attribute.font.set (widget, value)
|
||||
widget.attributes.font = value
|
||||
resetFont(widget)
|
||||
end
|
||||
|
||||
Attribute.font.get = cascade
|
||||
|
||||
--[[--
|
||||
Font size.
|
||||
|
||||
Should contain a number representing the size of the font, in points.
|
||||
Defaults to 12.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib size
|
||||
--]]--
|
||||
Attribute.size = {}
|
||||
|
||||
function Attribute.size.set (widget, value)
|
||||
widget.attributes.size = value
|
||||
widget.fontData = nil
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
Attribute.size.get = cascade
|
||||
|
||||
--[[--
|
||||
Text Attributes.
|
||||
|
||||
Setting these attributes resets the Text object
|
||||
associated with the widget.
|
||||
|
||||
@section text
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Text to display.
|
||||
|
||||
@attrib text
|
||||
--]]--
|
||||
Attribute.text = {}
|
||||
|
||||
function Attribute.text.set (widget, value)
|
||||
widget.attributes.text = value
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
Text color.
|
||||
|
||||
Should contain an array with 3 or 4 values (RGB or RGBA) from 0 to 255.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib color
|
||||
--]]--
|
||||
Attribute.color = {}
|
||||
|
||||
function Attribute.color.set (widget, value)
|
||||
widget.attributes.color = value
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
Attribute.color.get = cascade
|
||||
|
||||
--[[--
|
||||
Text and icon alignment.
|
||||
|
||||
Should contain a string defining vertical and horizontal alignment.
|
||||
Vertical alignment is defined by either 'top', 'middle', or 'bottom',
|
||||
and horizontal alignment is defined by either 'left', 'center', or 'right'.
|
||||
|
||||
For example, `align = 'top left'`
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib align
|
||||
--]]--
|
||||
Attribute.align = {}
|
||||
|
||||
function Attribute.align.set (widget, value)
|
||||
widget.attributes.align = value
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
Attribute.align.get = cascade
|
||||
|
||||
--[[--
|
||||
Wrap text onto multiple lines.
|
||||
|
||||
Should contain `true` for multiline text, or `false` or `nil`
|
||||
for a single line. Even text containing line breaks will display
|
||||
as a single line when this attribute is not set to `true`.
|
||||
|
||||
- This attribute cascades.
|
||||
|
||||
@attrib wrap
|
||||
--]]--
|
||||
Attribute.wrap = {}
|
||||
|
||||
function Attribute.wrap.set (widget, value)
|
||||
widget.attributes.wrap = value
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
Attribute.wrap.get = cascade
|
||||
|
||||
--[[--
|
||||
Visual Attributes.
|
||||
|
||||
@section visual
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Background color.
|
||||
|
||||
Should contain an array with 3 or 4 values (RGB or RGBA) from 0 to 255.
|
||||
|
||||
@attrib background
|
||||
--]]--
|
||||
Attribute.background = {}
|
||||
|
||||
--[[--
|
||||
Outline color.
|
||||
|
||||
Should contain an array with 3 or 4 values (RGB or RGBA) from 0 to 255.
|
||||
|
||||
@attrib outline
|
||||
--]]--
|
||||
Attribute.outline = {}
|
||||
|
||||
--[[--
|
||||
Slice image.
|
||||
|
||||
Should contain a path to an image with "slices" to display for this widget.
|
||||
|
||||
@attrib slices
|
||||
--]]--
|
||||
Attribute.slices = {}
|
||||
|
||||
--[[--
|
||||
Margin size.
|
||||
|
||||
The margin area occupies space outside of the `outline` and `slices`.
|
||||
|
||||
@attrib margin
|
||||
--]]--
|
||||
Attribute.margin = {}
|
||||
|
||||
function Attribute.margin.set (widget, value)
|
||||
widget.attributes.margin = value
|
||||
widget.textData = nil
|
||||
widget:reshape()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Padding size.
|
||||
|
||||
The padding area occupies space inside the `outline` and `slices`,
|
||||
and outside the space where the `icon` and `text` and any
|
||||
child widgets appear.
|
||||
|
||||
@attrib padding
|
||||
--]]--
|
||||
Attribute.padding = {}
|
||||
|
||||
function Attribute.padding.set (widget, value)
|
||||
widget.attributes.padding = value
|
||||
widget.textData = nil
|
||||
widget:reshape()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Icon path.
|
||||
|
||||
Should contain a path to an image file.
|
||||
|
||||
@attrib icon
|
||||
--]]--
|
||||
Attribute.icon = {}
|
||||
|
||||
function Attribute.icon.set (widget, value)
|
||||
widget.attributes.icon = value
|
||||
widget.textData = nil
|
||||
end
|
||||
|
||||
|
||||
return Attribute
|
27
src/lib/luigi/backend.lua
Normal file
|
@ -0,0 +1,27 @@
|
|||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
local Backend
|
||||
|
||||
if _G.love and (_G.love._version_major or _G.love._version_minor) then
|
||||
Backend = require(ROOT .. 'backend.love')
|
||||
else
|
||||
Backend = require(ROOT .. 'backend.ffisdl')
|
||||
end
|
||||
|
||||
Backend.intersectScissor = Backend.intersectScissor or function (x, y, w, h)
|
||||
local sx, sy, sw, sh = Backend.getScissor()
|
||||
if not sx then
|
||||
return Backend.setScissor(x, y, w, h)
|
||||
end
|
||||
local x1 = math.max(sx, x)
|
||||
local y1 = math.max(sy, y)
|
||||
local x2 = math.min(sx + sw, x + w)
|
||||
local y2 = math.min(sy + sh, y + h)
|
||||
if x2 > x1 and y2 > y1 then
|
||||
Backend.setScissor(x1, y1, x2 - x1, y2 - y1)
|
||||
else
|
||||
-- HACK
|
||||
Backend.setScissor(-100, -100, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
return Backend
|
540
src/lib/luigi/backend/ffisdl.lua
Normal file
|
@ -0,0 +1,540 @@
|
|||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
local Hooker = require(ROOT .. 'hooker')
|
||||
|
||||
local ffi = require 'ffi'
|
||||
local sdl = require((...) .. '.sdl')
|
||||
|
||||
local Image = require((...) .. '.image')
|
||||
local Font = require((...) .. '.font')
|
||||
local Keyboard = require((...) .. '.keyboard')
|
||||
local Text = require((...) .. '.text')
|
||||
|
||||
local IntOut = ffi.typeof 'int[1]'
|
||||
|
||||
local stack = {}
|
||||
|
||||
-- create window and renderer
|
||||
sdl.enableScreenSaver()
|
||||
|
||||
local window = sdl.createWindow('',
|
||||
sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, 800, 600,
|
||||
sdl.WINDOW_SHOWN + sdl.WINDOW_RESIZABLE)
|
||||
|
||||
if window == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
ffi.gc(window, sdl.destroyWindow)
|
||||
|
||||
local renderer = sdl.createRenderer(window, -1,
|
||||
sdl.RENDERER_ACCELERATED + sdl.RENDERER_PRESENTVSYNC)
|
||||
|
||||
if renderer == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
ffi.gc(renderer, sdl.destroyRenderer)
|
||||
|
||||
sdl.setRenderDrawBlendMode(renderer, sdl.BLENDMODE_BLEND)
|
||||
|
||||
local Backend = {}
|
||||
|
||||
Backend.sdl = sdl
|
||||
|
||||
Backend.isMac = function ()
|
||||
return sdl.getPlatform() == 'Mac OS X'
|
||||
end
|
||||
|
||||
local callback = {
|
||||
draw = function () end,
|
||||
resize = function () end,
|
||||
mousepressed = function () end,
|
||||
mousereleased = function () end,
|
||||
mousemoved = function () end,
|
||||
keypressed = function () end,
|
||||
keyreleased = function () end,
|
||||
textinput = function () end,
|
||||
wheelmoved = function () end,
|
||||
}
|
||||
|
||||
Backend.run = function ()
|
||||
local event = sdl.Event()
|
||||
local tickInterval = 16 -- ~60 fps (with room)
|
||||
local nextTick = 0
|
||||
local sdl = sdl
|
||||
|
||||
while true do
|
||||
|
||||
sdl.pumpEvents()
|
||||
|
||||
while sdl.pollEvent(event) ~= 0 do
|
||||
if event.type == sdl.QUIT then
|
||||
return
|
||||
elseif event.type == sdl.WINDOWEVENT
|
||||
and event.window.event == sdl.WINDOWEVENT_RESIZED then
|
||||
local window = event.window
|
||||
callback.resize(window.data1, window.data2)
|
||||
elseif event.type == sdl.MOUSEBUTTONDOWN then
|
||||
local button = event.button
|
||||
callback.mousepressed(button.x, button.y, button.button)
|
||||
elseif event.type == sdl.MOUSEBUTTONUP then
|
||||
local button = event.button
|
||||
callback.mousereleased(button.x, button.y, button.button)
|
||||
elseif event.type == sdl.MOUSEMOTION then
|
||||
local motion = event.motion
|
||||
callback.mousemoved(motion.x, motion.y)
|
||||
elseif event.type == sdl.KEYDOWN then
|
||||
local key = Keyboard.stringByKeycode[event.key.keysym.sym] or 'unknown'
|
||||
local scanCode = Keyboard.stringByScancode[event.key.keysym.scancode] or 'unknown'
|
||||
callback.keypressed(key, scanCode, event.key['repeat'])
|
||||
elseif event.type == sdl.KEYUP then
|
||||
local key = Keyboard.stringByKeycode[event.key.keysym.sym] or 'unknown'
|
||||
local scanCode = Keyboard.stringByScancode[event.key.keysym.scancode] or 'unknown'
|
||||
callback.keyreleased(key, scanCode, event.key['repeat'])
|
||||
elseif event.type == sdl.TEXTINPUT then
|
||||
callback.textinput(ffi.string(event.text.text))
|
||||
elseif event.type == sdl.MOUSEWHEEL then
|
||||
local wheel = event.wheel
|
||||
callback.wheelmoved(wheel.x, wheel.y)
|
||||
end
|
||||
end
|
||||
|
||||
sdl.renderSetClipRect(renderer, nil)
|
||||
sdl.setRenderDrawColor(renderer, 0, 0, 0, 255)
|
||||
sdl.renderClear(renderer)
|
||||
callback.draw()
|
||||
|
||||
local now = sdl.getTicks()
|
||||
if nextTick > now then
|
||||
sdl.delay(nextTick - now)
|
||||
end
|
||||
nextTick = now + tickInterval
|
||||
|
||||
sdl.renderPresent(renderer)
|
||||
end
|
||||
end
|
||||
|
||||
Backend.Cursor = function (image, x, y)
|
||||
return sdl.createColorCursor(image.sdlSurface, x, y)
|
||||
end
|
||||
|
||||
Backend.Font = Font
|
||||
|
||||
Backend.Image = function (path)
|
||||
return Image(renderer, path)
|
||||
end
|
||||
|
||||
Backend.Text = function (...)
|
||||
return Text(renderer, ...)
|
||||
end
|
||||
|
||||
Backend.Quad = function (x, y, w, h)
|
||||
return { x, y, w, h }
|
||||
end
|
||||
|
||||
Backend.SpriteBatch = require((...) .. '.spritebatch')
|
||||
|
||||
Backend.draw = function (drawable, x, y, sx, sy)
|
||||
if drawable.draw then
|
||||
return drawable:draw(x, y, sx, sy)
|
||||
end
|
||||
|
||||
if drawable.sdlTexture == nil
|
||||
or drawable.sdlRenderer == nil
|
||||
or drawable.getWidth == nil
|
||||
or drawable.getHeight == nil
|
||||
then return
|
||||
end
|
||||
|
||||
local w = drawable:getWidth() * (sx or 1)
|
||||
local h = drawable:getHeight() * (sy or 1)
|
||||
|
||||
-- HACK. Somehow drawing something first prevents renderCopy from
|
||||
-- incorrectly scaling up in some cases (after rendering slices).
|
||||
-- For example http://stackoverflow.com/questions/28218906
|
||||
sdl.renderDrawPoint(drawable.sdlRenderer, -1, -1)
|
||||
|
||||
-- Draw the image.
|
||||
sdl.renderCopy(drawable.sdlRenderer, drawable.sdlTexture,
|
||||
nil, sdl.Rect(x, y, w, h))
|
||||
end
|
||||
|
||||
Backend.drawRectangle = function (mode, x, y, w, h)
|
||||
if mode == 'fill' then
|
||||
sdl.renderFillRect(renderer, sdl.Rect(x, y, w, h))
|
||||
else
|
||||
sdl.renderDrawRect(renderer, sdl.Rect(x, y, w, h))
|
||||
end
|
||||
end
|
||||
|
||||
local currentFont = Font()
|
||||
|
||||
local lastColor
|
||||
|
||||
-- print( text, x, y, r, sx, sy, ox, oy, kx, ky )
|
||||
Backend.print = function (text, x, y)
|
||||
if not text or text == '' then return end
|
||||
local font = currentFont.sdlFont
|
||||
local color = sdl.Color(lastColor or { 0, 0, 0, 255 })
|
||||
local write = Font.SDL2_ttf.TTF_RenderUTF8_Blended
|
||||
|
||||
local surface = write(font, text, color)
|
||||
ffi.gc(surface, sdl.freeSurface)
|
||||
local texture = sdl.createTextureFromSurface(renderer, surface)
|
||||
ffi.gc(texture, sdl.destroyTexture)
|
||||
sdl.renderCopy(renderer, texture, nil, sdl.Rect(x, y, surface.w, surface.h))
|
||||
end
|
||||
|
||||
Backend.getClipboardText = function ()
|
||||
return ffi.string(sdl.getClipboardText())
|
||||
end
|
||||
|
||||
Backend.setClipboardText = sdl.setClipboardText
|
||||
|
||||
Backend.getMousePosition = function ()
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getMouseState(x, y)
|
||||
return x[0], y[0]
|
||||
end
|
||||
|
||||
local function SystemCursor (id)
|
||||
return ffi.gc(sdl.createSystemCursor(id), sdl.freeCursor)
|
||||
end
|
||||
|
||||
local systemCursors = {
|
||||
arrow = SystemCursor(sdl.SYSTEM_CURSOR_ARROW),
|
||||
ibeam = SystemCursor(sdl.SYSTEM_CURSOR_IBEAM),
|
||||
wait = SystemCursor(sdl.SYSTEM_CURSOR_WAIT),
|
||||
crosshair = SystemCursor(sdl.SYSTEM_CURSOR_CROSSHAIR),
|
||||
waitarrow = SystemCursor(sdl.SYSTEM_CURSOR_WAITARROW),
|
||||
sizenwse = SystemCursor(sdl.SYSTEM_CURSOR_SIZENWSE),
|
||||
sizenesw = SystemCursor(sdl.SYSTEM_CURSOR_SIZENESW),
|
||||
sizewe = SystemCursor(sdl.SYSTEM_CURSOR_SIZEWE),
|
||||
sizens = SystemCursor(sdl.SYSTEM_CURSOR_SIZENS),
|
||||
sizeall = SystemCursor(sdl.SYSTEM_CURSOR_SIZEALL),
|
||||
no = SystemCursor(sdl.SYSTEM_CURSOR_NO),
|
||||
hand = SystemCursor(sdl.SYSTEM_CURSOR_HAND),
|
||||
}
|
||||
|
||||
Backend.getSystemCursor = function (name)
|
||||
return systemCursors[name] or systemCursors.arrow
|
||||
end
|
||||
|
||||
Backend.getWindowSize = function ()
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getWindowSize(window, x, y)
|
||||
return x[0], y[0]
|
||||
end
|
||||
|
||||
Backend.getTime = function ()
|
||||
return sdl.getTicks() * 0.001
|
||||
end
|
||||
|
||||
Backend.isKeyDown = function (...)
|
||||
local state = sdl.getKeyboardState(nil)
|
||||
|
||||
for i = 1, select('#', ...) do
|
||||
local name = select(i, ...)
|
||||
local scan = Keyboard.scancodeByString[name]
|
||||
if scan and state[scan] ~= 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
Backend.isMouseDown = function ()
|
||||
end
|
||||
|
||||
Backend.quit = function ()
|
||||
sdl.quit()
|
||||
os.exit()
|
||||
end
|
||||
|
||||
Backend.setColor = function (color)
|
||||
lastColor = color
|
||||
sdl.setRenderDrawColor(renderer,
|
||||
color[1], color[2], color[3], color[4] or 255)
|
||||
end
|
||||
|
||||
Backend.setCursor = function (cursor)
|
||||
sdl.setCursor(cursor or Backend.getSystemCursor('arrow'))
|
||||
end
|
||||
|
||||
Backend.setFont = function (font)
|
||||
currentFont = font
|
||||
end
|
||||
|
||||
local lastScissor
|
||||
|
||||
Backend.setScissor = function (x, y, w, h)
|
||||
-- y = y and Backend.getWindowHeight() - (y + h)
|
||||
lastScissor = x and sdl.Rect(x, y, w, h)
|
||||
sdl.renderSetClipRect(renderer, lastScissor)
|
||||
end
|
||||
|
||||
Backend.getScissor = function ()
|
||||
if lastScissor ~= nil then
|
||||
local x, y = lastScissor.x, lastScissor.y
|
||||
local w, h = lastScissor.w, lastScissor.h
|
||||
-- y = y and Backend.getWindowHeight() - (y + h)
|
||||
return x, y, w, h
|
||||
end
|
||||
end
|
||||
|
||||
function Backend.hide (layout)
|
||||
for _, item in ipairs(layout.hooks) do
|
||||
Hooker.unhook(item)
|
||||
end
|
||||
layout.hooks = {}
|
||||
end
|
||||
|
||||
local function hook (layout, key, method, hookLast)
|
||||
layout.hooks[#layout.hooks + 1] = Hooker.hook(
|
||||
callback, key, method, hookLast)
|
||||
end
|
||||
|
||||
Backend.pop = function ()
|
||||
local history = stack[#stack]
|
||||
lastColor = history.color or { 0, 0, 0, 255 }
|
||||
lastScissor = history.scissor
|
||||
|
||||
sdl.setRenderDrawColor(renderer,
|
||||
lastColor[1], lastColor[2], lastColor[3], lastColor[4] or 255)
|
||||
sdl.renderSetClipRect(renderer, lastScissor) -- Backend.setScissor(history.scissor)
|
||||
stack[#stack] = nil
|
||||
end
|
||||
|
||||
Backend.push = function ()
|
||||
stack[#stack + 1] = {
|
||||
color = lastColor,
|
||||
scissor = lastScissor,
|
||||
}
|
||||
end
|
||||
|
||||
local isMouseDown = function ()
|
||||
return sdl.getMouseState(nil, nil) > 0
|
||||
end
|
||||
|
||||
local buttonIds = {
|
||||
[sdl.BUTTON_LEFT] = 'left',
|
||||
[sdl.BUTTON_MIDDLE] = 'middle',
|
||||
[sdl.BUTTON_RIGHT] = 'right',
|
||||
-- [sdl.BUTTON_X1] = 'x1',
|
||||
-- [sdl.BUTTON_X2] = 'x2',
|
||||
}
|
||||
|
||||
local function getMouseButtonId (value)
|
||||
return value and buttonIds[value] or value
|
||||
end
|
||||
|
||||
function Backend.show (layout)
|
||||
local input = layout.input
|
||||
|
||||
hook(layout, 'draw', function ()
|
||||
input:handleDisplay(layout)
|
||||
end, true)
|
||||
hook(layout, 'resize', function (width, height)
|
||||
return input:handleReshape(layout, width, height)
|
||||
end)
|
||||
hook(layout, 'mousepressed', function (x, y, button)
|
||||
return input:handlePressStart(layout, getMouseButtonId(button), x, y)
|
||||
end)
|
||||
hook(layout, 'mousereleased', function (x, y, button)
|
||||
return input:handlePressEnd(layout, getMouseButtonId(button), x, y)
|
||||
end)
|
||||
hook(layout, 'mousemoved', function (x, y, dx, dy)
|
||||
if isMouseDown() then
|
||||
return input:handlePressedMove(layout, x, y)
|
||||
else
|
||||
return input:handleMove(layout, x, y)
|
||||
end
|
||||
end)
|
||||
hook(layout, 'keypressed', function (key, scanCode, isRepeat)
|
||||
return input:handleKeyPress(layout, key, scanCode, Backend.getMousePosition())
|
||||
end)
|
||||
hook(layout, 'keyreleased', function (key, scanCode)
|
||||
return input:handleKeyRelease(layout, key, scanCode, Backend.getMousePosition())
|
||||
end)
|
||||
hook(layout, 'textinput', function (text)
|
||||
return input:handleTextInput(layout, text, Backend.getMousePosition())
|
||||
end)
|
||||
hook(layout, 'wheelmoved', function (x, y)
|
||||
return input:handleWheelMove(layout, x, y)
|
||||
end)
|
||||
end
|
||||
|
||||
function Backend.getWindowMaximized ()
|
||||
local flags = sdl.getWindowFlags(window)
|
||||
return bit.band(flags, sdl.WINDOW_MAXIMIZED) ~= 0
|
||||
end
|
||||
|
||||
function Backend.setWindowMaximized (maximized)
|
||||
if maximized then
|
||||
sdl.maximizeWindow(window)
|
||||
else
|
||||
sdl.restoreWindow(window)
|
||||
end
|
||||
end
|
||||
|
||||
function Backend.getWindowMinimized ()
|
||||
local flags = sdl.getWindowFlags(window)
|
||||
return bit.band(flags, sdl.WINDOW_MINIMIZED) ~= 0
|
||||
end
|
||||
|
||||
function Backend.setWindowMinimized (minimized)
|
||||
if minimized then
|
||||
sdl.minimizeWindow(window)
|
||||
else
|
||||
sdl.restoreWindow(window)
|
||||
end
|
||||
end
|
||||
|
||||
function Backend.getWindowBorderless ()
|
||||
local flags = sdl.getWindowFlags(window)
|
||||
return bit.band(flags, sdl.WINDOW_BORDERLESS) ~= 0
|
||||
end
|
||||
|
||||
function Backend.setWindowBorderless (borderless)
|
||||
return sdl.setWindowBordered(window, not borderless)
|
||||
end
|
||||
|
||||
function Backend.getWindowFullscreen ()
|
||||
local flags = sdl.getWindowFlags(window)
|
||||
return bit.band(flags, sdl.WINDOW_FULLSCREEN) ~= 0
|
||||
end
|
||||
|
||||
function Backend.setWindowFullscreen (fullscreen)
|
||||
return sdl.setWindowFullscreen(window, not not fullscreen)
|
||||
end
|
||||
|
||||
function Backend.getWindowGrab ()
|
||||
return sdl.getWindowGrab(window)
|
||||
end
|
||||
|
||||
function Backend.setWindowGrab (grab)
|
||||
return sdl.setWindowGrab(window, not not grab)
|
||||
end
|
||||
|
||||
local SDL2_image = ffi.load 'SDL2_image'
|
||||
|
||||
function Backend.setWindowIcon (icon)
|
||||
-- XXX: is it safe to free this?
|
||||
local surface = ffi.gc(SDL2_image.IMG_Load(icon), sdl.freeSurface)
|
||||
|
||||
if surface == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
sdl.setWindowIcon(window, surface)
|
||||
end
|
||||
|
||||
function Backend.getWindowMaxwidth ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMaximumSize(window, w, h)
|
||||
return w[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowMaxwidth (maxwidth)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMaximumSize(window, w, h)
|
||||
sdl.setWindowMaximumSize(window, maxwidth, h[0] or 16384)
|
||||
end
|
||||
|
||||
function Backend.getWindowMaxheight ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMaximumSize(window, w, h)
|
||||
return h[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowMaxheight (maxheight)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMaximumSize(window, w, h)
|
||||
sdl.setWindowMaximumSize(window, w[0] or 16384, maxheight)
|
||||
end
|
||||
|
||||
function Backend.getWindowMinwidth ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMinimumSize(window, w, h)
|
||||
return w[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowMinwidth (minwidth)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMinimumSize(window, w, h)
|
||||
sdl.setWindowMinimumSize(window, minwidth, h[0] or 0)
|
||||
end
|
||||
|
||||
function Backend.getWindowMinheight ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMinimumSize(window, w, h)
|
||||
return h[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowMinheight (minheight)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowMinimumSize(window, w, h)
|
||||
sdl.setWindowMinimumSize(window, w[0] or 0, minheight)
|
||||
end
|
||||
|
||||
function Backend.getWindowTop ()
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getWindowPosition(window, x, y)
|
||||
return y[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowTop (top)
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getWindowPosition(window, x, y)
|
||||
sdl.setWindowPosition(window, x[0] or 0, top)
|
||||
end
|
||||
|
||||
function Backend.getWindowLeft ()
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getWindowPosition(window, x, y)
|
||||
return x[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowLeft (left)
|
||||
local x, y = IntOut(), IntOut()
|
||||
sdl.getWindowPosition(window, x, y)
|
||||
sdl.setWindowPosition(window, left, y[0] or 0)
|
||||
end
|
||||
|
||||
function Backend.getWindowWidth ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowSize(window, w, h)
|
||||
return w[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowWidth (width)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowSize(window, w, h)
|
||||
sdl.setWindowSize(window, width, h[0] or 600)
|
||||
end
|
||||
|
||||
function Backend.getWindowHeight ()
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowSize(window, w, h)
|
||||
return h[0]
|
||||
end
|
||||
|
||||
function Backend.setWindowHeight (height)
|
||||
local w, h = IntOut(), IntOut()
|
||||
sdl.getWindowSize(window, w, h)
|
||||
sdl.setWindowSize(window, w[0] or 800, height)
|
||||
end
|
||||
|
||||
function Backend.getWindowTitle (title)
|
||||
return sdl.getWindowTitle(window)
|
||||
end
|
||||
|
||||
function Backend.setWindowTitle (title)
|
||||
sdl.setWindowTitle(window, title)
|
||||
end
|
||||
|
||||
|
||||
|
||||
return Backend
|
249
src/lib/luigi/backend/ffisdl/font.lua
Normal file
|
@ -0,0 +1,249 @@
|
|||
local REL = (...):gsub('[^.]*$', '')
|
||||
local APP_ROOT = rawget(_G, 'LUIGI_APP_ROOT') or ''
|
||||
|
||||
local ffi = require 'ffi'
|
||||
local sdl = require(REL .. 'sdl')
|
||||
|
||||
local SDL2_ttf = ffi.load 'SDL2_ttf'
|
||||
|
||||
local IntOut = ffi.typeof 'int[1]'
|
||||
|
||||
ffi.cdef [[
|
||||
|
||||
/* The internal structure containing font information */
|
||||
typedef struct _TTF_Font TTF_Font;
|
||||
|
||||
/* Initialize the TTF engine - returns 0 if successful, -1 on error */
|
||||
int TTF_Init(void);
|
||||
|
||||
/* Open a font file and create a font of the specified point size.
|
||||
* Some .fon fonts will have several sizes embedded in the file, so the
|
||||
* point size becomes the index of choosing which size. If the value
|
||||
* is too high, the last indexed size will be the default. */
|
||||
TTF_Font *TTF_OpenFont(const char *file, int ptsize);
|
||||
TTF_Font *TTF_OpenFontIndex(const char *file, int ptsize, long index);
|
||||
TTF_Font *TTF_OpenFontRW(SDL_RWops *src, int freesrc, int ptsize);
|
||||
TTF_Font *TTF_OpenFontIndexRW(SDL_RWops *src, int freesrc, int ptsize, long index);
|
||||
|
||||
/* Set and retrieve the font style
|
||||
#define TTF_STYLE_NORMAL 0x00
|
||||
#define TTF_STYLE_BOLD 0x01
|
||||
#define TTF_STYLE_ITALIC 0x02
|
||||
#define TTF_STYLE_UNDERLINE 0x04
|
||||
#define TTF_STYLE_STRIKETHROUGH 0x08
|
||||
*/
|
||||
|
||||
int TTF_GetFontStyle(const TTF_Font *font);
|
||||
void TTF_SetFontStyle(TTF_Font *font, int style);
|
||||
int TTF_GetFontOutline(const TTF_Font *font);
|
||||
void TTF_SetFontOutline(TTF_Font *font, int outline);
|
||||
|
||||
/* Set and retrieve FreeType hinter settings
|
||||
#define TTF_HINTING_NORMAL 0
|
||||
#define TTF_HINTING_LIGHT 1
|
||||
#define TTF_HINTING_MONO 2
|
||||
#define TTF_HINTING_NONE 3
|
||||
*/
|
||||
|
||||
int TTF_GetFontHinting(const TTF_Font *font);
|
||||
void TTF_SetFontHinting(TTF_Font *font, int hinting);
|
||||
|
||||
/* Get the total height of the font - usually equal to point size */
|
||||
int TTF_FontHeight(const TTF_Font *font);
|
||||
|
||||
/* Get the offset from the baseline to the top of the font
|
||||
This is a positive value, relative to the baseline.
|
||||
*/
|
||||
int TTF_FontAscent(const TTF_Font *font);
|
||||
|
||||
/* Get the offset from the baseline to the bottom of the font
|
||||
This is a negative value, relative to the baseline.
|
||||
*/
|
||||
int TTF_FontDescent(const TTF_Font *font);
|
||||
|
||||
/* Get the recommended spacing between lines of text for this font */
|
||||
int TTF_FontLineSkip(const TTF_Font *font);
|
||||
|
||||
/* Get/Set whether or not kerning is allowed for this font */
|
||||
int TTF_GetFontKerning(const TTF_Font *font);
|
||||
void TTF_SetFontKerning(TTF_Font *font, int allowed);
|
||||
|
||||
/* Get the number of faces of the font */
|
||||
long TTF_FontFaces(const TTF_Font *font);
|
||||
|
||||
/* Get the font face attributes, if any */
|
||||
int TTF_FontFaceIsFixedWidth(const TTF_Font *font);
|
||||
char *TTF_FontFaceFamilyName(const TTF_Font *font);
|
||||
char *TTF_FontFaceStyleName(const TTF_Font *font);
|
||||
|
||||
/* Check wether a glyph is provided by the font or not */
|
||||
int TTF_GlyphIsProvided(const TTF_Font *font, Uint16 ch);
|
||||
|
||||
/* Get the metrics (dimensions) of a glyph
|
||||
To understand what these metrics mean, here is a useful link:
|
||||
http://freetype.sourceforge.net/freetype2/docs/tutorial/step2.html
|
||||
*/
|
||||
int TTF_GlyphMetrics(TTF_Font *font, Uint16 ch,
|
||||
int *minx, int *maxx,
|
||||
int *miny, int *maxy, int *advance);
|
||||
|
||||
/* Get the dimensions of a rendered string of text */
|
||||
int TTF_SizeText(TTF_Font *font, const char *text, int *w, int *h);
|
||||
int TTF_SizeUTF8(TTF_Font *font, const char *text, int *w, int *h);
|
||||
int TTF_SizeUTF8_Wrapped(TTF_Font *font, const char *text, int wrapLength, int *w, int *h, int *lineCount);
|
||||
int TTF_SizeUNICODE(TTF_Font *font, const Uint16 *text, int *w, int *h);
|
||||
|
||||
/* Create an 8-bit palettized surface and render the given text at
|
||||
fast quality with the given font and color. The 0 pixel is the
|
||||
colorkey, giving a transparent background, and the 1 pixel is set
|
||||
to the text color.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderText_Solid(TTF_Font *font,
|
||||
const char *text, SDL_Color fg);
|
||||
SDL_Surface *TTF_RenderUTF8_Solid(TTF_Font *font,
|
||||
const char *text, SDL_Color fg);
|
||||
SDL_Surface *TTF_RenderUNICODE_Solid(TTF_Font *font,
|
||||
const Uint16 *text, SDL_Color fg);
|
||||
|
||||
/* Create an 8-bit palettized surface and render the given glyph at
|
||||
fast quality with the given font and color. The 0 pixel is the
|
||||
colorkey, giving a transparent background, and the 1 pixel is set
|
||||
to the text color. The glyph is rendered without any padding or
|
||||
centering in the X direction, and aligned normally in the Y direction.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderGlyph_Solid(TTF_Font *font,
|
||||
Uint16 ch, SDL_Color fg);
|
||||
|
||||
/* Create an 8-bit palettized surface and render the given text at
|
||||
high quality with the given font and colors. The 0 pixel is background,
|
||||
while other pixels have varying degrees of the foreground color.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderText_Shaded(TTF_Font *font,
|
||||
const char *text, SDL_Color fg, SDL_Color bg);
|
||||
SDL_Surface *TTF_RenderUTF8_Shaded(TTF_Font *font,
|
||||
const char *text, SDL_Color fg, SDL_Color bg);
|
||||
SDL_Surface *TTF_RenderUNICODE_Shaded(TTF_Font *font,
|
||||
const Uint16 *text, SDL_Color fg, SDL_Color bg);
|
||||
|
||||
/* Create an 8-bit palettized surface and render the given glyph at
|
||||
high quality with the given font and colors. The 0 pixel is background,
|
||||
while other pixels have varying degrees of the foreground color.
|
||||
The glyph is rendered without any padding or centering in the X
|
||||
direction, and aligned normally in the Y direction.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderGlyph_Shaded(TTF_Font *font,
|
||||
Uint16 ch, SDL_Color fg, SDL_Color bg);
|
||||
|
||||
/* Create a 32-bit ARGB surface and render the given text at high quality,
|
||||
using alpha blending to dither the font with the given color.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderText_Blended(TTF_Font *font,
|
||||
const char *text, SDL_Color fg);
|
||||
SDL_Surface *TTF_RenderUTF8_Blended(TTF_Font *font,
|
||||
const char *text, SDL_Color fg);
|
||||
SDL_Surface *TTF_RenderUNICODE_Blended(TTF_Font *font,
|
||||
const Uint16 *text, SDL_Color fg);
|
||||
|
||||
|
||||
/* Create a 32-bit ARGB surface and render the given text at high quality,
|
||||
using alpha blending to dither the font with the given color.
|
||||
Text is wrapped to multiple lines on line endings and on word boundaries
|
||||
if it extends beyond wrapLength in pixels.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderText_Blended_Wrapped(TTF_Font *font,
|
||||
const char *text, SDL_Color fg, Uint32 wrapLength);
|
||||
SDL_Surface *TTF_RenderUTF8_Blended_Wrapped(TTF_Font *font,
|
||||
const char *text, SDL_Color fg, Uint32 wrapLength);
|
||||
SDL_Surface *TTF_RenderUNICODE_Blended_Wrapped(TTF_Font *font,
|
||||
const Uint16 *text, SDL_Color fg, Uint32 wrapLength);
|
||||
|
||||
/* Create a 32-bit ARGB surface and render the given glyph at high quality,
|
||||
using alpha blending to dither the font with the given color.
|
||||
The glyph is rendered without any padding or centering in the X
|
||||
direction, and aligned normally in the Y direction.
|
||||
This function returns the new surface, or NULL if there was an error.
|
||||
*/
|
||||
SDL_Surface *TTF_RenderGlyph_Blended(TTF_Font *font,
|
||||
Uint16 ch, SDL_Color fg);
|
||||
|
||||
/* Close an opened font file */
|
||||
void TTF_CloseFont(TTF_Font *font);
|
||||
|
||||
/* De-initialize the TTF engine */
|
||||
void TTF_Quit(void);
|
||||
|
||||
/* Check if the TTF engine is initialized */
|
||||
int TTF_WasInit(void);
|
||||
|
||||
/* Get the kerning size of two glyphs */
|
||||
int TTF_GetFontKerningSize(TTF_Font *font, int prev_index, int index);
|
||||
]]
|
||||
|
||||
if SDL2_ttf.TTF_Init() ~= 0 then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
local Font = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
Font.SDL2_ttf = SDL2_ttf
|
||||
|
||||
local fontCache = {}
|
||||
|
||||
function Font:constructor (path, size)
|
||||
if not size then
|
||||
size = 12
|
||||
end
|
||||
if not path then
|
||||
path = REL:gsub('%.', '/') .. 'resource/DejaVuSans.ttf'
|
||||
end
|
||||
local key = path .. '_' .. size
|
||||
|
||||
if not fontCache[key] then
|
||||
local font = SDL2_ttf.TTF_OpenFont(APP_ROOT .. path, size)
|
||||
|
||||
if font == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
fontCache[key] = font
|
||||
end
|
||||
|
||||
self.sdlFont = fontCache[key]
|
||||
end
|
||||
|
||||
function Font:setAlignment (align)
|
||||
self.align = align
|
||||
end
|
||||
|
||||
function Font:setWidth (width)
|
||||
self.width = width
|
||||
end
|
||||
|
||||
function Font:getLineHeight ()
|
||||
return SDL2_ttf.TTF_FontHeight(self.sdlFont)
|
||||
end
|
||||
|
||||
function Font:getAscender ()
|
||||
return SDL2_ttf.TTF_FontAscent(self.sdlFont)
|
||||
end
|
||||
|
||||
function Font:getDescender ()
|
||||
return SDL2_ttf.TTF_FontDescent(self.sdlFont)
|
||||
end
|
||||
|
||||
function Font:getAdvance (text)
|
||||
local w, h = IntOut(), IntOut()
|
||||
SDL2_ttf.TTF_SizeUTF8(self.sdlFont, text, w, h)
|
||||
return w[0]
|
||||
end
|
||||
|
||||
return Font
|
46
src/lib/luigi/backend/ffisdl/image.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
local REL = (...):gsub('[^.]*$', '')
|
||||
local APP_ROOT = rawget(_G, 'LUIGI_APP_ROOT') or ''
|
||||
|
||||
local ffi = require 'ffi'
|
||||
local sdl = require(REL .. 'sdl')
|
||||
|
||||
local SDL2_image = ffi.load 'SDL2_image'
|
||||
|
||||
ffi.cdef [[ SDL_Surface *IMG_Load(const char *file); ]]
|
||||
|
||||
local Image = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
function Image:constructor (renderer, path)
|
||||
self.sdlRenderer = renderer
|
||||
self.sdlSurface = ffi.gc(
|
||||
SDL2_image.IMG_Load(APP_ROOT .. path),
|
||||
sdl.freeSurface)
|
||||
|
||||
if self.sdlSurface == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
self.sdlTexture = ffi.gc(
|
||||
sdl.createTextureFromSurface(renderer, self.sdlSurface),
|
||||
sdl.destroyTexture)
|
||||
|
||||
if self.sdlTexture == nil then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
self.width = self.sdlSurface.w
|
||||
self.height = self.sdlSurface.h
|
||||
end
|
||||
|
||||
function Image:getWidth ()
|
||||
return self.width
|
||||
end
|
||||
|
||||
function Image:getHeight ()
|
||||
return self.height
|
||||
end
|
||||
|
||||
return Image
|
494
src/lib/luigi/backend/ffisdl/keyboard.lua
Normal file
|
@ -0,0 +1,494 @@
|
|||
local REL = (...):gsub('[^.]*$', '')
|
||||
|
||||
local sdl = require(REL .. 'sdl')
|
||||
|
||||
local Keyboard = {
|
||||
scancodeByString = {},
|
||||
stringByScancode = {},
|
||||
keycodeByString = {},
|
||||
stringByKeycode = {},
|
||||
}
|
||||
|
||||
local function registerScancodes (registry)
|
||||
for _, entry in ipairs(registry) do
|
||||
Keyboard.scancodeByString[entry[1]] = entry[2]
|
||||
Keyboard.stringByScancode[entry[2]] = entry[1]
|
||||
end
|
||||
end
|
||||
|
||||
local function registerKeycodes (registry)
|
||||
for _, entry in ipairs(registry) do
|
||||
Keyboard.keycodeByString[entry[1]] = entry[2]
|
||||
Keyboard.stringByKeycode[entry[2]] = entry[1]
|
||||
end
|
||||
end
|
||||
|
||||
registerScancodes {
|
||||
{ "unknown", sdl.SCANCODE_UNKNOWN },
|
||||
{ "a", sdl.SCANCODE_A },
|
||||
{ "b", sdl.SCANCODE_B },
|
||||
{ "c", sdl.SCANCODE_C },
|
||||
{ "d", sdl.SCANCODE_D },
|
||||
{ "e", sdl.SCANCODE_E },
|
||||
{ "f", sdl.SCANCODE_F },
|
||||
{ "g", sdl.SCANCODE_G },
|
||||
{ "h", sdl.SCANCODE_H },
|
||||
{ "i", sdl.SCANCODE_I },
|
||||
{ "j", sdl.SCANCODE_J },
|
||||
{ "k", sdl.SCANCODE_K },
|
||||
{ "l", sdl.SCANCODE_L },
|
||||
{ "m", sdl.SCANCODE_M },
|
||||
{ "n", sdl.SCANCODE_N },
|
||||
{ "o", sdl.SCANCODE_O },
|
||||
{ "p", sdl.SCANCODE_P },
|
||||
{ "q", sdl.SCANCODE_Q },
|
||||
{ "r", sdl.SCANCODE_R },
|
||||
{ "s", sdl.SCANCODE_S },
|
||||
{ "t", sdl.SCANCODE_T },
|
||||
{ "u", sdl.SCANCODE_U },
|
||||
{ "v", sdl.SCANCODE_V },
|
||||
{ "w", sdl.SCANCODE_W },
|
||||
{ "x", sdl.SCANCODE_X },
|
||||
{ "y", sdl.SCANCODE_Y },
|
||||
{ "z", sdl.SCANCODE_Z },
|
||||
|
||||
{ "1", sdl.SCANCODE_1 },
|
||||
{ "2", sdl.SCANCODE_2 },
|
||||
{ "3", sdl.SCANCODE_3 },
|
||||
{ "4", sdl.SCANCODE_4 },
|
||||
{ "5", sdl.SCANCODE_5 },
|
||||
{ "6", sdl.SCANCODE_6 },
|
||||
{ "7", sdl.SCANCODE_7 },
|
||||
{ "8", sdl.SCANCODE_8 },
|
||||
{ "9", sdl.SCANCODE_9 },
|
||||
{ "0", sdl.SCANCODE_0 },
|
||||
|
||||
{ "return", sdl.SCANCODE_RETURN },
|
||||
{ "escape", sdl.SCANCODE_ESCAPE },
|
||||
{ "backspace", sdl.SCANCODE_BACKSPACE },
|
||||
{ "tab", sdl.SCANCODE_TAB },
|
||||
{ "space", sdl.SCANCODE_SPACE },
|
||||
|
||||
{ "-", sdl.SCANCODE_MINUS },
|
||||
{ "=", sdl.SCANCODE_EQUALS },
|
||||
{ "[", sdl.SCANCODE_LEFTBRACKET },
|
||||
{ "]", sdl.SCANCODE_RIGHTBRACKET },
|
||||
{ "\\", sdl.SCANCODE_BACKSLASH },
|
||||
{ "nonus#", sdl.SCANCODE_NONUSHASH },
|
||||
{ ";", sdl.SCANCODE_SEMICOLON },
|
||||
{ "'", sdl.SCANCODE_APOSTROPHE },
|
||||
{ "`", sdl.SCANCODE_GRAVE },
|
||||
{ ",", sdl.SCANCODE_COMMA },
|
||||
{ ".", sdl.SCANCODE_PERIOD },
|
||||
{ "/", sdl.SCANCODE_SLASH },
|
||||
|
||||
{ "capslock", sdl.SCANCODE_CAPSLOCK },
|
||||
|
||||
{ "f1", sdl.SCANCODE_F1 },
|
||||
{ "f2", sdl.SCANCODE_F2 },
|
||||
{ "f3", sdl.SCANCODE_F3 },
|
||||
{ "f4", sdl.SCANCODE_F4 },
|
||||
{ "f5", sdl.SCANCODE_F5 },
|
||||
{ "f6", sdl.SCANCODE_F6 },
|
||||
{ "f7", sdl.SCANCODE_F7 },
|
||||
{ "f8", sdl.SCANCODE_F8 },
|
||||
{ "f9", sdl.SCANCODE_F9 },
|
||||
{ "f10", sdl.SCANCODE_F10 },
|
||||
{ "f11", sdl.SCANCODE_F11 },
|
||||
{ "f12", sdl.SCANCODE_F12 },
|
||||
|
||||
{ "printscreen", sdl.SCANCODE_PRINTSCREEN },
|
||||
{ "scrolllock", sdl.SCANCODE_SCROLLLOCK },
|
||||
{ "pause", sdl.SCANCODE_PAUSE },
|
||||
{ "insert", sdl.SCANCODE_INSERT },
|
||||
{ "home", sdl.SCANCODE_HOME },
|
||||
{ "pageup", sdl.SCANCODE_PAGEUP },
|
||||
{ "delete", sdl.SCANCODE_DELETE },
|
||||
{ "end", sdl.SCANCODE_END },
|
||||
{ "pagedown", sdl.SCANCODE_PAGEDOWN },
|
||||
{ "right", sdl.SCANCODE_RIGHT },
|
||||
{ "left", sdl.SCANCODE_LEFT },
|
||||
{ "down", sdl.SCANCODE_DOWN },
|
||||
{ "up", sdl.SCANCODE_UP },
|
||||
|
||||
{ "numlock", sdl.SCANCODE_NUMLOCKCLEAR },
|
||||
{ "kp/", sdl.SCANCODE_KP_DIVIDE },
|
||||
{ "kp*", sdl.SCANCODE_KP_MULTIPLY },
|
||||
{ "kp-", sdl.SCANCODE_KP_MINUS },
|
||||
{ "kp+", sdl.SCANCODE_KP_PLUS },
|
||||
{ "kpenter", sdl.SCANCODE_KP_ENTER },
|
||||
{ "kp1", sdl.SCANCODE_KP_1 },
|
||||
{ "kp2", sdl.SCANCODE_KP_2 },
|
||||
{ "kp3", sdl.SCANCODE_KP_3 },
|
||||
{ "kp4", sdl.SCANCODE_KP_4 },
|
||||
{ "kp5", sdl.SCANCODE_KP_5 },
|
||||
{ "kp6", sdl.SCANCODE_KP_6 },
|
||||
{ "kp7", sdl.SCANCODE_KP_7 },
|
||||
{ "kp8", sdl.SCANCODE_KP_8 },
|
||||
{ "kp9", sdl.SCANCODE_KP_9 },
|
||||
{ "kp0", sdl.SCANCODE_KP_0 },
|
||||
{ "kp.", sdl.SCANCODE_KP_PERIOD },
|
||||
|
||||
{ "nonusbackslash", sdl.SCANCODE_NONUSBACKSLASH },
|
||||
{ "application", sdl.SCANCODE_APPLICATION },
|
||||
{ "power", sdl.SCANCODE_POWER },
|
||||
{ "=", sdl.SCANCODE_KP_EQUALS },
|
||||
{ "f13", sdl.SCANCODE_F13 },
|
||||
{ "f14", sdl.SCANCODE_F14 },
|
||||
{ "f15", sdl.SCANCODE_F15 },
|
||||
{ "f16", sdl.SCANCODE_F16 },
|
||||
{ "f17", sdl.SCANCODE_F17 },
|
||||
{ "f18", sdl.SCANCODE_F18 },
|
||||
{ "f19", sdl.SCANCODE_F19 },
|
||||
{ "f20", sdl.SCANCODE_F20 },
|
||||
{ "f21", sdl.SCANCODE_F21 },
|
||||
{ "f22", sdl.SCANCODE_F22 },
|
||||
{ "f23", sdl.SCANCODE_F23 },
|
||||
{ "f24", sdl.SCANCODE_F24 },
|
||||
{ "execute", sdl.SCANCODE_EXECUTE },
|
||||
{ "help", sdl.SCANCODE_HELP },
|
||||
{ "menu", sdl.SCANCODE_MENU },
|
||||
{ "select", sdl.SCANCODE_SELECT },
|
||||
{ "stop", sdl.SCANCODE_STOP },
|
||||
{ "again", sdl.SCANCODE_AGAIN },
|
||||
{ "undo", sdl.SCANCODE_UNDO },
|
||||
{ "cut", sdl.SCANCODE_CUT },
|
||||
{ "copy", sdl.SCANCODE_COPY },
|
||||
{ "paste", sdl.SCANCODE_PASTE },
|
||||
{ "find", sdl.SCANCODE_FIND },
|
||||
{ "mute", sdl.SCANCODE_MUTE },
|
||||
{ "volumeup", sdl.SCANCODE_VOLUMEUP },
|
||||
{ "volumedown", sdl.SCANCODE_VOLUMEDOWN },
|
||||
{ "kp,", sdl.SCANCODE_KP_COMMA },
|
||||
{ "kp=400", sdl.SCANCODE_KP_EQUALSAS400 },
|
||||
|
||||
{ "international1", sdl.SCANCODE_INTERNATIONAL1 },
|
||||
{ "international2", sdl.SCANCODE_INTERNATIONAL2 },
|
||||
{ "international3", sdl.SCANCODE_INTERNATIONAL3 },
|
||||
{ "international4", sdl.SCANCODE_INTERNATIONAL4 },
|
||||
{ "international5", sdl.SCANCODE_INTERNATIONAL5 },
|
||||
{ "international6", sdl.SCANCODE_INTERNATIONAL6 },
|
||||
{ "international7", sdl.SCANCODE_INTERNATIONAL7 },
|
||||
{ "international8", sdl.SCANCODE_INTERNATIONAL8 },
|
||||
{ "international9", sdl.SCANCODE_INTERNATIONAL9 },
|
||||
{ "lang1", sdl.SCANCODE_LANG1 },
|
||||
{ "lang2", sdl.SCANCODE_LANG2 },
|
||||
{ "lang3", sdl.SCANCODE_LANG3 },
|
||||
{ "lang4", sdl.SCANCODE_LANG4 },
|
||||
{ "lang5", sdl.SCANCODE_LANG5 },
|
||||
{ "lang6", sdl.SCANCODE_LANG6 },
|
||||
{ "lang7", sdl.SCANCODE_LANG7 },
|
||||
{ "lang8", sdl.SCANCODE_LANG8 },
|
||||
{ "lang9", sdl.SCANCODE_LANG9 },
|
||||
|
||||
{ "alterase", sdl.SCANCODE_ALTERASE },
|
||||
{ "sysreq", sdl.SCANCODE_SYSREQ },
|
||||
{ "cancel", sdl.SCANCODE_CANCEL },
|
||||
{ "clear", sdl.SCANCODE_CLEAR },
|
||||
{ "prior", sdl.SCANCODE_PRIOR },
|
||||
{ "return2", sdl.SCANCODE_RETURN2 },
|
||||
{ "separator", sdl.SCANCODE_SEPARATOR },
|
||||
{ "out", sdl.SCANCODE_OUT },
|
||||
{ "oper", sdl.SCANCODE_OPER },
|
||||
{ "clearagain", sdl.SCANCODE_CLEARAGAIN },
|
||||
{ "crsel", sdl.SCANCODE_CRSEL },
|
||||
{ "exsel", sdl.SCANCODE_EXSEL },
|
||||
|
||||
{ "kp00", sdl.SCANCODE_KP_00 },
|
||||
{ "kp000", sdl.SCANCODE_KP_000 },
|
||||
{ "thsousandsseparator", sdl.SCANCODE_THOUSANDSSEPARATOR },
|
||||
{ "decimalseparator", sdl.SCANCODE_DECIMALSEPARATOR },
|
||||
{ "currencyunit", sdl.SCANCODE_CURRENCYUNIT },
|
||||
{ "currencysubunit", sdl.SCANCODE_CURRENCYSUBUNIT },
|
||||
{ "kp(", sdl.SCANCODE_KP_LEFTPAREN },
|
||||
{ "kp)", sdl.SCANCODE_KP_RIGHTPAREN },
|
||||
{ "kp{", sdl.SCANCODE_KP_LEFTBRACE },
|
||||
{ "kp}", sdl.SCANCODE_KP_RIGHTBRACE },
|
||||
{ "kptab", sdl.SCANCODE_KP_TAB },
|
||||
{ "kpbackspace", sdl.SCANCODE_KP_BACKSPACE },
|
||||
{ "kpa", sdl.SCANCODE_KP_A },
|
||||
{ "kpb", sdl.SCANCODE_KP_B },
|
||||
{ "kpc", sdl.SCANCODE_KP_C },
|
||||
{ "kpd", sdl.SCANCODE_KP_D },
|
||||
{ "kpe", sdl.SCANCODE_KP_E },
|
||||
{ "kpf", sdl.SCANCODE_KP_F },
|
||||
{ "kpxor", sdl.SCANCODE_KP_XOR },
|
||||
{ "kpower", sdl.SCANCODE_KP_POWER },
|
||||
{ "kp%", sdl.SCANCODE_KP_PERCENT },
|
||||
{ "kp<", sdl.SCANCODE_KP_LESS },
|
||||
{ "kp>", sdl.SCANCODE_KP_GREATER },
|
||||
{ "kp&", sdl.SCANCODE_KP_AMPERSAND },
|
||||
{ "kp&&", sdl.SCANCODE_KP_DBLAMPERSAND },
|
||||
{ "kp|", sdl.SCANCODE_KP_VERTICALBAR },
|
||||
{ "kp||", sdl.SCANCODE_KP_DBLVERTICALBAR },
|
||||
{ "kp:", sdl.SCANCODE_KP_COLON },
|
||||
{ "kp#", sdl.SCANCODE_KP_HASH },
|
||||
{ "kp ", sdl.SCANCODE_KP_SPACE },
|
||||
{ "kp@", sdl.SCANCODE_KP_AT },
|
||||
{ "kp!", sdl.SCANCODE_KP_EXCLAM },
|
||||
{ "kpmemstore", sdl.SCANCODE_KP_MEMSTORE },
|
||||
{ "kpmemrecall", sdl.SCANCODE_KP_MEMRECALL },
|
||||
{ "kpmemclear", sdl.SCANCODE_KP_MEMCLEAR },
|
||||
{ "kpmem+", sdl.SCANCODE_KP_MEMADD },
|
||||
{ "kpmem-", sdl.SCANCODE_KP_MEMSUBTRACT },
|
||||
{ "kpmem*", sdl.SCANCODE_KP_MEMMULTIPLY },
|
||||
{ "kpmem/", sdl.SCANCODE_KP_MEMDIVIDE },
|
||||
{ "kp+-", sdl.SCANCODE_KP_PLUSMINUS },
|
||||
{ "kpclear", sdl.SCANCODE_KP_CLEAR },
|
||||
{ "kpclearentry", sdl.SCANCODE_KP_CLEARENTRY },
|
||||
{ "kpbinary", sdl.SCANCODE_KP_BINARY },
|
||||
{ "kpoctal", sdl.SCANCODE_KP_OCTAL },
|
||||
{ "kpdecimal", sdl.SCANCODE_KP_DECIMAL },
|
||||
{ "kphex", sdl.SCANCODE_KP_HEXADECIMAL },
|
||||
|
||||
{ "lctrl", sdl.SCANCODE_LCTRL },
|
||||
{ "lshift", sdl.SCANCODE_LSHIFT },
|
||||
{ "lalt", sdl.SCANCODE_LALT },
|
||||
{ "lgui", sdl.SCANCODE_LGUI },
|
||||
{ "rctrl", sdl.SCANCODE_RCTRL },
|
||||
{ "rshift", sdl.SCANCODE_RSHIFT },
|
||||
{ "ralt", sdl.SCANCODE_RALT },
|
||||
{ "rgui", sdl.SCANCODE_RGUI },
|
||||
|
||||
{ "mode", sdl.SCANCODE_MODE },
|
||||
|
||||
{ "audionext", sdl.SCANCODE_AUDIONEXT },
|
||||
{ "audioprev", sdl.SCANCODE_AUDIOPREV },
|
||||
{ "audiostop", sdl.SCANCODE_AUDIOSTOP },
|
||||
{ "audioplay", sdl.SCANCODE_AUDIOPLAY },
|
||||
{ "audiomute", sdl.SCANCODE_AUDIOMUTE },
|
||||
{ "mediaselect", sdl.SCANCODE_MEDIASELECT },
|
||||
{ "www", sdl.SCANCODE_WWW },
|
||||
{ "mail", sdl.SCANCODE_MAIL },
|
||||
{ "calculator", sdl.SCANCODE_CALCULATOR },
|
||||
{ "computer", sdl.SCANCODE_COMPUTER },
|
||||
{ "acsearch", sdl.SCANCODE_AC_SEARCH },
|
||||
{ "achome", sdl.SCANCODE_AC_HOME },
|
||||
{ "acback", sdl.SCANCODE_AC_BACK },
|
||||
{ "acforward", sdl.SCANCODE_AC_FORWARD },
|
||||
{ "acstop", sdl.SCANCODE_AC_STOP },
|
||||
{ "acrefresh", sdl.SCANCODE_AC_REFRESH },
|
||||
{ "acbookmarks", sdl.SCANCODE_AC_BOOKMARKS },
|
||||
|
||||
{ "brightnessdown", sdl.SCANCODE_BRIGHTNESSDOWN },
|
||||
{ "brightnessup", sdl.SCANCODE_BRIGHTNESSUP },
|
||||
{ "displayswitch", sdl.SCANCODE_DISPLAYSWITCH },
|
||||
{ "kbdillumtoggle", sdl.SCANCODE_KBDILLUMTOGGLE },
|
||||
{ "kbdillumdown", sdl.SCANCODE_KBDILLUMDOWN },
|
||||
{ "kbdillumup", sdl.SCANCODE_KBDILLUMUP },
|
||||
{ "eject", sdl.SCANCODE_EJECT },
|
||||
{ "sleep", sdl.SCANCODE_SLEEP },
|
||||
|
||||
{ "app1", sdl.SCANCODE_APP1 },
|
||||
{ "app2", sdl.SCANCODE_APP2 },
|
||||
}
|
||||
|
||||
registerKeycodes {
|
||||
{ "unknown", sdl.C.SDLK_UNKNOWN },
|
||||
|
||||
{ "return", sdl.C.SDLK_RETURN },
|
||||
{ "escape", sdl.C.SDLK_ESCAPE },
|
||||
{ "backspace", sdl.C.SDLK_BACKSPACE },
|
||||
{ "tab", sdl.C.SDLK_TAB },
|
||||
{ "space", sdl.C.SDLK_SPACE },
|
||||
{ "!", sdl.C.SDLK_EXCLAIM },
|
||||
{ "\"", sdl.C.SDLK_QUOTEDBL },
|
||||
{ "#", sdl.C.SDLK_HASH },
|
||||
{ "%", sdl.C.SDLK_PERCENT },
|
||||
{ "$", sdl.C.SDLK_DOLLAR },
|
||||
{ "&", sdl.C.SDLK_AMPERSAND },
|
||||
{ "'", sdl.C.SDLK_QUOTE },
|
||||
{ "(", sdl.C.SDLK_LEFTPAREN },
|
||||
{ ")", sdl.C.SDLK_RIGHTPAREN },
|
||||
{ "*", sdl.C.SDLK_ASTERISK },
|
||||
{ "+", sdl.C.SDLK_PLUS },
|
||||
{ ",", sdl.C.SDLK_COMMA },
|
||||
{ "-", sdl.C.SDLK_MINUS },
|
||||
{ ".", sdl.C.SDLK_PERIOD },
|
||||
{ "/", sdl.C.SDLK_SLASH },
|
||||
{ "0", sdl.C.SDLK_0 },
|
||||
{ "1", sdl.C.SDLK_1 },
|
||||
{ "2", sdl.C.SDLK_2 },
|
||||
{ "3", sdl.C.SDLK_3 },
|
||||
{ "4", sdl.C.SDLK_4 },
|
||||
{ "5", sdl.C.SDLK_5 },
|
||||
{ "6", sdl.C.SDLK_6 },
|
||||
{ "7", sdl.C.SDLK_7 },
|
||||
{ "8", sdl.C.SDLK_8 },
|
||||
{ "9", sdl.C.SDLK_9 },
|
||||
{ ":", sdl.C.SDLK_COLON },
|
||||
{ ";", sdl.C.SDLK_SEMICOLON },
|
||||
{ "<", sdl.C.SDLK_LESS },
|
||||
{ "=", sdl.C.SDLK_EQUALS },
|
||||
{ ">", sdl.C.SDLK_GREATER },
|
||||
{ "?", sdl.C.SDLK_QUESTION },
|
||||
{ "@", sdl.C.SDLK_AT },
|
||||
|
||||
{ "[", sdl.C.SDLK_LEFTBRACKET },
|
||||
{ "\\", sdl.C.SDLK_BACKSLASH },
|
||||
{ "]", sdl.C.SDLK_RIGHTBRACKET },
|
||||
{ "^", sdl.C.SDLK_CARET },
|
||||
{ "_", sdl.C.SDLK_UNDERSCORE },
|
||||
{ "`", sdl.C.SDLK_BACKQUOTE },
|
||||
{ "a", sdl.C.SDLK_a },
|
||||
{ "b", sdl.C.SDLK_b },
|
||||
{ "c", sdl.C.SDLK_c },
|
||||
{ "d", sdl.C.SDLK_d },
|
||||
{ "e", sdl.C.SDLK_e },
|
||||
{ "f", sdl.C.SDLK_f },
|
||||
{ "g", sdl.C.SDLK_g },
|
||||
{ "h", sdl.C.SDLK_h },
|
||||
{ "i", sdl.C.SDLK_i },
|
||||
{ "j", sdl.C.SDLK_j },
|
||||
{ "k", sdl.C.SDLK_k },
|
||||
{ "l", sdl.C.SDLK_l },
|
||||
{ "m", sdl.C.SDLK_m },
|
||||
{ "n", sdl.C.SDLK_n },
|
||||
{ "o", sdl.C.SDLK_o },
|
||||
{ "p", sdl.C.SDLK_p },
|
||||
{ "q", sdl.C.SDLK_q },
|
||||
{ "r", sdl.C.SDLK_r },
|
||||
{ "s", sdl.C.SDLK_s },
|
||||
{ "t", sdl.C.SDLK_t },
|
||||
{ "u", sdl.C.SDLK_u },
|
||||
{ "v", sdl.C.SDLK_v },
|
||||
{ "w", sdl.C.SDLK_w },
|
||||
{ "x", sdl.C.SDLK_x },
|
||||
{ "y", sdl.C.SDLK_y },
|
||||
{ "z", sdl.C.SDLK_z },
|
||||
|
||||
{ "capslock", sdl.C.SDLK_CAPSLOCK },
|
||||
|
||||
{ "f1", sdl.C.SDLK_F1 },
|
||||
{ "f2", sdl.C.SDLK_F2 },
|
||||
{ "f3", sdl.C.SDLK_F3 },
|
||||
{ "f4", sdl.C.SDLK_F4 },
|
||||
{ "f5", sdl.C.SDLK_F5 },
|
||||
{ "f6", sdl.C.SDLK_F6 },
|
||||
{ "f7", sdl.C.SDLK_F7 },
|
||||
{ "f8", sdl.C.SDLK_F8 },
|
||||
{ "f9", sdl.C.SDLK_F9 },
|
||||
{ "f10", sdl.C.SDLK_F10 },
|
||||
{ "f11", sdl.C.SDLK_F11 },
|
||||
{ "f12", sdl.C.SDLK_F12 },
|
||||
|
||||
{ "printscreen", sdl.C.SDLK_PRINTSCREEN },
|
||||
{ "scrolllock", sdl.C.SDLK_SCROLLLOCK },
|
||||
{ "pause", sdl.C.SDLK_PAUSE },
|
||||
{ "insert", sdl.C.SDLK_INSERT },
|
||||
{ "home", sdl.C.SDLK_HOME },
|
||||
{ "pageup", sdl.C.SDLK_PAGEUP },
|
||||
{ "delete", sdl.C.SDLK_DELETE },
|
||||
{ "end", sdl.C.SDLK_END },
|
||||
{ "pagedown", sdl.C.SDLK_PAGEDOWN },
|
||||
{ "right", sdl.C.SDLK_RIGHT },
|
||||
{ "left", sdl.C.SDLK_LEFT },
|
||||
{ "down", sdl.C.SDLK_DOWN },
|
||||
{ "up", sdl.C.SDLK_UP },
|
||||
|
||||
{ "numlock", sdl.C.SDLK_NUMLOCKCLEAR },
|
||||
{ "kp/", sdl.C.SDLK_KP_DIVIDE },
|
||||
{ "kp*", sdl.C.SDLK_KP_MULTIPLY },
|
||||
{ "kp-", sdl.C.SDLK_KP_MINUS },
|
||||
{ "kp+", sdl.C.SDLK_KP_PLUS },
|
||||
{ "kpenter", sdl.C.SDLK_KP_ENTER },
|
||||
{ "kp0", sdl.C.SDLK_KP_0 },
|
||||
{ "kp1", sdl.C.SDLK_KP_1 },
|
||||
{ "kp2", sdl.C.SDLK_KP_2 },
|
||||
{ "kp3", sdl.C.SDLK_KP_3 },
|
||||
{ "kp4", sdl.C.SDLK_KP_4 },
|
||||
{ "kp5", sdl.C.SDLK_KP_5 },
|
||||
{ "kp6", sdl.C.SDLK_KP_6 },
|
||||
{ "kp7", sdl.C.SDLK_KP_7 },
|
||||
{ "kp8", sdl.C.SDLK_KP_8 },
|
||||
{ "kp9", sdl.C.SDLK_KP_9 },
|
||||
{ "kp.", sdl.C.SDLK_KP_PERIOD },
|
||||
{ "kp,", sdl.C.SDLK_KP_COMMA },
|
||||
{ "kp=", sdl.C.SDLK_KP_EQUALS },
|
||||
|
||||
{ "application", sdl.C.SDLK_APPLICATION },
|
||||
{ "power", sdl.C.SDLK_POWER },
|
||||
{ "f13", sdl.C.SDLK_F13 },
|
||||
{ "f14", sdl.C.SDLK_F14 },
|
||||
{ "f15", sdl.C.SDLK_F15 },
|
||||
{ "f16", sdl.C.SDLK_F16 },
|
||||
{ "f17", sdl.C.SDLK_F17 },
|
||||
{ "f18", sdl.C.SDLK_F18 },
|
||||
{ "f19", sdl.C.SDLK_F19 },
|
||||
{ "f20", sdl.C.SDLK_F20 },
|
||||
{ "f21", sdl.C.SDLK_F21 },
|
||||
{ "f22", sdl.C.SDLK_F22 },
|
||||
{ "f23", sdl.C.SDLK_F23 },
|
||||
{ "f24", sdl.C.SDLK_F24 },
|
||||
{ "execute", sdl.C.SDLK_EXECUTE },
|
||||
{ "help", sdl.C.SDLK_HELP },
|
||||
{ "menu", sdl.C.SDLK_MENU },
|
||||
{ "select", sdl.C.SDLK_SELECT },
|
||||
{ "stop", sdl.C.SDLK_STOP },
|
||||
{ "again", sdl.C.SDLK_AGAIN },
|
||||
{ "undo", sdl.C.SDLK_UNDO },
|
||||
{ "cut", sdl.C.SDLK_CUT },
|
||||
{ "copy", sdl.C.SDLK_COPY },
|
||||
{ "paste", sdl.C.SDLK_PASTE },
|
||||
{ "find", sdl.C.SDLK_FIND },
|
||||
{ "mute", sdl.C.SDLK_MUTE },
|
||||
{ "volumeup", sdl.C.SDLK_VOLUMEUP },
|
||||
{ "volumedown", sdl.C.SDLK_VOLUMEDOWN },
|
||||
|
||||
{ "alterase", sdl.C.SDLK_ALTERASE },
|
||||
{ "sysreq", sdl.C.SDLK_SYSREQ },
|
||||
{ "cancel", sdl.C.SDLK_CANCEL },
|
||||
{ "clear", sdl.C.SDLK_CLEAR },
|
||||
{ "prior", sdl.C.SDLK_PRIOR },
|
||||
{ "return2", sdl.C.SDLK_RETURN2 },
|
||||
{ "separator", sdl.C.SDLK_SEPARATOR },
|
||||
{ "out", sdl.C.SDLK_OUT },
|
||||
{ "oper", sdl.C.SDLK_OPER },
|
||||
{ "clearagain", sdl.C.SDLK_CLEARAGAIN },
|
||||
|
||||
{ "thsousandsseparator", sdl.C.SDLK_THOUSANDSSEPARATOR },
|
||||
{ "decimalseparator", sdl.C.SDLK_DECIMALSEPARATOR },
|
||||
{ "currencyunit", sdl.C.SDLK_CURRENCYUNIT },
|
||||
{ "currencysubunit", sdl.C.SDLK_CURRENCYSUBUNIT },
|
||||
|
||||
{ "lctrl", sdl.C.SDLK_LCTRL },
|
||||
{ "lshift", sdl.C.SDLK_LSHIFT },
|
||||
{ "lalt", sdl.C.SDLK_LALT },
|
||||
{ "lgui", sdl.C.SDLK_LGUI },
|
||||
{ "rctrl", sdl.C.SDLK_RCTRL },
|
||||
{ "rshift", sdl.C.SDLK_RSHIFT },
|
||||
{ "ralt", sdl.C.SDLK_RALT },
|
||||
{ "rgui", sdl.C.SDLK_RGUI },
|
||||
|
||||
{ "mode", sdl.C.SDLK_MODE },
|
||||
|
||||
{ "audionext", sdl.C.SDLK_AUDIONEXT },
|
||||
{ "audioprev", sdl.C.SDLK_AUDIOPREV },
|
||||
{ "audiostop", sdl.C.SDLK_AUDIOSTOP },
|
||||
{ "audioplay", sdl.C.SDLK_AUDIOPLAY },
|
||||
{ "audiomute", sdl.C.SDLK_AUDIOMUTE },
|
||||
{ "mediaselect", sdl.C.SDLK_MEDIASELECT },
|
||||
{ "www", sdl.C.SDLK_WWW },
|
||||
{ "mail", sdl.C.SDLK_MAIL },
|
||||
{ "calculator", sdl.C.SDLK_CALCULATOR },
|
||||
{ "computer", sdl.C.SDLK_COMPUTER },
|
||||
|
||||
{ "appsearch", sdl.C.SDLK_AC_SEARCH },
|
||||
{ "apphome", sdl.C.SDLK_AC_HOME },
|
||||
{ "appback", sdl.C.SDLK_AC_BACK },
|
||||
{ "appforward", sdl.C.SDLK_AC_FORWARD },
|
||||
{ "appstop", sdl.C.SDLK_AC_STOP },
|
||||
{ "apprefresh", sdl.C.SDLK_AC_REFRESH },
|
||||
{ "appbookmarks", sdl.C.SDLK_AC_BOOKMARKS },
|
||||
|
||||
{ "brightnessdown", sdl.C.SDLK_BRIGHTNESSDOWN },
|
||||
{ "brightnessup", sdl.C.SDLK_BRIGHTNESSUP },
|
||||
{ "displayswitch", sdl.C.SDLK_DISPLAYSWITCH },
|
||||
{ "kbdillumtoggle", sdl.C.SDLK_KBDILLUMTOGGLE },
|
||||
{ "kbdillumdown", sdl.C.SDLK_KBDILLUMDOWN },
|
||||
{ "kbdillumup", sdl.C.SDLK_KBDILLUMUP },
|
||||
{ "eject", sdl.C.SDLK_EJECT },
|
||||
{ "sleep", sdl.C.SDLK_SLEEP },
|
||||
}
|
||||
|
||||
return Keyboard
|
BIN
src/lib/luigi/backend/ffisdl/resource/DejaVuSans.ttf
Normal file
66
src/lib/luigi/backend/ffisdl/sdl.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
local REL = (...):gsub('[^.]*$', '')
|
||||
|
||||
local ffi = require 'ffi'
|
||||
local sdl = require(REL .. 'sdl2.init')
|
||||
|
||||
sdl.AudioCVT = ffi.typeof 'SDL_AudioCVT'
|
||||
-- sdl.AudioDeviceEvent = ffi.typeof 'SDL_AudioDeviceEvent'
|
||||
sdl.AudioSpec = ffi.typeof 'SDL_AudioSpec'
|
||||
sdl.Color = ffi.typeof 'SDL_Color'
|
||||
sdl.ControllerAxisEvent = ffi.typeof 'SDL_ControllerAxisEvent'
|
||||
sdl.ControllerButtonEvent = ffi.typeof 'SDL_ControllerButtonEvent'
|
||||
sdl.ControllerDeviceEvent = ffi.typeof 'SDL_ControllerDeviceEvent'
|
||||
sdl.DisplayMode = ffi.typeof 'SDL_DisplayMode'
|
||||
sdl.DollarGestureEvent = ffi.typeof 'SDL_DollarGestureEvent'
|
||||
sdl.DropEvent = ffi.typeof 'SDL_DropEvent'
|
||||
sdl.Event = ffi.typeof 'SDL_Event'
|
||||
sdl.Finger = ffi.typeof 'SDL_Finger'
|
||||
sdl.HapticCondition = ffi.typeof 'SDL_HapticCondition'
|
||||
sdl.HapticConstant = ffi.typeof 'SDL_HapticConstant'
|
||||
sdl.HapticCustom = ffi.typeof 'SDL_HapticCustom'
|
||||
sdl.HapticDirection = ffi.typeof 'SDL_HapticDirection'
|
||||
sdl.HapticEffect = ffi.typeof 'SDL_HapticEffect'
|
||||
sdl.HapticLeftRight = ffi.typeof 'SDL_HapticLeftRight'
|
||||
sdl.HapticPeriodic = ffi.typeof 'SDL_HapticPeriodic'
|
||||
sdl.HapticRamp = ffi.typeof 'SDL_HapticRamp'
|
||||
sdl.JoyAxisEvent = ffi.typeof 'SDL_JoyAxisEvent'
|
||||
sdl.JoyBallEvent = ffi.typeof 'SDL_JoyBallEvent'
|
||||
sdl.JoyButtonEvent = ffi.typeof 'SDL_JoyButtonEvent'
|
||||
sdl.JoyDeviceEvent = ffi.typeof 'SDL_JoyDeviceEvent'
|
||||
sdl.JoyHatEvent = ffi.typeof 'SDL_JoyHatEvent'
|
||||
sdl.KeyboardEvent = ffi.typeof 'SDL_KeyboardEvent'
|
||||
sdl.Keysym = ffi.typeof 'SDL_Keysym'
|
||||
sdl.MessageBoxButtonData = ffi.typeof 'SDL_MessageBoxButtonData'
|
||||
sdl.MessageBoxColor = ffi.typeof 'SDL_MessageBoxColor'
|
||||
sdl.MessageBoxColorScheme = ffi.typeof 'SDL_MessageBoxColorScheme'
|
||||
sdl.MessageBoxData = ffi.typeof 'SDL_MessageBoxData'
|
||||
sdl.MouseButtonEvent = ffi.typeof 'SDL_MouseButtonEvent'
|
||||
sdl.MouseMotionEvent = ffi.typeof 'SDL_MouseMotionEvent'
|
||||
sdl.MouseWheelEvent = ffi.typeof 'SDL_MouseWheelEvent'
|
||||
sdl.MultiGestureEvent = ffi.typeof 'SDL_MultiGestureEvent'
|
||||
sdl.Palette = ffi.typeof 'SDL_Palette'
|
||||
sdl.PixelFormat = ffi.typeof 'SDL_PixelFormat'
|
||||
sdl.Point = ffi.typeof 'SDL_Point'
|
||||
sdl.QuitEvent = ffi.typeof 'SDL_QuitEvent'
|
||||
sdl.RWops = ffi.typeof 'SDL_RWops'
|
||||
sdl.Rect = ffi.typeof 'SDL_Rect'
|
||||
sdl.RendererInfo = ffi.typeof 'SDL_RendererInfo'
|
||||
sdl.Surface = ffi.typeof 'SDL_Surface'
|
||||
sdl.SysWMEvent = ffi.typeof 'SDL_SysWMEvent'
|
||||
-- sdl.SysWMinfo = ffi.typeof 'SDL_SysWMinfo'
|
||||
sdl.SysWMmsg = ffi.typeof 'SDL_SysWMmsg'
|
||||
sdl.TextEditingEvent = ffi.typeof 'SDL_TextEditingEvent'
|
||||
sdl.TextInputEvent = ffi.typeof 'SDL_TextInputEvent'
|
||||
sdl.Texture = ffi.typeof 'SDL_Texture'
|
||||
sdl.TouchFingerEvent = ffi.typeof 'SDL_TouchFingerEvent'
|
||||
sdl.UserEvent = ffi.typeof 'SDL_UserEvent'
|
||||
sdl.WindowEvent = ffi.typeof 'SDL_WindowEvent'
|
||||
sdl.assert_data = ffi.typeof 'SDL_assert_data'
|
||||
sdl.atomic_t = ffi.typeof 'SDL_atomic_t'
|
||||
sdl.version = ffi.typeof 'SDL_version'
|
||||
|
||||
if sdl.init(sdl.INIT_VIDEO) ~= 0 then
|
||||
error(ffi.string(sdl.getError()))
|
||||
end
|
||||
|
||||
return sdl
|
36
src/lib/luigi/backend/ffisdl/sdl2/LICENSE
Normal file
|
@ -0,0 +1,36 @@
|
|||
Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
|
||||
Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
|
||||
Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
|
||||
Copyright (c) 2011-2013 NYU (Clement Farabet)
|
||||
Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
|
||||
Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
|
||||
Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the names of Deepmind Technologies, NYU, NEC Laboratories America
|
||||
and IDIAP Research Institute nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
32
src/lib/luigi/backend/ffisdl/sdl2/README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
sdl2-ffi
|
||||
========
|
||||
|
||||
A LuaJIT interface to SDL2
|
||||
|
||||
# Installation #
|
||||
|
||||
First, make sure SDL2 is installed on your system. This package only requires the binary shared libraries (.so, .dylib, .dll).
|
||||
Please see your package management system to install SDL2. You can also download yourself binaries on the
|
||||
[SDL2 web page](http://libsdl.org/download-2.0.php)
|
||||
|
||||
```sh
|
||||
luarocks install https://raw.github.com/torch/sdl2-ffi/master/rocks/sdl2-scm-1.rockspec
|
||||
```
|
||||
|
||||
*Note*: this SDL interface supports only SDL2, not SDL 1.2.
|
||||
|
||||
# Usage #
|
||||
|
||||
```lua
|
||||
local sdl = require 'sdl2'
|
||||
sdl.init(sdl.INIT_VIDEO)
|
||||
...
|
||||
```
|
||||
|
||||
All SDL C functions are available in the `sdl` namespace returned by require. The only difference is the naming, which is not prefixed
|
||||
by `SDL_` anymore. The same goes for all C defines (like `SDL_INIT_VIDEO`, which can now be accessed with `sdl.INIT_VIDEO`).
|
||||
|
||||
Although the interface is quite complete, there are still few defines not ported in this package. Fill free to post a message about it,
|
||||
or to request pulls.
|
||||
|
||||
|
2600
src/lib/luigi/backend/ffisdl/sdl2/cdefs.lua
Normal file
58
src/lib/luigi/backend/ffisdl/sdl2/defines.lua
Normal file
|
@ -0,0 +1,58 @@
|
|||
-- Function definitions which were not output by
|
||||
-- the C preprocessor
|
||||
|
||||
local sdl
|
||||
|
||||
local function registerdefines(sdl)
|
||||
|
||||
-- audio
|
||||
|
||||
function sdl.AUDIO_BITSIZE(x)
|
||||
return bit.band(x, sdl.AUDIO_MASK_BITSIZE)
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISFLOAT(x)
|
||||
return bit.band(x, sdl.AUDIO_MASK_DATATYPE) ~= 0
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISBIGENDIAN(x)
|
||||
return bit.band(x, sdl.AUDIO_MASK_ENDIAN) ~= 0
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISSIGNED(x)
|
||||
return bit.band(x, sdl.AUDIO_MASK_SIGNED) ~= 0
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISINT(x)
|
||||
return not sdl.AUDIO_ISFLOAT(x)
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISLITTLEENDIAN(x)
|
||||
return not sdl.AUDIO_ISBIGENDIAN(x)
|
||||
end
|
||||
|
||||
function sdl.AUDIO_ISUNSIGNED(x)
|
||||
return not sdl.AUDIO_ISSIGNED(x)
|
||||
end
|
||||
|
||||
function sdl.loadWAV(file, spec, audio_buf, audio_len)
|
||||
return sdl.loadWAV_RW(sdl.RWFromFile(file, "rb"), 1, spec, audio_buf, audio_len)
|
||||
end
|
||||
|
||||
-- surface
|
||||
sdl.blitSurface = sdl.upperBlit
|
||||
|
||||
function sdl.MUSTLOCK(S)
|
||||
return bit.band(S.flags, sdl.RLEACCEL)
|
||||
end
|
||||
|
||||
function sdl.loadBMP(file)
|
||||
return sdl.loadBMP_RW(sdl.RWFromFile(file, 'rb'), 1)
|
||||
end
|
||||
|
||||
function sdl.saveBMP(surface, file)
|
||||
return sdl.saveBMP_RW(surface, sdl.RWFromFile(file, 'wb'), 1)
|
||||
end
|
||||
end
|
||||
|
||||
return registerdefines
|
1500
src/lib/luigi/backend/ffisdl/sdl2/init.lua
Normal file
79
src/lib/luigi/backend/ffisdl/spritebatch.lua
Normal file
|
@ -0,0 +1,79 @@
|
|||
local REL = (...):gsub('[^.]*$', '')
|
||||
|
||||
local sdl = require(REL .. 'sdl')
|
||||
|
||||
local SpriteBatch = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
--[[
|
||||
spriteBatch = SpriteBatch( image, size )
|
||||
|
||||
Arguments
|
||||
|
||||
Image image
|
||||
The Image to use for the sprites.
|
||||
number size (1000)
|
||||
The max number of sprites.
|
||||
|
||||
Returns
|
||||
|
||||
SpriteBatch spriteBatch
|
||||
The new SpriteBatch.
|
||||
--]]
|
||||
function SpriteBatch:constructor (image)
|
||||
self.image = image
|
||||
self.sprites = {}
|
||||
end
|
||||
|
||||
function SpriteBatch:clear ()
|
||||
self.sprites = {}
|
||||
end
|
||||
|
||||
--[[
|
||||
id = SpriteBatch:add( quad, x, y, r, sx, sy )
|
||||
|
||||
Arguments
|
||||
|
||||
Quad quad
|
||||
The Quad to add.
|
||||
number x
|
||||
The position to draw the object (x-axis).
|
||||
number y
|
||||
The position to draw the object (y-axis).
|
||||
number r (0)
|
||||
Orientation (radians). (not implemented)
|
||||
number sx (1)
|
||||
Scale factor (x-axis).
|
||||
number sy (sx)
|
||||
Scale factor (y-axis).
|
||||
|
||||
Returns
|
||||
|
||||
number id
|
||||
An identifier for the added sprite.
|
||||
--]]
|
||||
function SpriteBatch:add (quad, x, y, r, sx, sy)
|
||||
local sprites = self.sprites
|
||||
|
||||
sprites[#sprites + 1] = { quad = quad, x = x, y = y,
|
||||
sx = sx or 1, sy = sy or 1 }
|
||||
end
|
||||
|
||||
function SpriteBatch:draw ()
|
||||
local image = self.image
|
||||
local renderer = image.sdlRenderer
|
||||
local texture = image.sdlTexture
|
||||
|
||||
for _, sprite in ipairs(self.sprites) do
|
||||
local quad = sprite.quad
|
||||
local w = math.ceil(quad[3] * sprite.sx)
|
||||
local h = math.ceil(quad[4] * sprite.sy)
|
||||
local src = sdl.Rect(quad)
|
||||
local dst = sdl.Rect(sprite.x, sprite.y, w, h)
|
||||
sdl.renderCopy(renderer, texture, src, dst)
|
||||
end
|
||||
end
|
||||
|
||||
return SpriteBatch
|
104
src/lib/luigi/backend/ffisdl/text.lua
Normal file
|
@ -0,0 +1,104 @@
|
|||
local ROOT = (...):gsub('[^.]*.[^.]*.[^.]*$', '')
|
||||
local REL = (...):gsub('[^.]*$', '')
|
||||
|
||||
local ffi = require 'ffi'
|
||||
local sdl = require(REL .. 'sdl')
|
||||
local Font = require(REL .. 'font')
|
||||
local ttf = Font.SDL2_ttf
|
||||
|
||||
local Multiline = require(ROOT .. 'multiline')
|
||||
|
||||
local Text = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
local function renderSingle (self, font, text, color)
|
||||
local alphaMod = color and color[4]
|
||||
color = sdl.Color(color or 0)
|
||||
local surface = ffi.gc(
|
||||
ttf.TTF_RenderUTF8_Blended(font.sdlFont, text, color),
|
||||
sdl.freeSurface)
|
||||
self.sdlSurface = surface
|
||||
self.sdlTexture = ffi.gc(
|
||||
sdl.createTextureFromSurface(self.sdlRenderer, surface),
|
||||
sdl.destroyTexture)
|
||||
if alphaMod then
|
||||
sdl.setTextureAlphaMod(self.sdlTexture, alphaMod)
|
||||
end
|
||||
self.width, self.height = surface.w, surface.h
|
||||
end
|
||||
|
||||
local function renderMulti (self, font, text, color, align, limit)
|
||||
local alphaMod = color and color[4]
|
||||
local lines = Multiline.wrap(font, text, limit)
|
||||
local lineHeight = font:getLineHeight()
|
||||
local height = #lines * lineHeight
|
||||
color = sdl.Color(color or 0)
|
||||
|
||||
-- mask values from SDL_ttf.c
|
||||
-- TODO: something with sdl.BYTEORDER == sdl.BIG_ENDIAN ?
|
||||
local r, g, b, a = 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000
|
||||
|
||||
local surface = ffi.gc(
|
||||
sdl.createRGBSurface(sdl.SWSURFACE, limit, height, 32, r, g, b, a),
|
||||
sdl.freeSurface)
|
||||
self.sdlSurface = surface
|
||||
|
||||
for index, line in ipairs(lines) do
|
||||
local text = table.concat(line)
|
||||
local lineSurface = ffi.gc(
|
||||
ttf.TTF_RenderUTF8_Blended(font.sdlFont, text, color),
|
||||
sdl.freeSurface)
|
||||
if lineSurface ~= nil then
|
||||
sdl.setSurfaceBlendMode(lineSurface, sdl.BLENDMODE_NONE)
|
||||
|
||||
local w, h = lineSurface.w, lineSurface.h
|
||||
local top = (index - 1) * lineHeight
|
||||
|
||||
if align == 'left' then
|
||||
sdl.blitSurface(lineSurface, nil, surface,
|
||||
sdl.Rect(0, top, w, h))
|
||||
elseif align == 'right' then
|
||||
sdl.blitSurface(lineSurface, nil, surface,
|
||||
sdl.Rect(limit - line.width, top, w, h))
|
||||
elseif align == 'center' then
|
||||
sdl.blitSurface(lineSurface, nil, surface,
|
||||
sdl.Rect((limit - line.width) / 2, top, w, h))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.sdlTexture = ffi.gc(
|
||||
sdl.createTextureFromSurface(self.sdlRenderer, surface),
|
||||
sdl.destroyTexture)
|
||||
|
||||
if alphaMod then
|
||||
sdl.setTextureAlphaMod(self.sdlTexture, alphaMod)
|
||||
end
|
||||
|
||||
self.width, self.height = limit, height
|
||||
end
|
||||
|
||||
function Text:constructor (renderer, font, text, color, align, limit)
|
||||
self.width, self.height = 0, 0
|
||||
if not text or text == '' then return end
|
||||
|
||||
self.sdlRenderer = renderer
|
||||
|
||||
if limit then
|
||||
renderMulti(self, font, text, color, align, limit)
|
||||
else
|
||||
renderSingle(self, font, text, color)
|
||||
end
|
||||
end
|
||||
|
||||
function Text:getWidth ()
|
||||
return self.width
|
||||
end
|
||||
|
||||
function Text:getHeight ()
|
||||
return self.height
|
||||
end
|
||||
|
||||
return Text
|
177
src/lib/luigi/backend/love.lua
Normal file
|
@ -0,0 +1,177 @@
|
|||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Hooker = require(ROOT .. 'hooker')
|
||||
|
||||
local Backend = {}
|
||||
|
||||
Backend.isMac = function ()
|
||||
return love.system.getOS() == 'OS X'
|
||||
end
|
||||
|
||||
Backend.run = function () end
|
||||
|
||||
Backend.Cursor = love.mouse.newCursor
|
||||
|
||||
Backend.Font = require(ROOT .. 'backend.love.font')
|
||||
|
||||
Backend.Text = require(ROOT .. 'backend.love.text')
|
||||
|
||||
Backend.Image = love.graphics.newImage
|
||||
|
||||
Backend.Quad = love.graphics.newQuad
|
||||
|
||||
Backend.SpriteBatch = love.graphics.newSpriteBatch
|
||||
|
||||
-- love.graphics.draw( drawable, x, y, r, sx, sy, ox, oy, kx, ky )
|
||||
Backend.draw = function (drawable, ...)
|
||||
if drawable.typeOf and drawable:typeOf 'Drawable' then
|
||||
return love.graphics.draw(drawable, ...)
|
||||
end
|
||||
return drawable:draw(...)
|
||||
end
|
||||
|
||||
Backend.drawRectangle = love.graphics.rectangle
|
||||
|
||||
Backend.print = love.graphics.print
|
||||
|
||||
Backend.getClipboardText = love.system.getClipboardText
|
||||
|
||||
Backend.setClipboardText = love.system.setClipboardText
|
||||
|
||||
Backend.getMousePosition = love.mouse.getPosition
|
||||
|
||||
Backend.setMousePosition = love.mouse.setPosition
|
||||
|
||||
Backend.getSystemCursor = love.mouse.getSystemCursor
|
||||
|
||||
Backend.getWindowSize = function ()
|
||||
return love.graphics.getWidth(), love.graphics.getHeight()
|
||||
end
|
||||
|
||||
Backend.getTime = love.timer.getTime
|
||||
|
||||
Backend.isKeyDown = love.keyboard.isDown
|
||||
|
||||
Backend.isMouseDown = love.mouse.isDown
|
||||
|
||||
Backend.pop = love.graphics.pop
|
||||
|
||||
local push = love.graphics.push
|
||||
|
||||
Backend.push = function ()
|
||||
return push 'all'
|
||||
end
|
||||
|
||||
Backend.quit = function ()
|
||||
love.event.quit()
|
||||
end
|
||||
|
||||
if _G.love._version_major >= 11 then
|
||||
Backend.setColor = function(r, g, b, a)
|
||||
if type(r) == "table" then
|
||||
r, g, b, a = r[1], r[2], r[3], r[4]
|
||||
end
|
||||
if a == nil then
|
||||
a = 255
|
||||
end
|
||||
love.graphics.setColor(r / 255, g / 255, b / 255, a / 255)
|
||||
end
|
||||
else
|
||||
Backend.setColor = love.graphics.setColor
|
||||
end
|
||||
|
||||
Backend.setCursor = love.mouse.setCursor
|
||||
|
||||
Backend.setFont = function (font)
|
||||
return love.graphics.setFont(font.loveFont)
|
||||
end
|
||||
|
||||
Backend.setScissor = love.graphics.setScissor
|
||||
|
||||
Backend.getScissor = love.graphics.getScissor
|
||||
|
||||
Backend.intersectScissor = love.graphics.intersectScissor
|
||||
|
||||
function Backend.hide (layout)
|
||||
for _, item in ipairs(layout.hooks) do
|
||||
Hooker.unhook(item)
|
||||
end
|
||||
layout.hooks = {}
|
||||
end
|
||||
|
||||
local function hook (layout, key, method, hookLast)
|
||||
layout.hooks[#layout.hooks + 1] = Hooker.hook(love, key, method, hookLast)
|
||||
end
|
||||
|
||||
local getMouseButtonId, isMouseDown
|
||||
|
||||
if love._version_major == 0 and love._version_minor < 10 then
|
||||
getMouseButtonId = function (value)
|
||||
return value == 'l' and 'left'
|
||||
or value == 'r' and 'right'
|
||||
or value == 'm' and 'middle'
|
||||
or value == 'x1' and 4
|
||||
or value == 'x2' and 5
|
||||
or value
|
||||
end
|
||||
isMouseDown = function ()
|
||||
return love.mouse.isDown('l', 'r', 'm')
|
||||
end
|
||||
else
|
||||
getMouseButtonId = function (value)
|
||||
return value == 1 and 'left'
|
||||
or value == 2 and 'right'
|
||||
or value == 3 and 'middle'
|
||||
or value
|
||||
end
|
||||
isMouseDown = function ()
|
||||
return love.mouse.isDown(1, 2, 3)
|
||||
end
|
||||
end
|
||||
|
||||
function Backend.show (layout)
|
||||
|
||||
local input = layout.input
|
||||
|
||||
hook(layout, 'draw', function ()
|
||||
input:handleDisplay(layout)
|
||||
end, true)
|
||||
hook(layout, 'resize', function (width, height)
|
||||
return input:handleReshape(layout, width, height)
|
||||
end)
|
||||
hook(layout, 'mousepressed', function (x, y, button)
|
||||
if button == 'wu' or button == 'wd' then
|
||||
return input:handleWheelMove(layout, 0, button == 'wu' and 1 or -1)
|
||||
end
|
||||
return input:handlePressStart(layout, getMouseButtonId(button), x, y)
|
||||
end)
|
||||
hook(layout, 'mousereleased', function (x, y, button)
|
||||
return input:handlePressEnd(layout, getMouseButtonId(button), x, y)
|
||||
end)
|
||||
hook(layout, 'mousemoved', function (x, y, dx, dy)
|
||||
if isMouseDown() then
|
||||
return input:handlePressedMove(layout, x, y)
|
||||
else
|
||||
return input:handleMove(layout, x, y)
|
||||
end
|
||||
end)
|
||||
hook(layout, 'keypressed', function (key, sc, isRepeat)
|
||||
if key == ' ' then key = 'space' end
|
||||
return input:handleKeyPress(layout, key, sc, Backend.getMousePosition())
|
||||
end)
|
||||
hook(layout, 'keyreleased', function (key, sc)
|
||||
if key == ' ' then key = 'space' end
|
||||
return input:handleKeyRelease(layout, key, sc, Backend.getMousePosition())
|
||||
end)
|
||||
hook(layout, 'textinput', function (text)
|
||||
return input:handleTextInput(layout, text, Backend.getMousePosition())
|
||||
end)
|
||||
if (love._version_major == 0 and love._version_minor > 9) or love._version_major >= 11 then
|
||||
hook(layout, 'wheelmoved', function (x, y)
|
||||
return input:handleWheelMove(layout, x, y)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return Backend
|
53
src/lib/luigi/backend/love/font.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
local Font = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
local fontCache = {}
|
||||
|
||||
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 fontCache[key] then
|
||||
if path then
|
||||
fontCache[key] = love.graphics.newFont(path, size)
|
||||
else
|
||||
fontCache[key] = love.graphics.newFont(size)
|
||||
end
|
||||
end
|
||||
|
||||
self.loveFont = fontCache[key]
|
||||
self.color = color
|
||||
end
|
||||
|
||||
function Font:setAlignment (align)
|
||||
self.align = align
|
||||
end
|
||||
|
||||
function Font:setWidth (width)
|
||||
self.width = width
|
||||
end
|
||||
|
||||
function Font:getLineHeight ()
|
||||
return self.loveFont:getHeight()
|
||||
end
|
||||
|
||||
function Font:getAscender ()
|
||||
return self.loveFont:getAscent()
|
||||
end
|
||||
|
||||
function Font:getDescender ()
|
||||
return self.loveFont:getDescent()
|
||||
end
|
||||
|
||||
function Font:getAdvance (text)
|
||||
return (self.loveFont:getWidth(text))
|
||||
end
|
||||
|
||||
return Font
|
75
src/lib/luigi/backend/love/text.lua
Normal file
|
@ -0,0 +1,75 @@
|
|||
local ROOT = (...):gsub('[^.]*.[^.]*.[^.]*$', '')
|
||||
local REL = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Multiline = require(ROOT .. 'multiline')
|
||||
|
||||
local Text = setmetatable({}, { __call = function (self, ...)
|
||||
local object = setmetatable({}, { __index = self })
|
||||
return object, self.constructor(object, ...)
|
||||
end })
|
||||
|
||||
local function renderSingle (self, x, y, font, text, color)
|
||||
love.graphics.push('all')
|
||||
love.graphics.setColor(color or { 0, 0, 0 })
|
||||
love.graphics.setFont(font.loveFont)
|
||||
love.graphics.print(text, math.floor(x), math.floor(y))
|
||||
love.graphics.pop()
|
||||
|
||||
self.height = font:getLineHeight()
|
||||
self.width = font:getAdvance(text)
|
||||
end
|
||||
|
||||
local function renderMulti (self, x, y, font, text, color, align, limit)
|
||||
local lines = Multiline.wrap(font, text, limit)
|
||||
local lineHeight = font:getLineHeight()
|
||||
local height = #lines * lineHeight
|
||||
|
||||
love.graphics.push('all')
|
||||
love.graphics.setColor(color or { 0, 0, 0 })
|
||||
love.graphics.setFont(font.loveFont)
|
||||
|
||||
for index, line in ipairs(lines) do
|
||||
local text = table.concat(line)
|
||||
local top = (index - 1) * lineHeight
|
||||
local w = line.width
|
||||
|
||||
if align == 'left' then
|
||||
love.graphics.print(text,
|
||||
math.floor(x), math.floor(top + y))
|
||||
elseif align == 'right' then
|
||||
love.graphics.print(text,
|
||||
math.floor(limit - w + x), math.floor(top + y))
|
||||
elseif align == 'center' then
|
||||
love.graphics.print(text,
|
||||
math.floor((limit - w) / 2 + x), math.floor(top + y))
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.pop()
|
||||
|
||||
self.height = height
|
||||
self.width = limit
|
||||
end
|
||||
|
||||
function Text:constructor (font, text, color, align, limit)
|
||||
if limit then
|
||||
function self:draw (x, y)
|
||||
return renderMulti(self, x, y, font, text, color, align, limit)
|
||||
end
|
||||
else
|
||||
function self:draw (x, y)
|
||||
return renderSingle(self, x, y, font, text, color)
|
||||
end
|
||||
end
|
||||
self:draw(-1000000, -1000000)
|
||||
end
|
||||
|
||||
function Text:getWidth ()
|
||||
return self.width
|
||||
end
|
||||
|
||||
function Text:getHeight ()
|
||||
return self.height
|
||||
end
|
||||
|
||||
return Text
|
13
src/lib/luigi/base.lua
Normal file
|
@ -0,0 +1,13 @@
|
|||
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,
|
||||
}
|
||||
|
251
src/lib/luigi/engine/alpha.lua
Normal file
|
@ -0,0 +1,251 @@
|
|||
local RESOURCE = (...):gsub('%.', '/') .. '/'
|
||||
|
||||
return function (config)
|
||||
config = config or {}
|
||||
local resources = assert(config.resources, 'missing config.resources')
|
||||
local backColor = config.backColor or { 240, 240, 240 }
|
||||
local lineColor = config.lineColor or { 220, 220, 220 }
|
||||
local textColor = config.textColor or { 0, 0, 0 }
|
||||
local highlight = config.highlight or { 0x19, 0xAE, 0xFF }
|
||||
|
||||
local button_pressed = resources .. 'button_pressed.png'
|
||||
local button_focused = resources .. 'button_focused.png'
|
||||
local button_hovered = resources .. 'button_hovered.png'
|
||||
local button = resources .. 'button.png'
|
||||
|
||||
local check_checked_pressed = resources .. 'check_checked_pressed.png'
|
||||
local check_unchecked_pressed = resources .. 'check_unchecked_pressed.png'
|
||||
local check_checked_focused = resources .. 'check_checked_focused.png'
|
||||
local check_unchecked_focused = resources .. 'check_unchecked_focused.png'
|
||||
local check_checked = resources .. 'check_checked.png'
|
||||
local check_unchecked = resources .. 'check_unchecked.png'
|
||||
|
||||
local radio_checked_pressed = resources .. 'radio_checked_pressed.png'
|
||||
local radio_unchecked_pressed = resources .. 'radio_unchecked_pressed.png'
|
||||
local radio_checked_focused = resources .. 'radio_checked_focused.png'
|
||||
local radio_unchecked_focused = resources .. 'radio_unchecked_focused.png'
|
||||
local radio_checked = resources .. 'radio_checked.png'
|
||||
local radio_unchecked = resources .. 'radio_unchecked.png'
|
||||
|
||||
local triangle_left = resources .. 'triangle_left.png'
|
||||
local triangle_up = resources .. 'triangle_up.png'
|
||||
local triangle_right = resources .. 'triangle_right.png'
|
||||
local triangle_down = resources .. 'triangle_down.png'
|
||||
|
||||
local text_focused = resources .. 'text_focused.png'
|
||||
local text = resources .. 'text.png'
|
||||
|
||||
local function getButtonSlices (self)
|
||||
return self.pressed.left and button_pressed
|
||||
or self.focused and button_focused
|
||||
or self.hovered and button_hovered
|
||||
or button
|
||||
end
|
||||
|
||||
local function getCheckIcon (self)
|
||||
if self.pressed.left then
|
||||
return self.value and check_checked_pressed
|
||||
or check_unchecked_pressed
|
||||
end
|
||||
if self.focused then
|
||||
return self.value and check_checked_focused
|
||||
or check_unchecked_focused
|
||||
end
|
||||
return self.value and check_checked or check_unchecked
|
||||
end
|
||||
|
||||
local function getControlHeight (self)
|
||||
return self.flow == 'x' and self._defaultDimension
|
||||
end
|
||||
|
||||
local function getControlWidth (self)
|
||||
return self.flow ~= 'x' and self._defaultDimension
|
||||
end
|
||||
|
||||
local function getMenuItemBackground (self)
|
||||
return self.active and highlight
|
||||
end
|
||||
|
||||
local function getRadioIcon (self)
|
||||
if self.pressed.left then
|
||||
return self.value and radio_checked_pressed
|
||||
or radio_unchecked_pressed
|
||||
end
|
||||
if self.focused then
|
||||
return self.value and radio_checked_focused
|
||||
or radio_unchecked_focused
|
||||
end
|
||||
return self.value and radio_checked or radio_unchecked
|
||||
end
|
||||
|
||||
local function getSashBackground (self)
|
||||
return self.hovered and highlight or lineColor
|
||||
end
|
||||
|
||||
local function getSashHeight (self)
|
||||
return self.parent and self.parent.flow ~= 'x' and 4
|
||||
end
|
||||
|
||||
local function getSashWidth (self)
|
||||
return self.parent and self.parent.flow == 'x' and 4
|
||||
end
|
||||
|
||||
local function getSliderThumbWidth (self)
|
||||
return self.parent.flow == 'x' and 32 or false
|
||||
end
|
||||
|
||||
local function getSliderThumbHeight (self)
|
||||
return self.parent.flow ~= 'x' and 32 or false
|
||||
end
|
||||
|
||||
local function getStepperBeforeIcon (self)
|
||||
return self.parent.flow == 'x' and triangle_left or triangle_up
|
||||
end
|
||||
|
||||
local function getStepperAfterIcon (self)
|
||||
return self.parent.flow == 'x' and triangle_right or triangle_down
|
||||
end
|
||||
|
||||
local function getTextSlices (self)
|
||||
return self.focused and text_focused or text
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
-- generic types for widgets to inherit
|
||||
|
||||
Control = {
|
||||
flow = 'x',
|
||||
height = getControlHeight,
|
||||
width = getControlWidth,
|
||||
color = textColor,
|
||||
align = 'center middle',
|
||||
margin = 2,
|
||||
color = textColor,
|
||||
solid = true,
|
||||
_defaultDimension = 36,
|
||||
},
|
||||
|
||||
Line = {
|
||||
margin = 0,
|
||||
padding = 4,
|
||||
align = 'left middle',
|
||||
_defaultDimension = 24,
|
||||
},
|
||||
|
||||
-- widget types
|
||||
|
||||
button = {
|
||||
type = { 'Control' },
|
||||
padding = 6,
|
||||
slices = getButtonSlices,
|
||||
focusable = true,
|
||||
},
|
||||
check = {
|
||||
type = { 'Line', 'Control' },
|
||||
focusable = true,
|
||||
icon = getCheckIcon,
|
||||
},
|
||||
label = {
|
||||
type = { 'Line', 'Control' },
|
||||
},
|
||||
menu = {
|
||||
flow = 'x',
|
||||
height = 24,
|
||||
background = backColor,
|
||||
color = textColor,
|
||||
},
|
||||
['menu.expander'] = {
|
||||
icon = resources .. 'triangle_right.png',
|
||||
},
|
||||
['menu.item'] = {
|
||||
padding = 4,
|
||||
height = 24,
|
||||
align = 'left middle',
|
||||
background = getMenuItemBackground,
|
||||
},
|
||||
panel = {
|
||||
padding = 2,
|
||||
background = backColor,
|
||||
color = textColor,
|
||||
solid = true,
|
||||
},
|
||||
progress = {
|
||||
type = { 'Control' },
|
||||
slices = resources .. 'button_pressed.png',
|
||||
},
|
||||
['progress.bar'] = {
|
||||
slices = resources .. 'progress.png',
|
||||
minwidth = 12,
|
||||
minheight = 22,
|
||||
},
|
||||
radio = {
|
||||
type = { 'Line', 'Control' },
|
||||
focusable = true,
|
||||
icon = getRadioIcon,
|
||||
},
|
||||
sash = {
|
||||
background = getSashBackground,
|
||||
height = getSashHeight,
|
||||
width = getSashWidth,
|
||||
},
|
||||
slider = {
|
||||
type = { 'Control' },
|
||||
slices = resources .. 'button_pressed.png',
|
||||
},
|
||||
['slider.thumb'] = {
|
||||
type = { 'button' },
|
||||
align = 'middle center',
|
||||
margin = 0,
|
||||
width = getSliderThumbWidth,
|
||||
height = getSliderThumbHeight,
|
||||
},
|
||||
status = {
|
||||
type = { 'Line', 'Control' },
|
||||
background = backColor,
|
||||
color = textColor,
|
||||
},
|
||||
stepper = {
|
||||
type = { 'Control' },
|
||||
slices = resources .. 'button_pressed.png',
|
||||
},
|
||||
['stepper.after'] = {
|
||||
type = { 'button' },
|
||||
icon = getStepperAfterIcon,
|
||||
margin = 0,
|
||||
minwidth = 32,
|
||||
minheight = 32,
|
||||
},
|
||||
['stepper.before'] = {
|
||||
type = { 'button' },
|
||||
icon = getStepperBeforeIcon,
|
||||
margin = 0,
|
||||
minwidth = 32,
|
||||
minheight = 32,
|
||||
},
|
||||
['stepper.item'] = {
|
||||
align = 'center middle',
|
||||
color = textColor,
|
||||
},
|
||||
['stepper.view'] = {
|
||||
margin = 4,
|
||||
},
|
||||
submenu = {
|
||||
padding = 10,
|
||||
margin = -10,
|
||||
slices = resources .. 'submenu.png',
|
||||
color = textColor,
|
||||
solid = true,
|
||||
},
|
||||
text = {
|
||||
type = { 'Control' },
|
||||
align = 'left middle',
|
||||
slices = getTextSlices,
|
||||
padding = 6,
|
||||
focusable = true,
|
||||
cursor = 'ibeam',
|
||||
highlight = highlight,
|
||||
},
|
||||
}
|
||||
|
||||
end
|
69
src/lib/luigi/event.lua
Normal file
|
@ -0,0 +1,69 @@
|
|||
--[[--
|
||||
Event class.
|
||||
|
||||
@classmod Event
|
||||
--]]--
|
||||
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Hooker = require(ROOT .. 'hooker')
|
||||
|
||||
local Event = Base:extend({ name = 'Event' })
|
||||
|
||||
function Event:emit (target, data, defaultAction)
|
||||
local callbacks = self.registry[target]
|
||||
local result = callbacks and callbacks(data or {})
|
||||
if result ~= nil then return result end
|
||||
if defaultAction then defaultAction() end
|
||||
end
|
||||
|
||||
function Event:bind (target, callback)
|
||||
local registry = self.registry
|
||||
return Hooker.hook(registry, target, callback)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Event names.
|
||||
--]]--
|
||||
Event.names = {
|
||||
'Reshape', -- A widget is being reshaped.
|
||||
'PreDisplay', -- A widget is about to be drawn.
|
||||
'Display', -- A widget was drawn.
|
||||
'KeyPress', -- A keyboard key was pressed.
|
||||
'KeyRelease', -- A keyboard key was released.
|
||||
'TextInput', -- Text was entered.
|
||||
'Move', -- The cursor moved, and no button was pressed.
|
||||
'Focus', -- A widget received focus.
|
||||
'Blur', -- A widget lost focus.
|
||||
'Enter', -- The cursor entered a widget, and no button was pressed.
|
||||
'Leave', -- The cursor left a widget, and no button was pressed.
|
||||
'PressEnter', -- The cursor entered a widget, and a button was pressed.
|
||||
'PressLeave', -- The cursor left a widget, and a button was pressed.
|
||||
'PressStart', -- A pointer button or keyboard shortcut was pressed.
|
||||
'PressEnd', -- A pointer button or keyboard shortcut was released.
|
||||
'PressDrag', -- A pressed cursor moved; targets originating widget.
|
||||
'PressMove', -- A pressed cursor moved; targets widget at cursor position.
|
||||
'Press', -- A pointer button was pressed and released on the same widget.
|
||||
'Change', -- A widget's value changed.
|
||||
'WheelMove', -- The scroll wheel on the mouse moved.
|
||||
'Show', -- A layout is shown.
|
||||
'Hide', -- A layout is hidden.
|
||||
}
|
||||
|
||||
local weakKeyMeta = { __mode = 'k' }
|
||||
|
||||
for i, name in ipairs(Event.names) do
|
||||
Event[name] = Event:extend({
|
||||
name = name,
|
||||
registry = setmetatable({}, weakKeyMeta),
|
||||
})
|
||||
end
|
||||
|
||||
function Event.injectBinders (t)
|
||||
for i, name in ipairs(Event.names) do
|
||||
t['on' .. name] = function (...) return Event[name]:bind(...) end
|
||||
end
|
||||
end
|
||||
|
||||
return Event
|
91
src/lib/luigi/hooker.lua
Normal file
|
@ -0,0 +1,91 @@
|
|||
local Hooker = {}
|
||||
|
||||
local wrapped = setmetatable({}, { __mode = 'k' })
|
||||
|
||||
local hooks = setmetatable({}, { __mode = 'k' })
|
||||
|
||||
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.host][item.key] == item then
|
||||
hooks[item.host][item.key] = item.next
|
||||
end
|
||||
end
|
||||
|
||||
local function hook (host, key, func, atEnd)
|
||||
if not func then
|
||||
return
|
||||
end
|
||||
|
||||
if not hooks[host] then
|
||||
hooks[host] = {}
|
||||
end
|
||||
|
||||
local current = hooks[host][key]
|
||||
local item = {
|
||||
next = not atEnd and current or nil,
|
||||
unhook = unhook,
|
||||
host = host,
|
||||
key = key,
|
||||
func = func,
|
||||
}
|
||||
|
||||
if atEnd then
|
||||
if current then
|
||||
while current.next do
|
||||
current = current.next
|
||||
end
|
||||
current.next = item
|
||||
item.prev = current
|
||||
else
|
||||
hooks[host][key] = item
|
||||
end
|
||||
return item
|
||||
end
|
||||
|
||||
if current then
|
||||
current.prev = item
|
||||
end
|
||||
|
||||
hooks[host][key] = item
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
function Hooker.unhook (item)
|
||||
return unhook(item)
|
||||
end
|
||||
|
||||
function Hooker.hook (host, key, func, atEnd)
|
||||
if not wrapped[host] then
|
||||
wrapped[host] = {}
|
||||
end
|
||||
|
||||
if not wrapped[host][key] then
|
||||
wrapped[host][key] = true
|
||||
|
||||
hook(host, key, host[key])
|
||||
|
||||
host[key] = function (...)
|
||||
local item = hooks[host][key]
|
||||
|
||||
while item do
|
||||
local result = item.func(...)
|
||||
if result ~= nil then
|
||||
return result
|
||||
end
|
||||
item = item.next
|
||||
end -- while
|
||||
end -- function
|
||||
end -- if
|
||||
|
||||
return hook(host, key, func, atEnd)
|
||||
end
|
||||
|
||||
return Hooker
|
232
src/lib/luigi/input.lua
Normal file
|
@ -0,0 +1,232 @@
|
|||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Shortcut = require(ROOT .. 'shortcut')
|
||||
|
||||
local Input = Base:extend()
|
||||
|
||||
local weakValueMeta = { __mode = 'v' }
|
||||
|
||||
function Input:constructor ()
|
||||
self.pressedWidgets = setmetatable({}, weakValueMeta)
|
||||
self.passedWidgets = setmetatable({}, weakValueMeta)
|
||||
end
|
||||
|
||||
function Input:handleDisplay (layout)
|
||||
local root = layout.root
|
||||
if root then root:paint() end
|
||||
Event.Display:emit(layout)
|
||||
end
|
||||
|
||||
function Input:handleKeyPress (layout, key, sc, x, y)
|
||||
local widget = layout.focusedWidget or layout.root
|
||||
local result = widget:bubbleEvent('KeyPress', {
|
||||
key = key,
|
||||
scanCode = sc,
|
||||
modifierFlags = Shortcut.getModifierFlags(),
|
||||
x = x, y = y
|
||||
})
|
||||
if result ~= nil then return result end
|
||||
if layout.root.modal then return false end
|
||||
end
|
||||
|
||||
function Input:handleKeyRelease (layout, key, sc, x, y)
|
||||
local widget = layout.focusedWidget or layout.root
|
||||
local result = widget:bubbleEvent('KeyRelease', {
|
||||
key = key,
|
||||
scanCode = sc,
|
||||
modifierFlags = Shortcut.getModifierFlags(),
|
||||
x = x, y = y
|
||||
})
|
||||
if result ~= nil then return result end
|
||||
if layout.root.modal then return false end
|
||||
end
|
||||
|
||||
function Input:handleTextInput (layout, text, x, y)
|
||||
local widget = layout.focusedWidget or layout.root
|
||||
local result = widget:bubbleEvent('TextInput', {
|
||||
text = text,
|
||||
x = x, y = y
|
||||
})
|
||||
if result ~= nil then return result end
|
||||
if layout.root.modal then return false end
|
||||
end
|
||||
|
||||
local function checkHit (widget, layout)
|
||||
local root = layout.root
|
||||
return widget and widget.solid or root.modal, widget or root
|
||||
end
|
||||
|
||||
function Input:handleMove (layout, x, y)
|
||||
local hit, widget = checkHit(layout:getWidgetAt(x, y), layout)
|
||||
local previousWidget = self.previousMoveWidget
|
||||
if widget ~= previousWidget then
|
||||
if previousWidget then
|
||||
for ancestor in previousWidget:eachAncestor(true) do
|
||||
ancestor.hovered = nil
|
||||
end
|
||||
end
|
||||
for ancestor in widget:eachAncestor(true) do
|
||||
ancestor.hovered = true
|
||||
end
|
||||
end
|
||||
widget:bubbleEvent('Move', {
|
||||
hit = hit,
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
if widget ~= previousWidget then
|
||||
if previousWidget then
|
||||
previousWidget:bubbleEvent('Leave', {
|
||||
hit = hit,
|
||||
newTarget = widget,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
widget:bubbleEvent('Enter', {
|
||||
hit = hit,
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
if widget.cursor then
|
||||
Backend.setCursor(Backend.getSystemCursor(widget.cursor))
|
||||
else
|
||||
Backend.setCursor()
|
||||
end
|
||||
self.previousMoveWidget = widget
|
||||
end
|
||||
return hit
|
||||
end
|
||||
|
||||
function Input:handlePressedMove (layout, x, y)
|
||||
local hit, widget = checkHit(layout:getWidgetAt(x, y), layout)
|
||||
for _, button in ipairs { 'left', 'middle', 'right' } do
|
||||
local originWidget = self.pressedWidgets[button]
|
||||
if originWidget then
|
||||
local passedWidget = self.passedWidgets[button]
|
||||
originWidget:bubbleEvent('PressDrag', {
|
||||
hit = hit,
|
||||
newTarget = widget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
if (widget == passedWidget) then
|
||||
widget:bubbleEvent('PressMove', {
|
||||
hit = hit,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
else
|
||||
originWidget.pressed[button] = (widget == originWidget) or nil
|
||||
if passedWidget then
|
||||
passedWidget:bubbleEvent('PressLeave', {
|
||||
hit = hit,
|
||||
newTarget = widget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
widget:bubbleEvent('PressEnter', {
|
||||
hit = hit,
|
||||
oldTarget = passedWidget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
self.passedWidgets[button] = widget
|
||||
end
|
||||
end -- if originWidget
|
||||
end -- mouse buttons
|
||||
return hit
|
||||
end
|
||||
|
||||
function Input:handlePressStart (layout, button, x, y, widget, shortcut)
|
||||
local hit, widget = checkHit(widget or layout:getWidgetAt(x, y), layout)
|
||||
-- if hit then
|
||||
self.pressedWidgets[button] = widget
|
||||
self.passedWidgets[button] = widget
|
||||
widget.pressed[button] = true
|
||||
if button == 'left' then
|
||||
widget:focus()
|
||||
end
|
||||
-- end
|
||||
widget:bubbleEvent('PressStart', {
|
||||
hit = hit,
|
||||
button = button,
|
||||
shortcut = shortcut,
|
||||
x = x, y = y
|
||||
})
|
||||
return hit
|
||||
end
|
||||
|
||||
function Input:handlePressEnd (layout, button, x, y, widget, shortcut)
|
||||
local originWidget = widget or self.pressedWidgets[button]
|
||||
if not originWidget then return end
|
||||
local hit, widget = checkHit(widget or layout:getWidgetAt(x, y), layout)
|
||||
local wasPressed = originWidget.pressed[button]
|
||||
if hit then
|
||||
originWidget.pressed[button] = nil
|
||||
end
|
||||
widget:bubbleEvent('PressEnd', {
|
||||
hit = hit,
|
||||
origin = originWidget,
|
||||
shortcut = shortcut,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
if (widget == originWidget and wasPressed) then
|
||||
widget:bubbleEvent('Press', {
|
||||
hit = hit,
|
||||
button = button,
|
||||
shortcut = shortcut,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
if hit then
|
||||
self.pressedWidgets[button] = nil
|
||||
self.passedWidgets[button] = nil
|
||||
end
|
||||
return hit
|
||||
end
|
||||
|
||||
function Input:handleReshape (layout, width, height)
|
||||
local root = layout.root
|
||||
|
||||
root:reshape()
|
||||
|
||||
if root.type ~= 'window' then -- FIXME: move stuff below to a Widget method
|
||||
if not root.width then
|
||||
root.dimensions.width = width
|
||||
end
|
||||
if not root.height then
|
||||
root.dimensions.height = height
|
||||
end
|
||||
end
|
||||
|
||||
Event.Reshape:emit(layout, {
|
||||
target = layout,
|
||||
width = width,
|
||||
height = height
|
||||
})
|
||||
end
|
||||
|
||||
function Input:handleWheelMove (layout, scrollX, scrollY)
|
||||
local x, y = Backend.getMousePosition()
|
||||
local hit, widget = checkHit(layout:getWidgetAt(x, y), layout)
|
||||
|
||||
widget:bubbleEvent('WheelMove', {
|
||||
hit = hit,
|
||||
x = x, y = y,
|
||||
scrollX = scrollX, scrollY = scrollY
|
||||
})
|
||||
|
||||
return hit
|
||||
end
|
||||
|
||||
Input.default = Input()
|
||||
|
||||
return Input
|
54
src/lib/luigi/launch.lua
Normal file
|
@ -0,0 +1,54 @@
|
|||
--[[--
|
||||
Launcher for the LuaJIT backend.
|
||||
|
||||
Looks for main.lua. Launch it like this:
|
||||
|
||||
luajit myapp/lib/luigi/launch.lua
|
||||
|
||||
If luigi isn't inside the project directory, pass
|
||||
the path containing main.lua as the second argument.
|
||||
The path must end with a directory separator.
|
||||
|
||||
luajit /opt/luigi/launch.lua ./myapp/
|
||||
|
||||
If the app prefixes luigi modules with something
|
||||
other then 'luigi', pass that prefix as the third
|
||||
argument.
|
||||
|
||||
luajit /opt/luigi/launch.lua ./myapp/ lib.luigi
|
||||
|
||||
--]]--
|
||||
local packagePath = package.path
|
||||
local libRoot = arg[0]:gsub('[^/\\]*%.lua$', '')
|
||||
local appRoot, modPath = ...
|
||||
|
||||
local function run (appRoot, modPath)
|
||||
package.path = packagePath .. ';' .. appRoot .. '?.lua'
|
||||
rawset(_G, 'LUIGI_APP_ROOT', appRoot)
|
||||
require 'main'
|
||||
require (modPath .. '.backend').run()
|
||||
end
|
||||
|
||||
-- expect to find main.lua in appRoot if specified
|
||||
if appRoot then
|
||||
return run(appRoot, modPath or 'luigi')
|
||||
end
|
||||
|
||||
-- try to find main.lua in a parent of this library, recursively.
|
||||
local lastLibRoot = libRoot
|
||||
repeat
|
||||
if io.open(libRoot .. 'main.lua') then
|
||||
return run(libRoot, modPath)
|
||||
end
|
||||
lastLibRoot = libRoot
|
||||
libRoot = libRoot:gsub('([^/\\]*).$', function (m)
|
||||
modPath = modPath and (m .. '.' .. modPath) or m
|
||||
return ''
|
||||
end)
|
||||
until libRoot == lastLibRoot
|
||||
|
||||
error 'main.lua not found'
|
||||
|
||||
|
||||
|
||||
|
444
src/lib/luigi/layout.lua
Normal file
|
@ -0,0 +1,444 @@
|
|||
--[[--
|
||||
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 Backend = require(ROOT .. 'backend')
|
||||
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 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, master)
|
||||
data = data or {}
|
||||
|
||||
if master then
|
||||
self:setMaster(master)
|
||||
else
|
||||
self:setTheme(require(ROOT .. 'theme.light'))
|
||||
self:setStyle()
|
||||
end
|
||||
|
||||
self:addDefaultHandlers()
|
||||
|
||||
self.hooks = {}
|
||||
self.isShown = false
|
||||
self.root = data
|
||||
|
||||
Widget(self, data)
|
||||
|
||||
self.isReady = true
|
||||
end
|
||||
|
||||
--[[--
|
||||
Create a detached widget.
|
||||
|
||||
Internal function used to create widgets that are associated with
|
||||
a layout, but "detached" from it.
|
||||
|
||||
Used by context menus, which use their "owner" widget's layout
|
||||
for theme and style information but appear in a separate layout.
|
||||
|
||||
@tparam table data
|
||||
A tree of widget data.
|
||||
|
||||
@treturn Widget
|
||||
A widget instance.
|
||||
--]]--
|
||||
function Layout:createWidget (data)
|
||||
return Widget(self, data)
|
||||
end
|
||||
|
||||
local function clearWidget (widget)
|
||||
widget.textData = nil
|
||||
widget.fontData = nil
|
||||
widget.position = {}
|
||||
widget.dimensions = {}
|
||||
widget.type = widget.type
|
||||
for _, child in ipairs(widget) do
|
||||
clearWidget(child)
|
||||
end
|
||||
local items = widget.items
|
||||
if items then
|
||||
for _, item in ipairs(items) do
|
||||
clearWidget(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function reset (self)
|
||||
if not self.root then return end
|
||||
clearWidget(self.root)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Set the master layout for this layout.
|
||||
|
||||
This layout's theme and style will be set the same as the master layout, and
|
||||
widgets added to this layout will be indexed and keyboard-accelerated by the
|
||||
master layout instead of this layout.
|
||||
|
||||
@tparam Layout layout
|
||||
Master layout
|
||||
|
||||
@treturn Layout Self
|
||||
--]]--
|
||||
function Layout:setMaster (layout)
|
||||
self.master = layout
|
||||
|
||||
reset(self)
|
||||
return self
|
||||
end
|
||||
|
||||
--[[--
|
||||
Set the style from a definition table or function.
|
||||
|
||||
@tparam table|function rules
|
||||
Style definition.
|
||||
|
||||
@treturn Layout Self
|
||||
--]]--
|
||||
function Layout:setStyle (rules)
|
||||
if type(rules) == 'function' then
|
||||
rules = rules()
|
||||
end
|
||||
self.style = Style(rules or {}, { 'style' })
|
||||
|
||||
reset(self)
|
||||
return self
|
||||
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' })
|
||||
reset(self)
|
||||
return self
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the style from master layout or this layout.
|
||||
|
||||
@treturn table
|
||||
Style table.
|
||||
--]]--
|
||||
function Layout:getStyle ()
|
||||
return self.master and self.master:getStyle() or self.style
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the theme from master layout or this layout.
|
||||
|
||||
@treturn table
|
||||
Theme table.
|
||||
--]]--
|
||||
function Layout:getTheme ()
|
||||
return self.master and self.master:getTheme() or self.theme
|
||||
end
|
||||
|
||||
--[[--
|
||||
Show the layout.
|
||||
|
||||
Hooks all appropriate Love events and callbacks.
|
||||
|
||||
@treturn Layout
|
||||
Return this layout for chaining.
|
||||
--]]--
|
||||
function Layout:show ()
|
||||
if self.isShown then
|
||||
Backend.hide(self)
|
||||
self.isShown = nil
|
||||
end
|
||||
|
||||
self.isShown = true
|
||||
|
||||
if not self.input then
|
||||
self.input = Input.default -- Input(self)
|
||||
end
|
||||
|
||||
Backend.show(self)
|
||||
self.root:reshape()
|
||||
|
||||
Event.Show:emit(self, self)
|
||||
return self
|
||||
end
|
||||
|
||||
--[[--
|
||||
Hide the layout.
|
||||
|
||||
Unhooks Love events and callbacks.
|
||||
|
||||
@treturn Layout
|
||||
Return this layout for chaining.
|
||||
--]]--
|
||||
function Layout:hide ()
|
||||
if not self.isShown then
|
||||
return
|
||||
end
|
||||
self.isShown = nil
|
||||
Backend.hide(self)
|
||||
|
||||
Event.Hide:emit(self, self)
|
||||
return self
|
||||
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)
|
||||
if not root then
|
||||
root = self.root
|
||||
end
|
||||
-- Loop through in reverse, because siblings defined later in the tree
|
||||
-- will overdraw earlier siblings.
|
||||
for i = #root, 1, -1 do
|
||||
local child = root[i]
|
||||
if child:isAt(x, y) then
|
||||
local inner = self:getWidgetAt(x, y, child)
|
||||
if inner then return inner end
|
||||
end
|
||||
end
|
||||
|
||||
if root:isAt(x, y) then return root end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Place a layout near a point or rectangle.
|
||||
|
||||
@tparam number left
|
||||
Number of pixels from window's left edge.
|
||||
|
||||
@tparam number top
|
||||
Number of pixels from window's top edge.
|
||||
|
||||
@tparam[opt] number width
|
||||
Width of the rectangle to place layout outside of, defaults to 0.
|
||||
|
||||
@tparam[opt] number height
|
||||
Height of the rectangle to place layout outside of, defaults to 0.
|
||||
|
||||
@treturn Layout
|
||||
Return this layout for chaining.
|
||||
--]]--
|
||||
function Layout:placeNear (left, top, width, height)
|
||||
width, height = width or 0, height or 0
|
||||
local root = self.root
|
||||
-- place to the left if there's no room to the right
|
||||
local layoutWidth = root:getWidth()
|
||||
local windowWidth, windowHeight = Backend.getWindowSize()
|
||||
if left + width + layoutWidth > windowWidth then
|
||||
left = left - layoutWidth - width
|
||||
else
|
||||
left = left + width
|
||||
end
|
||||
-- place above if there's no room below
|
||||
local layoutHeight = root:getHeight()
|
||||
if top + height + layoutHeight > windowHeight then
|
||||
top = top - layoutHeight - height
|
||||
else
|
||||
top = top + height
|
||||
end
|
||||
root.left = left
|
||||
root.top = top
|
||||
end
|
||||
|
||||
|
||||
-- Add handlers for keyboard shortcuts, tab focus, and mouse wheel scroll
|
||||
function Layout:addDefaultHandlers ()
|
||||
self.shortcuts = {}
|
||||
|
||||
for i = 0, 15 do
|
||||
self.shortcuts[i] = {}
|
||||
end
|
||||
|
||||
self.behavior = {}
|
||||
|
||||
local function createBehavior (name, hooks)
|
||||
self.behavior[name] = hooks
|
||||
function hooks.destroy ()
|
||||
for _, hook in ipairs(hooks) do
|
||||
hook:unhook()
|
||||
end
|
||||
self.behavior[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
createBehavior('context', {
|
||||
self:onPressStart(function (event)
|
||||
-- show context menu on right click
|
||||
if event.button ~= 'right' then return end
|
||||
local menu = event.target.context
|
||||
if not menu then return end
|
||||
menu:bubbleEvent('PressStart', event)
|
||||
-- make sure it fits in the window
|
||||
-- TODO: open in a new borderless window under SDL?
|
||||
menu.menuLayout:placeNear(event.x - 1, event.y - 1, 2, 2)
|
||||
return false
|
||||
end)
|
||||
})
|
||||
|
||||
createBehavior('shortcut', {
|
||||
self:onKeyPress(function (event)
|
||||
local entry = self.shortcuts[event.modifierFlags]
|
||||
local widget = entry and entry[event.key]
|
||||
if not widget then return end
|
||||
widget.hovered = true
|
||||
self.input:handlePressStart(self, 'left', event.x, event.y,
|
||||
widget, widget.shortcut)
|
||||
return false
|
||||
end),
|
||||
|
||||
self:onKeyRelease(function (event)
|
||||
local entry = self.shortcuts[event.modifierFlags]
|
||||
local widget = entry and entry[event.key]
|
||||
if not widget then return end
|
||||
widget.hovered = false
|
||||
self.input:handlePressEnd(self, 'left', event.x, event.y,
|
||||
widget, widget.shortcut)
|
||||
return false
|
||||
end)
|
||||
})
|
||||
|
||||
createBehavior('navigate', {
|
||||
self:onKeyPress(function (event)
|
||||
-- tab/shift-tab cycles focused widget
|
||||
if event.key == 'tab' then
|
||||
if Backend.isKeyDown('lshift', 'rshift') then
|
||||
self:focusPreviousWidget()
|
||||
else
|
||||
self:focusNextWidget()
|
||||
end
|
||||
return false
|
||||
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, 'left', event.x, event.y,
|
||||
widget, event.key)
|
||||
return false
|
||||
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, 'left', event.x, event.y,
|
||||
widget, event.key)
|
||||
return false
|
||||
end
|
||||
end)
|
||||
})
|
||||
|
||||
createBehavior('scroll', {
|
||||
self:onWheelMove(function (event)
|
||||
if not event.hit then return end
|
||||
local amount = event.scrollY ~= 0 and event.scrollY or event.scrollX
|
||||
for widget in event.target:eachAncestor(true) do
|
||||
if widget:scrollBy(amount) then return false end
|
||||
end -- ancestor loop
|
||||
return false
|
||||
end) -- wheel move
|
||||
})
|
||||
|
||||
createBehavior('status', {
|
||||
self:onEnter(function (event)
|
||||
local statusWidget = (self.master or self).statusWidget
|
||||
if not statusWidget then return end
|
||||
|
||||
statusWidget.text = event.target.status
|
||||
return false
|
||||
end)
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
Event.injectBinders(Layout)
|
||||
|
||||
return Layout
|
110
src/lib/luigi/mosaic.lua
Normal file
|
@ -0,0 +1,110 @@
|
|||
--[[--
|
||||
Mosiac, drawable class for 9-slice images.
|
||||
|
||||
@classmod Mosiac
|
||||
--]]--
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
local Base = require(ROOT .. 'base')
|
||||
|
||||
local Mosaic = Base:extend()
|
||||
|
||||
local imageCache = {}
|
||||
local sliceCache = {}
|
||||
|
||||
local function loadImage (path)
|
||||
if not imageCache[path] then
|
||||
imageCache[path] = Backend.Image(path)
|
||||
end
|
||||
|
||||
return imageCache[path]
|
||||
end
|
||||
|
||||
function Mosaic.fromWidget (widget)
|
||||
local mosaic = widget.mosaic
|
||||
if mosaic and mosaic.slicePath == widget.slices then
|
||||
return mosaic
|
||||
end
|
||||
if widget.slices then
|
||||
widget.mosaic = Mosaic(widget.slices)
|
||||
return widget.mosaic
|
||||
end
|
||||
end
|
||||
|
||||
function Mosaic:constructor (path)
|
||||
local slices = self:loadSlices(path)
|
||||
self.batch = Backend.SpriteBatch(slices.image)
|
||||
self.slices = slices
|
||||
self.slicePath = path
|
||||
end
|
||||
|
||||
function Mosaic:setRectangle (x, y, w, h)
|
||||
if self.x == x and self.y == y and self.width == w and self.height == h then
|
||||
self.needsRefresh = false
|
||||
return
|
||||
end
|
||||
self.needsRefresh = true
|
||||
self.x, self.y, self.width, self.height = x, y, w, h
|
||||
end
|
||||
|
||||
function Mosaic:loadSlices (path)
|
||||
local slices = sliceCache[path]
|
||||
|
||||
if not slices then
|
||||
slices = {}
|
||||
sliceCache[path] = slices
|
||||
local image = loadImage(path)
|
||||
local iw, ih = image:getWidth(), image:getHeight()
|
||||
local w, h = math.floor(iw / 3), math.floor(ih / 3)
|
||||
local Quad = Backend.Quad
|
||||
|
||||
slices.image = image
|
||||
slices.width = w
|
||||
slices.height = h
|
||||
|
||||
slices.topLeft = Quad(0, 0, w, h, iw, ih)
|
||||
slices.topCenter = Quad(w, 0, w, h, iw, ih)
|
||||
slices.topRight = Quad(iw - w, 0, w, h, iw, ih)
|
||||
slices.middleLeft = Quad(0, h, w, h, iw, ih)
|
||||
slices.middleCenter = Quad(w, h, w, h, iw, ih)
|
||||
slices.middleRight = Quad(iw - w, h, w, h, iw, ih)
|
||||
slices.bottomLeft = Quad(0, ih - h, w, h, iw, ih)
|
||||
slices.bottomCenter = Quad(w, ih - h, w, h, iw, ih)
|
||||
slices.bottomRight = Quad(iw - w, ih - h, w, h, iw, ih)
|
||||
end
|
||||
|
||||
return slices
|
||||
end
|
||||
|
||||
function Mosaic:draw ()
|
||||
local batch = self.batch
|
||||
|
||||
if not self.needsRefresh then
|
||||
Backend.draw(batch)
|
||||
return
|
||||
end
|
||||
self.needsRefresh = false
|
||||
|
||||
local x, y, w, h = self.x, self.y, self.width, self.height
|
||||
local slices = self.slices
|
||||
local sw, sh = slices.width, slices.height
|
||||
local xs = (w - sw * 2) / sw -- x scale
|
||||
local ys = (h - sh * 2) / sh -- y scale
|
||||
|
||||
batch:clear()
|
||||
|
||||
batch:add(slices.middleCenter, x + sw, y + sh, 0, xs, ys)
|
||||
batch:add(slices.topCenter, x + sw, y, 0, xs, 1)
|
||||
batch:add(slices.bottomCenter, x + sw, y + h - sh, 0, xs, 1)
|
||||
batch:add(slices.middleLeft, x, y + sh, 0, 1, ys)
|
||||
batch:add(slices.middleRight, x + w - sw, y + sh, 0, 1, ys)
|
||||
batch:add(slices.topLeft, x, y)
|
||||
batch:add(slices.topRight, x + w - sw, y)
|
||||
batch:add(slices.bottomLeft, x, y + h - sh)
|
||||
batch:add(slices.bottomRight, x + w - sw, y + h - sh)
|
||||
|
||||
Backend.draw(batch)
|
||||
end
|
||||
|
||||
return Mosaic
|
57
src/lib/luigi/multiline.lua
Normal file
|
@ -0,0 +1,57 @@
|
|||
local Multiline = {}
|
||||
|
||||
function Multiline.wrap (font, text, limit)
|
||||
local lines = {{ width = 0 }}
|
||||
local advance = 0
|
||||
local lastSpaceAdvance = 0
|
||||
|
||||
local function append (word, space)
|
||||
local wordAdvance = font:getAdvance(word)
|
||||
local spaceAdvance = font:getAdvance(space)
|
||||
local words = lines[#lines]
|
||||
if advance + wordAdvance > limit then
|
||||
words.width = (words.width or 0) - lastSpaceAdvance
|
||||
advance = wordAdvance + spaceAdvance
|
||||
lines[#lines + 1] = { width = advance, word, space }
|
||||
else
|
||||
advance = advance + wordAdvance + spaceAdvance
|
||||
words.width = advance
|
||||
words[#words + 1] = word
|
||||
words[#words + 1] = space
|
||||
end
|
||||
lastSpaceAdvance = spaceAdvance
|
||||
end
|
||||
|
||||
local function appendFrag (frag, isFirst)
|
||||
if isFirst then
|
||||
append(frag, '')
|
||||
else
|
||||
local wordAdvance = font:getAdvance(frag)
|
||||
lines[#lines + 1] = { width = wordAdvance, frag }
|
||||
advance = wordAdvance
|
||||
end
|
||||
end
|
||||
|
||||
local leadSpace = text:match '^ +'
|
||||
|
||||
if leadSpace then
|
||||
append('', leadSpace)
|
||||
end
|
||||
|
||||
for word, space in text:gmatch '([^ ]+)( *)' do
|
||||
if word:match '\n' then
|
||||
local isFirst = true
|
||||
for frag in (word .. '\n'):gmatch '([^\n]*)\n' do
|
||||
appendFrag(frag, isFirst)
|
||||
isFirst = false
|
||||
end
|
||||
append('', space)
|
||||
else
|
||||
append(word, space)
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
return Multiline
|
237
src/lib/luigi/painter.lua
Normal file
|
@ -0,0 +1,237 @@
|
|||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
local Base = require(ROOT .. 'base')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Mosaic = require(ROOT .. 'mosaic')
|
||||
local Text = Backend.Text
|
||||
|
||||
local Painter = Base:extend()
|
||||
|
||||
local imageCache = {}
|
||||
-- local sliceCache = {}
|
||||
|
||||
function Painter:constructor (widget)
|
||||
self.widget = widget
|
||||
end
|
||||
|
||||
function Painter:loadImage (path)
|
||||
if not imageCache[path] then
|
||||
imageCache[path] = Backend.Image(path)
|
||||
end
|
||||
|
||||
return imageCache[path]
|
||||
end
|
||||
|
||||
function Painter:paintSlices ()
|
||||
local widget = self.widget
|
||||
local mosaic = Mosaic.fromWidget(widget)
|
||||
if not mosaic then return end
|
||||
local x, y, w, h = widget:getRectangle(true)
|
||||
mosaic:setRectangle(x, y, w, h)
|
||||
mosaic:draw()
|
||||
end
|
||||
|
||||
function Painter:paintBackground ()
|
||||
local widget = self.widget
|
||||
if not widget.background then return end
|
||||
local x, y, w, h = widget:getRectangle(true)
|
||||
|
||||
Backend.push()
|
||||
Backend.setColor(widget.background)
|
||||
Backend.drawRectangle('fill', x, y, w, h)
|
||||
Backend.pop()
|
||||
end
|
||||
|
||||
function Painter:paintOutline ()
|
||||
local widget = self.widget
|
||||
if not widget.outline then return end
|
||||
local x, y, w, h = widget:getRectangle(true)
|
||||
|
||||
Backend.push()
|
||||
Backend.setColor(widget.outline)
|
||||
Backend.drawRectangle('line', x + 0.5, y + 0.5, w, h)
|
||||
Backend.pop()
|
||||
end
|
||||
|
||||
-- returns icon coordinates and rectangle with remaining space
|
||||
function Painter:positionIcon (x1, y1, x2, y2)
|
||||
local widget = self.widget
|
||||
if not widget.icon then
|
||||
return nil, nil, x1, y1, x2, y2
|
||||
end
|
||||
|
||||
local icon = self:loadImage(widget.icon)
|
||||
local iconWidth, iconHeight = icon:getWidth(), icon:getHeight()
|
||||
local align = widget.align or ''
|
||||
local padding = widget.padding or 0
|
||||
local x, y
|
||||
|
||||
-- horizontal alignment
|
||||
if align:find('right') then
|
||||
x = x2 - iconWidth
|
||||
x2 = x2 - iconWidth - padding
|
||||
elseif align:find('center') then
|
||||
x = x1 + (x2 - x1) / 2 - iconWidth / 2
|
||||
else -- if align:find('left') then
|
||||
x = x1
|
||||
x1 = x1 + iconWidth + padding
|
||||
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
|
||||
|
||||
return x, y, x1, y1, x2, y2
|
||||
end
|
||||
|
||||
-- returns text coordinates
|
||||
function Painter:positionText (x1, y1, x2, y2)
|
||||
local widget = self.widget
|
||||
if not widget.text or x1 >= x2 then
|
||||
return nil, nil, x1, y1, x2, y2
|
||||
end
|
||||
|
||||
local font = widget:getFont()
|
||||
local align = widget.align or ''
|
||||
local horizontal = 'left'
|
||||
|
||||
-- horizontal alignment
|
||||
if align:find 'right' then
|
||||
horizontal = 'right'
|
||||
elseif align:find 'center' then
|
||||
horizontal = 'center'
|
||||
elseif align:find 'justify' then
|
||||
horizontal = 'justify'
|
||||
end
|
||||
|
||||
if not widget.textData then
|
||||
local limit = widget.wrap and x2 - x1 or nil
|
||||
widget.textData = Text(
|
||||
font, widget.text, widget.color, horizontal, limit)
|
||||
end
|
||||
|
||||
local textHeight = widget.textData:getHeight()
|
||||
local y
|
||||
|
||||
-- vertical alignment
|
||||
if align:find('bottom') then
|
||||
y = y2 - textHeight
|
||||
elseif align:find('middle') then
|
||||
y = y2 - (y2 - y1) / 2 - textHeight / 2
|
||||
else -- if align:find('top') then
|
||||
y = y1
|
||||
end
|
||||
|
||||
return font, x1, y
|
||||
end
|
||||
|
||||
function Painter:paintIconAndText ()
|
||||
local widget = self.widget
|
||||
if not (widget.icon or widget.text) then return end
|
||||
local x, y, w, h = widget:getRectangle(true, true)
|
||||
if w < 1 or h < 1 then return end
|
||||
|
||||
-- calculate position for icon and text based on alignment and padding
|
||||
local iconX, iconY, x1, y1, x2, y2 = self:positionIcon(x, y, x + w, y + h)
|
||||
local font, textX, textY = self:positionText(x1, y1, x2, y2)
|
||||
|
||||
local icon = widget.icon and self:loadImage(widget.icon)
|
||||
local text = widget.text
|
||||
local align = widget.align or ''
|
||||
local padding = widget.padding or 0
|
||||
|
||||
-- if aligned center, icon displays above the text
|
||||
-- reposition icon and text for proper vertical alignment
|
||||
if icon and text and align:find('center') then
|
||||
local iconHeight = icon:getHeight()
|
||||
|
||||
if align:find 'middle' then
|
||||
local textHeight = widget.textData:getHeight()
|
||||
local contentHeight = textHeight + padding + iconHeight
|
||||
local offset = (h - contentHeight) / 2
|
||||
iconY = y + offset
|
||||
textY = y + offset + padding + iconHeight
|
||||
elseif align:find 'top' then
|
||||
iconY = y
|
||||
textY = y + padding + iconHeight
|
||||
else -- if align:find 'bottom'
|
||||
local textHeight = widget.textData:getHeight()
|
||||
textY = y + h - textHeight
|
||||
iconY = textY - padding - iconHeight
|
||||
end
|
||||
end
|
||||
|
||||
-- horizontal alignment for non-wrapped text
|
||||
-- TODO: handle this in Backend.Text
|
||||
if text and not widget.wrap then
|
||||
if align:find 'right' then
|
||||
textX = textX + (w - widget.textData:getWidth())
|
||||
elseif align:find 'center' then
|
||||
textX = textX + (w - widget.textData:getWidth()) / 2
|
||||
end
|
||||
end
|
||||
|
||||
Backend.push()
|
||||
|
||||
Backend.intersectScissor(x, y, w, h)
|
||||
|
||||
-- draw the icon
|
||||
if icon then
|
||||
iconX, iconY = math.floor(iconX), math.floor(iconY)
|
||||
Backend.draw(icon, iconX, iconY)
|
||||
end
|
||||
|
||||
-- draw the text
|
||||
if text and textX and textY and w > 1 then
|
||||
widget.innerHeight = textY - y + widget.textData:getHeight()
|
||||
widget.innerWidth = textX - x + widget.textData:getWidth()
|
||||
textX = math.floor(textX - (widget.scrollX or 0))
|
||||
textY = math.floor(textY - (widget.scrollY or 0))
|
||||
Backend.draw(widget.textData, textX, textY)
|
||||
end
|
||||
|
||||
Backend.pop()
|
||||
end
|
||||
|
||||
function Painter:paintChildren ()
|
||||
for i, child in ipairs(self.widget) do
|
||||
child:paint()
|
||||
end
|
||||
end
|
||||
|
||||
function Painter:paint ()
|
||||
local widget = self.widget
|
||||
local x, y, w, h = widget:getRectangle()
|
||||
|
||||
-- if the drawable area has no width or height, don't paint
|
||||
if w < 1 or h < 1 then return end
|
||||
|
||||
Event.PreDisplay:emit(widget, { target = widget }, function()
|
||||
|
||||
Backend.push()
|
||||
|
||||
if widget.parent then
|
||||
Backend.intersectScissor(x, y, w, h)
|
||||
else
|
||||
Backend.setScissor()
|
||||
end
|
||||
|
||||
self:paintBackground()
|
||||
self:paintOutline()
|
||||
self:paintSlices()
|
||||
self:paintIconAndText()
|
||||
self:paintChildren()
|
||||
|
||||
Backend.pop()
|
||||
|
||||
end)
|
||||
Event.Display:emit(widget, { target = widget })
|
||||
end
|
||||
|
||||
return Painter
|
99
src/lib/luigi/shortcut.lua
Normal file
|
@ -0,0 +1,99 @@
|
|||
--[[--
|
||||
Keyboard shortcut module.
|
||||
--]]--
|
||||
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
|
||||
local Shortcut = {}
|
||||
|
||||
local isMac = Backend.isMac()
|
||||
|
||||
local ALT = 1
|
||||
local CTRL = 2
|
||||
local SHIFT = 4
|
||||
local GUI = 8
|
||||
|
||||
function Shortcut.appliesToPlatform (value)
|
||||
if isMac and value:match '%f[%a]win%-'
|
||||
or not isMac and value:match '%f[%a]mac%-' then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Shortcut.expandAliases (value)
|
||||
return value
|
||||
:gsub('%f[%a]cmd%-', 'mac-gui-')
|
||||
:gsub('%f[%a]command%-', 'mac-gui-')
|
||||
:gsub('%f[%a]option%-', 'mac-alt-')
|
||||
end
|
||||
|
||||
function Shortcut.parseKeyCombo (value)
|
||||
-- expand command- and option- aliases
|
||||
value = Shortcut.expandAliases(value)
|
||||
|
||||
-- exit early if shortcut is for different platform
|
||||
if not Shortcut.appliesToPlatform(value) then return end
|
||||
|
||||
-- expand c- special modifier
|
||||
if isMac then
|
||||
value = value:gsub('%f[%a]c%-', 'gui-')
|
||||
else
|
||||
value = value:gsub('%f[%a]c%-', 'ctrl-')
|
||||
end
|
||||
|
||||
-- extract main key
|
||||
local mainKey = value:match '[^%-]*%-?$'
|
||||
|
||||
-- extract modifiers
|
||||
local alt = value:match '%f[%a]alt%-' and ALT or 0
|
||||
local ctrl = value:match '%f[%a]ctrl%-' and CTRL or 0
|
||||
local shift = value:match '%f[%a]shift%-' and SHIFT or 0
|
||||
local gui = value:match '%f[%a]gui%-' and GUI or 0
|
||||
|
||||
return mainKey, alt + ctrl + shift + gui
|
||||
end
|
||||
|
||||
function Shortcut.getModifierFlags ()
|
||||
local alt = Backend.isKeyDown('lalt', 'ralt') and ALT or 0
|
||||
local ctrl = Backend.isKeyDown('lctrl', 'rctrl') and CTRL or 0
|
||||
local shift = Backend.isKeyDown('lshift', 'rshift') and SHIFT or 0
|
||||
local gui = Backend.isKeyDown('lgui', 'rgui') and GUI or 0
|
||||
return alt + ctrl + shift + gui
|
||||
end
|
||||
|
||||
function Shortcut.stringify (shortcut)
|
||||
if type(shortcut) ~= 'table' then
|
||||
shortcut = { shortcut }
|
||||
end
|
||||
for _, value in ipairs(shortcut) do
|
||||
value = Shortcut.expandAliases(value)
|
||||
if Shortcut.appliesToPlatform(value) then
|
||||
if isMac then
|
||||
value = value
|
||||
:gsub('%f[%a]c%-', 'cmd-')
|
||||
:gsub('%f[%a]gui%-', 'cmd-')
|
||||
:gsub('%f[%a]alt%-', 'option-')
|
||||
-- Have Love backend default to DejaVuSans
|
||||
-- so we can use these instead of the above
|
||||
--[[
|
||||
:gsub('%f[%a]c%-', '⌘')
|
||||
:gsub('%f[%a]gui%-', '⌘')
|
||||
:gsub('%f[%a]alt%-', '⌥')
|
||||
:gsub('%f[%a]shift%-', '⇧')
|
||||
]]
|
||||
else
|
||||
value = value
|
||||
:gsub('%f[%a]c%-', 'ctrl-')
|
||||
:gsub('%f[%a]gui%-', 'windows-')
|
||||
end
|
||||
value = value:gsub('%f[%a]win%-', ''):gsub('%f[%a]mac%-', '')
|
||||
value = value:gsub('%f[%w].', string.upper)
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Shortcut
|
40
src/lib/luigi/style.lua
Normal file
|
@ -0,0 +1,40 @@
|
|||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Base = require(ROOT .. 'base')
|
||||
|
||||
local Style = Base:extend()
|
||||
|
||||
function Style:constructor (rules, lookupNames)
|
||||
self.rules = rules
|
||||
self.lookupNames = lookupNames
|
||||
end
|
||||
|
||||
function Style:getProperty (object, property, original)
|
||||
local value = rawget(object, property)
|
||||
if value ~= nil then return value end
|
||||
|
||||
local rules = self.rules
|
||||
original = original or object
|
||||
|
||||
for _, lookupName in ipairs(self.lookupNames) do
|
||||
local lookup = rawget(object, lookupName)
|
||||
or object.attributes and rawget(object.attributes, lookupName)
|
||||
if lookup then
|
||||
if type(lookup) ~= 'table' then
|
||||
lookup = { lookup }
|
||||
end
|
||||
for _, lookupValue in ipairs(lookup) do
|
||||
local rule = rules[lookupValue]
|
||||
if rule then
|
||||
local value = self:getProperty(rule, property, original)
|
||||
if type(value) == 'function' then
|
||||
value = value(original)
|
||||
end
|
||||
if value ~= nil then return value end
|
||||
end
|
||||
end -- lookup values
|
||||
end -- if lookup
|
||||
end -- lookup names
|
||||
end
|
||||
|
||||
return Style
|
12
src/lib/luigi/theme/dark.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
local RESOURCE = (...):gsub('%.', '/') .. '/'
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
return function (config)
|
||||
config = config or {}
|
||||
config.resources = config.resources or RESOURCE
|
||||
config.backColor = config.backColor or { 40, 40, 40 }
|
||||
config.lineColor = config.lineColor or { 60, 60, 60 }
|
||||
config.textColor = config.textColor or { 240, 240, 240 }
|
||||
config.highlight = config.highlight or { 0x00, 0x5c, 0x94 }
|
||||
return require(ROOT .. 'engine.alpha')(config)
|
||||
end
|
BIN
src/lib/luigi/theme/dark/button.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
src/lib/luigi/theme/dark/button_focused.png
Normal file
After Width: | Height: | Size: 580 B |
BIN
src/lib/luigi/theme/dark/button_hovered.png
Normal file
After Width: | Height: | Size: 490 B |
BIN
src/lib/luigi/theme/dark/button_pressed.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
src/lib/luigi/theme/dark/check_checked.png
Normal file
After Width: | Height: | Size: 512 B |
BIN
src/lib/luigi/theme/dark/check_checked_focused.png
Normal file
After Width: | Height: | Size: 523 B |
BIN
src/lib/luigi/theme/dark/check_checked_pressed.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
src/lib/luigi/theme/dark/check_unchecked.png
Normal file
After Width: | Height: | Size: 271 B |
BIN
src/lib/luigi/theme/dark/check_unchecked_focused.png
Normal file
After Width: | Height: | Size: 305 B |
BIN
src/lib/luigi/theme/dark/check_unchecked_pressed.png
Normal file
After Width: | Height: | Size: 255 B |
BIN
src/lib/luigi/theme/dark/progress.png
Normal file
After Width: | Height: | Size: 350 B |
BIN
src/lib/luigi/theme/dark/radio_checked.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
src/lib/luigi/theme/dark/radio_checked_focused.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
src/lib/luigi/theme/dark/radio_checked_pressed.png
Normal file
After Width: | Height: | Size: 595 B |
BIN
src/lib/luigi/theme/dark/radio_unchecked.png
Normal file
After Width: | Height: | Size: 491 B |
BIN
src/lib/luigi/theme/dark/radio_unchecked_focused.png
Normal file
After Width: | Height: | Size: 556 B |
BIN
src/lib/luigi/theme/dark/radio_unchecked_pressed.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
src/lib/luigi/theme/dark/submenu.png
Normal file
After Width: | Height: | Size: 768 B |
BIN
src/lib/luigi/theme/dark/text.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
src/lib/luigi/theme/dark/text_focused.png
Normal file
After Width: | Height: | Size: 556 B |
BIN
src/lib/luigi/theme/dark/triangle_down.png
Normal file
After Width: | Height: | Size: 188 B |
BIN
src/lib/luigi/theme/dark/triangle_left.png
Normal file
After Width: | Height: | Size: 186 B |
BIN
src/lib/luigi/theme/dark/triangle_right.png
Normal file
After Width: | Height: | Size: 194 B |
BIN
src/lib/luigi/theme/dark/triangle_up.png
Normal file
After Width: | Height: | Size: 185 B |
13
src/lib/luigi/theme/light-big.lua
Normal file
|
@ -0,0 +1,13 @@
|
|||
local RESOURCE = (...):gsub('%.', '/') .. '/'
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
return function (config)
|
||||
local theme = require(ROOT .. 'theme.light')()
|
||||
theme.Control._defaultDimension = 44
|
||||
theme.Line._defaultDimension = 32
|
||||
theme.menu.height = 32
|
||||
theme['menu.item'].height = 32
|
||||
theme['menu.item'].padding = 8
|
||||
theme.panel.padding = 8
|
||||
return theme
|
||||
end
|
12
src/lib/luigi/theme/light.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
local RESOURCE = (...):gsub('%.', '/') .. '/'
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
return function (config)
|
||||
config = config or {}
|
||||
config.resources = config.resources or RESOURCE
|
||||
config.backColor = config.backColor or { 240, 240, 240 }
|
||||
config.lineColor = config.lineColor or { 220, 220, 220 }
|
||||
config.textColor = config.textColor or { 0, 0, 0 }
|
||||
config.highlight = config.highlight or { 0x19, 0xAE, 0xFF }
|
||||
return require(ROOT .. 'engine.alpha')(config)
|
||||
end
|
BIN
src/lib/luigi/theme/light/button.png
Normal file
After Width: | Height: | Size: 491 B |
BIN
src/lib/luigi/theme/light/button_disabled.png
Normal file
After Width: | Height: | Size: 534 B |
BIN
src/lib/luigi/theme/light/button_focused.png
Normal file
After Width: | Height: | Size: 597 B |
BIN
src/lib/luigi/theme/light/button_hovered.png
Normal file
After Width: | Height: | Size: 521 B |
BIN
src/lib/luigi/theme/light/button_pressed.png
Normal file
After Width: | Height: | Size: 495 B |
BIN
src/lib/luigi/theme/light/check_checked.png
Normal file
After Width: | Height: | Size: 563 B |
BIN
src/lib/luigi/theme/light/check_checked_focused.png
Normal file
After Width: | Height: | Size: 648 B |
BIN
src/lib/luigi/theme/light/check_checked_pressed.png
Normal file
After Width: | Height: | Size: 567 B |
BIN
src/lib/luigi/theme/light/check_unchecked.png
Normal file
After Width: | Height: | Size: 297 B |
BIN
src/lib/luigi/theme/light/check_unchecked_focused.png
Normal file
After Width: | Height: | Size: 313 B |
BIN
src/lib/luigi/theme/light/check_unchecked_pressed.png
Normal file
After Width: | Height: | Size: 288 B |
BIN
src/lib/luigi/theme/light/light_theme.svgz
Normal file
BIN
src/lib/luigi/theme/light/progress.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
src/lib/luigi/theme/light/radio_checked.png
Normal file
After Width: | Height: | Size: 735 B |
BIN
src/lib/luigi/theme/light/radio_checked_focused.png
Normal file
After Width: | Height: | Size: 802 B |
BIN
src/lib/luigi/theme/light/radio_checked_pressed.png
Normal file
After Width: | Height: | Size: 704 B |
BIN
src/lib/luigi/theme/light/radio_unchecked.png
Normal file
After Width: | Height: | Size: 538 B |
BIN
src/lib/luigi/theme/light/radio_unchecked_focused.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
src/lib/luigi/theme/light/radio_unchecked_pressed.png
Normal file
After Width: | Height: | Size: 520 B |
BIN
src/lib/luigi/theme/light/submenu.png
Normal file
After Width: | Height: | Size: 786 B |
BIN
src/lib/luigi/theme/light/text.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
src/lib/luigi/theme/light/text_focused.png
Normal file
After Width: | Height: | Size: 485 B |
BIN
src/lib/luigi/theme/light/triangle_down.png
Normal file
After Width: | Height: | Size: 188 B |
BIN
src/lib/luigi/theme/light/triangle_left.png
Normal file
After Width: | Height: | Size: 189 B |
BIN
src/lib/luigi/theme/light/triangle_right.png
Normal file
After Width: | Height: | Size: 193 B |
BIN
src/lib/luigi/theme/light/triangle_up.png
Normal file
After Width: | Height: | Size: 185 B |
339
src/lib/luigi/utf8.lua
Normal file
|
@ -0,0 +1,339 @@
|
|||
-- modified for partial compatibility with Lua 5.3
|
||||
|
||||
--utf8 module (Cosmin Apreutesei, public domain).
|
||||
--byte indices are i's, char (codepoint) indices are ci's.
|
||||
--invalid characters are counted as 1-byte chars so they don't get lost. validate/sanitize beforehand as needed.
|
||||
|
||||
local utf8 = {}
|
||||
|
||||
--byte index of the next char after the char at byte index i, followed by a valid flag for the char at byte index i.
|
||||
--nil if not found. invalid characters are iterated as 1-byte chars.
|
||||
function utf8.next_raw(s, i)
|
||||
if not i then
|
||||
if #s == 0 then return nil end
|
||||
return 1, true --fake flag (doesn't matter since this flag is not to be taken as full validation)
|
||||
end
|
||||
if i > #s then return end
|
||||
local c = s:byte(i)
|
||||
if c >= 0x00 and c <= 0x7F then
|
||||
i = i + 1
|
||||
elseif c >= 0xC2 and c <= 0xDF then
|
||||
i = i + 2
|
||||
elseif c >= 0xE0 and c <= 0xEF then
|
||||
i = i + 3
|
||||
elseif c >= 0xF0 and c <= 0xF4 then
|
||||
i = i + 4
|
||||
else --invalid
|
||||
return i + 1, false
|
||||
end
|
||||
if i > #s then return end
|
||||
return i, true
|
||||
end
|
||||
|
||||
--next() is the generic iterator and can be replaced for different semantics. next_raw() must preserve its semantics.
|
||||
utf8.next = utf8.next_raw
|
||||
|
||||
--iterate chars, returning the byte index where each char starts
|
||||
function utf8.byte_indices(s, previ)
|
||||
return utf8.next, s, previ
|
||||
end
|
||||
|
||||
--number of chars in string
|
||||
function utf8.len(s)
|
||||
local len = 0
|
||||
for _ in utf8.byte_indices(s) do
|
||||
len = len + 1
|
||||
end
|
||||
return len
|
||||
end
|
||||
|
||||
--byte index given char index. nil if the index is outside the string.
|
||||
function utf8.byte_index(s, target_ci)
|
||||
if target_ci < 1 then return end
|
||||
local ci = 0
|
||||
for i in utf8.byte_indices(s) do
|
||||
ci = ci + 1
|
||||
if ci == target_ci then
|
||||
return i
|
||||
end
|
||||
end
|
||||
assert(target_ci > ci, 'invalid index')
|
||||
return #s + 1
|
||||
end
|
||||
|
||||
--char index given byte index. nil if the index is outside the string.
|
||||
function utf8.char_index(s, target_i)
|
||||
if target_i < 1 or target_i > #s + 1 then return end
|
||||
local ci = 0
|
||||
for i in utf8.byte_indices(s) do
|
||||
ci = ci + 1
|
||||
if i == target_i then
|
||||
return ci
|
||||
end
|
||||
end
|
||||
return ci + 1
|
||||
-- error'invalid index'
|
||||
end
|
||||
|
||||
--byte index of the prev. char before the char at byte index i, which defaults to #s + 1.
|
||||
--nil if the index is outside the 2..#s+1 range.
|
||||
--NOTE: unlike next(), this is a O(N) operation!
|
||||
function utf8.prev(s, nexti)
|
||||
nexti = nexti or #s + 1
|
||||
if nexti <= 1 or nexti > #s + 1 then return end
|
||||
local lasti, lastvalid = utf8.next(s)
|
||||
for i, valid in utf8.byte_indices(s) do
|
||||
if i == nexti then
|
||||
return lasti, lastvalid
|
||||
end
|
||||
lasti, lastvalid = i, valid
|
||||
end
|
||||
if nexti == #s + 1 then
|
||||
return lasti, lastvalid
|
||||
end
|
||||
error'invalid index'
|
||||
end
|
||||
|
||||
--iterate chars in reverse order, returning the byte index where each char starts.
|
||||
function utf8.byte_indices_reverse(s, nexti)
|
||||
if #s < 200 then
|
||||
--using prev() is a O(N^2/2) operation, ok for small strings (200 chars need 40,000 iterations)
|
||||
return utf8.prev, s, nexti
|
||||
else
|
||||
--store byte indices in a table and iterate them in reverse.
|
||||
--this is 40x slower than byte_indices() but still fast at 2mil chars/second (but eats RAM and makes garbage).
|
||||
local t = {}
|
||||
for i in utf8.byte_indices(s) do
|
||||
if nexti and i >= nexti then break end
|
||||
table.insert(t, i)
|
||||
end
|
||||
local i = #t + 1
|
||||
return function()
|
||||
i = i - 1
|
||||
return t[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--sub based on char indices, which, unlike with standard string.sub(), can't be negative.
|
||||
--start_ci can be 1..inf and end_ci can be 0..inf. end_ci can be nil meaning last char.
|
||||
--if start_ci is out of range or end_ci < start_ci, the empty string is returned.
|
||||
--if end_ci is out of range, it is considered to be the last position in the string.
|
||||
function utf8.sub(s, start_ci, end_ci)
|
||||
--assert for positive indices because we might implement negative indices in the future.
|
||||
assert(start_ci >= 1)
|
||||
assert(not end_ci or end_ci >= 0)
|
||||
local ci = 0
|
||||
local start_i, end_i
|
||||
for i in utf8.byte_indices(s) do
|
||||
ci = ci + 1
|
||||
if ci == start_ci then
|
||||
start_i = i
|
||||
end
|
||||
if ci == end_ci then
|
||||
end_i = i
|
||||
end
|
||||
end
|
||||
if not start_i then
|
||||
assert(start_ci > ci, 'invalid index')
|
||||
return ''
|
||||
end
|
||||
if end_ci and not end_i then
|
||||
if end_ci < start_ci then
|
||||
return ''
|
||||
end
|
||||
assert(end_ci > ci, 'invalid index')
|
||||
end
|
||||
return s:sub(start_i, end_i)
|
||||
end
|
||||
|
||||
--check if a string contains a substring at byte index i without making garbage.
|
||||
--nil if the index is out of range. true if searching for the empty string.
|
||||
function utf8.contains(s, i, sub)
|
||||
if i < 1 or i > #s then return nil end
|
||||
for si = 1, #sub do
|
||||
if s:byte(i + si - 1) ~= sub:byte(si) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--count the number of occurences of a substring in a string. the substring cannot be the empty string.
|
||||
function utf8.count(s, sub)
|
||||
assert(#sub > 0)
|
||||
local count = 0
|
||||
local i = 1
|
||||
while i do
|
||||
if utf8.contains(s, i, sub) then
|
||||
count = count + 1
|
||||
i = i + #sub
|
||||
if i > #s then break end
|
||||
else
|
||||
i = utf8.next(s, i)
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--utf8 validation and sanitization
|
||||
|
||||
--check if there's a valid utf8 codepoint at byte index i. valid ranges for each utf8 byte are:
|
||||
-- byte 1 2 3 4
|
||||
--------------------------------------------
|
||||
-- 00 - 7F
|
||||
-- C2 - DF 80 - BF
|
||||
-- E0 A0 - BF 80 - BF
|
||||
-- E1 - EC 80 - BF 80 - BF
|
||||
-- ED 80 - 9F 80 - BF
|
||||
-- EE - EF 80 - BF 80 - BF
|
||||
-- F0 90 - BF 80 - BF 80 - BF
|
||||
-- F1 - F3 80 - BF 80 - BF 80 - BF
|
||||
-- F4 80 - 8F 80 - BF 80 - BF
|
||||
function utf8.isvalid(s, i)
|
||||
local c = s:byte(i)
|
||||
if not c then
|
||||
return false
|
||||
elseif c >= 0x00 and c <= 0x7F then
|
||||
return true
|
||||
elseif c >= 0xC2 and c <= 0xDF then
|
||||
local c2 = s:byte(i + 1)
|
||||
return c2 and c2 >= 0x80 and c2 <= 0xBF
|
||||
elseif c >= 0xE0 and c <= 0xEF then
|
||||
local c2 = s:byte(i + 1)
|
||||
local c3 = s:byte(i + 2)
|
||||
if c == 0xE0 then
|
||||
return c2 and c3 and
|
||||
c2 >= 0xA0 and c2 <= 0xBF and
|
||||
c3 >= 0x80 and c3 <= 0xBF
|
||||
elseif c >= 0xE1 and c <= 0xEC then
|
||||
return c2 and c3 and
|
||||
c2 >= 0x80 and c2 <= 0xBF and
|
||||
c3 >= 0x80 and c3 <= 0xBF
|
||||
elseif c == 0xED then
|
||||
return c2 and c3 and
|
||||
c2 >= 0x80 and c2 <= 0x9F and
|
||||
c3 >= 0x80 and c3 <= 0xBF
|
||||
elseif c >= 0xEE and c <= 0xEF then
|
||||
if c == 0xEF and c2 == 0xBF and (c3 == 0xBE or c3 == 0xBF) then
|
||||
return false --uFFFE and uFFFF non-characters
|
||||
end
|
||||
return c2 and c3 and
|
||||
c2 >= 0x80 and c2 <= 0xBF and
|
||||
c3 >= 0x80 and c3 <= 0xBF
|
||||
end
|
||||
elseif c >= 0xF0 and c <= 0xF4 then
|
||||
local c2 = s:byte(i + 1)
|
||||
local c3 = s:byte(i + 2)
|
||||
local c4 = s:byte(i + 3)
|
||||
if c == 0xF0 then
|
||||
return c2 and c3 and c4 and
|
||||
c2 >= 0x90 and c2 <= 0xBF and
|
||||
c3 >= 0x80 and c3 <= 0xBF and
|
||||
c4 >= 0x80 and c4 <= 0xBF
|
||||
elseif c >= 0xF1 and c <= 0xF3 then
|
||||
return c2 and c3 and c4 and
|
||||
c2 >= 0x80 and c2 <= 0xBF and
|
||||
c3 >= 0x80 and c3 <= 0xBF and
|
||||
c4 >= 0x80 and c4 <= 0xBF
|
||||
elseif c == 0xF4 then
|
||||
return c2 and c3 and c4 and
|
||||
c2 >= 0x80 and c2 <= 0x8F and
|
||||
c3 >= 0x80 and c3 <= 0xBF and
|
||||
c4 >= 0x80 and c4 <= 0xBF
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--byte index of the next valid utf8 char after the char at byte index i.
|
||||
--nil if indices go out of range. invalid characters are skipped.
|
||||
function utf8.next_valid(s, i)
|
||||
local valid
|
||||
i, valid = utf8.next_raw(s, i)
|
||||
while i and (not valid or not utf8.isvalid(s, i)) do
|
||||
i, valid = utf8.next(s, i)
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
--iterate valid chars, returning the byte index where each char starts
|
||||
function utf8.valid_byte_indices(s)
|
||||
return utf8.next_valid, s
|
||||
end
|
||||
|
||||
--assert that a string only contains valid utf8 characters
|
||||
function utf8.validate(s)
|
||||
for i, valid in utf8.byte_indices(s) do
|
||||
if not valid or not utf8.isvalid(s, i) then
|
||||
error(string.format('invalid utf8 char at #%d', i))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function table_lookup(s, i, j, t)
|
||||
return t[s:sub(i, j)]
|
||||
end
|
||||
|
||||
--replace characters in string based on a function f(s, i, j, ...) -> replacement_string | nil
|
||||
function utf8.replace(s, f, ...)
|
||||
if type(f) == 'table' then
|
||||
return utf8.replace(s, table_lookup, f)
|
||||
end
|
||||
if s == '' then
|
||||
return s
|
||||
end
|
||||
local t = {}
|
||||
local lasti = 1
|
||||
for i in utf8.byte_indices(s) do
|
||||
local nexti = utf8.next(s, i) or #s + 1
|
||||
local repl = f(s, i, nexti - 1, ...)
|
||||
if repl then
|
||||
table.insert(t, s:sub(lasti, i - 1))
|
||||
table.insert(t, repl)
|
||||
lasti = nexti
|
||||
end
|
||||
end
|
||||
table.insert(t, s:sub(lasti))
|
||||
return table.concat(t)
|
||||
end
|
||||
|
||||
local function replace_invalid(s, i, j, repl_char)
|
||||
if not utf8.isvalid(s, i) then
|
||||
return repl_char
|
||||
end
|
||||
end
|
||||
|
||||
--replace invalid utf8 chars with a replacement char
|
||||
function utf8.sanitize(s, repl_char)
|
||||
repl_char = repl_char or '<EFBFBD>' --\uFFFD
|
||||
return utf8.replace(s, replace_invalid, repl_char)
|
||||
end
|
||||
|
||||
-- Returns the position (in bytes) where the encoding of the n-th character
|
||||
-- of s (counting from position i) starts.
|
||||
function utf8.offset(s, n, i)
|
||||
|
||||
-- The default for i is 1 when n is non-negative and #s + 1 otherwise
|
||||
if not i then
|
||||
i = n < 0 and #s + 1 or 1
|
||||
end
|
||||
|
||||
local ci = utf8.char_index(s, i)
|
||||
|
||||
-- As a special case, when n is 0 the function returns the start of
|
||||
-- the encoding of the character that contains the i-th byte of s.
|
||||
if n == 0 then
|
||||
return ci
|
||||
end
|
||||
|
||||
if n > 0 then
|
||||
n = n - 1
|
||||
end
|
||||
|
||||
return utf8.byte_index(s, ci + n)
|
||||
end
|
||||
|
||||
utf8.codes = utf8.byte_indices
|
||||
|
||||
return utf8
|
830
src/lib/luigi/widget.lua
Normal file
|
@ -0,0 +1,830 @@
|
|||
--[[--
|
||||
Widget class.
|
||||
|
||||
@classmod Widget
|
||||
--]]--
|
||||
|
||||
local STRICT = false
|
||||
local ROOT = (...):gsub('[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
local Event = require(ROOT .. 'event')
|
||||
local Attribute = require(ROOT .. 'attribute')
|
||||
local Painter = require(ROOT .. 'painter')
|
||||
local Font = Backend.Font
|
||||
|
||||
local Widget = {}
|
||||
|
||||
Event.injectBinders(Widget)
|
||||
|
||||
--[[--
|
||||
API Properties
|
||||
|
||||
These properties may be useful when creating user interfaces,
|
||||
and are a formal part of the API.
|
||||
|
||||
@section api
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Whether this widget has keyboard focus.
|
||||
|
||||
Can be used by styles and themes. This value is automatically set by
|
||||
the `Input` class, and should generally be treated as read-only.
|
||||
--]]--
|
||||
Widget.focused = false
|
||||
|
||||
--[[--
|
||||
Whether the pointer is within this widget.
|
||||
|
||||
Can be used by styles and themes. This value is automatically set by
|
||||
the `Input` class, and should generally be treated as read-only.
|
||||
--]]--
|
||||
Widget.hovered = false
|
||||
|
||||
--[[--
|
||||
Table of mouse buttons pressed on this widget and not yet released,
|
||||
keyed by mouse button name with booleans as values.
|
||||
|
||||
Can be used by styles and themes. Values are automatically set by
|
||||
the `Input` class, and should generally be treated as read-only.
|
||||
--]]--
|
||||
Widget.pressed = nil
|
||||
|
||||
--[[--
|
||||
Internal Properties
|
||||
|
||||
These properties are used internally, but are not likely to be useful
|
||||
when creating user interfaces; they are not a formal part of the API
|
||||
and may change at any time.
|
||||
|
||||
@section internal
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Identifies this object as a widget.
|
||||
|
||||
Can be used to determine whether an unknown object is a widget.
|
||||
--]]--
|
||||
Widget.isWidget = true
|
||||
|
||||
--[[--
|
||||
Whether the widget is currently being reshaped.
|
||||
|
||||
Used internally by `reshape` to prevent stack overflows when handling
|
||||
`Reshape` events.
|
||||
--]]--
|
||||
Widget.isReshaping = false
|
||||
|
||||
--[[--
|
||||
Whether this widget has a type.
|
||||
|
||||
Used by the @{attribute.type|type} attribute to determine whether to
|
||||
run the type initializer when the widget's type is set. After a type
|
||||
initializer has run, `hasType` becomes `true` and no other type
|
||||
initializers should run on the widget.
|
||||
--]]--
|
||||
Widget.hasType = false
|
||||
|
||||
--[[--
|
||||
The `Font` object associated with the widget.
|
||||
--]]--
|
||||
Widget.fontData = nil
|
||||
|
||||
--[[--
|
||||
The `Text` object associated with the widget.
|
||||
--]]--
|
||||
Widget.textData = nil
|
||||
|
||||
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
|
||||
Widget.typeDecorators = {
|
||||
button = require(ROOT .. 'widget.button'),
|
||||
check = require(ROOT .. 'widget.check'),
|
||||
menu = require(ROOT .. 'widget.menu'),
|
||||
['menu.item'] = require(ROOT .. 'widget.menu.item'),
|
||||
progress = require(ROOT .. 'widget.progress'),
|
||||
radio = require(ROOT .. 'widget.radio'),
|
||||
sash = require(ROOT .. 'widget.sash'),
|
||||
slider = require(ROOT .. 'widget.slider'),
|
||||
status = require(ROOT .. 'widget.status'),
|
||||
stepper = require(ROOT .. 'widget.stepper'),
|
||||
text = require(ROOT .. 'widget.text'),
|
||||
window = require(ROOT .. 'widget.window'),
|
||||
}
|
||||
|
||||
--[[--
|
||||
Static Functions
|
||||
|
||||
@section static
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Register a custom widget type.
|
||||
|
||||
@static
|
||||
|
||||
@tparam string name
|
||||
A unique name for this type of widget.
|
||||
|
||||
@tparam function(Widget) decorator
|
||||
An initialization function for this type of widget.
|
||||
--]]--
|
||||
function Widget.register (name, decorator)
|
||||
Widget.typeDecorators[name] = decorator
|
||||
end
|
||||
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
|
||||
-- look for properties in attributes, Widget, style, and theme
|
||||
local function metaIndex (self, property)
|
||||
-- look in widget's own attributes
|
||||
local A = self.attributeDescriptors[property] or Attribute[property]
|
||||
if A then
|
||||
local value = A.get and A.get(self, property)
|
||||
or self.attributes[property]
|
||||
if type(value) == 'function' then value = value(self) end
|
||||
if value ~= nil then return value end
|
||||
end
|
||||
|
||||
-- look in Widget class properties
|
||||
local value = Widget[property]
|
||||
if value ~= nil then return value end
|
||||
|
||||
-- look in style
|
||||
local layout = self.layout
|
||||
value = layout:getStyle():getProperty(self, property)
|
||||
if value ~= nil then return value end
|
||||
|
||||
-- look in theme
|
||||
return layout:getTheme():getProperty(self, property)
|
||||
end
|
||||
|
||||
-- setting attributes triggers special behavior
|
||||
local function metaNewIndex (self, property, value)
|
||||
local A = self.attributeDescriptors[property] or Attribute[property]
|
||||
if A then
|
||||
if A.set then
|
||||
A.set(self, value, property)
|
||||
else
|
||||
self.attributes[property] = value
|
||||
end
|
||||
else
|
||||
if STRICT and Widget[property] == nil then
|
||||
error(property .. ' is not a valid widget property.')
|
||||
else
|
||||
rawset(self, property, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local attributeNames = {}
|
||||
|
||||
for name in pairs(Attribute) do
|
||||
if name ~= 'type' then -- type must be handled last
|
||||
attributeNames[#attributeNames + 1] = name
|
||||
end
|
||||
end
|
||||
|
||||
attributeNames[#attributeNames + 1] = 'type'
|
||||
|
||||
--[[--
|
||||
Widget pseudo-constructor.
|
||||
|
||||
@function Luigi.Widget
|
||||
|
||||
@within Constructor
|
||||
|
||||
@tparam Layout layout
|
||||
The layout this widget belongs to.
|
||||
|
||||
@tparam[opt] table data
|
||||
The data definition table for this widget.
|
||||
This table is identical to the constructed widget.
|
||||
|
||||
@treturn Widget
|
||||
A Widget instance.
|
||||
--]]--
|
||||
local function metaCall (Widget, layout, self)
|
||||
self = self or {}
|
||||
self.layout = layout
|
||||
self.position = { x = nil, y = nil }
|
||||
self.dimensions = { width = nil, height = nil }
|
||||
self.attributes = {}
|
||||
self.attributeDescriptors = {}
|
||||
self.pressed = {}
|
||||
self.painter = Painter(self)
|
||||
|
||||
setmetatable(self, { __index = metaIndex, __newindex = metaNewIndex })
|
||||
|
||||
for _, property in ipairs(attributeNames) do
|
||||
local value = rawget(self, property)
|
||||
rawset(self, property, nil)
|
||||
self[property] = value
|
||||
end
|
||||
|
||||
for k, v in ipairs(self) do
|
||||
self[k] = v.isWidget and v or metaCall(Widget, self.layout, v)
|
||||
self[k].parent = self
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Widget:getMasterLayout ()
|
||||
return self.layout.master or self.layout
|
||||
end
|
||||
|
||||
--[[--
|
||||
Define a custom attribute for this widget.
|
||||
|
||||
When an attribute is defined, the current value is stored locally and
|
||||
removed from the widget's own properties and its attributes collection.
|
||||
Then, the newly-defined setter is called with the stored value.
|
||||
|
||||
@tparam string name
|
||||
The name of the attribute.
|
||||
|
||||
@tparam table descriptor
|
||||
A table, optionally containing `get` and `set` functions (see `Attribute`).
|
||||
|
||||
@treturn Widget
|
||||
Return this widget for chaining.
|
||||
--]]--
|
||||
function Widget:defineAttribute (name, descriptor)
|
||||
local value = rawget(self, name)
|
||||
if value == nil then value = self.attributes[name] end
|
||||
self.attributeDescriptors[name] = descriptor or {}
|
||||
rawset(self, name, nil)
|
||||
self.attributes[name] = nil
|
||||
self[name] = value
|
||||
return self
|
||||
end
|
||||
|
||||
--[[--
|
||||
Fire an event on this widget and each ancestor.
|
||||
|
||||
If any event handler returns non-nil, stop the event from propagating.
|
||||
|
||||
@tparam string eventName
|
||||
The name of the Event.
|
||||
|
||||
@tparam[opt] table data
|
||||
Information about the event to send to handlers.
|
||||
|
||||
@treturn mixed
|
||||
The first value returned by an event handler.
|
||||
--]]--
|
||||
function Widget:bubbleEvent (eventName, data)
|
||||
local event = Event[eventName]
|
||||
data = data or {}
|
||||
data.target = self
|
||||
for ancestor in self:eachAncestor(true) do
|
||||
local result = event:emit(ancestor, data)
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
return event:emit(self.layout, data)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get widget's previous sibling.
|
||||
|
||||
@treturn Widget|nil
|
||||
The widget's previous sibling, if any.
|
||||
--]]--
|
||||
function Widget:getPreviousSibling ()
|
||||
local parent = self.parent
|
||||
if not parent then return end
|
||||
for i, widget in ipairs(parent) do
|
||||
if widget == self then return parent[i - 1] end
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get widget's next sibling.
|
||||
|
||||
@treturn Widget|nil
|
||||
The widget's next sibling, if any.
|
||||
--]]--
|
||||
function Widget:getNextSibling ()
|
||||
local parent = self.parent
|
||||
if not parent then return end
|
||||
for i, widget in ipairs(parent) do
|
||||
if widget == self then return parent[i + 1] end
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Attempt to focus the widget.
|
||||
|
||||
Unfocus currently focused widget, and focus this widget if it's focusable.
|
||||
|
||||
@treturn boolean
|
||||
true if this widget was focused, else false.
|
||||
--]]--
|
||||
function Widget:focus ()
|
||||
local layout = self.layout
|
||||
|
||||
if layout.focusedWidget == self then
|
||||
return true
|
||||
end
|
||||
|
||||
if layout.focusedWidget then
|
||||
layout.focusedWidget.focused = nil
|
||||
Event.Blur:emit(self.layout, layout.focusedWidget)
|
||||
layout.focusedWidget = nil
|
||||
end
|
||||
|
||||
if self.focusable then
|
||||
self.focused = true
|
||||
layout.focusedWidget = self
|
||||
Event.Focus:emit(self.layout, self)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the next widget, depth-first.
|
||||
|
||||
If the widget has children, returns the first child.
|
||||
Otherwise, returns the next sibling of the nearest possible ancestor.
|
||||
Cycles back around to the layout root from the last widget in the tree.
|
||||
|
||||
@treturn Widget
|
||||
The next widget in the tree.
|
||||
--]]--
|
||||
function Widget:getNextNeighbor ()
|
||||
if #self > 0 then
|
||||
return self[1]
|
||||
end
|
||||
for ancestor in self:eachAncestor(true) do
|
||||
local nextWidget = ancestor:getNextSibling()
|
||||
if nextWidget then return nextWidget end
|
||||
end
|
||||
return self.layout.root
|
||||
end
|
||||
|
||||
-- get the last child of the last child of the last child of the...
|
||||
local function getGreatestDescendant (widget)
|
||||
while #widget > 0 do
|
||||
widget = widget[#widget]
|
||||
end
|
||||
return widget
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the previous widget, depth-first.
|
||||
|
||||
Uses the reverse of the traversal order used by `getNextNeighbor`.
|
||||
Cycles back around to the last widget in the tree from the layout root.
|
||||
|
||||
@treturn Widget
|
||||
The previous widget in the tree.
|
||||
--]]--
|
||||
function Widget:getPreviousNeighbor ()
|
||||
local layout = self.layout
|
||||
|
||||
if self == layout.root then
|
||||
return getGreatestDescendant(self)
|
||||
end
|
||||
|
||||
for ancestor in self:eachAncestor(true) do
|
||||
local previousWidget = ancestor:getPreviousSibling()
|
||||
if previousWidget then
|
||||
return getGreatestDescendant(previousWidget)
|
||||
end
|
||||
if ancestor ~= self then return ancestor end
|
||||
end
|
||||
|
||||
return layout.root
|
||||
end
|
||||
|
||||
--[[--
|
||||
Add a child to this widget.
|
||||
|
||||
@tparam Widget|table data
|
||||
A widget or definition table representing a widget.
|
||||
|
||||
@treturn Widget
|
||||
The newly added child widget.
|
||||
--]]--
|
||||
function Widget:addChild (data)
|
||||
local layout = self.layout
|
||||
local child = data and data.isWidget and data or Widget(layout, data or {})
|
||||
|
||||
self[#self + 1] = child
|
||||
child.parent = self
|
||||
child.layout = self.layout
|
||||
|
||||
return child
|
||||
end
|
||||
|
||||
function Widget:calculateDimension (name)
|
||||
-- If dimensions are already calculated, return them.
|
||||
if self.dimensions[name] then
|
||||
return self.dimensions[name]
|
||||
end
|
||||
|
||||
-- Get minimum width/height from attributes.
|
||||
local min = (name == 'width') and (self.minwidth or 0)
|
||||
or (self.minheight or 0)
|
||||
|
||||
-- If width/height attribute is found (in widget, style or theme)
|
||||
if self[name] then
|
||||
-- and if width/height is "auto" then shrink to fit content
|
||||
if self[name] == 'auto' then
|
||||
self.dimensions[name] = self:calculateDimensionMinimum(name)
|
||||
return self.dimensions[name]
|
||||
end
|
||||
-- else width/height should be a number; use that value,
|
||||
-- clamped to minimum.
|
||||
self.dimensions[name] = math.max(self[name], min)
|
||||
return self.dimensions[name]
|
||||
end
|
||||
|
||||
-- If the widget is a layout root (and has no width/height),
|
||||
-- it's the same size as the window.
|
||||
local parent = self.parent
|
||||
if not parent then
|
||||
local windowWidth, windowHeight = Backend.getWindowSize()
|
||||
local size = name == 'width' and windowWidth or windowHeight
|
||||
self.dimensions[name] = size
|
||||
return self.dimensions[name]
|
||||
end
|
||||
|
||||
-- Widgets expand to fit their parents when no width/height is specified.
|
||||
local parentDimension = parent:calculateDimension(name)
|
||||
parentDimension = parentDimension - (parent.margin or 0) * 2
|
||||
parentDimension = parentDimension - (parent.padding or 0) * 2
|
||||
|
||||
-- If the dimension is in the opposite direction of the parent flow
|
||||
-- (for example if parent.flow is 'x' and the dimension is 'height'),
|
||||
-- then return the parent dimension.
|
||||
local parentFlow = parent.flow or 'y'
|
||||
if (parentFlow ~= 'x' and name == 'width')
|
||||
or (parentFlow == 'x' and name == 'height') then
|
||||
self.dimensions[name] = math.max(parentDimension, min)
|
||||
return self.dimensions[name]
|
||||
end
|
||||
|
||||
-- If the dimension is in the same direction as the parent flow
|
||||
-- (for example if parent.flow is 'x' and the dimension is 'width'),
|
||||
-- then return an equal portion of the unclaimed space in the parent.
|
||||
local claimed = 0
|
||||
local unsized = 1
|
||||
for i, widget in ipairs(self.parent) do
|
||||
if widget ~= self then
|
||||
local value = widget[name]
|
||||
if value == 'auto' then
|
||||
if not widget.dimensions[name] then
|
||||
widget.dimensions[name] = widget:calculateDimensionMinimum(name)
|
||||
end
|
||||
claimed = claimed + widget.dimensions[name]
|
||||
elseif value then
|
||||
local min = (name == 'width') and (widget.minwidth or 0)
|
||||
or (widget.minheight or 0)
|
||||
claimed = claimed + math.max(value, min)
|
||||
else
|
||||
unsized = unsized + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local size = (parentDimension - claimed) / unsized
|
||||
|
||||
size = math.max(size, min)
|
||||
self.dimensions[name] = size
|
||||
return size
|
||||
end
|
||||
|
||||
function Widget:calculateRootPosition (axis)
|
||||
local value = (axis == 'x' and self.left) or (axis ~= 'x' and self.top)
|
||||
|
||||
if value then
|
||||
self.position[axis] = value
|
||||
return value
|
||||
end
|
||||
|
||||
local ww, wh = Backend.getWindowSize()
|
||||
|
||||
if axis == 'x' and type(self.width) == 'number' then
|
||||
value = (ww - self.width) / 2
|
||||
elseif axis ~= 'x' and type(self.height) == 'number' then
|
||||
value = (wh - self.height) / 2
|
||||
else
|
||||
value = 0
|
||||
end
|
||||
|
||||
self.position[axis] = value
|
||||
return value
|
||||
end
|
||||
|
||||
function Widget:calculatePosition (axis)
|
||||
if self.position[axis] then
|
||||
return self.position[axis]
|
||||
end
|
||||
local parent = self.parent
|
||||
local scroll = 0
|
||||
if not parent then
|
||||
return self:calculateRootPosition(axis)
|
||||
else
|
||||
scroll = axis == 'x' and (parent.scrollX or 0)
|
||||
or axis ~= 'x' and (parent.scrollY or 0)
|
||||
end
|
||||
local parentPos = parent:calculatePosition(axis)
|
||||
local p = parentPos - scroll + (parent.margin or 0) + (parent.padding or 0)
|
||||
local parentFlow = parent.flow or 'y'
|
||||
for i, widget in ipairs(parent) 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:calculateDimensionMinimum (name)
|
||||
local dim = self[name]
|
||||
local min = (name == 'width') and (self.minwidth or 0)
|
||||
or (self.minheight or 0)
|
||||
|
||||
if type(dim) == 'number' then
|
||||
return math.max(dim, min)
|
||||
end
|
||||
|
||||
local value = 0
|
||||
|
||||
for _, child in ipairs(self) do
|
||||
if (name == 'width' and self.flow == 'x')
|
||||
or (name == 'height' and self.flow ~= 'x') then
|
||||
value = value + child:calculateDimensionMinimum(name)
|
||||
else
|
||||
value = math.max(value, child:calculateDimensionMinimum(name))
|
||||
end
|
||||
end
|
||||
|
||||
if value > 0 then
|
||||
local space = (self.margin or 0) * 2 + (self.padding or 0) * 2
|
||||
value = value + space
|
||||
end
|
||||
|
||||
return math.max(value, min)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the widget's X coordinate.
|
||||
|
||||
@treturn number
|
||||
The widget's X coordinate.
|
||||
--]]--
|
||||
function Widget:getX ()
|
||||
return self:calculatePosition('x')
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the widget's Y coordinate.
|
||||
|
||||
@treturn number
|
||||
The widget's Y coordinate.
|
||||
--]]--
|
||||
function Widget:getY ()
|
||||
return self:calculatePosition('y')
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the widget's calculated width.
|
||||
|
||||
@treturn number
|
||||
The widget's calculated width.
|
||||
--]]--
|
||||
function Widget:getWidth ()
|
||||
return self:calculateDimension('width')
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the widget's calculated height.
|
||||
|
||||
@treturn number
|
||||
The widget's calculated height.
|
||||
--]]--
|
||||
function Widget:getHeight ()
|
||||
return self:calculateDimension('height')
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the content width.
|
||||
|
||||
Gets the combined width of the widget's children.
|
||||
|
||||
@treturn number
|
||||
The content width.
|
||||
--]]--
|
||||
function Widget:getContentWidth ()
|
||||
if not self.layout.isReady then return 0 end
|
||||
local width = 0
|
||||
if self.flow == 'x' then
|
||||
for _, child in ipairs(self) do
|
||||
width = width + child:getWidth()
|
||||
end
|
||||
else
|
||||
for _, child in ipairs(self) do
|
||||
width = math.max(width, child:getWidth())
|
||||
end
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get the content height.
|
||||
|
||||
Gets the combined height of the widget's children.
|
||||
|
||||
@treturn number
|
||||
The content height.
|
||||
--]]--
|
||||
function Widget:getContentHeight ()
|
||||
if not self.layout.isReady then return 0 end
|
||||
local height = 0
|
||||
if self.flow ~= 'x' then
|
||||
for _, child in ipairs(self) do
|
||||
height = height + child:getHeight()
|
||||
end
|
||||
else
|
||||
for _, child in ipairs(self) do
|
||||
height = math.max(height, child:getHeight())
|
||||
end
|
||||
end
|
||||
return height
|
||||
end
|
||||
|
||||
function Widget:getFont ()
|
||||
if not self.fontData then
|
||||
self.fontData = Font(self.font, self.size)
|
||||
end
|
||||
return self.fontData
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get x/y/width/height values describing a rectangle within the widget.
|
||||
|
||||
@tparam boolean useMargin
|
||||
Whether to adjust the rectangle based on the widget's margin.
|
||||
|
||||
@tparam boolean usePadding
|
||||
Whether to adjust the rectangle based on the widget's padding.
|
||||
|
||||
@treturn number
|
||||
The upper left corner's X position.
|
||||
|
||||
@treturn number
|
||||
The upper left corner's Y position.
|
||||
|
||||
@treturn number
|
||||
The rectangle's width
|
||||
|
||||
@treturn number
|
||||
The rectangle's height
|
||||
--]]--
|
||||
function Widget:getRectangle (useMargin, usePadding)
|
||||
local x, y = self:getX(), self:getY()
|
||||
local w, h = self:getWidth(), self:getHeight()
|
||||
local function shrink(amount)
|
||||
x = x + amount
|
||||
y = y + amount
|
||||
w = w - amount * 2
|
||||
h = h - amount * 2
|
||||
end
|
||||
if useMargin then
|
||||
shrink(self.margin or 0)
|
||||
end
|
||||
if usePadding then
|
||||
shrink(self.padding or 0)
|
||||
end
|
||||
return math.floor(x), math.floor(y), math.floor(w), math.floor(h)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Determine whether a point is within a widget.
|
||||
|
||||
@tparam number x
|
||||
The point's X coordinate.
|
||||
|
||||
@tparam number y
|
||||
The point's Y coordinate.
|
||||
|
||||
@treturn boolean
|
||||
true if the point is within the widget, else false.
|
||||
--]]--
|
||||
function Widget:isAt (x, y)
|
||||
local x1, y1, w, h = self:getRectangle()
|
||||
local x2, y2 = x1 + w, y1 + h
|
||||
return (x1 <= x) and (x2 >= x) and (y1 <= y) and (y2 >= y)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Iterate widget's ancestors.
|
||||
|
||||
@tparam boolean includeSelf
|
||||
Whether to include this widget as the first result.
|
||||
|
||||
@treturn function
|
||||
Returns an iterator function that returns widgets.
|
||||
|
||||
@usage
|
||||
for ancestor in myWidget:eachAncestor(true) do
|
||||
print(widget.type or 'generic')
|
||||
end
|
||||
--]]--
|
||||
function Widget:eachAncestor (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:paint ()
|
||||
return self.painter:paint()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Reshape the widget.
|
||||
|
||||
Clears calculated widget dimensions, allowing them to be recalculated, and
|
||||
fires a Reshape event (does not bubble). Called recursively for each child.
|
||||
|
||||
When setting a widget's width or height, this function is automatically called
|
||||
on the parent widget.
|
||||
--]]--
|
||||
function Widget:reshape ()
|
||||
if self.isReshaping then return end
|
||||
self.isReshaping = true
|
||||
|
||||
self:scrollBy(0, 0)
|
||||
|
||||
self.position = {}
|
||||
self.dimensions = {}
|
||||
|
||||
self.textData = nil
|
||||
|
||||
Event.Reshape:emit(self, { target = self })
|
||||
for _, child in ipairs(self) do
|
||||
if child.reshape then
|
||||
child:reshape()
|
||||
end
|
||||
end
|
||||
local items = self.items
|
||||
if items then
|
||||
for _, child in ipairs(items) do
|
||||
if child.reshape then
|
||||
child:reshape()
|
||||
end
|
||||
end
|
||||
end
|
||||
self.isReshaping = nil
|
||||
end
|
||||
|
||||
function Widget:scrollBy (amount)
|
||||
if not self.scroll then return end
|
||||
--TODO: eliminate redundancy
|
||||
if self.flow == 'x' then
|
||||
if not self.scrollX then self.scrollX = 0 end
|
||||
local scrollX = self.scrollX - amount * 10
|
||||
local inner = math.max(self:getContentWidth(), self.innerWidth or 0)
|
||||
local maxX = inner - self:getWidth()
|
||||
+ (self.padding or 0) * 2 + (self.margin or 0) * 2
|
||||
scrollX = math.max(math.min(scrollX, maxX), 0)
|
||||
if scrollX ~= self.scrollX then
|
||||
self.scrollX = scrollX
|
||||
self:reshape()
|
||||
return true
|
||||
end
|
||||
else
|
||||
if not self.scrollY then self.scrollY = 0 end
|
||||
local scrollY = self.scrollY - amount * 10
|
||||
local inner = math.max(self:getContentHeight(), self.innerHeight or 0)
|
||||
local maxY = inner - self:getHeight()
|
||||
+ (self.padding or 0) * 2 + (self.margin or 0) * 2
|
||||
scrollY = math.max(math.min(scrollY, maxY), 0)
|
||||
if scrollY ~= self.scrollY then
|
||||
self.scrollY = scrollY
|
||||
self:reshape()
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(Widget, { __call = metaCall })
|
30
src/lib/luigi/widget/button.lua
Normal file
|
@ -0,0 +1,30 @@
|
|||
--[[--
|
||||
A button.
|
||||
|
||||
Buttons have no special behavior beyond that of generic widgets,
|
||||
but themes should give buttons an appropriate appearance.
|
||||
|
||||
@usage
|
||||
-- create a layout containing only a button
|
||||
local layout = Layout {
|
||||
type = 'button',
|
||||
id = 'exampleButton',
|
||||
text = 'Press me',
|
||||
width = 100,
|
||||
height = 32,
|
||||
}
|
||||
|
||||
-- handle Press events
|
||||
layout.exampleButton:onPress(function (event)
|
||||
print 'You pressed the button.'
|
||||
end)
|
||||
|
||||
-- show the layout
|
||||
layout:show()
|
||||
|
||||
@widget button
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
|
||||
end
|
22
src/lib/luigi/widget/check.lua
Normal file
|
@ -0,0 +1,22 @@
|
|||
--[[--
|
||||
A check box.
|
||||
|
||||
Check boxes toggle their @{attribute.value|value} attribute between
|
||||
`true` and `false` when pressed.
|
||||
|
||||
Changing the value of a check box causes it to change its appearance to
|
||||
indicate its value. The standard themes use the @{attribute.icon|icon}
|
||||
attribute for this purpose. If a custom icon is provided when using the
|
||||
standard themes, the widget's value should be indicated in some other way.
|
||||
|
||||
@widget check
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
self:onPress(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
self.value = not self.value
|
||||
end)
|
||||
|
||||
self.value = not not self.value
|
||||
end
|
15
src/lib/luigi/widget/menu.lua
Normal file
|
@ -0,0 +1,15 @@
|
|||
--[[--
|
||||
A menu bar.
|
||||
|
||||
@widget menu
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
|
||||
for index, child in ipairs(self) do
|
||||
child.type = child.type or 'menu.item'
|
||||
child.parentMenu = self
|
||||
child.rootMenu = self
|
||||
end
|
||||
|
||||
end
|
243
src/lib/luigi/widget/menu/item.lua
Normal file
|
@ -0,0 +1,243 @@
|
|||
--[[--
|
||||
A menu item.
|
||||
|
||||
When a `menu` is created, any sub-items not having a specified type
|
||||
are automatically given a type of `'menu.item'`. These widgets should
|
||||
not be explicitly created.
|
||||
|
||||
@widget menu.item
|
||||
--]]--
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*.[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
local Shortcut = require(ROOT .. 'shortcut')
|
||||
|
||||
local Layout, Event
|
||||
|
||||
local function checkMouseButton (self, event)
|
||||
local button = event.button
|
||||
if not button then return false end
|
||||
if self.isContextMenu then
|
||||
return button == 'left' or button == 'right'
|
||||
end
|
||||
return button == 'left'
|
||||
end
|
||||
|
||||
local function addLayoutChildren (self)
|
||||
local root = self.menuLayout.root
|
||||
local textWidth = 0
|
||||
local keyWidth = 0
|
||||
local height = 0
|
||||
|
||||
while #root > 0 do rawset(root, #root, nil) end
|
||||
|
||||
root.height = 0
|
||||
root.width = 0
|
||||
|
||||
for index, child in ipairs(self.items) do
|
||||
child.type = child.type or 'menu.item'
|
||||
root:addChild(child)
|
||||
local childHeight = child:getHeight()
|
||||
height = height + childHeight
|
||||
if child.type == 'menu.item' then
|
||||
local font = child:getFont()
|
||||
local pad = child.padding or 0
|
||||
local tw = font:getAdvance(child[2].text)
|
||||
+ pad * 2 + childHeight
|
||||
local kw = font:getAdvance(child[3].text)
|
||||
+ pad * 2 + childHeight
|
||||
textWidth = math.max(textWidth, tw)
|
||||
keyWidth = math.max(keyWidth, kw)
|
||||
end
|
||||
end
|
||||
|
||||
root.height = height
|
||||
root.width = textWidth + keyWidth + (root.padding or 0)
|
||||
|
||||
local isSubmenu = self.parentMenu and self.parentMenu.parentMenu
|
||||
local w = isSubmenu and self:getWidth() or 0
|
||||
local h = isSubmenu and 0 or self:getHeight()
|
||||
self.menuLayout:placeNear(self:getX(), self:getY(), w, h)
|
||||
end
|
||||
|
||||
local function show (self)
|
||||
if not self.items or #self.items < 1 then return end
|
||||
|
||||
addLayoutChildren(self)
|
||||
self.menuLayout:show()
|
||||
end
|
||||
|
||||
local function deactivateSiblings (target)
|
||||
local sibling = target.parent and target.parent[1]
|
||||
local wasSiblingOpen
|
||||
|
||||
if not sibling then
|
||||
return
|
||||
end
|
||||
|
||||
while sibling do
|
||||
local layout = sibling.menuLayout
|
||||
local items = sibling.items
|
||||
|
||||
sibling.active = nil
|
||||
|
||||
if layout and layout.isShown then
|
||||
wasSiblingOpen = true
|
||||
layout:hide()
|
||||
end
|
||||
|
||||
if items and items[1] then
|
||||
deactivateSiblings(items[1])
|
||||
end
|
||||
|
||||
sibling = sibling:getNextSibling()
|
||||
end
|
||||
|
||||
return wasSiblingOpen
|
||||
end
|
||||
|
||||
local function activate (event, ignoreIfNoneOpen)
|
||||
-- if event.button and event.button ~= 'left' then return end
|
||||
local target = event.target
|
||||
|
||||
while target.parent
|
||||
and target.parent.type ~= 'menu' and target.parent.type ~= 'submenu' do
|
||||
target = target.parent
|
||||
if not target then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- if not checkMouseButton(event) then return end
|
||||
|
||||
local wasSiblingOpen = deactivateSiblings(target)
|
||||
local ignore = ignoreIfNoneOpen and not wasSiblingOpen
|
||||
|
||||
if not ignore then
|
||||
show(target)
|
||||
target.active = true
|
||||
end
|
||||
end
|
||||
|
||||
local function registerLayoutEvents (self)
|
||||
local menuLayout = self.menuLayout
|
||||
|
||||
menuLayout:onReshape(function (event)
|
||||
menuLayout:hide()
|
||||
deactivateSiblings(self.rootMenu[1])
|
||||
end)
|
||||
|
||||
menuLayout:onPressStart(function (event)
|
||||
if not event.hit then
|
||||
menuLayout:hide()
|
||||
if self.parentMenu == self.rootMenu then
|
||||
deactivateSiblings(self.rootMenu[1])
|
||||
end
|
||||
elseif checkMouseButton(self, event) then
|
||||
activate(event)
|
||||
end
|
||||
end)
|
||||
|
||||
menuLayout:onPress(function (event)
|
||||
-- if event.button ~= 'left' then return end
|
||||
if not checkMouseButton(self, event) then return end
|
||||
for widget in event.target:eachAncestor(true) do
|
||||
if widget.type == 'menu.item' and #widget.items == 0 then
|
||||
menuLayout:hide()
|
||||
deactivateSiblings(self.rootMenu[1])
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
menuLayout:onPressEnd(function (event)
|
||||
-- if event.button ~= 'left' then return end
|
||||
if not checkMouseButton(self, event) then return end
|
||||
for widget in event.target:eachAncestor(true) do
|
||||
if widget.type == 'menu.item' and #widget.items == 0
|
||||
and event.target ~= event.origin then
|
||||
widget:bubbleEvent('Press', event)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
menuLayout:onEnter(activate)
|
||||
menuLayout:onPressEnter(activate)
|
||||
end
|
||||
|
||||
local function initialize (self)
|
||||
local font = self:getFont()
|
||||
local pad = self.padding or 0
|
||||
local isSubmenu = self.parentMenu and self.parentMenu.parentMenu
|
||||
local text, shortcut, icon = self.text or '', self.shortcut or '', self.icon
|
||||
local textWidth = font:getAdvance(text) + pad * 2
|
||||
|
||||
if isSubmenu then
|
||||
local edgeType
|
||||
if #self.items > 0 then
|
||||
shortcut = ' '
|
||||
edgeType = 'menu.expander'
|
||||
else
|
||||
shortcut = Shortcut.stringify(shortcut)
|
||||
end
|
||||
self.flow = 'x'
|
||||
self:addChild { icon = icon, width = self.height }
|
||||
self:addChild { text = text, width = textWidth }
|
||||
self:addChild {
|
||||
type = edgeType,
|
||||
text = shortcut,
|
||||
align = 'middle right',
|
||||
minwidth = self.height,
|
||||
color = function ()
|
||||
local c = self.color or { 0, 0, 0 }
|
||||
return { c[1], c[2], c[3], (c[4] or 256) / 2 }
|
||||
end
|
||||
}
|
||||
|
||||
self.icon = nil
|
||||
self.text = nil
|
||||
else
|
||||
-- top level menu
|
||||
self.width = textWidth + pad * 2
|
||||
self.align = 'middle center'
|
||||
end
|
||||
end
|
||||
|
||||
local function extractChildren (self)
|
||||
self.items = {}
|
||||
for index, child in ipairs(self) do
|
||||
self[index] = nil
|
||||
self.items[#self.items + 1] = child
|
||||
child.parentMenu = self
|
||||
child.rootMenu = self.rootMenu
|
||||
child.type = child.type or 'menu.item'
|
||||
end
|
||||
end
|
||||
|
||||
local function registerEvents (self)
|
||||
self:onPressStart(activate)
|
||||
|
||||
self:onEnter(function (event)
|
||||
activate(event, true)
|
||||
end)
|
||||
|
||||
self:onPressEnter(function (event)
|
||||
activate(event, true)
|
||||
end)
|
||||
end
|
||||
|
||||
local function createLayout (self)
|
||||
Layout = Layout or require(ROOT .. 'layout')
|
||||
|
||||
self.menuLayout = Layout({ type = 'submenu' }, self.rootMenu.layout)
|
||||
end
|
||||
|
||||
return function (self)
|
||||
extractChildren(self)
|
||||
initialize(self)
|
||||
registerEvents(self)
|
||||
|
||||
if not self.items or #self.items < 1 then return end
|
||||
createLayout(self)
|
||||
registerLayoutEvents(self)
|
||||
addLayoutChildren(self)
|
||||
end
|
41
src/lib/luigi/widget/progress.lua
Normal file
|
@ -0,0 +1,41 @@
|
|||
--[[--
|
||||
A progress bar.
|
||||
|
||||
Set the widget's `value` property to a decimal value
|
||||
between 0 and 1 (inclusive) to change the width of the bar.
|
||||
|
||||
@widget progress
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
self.value = self.value or 0
|
||||
|
||||
local pad = self:addChild {
|
||||
width = 0,
|
||||
}
|
||||
local bar = self:addChild {
|
||||
type = 'progress.bar',
|
||||
}
|
||||
|
||||
self:onChange(function ()
|
||||
self:reshape()
|
||||
end)
|
||||
|
||||
self:onReshape(function ()
|
||||
local x1, y1, w, h = self:getRectangle(true, true)
|
||||
local x2, y2 = x1 + w, y1 + h
|
||||
if self.flow == 'x' then
|
||||
local min = bar.minwidth or 0
|
||||
x1 = x1 + min
|
||||
bar.width = self.value * (x2 - x1) + min
|
||||
bar.height = false
|
||||
pad.height = 0
|
||||
else
|
||||
local min = bar.minheight or 0
|
||||
y1 = y1 + min
|
||||
bar.width = false
|
||||
bar.height = false
|
||||
pad.height = math.ceil(h - (self.value * (y2 - y1) + min))
|
||||
end
|
||||
end)
|
||||
end
|
94
src/lib/luigi/widget/radio.lua
Normal file
|
@ -0,0 +1,94 @@
|
|||
--[[--
|
||||
A radio widget.
|
||||
|
||||
When pressed, a radio widget's @{attribute.value|value} changes to
|
||||
`true`, and the values of other radio widgets in the same `group`
|
||||
change to `false`.
|
||||
|
||||
Changing the value of a radio button causes it to change its appearance to
|
||||
indicate its value. The standard themes use the @{attribute.icon|icon}
|
||||
attribute for this purpose. If a custom icon is provided when using the
|
||||
standard themes, the widget's value should be indicated in some other way.
|
||||
|
||||
@widget radio
|
||||
--]]--
|
||||
|
||||
local groups = {}
|
||||
|
||||
local function remove (t, value)
|
||||
for i, v in t do
|
||||
if v == value then return table.remove(t, i) end
|
||||
end
|
||||
end
|
||||
|
||||
local function setGroup (self, value)
|
||||
-- remove the widget from the old group
|
||||
local oldValue = self.attributes.group
|
||||
local oldGroup = oldValue and groups[oldValue]
|
||||
if oldGroup then
|
||||
remove(oldGroup, self)
|
||||
-- TODO: is it safe to remove these?
|
||||
if #oldGroup < 1 then groups[oldValue] = nil end
|
||||
end
|
||||
-- add the widget to the new group, or 'defaultGroup' if no group specified
|
||||
value = value or 'defaultGroup'
|
||||
if not groups[value] then
|
||||
groups[value] = {}
|
||||
end
|
||||
local group = groups[value]
|
||||
group[#group + 1] = self
|
||||
self.attributes.group = value
|
||||
local layout = self:getMasterLayout()
|
||||
if not layout[value] then
|
||||
layout:createWidget { id = value, items = group }
|
||||
end
|
||||
self.groupWidget = layout[value]
|
||||
end
|
||||
|
||||
return function (self)
|
||||
|
||||
--[[--
|
||||
Special Attributes
|
||||
|
||||
@section special
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Widget group.
|
||||
|
||||
Should contain a string identifying the widget's group.
|
||||
If not defined, defaults to the string `'default'`.
|
||||
|
||||
When a radio widget is pressed, the values of other radio widgets
|
||||
in the same group change to `false`.
|
||||
|
||||
@attrib group
|
||||
--]]--
|
||||
self:defineAttribute('group', { set = setGroup })
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
|
||||
-- when we bubble events, send them to the group widget
|
||||
local bubbleEvent = self.bubbleEvent
|
||||
function self:bubbleEvent (...)
|
||||
local result = bubbleEvent(self, ...)
|
||||
if result ~= nil then return result end
|
||||
return self.groupWidget:bubbleEvent(...)
|
||||
end
|
||||
|
||||
self:onPress(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
for _, widget in ipairs(groups[self.group]) do
|
||||
widget.value = widget == self
|
||||
end
|
||||
end)
|
||||
|
||||
self:onChange(function (event)
|
||||
-- change event is only sent to group widget once.
|
||||
if not self.value then return false end
|
||||
self.groupWidget.selected = self
|
||||
end)
|
||||
|
||||
self.value = not not self.value
|
||||
end
|
92
src/lib/luigi/widget/sash.lua
Normal file
|
@ -0,0 +1,92 @@
|
|||
--[[--
|
||||
A sash.
|
||||
|
||||
Dragging this widget resizes the widgets adjacent to it.
|
||||
A sash should be adjacent to a widget with a specified size
|
||||
in the same direction as the parent element's @{attribute.flow|flow}.
|
||||
|
||||
For example, if the parent of the sash is `flow = 'x'`
|
||||
then either or both of the siblings adjacent to the sash
|
||||
should have a specified @{attribute.width|width} attribute.
|
||||
|
||||
@usage
|
||||
-- create a layout containing two widgets separated by a sash
|
||||
local layout = Layout {
|
||||
type = 'panel', flow = 'x',
|
||||
{ text = 'This is the left side', wrap = true, width = 100 },
|
||||
{ type = 'sash' },
|
||||
{ text = 'This is the right side', wrap = true },
|
||||
}
|
||||
|
||||
-- show the layout
|
||||
layout:show()
|
||||
|
||||
@widget sash
|
||||
--]]--
|
||||
|
||||
local function setDimension (widget, name, size)
|
||||
if not widget.parent then
|
||||
widget[name] = size
|
||||
return
|
||||
end
|
||||
local parentDimension = widget.parent:calculateDimension(name)
|
||||
local claimed = 0
|
||||
for i, sibling in ipairs(widget.parent) do
|
||||
local value = sibling[name]
|
||||
if sibling ~= widget and value then
|
||||
if value == 'auto' then
|
||||
value = sibling:calculateDimensionMinimum(name)
|
||||
end
|
||||
claimed = claimed + value
|
||||
end
|
||||
end
|
||||
if claimed + size > parentDimension then
|
||||
size = parentDimension - claimed
|
||||
end
|
||||
|
||||
local min = (name == 'width') and (widget.minwidth or 0)
|
||||
or (widget.minheight or 0)
|
||||
|
||||
widget[name] = math.max(size, min)
|
||||
end
|
||||
|
||||
|
||||
return function (self)
|
||||
|
||||
self:onEnter(function (event)
|
||||
local axis = self.parent.flow
|
||||
if axis == 'x' then
|
||||
self.cursor = 'sizewe'
|
||||
else
|
||||
self.cursor = 'sizens'
|
||||
end
|
||||
end)
|
||||
|
||||
self:onPressDrag(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
local axis = self.parent.flow
|
||||
if axis == 'x' then
|
||||
dimension = 'width'
|
||||
else
|
||||
axis = 'y'
|
||||
dimension = 'height'
|
||||
end
|
||||
local prevSibling = self:getPreviousSibling()
|
||||
local nextSibling = self:getNextSibling()
|
||||
local prevSize = prevSibling and prevSibling[dimension]
|
||||
local nextSize = nextSibling and nextSibling[dimension]
|
||||
|
||||
if prevSize or not nextSize then
|
||||
setDimension(prevSibling, dimension,
|
||||
event[axis] - prevSibling:calculatePosition(axis))
|
||||
elseif nextSize then
|
||||
if nextSize == 'auto' then
|
||||
nextSize = nextSibling:calculateDimensionMinimum(dimension)
|
||||
end
|
||||
setDimension(nextSibling, dimension,
|
||||
nextSibling:calculatePosition(axis) + nextSize - event[axis])
|
||||
end
|
||||
self.parent:reshape()
|
||||
end)
|
||||
|
||||
end
|
88
src/lib/luigi/widget/slider.lua
Normal file
|
@ -0,0 +1,88 @@
|
|||
--[[--
|
||||
A slider.
|
||||
|
||||
Dragging this widget changes its `value` property to a
|
||||
number between 0 and 1, inclusive.
|
||||
|
||||
@widget slider
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
local function clamp (value)
|
||||
return value < 0 and 0 or value > 1 and 1 or value
|
||||
end
|
||||
|
||||
self.value = clamp(self.value or 0)
|
||||
self.step = self.step or 0.01
|
||||
|
||||
local spacer = self:addChild()
|
||||
|
||||
local thumb = self:addChild {
|
||||
type = 'slider.thumb',
|
||||
}
|
||||
|
||||
local function unpress (event)
|
||||
if event.button ~= 'left' then return end
|
||||
thumb.pressed.left = nil -- don't make the thumb appear pushed in
|
||||
return false -- don't press thumb on focused keyboard activation
|
||||
end
|
||||
|
||||
thumb:onPressStart(unpress)
|
||||
thumb:onPressEnter(unpress)
|
||||
|
||||
thumb:onKeyPress(function (event)
|
||||
local key = event.key
|
||||
if key == 'left' or key == 'down' then
|
||||
self.value = clamp(self.value - self.step)
|
||||
elseif key == 'right' or key == 'up' then
|
||||
self.value = clamp(self.value + self.step)
|
||||
end
|
||||
end)
|
||||
|
||||
local function press (event)
|
||||
if event.button ~= 'left' then return end
|
||||
local x1, y1, w, h = self:getRectangle(true, true)
|
||||
local x2, y2 = x1 + w, y1 + h
|
||||
if self.flow == 'x' then
|
||||
local halfThumb = thumb:getWidth() / 2
|
||||
x1, x2 = x1 + halfThumb, x2 - halfThumb
|
||||
self.value = clamp((event.x - x1) / (x2 - x1))
|
||||
else
|
||||
local halfThumb = thumb:getHeight() / 2
|
||||
y1, y2 = y1 + halfThumb, y2 - halfThumb
|
||||
self.value = 1 - clamp((event.y - y1) / (y2 - y1))
|
||||
end
|
||||
thumb:focus()
|
||||
end
|
||||
|
||||
self:onPressStart(press)
|
||||
self:onPressDrag(press)
|
||||
|
||||
self:onEnter(function (event)
|
||||
thumb.hovered = true
|
||||
end)
|
||||
|
||||
self:onLeave(function (event)
|
||||
thumb.hovered = false
|
||||
end)
|
||||
|
||||
self:onChange(function (event)
|
||||
self:reshape()
|
||||
end)
|
||||
|
||||
self:onReshape(function (event)
|
||||
local x1, y1, w, h = self:getRectangle(true, true)
|
||||
local x2, y2 = x1 + w, y1 + h
|
||||
if self.flow == 'x' then
|
||||
local halfThumb = thumb:getWidth() / 2
|
||||
x1, x2 = x1 + halfThumb, x2 - halfThumb
|
||||
spacer.width = self.value * (x2 - x1)
|
||||
spacer.height = false
|
||||
else
|
||||
local halfThumb = thumb:getHeight() / 2
|
||||
y1, y2 = y1 + halfThumb, y2 - halfThumb
|
||||
spacer.width = false
|
||||
spacer.height = (1 - self.value) * (y2 - y1)
|
||||
end
|
||||
end)
|
||||
end
|
26
src/lib/luigi/widget/status.lua
Normal file
|
@ -0,0 +1,26 @@
|
|||
--[[--
|
||||
A status bar.
|
||||
|
||||
This widget will display the @{attribute.status|status} attribute of the
|
||||
hovered widget. Only one status widget should exist per layout. If multiple
|
||||
status widgets exist in the same layout, only the last one created will
|
||||
display status messages.
|
||||
|
||||
@usage
|
||||
-- create a layout containing some buttons and a status bar
|
||||
local layout = Layout {
|
||||
{ type = 'panel', flow = 'x',
|
||||
{ text = 'Do stuff', status = 'Press to do stuff' },
|
||||
{ text = 'Quit', status = 'Press to quit' },
|
||||
},
|
||||
{ type = 'status', height = 24 },
|
||||
}
|
||||
|
||||
-- show the layout
|
||||
layout:show()
|
||||
|
||||
@widget status
|
||||
--]]--
|
||||
return function (self)
|
||||
self.layout.statusWidget = self
|
||||
end
|
106
src/lib/luigi/widget/stepper.lua
Normal file
|
@ -0,0 +1,106 @@
|
|||
--[[--
|
||||
A stepper.
|
||||
|
||||
This widget is composed of two buttons and a content area.
|
||||
Upon creation, this widget's children are moved into the
|
||||
`items` attribute. The items are displayed one at a time in
|
||||
the content area. Pressing the buttons cycles through the
|
||||
item displayed in the content area.
|
||||
|
||||
@widget stepper
|
||||
--]]--
|
||||
|
||||
return function (self)
|
||||
|
||||
--[[--
|
||||
Special Attributes
|
||||
|
||||
@section special
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Content items.
|
||||
|
||||
Contains an array of child widgets to be displayed.
|
||||
|
||||
@attrib items
|
||||
--]]--
|
||||
self:defineAttribute('items', {})
|
||||
|
||||
--[[--
|
||||
Child item index.
|
||||
|
||||
Contains the index in `items` of the item being displayed.
|
||||
|
||||
@attrib index
|
||||
--]]--
|
||||
self:defineAttribute('index', {})
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
|
||||
self.items = {}
|
||||
self.index = 1
|
||||
|
||||
for index, child in ipairs(self) do
|
||||
child.type = child.type or 'stepper.item'
|
||||
self.items[index] = child
|
||||
self[index] = nil
|
||||
end
|
||||
|
||||
local before = self:addChild { type = 'stepper.before' }
|
||||
local view = self:addChild { type = 'stepper.view' }
|
||||
local after = self:addChild { type = 'stepper.after' }
|
||||
|
||||
self:onReshape(function (event)
|
||||
if self.flow == 'x' then
|
||||
before.height = false
|
||||
after.height = false
|
||||
before.width = 0
|
||||
after.width = 0
|
||||
else
|
||||
before.width = false
|
||||
after.width = false
|
||||
before.height = 0
|
||||
after.height = 0
|
||||
end
|
||||
end)
|
||||
|
||||
local function updateValue ()
|
||||
local item = self.items[self.index]
|
||||
self.value = item.value
|
||||
view[1] = nil
|
||||
view:addChild(item)
|
||||
view:reshape()
|
||||
end
|
||||
|
||||
local function decrement ()
|
||||
if not self.items then return end
|
||||
self.index = self.index - 1
|
||||
if self.index < 1 then
|
||||
self.index = #self.items
|
||||
end
|
||||
updateValue()
|
||||
end
|
||||
|
||||
local function increment ()
|
||||
if not self.items then return end
|
||||
self.index = self.index + 1
|
||||
if self.index > #self.items then
|
||||
self.index = 1
|
||||
end
|
||||
updateValue()
|
||||
end
|
||||
|
||||
before:onPress(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
if self.flow == 'x' then decrement() else increment() end
|
||||
end)
|
||||
|
||||
after:onPress(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
if self.flow == 'x' then increment() else decrement() end
|
||||
end)
|
||||
|
||||
updateValue()
|
||||
end
|
441
src/lib/luigi/widget/text.lua
Normal file
|
@ -0,0 +1,441 @@
|
|||
--[[--
|
||||
A text entry area.
|
||||
|
||||
@widget text
|
||||
--]]--
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
local utf8 = require(ROOT .. 'utf8')
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
|
||||
-- make sure selection range doesn't extend past EOT
|
||||
local function trimRange (self)
|
||||
local max = #self.value
|
||||
if self.startIndex > max then self.startIndex = max end
|
||||
if self.endIndex > max then self.endIndex = max end
|
||||
end
|
||||
|
||||
local function updateHighlight (self)
|
||||
local value = self.value
|
||||
local font = self:getFont()
|
||||
local startIndex, endIndex = self.startIndex, self.endIndex
|
||||
local offset = self:getRectangle(true, true) - self.scrollX
|
||||
self.startX = font:getAdvance(value:sub(1, startIndex)) + offset
|
||||
self.endX = font:getAdvance(value:sub(1, endIndex)) + offset
|
||||
end
|
||||
|
||||
local function scrollToCaret (self)
|
||||
updateHighlight(self)
|
||||
local x1, y1, w, h = self:getRectangle(true, true)
|
||||
local x2, y2 = x1 + w, y1 + h
|
||||
local oldX = self.endX or x1
|
||||
|
||||
if oldX <= x1 then
|
||||
self.scrollX = self.scrollX - (x1 - oldX)
|
||||
updateHighlight(self)
|
||||
elseif oldX >= x2 then
|
||||
self.scrollX = self.scrollX + (oldX - x2 + 1)
|
||||
updateHighlight(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function selectRange (self, startIndex, endIndex)
|
||||
if startIndex then self.startIndex = startIndex end
|
||||
if endIndex then self.endIndex = endIndex end
|
||||
trimRange(self)
|
||||
|
||||
scrollToCaret(self)
|
||||
end
|
||||
|
||||
-- return caret index
|
||||
local function findIndexFromPoint (self, x, y)
|
||||
local x1 = self:getRectangle(true, true)
|
||||
|
||||
local font = self.fontData
|
||||
local width = 0
|
||||
local lastPosition = 0
|
||||
|
||||
local function checkPosition (position)
|
||||
local text = self.value:sub(1, position - 1)
|
||||
width = font:getAdvance(text)
|
||||
if width > x + self.scrollX - x1 then
|
||||
if position == 1 then
|
||||
return 0
|
||||
end
|
||||
return lastPosition
|
||||
end
|
||||
lastPosition = position - 1
|
||||
end
|
||||
|
||||
for position in utf8.codes(self.value) do
|
||||
local index = checkPosition(position)
|
||||
if index then return index end
|
||||
end
|
||||
|
||||
local index = checkPosition(#self.value + 1)
|
||||
if index then return index end
|
||||
|
||||
return #self.value
|
||||
end
|
||||
|
||||
-- move the caret or end of selection range one character to the left
|
||||
local function moveCharLeft (self, alterRange)
|
||||
trimRange(self)
|
||||
local text, endIndex = self.value, self.endIndex
|
||||
|
||||
-- clamp caret to beginning
|
||||
if endIndex < 1 then endIndex = 1 end
|
||||
|
||||
-- move left
|
||||
local index = (utf8.offset(text, -1, endIndex + 1) or 0) - 1
|
||||
selectRange(self, not alterRange and index, index)
|
||||
end
|
||||
|
||||
-- move caret or end of selection range one word to the left
|
||||
local function moveWordLeft (self, alterRange)
|
||||
trimRange(self)
|
||||
local text = self.value:sub(1, self.endIndex)
|
||||
local pos = text:find('%s[^%s]+%s*$') or 0
|
||||
selectRange(self, not alterRange and pos, pos)
|
||||
end
|
||||
|
||||
-- move the caret or end of selection range to the beginning of the line
|
||||
local function moveLineLeft (self, alterRange)
|
||||
trimRange(self)
|
||||
selectRange(self, not alterRange and 0, 0)
|
||||
end
|
||||
|
||||
-- move caret or end of selection range one character to the right
|
||||
local function moveCharRight (self, alterRange)
|
||||
trimRange(self)
|
||||
local text, endIndex = self.value, self.endIndex
|
||||
|
||||
-- clamp caret to end
|
||||
if endIndex >= #text then endIndex = #text - 1 end
|
||||
|
||||
-- move right
|
||||
local index = (utf8.offset(text, 2, endIndex + 1) or #text) - 1
|
||||
selectRange(self, not alterRange and index, index)
|
||||
end
|
||||
|
||||
-- move caret or end of selection range one word to the right
|
||||
local function moveWordRight (self, alterRange)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local _, pos = text:find('^%s*[^%s]+', self.endIndex + 1)
|
||||
pos = pos or #text + 1
|
||||
selectRange(self, not alterRange and pos, pos)
|
||||
end
|
||||
|
||||
-- move caret or end of selection range to the end of the line
|
||||
local function moveLineRight (self, alterRange)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
selectRange(self, not alterRange and #text, #text)
|
||||
end
|
||||
|
||||
local function getRange (self)
|
||||
trimRange(self)
|
||||
if self.startIndex <= self.endIndex then
|
||||
return self.startIndex, self.endIndex
|
||||
end
|
||||
|
||||
return self.endIndex, self.startIndex
|
||||
end
|
||||
|
||||
local function deleteRange (self)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local first, last = getRange(self)
|
||||
|
||||
-- if expanded range is selected, delete text in range
|
||||
if first ~= last then
|
||||
local left = text:sub(1, first)
|
||||
local index = #left
|
||||
self.value = left .. text:sub(last + 1)
|
||||
selectRange(self, index, index)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function deleteCharacterLeft (self)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local first, last = getRange(self)
|
||||
|
||||
-- if cursor is at beginning, do nothing
|
||||
if first < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- delete character to the left
|
||||
local offset = utf8.offset(text, -1, last + 1) or 0
|
||||
local left = text:sub(1, offset - 1)
|
||||
local index = #left
|
||||
self.value = left .. text:sub(first + 1)
|
||||
selectRange(self, index, index)
|
||||
end
|
||||
|
||||
local function deleteCharacterRight (self)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local first, last = getRange(self)
|
||||
|
||||
-- if cursor is at end, do nothing
|
||||
if first == #text then
|
||||
return
|
||||
end
|
||||
|
||||
-- delete character to the right
|
||||
local offset = utf8.offset(text, 2, last + 1) or 0
|
||||
local left = text:sub(1, first)
|
||||
local index = #left
|
||||
self.value = left .. text:sub(offset)
|
||||
selectRange(self, index, index)
|
||||
end
|
||||
|
||||
local function copyRangeToClipboard (self)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local first, last = getRange(self)
|
||||
if last >= first + 1 then
|
||||
Backend.setClipboardText(text:sub(first + 1, last))
|
||||
end
|
||||
end
|
||||
|
||||
local function pasteFromClipboard (self)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local pasted = Backend.getClipboardText() or ''
|
||||
local first, last = getRange(self)
|
||||
local left = text:sub(1, first) .. pasted
|
||||
local index = #left
|
||||
self.value = left .. text:sub(last + 1)
|
||||
selectRange(self, index, index)
|
||||
end
|
||||
|
||||
local function insertText (self, newText)
|
||||
trimRange(self)
|
||||
local text = self.value
|
||||
local first, last = getRange(self)
|
||||
local left = text:sub(1, first) .. newText
|
||||
local index = #left
|
||||
self.value = left .. text:sub(last + 1)
|
||||
selectRange(self, index, index)
|
||||
end
|
||||
|
||||
local function isShiftPressed ()
|
||||
return Backend.isKeyDown('lshift', 'rshift')
|
||||
end
|
||||
|
||||
-- check command (gui) key, only on Mac.
|
||||
local isCommandPressed
|
||||
if Backend.isMac() then
|
||||
isCommandPressed = function ()
|
||||
return Backend.isKeyDown('lgui', 'rgui')
|
||||
end
|
||||
else
|
||||
isCommandPressed = function ()
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- check command (gui) key on Mac and ctrl key everywhere else.
|
||||
local isCommandOrCtrlPressed
|
||||
if Backend.isMac() then
|
||||
isCommandOrCtrlPressed = function ()
|
||||
return Backend.isKeyDown('lgui', 'rgui')
|
||||
end
|
||||
else
|
||||
isCommandOrCtrlPressed = function ()
|
||||
return Backend.isKeyDown('lctrl', 'rctrl')
|
||||
end
|
||||
end
|
||||
|
||||
-- check option (alt) key on Mac and ctrl key everywhere else.
|
||||
local isOptionOrCtrlPressed
|
||||
if Backend.isMac() then
|
||||
isOptionOrCtrlPressed = function ()
|
||||
return Backend.isKeyDown('lalt', 'ralt')
|
||||
end
|
||||
else
|
||||
isOptionOrCtrlPressed = function ()
|
||||
return Backend.isKeyDown('lctrl', 'rctrl')
|
||||
end
|
||||
end
|
||||
|
||||
-- Special keys.
|
||||
local function createDefaultKeyActions (self)
|
||||
return {
|
||||
-- let tab press propagate
|
||||
['tab'] = function () return true end,
|
||||
['backspace'] = function ()
|
||||
if not deleteRange(self) then
|
||||
deleteCharacterLeft(self)
|
||||
end
|
||||
end,
|
||||
['delete'] = function ()
|
||||
if not deleteRange(self) then
|
||||
deleteCharacterRight(self)
|
||||
end
|
||||
end,
|
||||
['left'] = function ()
|
||||
if isOptionOrCtrlPressed() then
|
||||
moveWordLeft(self, isShiftPressed())
|
||||
elseif isCommandPressed() then
|
||||
moveLineLeft(self, isShiftPressed())
|
||||
else
|
||||
moveCharLeft(self, isShiftPressed())
|
||||
end
|
||||
end,
|
||||
['right'] = function ()
|
||||
if isOptionOrCtrlPressed() then
|
||||
moveWordRight(self, isShiftPressed())
|
||||
elseif isCommandPressed() then
|
||||
moveLineRight(self, isShiftPressed())
|
||||
else
|
||||
moveCharRight(self, isShiftPressed())
|
||||
end
|
||||
end,
|
||||
['home'] = function ()
|
||||
moveLineLeft(self, isShiftPressed())
|
||||
end,
|
||||
['end'] = function ()
|
||||
moveLineRight(self, isShiftPressed())
|
||||
end,
|
||||
['x'] = function ()
|
||||
if isCommandOrCtrlPressed() then
|
||||
copyRangeToClipboard(self)
|
||||
deleteRange(self)
|
||||
end
|
||||
end,
|
||||
['c'] = function ()
|
||||
if isCommandOrCtrlPressed() then
|
||||
copyRangeToClipboard(self)
|
||||
end
|
||||
end,
|
||||
['v'] = function ()
|
||||
if isCommandOrCtrlPressed() then
|
||||
pasteFromClipboard(self)
|
||||
end
|
||||
end,
|
||||
['a'] = function ()
|
||||
if isCommandOrCtrlPressed() then
|
||||
selectRange(self, 0, #self.value)
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local textInputKeys = {
|
||||
['space'] = true,
|
||||
['return'] = true,
|
||||
['kp00'] = true,
|
||||
['kp000'] = true,
|
||||
['kp&&'] = true,
|
||||
['kp||'] = true,
|
||||
}
|
||||
|
||||
local function isKeyTextInput (key)
|
||||
if textInputKeys[key] or #key == 1
|
||||
or (#key == 3 and key:sub(1, 2) == "kp") then
|
||||
return not Backend.isKeyDown(
|
||||
'lalt', 'ralt', 'lctrl', 'rctrl', 'lgui', 'rgui')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return function (self)
|
||||
self.startIndex, self.endIndex = 0, 0
|
||||
self.startX, self.endX = -1, -1
|
||||
self.scrollX = 0
|
||||
self.value = tostring(self.value or self.text or '')
|
||||
self.text = ''
|
||||
self.keyActions = createDefaultKeyActions(self)
|
||||
|
||||
--[[--
|
||||
Special Attributes
|
||||
|
||||
@section special
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Highlight color.
|
||||
|
||||
Should contain an array with 3 or 4 values (RGB or RGBA) from 0 to 255.
|
||||
|
||||
This color is used to indicate the selected range of text.
|
||||
|
||||
@attrib highlight
|
||||
--]]--
|
||||
|
||||
self:defineAttribute('highlight')
|
||||
local defaultHighlight = { 0x80, 0x80, 0x80, 0x80 }
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
|
||||
self:onPressStart(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
self.startIndex = findIndexFromPoint(self, event.x)
|
||||
self.endIndex = self.startIndex
|
||||
scrollToCaret(self)
|
||||
end)
|
||||
|
||||
self:onPressDrag(function (event)
|
||||
if event.button ~= 'left' then return end
|
||||
self.endIndex = findIndexFromPoint(self, event.x)
|
||||
scrollToCaret(self)
|
||||
end)
|
||||
|
||||
self:onTextInput(function (event)
|
||||
insertText(self, event.text)
|
||||
end)
|
||||
|
||||
self:onKeyPress(function (event)
|
||||
local key = event.key
|
||||
local act = self.keyActions[key]
|
||||
if act then
|
||||
if act() then return end
|
||||
end
|
||||
return isKeyTextInput(key) or nil
|
||||
end)
|
||||
|
||||
self:onDisplay(function (event)
|
||||
local startX, endX = self.startX, self.endX
|
||||
local x, y, w, h = self:getRectangle(true, true)
|
||||
local width, height = endX - startX, h
|
||||
local font = self:getFont()
|
||||
local color = self.color or { 0, 0, 0, 255 }
|
||||
local textTop = math.floor(y + (h - font:getLineHeight()) / 2)
|
||||
|
||||
Backend.push()
|
||||
Backend.intersectScissor(x, y, w, h)
|
||||
Backend.setFont(font)
|
||||
|
||||
if self.focused then
|
||||
-- draw highlighted selection
|
||||
Backend.setColor(self.highlight or defaultHighlight)
|
||||
Backend.drawRectangle('fill', startX, y, width, height)
|
||||
-- draw cursor selection
|
||||
if Backend.getTime() % 2 < 1.75 then
|
||||
Backend.setColor(color)
|
||||
Backend.drawRectangle('fill', endX, y, 1, height)
|
||||
end
|
||||
else
|
||||
Backend.setColor { color[1], color[2], color[3],
|
||||
(color[4] or 256) / 8 }
|
||||
Backend.drawRectangle('fill', startX, y, width, height)
|
||||
end
|
||||
|
||||
-- draw text
|
||||
Backend.setColor(color)
|
||||
Backend.print(self.value, x - self.scrollX, textTop)
|
||||
|
||||
Backend.pop()
|
||||
end)
|
||||
|
||||
self:onReshape(function ()
|
||||
updateHighlight(self)
|
||||
end)
|
||||
end
|
265
src/lib/luigi/widget/window.lua
Normal file
|
@ -0,0 +1,265 @@
|
|||
--[[--
|
||||
Window widget.
|
||||
|
||||
Set properties of the window with this widget's attributes.
|
||||
This widget should only be used as the root widget of a layout.
|
||||
|
||||
@usage
|
||||
-- create a new window
|
||||
local window = Layout {
|
||||
type = 'window',
|
||||
icon = 'logo.png',
|
||||
text = 'Window Example',
|
||||
width = 800,
|
||||
height = 600,
|
||||
{ icon = 'logo.png', text = 'Window Example', align = 'middle center' },
|
||||
{ type = 'panel', flow = 'x', height = 'auto',
|
||||
{}, -- spacer
|
||||
{ type = 'button', id = 'quitButton', text = 'Quit' }
|
||||
}
|
||||
}
|
||||
|
||||
-- handle quit button
|
||||
window.quitButton:onPress(function ()
|
||||
os.exit()
|
||||
end)
|
||||
|
||||
-- show the window
|
||||
window:show()
|
||||
|
||||
@widget window
|
||||
--]]--
|
||||
|
||||
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
|
||||
|
||||
local Backend = require(ROOT .. 'backend')
|
||||
|
||||
return function (self)
|
||||
|
||||
function self:calculateRootPosition (axis)
|
||||
self.position[axis] = 0
|
||||
return 0
|
||||
end
|
||||
|
||||
function self.painter:paintIconAndText () end
|
||||
|
||||
--[[--
|
||||
Special Attributes
|
||||
|
||||
@section special
|
||||
--]]--
|
||||
|
||||
--[[--
|
||||
Maximized. Set to `true` to make the window as large as possible.
|
||||
Set to `false` to restore the size and position.
|
||||
|
||||
@attrib maximized
|
||||
--]]--
|
||||
self:defineAttribute('maximized', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMaximized(value)
|
||||
self.layout.root:reshape()
|
||||
end,
|
||||
get = Backend.getWindowMaximized
|
||||
})
|
||||
|
||||
--[[--
|
||||
Minimized. Set to `true` to minimize the window to an iconic representation.
|
||||
Set to `false` to restore the size and position.
|
||||
|
||||
@attrib minimized
|
||||
--]]--
|
||||
self:defineAttribute('minimized', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMinimized(value)
|
||||
end,
|
||||
get = Backend.getWindowMinimized
|
||||
})
|
||||
|
||||
--[[--
|
||||
Borderless. Set to `true` or `false` to change the border state of the window.
|
||||
You can't change the border state of a fullscreen window.
|
||||
|
||||
@attrib borderless
|
||||
--]]--
|
||||
self:defineAttribute('borderless', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowBorderless(value)
|
||||
self.layout.root:reshape()
|
||||
end,
|
||||
get = Backend.getWindowBorderless
|
||||
})
|
||||
|
||||
--[[--
|
||||
Fullscreen. Set to `true` or `false` to change the fullscreen state
|
||||
of the window.
|
||||
|
||||
@attrib fullscreen
|
||||
--]]--
|
||||
self:defineAttribute('fullscreen', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowFullscreen(value)
|
||||
self.layout.root:reshape()
|
||||
end,
|
||||
get = Backend.getWindowFullscreen
|
||||
})
|
||||
|
||||
--[[--
|
||||
Mouse grab. Set to `true` or `false` to change the window's input grab mode.
|
||||
When input is grabbed the mouse is confined to the window.
|
||||
|
||||
If the caller enables a grab while another window is currently grabbed,
|
||||
the other window loses its grab in favor of the caller's window.
|
||||
|
||||
@attrib grab
|
||||
--]]--
|
||||
self:defineAttribute('grab', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowGrab(value)
|
||||
end,
|
||||
get = Backend.getWindowGrab
|
||||
})
|
||||
|
||||
--[[--
|
||||
Window icon. Should be a string containing a path to an image.
|
||||
|
||||
@attrib icon
|
||||
--]]--
|
||||
local icon
|
||||
|
||||
self:defineAttribute('icon', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
icon = value
|
||||
Backend.setWindowIcon(value)
|
||||
end,
|
||||
get = function () return icon end
|
||||
})
|
||||
--[[--
|
||||
Maximum width of the window's client area.
|
||||
|
||||
@attrib maxwidth
|
||||
--]]--
|
||||
self:defineAttribute('maxwidth', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMaxwidth(value)
|
||||
end,
|
||||
get = Backend.getWindowMaxwidth
|
||||
})
|
||||
|
||||
--[[--
|
||||
Maximum height of the window's client area.
|
||||
|
||||
@attrib maxheight
|
||||
--]]--
|
||||
self:defineAttribute('maxheight', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMaxheight(value)
|
||||
end,
|
||||
get = Backend.getWindowMaxheight
|
||||
})
|
||||
|
||||
--[[--
|
||||
Minimum width of the window's client area.
|
||||
|
||||
@attrib minwidth
|
||||
--]]--
|
||||
self:defineAttribute('minwidth', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMinwidth(value)
|
||||
end,
|
||||
get = Backend.getWindowMinwidth
|
||||
})
|
||||
|
||||
--[[--
|
||||
Minimum height of the window's client area.
|
||||
|
||||
@attrib minheight
|
||||
--]]--
|
||||
self:defineAttribute('minheight', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowMinheight(value)
|
||||
end,
|
||||
get = Backend.getWindowMinheight
|
||||
})
|
||||
|
||||
--[[--
|
||||
Position of the window's top edge.
|
||||
|
||||
@attrib top
|
||||
--]]--
|
||||
self:defineAttribute('top', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowTop(value)
|
||||
end,
|
||||
get = Backend.getWindowTop
|
||||
})
|
||||
|
||||
--[[--
|
||||
Position of the window's left edge.
|
||||
|
||||
@attrib left
|
||||
--]]--
|
||||
self:defineAttribute('left', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowLeft(value)
|
||||
end,
|
||||
get = Backend.getWindowLeft
|
||||
})
|
||||
|
||||
--[[--
|
||||
Width of the window's content area.
|
||||
|
||||
@attrib width
|
||||
--]]--
|
||||
self:defineAttribute('width', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowWidth(value)
|
||||
self.layout.root:reshape()
|
||||
end,
|
||||
get = Backend.getWindowWidth
|
||||
})
|
||||
|
||||
--[[--
|
||||
Height of the window's content area.
|
||||
|
||||
@attrib height
|
||||
--]]--
|
||||
self:defineAttribute('height', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowHeight(value)
|
||||
self.layout.root:reshape()
|
||||
end,
|
||||
get = Backend.getWindowHeight
|
||||
})
|
||||
|
||||
--[[--
|
||||
Title of the window.
|
||||
|
||||
@attrib title
|
||||
--]]--
|
||||
self:defineAttribute('title', {
|
||||
set = function (_, value)
|
||||
if value == nil then return end
|
||||
Backend.setWindowTitle(value)
|
||||
end,
|
||||
get = Backend.getWindowTitle
|
||||
})
|
||||
|
||||
--[[--
|
||||
@section end
|
||||
--]]--
|
||||
end
|