Replace TextDrawer with more capable system
This commit is contained in:
		
							parent
							
								
									0c22977693
								
							
						
					
					
						commit
						ae551c47ea
					
				
							
								
								
									
										389
									
								
								artist/text.go
									
									
									
									
									
								
							
							
						
						
									
										389
									
								
								artist/text.go
									
									
									
									
									
								
							@ -1,389 +0,0 @@
 | 
				
			|||||||
package artist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// import "fmt"
 | 
					 | 
				
			||||||
import "image"
 | 
					 | 
				
			||||||
import "unicode"
 | 
					 | 
				
			||||||
import "image/draw"
 | 
					 | 
				
			||||||
import "golang.org/x/image/font"
 | 
					 | 
				
			||||||
import "golang.org/x/image/math/fixed"
 | 
					 | 
				
			||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type characterLayout struct {
 | 
					 | 
				
			||||||
	x         int
 | 
					 | 
				
			||||||
	width     int
 | 
					 | 
				
			||||||
	character rune
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type wordLayout struct {
 | 
					 | 
				
			||||||
	position    image.Point
 | 
					 | 
				
			||||||
	width       int
 | 
					 | 
				
			||||||
	spaceAfter  int
 | 
					 | 
				
			||||||
	breaksAfter int
 | 
					 | 
				
			||||||
	text        []characterLayout
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Align specifies a text alignment method.
 | 
					 | 
				
			||||||
type Align int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	// AlignLeft aligns the start of each line to the beginning point
 | 
					 | 
				
			||||||
	// of each dot.
 | 
					 | 
				
			||||||
	AlignLeft Align = iota
 | 
					 | 
				
			||||||
	AlignRight
 | 
					 | 
				
			||||||
	AlignCenter
 | 
					 | 
				
			||||||
	AlignJustify
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TextDrawer is a struct that is capable of efficient rendering of wrapped
 | 
					 | 
				
			||||||
// text, and calculating text bounds. It avoids doing redundant work
 | 
					 | 
				
			||||||
// automatically.
 | 
					 | 
				
			||||||
type TextDrawer struct {
 | 
					 | 
				
			||||||
	runes   []rune
 | 
					 | 
				
			||||||
	face    font.Face
 | 
					 | 
				
			||||||
	width   int
 | 
					 | 
				
			||||||
	height  int
 | 
					 | 
				
			||||||
	align   Align
 | 
					 | 
				
			||||||
	wrap    bool
 | 
					 | 
				
			||||||
	cut     bool
 | 
					 | 
				
			||||||
	metrics font.Metrics
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	layout       []wordLayout
 | 
					 | 
				
			||||||
	layoutClean  bool
 | 
					 | 
				
			||||||
	layoutBounds image.Rectangle
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetText sets the text of the text drawer.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) SetText (runes []rune) {
 | 
					 | 
				
			||||||
	// if drawer.runes == runes { return }
 | 
					 | 
				
			||||||
	drawer.runes = runes
 | 
					 | 
				
			||||||
	drawer.layoutClean = false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetFace sets the font face of the text drawer.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) SetFace (face font.Face) {
 | 
					 | 
				
			||||||
	if drawer.face == face { return }
 | 
					 | 
				
			||||||
	drawer.face = face
 | 
					 | 
				
			||||||
	drawer.layoutClean = false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
 | 
					 | 
				
			||||||
// layout if needed. If zero is given, there will be no width limit and the text
 | 
					 | 
				
			||||||
// will not wrap.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) SetMaxWidth (width int) {
 | 
					 | 
				
			||||||
	if drawer.width == width { return }
 | 
					 | 
				
			||||||
	drawer.width = width
 | 
					 | 
				
			||||||
	drawer.wrap = width != 0
 | 
					 | 
				
			||||||
	drawer.layoutClean = false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
 | 
					 | 
				
			||||||
// entirely below this height will not be drawn, and lines that are on the cusp
 | 
					 | 
				
			||||||
// of this maximum height will be clipped at the point that they cross it.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) SetMaxHeight (height int) {
 | 
					 | 
				
			||||||
	if drawer.height == height { return }
 | 
					 | 
				
			||||||
	drawer.height = height
 | 
					 | 
				
			||||||
	drawer.cut = height != 0
 | 
					 | 
				
			||||||
	drawer.layoutClean = false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetAlignment specifies how the drawer should align its text. For this to have
 | 
					 | 
				
			||||||
// an effect, a maximum width must have been set.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) SetAlignment (align Align) {
 | 
					 | 
				
			||||||
	if drawer.align == align { return }
 | 
					 | 
				
			||||||
	drawer.align = align
 | 
					 | 
				
			||||||
	drawer.layoutClean = false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Draw draws the drawer's text onto the specified canvas at the given offset.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) Draw (
 | 
					 | 
				
			||||||
	destination canvas.Canvas,
 | 
					 | 
				
			||||||
	source      Pattern,
 | 
					 | 
				
			||||||
	offset      image.Point,
 | 
					 | 
				
			||||||
) (
 | 
					 | 
				
			||||||
	updatedRegion image.Rectangle,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
	wrappedSource := WrappedPattern {
 | 
					 | 
				
			||||||
		Pattern: source,
 | 
					 | 
				
			||||||
		Width:  0,
 | 
					 | 
				
			||||||
		Height: 0, // TODO: choose a better width and height
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !drawer.layoutClean { drawer.recalculate() }
 | 
					 | 
				
			||||||
	// TODO: reimplement a version of draw mask that takes in a pattern and
 | 
					 | 
				
			||||||
	// only draws to a tomo.Canvas.
 | 
					 | 
				
			||||||
	for _, word := range drawer.layout {
 | 
					 | 
				
			||||||
	for _, character := range word.text {
 | 
					 | 
				
			||||||
		destinationRectangle,
 | 
					 | 
				
			||||||
		mask, maskPoint, _, ok := drawer.face.Glyph (
 | 
					 | 
				
			||||||
			fixed.P (
 | 
					 | 
				
			||||||
				offset.X + word.position.X + character.x,
 | 
					 | 
				
			||||||
				offset.Y + word.position.Y),
 | 
					 | 
				
			||||||
			character.character)
 | 
					 | 
				
			||||||
		invalid :=
 | 
					 | 
				
			||||||
			!ok || 
 | 
					 | 
				
			||||||
			unicode.IsSpace(character.character) ||
 | 
					 | 
				
			||||||
			character.character == 0
 | 
					 | 
				
			||||||
		if invalid { continue }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// FIXME:? clip destination rectangle if we are on the cusp of
 | 
					 | 
				
			||||||
		// the maximum height.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		draw.DrawMask (
 | 
					 | 
				
			||||||
			destination,
 | 
					 | 
				
			||||||
			destinationRectangle,
 | 
					 | 
				
			||||||
			wrappedSource, image.Point { },
 | 
					 | 
				
			||||||
			mask, maskPoint,
 | 
					 | 
				
			||||||
			draw.Over)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		updatedRegion = updatedRegion.Union(destinationRectangle)
 | 
					 | 
				
			||||||
	}}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LayoutBounds returns a semantic bounding box for text to be used to determine
 | 
					 | 
				
			||||||
// an offset for drawing. If a maximum width or height has been set, those will
 | 
					 | 
				
			||||||
// be used as the width and height of the bounds respectively. The origin point
 | 
					 | 
				
			||||||
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
 | 
					 | 
				
			||||||
// of the first line. As such, the minimum of the bounds will be negative.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
 | 
					 | 
				
			||||||
	if !drawer.layoutClean { drawer.recalculate() }
 | 
					 | 
				
			||||||
	bounds = drawer.layoutBounds
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Em returns the width of an emspace.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) Em () (width fixed.Int26_6) {
 | 
					 | 
				
			||||||
	if drawer.face == nil { return }
 | 
					 | 
				
			||||||
	width, _ = drawer.face.GlyphAdvance('M')
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LineHeight returns the height of one line.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
 | 
					 | 
				
			||||||
	if drawer.face == nil { return }
 | 
					 | 
				
			||||||
	metrics := drawer.face.Metrics()
 | 
					 | 
				
			||||||
	height = metrics.Height
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 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
 | 
					 | 
				
			||||||
// drawer's state.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
 | 
					 | 
				
			||||||
	if drawer.face == nil { return }
 | 
					 | 
				
			||||||
	if !drawer.layoutClean { drawer.recalculate() }
 | 
					 | 
				
			||||||
	metrics := drawer.face.Metrics()
 | 
					 | 
				
			||||||
	dot := fixed.Point26_6 { 0, metrics.Height }
 | 
					 | 
				
			||||||
	for _, word := range drawer.layout {
 | 
					 | 
				
			||||||
		if word.width + dot.X.Round() > width {
 | 
					 | 
				
			||||||
			dot.Y += metrics.Height
 | 
					 | 
				
			||||||
			dot.X = 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		dot.X += fixed.I(word.width + word.spaceAfter)
 | 
					 | 
				
			||||||
		if word.breaksAfter > 0 {
 | 
					 | 
				
			||||||
			dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height)
 | 
					 | 
				
			||||||
			dot.X = 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return dot.Y.Round()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// PositionOf returns the position of the character at the specified index
 | 
					 | 
				
			||||||
// relative to the baseline.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) PositionOf (index int) (position image.Point) {
 | 
					 | 
				
			||||||
	if !drawer.layoutClean { drawer.recalculate() }
 | 
					 | 
				
			||||||
	index ++
 | 
					 | 
				
			||||||
	for _, word := range drawer.layout {
 | 
					 | 
				
			||||||
		position = word.position
 | 
					 | 
				
			||||||
		for _, character := range word.text {
 | 
					 | 
				
			||||||
			index --
 | 
					 | 
				
			||||||
			position.X = word.position.X + character.x
 | 
					 | 
				
			||||||
			if index < 1 { return }
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// AtPosition returns the index at the specified position relative to the
 | 
					 | 
				
			||||||
// baseline.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) AtPosition (position image.Point) (index int) {
 | 
					 | 
				
			||||||
	cursor := 0
 | 
					 | 
				
			||||||
	if !drawer.layoutClean { drawer.recalculate() }
 | 
					 | 
				
			||||||
	for _, word := range drawer.layout {
 | 
					 | 
				
			||||||
		for _, character := range word.text {
 | 
					 | 
				
			||||||
			bounds := drawer.boundsOfChar(character).Add(word.position)
 | 
					 | 
				
			||||||
			if position.In(bounds) {
 | 
					 | 
				
			||||||
				return cursor
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			cursor ++
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return -1
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) boundsOfChar (char characterLayout) (image.Rectangle) {
 | 
					 | 
				
			||||||
	return image.Rect (
 | 
					 | 
				
			||||||
		char.x, 0,
 | 
					 | 
				
			||||||
		char.x + char.width,
 | 
					 | 
				
			||||||
		drawer.metrics.Height.Ceil()).
 | 
					 | 
				
			||||||
			Sub(image.Pt(0, drawer.metrics.Descent.Round()))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Length returns the amount of runes in the drawer's text.
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) Length () (length int) {
 | 
					 | 
				
			||||||
	return len(drawer.runes)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (drawer *TextDrawer) recalculate () {
 | 
					 | 
				
			||||||
	drawer.layoutClean = true
 | 
					 | 
				
			||||||
	drawer.layout = nil
 | 
					 | 
				
			||||||
	drawer.layoutBounds = image.Rectangle { }
 | 
					 | 
				
			||||||
	if drawer.runes == nil { return }
 | 
					 | 
				
			||||||
	if drawer.face  == nil { return }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	drawer.metrics = drawer.face.Metrics()
 | 
					 | 
				
			||||||
	dot := fixed.Point26_6 { 0, 0 }
 | 
					 | 
				
			||||||
	index := 0
 | 
					 | 
				
			||||||
	horizontalExtent  := 0
 | 
					 | 
				
			||||||
	currentCharacterX := fixed.Int26_6(0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	previousCharacter := rune(-1)
 | 
					 | 
				
			||||||
	for index < len(drawer.runes) {
 | 
					 | 
				
			||||||
		word := wordLayout { }
 | 
					 | 
				
			||||||
		word.position.X = dot.X.Round()
 | 
					 | 
				
			||||||
		word.position.Y = dot.Y.Round()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// process a word
 | 
					 | 
				
			||||||
		currentCharacterX  = 0
 | 
					 | 
				
			||||||
		wordWidth         := fixed.Int26_6(0)
 | 
					 | 
				
			||||||
		for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
 | 
					 | 
				
			||||||
			character := drawer.runes[index]
 | 
					 | 
				
			||||||
			_, advance, ok := drawer.face.GlyphBounds(character)
 | 
					 | 
				
			||||||
			index ++
 | 
					 | 
				
			||||||
			if !ok { continue }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			word.text = append(word.text, characterLayout {
 | 
					 | 
				
			||||||
				x: currentCharacterX.Round(),
 | 
					 | 
				
			||||||
				character: character,
 | 
					 | 
				
			||||||
				width: advance.Ceil(),
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			dot.X             += advance
 | 
					 | 
				
			||||||
			wordWidth         += advance
 | 
					 | 
				
			||||||
			currentCharacterX += advance
 | 
					 | 
				
			||||||
			if dot.X.Round () > horizontalExtent {
 | 
					 | 
				
			||||||
				horizontalExtent = dot.X.Round()
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if previousCharacter >= 0 {
 | 
					 | 
				
			||||||
				dot.X += drawer.face.Kern (
 | 
					 | 
				
			||||||
					previousCharacter,
 | 
					 | 
				
			||||||
					character)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			previousCharacter = character
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		word.width = wordWidth.Round()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// detect if the word that was just processed goes out of
 | 
					 | 
				
			||||||
		// bounds, and if it does, wrap it
 | 
					 | 
				
			||||||
		if drawer.wrap &&
 | 
					 | 
				
			||||||
			word.width + word.position.X > drawer.width &&
 | 
					 | 
				
			||||||
			word.position.X > 0 {
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			word.position.Y += drawer.metrics.Height.Round()
 | 
					 | 
				
			||||||
			word.position.X = 0
 | 
					 | 
				
			||||||
			dot.Y += drawer.metrics.Height
 | 
					 | 
				
			||||||
			dot.X = wordWidth
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// process whitespace, going onto a new line if there is a
 | 
					 | 
				
			||||||
		// newline character
 | 
					 | 
				
			||||||
		spaceWidth := fixed.Int26_6(0)
 | 
					 | 
				
			||||||
		for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
 | 
					 | 
				
			||||||
			character := drawer.runes[index]
 | 
					 | 
				
			||||||
			_, advance, ok := drawer.face.GlyphBounds(character)
 | 
					 | 
				
			||||||
			index ++
 | 
					 | 
				
			||||||
			if !ok { continue }
 | 
					 | 
				
			||||||
			word.text = append(word.text, characterLayout {
 | 
					 | 
				
			||||||
				x: currentCharacterX.Round(),
 | 
					 | 
				
			||||||
				character: character,
 | 
					 | 
				
			||||||
				width: advance.Ceil(),
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
			spaceWidth        += advance
 | 
					 | 
				
			||||||
			currentCharacterX += advance
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if character == '\n' {
 | 
					 | 
				
			||||||
				dot.Y += drawer.metrics.Height
 | 
					 | 
				
			||||||
				dot.X = 0
 | 
					 | 
				
			||||||
				word.breaksAfter ++
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				dot.X += advance
 | 
					 | 
				
			||||||
				if previousCharacter >= 0 {
 | 
					 | 
				
			||||||
					dot.X += drawer.face.Kern (
 | 
					 | 
				
			||||||
						previousCharacter,
 | 
					 | 
				
			||||||
						character)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			previousCharacter = character
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		word.spaceAfter = spaceWidth.Round()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// add the word to the layout
 | 
					 | 
				
			||||||
		drawer.layout = append(drawer.layout, word)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// if there is a set maximum height, and we have crossed it,
 | 
					 | 
				
			||||||
		// stop processing more words. and remove any words that have
 | 
					 | 
				
			||||||
		// also crossed the line.
 | 
					 | 
				
			||||||
		if
 | 
					 | 
				
			||||||
			drawer.cut && (
 | 
					 | 
				
			||||||
				dot.Y - drawer.metrics.Ascent -
 | 
					 | 
				
			||||||
				drawer.metrics.Descent).Round() >
 | 
					 | 
				
			||||||
			drawer.height {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			for
 | 
					 | 
				
			||||||
				index := len(drawer.layout) - 1;
 | 
					 | 
				
			||||||
				index >= 0; index -- {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if drawer.layout[index].position.Y < dot.Y.Round() {
 | 
					 | 
				
			||||||
					break
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				drawer.layout = drawer.layout[:index]
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			break
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// add a little null to the last character
 | 
					 | 
				
			||||||
	if len(drawer.layout) > 0 {
 | 
					 | 
				
			||||||
		lastWord := &drawer.layout[len(drawer.layout) - 1]
 | 
					 | 
				
			||||||
		lastWord.text = append (
 | 
					 | 
				
			||||||
			lastWord.text,
 | 
					 | 
				
			||||||
			characterLayout {
 | 
					 | 
				
			||||||
				x: currentCharacterX.Round(),
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if drawer.wrap {
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Max.X = drawer.width
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Max.X = horizontalExtent
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if drawer.cut {
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Min.Y = 0 - drawer.metrics.Ascent.Round()
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Max.Y =
 | 
					 | 
				
			||||||
			drawer.height -
 | 
					 | 
				
			||||||
			drawer.metrics.Ascent.Round()
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Min.Y = 0 - drawer.metrics.Ascent.Round()
 | 
					 | 
				
			||||||
		drawer.layoutBounds.Max.Y =
 | 
					 | 
				
			||||||
			dot.Y.Round() +
 | 
					 | 
				
			||||||
			drawer.metrics.Descent.Round()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	// TODO:
 | 
					 | 
				
			||||||
	// for each line, calculate the bounds as if the words are left aligned,
 | 
					 | 
				
			||||||
	// and then at the end of the process go through each line and re-align
 | 
					 | 
				
			||||||
	// everything. this will make the process far simpler.
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								textdraw/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								textdraw/doc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					// Package textdraw implements utilities for drawing text, as well as performing
 | 
				
			||||||
 | 
					// text wrapping and bounds calculation.
 | 
				
			||||||
 | 
					package textdraw
 | 
				
			||||||
							
								
								
									
										57
									
								
								textdraw/drawer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								textdraw/drawer.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					package textdraw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "image"
 | 
				
			||||||
 | 
					import "unicode"
 | 
				
			||||||
 | 
					import "image/draw"
 | 
				
			||||||
 | 
					import "golang.org/x/image/math/fixed"
 | 
				
			||||||
 | 
					import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
				
			||||||
 | 
					import "git.tebibyte.media/sashakoshka/tomo/artist"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 canvas.Canvas,
 | 
				
			||||||
 | 
						source      artist.Pattern,
 | 
				
			||||||
 | 
						offset      image.Point,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						updatedRegion image.Rectangle,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						wrappedSource := artist.WrappedPattern {
 | 
				
			||||||
 | 
							Pattern: source,
 | 
				
			||||||
 | 
							Width:  0,
 | 
				
			||||||
 | 
							Height: 0, // TODO: choose a better width and height
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						drawer.For (func (
 | 
				
			||||||
 | 
							index    int,
 | 
				
			||||||
 | 
							char     rune,
 | 
				
			||||||
 | 
							position fixed.Point26_6,
 | 
				
			||||||
 | 
						) bool {
 | 
				
			||||||
 | 
							destinationRectangle,
 | 
				
			||||||
 | 
							mask, maskPoint, _, ok := drawer.face.Glyph (
 | 
				
			||||||
 | 
								fixed.P (
 | 
				
			||||||
 | 
									offset.X + position.X.Round(),
 | 
				
			||||||
 | 
									offset.Y + position.Y.Round()),
 | 
				
			||||||
 | 
								char)
 | 
				
			||||||
 | 
							if !ok || unicode.IsSpace(char) || char == 0 {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// FIXME:? clip destination rectangle if we are on the cusp of
 | 
				
			||||||
 | 
							// the maximum height.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							draw.DrawMask (
 | 
				
			||||||
 | 
								destination,
 | 
				
			||||||
 | 
								destinationRectangle,
 | 
				
			||||||
 | 
								wrappedSource, image.Point { },
 | 
				
			||||||
 | 
								mask, maskPoint,
 | 
				
			||||||
 | 
								draw.Over)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							updatedRegion = updatedRegion.Union(destinationRectangle)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										168
									
								
								textdraw/layout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								textdraw/layout.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
				
			|||||||
 | 
					package textdraw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 (
 | 
				
			||||||
 | 
						// AlignLeft aligns the start of each line to the beginning point
 | 
				
			||||||
 | 
						// of each dot.
 | 
				
			||||||
 | 
						AlignLeft Align = iota
 | 
				
			||||||
 | 
						AlignRight
 | 
				
			||||||
 | 
						AlignCenter
 | 
				
			||||||
 | 
						AlignJustify
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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.GlyphBounds(char)
 | 
				
			||||||
 | 
							if !ok { continue }
 | 
				
			||||||
 | 
							word.Runes = append (word.Runes, RuneLayout {
 | 
				
			||||||
 | 
								X:     x,
 | 
				
			||||||
 | 
								Width: advance,
 | 
				
			||||||
 | 
								Rune:  char,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 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
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					// maxWidth is greater than zero, this function will stop processing words once
 | 
				
			||||||
 | 
					// the limit is crossed. The word which would have crossed over the limit will
 | 
				
			||||||
 | 
					// not be processed.
 | 
				
			||||||
 | 
					func DoLine (text []rune, face font.Face, maxWidth fixed.Int26_6) (line LineLayout, remaining []rune) {
 | 
				
			||||||
 | 
						remaining = text
 | 
				
			||||||
 | 
						x        := fixed.Int26_6(0)
 | 
				
			||||||
 | 
						lastRune := rune(-1)
 | 
				
			||||||
 | 
						lastWord := WordLayout { }
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							// process one word
 | 
				
			||||||
 | 
							word, remainingFromWord := DoWord(remaining, face)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// apply kerning and position. yeah, its unlikely that a letter
 | 
				
			||||||
 | 
							// will have kerning with a whitespace character. but like, what
 | 
				
			||||||
 | 
							// if, you know?
 | 
				
			||||||
 | 
							if lastRune >= 0 && word.FirstRune() >= 0 {
 | 
				
			||||||
 | 
								x += face.Kern(lastRune, word.FirstRune())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							lastRune = word.LastRune()
 | 
				
			||||||
 | 
							word.X = x
 | 
				
			||||||
 | 
							x += word.Width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if we have gone over the maximum width, stop processing
 | 
				
			||||||
 | 
							// words (if maxWidth is even specified)
 | 
				
			||||||
 | 
							if maxWidth > 0 && x > maxWidth { break }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							remaining = remainingFromWord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if the word actually has contents, add it
 | 
				
			||||||
 | 
							if word.Runes != nil {
 | 
				
			||||||
 | 
								lastWord   = word
 | 
				
			||||||
 | 
								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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set the line's width. this is subject to be overridden by the
 | 
				
			||||||
 | 
						// TypeSetter to match the longest line.
 | 
				
			||||||
 | 
						if maxWidth > 0 {
 | 
				
			||||||
 | 
							line.Width = maxWidth
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							line.Width      = lastWord.X + lastWord.Width
 | 
				
			||||||
 | 
							line.SpaceAfter = lastWord.SpaceAfter
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Align aligns the text in the line according to the specified alignment
 | 
				
			||||||
 | 
					// method.
 | 
				
			||||||
 | 
					func (line *LineLayout) Align (align Align) {
 | 
				
			||||||
 | 
						// TODO
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								textdraw/layout_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								textdraw/layout_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					package textdraw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "testing"
 | 
				
			||||||
 | 
					import "golang.org/x/image/math/fixed"
 | 
				
			||||||
 | 
					import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDoWord (test *testing.T) {
 | 
				
			||||||
 | 
						text := []rune("The quick brown fox")
 | 
				
			||||||
 | 
						word, remaining := DoWord(text, defaultfont.FaceRegular)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						expect := "quick brown fox"
 | 
				
			||||||
 | 
						if string(remaining) != expect {
 | 
				
			||||||
 | 
							test.Fatalf (
 | 
				
			||||||
 | 
								`text: "%s", remaining: "%s" expected: "%s"`,
 | 
				
			||||||
 | 
								string(text), string(remaining), expect)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(word.Runes) != 4 {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong rune length %d`, len(word.Runes))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if word.FirstRune() != 'T' {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong first rune %s`, string(word.FirstRune()))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if word.LastRune() != ' ' {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong last rune %s`, string(word.FirstRune()))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDoLine (test *testing.T) {
 | 
				
			||||||
 | 
						// case 1
 | 
				
			||||||
 | 
						text := []rune("The quick brown fox\njumped over the lazy dog")
 | 
				
			||||||
 | 
						line, remaining := DoLine(text, defaultfont.FaceRegular, 0)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						expect := "jumped over the lazy dog"
 | 
				
			||||||
 | 
						if string(remaining) != expect {
 | 
				
			||||||
 | 
							test.Fatalf (
 | 
				
			||||||
 | 
								`text: "%s", remaining: "%s" expected: "%s"`,
 | 
				
			||||||
 | 
								string(text), string(remaining), expect)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(line.Words) != 4 {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong word count %d`, len(line.Words))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						if !line.BreakAfter {
 | 
				
			||||||
 | 
							test.Fatalf(`did not set BreakAfter to true`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// case 2
 | 
				
			||||||
 | 
						text = []rune("jumped over the lazy dog")
 | 
				
			||||||
 | 
						line, remaining = DoLine(text, defaultfont.FaceRegular, 0)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						expect = ""
 | 
				
			||||||
 | 
						if string(remaining) != expect {
 | 
				
			||||||
 | 
							test.Fatalf (
 | 
				
			||||||
 | 
								`text: "%s", remaining: "%s" expected: "%s"`,
 | 
				
			||||||
 | 
								string(text), string(remaining), expect)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(line.Words) != 5 {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong word count %d`, len(line.Words))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						if line.BreakAfter {
 | 
				
			||||||
 | 
							test.Fatalf(`did not set BreakAfter to false`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						// case 3
 | 
				
			||||||
 | 
						text = []rune("jumped over the lazy dog")
 | 
				
			||||||
 | 
						line, remaining = DoLine(text, defaultfont.FaceRegular, fixed.I(7 * 12))
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						expect = "the lazy dog"
 | 
				
			||||||
 | 
						if string(remaining) != expect {
 | 
				
			||||||
 | 
							test.Fatalf (
 | 
				
			||||||
 | 
								`text: "%s", remaining: "%s" expected: "%s"`,
 | 
				
			||||||
 | 
								string(text), string(remaining), expect)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(line.Words) != 2 {
 | 
				
			||||||
 | 
							test.Fatalf(`wrong word count %d`, len(line.Words))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						if line.BreakAfter {
 | 
				
			||||||
 | 
							test.Fatalf(`did not set BreakAfter to false`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										292
									
								
								textdraw/setter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								textdraw/setter.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,292 @@
 | 
				
			|||||||
 | 
					package textdraw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						layoutBounds      image.Rectangle
 | 
				
			||||||
 | 
						layoutBoundsSpace image.Rectangle
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (setter *TypeSetter) needLayout () {
 | 
				
			||||||
 | 
						if setter.layoutClean { return }
 | 
				
			||||||
 | 
						setter.layoutClean = true
 | 
				
			||||||
 | 
						setter.alignClean  = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we need to have a font and some text to do anything
 | 
				
			||||||
 | 
						setter.lines = nil
 | 
				
			||||||
 | 
						setter.layoutBounds      = image.Rectangle { }
 | 
				
			||||||
 | 
						setter.layoutBoundsSpace = image.Rectangle { }
 | 
				
			||||||
 | 
						if len(setter.text) == 0 { return }
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
						maxY             := fixed.I(setter.maxHeight) + metrics.Height
 | 
				
			||||||
 | 
						for len(remaining) > 0 && y < maxY {
 | 
				
			||||||
 | 
							// process one line
 | 
				
			||||||
 | 
							line, remainingFromLine := DoLine (
 | 
				
			||||||
 | 
								remaining, setter.face, fixed.I(setter.maxWidth))
 | 
				
			||||||
 | 
							remaining = remainingFromLine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// add the line
 | 
				
			||||||
 | 
							line.Y = y
 | 
				
			||||||
 | 
							y += metrics.Height
 | 
				
			||||||
 | 
							if line.Width > horizontalExtent {
 | 
				
			||||||
 | 
								horizontalExtent = line.Width
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							lineWidthSpace := line.Width + line.SpaceAfter
 | 
				
			||||||
 | 
							if lineWidthSpace > horizontalExtentSpace {
 | 
				
			||||||
 | 
								horizontalExtentSpace = lineWidthSpace
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							setter.lines = append(setter.lines, line)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set all line widths to horizontalExtent if we don't have a specified
 | 
				
			||||||
 | 
						// maximum width
 | 
				
			||||||
 | 
						if setter.maxWidth == 0 {
 | 
				
			||||||
 | 
							for index := range setter.lines {
 | 
				
			||||||
 | 
								setter.lines[index].Width = horizontalExtent
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							setter.layoutBounds.Max.X      = horizontalExtent.Round()
 | 
				
			||||||
 | 
							setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round()
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							setter.layoutBounds.Max.X      = setter.maxWidth
 | 
				
			||||||
 | 
							setter.layoutBoundsSpace.Max.X = setter.maxWidth
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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 {
 | 
				
			||||||
 | 
							setter.lines[index].Align(setter.align)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetMaxWidth sets the maximum width of the typesetter. If the maximum width
 | 
				
			||||||
 | 
					// is greater than zero, the text will wrap to that width. If the maximum width
 | 
				
			||||||
 | 
					// is zero, the text will not wrap and instead extend as far as it needs to.
 | 
				
			||||||
 | 
					func (setter *TypeSetter) SetMaxWidth (width int) {
 | 
				
			||||||
 | 
						if setter.maxWidth == width { return }
 | 
				
			||||||
 | 
						setter.layoutClean = false
 | 
				
			||||||
 | 
						setter.alignClean  = false
 | 
				
			||||||
 | 
						setter.maxWidth = width
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetMaxHeight sets the maximum height of the typesetter. If the maximum height
 | 
				
			||||||
 | 
					// is greater than zero, no lines will be laid out past that point. If the
 | 
				
			||||||
 | 
					// maximum 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MaxWidth returns the maximum width of the typesetter as set by SetMaxWidth.
 | 
				
			||||||
 | 
					func (setter *TypeSetter) MaxWidth () int {
 | 
				
			||||||
 | 
						return setter.maxWidth
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MaxHeight returns the maximum height of the typesetter as set by
 | 
				
			||||||
 | 
					// SetMaxHeight.
 | 
				
			||||||
 | 
					func (setter *TypeSetter) MaxHeight () int {
 | 
				
			||||||
 | 
						return setter.maxHeight
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Face returns the TypeSetter's font face as set by SetFace.
 | 
				
			||||||
 | 
					func (setter *TypeSetter) Face () font.Face {
 | 
				
			||||||
 | 
						return setter.face
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
						for _, line := range setter.lines {
 | 
				
			||||||
 | 
							for _, word := range line.Words {
 | 
				
			||||||
 | 
							for _, char := range word.Runes {
 | 
				
			||||||
 | 
								iterator (index, char.Rune, fixed.Point26_6 {
 | 
				
			||||||
 | 
									X: word.X + char.X,
 | 
				
			||||||
 | 
									Y: line.Y,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								index ++
 | 
				
			||||||
 | 
							}}
 | 
				
			||||||
 | 
							if line.BreakAfter { 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 }
 | 
				
			||||||
 | 
						metrics := setter.face.Metrics()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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)
 | 
				
			||||||
 | 
						line := setter.lines[len(setter.lines) - 1]
 | 
				
			||||||
 | 
						for _, curLine := range setter.lines {
 | 
				
			||||||
 | 
							for _, curWord := range curLine.Words {
 | 
				
			||||||
 | 
								index += len(curWord.Runes)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if curLine.Y + metrics.Descent > position.Y {
 | 
				
			||||||
 | 
								line = curLine
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if line.Words == nil { return }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// find the first rune who's right bound is greater than position.X.
 | 
				
			||||||
 | 
						for _, curWord := range line.Words {
 | 
				
			||||||
 | 
							for _, curChar := range curWord.Runes {
 | 
				
			||||||
 | 
								if curWord.X + curChar.X + curChar.Width > position.X {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								index ++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if line.BreakAfter { index ++ }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 { 0, metrics.Height }
 | 
				
			||||||
 | 
						for _, line := range setter.lines {
 | 
				
			||||||
 | 
							for _, word := range line.Words {
 | 
				
			||||||
 | 
								if word.Width + dot.X > fixed.I(width) {
 | 
				
			||||||
 | 
									dot.Y += metrics.Height
 | 
				
			||||||
 | 
									dot.X = 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								dot.X += word.Width + word.SpaceAfter
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if line.BreakAfter {
 | 
				
			||||||
 | 
								dot.Y += metrics.Height
 | 
				
			||||||
 | 
								dot.X = 0
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return dot.Y.Round()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user