diff --git a/flow.go b/flow.go new file mode 100644 index 0000000..1797d9a --- /dev/null +++ b/flow.go @@ -0,0 +1,191 @@ +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 +}