240 lines
5.9 KiB
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
|
|
}
|