typeset/setter.go

228 lines
6.2 KiB
Go

package typeset
import "fmt"
import "strconv"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
type validationLevel int; const (
validationLevelNone validationLevel = iota
validationLevelTokens
validationLevelMeasurement
validationLevelFlow
)
type tokenKind int; const (
tokenKindWord tokenKind = iota // contains everything that isn't:
tokenKindSpace // only unicode space runes, except \r or \n
tokenKindTab // only \t runes
tokenKindLineBreak // either "\n", or "\r\n"
)
func (kind tokenKind) String () string {
switch kind {
case tokenKindWord: return "Word"
case tokenKindSpace: return "Space"
case tokenKindTab: return "Tab"
case tokenKindLineBreak: return "LineBreak"
}
return fmt.Sprintf("typeset.tokenKind(%d)", kind)
}
type token struct {
kind tokenKind
width fixed.Int26_6
position fixed.Point26_6
runes []runeLayout
}
func (tok token) String () string {
str := ""
for _, runl := range tok.runes {
str += string(runl.run)
}
return fmt.Sprintf (
"%v:%v{%v,%v-%v}",
tok.kind, strconv.Quote(str),
tok.position.X, tok.position.Y, tok.width)
}
type runeLayout struct {
x fixed.Int26_6
run rune
}
func (run runeLayout) String () string {
return fmt.Sprintf("%s-{%v}", strconv.Quote(string([]rune { run.run })), run.x)
}
// RuneIter is an iterator that iterates over positioned runes.
type RuneIter func (yield func(fixed.Point26_6, rune) bool)
// Align specifies a text alignment method.
type Align int; const (
// X | Y
AlignStart Align = iota // left | top
AlignMiddle // center | center
AlignEnd // right | bottom
AlignEven // justified | (unsupported)
)
// TypeSetter manages text, and can perform layout operations on it. It
// automatically avoids performing redundant work. It has no constructor and its
// zero value can be used safely, but it must not be copied after first use.
type TypeSetter struct {
text string
runes []runeLayout
tokens []token
validationLevel validationLevel
xAlign, yAlign Align
face font.Face
size fixed.Point26_6 // width, height
wrap bool
minimumSize fixed.Point26_6
layoutBounds fixed.Rectangle26_6
layoutBoundsSpace fixed.Rectangle26_6
}
// Runes returns an iterator for all runes in the TypeSetter, and thier positions.
func (this *TypeSetter) Runes () RuneIter {
this.needFlow()
return func (yield func (fixed.Point26_6, rune) bool) {
for _, token := range this.tokens {
for _, runl := range token.runes {
pos := token.position
pos.X += runl.x
if !yield(pos, runl.run) { return }
}
}
}
}
// Em returns the width of one emspace according to the typesetter's typeface,
// which is the width of the capital letter 'M'.
func (this *TypeSetter) Em () fixed.Int26_6 {
if this.face == nil { return 0 }
width, _ := this.face.GlyphAdvance('M')
return width
}
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return { X: Em(), Y: 0 }.
func (this *TypeSetter) MinimumSize () fixed.Point26_6 {
if this.wrap { return fixed.Point26_6{ X: this.Em(), Y: 0 } }
this.needFlow()
return this.minimumSize
}
// 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 (this *TypeSetter) LayoutBounds () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBounds
}
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
// trailing whitespace at the end of each line (if it exists).
func (this *TypeSetter) LayoutBoundsSpace () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBoundsSpace
}
// PositionAt returns the position of the rune at the specified index.
func (this *TypeSetter) PositionAt (index int) fixed.Point26_6 {
idx := 0
var position fixed.Point26_6
this.Runes()(func (pos fixed.Point26_6, run rune) bool {
if index == idx {
position = pos
return false
}
idx ++
return true
})
return position
}
// SetText sets the text of the TypeSetter.
func (this *TypeSetter) SetText (text string) {
if this.text == text { return }
this.text = text
this.invalidate(validationLevelTokens)
}
// SetSize sets the width and height of the TypeSetter.
func (this *TypeSetter) SetSize (size fixed.Point26_6) {
if this.size == size { return }
this.size = size
this.invalidate(validationLevelFlow)
}
// SetWrap sets whether the text will wrap to the width specified by SetSize.
func (this *TypeSetter) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.wrap = wrap
this.invalidate(validationLevelFlow)
}
// SetAlign sets the horizontal and vertical alignment of the text.
func (this *TypeSetter) SetAlign (x, y Align) {
if this.xAlign == x && this.yAlign == y { return }
this.xAlign = x
this.yAlign = y
this.invalidate(validationLevelFlow)
}
// Face returns the font face as set by SetFace.
func (this *TypeSetter) Face () font.Face {
return this.face
}
// SetFace sets the font face the text will be laid out according to.
func (this *TypeSetter) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.invalidate(validationLevelMeasurement)
}
func (this *TypeSetter) needTokens () {
if this.valid(validationLevelTokens) { return }
this.runes, this.tokens = parseString(this.text)
this.validate(validationLevelTokens)
}
func (this *TypeSetter) needMeasurement () {
if this.valid(validationLevelMeasurement) { return }
this.needTokens()
measure(this.tokens, this.face)
this.validate(validationLevelMeasurement)
}
func (this *TypeSetter) needFlow () {
if this.valid(validationLevelFlow) { return }
this.needMeasurement()
this.layoutBounds, this.layoutBoundsSpace, this.minimumSize = reflow (
this.tokens,
this.face, this.size,
this.wrap, this.xAlign, this.yAlign)
this.validate(validationLevelFlow)
}
func (this *TypeSetter) validate (level validationLevel) {
this.validationLevel = level
}
func (this *TypeSetter) invalidate (level validationLevel) {
if this.valid(level) {
this.validationLevel = level - 1
}
}
func (this *TypeSetter) valid (level validationLevel) bool {
return this.validationLevel >= level
}