add luigi graphics library

This commit is contained in:
duncathan 2019-09-10 02:48:34 -06:00
parent a0b28cc111
commit 8061174638
97 changed files with 11104 additions and 0 deletions

602
src/lib/luigi/attribute.lua Normal file
View 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
View 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

View 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

View 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

View 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

View 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

Binary file not shown.

View 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

View 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.

View 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.

File diff suppressed because it is too large Load diff

View 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

File diff suppressed because it is too large Load diff

View 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

View 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

View 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

View 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

View 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
View 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,
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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

View 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
View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

339
src/lib/luigi/utf8.lua Normal file
View 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
View 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 })

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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