typeset/setter.go

398 lines
11 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 "image"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// TypeSetter manages several lines of text, and can perform layout operations
// on them. It automatically avoids performing redundant work. It has no
// constructor and its zero value can be used safely.
type TypeSetter struct {
lines []LineLayout
text []rune
layoutClean bool
alignClean bool
2023-09-15 13:22:44 -06:00
hAlign, vAlign Align
face font.Face
width, height int
wrap bool
2023-07-15 17:52:06 -06:00
2023-07-15 18:03:19 -06:00
minWidth fixed.Int26_6
2023-07-06 23:49:32 -06:00
layoutBounds image.Rectangle
layoutBoundsSpace image.Rectangle
}
func (setter *TypeSetter) needLayout () {
if setter.layoutClean { return }
setter.layoutClean = true
setter.alignClean = false
setter.lines = nil
setter.layoutBounds = image.Rectangle { }
setter.layoutBoundsSpace = image.Rectangle { }
2023-07-15 18:03:19 -06:00
setter.minWidth = 0
2023-09-15 13:22:44 -06:00
if setter.face == nil { return }
2023-07-06 23:49:32 -06:00
2023-07-15 18:03:19 -06:00
horizontalExtent := fixed.Int26_6(0)
horizontalExtentSpace := fixed.Int26_6(0)
metrics := setter.face.Metrics()
remaining := setter.text
y := fixed.Int26_6(0)
2023-09-15 13:22:44 -06:00
// function to add line and update bounds statistics
addLine := func (line LineLayout) {
2023-07-06 23:49:32 -06:00
line.Y = y
y += metrics.Height
if line.ContentWidth > horizontalExtent {
horizontalExtent = line.ContentWidth
2023-07-06 23:49:32 -06:00
}
lineWidthSpace := line.ContentWidth + line.SpaceAfter
2023-07-15 18:03:19 -06:00
if lineWidthSpace > horizontalExtentSpace {
horizontalExtentSpace = lineWidthSpace
2023-07-06 23:49:32 -06:00
}
setter.lines = append(setter.lines, line)
}
2023-09-14 18:28:32 -06:00
// process every line until there are no more remaining runes
for len(remaining) > 0 {
line, remainingFromLine := DoLine (
remaining, setter.face, setter.wrap,
2023-09-15 13:22:44 -06:00
fixed.I(setter.width))
remaining = remainingFromLine
addLine(line)
}
// if there were no lines processed or the last line has a break after
// it, add a blank line at the end
needBlankLine :=
len(setter.lines) == 0 ||
setter.lines[len(setter.lines) - 1].BreakAfter
if needBlankLine { addLine(LineLayout { }) }
2023-09-15 13:22:44 -06:00
// if we are wrapping text, the width must be the user-set width
if setter.wrap {
horizontalExtent = fixed.I(setter.width)
horizontalExtentSpace = fixed.I(setter.width)
}
// calculate layout boundaries
setter.minWidth = horizontalExtentSpace
2023-09-14 18:28:32 -06:00
setter.layoutBounds.Max.X = horizontalExtent.Round()
setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round()
2023-07-06 23:49:32 -06:00
y -= metrics.Height
setter.layoutBounds.Min.Y = -metrics.Ascent.Round()
setter.layoutBounds.Max.Y =
y.Round() +
metrics.Descent.Round()
2023-07-06 23:49:32 -06:00
setter.layoutBoundsSpace.Min.Y = setter.layoutBounds.Min.Y
setter.layoutBoundsSpace.Max.Y = setter.layoutBounds.Max.Y
}
func (setter *TypeSetter) needAlignedLayout () {
if setter.alignClean && setter.layoutClean { return }
setter.needLayout()
setter.alignClean = true
setter.alignHorizontally()
setter.alignVertically()
}
func (setter *TypeSetter) alignHorizontally () {
if len(setter.lines) == 0 { return }
2023-07-06 23:49:32 -06:00
for index := range setter.lines {
2023-09-15 13:22:44 -06:00
align := setter.hAlign
// if the horizontal align is even, align lines with breaks
// after them to the left anyways
if align == AlignEven {
except :=
index == len(setter.lines) - 1 ||
setter.lines[index].BreakAfter
2023-09-15 13:22:44 -06:00
if except { align = AlignStart }
2023-07-06 23:49:32 -06:00
}
2023-09-15 13:22:44 -06:00
// align line
2023-07-06 23:49:32 -06:00
setter.lines[index].Align(align)
}
}
func (setter *TypeSetter) alignVertically () {
if setter.height == 0 { return }
if len(setter.lines) == 0 { return }
if setter.vAlign == AlignEven {
setter.justifyVertically()
return
}
// determine how much to shift lines
topOffset := 0
contentHeight := setter.layoutBoundsSpace.Dy()
if setter.vAlign == AlignMiddle {
topOffset += (setter.height - contentHeight) / 2
} else if setter.vAlign == AlignEnd {
topOffset += setter.height - contentHeight
}
// shift lines
for index := range setter.lines {
setter.lines[index].Y += fixed.I(topOffset)
}
}
func (setter *TypeSetter) justifyVertically () {
spaceCount := len(setter.lines) - 1
contentHeight := setter.layoutBoundsSpace.Dy()
spacePerLine :=
fixed.Int26_6(setter.height - contentHeight) /
fixed.Int26_6(spaceCount)
y := fixed.Int26_6(0)
for index := range setter.lines {
setter.lines[index].Y = y
y += spacePerLine + setter.LineHeight()
}
}
// SetWrap sets whether or not the text wraps around and forms new lines.
func (setter *TypeSetter) SetWrap (wrap bool) {
if setter.wrap == wrap { return }
setter.layoutClean = false
setter.wrap = wrap
}
2023-07-06 23:49:32 -06:00
// SetAlign sets the alignment method of the typesetter.
func (setter *TypeSetter) SetAlign (horizontal, vertical Align) {
2023-09-15 13:22:44 -06:00
if setter.hAlign == horizontal && setter.vAlign == vertical { return }
2023-07-06 23:49:32 -06:00
setter.alignClean = false
2023-09-15 13:22:44 -06:00
setter.hAlign = horizontal
setter.vAlign = vertical
2023-07-06 23:49:32 -06:00
}
// SetText sets the text content of the typesetter.
func (setter *TypeSetter) SetText (text []rune) {
setter.layoutClean = false
setter.alignClean = false
setter.text = text
}
// SetFace sets the font face of the typesetter.
func (setter *TypeSetter) SetFace (face font.Face) {
if setter.face == face { return }
setter.layoutClean = false
setter.alignClean = false
setter.face = face
}
2023-09-14 18:28:32 -06:00
// SetWidth sets the width of the typesetter. Text will still be able
// to overflow outside of this width if wrapping is disabled.
func (setter *TypeSetter) SetWidth (width int) {
2023-09-15 13:22:44 -06:00
if setter.width == width { return }
2023-07-06 23:49:32 -06:00
setter.layoutClean = false
setter.alignClean = false
2023-09-15 13:22:44 -06:00
setter.width = width
2023-07-06 23:49:32 -06:00
}
2023-09-14 18:28:32 -06:00
// SetHeight sets the height of the typesetter. If the height is greater than
// zero, no lines will be laid out past it. If the height is zero, the text's
// maximum height will not be constrained.
func (setter *TypeSetter) SetHeight (heignt int) {
2023-09-15 13:22:44 -06:00
if setter.height == heignt { return }
2023-07-06 23:49:32 -06:00
setter.layoutClean = false
setter.alignClean = false
2023-09-15 13:22:44 -06:00
setter.height = heignt
2023-07-06 23:49:32 -06:00
}
// Em returns the width of one emspace according to the typesetter's font, which
// is the width of the capital letter 'M'.
func (setter *TypeSetter) Em () (width fixed.Int26_6) {
if setter.face == nil { return 0 }
width, _ = setter.face.GlyphAdvance('M')
return
}
// LineHeight returns the height of one line according to the typesetter's font.
func (setter *TypeSetter) LineHeight () fixed.Int26_6 {
if setter.face == nil { return 0 }
return setter.face.Metrics().Height
}
2023-09-14 18:28:32 -06:00
// Width returns the height of the typesetter as set by SetWidth.
func (setter *TypeSetter) Width () int {
2023-09-15 13:22:44 -06:00
return setter.width
2023-07-06 23:49:32 -06:00
}
2023-09-14 18:28:32 -06:00
// Height returns the height of the typesetter as set by SetHeight.
func (setter *TypeSetter) Height () int {
2023-09-15 13:22:44 -06:00
return setter.height
2023-07-06 23:49:32 -06:00
}
// Face returns the TypeSetter's font face as set by SetFace.
func (setter *TypeSetter) Face () font.Face {
return setter.face
}
// Length returns the amount of runes in the typesetter.
func (setter *TypeSetter) Length () int {
return len(setter.text)
}
// RuneIterator is a function that can iterate accross a typesetter's runes.
type RuneIterator func (
index int,
char rune,
position fixed.Point26_6,
) (
keepGoing bool,
)
// For calls the specified iterator for every rune in the typesetter. If the
// iterator returns false, the loop will immediately stop.
func (setter *TypeSetter) For (iterator RuneIterator) {
setter.needAlignedLayout()
index := 0
lastLineY := fixed.Int26_6(0)
lastCharRightBound := fixed.Int26_6(0)
for _, line := range setter.lines {
lastLineY = line.Y
for _, word := range line.Words {
for _, char := range word.Runes {
lastCharRightBound = word.X + char.X + char.Width
keepGoing := iterator (index, char.Rune, fixed.Point26_6 {
X: word.X + char.X,
Y: line.Y,
})
if !keepGoing { return }
index ++
}}
if line.BreakAfter {
keepGoing := iterator (index, '\n', fixed.Point26_6 {
X: lastCharRightBound,
Y: line.Y,
})
if !keepGoing { return }
index ++
}
}
keepGoing := iterator (index, '\000', fixed.Point26_6 {
X: lastCharRightBound,
Y: lastLineY,
})
if !keepGoing { return }
index ++
}
// AtPosition returns the index of the rune at the specified position.
func (setter *TypeSetter) AtPosition (position fixed.Point26_6) (index int) {
setter.needAlignedLayout()
if setter.lines == nil { return }
if setter.face == nil { return }
// find the first line who's bottom bound is greater than position.Y. if
// we haven't found it, then dont set the line variable (defaults to the
// last line)
2023-08-06 01:37:08 -06:00
metrics := setter.face.Metrics()
lastLine := setter.lines[len(setter.lines) - 1]
for _, curLine := range setter.lines {
2023-07-06 23:49:32 -06:00
if curLine.Y + metrics.Descent > position.Y {
2023-08-06 01:37:08 -06:00
lastLine = curLine
2023-07-06 23:49:32 -06:00
break
}
2023-08-06 01:37:08 -06:00
index += curLine.Length()
2023-07-06 23:49:32 -06:00
}
2023-08-06 01:37:08 -06:00
if lastLine.Words == nil { return }
2023-07-06 23:49:32 -06:00
// find the first rune who's right bound is greater than position.X.
2023-08-06 01:37:08 -06:00
for _, curWord := range lastLine.Words {
2023-07-06 23:49:32 -06:00
for _, curChar := range curWord.Runes {
x := curWord.X + curChar.X + curChar.Width
if x > position.X { goto foundRune }
index ++
}
}
foundRune:
return
}
// PositionAt returns the position of the rune at the specified index.
func (setter *TypeSetter) PositionAt (index int) (position fixed.Point26_6) {
setter.needAlignedLayout()
setter.For (func (i int, r rune, p fixed.Point26_6) bool {
position = p
return i < index
})
return
}
// LayoutBounds returns the semantic bounding box of the text. The origin point
// (0, 0) of the rectangle corresponds to the origin of the first line's
// baseline.
func (setter *TypeSetter) LayoutBounds () (image.Rectangle) {
setter.needLayout()
return setter.layoutBounds
}
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
// trailing whitespace at the end of each line (if it exists).
func (setter *TypeSetter) LayoutBoundsSpace () (image.Rectangle) {
setter.needLayout()
return setter.layoutBoundsSpace
}
2023-07-15 17:52:06 -06:00
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return (Em(), 0)
func (setter *TypeSetter) MinimumSize () image.Point {
setter.needLayout()
if setter.wrap {
2023-07-15 17:52:06 -06:00
return image.Pt(setter.Em().Round(), 0)
}
2023-07-15 18:03:19 -06:00
width := setter.minWidth
height := fixed.Int26_6(len(setter.lines)) * setter.LineHeight()
return image.Pt(width.Round(), height.Round())
2023-07-15 17:52:06 -06:00
}
2023-07-06 23:49:32 -06:00
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not alter the
// typesetter's state.
func (setter *TypeSetter) ReccomendedHeightFor (width int) (height int) {
setter.needLayout()
if setter.lines == nil { return }
if setter.face == nil { return }
metrics := setter.face.Metrics()
dot := fixed.Point26_6 { X: 0, Y: metrics.Height }
firstWord := true
for _, line := range setter.lines {
for _, word := range line.Words {
if word.Width + dot.X > fixed.I(width) && !firstWord {
dot.Y += metrics.Height
dot.X = 0
firstWord = true
}
dot.X += word.Width + word.SpaceAfter
firstWord = false
}
if line.BreakAfter {
dot.Y += metrics.Height
dot.X = 0
firstWord = true
}
}
return dot.Y.Round()
}