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 }