typeset/setter.go

407 lines
11 KiB
Go

package typeset
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
hAlign, vAlign Align
face font.Face
width, height int
wrap bool
minWidth fixed.Int26_6
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 { }
setter.minWidth = 0
if setter.face == nil { return }
horizontalExtent := fixed.Int26_6(0)
horizontalExtentSpace := fixed.Int26_6(0)
metrics := setter.face.Metrics()
remaining := setter.text
y := fixed.Int26_6(0)
// function to add line and update bounds statistics
addLine := func (line LineLayout) {
line.Y = y
y += metrics.Height
if line.ContentWidth > horizontalExtent {
horizontalExtent = line.ContentWidth
}
lineWidthSpace := line.ContentWidth + line.SpaceAfter
if lineWidthSpace > horizontalExtentSpace {
horizontalExtentSpace = lineWidthSpace
}
setter.lines = append(setter.lines, line)
}
// process every line until there are no more remaining runes
for len(remaining) > 0 {
line, remainingFromLine := DoLine (
remaining, setter.face, setter.wrap,
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 { }) }
// 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
setter.layoutBounds.Max.X = horizontalExtent.Round()
setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round()
y -= metrics.Height
setter.layoutBounds.Min.Y = -metrics.Ascent.Round()
setter.layoutBounds.Max.Y =
y.Round() +
metrics.Descent.Round()
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()
}
// should only be called from within setter.needAlignedLayout
func (setter *TypeSetter) alignHorizontally () {
if len(setter.lines) == 0 { return }
for index := range setter.lines {
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
if except { align = AlignStart }
}
// align line
setter.lines[index].Align(align)
}
}
// should only be called from within setter.needAlignedLayout
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 := fixed.I(0)
contentHeight := setter.layoutBoundsSpace.Dy()
if setter.vAlign == AlignMiddle {
topOffset += fixed.I((setter.height - contentHeight) / 2)
} else if setter.vAlign == AlignEnd {
topOffset += fixed.I(setter.height - contentHeight)
}
// we may be re-aligning already aligned text. if the text is shifted
// away from the origin, account for that.
if len(setter.lines) > 0 {
topOffset -= setter.lines[0].Y
}
// shift lines
for index := range setter.lines {
setter.lines[index].Y += topOffset
}
}
// should only be called from within setter.alignVertically
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
}
// SetAlign sets the alignment method of the typesetter.
func (setter *TypeSetter) SetAlign (horizontal, vertical Align) {
if setter.hAlign == horizontal && setter.vAlign == vertical { return }
setter.alignClean = false
setter.hAlign = horizontal
setter.vAlign = vertical
}
// 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
}
// 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) {
if setter.width == width { return }
setter.layoutClean = false
setter.alignClean = false
setter.width = width
}
// 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) {
if setter.height == heignt { return }
setter.layoutClean = false
setter.alignClean = false
setter.height = heignt
}
// 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
}
// Width returns the height of the typesetter as set by SetWidth.
func (setter *TypeSetter) Width () int {
return setter.width
}
// Height returns the height of the typesetter as set by SetHeight.
func (setter *TypeSetter) Height () int {
return setter.height
}
// 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)
metrics := setter.face.Metrics()
lastLine := setter.lines[len(setter.lines) - 1]
for _, curLine := range setter.lines {
if curLine.Y + metrics.Descent > position.Y {
lastLine = curLine
break
}
index += curLine.Length()
}
if lastLine.Words == nil { return }
// find the first rune who's right bound is greater than position.X.
for _, curWord := range lastLine.Words {
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
}
// 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 {
return image.Pt(setter.Em().Round(), 0)
}
width := setter.minWidth
height := fixed.Int26_6(len(setter.lines)) * setter.LineHeight()
return image.Pt(width.Round(), height.Round())
}
// 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()
}