2023-07-11 19:54:29 -06:00
|
|
|
package typeset
|
2023-07-06 23:49:32 -06:00
|
|
|
|
|
|
|
import "unicode"
|
|
|
|
import "golang.org/x/image/font"
|
|
|
|
import "golang.org/x/image/math/fixed"
|
|
|
|
|
|
|
|
// Align specifies a text alignment method.
|
|
|
|
type Align int
|
|
|
|
|
|
|
|
const (
|
2023-09-15 13:22:44 -06:00
|
|
|
// X | Y
|
|
|
|
AlignStart Align = iota // left | top
|
|
|
|
AlignMiddle // center | center
|
|
|
|
AlignEnd // right | bottom
|
|
|
|
AlignEven // justified | evenly spaced
|
2023-07-06 23:49:32 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// RuneLayout contains layout information for a single rune relative to its
|
|
|
|
// word.
|
|
|
|
type RuneLayout struct {
|
|
|
|
X fixed.Int26_6
|
|
|
|
Width fixed.Int26_6
|
|
|
|
Rune rune
|
|
|
|
}
|
|
|
|
|
|
|
|
// WordLayout contains layout information for a single word relative to its
|
|
|
|
// line.
|
|
|
|
type WordLayout struct {
|
|
|
|
X fixed.Int26_6
|
|
|
|
Width fixed.Int26_6
|
|
|
|
SpaceAfter fixed.Int26_6
|
|
|
|
Runes []RuneLayout
|
|
|
|
}
|
|
|
|
|
|
|
|
// DoWord consumes exactly one word from the given string, and produces a word
|
|
|
|
// layout according to the given font. It returns the remaining text as well.
|
|
|
|
func DoWord (text []rune, face font.Face) (word WordLayout, remaining []rune) {
|
|
|
|
remaining = text
|
|
|
|
gettingSpace := false
|
|
|
|
x := fixed.Int26_6(0)
|
|
|
|
lastRune := rune(-1)
|
|
|
|
for _, char := range text {
|
|
|
|
// if we run into a line break, we must break out immediately
|
|
|
|
// because it is not DoWord's job to handle that.
|
|
|
|
if char == '\n' { break }
|
|
|
|
|
|
|
|
// if we suddenly run into spaces, and then run into a word
|
|
|
|
// again, we must break out immediately.
|
|
|
|
if unicode.IsSpace(char) {
|
|
|
|
gettingSpace = true
|
|
|
|
} else if gettingSpace {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// apply kerning
|
|
|
|
if lastRune >= 0 { x += face.Kern(lastRune, char) }
|
|
|
|
lastRune = char
|
|
|
|
|
|
|
|
// consume and process the rune
|
|
|
|
remaining = remaining[1:]
|
2024-09-03 15:04:57 -06:00
|
|
|
advance, ok := face.GlyphAdvance(char)
|
|
|
|
if !ok {
|
|
|
|
advance = tofuAdvance(face)
|
|
|
|
}
|
|
|
|
runeLayout := RuneLayout {
|
2023-07-06 23:49:32 -06:00
|
|
|
X: x,
|
|
|
|
Width: advance,
|
|
|
|
Rune: char,
|
2024-09-03 15:04:57 -06:00
|
|
|
}
|
|
|
|
word.Runes = append(word.Runes, runeLayout)
|
2023-07-06 23:49:32 -06:00
|
|
|
|
|
|
|
// advance
|
|
|
|
if gettingSpace {
|
|
|
|
word.SpaceAfter += advance
|
|
|
|
} else {
|
|
|
|
word.Width += advance
|
|
|
|
}
|
|
|
|
x += advance
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// LastRune returns the last rune in the word.
|
|
|
|
func (word WordLayout) LastRune () rune {
|
|
|
|
if word.Runes == nil {
|
|
|
|
return -1
|
|
|
|
} else {
|
|
|
|
return word.Runes[len(word.Runes) - 1].Rune
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FirstRune returns the last rune in the word.
|
|
|
|
func (word WordLayout) FirstRune () rune {
|
|
|
|
if word.Runes == nil {
|
|
|
|
return -1
|
|
|
|
} else {
|
|
|
|
return word.Runes[0].Rune
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// LineLayout contains layout information for a single line.
|
|
|
|
type LineLayout struct {
|
|
|
|
Y fixed.Int26_6
|
|
|
|
Width fixed.Int26_6
|
|
|
|
ContentWidth fixed.Int26_6
|
|
|
|
SpaceAfter fixed.Int26_6
|
|
|
|
Words []WordLayout
|
|
|
|
BreakAfter bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// DoLine consumes exactly one line from the given string, and produces a line
|
|
|
|
// layout according to the given font. It returns the remaining text as well. If
|
2023-07-15 18:19:35 -06:00
|
|
|
// wrap is set to true, this function will stop processing words once maxWidth
|
|
|
|
// is crossed. The word which would have crossed over the limit will not be
|
|
|
|
// processed.
|
2023-09-14 18:28:32 -06:00
|
|
|
func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line LineLayout, remaining []rune) {
|
2023-07-06 23:49:32 -06:00
|
|
|
remaining = text
|
|
|
|
x := fixed.Int26_6(0)
|
|
|
|
lastWord := WordLayout { }
|
|
|
|
isFirstWord := true
|
|
|
|
for {
|
|
|
|
// process one word
|
|
|
|
word, remainingFromWord := DoWord(remaining, face)
|
|
|
|
word.X = x
|
|
|
|
x += word.Width
|
|
|
|
|
2023-09-14 18:28:32 -06:00
|
|
|
// if we have gone over the preferred width, stop processing
|
2023-07-15 18:19:35 -06:00
|
|
|
// words (if wrap is enabled)
|
2023-09-14 18:28:32 -06:00
|
|
|
if !isFirstWord && wrap && x > width {
|
2023-07-06 23:49:32 -06:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
x += word.SpaceAfter
|
|
|
|
remaining = remainingFromWord
|
|
|
|
|
|
|
|
// if the word actually has contents, add it
|
|
|
|
if word.Runes != nil {
|
|
|
|
lastWord = word
|
|
|
|
line.Words = append(line.Words, word)
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we have hit the end of the line, stop processing words
|
|
|
|
if len(remaining) == 0 { break }
|
|
|
|
if remaining[0] == '\n' {
|
|
|
|
line.BreakAfter = true
|
|
|
|
remaining = remaining[1:]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
isFirstWord = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// set the width of the line's content.
|
|
|
|
line.ContentWidth = lastWord.X + lastWord.Width
|
2023-09-15 13:22:44 -06:00
|
|
|
line.Width = width
|
2023-09-14 18:28:32 -06:00
|
|
|
line.SpaceAfter = lastWord.SpaceAfter
|
2023-07-06 23:49:32 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-06 01:37:08 -06:00
|
|
|
// Length returns the amount of runes within the line, including the trailing
|
|
|
|
// line break if it exists.
|
|
|
|
func (line *LineLayout) Length () int {
|
|
|
|
lineSize := 0
|
|
|
|
for _, word := range line.Words {
|
|
|
|
lineSize += len(word.Runes)
|
|
|
|
}
|
|
|
|
if line.BreakAfter { lineSize ++ }
|
|
|
|
return lineSize
|
|
|
|
}
|
|
|
|
|
2023-07-06 23:49:32 -06:00
|
|
|
// Align aligns the text in the line according to the specified alignment
|
|
|
|
// method.
|
|
|
|
func (line *LineLayout) Align (align Align) {
|
|
|
|
if len(line.Words) == 0 { return }
|
2023-09-15 13:22:44 -06:00
|
|
|
|
|
|
|
if align == AlignEven {
|
2023-07-06 23:49:32 -06:00
|
|
|
line.justify()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
leftOffset := -line.Words[0].X
|
|
|
|
|
2023-09-15 13:22:44 -06:00
|
|
|
if align == AlignMiddle {
|
2023-07-06 23:49:32 -06:00
|
|
|
leftOffset += (line.Width - line.ContentWidth) / 2
|
2023-09-15 13:22:44 -06:00
|
|
|
} else if align == AlignEnd {
|
2023-07-06 23:49:32 -06:00
|
|
|
leftOffset += line.Width - line.ContentWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
for index := range line.Words {
|
|
|
|
line.Words[index].X += leftOffset
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (line *LineLayout) justify () {
|
|
|
|
if len(line.Words) < 2 {
|
2023-09-15 13:22:44 -06:00
|
|
|
line.Align(AlignStart)
|
2023-07-06 23:49:32 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// We are going to be moving the words, so we can't take SpaceAfter into
|
|
|
|
// account.
|
|
|
|
trueContentWidth := fixed.Int26_6(0)
|
|
|
|
for _, word := range line.Words {
|
|
|
|
trueContentWidth += word.Width
|
|
|
|
}
|
|
|
|
|
2023-09-15 13:54:46 -06:00
|
|
|
spaceCount := len(line.Words) - 1
|
|
|
|
spacePerWord := (line.Width - trueContentWidth) / fixed.Int26_6(spaceCount)
|
2023-07-06 23:49:32 -06:00
|
|
|
x := fixed.Int26_6(0)
|
|
|
|
for index, word := range line.Words {
|
|
|
|
line.Words[index].X = x
|
|
|
|
x += spacePerWord + word.Width
|
|
|
|
}
|
|
|
|
}
|
2024-09-03 15:04:57 -06:00
|
|
|
|
|
|
|
func tofuAdvance (face font.Face) fixed.Int26_6 {
|
|
|
|
if advance, ok := face.GlyphAdvance('M'); ok {
|
|
|
|
return advance
|
|
|
|
} else {
|
|
|
|
return 16
|
|
|
|
}
|
|
|
|
}
|