package typeset import "image" 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 align Align face font.Face maxWidth int maxHeight int wrap bool 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) 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.maxWidth)) 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 { }) } // calculate layout boundaries setter.minWidth = horizontalExtentSpace setter.layoutBounds.Max.X = horizontalExtent.Round() setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round() y -= metrics.Height if setter.maxHeight == 0 { setter.layoutBounds.Min.Y = -metrics.Ascent.Round() setter.layoutBounds.Max.Y = y.Round() + metrics.Descent.Round() } else { setter.layoutBounds.Min.Y = -metrics.Ascent.Round() setter.layoutBounds.Max.Y = setter.maxHeight - metrics.Ascent.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 for index := range setter.lines { align := setter.align if align == AlignJustify { except := index == len(setter.lines) - 1 || setter.lines[index].BreakAfter if except { align = AlignLeft } } setter.lines[index].Align(align) } } // 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 (align Align) { if setter.align == align { return } setter.alignClean = false setter.align = align } // 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 } // TODO rename to SetWidth and SetHeight // 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) SetMaxWidth (width int) { if setter.maxWidth == width { return } setter.layoutClean = false setter.alignClean = false setter.maxWidth = 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) SetMaxHeight (heignt int) { if setter.maxHeight == heignt { return } setter.layoutClean = false setter.alignClean = false setter.maxHeight = heignt } // 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.maxWidth } // Height returns the height of the typesetter as set by SetHeight. func (setter *TypeSetter) Height () int { return setter.maxHeight } // 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, ) // For calls the specified iterator for every rune in the typesetter. If the // iterator returns false, the loop will immediately stop. func (setter *TypeSetter) For (iterator RuneIterator) { setter.needAlignedLayout() 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 ++ } } 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 } // 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 } // 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 } // 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 } // 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()) } // ReccomendedHeightFor 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) ReccomendedHeightFor (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 } if line.BreakAfter { dot.Y += metrics.Height dot.X = 0 firstWord = true } } return dot.Y.Round() }