typeset/layout.go

246 lines
6.0 KiB
Go
Raw Normal View History

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:]
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,
}
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
// 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)
isFirstWord := true
for {
// process one word
word, remainingFromWord := DoWord(remaining, face)
x += word.Width
2023-09-14 18:28:32 -06:00
// if we have gone over the preferred width, stop processing
// 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 {
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.
2023-09-15 13:22:44 -06:00
line.Width = width
2024-09-05 23:22:51 -06:00
if len(line.Words) > 0 {
lastWord := line.Words[len(line.Words) - 1]
line.ContentWidth = x - lastWord.SpaceAfter
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.
2024-09-05 23:00:28 -06:00
func (line *LineLayout) Align (align Align, tabWidth fixed.Int26_6) {
2023-07-06 23:49:32 -06:00
if len(line.Words) == 0 { return }
2023-09-15 13:22:44 -06:00
if align == AlignEven {
2024-09-05 23:00:28 -06:00
line.justify(tabWidth)
} else {
line.contract(tabWidth)
var leftOffset fixed.Int26_6
if align == AlignMiddle {
leftOffset = (line.Width - line.ContentWidth) / 2
} else if align == AlignEnd {
leftOffset = line.Width - line.ContentWidth
}
2023-07-06 23:49:32 -06:00
2024-09-05 23:00:28 -06:00
for index := range line.Words {
line.Words[index].X += leftOffset
}
2023-07-06 23:49:32 -06:00
}
2024-09-05 23:00:28 -06:00
}
// assume line has content > 0
func (line *LineLayout) contract (tabWidth fixed.Int26_6) {
x := fixed.Int26_6(0)
2024-09-05 23:22:51 -06:00
for index, word := range line.Words {
2024-09-05 23:00:28 -06:00
word.X = x
x += word.Width
x += word.SpaceAfter
2024-09-05 23:22:51 -06:00
line.Words[index] = word
2023-07-06 23:49:32 -06:00
}
2024-09-05 23:00:28 -06:00
lastWord := line.Words[len(line.Words) - 1]
line.ContentWidth = lastWord.X + lastWord.Width
line.SpaceAfter = lastWord.SpaceAfter
2023-07-06 23:49:32 -06:00
}
2024-09-05 23:00:28 -06:00
// assume line has content > 0
func (line *LineLayout) justify (tabWidth fixed.Int26_6) {
2024-09-05 23:22:51 -06:00
if len(line.Words) <= 1 {
2024-09-05 23:00:28 -06:00
line.Align(AlignStart, tabWidth)
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
}
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
}
}
func tofuAdvance (face font.Face) fixed.Int26_6 {
if advance, ok := face.GlyphAdvance('M'); ok {
return advance
} else {
return 16
}
}
2024-09-03 15:59:07 -06:00
func tofuBounds (face font.Face) (fixed.Rectangle26_6, fixed.Int26_6) {
if bounds, advance, ok := face.GlyphBounds('M'); ok {
return bounds, advance
} else {
return fixed.R(0, -16, 14, 0), 16
}
}