398 lines
11 KiB
Go
398 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()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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()
|
|
}
|