192 lines
4.5 KiB
Go
192 lines
4.5 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
|
||
|
lineStart := 0
|
||
|
lineEnd := 0
|
||
|
lastWord := 0
|
||
|
lastToken := 0
|
||
|
nLines := 0
|
||
|
|
||
|
newline := func () {
|
||
|
// if the line isn't empty
|
||
|
if lineStart != lineEnd {
|
||
|
// align line
|
||
|
alignLine (
|
||
|
tokens[lineStart:lineEnd],
|
||
|
size.X, xAlign, lineEnd == len(tokens))
|
||
|
|
||
|
// calculate extents
|
||
|
lastWordTok := tokens[lastWord]
|
||
|
lastTokenTok := tokens[lastToken]
|
||
|
lineMax := lastWordTok.position.X + lastWordTok.width
|
||
|
lineMaxSpace := lastTokenTok.position.X + lastTokenTok.width
|
||
|
if lineMax > minimumSize.X { minimumSize.X = lineMax }
|
||
|
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
|
||
|
}
|
||
|
|
||
|
// update dot
|
||
|
dot.Y += metrics.Height
|
||
|
dot.X = 0
|
||
|
|
||
|
// update indices, counts
|
||
|
lineStart = lineEnd
|
||
|
lastWord = lineEnd
|
||
|
nLines ++
|
||
|
}
|
||
|
|
||
|
// for each line, arrange and align while calculating effective
|
||
|
// bounds/extents
|
||
|
sawLineBreak := false
|
||
|
for index, token := range tokens {
|
||
|
lineEnd = index
|
||
|
updateIndices := func () {
|
||
|
lastToken = index
|
||
|
if token.kind == tokenKindWord {
|
||
|
lastWord = index
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// demarcate lines
|
||
|
if sawLineBreak {
|
||
|
newline()
|
||
|
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()
|
||
|
}
|
||
|
updateIndices()
|
||
|
tokens[index].position = dot
|
||
|
dot.X += token.width
|
||
|
}
|
||
|
}
|
||
|
lineEnd ++ // make lineEnd equal to len(tokens)
|
||
|
newline()
|
||
|
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
|
||
|
|
||
|
// second, vertical alignment pass
|
||
|
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
|
||
|
|
||
|
// calculate extents
|
||
|
extentsOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
|
||
|
extents.Max.X = minimumSize.X
|
||
|
extents.Max.Y = dot.Y + metrics.Descent
|
||
|
extentsSpace.Max.Y = dot.Y + metrics.Descent
|
||
|
extents = extents.Sub(extentsOffset)
|
||
|
extentsSpace = extentsSpace.Sub(extentsOffset)
|
||
|
|
||
|
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
|
||
|
}
|