287 lines
7.9 KiB
Go
287 lines
7.9 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)
|
|
}
|
|
|
|
// TODO: perhaps rename this to just "glyph"
|
|
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
|
|
}
|
|
|
|
// AtPosition returns the index of the rune at the specified position. The
|
|
// returned index may be greater than the length of runes in the TypeSetter.
|
|
func (this *TypeSetter) AtPosition (position fixed.Point26_6) int {
|
|
metrics := this.face.Metrics()
|
|
lastValidIndex := 0
|
|
index := 0
|
|
for _, tok := range this.tokens {
|
|
pos := tok.position
|
|
yValid :=
|
|
position.Y >= pos.Y - metrics.Ascent &&
|
|
position.Y <= pos.Y + metrics.Descent
|
|
if !yValid { index += len(tok.runes); continue }
|
|
|
|
for _, runl := range tok.runes {
|
|
x := pos.X + runl.x
|
|
xValid := position.X >= pos.X + runl.x
|
|
if xValid {
|
|
lastValidIndex = index
|
|
} else if x > position.X {
|
|
return lastValidIndex
|
|
}
|
|
|
|
index ++
|
|
}
|
|
}
|
|
index ++
|
|
return lastValidIndex
|
|
}
|
|
|
|
// Runes returns an iterator for all runes in the TypeSetter, and their positions.
|
|
func (this *TypeSetter) Runes () RuneIter {
|
|
this.needFlow()
|
|
return func (yield func (fixed.Point26_6, rune) bool) {
|
|
for _, tok := range this.tokens {
|
|
for _, runl := range tok.runes {
|
|
pos := tok.position
|
|
pos.X += runl.x
|
|
if !yield(pos, runl.run) { return }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RunesWithNull returns an iterator for all runes in the TypeSetter, plus an
|
|
// additional null rune at the end. This is useful for calculating the positions
|
|
// of things.
|
|
func (this *TypeSetter) RunesWithNull () RuneIter {
|
|
this.needFlow()
|
|
return func (yield func (fixed.Point26_6, rune) bool) {
|
|
var tok token
|
|
for _, tok = range this.tokens {
|
|
for _, runl := range tok.runes {
|
|
pos := tok.position
|
|
pos.X += runl.x
|
|
if !yield(pos, runl.run) { return }
|
|
}
|
|
}
|
|
|
|
pos := tok.position
|
|
pos.X += tok.width
|
|
yield(pos, 0)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Face returns the font face as set by SetFace.
|
|
func (this *TypeSetter) Face () font.Face {
|
|
return this.face
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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.RunesWithNull()(func (pos fixed.Point26_6, run rune) bool {
|
|
if index == idx {
|
|
position = pos
|
|
return false
|
|
}
|
|
idx ++
|
|
return true
|
|
})
|
|
return position
|
|
}
|
|
|
|
// ReccomendedHeightFor returns the reccomended max height if the text were to
|
|
// have its maximum width set to the given width. This does not actually move
|
|
// any text, it only simulates it.
|
|
func (this *TypeSetter) RecommendedHeight (width fixed.Int26_6) fixed.Int26_6 {
|
|
this.needMeasurement()
|
|
return recommendHeight(this.tokens, this.face, width)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// SetText sets the text of the TypeSetter.
|
|
func (this *TypeSetter) SetText (text string) {
|
|
if this.text == text { return }
|
|
this.text = text
|
|
this.invalidate(validationLevelTokens)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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
|
|
}
|