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 lineMax, lineMaxSpace := calculateLineExtents ( tokens[lastWord], tokens[lastToken]) if lineMax > extents.Max.X { extents.Max.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 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 (lastWord, lastToken token) (lineMax, lineMaxSpace fixed.Int26_6) { if lastWord.kind == tokenKindWord { // the line had a word in it lineMax = lastWord.position.X + lastWord.width } lineMaxSpace = lastToken.position.X + lastToken.width // println(lineMax.String(), lineMaxSpace.String()) return lineMax, lineMaxSpace } 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 }