Skip to content

Instantly share code, notes, and snippets.

@ggcrunchy
Created January 10, 2015 03:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ggcrunchy/f53a38d889ca423294b6 to your computer and use it in GitHub Desktop.
Save ggcrunchy/f53a38d889ca423294b6 to your computer and use it in GitHub Desktop.
A couple of mostly self-contained layout utility modules for Corona SDK
--- Utilities for layout handling.
--
-- These are designed to allow layout decisions and queries, without worrying about the
-- anchor points of the objects in question. In many cases, positions may be substituted
-- for objects, as well.
--
-- With respect to this module, a **Number** may be any of the following:
--
-- * A number, or a string that @{tonumber} is able to convert. These values are used as is.
-- * A string of the form **"AMOUNT%"**, e.g. `"20%"` or `"-4.2%"`, which resolves to the
-- indicated percent of the content width or height.
-- * **nil**, which resolves to 0.
-- TODO: Where two objects are concerned, layout should be able to handle mixed parents
-- TODO: Positions should respect parent's positions, and not assume screen coordiantes
--
-- Permission is hereby granted, free of charge, to any person obtaining
-- a copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be
-- included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--
-- [ MIT license: http://www.opensource.org/licenses/mit-license.php ]
--
-- Standard library imports --
local floor = math.floor
local ipairs = ipairs
local sub = string.sub
local tonumber = tonumber
local type = type
-- Corona globals --
local display = display
-- Cached module references --
local _Above_
local _Below_
local _CenterAlignWith_
local _CenterAt_
local _CenterOf_
local _LeftOf_
local _PutAbove_
local _PutBelow_
local _PutLeftOf_
local _PutRightOf_
local _RightOf_
-- Exports --
local M = {}
-- Resolves a Number (most often being a delta) to a value
local function Delta (n, dim)
if type(n) ~= "string" then
return n or 0
elseif sub(n, -1) == "%" then
return tonumber(sub(n, 1, -2)) * display[dim] / 100
else
return tonumber(n)
end
end
-- Helper for horizontal deltas...
local function DX (n)
return Delta(n, "contentWidth")
end
-- ...and vertical ones
local function DY (n)
return Delta(n, "contentHeight")
end
-- Is the object not a group?
local function NonGroup (object)
return object._type ~= "GroupObject"
end
-- Is the object a number, or can it be coerced / defaulted to one?
local function Number (object)
local otype = type(object)
return object == nil or otype == "string" or otype == "number"
end
-- Helper to get an x-coordinate relative to a position, in terms of width...
local function RelativeX (object, t)
return object.x + t * object.contentWidth
end
-- ...and y-coordinate, in terms of height
local function RelativeY (object, t)
return object.y + t * object.contentHeight
end
-- Finds the y-coordinate at the bottom of an object; Numbers resolve like deltas, directly to themselves
local function BottomY (object)
if Number(object) then
return DY(object)
elseif NonGroup(object) then
return RelativeY(object, 1 - object.anchorY)
else
return object.contentBounds.yMax
end
end
-- Finds the x-coordinate at the left side of an object; Numbers behave as per BottomY
local function LeftX (object)
if Number(object) then
return DX(object)
elseif NonGroup(object) then
return RelativeX(object, -object.anchorX)
else
return object.contentBounds.xMin
end
end
-- Finds the x-coordinate at the right side of an object; Numbers behave as per BottomY
local function RightX (object)
if Number(object) then
return DX(object)
elseif NonGroup(object) then
return RelativeX(object, 1 - object.anchorX)
else
return object.contentBounds.xMax
end
end
-- Finds the y-coordinate at the top of an object; Numbers behave as per BottomY
local function TopY (object)
if Number(object) then
return DY(object)
elseif NonGroup(object) then
return RelativeY(object, -object.anchorY)
else
return object.contentBounds.yMin
end
end
--- Finds the y-coordinate above an object or position.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the "above" position.
-- @treturn number Final result, i.e. y-coordinate plus any displacement.
function M.Above (ref, dy)
return floor(TopY(ref) + DY(dy))
end
--- Finds the y-coordinate below an object or position.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the "below" position.
-- @treturn number Final result, i.e. y-coordinate plus any displacement.
function M.Below (ref, dy)
return floor(BottomY(ref) + DY(dy))
end
--- Assigns an object's y-coordinate so that its bottom aligns with either the bottom of a
-- reference object or a y-coordinate.
-- @pobject object Object to align.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the aligned position.
function M.BottomAlignWith (object, ref, dy)
_PutAbove_(object, _Below_(ref), dy)
end
-- Finds the x-coordinate at the center of an object...
local function CenterX (object)
if NonGroup(object) then
return RelativeX(object, .5 - object.anchorX)
else
local bounds = object.contentBounds
return .5 * (bounds.xMin + bounds.xMax)
end
end
-- ...and the y-coordinate
local function CenterY (object)
if NonGroup(object) then
return RelativeY(object, .5 - object.anchorY)
else
local bounds = object.contentBounds
return .5 * (bounds.yMin + bounds.yMax)
end
end
-- Aligns an object's center x-coordinate to an x-coordinate (plus optional delta)...
local function PutCenterAtX (object, x, dx)
object.x = floor(object.x + DX(x) - CenterX(object) + DX(dx))
end
-- ...and aligns the y-coordinate, likewise
local function PutCenterAtY (object, y, dy)
object.y = floor(object.y + DY(y) - CenterY(object) + DY(dy))
end
-- Centers an object horizontally at the content center x-coordinate...
local function PutAtContentCenterX (object, dx)
PutCenterAtX(object, display.contentCenterX, dx)
end
-- ...and vertically at the y-coordinate
local function PutAtContentCenterY (object, dy)
PutCenterAtY(object, display.contentCenterY, dy)
end
--- Assigns an object's x- and y-coordinates so that its center aligns with the center of a
-- reference object.
-- @pobject object Object to align.
-- @pobject ref_object Reference object.
-- @tparam[opt] Number dx Displacement from the aligned position's x-coordinate...
-- @tparam[opt] Number dy ...and from its y-coordinate.
function M.CenterAlignWith (object, ref_object, dx, dy)
_CenterAt_(object, _CenterOf_(ref_object, dx, dy))
end
--- Assigns an object's x- and y-coordinates so that its center is at a position.
-- @pobject object Object to center.
-- @tparam[opt] Number x Position x-coordinate...
-- @tparam[opt] Number y ...and y-coordinate.
-- @tparam[opt] Number dx Displacement from the center's x-coordinate...
-- @tparam[opt] Number dy ...and from its y-coordinate.
function M.CenterAt (object, x, y, dx, dy)
PutCenterAtX(object, x, dx)
PutCenterAtY(object, y, dy)
end
--- Variant of @{CenterAt} that only assigns the x-coordinate.
-- @pobject object Object to center.
-- @tparam[opt] Number x Position x-coordinate.
-- @tparam[opt] Number dx Displacement from the x-coordinate.
function M.CenterAtX (object, x, dx)
PutCenterAtY(object, x, dx)
end
--- Variant of @{CenterAt} that only assigns the y-coordinate.
-- @pobject object Object to center.
-- @tparam[opt] Number y Position y-coordinate.
-- @tparam[opt] Number dy Displacement from the y-coordinate.
function M.CenterAtY (object, y, dy)
PutCenterAtY(object, y, dy)
end
--- Finds the x- and y-coordinates of an object's center.
-- @pobject object Object to query.
-- @tparam[opt] Number dx Displacement from the "center" position, x-coordinate...
-- @tparam[opt] Number dy ...and the y-coordinate.
-- @treturn number Final result, i.e. x-coordinate plus any displacement...
-- @treturn number ...and likewise, for the y-coordinate.
function M.CenterOf (object, dx, dy)
return CenterX(object, dx), CenterY(object, dy)
end
--- Variant of @{CenterOf} that only supplies the x-coordinate.
-- @pobject object Object to query.
-- @tparam[opt] Number dx Displacement from the "center" position.
-- @treturn number Final result, i.e. x-coordinate plus any displacement.
function M.CenterX (object, dx)
return CenterX(object, dx)
end
--- Variant of @{CenterOf} that only supplies the y-coordinate.
-- @pobject object Object to query.
-- @tparam[opt] Number dy Displacement from the "center" position.
-- @treturn number Final result, i.e. y-coordinate plus any displacement.
function M.CenterY (object, dy)
return CenterY(object, dy)
end
--- Assigns an object's x-coordinate so that its left side aligns with either the left side
-- of a reference object or an x-coordinate.
-- @pobject object Object to align.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the aligned position.
function M.LeftAlignWith (object, ref, dx)
_PutRightOf_(object, _LeftOf_(ref), dx)
end
--- Finds the x-coordinate to the left of an object or position.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the "left of" position.
-- @treturn number Final result, i.e. x-coordinate plus any displacement.
function M.LeftOf (ref, dx)
return floor(LeftX(ref) + DX(dx))
end
--- Moves an object along the x-axis relative to its current position.
-- @pobject object Object to move.
-- @tparam[opt] Number dx Displacement.
function M.MoveX (object, dx)
object.x = floor(object.x + DX(dx))
end
--- Moves an object along the y-axis relative to its current position.
-- @pobject object Object to move.
-- @tparam[opt] Number dy Displacement.
function M.MoveY (object, dy)
object.y = floor(object.y + DY(dy))
end
--- Assigns an object's y-coordinate such that its bottom is aligned with the top of a
-- reference object or a y-coordinate.
-- @pobject object Object to position.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the "above" position.
function M.PutAbove (object, ref, dy)
local y = TopY(ref)
if NonGroup(object) then
y = y - (1 - object.anchorY) * object.contentHeight
else
y = y - (object.contentBounds.yMax - object.y)
end
object.y = floor(y + DY(dy))
end
--- Centers an object horizontally and bottom-aligns it to the bottom of the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the center.
-- @tparam[opt] Number dy Displacement from the bottom.
function M.PutAtBottomCenter (object, dx, dy)
PutAtContentCenterX(object, dx)
_PutAbove_(object, display.contentHeight, dy)
end
--- Left-aligns an object to the left side of the content and bottom-aligns it to the
-- bottom of the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the left side.
-- @tparam[opt] Number dy Displacement from the bottom.
function M.PutAtBottomLeft (object, dx, dy)
_PutRightOf_(object, 0, dx)
_PutAbove_(object, display.contentHeight, dy)
end
--- Right-aligns an object to the right side of the content and bottom-aligns it to the
-- bottom of the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the right side.
-- @tparam[opt] Number dy Displacement from the bottom.
function M.PutAtBottomRight (object, dx, dy)
_PutLeftOf_(object, display.contentWidth, dx)
_PutAbove_(object, display.contentHeight, dy)
end
--- Assigns an object's x- and y-coordinates so that its center is at the content center.
-- @pobject object Object to center.
-- @tparam[opt] Number dx Displacement from the center position's x-coordinate...
-- @tparam[opt] Number dy ...and from its y-coordinate.
function M.PutAtCenter (object, dx, dy)
PutAtContentCenterX(object, dx)
PutAtContentCenterY(object, dy)
end
--- Left-aligns an object to the left side of the content and centers it vertically.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the left side.
-- @tparam[opt] Number dy Displacement from the center.
function M.PutAtCenterLeft (object, dx, dy)
_PutRightOf_(object, 0, dx)
PutAtContentCenterY(object, dy)
end
--- Right-aligns an object to the right side of the content and centers it vertically.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the right side.
-- @tparam[opt] Number dy Displacement from the center.
function M.PutAtCenterRight (object, dx, dy)
_PutLeftOf_(object, display.contentWidth, dx)
PutAtContentCenterY(object, dy)
end
--- Variant of @{PutAtCenter} that only assigns the x-coordinate.
-- @pobject object Object to center.
-- @tparam[opt] Number dx Displacement from the x-coordinate.
function M.PutAtCenterX (object, dx)
PutAtContentCenterX(object, dx)
end
--- Variant of @{PutAtCenter} that only assigns the y-coordinate.
-- @pobject object Object to center.
-- @tparam[opt] Number dy Displacement from the y-coordinate.
function M.PutAtCenterY (object, dy)
PutAtContentCenterY(object, dy)
end
--- DOCME
-- TODO: This doesn't seem to be adequate, needs to be split on (x, y)
function M.PutAtFirstHit (object, ref_object, choices, center_on_fail)
local x, y, dx, dy = object.x, object.y, DX(choices.dx), DY(choices.dy)
--
for _, choice in ipairs(choices) do
_CenterAlignWith_(object, ref_object)
--
if choice == "above" or choice == "below" then
if choice == "above" then
_PutAbove_(object, ref_object, -dy)
if _Above_(object) >= 0 then
return
end
else
_PutBelow_(object, ref_object, dy)
if _Below_(object) < display.contentHeight then
return
end
end
--
elseif choice == "left_of" or choice == "right_of" then
if choice == "left_of" then
_PutLeftOf_(object, ref_object, -dx)
if _LeftOf_(object) >= 0 then
return
end
else
_PutRightOf_(object, ref_object, dx)
if _RightOf_(object) < display.contentWidth then
return
end
end
end
end
--
if center_on_fail then
_CenterAlignWith_(object, ref_object)
else
object.x, object.y = x, y
end
end
--- Centers an object horizontally and top-aligns it to the top of the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the center.
-- @tparam[opt] Number dy Displacement from the top.
function M.PutAtTopCenter (object, dx, dy)
PutAtContentCenterX(object, dx)
_PutAbove_(object, 0, dy)
end
--- Left-aligns an object to the left side of the content and top-aligns it to the top of
-- the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the left side.
-- @tparam[opt] Number dy Displacement from the top.
function M.PutAtTopLeft (object, dx, dy)
_PutRightOf_(object, 0, dx)
_PutBelow_(object, 0, dy)
end
--- Right-aligns an object to the right side of the content and top-aligns it to the top
-- of the content.
-- @pobject object Object to position.
-- @tparam[opt] Number dx Displacement from the right side.
-- @tparam[opt] Number dy Displacement from the top.
function M.PutAtTopRight (object, dx, dy)
_PutLeftOf_(object, display.contentWidth, dx)
_PutBelow_(object, 0, dy)
end
--- Assigns an object's y-coordinate such that its top is aligned with the bottom of a
-- reference object or a y-coordinate.
-- @pobject object Object to position.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the "below" position.
function M.PutBelow (object, ref, dy)
local y = BottomY(ref)
if NonGroup(object) then
y = y + object.anchorY * object.contentHeight
else
y = y + (object.y - object.contentBounds.yMin)
end
object.y = floor(y + DY(dy))
end
--- Assigns an object's x-coordinate so that its right side is to the left of a reference
-- object or an x-coordinate.
-- @pobject object Object to position.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the "left of" position.
function M.PutLeftOf (object, ref, dx)
local x = LeftX(ref)
if NonGroup(object) then
x = x - (1 - object.anchorX) * object.contentWidth
else
x = x - (object.contentBounds.xMax - object.x)
end
object.x = floor(x + DX(dx))
end
--- Assigns an object's x-coordinate so that its left side is to the right of a reference
-- object or an x-coordinate.
-- @pobject object Object to position.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the "right of" position.
function M.PutRightOf (object, ref, dx)
local x = RightX(ref)
if NonGroup(object) then
x = x + object.anchorX * object.contentWidth
else
x = x + (object.x - object.contentBounds.xMin)
end
object.x = floor(x + DX(dx))
end
--- Assigns an object's x-coordinate so that its right side aligns with either the right side
-- of a reference object or an x-coordinate.
-- @pobject object Object to align.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the aligned position.
function M.RightAlignWith (object, ref, dx)
_PutLeftOf_(object, _RightOf_(ref), dx)
end
--- Finds the x-coordinate to the right of an object or position.
-- @tparam ?|DisplayObject|Number ref Reference object or x-coordinate.
-- @tparam[opt] Number dx Displacement from the "right of" position.
-- @treturn number Final result, i.e. x-coordinate plus any displacement.
function M.RightOf (ref, dx)
return floor(RightX(ref) + DX(dx))
end
--- Assigns an object's y-coordinate so that its top aligns with either the top of a
-- reference object or a y-coordinate.
-- @pobject object Object to align.
-- @tparam ?|DisplayObject|Number ref Reference object or y-coordinate.
-- @tparam[opt] Number dy Displacement from the aligned position.
function M.TopAlignWith (object, ref, dy)
_PutBelow_(object, _Above_(ref), dy)
end
-- Cache module members.
_Above_ = M.Above
_Below_ = M.Below
_CenterAlignWith_ = M.CenterAlignWith
_CenterAt_ = M.CenterAt
_CenterOf_ = M.CenterOf
_LeftOf_ = M.LeftOf
_PutAbove_ = M.PutAbove
_PutBelow_ = M.PutBelow
_PutLeftOf_ = M.PutLeftOf
_PutRightOf_ = M.PutRightOf
_RightOf_ = M.RightOf
-- Export the module.
return M
--- Implements a small domain-specific language on top of the layout system, in order to
-- make positions and dimensions more expressive.
--
-- With respect to this module, a **DSL_Number** may be either of the following:
--
-- * A number, or a string that @{tonumber} is able to convert. These values are used as is.
-- * A string of the form **"AMOUNT%"**, e.g. `"20%"` or `"-4.2%"`, which resolves to the
-- indicated percent of the content width or height.
--
-- Permission is hereby granted, free of charge, to any person obtaining
-- a copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be
-- included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--
-- [ MIT license: http://www.opensource.org/licenses/mit-license.php ]
--
-- Standard library imports --
local assert = assert
local ceil = math.ceil
local getmetatable = getmetatable
local gsub = string.gsub
local sub = string.sub
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
-- Modules --
local layout = require("layout")
-- Corona globals --
local display = display
-- Cached module references --
local _AddProperties_Metatable_
local _EvalDims_
local _EvalPos_
-- Exports --
local M = {}
--- Augments a display object's metatable, so that its objects can also query **left**,
-- **center_x**, **right**, **bottom**, **center_y**, and **top** properties, with semantics
-- as in @{corona_ui.utils.layout}. Additionally, any of these (and also **x** and **y**)
-- may be assigned, accepting the same inputs as @{PutObjectAt}.
-- @pobject object Object which will have its metatable modified.
function M.AddProperties (object)
local mt = getmetatable(object)
_AddProperties_Metatable_(mt)
setmetatable(object, mt)
end
-- Lookup for __index properties --
local Index = {
left = layout.LeftOf, center_x = layout.CenterX, right = layout.RightOf,
bottom = layout.Below, center_y = layout.CenterY, top = layout.Above
}
-- Lookup for __newindex properties --
local NewIndex = {
left = layout.LeftAlignWith, center_x = layout.CenterAtX, right = layout.RightAlignWith,
bottom = layout.BottomAlignWith, center_y = layout.CenterAtY, top = layout.TopAlignWith
}
-- Forward declarations --
local EvalNewIndex
--- Variant of @{AddProperties} that populates the metatable, e.g. for multiple uses.
-- @ptable mt Metatable (assumed to originate from a display object).
function M.AddProperties_Metatable (mt)
local index = assert(mt.__index, "Missing __index metamethod")
local newindex = assert(mt.__newindex, "Missing __newindex method")
-- Augment __index...
function mt.__index (object, k)
local prop = Index[k]
if prop then
return prop(object)
else
return index(object, k)
end
end
-- ...and __newindex.
function mt.__newindex (object, k, v)
local prop = NewIndex[k]
if type(v) == "string" then
v = EvalNewIndex(object, k, v)
if v == nil then
return
end
end
if prop then
prop(object, v)
else
newindex(object, k, v)
end
end
end
-- Helper to extract a number from a DSL_Number
local function ParseNumber (arg, dim, can_fail)
local num = tonumber(arg)
if num then
return num
else
local ok = sub(arg, -1) == "%"
assert(ok or can_fail, "Invalid argument")
num = ok and tonumber(sub(arg, 1, -2))
return num and num * display[dim] / 100
end
end
--- Normalizes width and height values.
-- @tparam[opt] DSL_Number w If present, the width (e.g. 20, "10%") to normalize...
-- @tparam[opt] DSL_Number h ...and likewise, the height.
-- @treturn ?|number|nil If _w_ is absent, **nil**. Otherwise, the evaluated width.
-- @treturn ?|number|nil As per _w_, for the height.
function M.EvalDims (w, h)
w = w and ceil(ParseNumber(w, "contentWidth")) or nil
h = h and ceil(ParseNumber(h, "contentHeight")) or nil
return w, h
end
-- IDEA: EvalDims_Object... could look for, say, text fields and calculate widths relative to those
-- Command parsed out a choice string; first and second number arguments to command --
local Command, Num1, Num2
-- Dimension corresponding to command --
local Dim
-- Number of tokens parsed during replacement --
local N
-- Tokenizes a command string, of the form "command[, num1[, num2]]"
local function RepToken (token)
if not Command then
Command = token
else
local num = ParseNumber(token, Dim)
if Num1 then
Num2 = num
else
Num1 = num
end
N = N + 1
end
end
-- Helper to evaluate a coordinate command string
local function AuxEvalCoord (arg, choices, dim)
Dim, N, Command, Num1, Num2 = dim, 0
gsub(arg, "[^%s]+", RepToken, 3)
-- Validate that a command was found, that it exists among the choices, and that not
-- too many arguments were provided. If all these are okay, pass along the handler.
local choice = assert(choices[Command], "Unrecognized or missing command")
assert(N <= choice[2], "Too many arguments")
return choice[1]
end
-- Evaluate the more basic commands
local function EvalBasic (arg, choices, dim)
return ParseNumber(arg, dim, true) or AuxEvalCoord(arg, choices, dim)(Num1, Num2)
end
-- Helper for center commands
local function Center (func, coord)
return {
function(delta)
return func(display[coord], delta)
end, 1
}
end
-- Helper to reverse a basic evaluation command
local function ReverseBasic (func)
return {
function(delta)
return func("100%", delta)
end, 1
}
end
-- Choices used by EvalPos to evaluate the x-coordinte... --
local ChoicesX = {
at = { layout.LeftOf, 2 },
center = Center(layout.RightOf, "contentCenterX"),
from_right = ReverseBasic(layout.LeftOf)
}
-- ...and the y-coordinate --
local ChoicesY = {
at = { layout.Above, 2 },
center = Center(layout.Below, "contentCenterY"),
from_bottom = ReverseBasic(layout.Above)
}
--- Normalizes position values.
-- @tparam[opt] ?|DSL_Number|string x If _x_ is a **DSL_Number**, it evaluates as described
-- in the summary. Otherwise, if it is a string, the following commands are available:
--
-- * **"at xpos dx"**: Evaluates both _xpos_ and _dx_ and returns their sum.
-- * **"center dx"**: Evaluates _dx_ and adds it to the content center.
-- * **"from_right dx"**: Evaluates _dx_ and adds it to the right side of the content.
--
-- In the above, _xpos_ and _dx_ are of type **DSL_Number**, and resolve to 0 when absent.
--
-- Example commands: `"at 20 5%"`, `"center 10%"`, `"from_right -20"`, `"from_right -3.5%"`.
-- @tparam[opt] ?|DSL_Number|string y As per _x_. The corresponding choices are **"at"**,
-- **"center"**, and **"from_bottom"**, with the obvious changes.
-- @treturn ?|number|nil If _x_ is absent, **nil**. Otherwise, the evaluated x-coordinate.
-- @treturn ?|number|nil As per _x_.
function M.EvalPos (x, y)
x = x and EvalBasic(x, ChoicesX, "contentWidth")
y = y and EvalBasic(y, ChoicesY, "contentHeight")
return x or nil, y or nil
end
-- Helper to process position / dimension fields in widget constructor options
local function AuxProcessWidgetParams (params, t)
local x, y, w, h = params.x, params.y, _EvalDims_(params.width, params.height)
t.left, t.top, t.x, t.y = _EvalPos_(not x and params.left, not y and params.top)
t.width, t.height = w or t.width, h or t.height
return t, x, y
end
--- Convenience utility for doing DSL evaluation of position- or dimension-type fields in
-- Corona-style widget constructors.
-- @ptable[opt] params If absent, this is a no-op. Otherwise, its **left**, **top**, **x**,
-- **y**, **width**, and **height** fields will be processed.
-- @ptable[opt] t Receives the evaluated params. If absent, a table is supplied (if _params_
-- also exists).
--
-- Any **x** or **y** field in _t_ is removed; the same fields in _params_ are returned,
-- instead. This separation is motivated by consistency with other DSL-using code, where
-- assignment to **x** and **y** fields support commands such as **"from_right -20"**; these
-- assume that the correct dimensions are available, yet this is not so before construction.
--
-- Any **width** or **height** field in _params_ is evaluated, cf. @{EvalDims}, the result
-- being placed into _t_. If a field is absent, it is also left untouched in _t_.
--
-- If **x** (or **y**) is present, any **left** (or **top**) field is removed from _t_.
-- Otherwise, that field is evaluated, cf. @{EvalPos}, and added to _t_.
-- @todo: The width / height are not used, say, to allow from-right/bottom alignment since
-- they aren't officially known until widget creation, and likewise this motivates the
-- separate x, y handling... however the widgets are probably predictable enough in general
-- to relax this, with some relevant notes
-- @treturn ?|table|nil _t_.
-- @treturn ?|DSL_number|string|nil If _params_ is present, the original value of `params.x`...
-- @treturn ?|DSL_number|string|nil ...and `params.y`.
function M.ProcessWidgetParams (params, t)
local x, y
if params then
t, x, y = AuxProcessWidgetParams(params, t or {})
end
return t, x, y
end
--- Variant of @{ProcessWidgetParams} where _params_ does double duty as _t_.
-- @ptable[opt] params If absent, this is a no-op. Otherwise, as per @{ProcessWidgetParams}.
-- @treturn ?|table|nil _params_.
-- @treturn ?|DSL_number|string|nil If _params_ is present, the original value of `params.x`...
-- @treturn ?|DSL_number|string|nil ...and `params.y`.
function M.ProcessWidgetParams_InPlace (params)
local t, x, y
if params then
t, x, y = AuxProcessWidgetParams(params, params)
end
return t, x, y
end
-- Helper to reverse an object-based evaluation command
local function ReversePut (func)
return {
function(object, delta)
func(object, "100%", delta)
end, 1
}
end
-- Choices used by PutObjectAt to evaluate the x-coordinate... --
local PutChoicesX = {
center = { layout.PutAtCenterX, 1 },
from_right = ReversePut(layout.PutLeftOf),
from_right_align = ReversePut(layout.RightAlignWith),
left_of = { layout.PutLeftOf, 2 },
right_of = { layout.PutRightOf, 2 }
}
-- ...and the y-coordinate --
local PutChoicesY = {
above = { layout.PutAbove, 2 },
below = { layout.PutBelow, 2 },
center = { layout.PutAtCenterY, 1 },
from_bottom = ReversePut(layout.PutAbove),
from_bottom_align = ReversePut(layout.BottomAlignWith)
}
-- Evaluate commands that involve objects
local function EvalPut (object, arg, choices, coord, dim)
if arg then
local dim = choices == PutChoicesX and "contentWidth" or "contentHeight"
local num = ParseNumber(arg, dim, true)
if num then
object[coord] = num
else
AuxEvalCoord(arg, choices, dim)(object, Num1, Num2)
end
end
end
--- Puts an object at a given normalized position.
-- @pobject object Object to position.
-- @tparam ?|DSL_Number|string|nil x If absent, the x-coordinate is untouched. Otherwise,
-- the number is evaluated as in @{EvalPos} and assigned to the x-coordinate. Available
-- commands, and the corresponding x-coordinate, are:
--
-- * **"center dx"**: The content center's x-coordinate.
-- * **"from_right dx"**: The right side of the content.
-- * **"from\_right\_align dx"**: The value that aligns the right side of _object_ to the right
-- side of the content.
-- * **"left_of xpos dx"**: The value that puts the right side of _object_ at _xpos_.
-- * **"right_of xpos dx"**: The value that puts the left side of _object_ at _xpos_.
--
-- In each case, _dx_ is evaluated and added to the coordinate; the sum is the final result.
-- @tparam ?|DSL_Number|string|nil y As per _x_. The corresponding choices are **"center"**,
-- **"from_bottom"**, **"from\_bottom\_align"**, **"above"**, and **"below"**, with the obvious changes.
function M.PutObjectAt (object, x, y)
EvalPut(object, x, PutChoicesX, "x")
EvalPut(object, y, PutChoicesY, "y")
end
-- Set of valid "put choices" for the x-coordinate... --
local X = { x = true, left = true, center_x = true, right = true }
-- ...and for the y-coordinate --
local Y = { y = true, bottom = true, center_y = true, top = true }
-- Helper to evaluate a __newindex'd property
function EvalNewIndex (object, k, v)
if k == "width" or k == "height" then
local w, h = _EvalDims_(v, v)
return k == "width" and w or h -- Let the original __newindex take it from here
elseif X[k] then
EvalPut(object, v, PutChoicesX, k) -- All done, so return nothing
elseif Y[k] then
EvalPut(object, v, PutChoicesY, k) -- Ditto
else
return v -- Unhandled; just let the original __newindex handle it
end
end
-- Cache module members.
_AddProperties_Metatable_ = M.AddProperties_Metatable
_EvalDims_ = M.EvalDims
_EvalPos_ = M.EvalPos
-- TODO: Pens, cursors?
-- Export the module.
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment