diff --git a/typesetter.go b/typesetter.go new file mode 100644 index 0000000..a8f360a --- /dev/null +++ b/typesetter.go @@ -0,0 +1,227 @@ +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 +}