SILE.frames = {}
local cassowary = require("cassowary")
local solver = cassowary.SimplexSolver()
local solverNeedsReloading = true
local widthdims = pl.Set({ "left", "right", "width" })
local heightdims = pl.Set({ "top", "bottom", "height" })
local alldims = widthdims + heightdims
SILE.framePrototype = pl.class({
direction = "LTR-TTB",
enterHooks = {},
leaveHooks = {},
_init = function (self, spec, dummy)
local direction = SILE.documentState.direction
if direction then
self.direction = direction
end
self.constraints = {}
self.variables = {}
self.id = spec.id
for k, v in pairs(spec) do
if not alldims[k] then
self[k] = v
end
end
self.balanced = SU.boolean(self.balanced, false)
if not dummy then
for method in pairs(alldims) do
self.variables[method] = cassowary.Variable({ name = spec.id .. "_" .. method })
self[method] = function (instance_self)
instance_self:solve()
return SILE.types.measurement(instance_self.variables[method].value)
end
end
for method in pairs(alldims) do
if spec[method] then
self:constrain(method, spec[method])
end
end
end
end,
init = function (self, typesetter)
self.state = { totals = { height = SILE.types.measurement(0) } }
self:enter(typesetter)
self:newLine(typesetter)
if self:pageAdvanceDirection() == "TTB" then
self.state.cursorY = self:top()
elseif self:pageAdvanceDirection() == "LTR" then
self.state.cursorX = self:left()
elseif self:pageAdvanceDirection() == "RTL" then
self.state.cursorX = self:right()
elseif self:pageAdvanceDirection() == "BTT" then
self.state.cursorY = self:bottom()
end
end,
constrain = function (self, method, dimension)
self.constraints[method] = tostring(dimension)
self:invalidate()
end,
invalidate = function ()
solverNeedsReloading = true
end,
relax = function (self, method)
self.constraints[method] = nil
end,
reifyConstraint = function (self, solver, method, stay)
local constraint = self.constraints[method]
if not constraint then
return
end
constraint = SU.type(constraint) == "measurement" and constraint:tonumber() or SILE.frameParser:match(constraint)
SU.debug("frames", "Adding constraint", self.id, function ()
return "(" .. method .. ") = " .. tostring(constraint)
end)
local eq = cassowary.Equation(self.variables[method], constraint)
solver:addConstraint(eq)
if stay then
solver:addStay(eq)
end
end,
addWidthHeightDefinitions = function (self, solver)
local vars = self.variables
solver:addConstraint(cassowary.Equation(vars.width, cassowary.minus(vars.right, vars.left)))
solver:addConstraint(cassowary.Equation(vars.height, cassowary.minus(vars.bottom, vars.top)))
end,
solve = function (_)
if not solverNeedsReloading then
return
end
SU.debug("frames", "Solving...")
solver = cassowary.SimplexSolver()
if SILE.frames.page then
for method, _ in pairs(SILE.frames.page.constraints) do
SILE.frames.page:reifyConstraint(solver, method, true)
end
SILE.frames.page:addWidthHeightDefinitions(solver)
end
for id, frame in pairs(SILE.frames) do
if not (id == "page") then
for method, _ in pairs(frame.constraints) do
frame:reifyConstraint(solver, method)
end
frame:addWidthHeightDefinitions(solver)
end
end
solver:solve()
solverNeedsReloading = false
end,
writingDirection = function (self)
return self.direction:match("^(%a+)") or "LTR"
end,
pageAdvanceDirection = function (self)
return self.direction:match("-(%a+)$") or "TTB"
end,
advanceWritingDirection = function (self, length)
local amount = SU.cast("number", length)
if amount == 0 then
return
end
if self:writingDirection() == "RTL" then
self.state.cursorX = self.state.cursorX - amount
elseif self:writingDirection() == "LTR" then
self.state.cursorX = self.state.cursorX + amount
elseif self:writingDirection() == "TTB" then
self.state.cursorY = self.state.cursorY + amount
elseif self:writingDirection() == "BTT" then
self.state.cursorY = self.state.cursorY - amount
end
end,
advancePageDirection = function (self, length)
local amount = SU.cast("number", length)
if amount == 0 then
return
end
if self:pageAdvanceDirection() == "TTB" then
self.state.cursorY = self.state.cursorY + amount
elseif self:pageAdvanceDirection() == "RTL" then
self.state.cursorX = self.state.cursorX - amount
elseif self:pageAdvanceDirection() == "LTR" then
self.state.cursorX = self.state.cursorX + amount
elseif self:pageAdvanceDirection() == "BTT" then
self.state.cursorY = self.state.cursorY - amount
end
end,
newLine = function (self, _)
if self:writingDirection() == "LTR" then
self.state.cursorX = self:left()
elseif self:writingDirection() == "RTL" then
self.state.cursorX = self:right()
elseif self:writingDirection() == "TTB" then
self.state.cursorY = self:top()
elseif self:writingDirection() == "BTT" then
self.state.cursorY = self:bottom()
end
end,
lineWidth = function (self)
SU.warn("Method :lineWidth() is deprecated, please use :getLineWidth()")
return self:getLineWidth()
end,
getLineWidth = function (self)
if self:writingDirection() == "LTR" or self:writingDirection() == "RTL" then
return self:width()
else
return self:height()
end
end,
pageTarget = function (self)
SU.warn("Method :pageTarget() is deprecated, please use :getTargetLength()")
return self:getTargetLength()
end,
getTargetLength = function (self)
local direction = self:pageAdvanceDirection()
if direction == "TTB" or direction == "BTT" then
return self:height()
else
return self:width()
end
end,
enter = function (self, typesetter)
for i = 1, #self.enterHooks do
self.enterHooks[i](self, typesetter)
end
end,
leave = function (self, typesetter)
for i = 1, #self.leaveHooks do
self.leaveHooks[i](self, typesetter)
end
end,
isAbsoluteConstraint = function (self, method)
if not self.constraints[method] then
return false
end
local constraint = SILE.frameParser:match(self.constraints[method])
if type(constraint) ~= "table" then
return true
end
if not constraint.terms then
return false
end
for clv, _ in pairs(constraint.terms) do
if clv.name and not clv.name:match("^page_") then
return false
end
end
return true
end,
isMainContentFrame = function (self)
local tpt = SILE.documentState.thisPageTemplate
local frame = tpt.firstContentFrame
while frame do
if frame == self then
return true
end
if frame.next then
frame = SILE.getFrame(frame.next)
else
return false
end
end
return false
end,
__tostring = function (self)
local str = "<Frame: " .. self.id .. ": "
str = str .. " next=" .. (self.next or "nil") .. " "
for method, dimension in pairs(self.constraints) do
str = str .. method .. "=" .. dimension .. "; "
end
if self.hanmen then
str = str .. "tate=" .. (self.tate and "true" or "false") .. "; "
str = str .. "gridsize=" .. self.gridsize .. "; "
str = str .. "linegap=" .. self.linegap .. "; "
str = str .. "linelength=" .. self.linelength .. "; "
str = str .. "linecount=" .. self.linecount .. "; "
end
str = str .. ">"
return str
end,
})
SILE.newFrame = function (spec, prototype)
SU.required(spec, "id", "frame declaration")
prototype = prototype or SILE.framePrototype
local frame = prototype(spec)
SILE.frames[spec.id] = frame
return frame
end
SILE.getFrame = function (id)
if type(id) == "table" then
SU.error("Passed a table, expected a string", true)
end
local frame, last_attempt
while not frame do
frame = SILE.frames[id]
id = id:gsub("_$", "")
if id == last_attempt then
break
end
last_attempt = id
end
return frame or SU.warn("Couldn't find frame ID " .. id, true)
end
SILE.parseComplexFrameDimension = function (dimension)
local length = SILE.frameParser:match(SU.cast("string", dimension))
if type(length) == "table" then
local g = cassowary.Variable({ name = "t" })
local eq = cassowary.Equation(g, length)
solverNeedsReloading = true
solver:addConstraint(eq)
SILE.frames.page:solve()
solverNeedsReloading = true
return g.value
end
return length
end