typeset/flow.go

240 lines
5.9 KiB
Go

package typeset
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// TODO perhaps follow https://unicode.org/reports/tr14/
func reflow (
tokens []token,
face font.Face, size fixed.Point26_6,
wrap bool, xAlign, yAlign Align,
) (
extents, extentsSpace fixed.Rectangle26_6,
minimumSize fixed.Point26_6,
) {
if len(tokens) == 0 { return }
metrics := face.Metrics()
var dot fixed.Point26_6
const notSeen = -1
firstWord := notSeen
lineStart := 0
lineEnd := 0
lastWord := 0
lastNonLineBreak := notSeen
nLines := 0
firstLine := true
newline := func (wrapped bool) {
// if the line isn't empty
if lineStart != lineEnd {
// align line
alignLine (
tokens[lineStart:lineEnd],
size.X, xAlign, lineEnd == len(tokens))
// calculate extents
var lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6
lineMinSpace = tokens[lineStart].position.X
if firstWord == notSeen {
lineMin = lineMinSpace
} else {
lineMin = tokens[firstWord].position.X
}
lastWordTok := tokens[lastWord]
if lastWordTok.kind == tokenKindWord {
// the line had a word in it
lineMax = lastWordTok.position.X + lastWordTok.width
}
if wrapped || lastNonLineBreak == notSeen {
lineMaxSpace = lineMax
} else {
lastTokenTok := tokens[lastNonLineBreak]
lineMaxSpace = lastTokenTok.position.X + lastTokenTok.width
}
// println(lineMax.String(), lineMaxSpace.String())
if lineMin < extents.Min.X || firstLine { extents.Min.X = lineMin }
if lineMinSpace < extentsSpace.Min.X || firstLine { extentsSpace.Min.X = lineMinSpace }
if lineMax > extents.Max.X { extents.Max.X = lineMax }
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
firstLine = false
}
// update dot
dot.Y += metrics.Height
dot.X = 0
// update indices, counts
lineStart = lineEnd
lastWord = lineEnd
lastNonLineBreak = notSeen
firstWord = notSeen
nLines ++
}
// for each line, arrange and align while calculating effective
// bounds/extents
sawLineBreak := false
for index, token := range tokens {
lineEnd = index
updateIndices := func () {
if token.kind != tokenKindLineBreak {
lastNonLineBreak = index
}
if token.kind == tokenKindWord {
lastWord = index
if firstWord == notSeen {
firstWord = index
}
}
}
// demarcate lines
if sawLineBreak {
newline(false)
sawLineBreak = false
}
if token.kind == tokenKindLineBreak {
updateIndices()
tokens[index].position = dot
sawLineBreak = true
} else {
needWrap :=
wrap &&
token.kind == tokenKindWord &&
dot.X + token.width > size.X
if needWrap {
newline(true)
}
updateIndices()
tokens[index].position = dot
dot.X += token.width
}
}
lineEnd ++ // make lineEnd equal to len(tokens)
newline(false)
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
// second, vertical alignment pass
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
// calculate extents
extentsVerticalOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
extents.Max.Y = dot.Y + metrics.Descent
extentsSpace.Max.Y = dot.Y + metrics.Descent
minimumSize.X = fixedRectDx(extents)
minimumSize.Y = fixedRectDy(extents)
extents = extents.Sub(extentsVerticalOffset)
extentsSpace = extentsSpace.Sub(extentsVerticalOffset)
return
}
func fixedRectDx (rect fixed.Rectangle26_6) fixed.Int26_6 {
return rect.Max.X - rect.Min.X
}
func fixedRectDy (rect fixed.Rectangle26_6) fixed.Int26_6 {
return rect.Max.Y - rect.Min.Y
}
func calculateLineExtents (
firstWord, firstToken, lastWord, lastToken token,
) (
lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6,
) {
return
}
func alignLinesVertically (tokens []token, height, contentHeight fixed.Int26_6, align Align) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
var topOffset fixed.Int26_6
switch align {
case AlignMiddle: topOffset = (height - contentHeight) / 2
case AlignEnd, AlignEven: topOffset = height - contentHeight
}
for index := range tokens {
tokens[index].position.Y += topOffset
}
}
func alignLine (tokens []token, width fixed.Int26_6, align Align, atEnd bool) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
if align == AlignEven {
alignLineJustify(tokens, width, atEnd)
return
}
var leftOffset fixed.Int26_6
contentWidth := lineContentWidth(tokens)
switch align {
case AlignMiddle: leftOffset = (width - contentWidth) / 2
case AlignEnd: leftOffset = width - contentWidth
}
for index := range tokens {
tokens[index].position.X += leftOffset
}
}
func alignLineJustify (tokens []token, width fixed.Int26_6, atEnd bool) {
cantJustify :=
len(tokens) < 2 ||
atEnd ||
tokens[len(tokens) - 1].kind == tokenKindLineBreak
if cantJustify {
alignLine(tokens, width, AlignStart, atEnd)
return
}
contentWidth, wordCount := lineContentWordWidth(tokens)
spaceCount := wordCount - 1
if spaceCount == 0 { return }
spacePerWord := (width - contentWidth) / fixed.Int26_6(spaceCount)
var x fixed.Int26_6
for index, token := range tokens {
if token.kind == tokenKindWord {
tokens[index].position.X = x
x += spacePerWord + token.width
} else {
tokens[index].position.X = x
}
}
}
func lineContentWordWidth (tokens []token) (fixed.Int26_6, int) {
var width fixed.Int26_6
var count int
for _, token := range tokens {
if token.kind == tokenKindWord {
width += token.width
count ++
}
}
return width, count
}
func lineContentWidth (tokens []token) fixed.Int26_6 {
var width, spaceWidth fixed.Int26_6
for _, token := range tokens {
if token.kind == tokenKindWord {
width += spaceWidth + token.width
spaceWidth = 0
} else {
spaceWidth = token.width
}
}
return width
}