diff --git a/drawer.go b/drawer.go deleted file mode 100644 index 044fb1e..0000000 --- a/drawer.go +++ /dev/null @@ -1,84 +0,0 @@ -package typeset - -import "image" -import "unicode" -import "image/draw" -import "image/color" -import "golang.org/x/image/math/fixed" - -// Drawer is an extended TypeSetter that is able to draw text. Much like -// TypeSetter, It has no constructor and its zero value can be used safely. -type Drawer struct { TypeSetter } - -// Draw draws the drawer's text onto the specified canvas at the given offset. -func (drawer Drawer) Draw ( - destination draw.Image, - col color.Color, - offset image.Point, -) ( - updatedRegion image.Rectangle, -) { - source := image.NewUniform(col) - - drawer.ForRunes (func ( - index int, - char rune, - position fixed.Point26_6, - ) bool { - // leave empty space for space characters - if unicode.IsSpace(char) { - return true - } - - dot := fixed.P ( - offset.X + position.X.Round(), - offset.Y + position.Y.Round()) - destinationRectangle, - mask, maskPoint, _, ok := drawer.face.Glyph(dot, char) - // tofu - if !ok { - drawer.drawTofu(char, destination, col, dot) - return true - } - - // FIXME:? clip destination rectangle if we are on the cusp of - // the maximum height. - - draw.DrawMask ( - destination, - destinationRectangle, - source, image.Point { }, - mask, maskPoint, - draw.Over) - - updatedRegion = updatedRegion.Union(destinationRectangle) - return true - }) - return -} - -func (drawer Drawer) drawTofu ( - char rune, - destination draw.Image, - col color.Color, - position fixed.Point26_6, -) { - bounds, _ := tofuBounds(drawer.face) - rectBounds := image.Rect ( - bounds.Min.X.Round(), - bounds.Min.Y.Round(), - bounds.Max.X.Round(), - bounds.Max.Y.Round()).Add(image.Pt( - position.X.Round(), - position.Y.Round())) - for x := rectBounds.Min.X; x < rectBounds.Max.X; x ++ { - destination.Set(x, rectBounds.Min.Y, col) - } - for y := rectBounds.Min.Y; y < rectBounds.Max.Y; y ++ { - destination.Set(rectBounds.Min.X, y, col) - destination.Set(rectBounds.Max.X - 1, y, col) - } - for x := rectBounds.Min.X; x < rectBounds.Max.X; x ++ { - destination.Set(x, rectBounds.Max.Y - 1, col) - } -} diff --git a/layout.go b/layout.go deleted file mode 100644 index 1875602..0000000 --- a/layout.go +++ /dev/null @@ -1,245 +0,0 @@ -package typeset - -import "unicode" -import "golang.org/x/image/font" -import "golang.org/x/image/math/fixed" - -// 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 | evenly spaced -) - -// RuneLayout contains layout information for a single rune relative to its -// word. -type RuneLayout struct { - X fixed.Int26_6 - Width fixed.Int26_6 - Rune rune -} - -// WordLayout contains layout information for a single word relative to its -// line. -type WordLayout struct { - X fixed.Int26_6 - Width fixed.Int26_6 - SpaceAfter fixed.Int26_6 - Runes []RuneLayout -} - -// DoWord consumes exactly one word from the given string, and produces a word -// layout according to the given font. It returns the remaining text as well. -func DoWord (text []rune, face font.Face) (word WordLayout, remaining []rune) { - remaining = text - gettingSpace := false - x := fixed.Int26_6(0) - lastRune := rune(-1) - for _, char := range text { - // if we run into a line break, we must break out immediately - // because it is not DoWord's job to handle that. - if char == '\n' { break } - - // if we suddenly run into spaces, and then run into a word - // again, we must break out immediately. - if unicode.IsSpace(char) { - gettingSpace = true - } else if gettingSpace { - break - } - - // apply kerning - if lastRune >= 0 { x += face.Kern(lastRune, char) } - lastRune = char - - // consume and process the rune - remaining = remaining[1:] - advance, ok := face.GlyphAdvance(char) - if !ok { - advance = tofuAdvance(face) - } - runeLayout := RuneLayout { - X: x, - Width: advance, - Rune: char, - } - word.Runes = append(word.Runes, runeLayout) - - // advance - if gettingSpace { - word.SpaceAfter += advance - } else { - word.Width += advance - } - x += advance - } - return -} - -// LastRune returns the last rune in the word. -func (word WordLayout) LastRune () rune { - if word.Runes == nil { - return -1 - } else { - return word.Runes[len(word.Runes) - 1].Rune - } -} - -// FirstRune returns the last rune in the word. -func (word WordLayout) FirstRune () rune { - if word.Runes == nil { - return -1 - } else { - return word.Runes[0].Rune - } -} - -// LineLayout contains layout information for a single line. -type LineLayout struct { - Y fixed.Int26_6 - Width fixed.Int26_6 - ContentWidth fixed.Int26_6 - SpaceAfter fixed.Int26_6 - Words []WordLayout - BreakAfter bool -} - -// DoLine consumes exactly one line from the given string, and produces a line -// layout according to the given font. It returns the remaining text as well. If -// wrap is set to true, this function will stop processing words once maxWidth -// is crossed. The word which would have crossed over the limit will not be -// processed. -func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line LineLayout, remaining []rune) { - remaining = text - x := fixed.Int26_6(0) - isFirstWord := true - for { - // process one word - word, remainingFromWord := DoWord(remaining, face) - x += word.Width - - // if we have gone over the preferred width, stop processing - // words (if wrap is enabled) - if !isFirstWord && wrap && x > width { - break - } - - x += word.SpaceAfter - remaining = remainingFromWord - - // if the word actually has contents, add it - if word.Runes != nil { - line.Words = append(line.Words, word) - } - - // if we have hit the end of the line, stop processing words - if len(remaining) == 0 { break } - if remaining[0] == '\n' { - line.BreakAfter = true - remaining = remaining[1:] - break - } - - isFirstWord = false - } - - // set the width of the line's content. - line.Width = width - if len(line.Words) > 0 { - lastWord := line.Words[len(line.Words) - 1] - line.ContentWidth = x - lastWord.SpaceAfter - line.SpaceAfter = lastWord.SpaceAfter - } - return -} - -// Length returns the amount of runes within the line, including the trailing -// line break if it exists. -func (line *LineLayout) Length () int { - lineSize := 0 - for _, word := range line.Words { - lineSize += len(word.Runes) - } - if line.BreakAfter { lineSize ++ } - return lineSize -} - -// Align aligns the text in the line according to the specified alignment -// method. -func (line *LineLayout) Align (align Align, tabWidth fixed.Int26_6) { - if len(line.Words) == 0 { return } - - if align == AlignEven { - line.justify(tabWidth) - } else { - line.contract(tabWidth) - - var leftOffset fixed.Int26_6 - if align == AlignMiddle { - leftOffset = (line.Width - line.ContentWidth) / 2 - } else if align == AlignEnd { - leftOffset = line.Width - line.ContentWidth - } - - for index := range line.Words { - line.Words[index].X += leftOffset - } - } -} - -// assume line has content > 0 -func (line *LineLayout) contract (tabWidth fixed.Int26_6) { - x := fixed.Int26_6(0) - for index, word := range line.Words { - word.X = x - x += word.Width - x += word.SpaceAfter - line.Words[index] = word - } - lastWord := line.Words[len(line.Words) - 1] - line.ContentWidth = lastWord.X + lastWord.Width - line.SpaceAfter = lastWord.SpaceAfter -} - -// assume line has content > 0 -func (line *LineLayout) justify (tabWidth fixed.Int26_6) { - if len(line.Words) <= 1 { - line.Align(AlignStart, tabWidth) - return - } - - // We are going to be moving the words, so we can't take SpaceAfter into - // account. - trueContentWidth := fixed.Int26_6(0) - for _, word := range line.Words { - trueContentWidth += word.Width - } - - spaceCount := len(line.Words) - 1 - spacePerWord := (line.Width - trueContentWidth) / fixed.Int26_6(spaceCount) - x := fixed.Int26_6(0) - for index, word := range line.Words { - line.Words[index].X = x - x += spacePerWord + word.Width - } -} - -func tofuAdvance (face font.Face) fixed.Int26_6 { - if advance, ok := face.GlyphAdvance('M'); ok { - return advance - } else { - return 16 - } -} - -func tofuBounds (face font.Face) (fixed.Rectangle26_6, fixed.Int26_6) { - if bounds, advance, ok := face.GlyphBounds('M'); ok { - return bounds, advance - } else { - return fixed.R(0, -16, 14, 0), 16 - } -} diff --git a/setter.go b/setter.go index 6176a4f..a8f360a 100644 --- a/setter.go +++ b/setter.go @@ -1,428 +1,227 @@ package typeset -import "image" +import "fmt" +import "strconv" 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 - tabWidth fixed.Int26_6 - - 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() -} - -// should only be called from within setter.needAlignedLayout -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, setter.tabWidth) - } -} - -// should only be called from within setter.needAlignedLayout -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 := fixed.I(0) - contentHeight := setter.layoutBoundsSpace.Dy() - if setter.vAlign == AlignMiddle { - topOffset += fixed.I((setter.height - contentHeight) / 2) - } else if setter.vAlign == AlignEnd { - topOffset += fixed.I(setter.height - contentHeight) - } - - // we may be re-aligning already aligned text. if the text is shifted - // away from the origin, account for that. - if len(setter.lines) > 0 { - topOffset -= setter.lines[0].Y - } - - // shift lines - for index := range setter.lines { - setter.lines[index].Y += topOffset - } -} - -// should only be called from within setter.alignVertically -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 -} - -// SetTabWidth sets the distance between tab stops. -func (setter *TypeSetter) SetTabWidth (tabWidth fixed.Int26_6) { - if setter.tabWidth == tabWidth { return } - setter.layoutClean = false - setter.alignClean = false - setter.tabWidth = tabWidth -} - -// 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, +type validationLevel int; const ( + validationLevelNone validationLevel = iota + validationLevelTokens + validationLevelMeasurement + validationLevelFlow ) -// For calls the specified iterator for every rune in the typesetter. If the -// iterator returns false, the loop will immediately stop. This method will -// insert a fake null rune at the end. -func (setter *TypeSetter) For (iterator RuneIterator) { - setter.forInternal(iterator, true) +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) } -// ForRunes is like For, but leaves out the fake null rune. -func (setter *TypeSetter) ForRunes (iterator RuneIterator) { - setter.forInternal(iterator, false) +type token struct { + kind tokenKind + width fixed.Int26_6 + position fixed.Point26_6 + runes []runeLayout } -func (setter *TypeSetter) forInternal (iterator RuneIterator, fakeNull bool) { - setter.needAlignedLayout() +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) +} - 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 ++ - lastCharRightBound = fixed.Int26_6(0) +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 } + } } } - - if fakeNull { - 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 +// 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 } -// 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 +// 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 (setter *TypeSetter) LayoutBounds () (image.Rectangle) { - setter.needLayout() - return setter.layoutBounds - +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 (setter *TypeSetter) LayoutBoundsSpace () (image.Rectangle) { - setter.needLayout() - return setter.layoutBoundsSpace +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 (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()) -} - -// RecommendedHeight 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) RecommendedHeight (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 +// 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 } - if line.BreakAfter { - dot.Y += metrics.Height - dot.X = 0 - firstWord = true - } - } - - return dot.Y.Round() + 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 }