Compare commits
	
		
			3 Commits
		
	
	
		
			aa00b93bd3
			...
			0592fe32b6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0592fe32b6 | |||
| 56cf7e3fb8 | |||
| 650ecf0c2e | 
							
								
								
									
										12
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
indent_style = tab
 | 
			
		||||
indent_size = 8
 | 
			
		||||
charset = utf-8
 | 
			
		||||
 | 
			
		||||
[*.md]
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 2
 | 
			
		||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
								
							@ -2,7 +2,20 @@
 | 
			
		||||
 | 
			
		||||
[](https://pkg.go.dev/git.tebibyte.media/tomo/typeset)
 | 
			
		||||
 | 
			
		||||
Typeset provides utilities for text layout, wrapping, and rendering.
 | 
			
		||||
Typeset provides utilities for text layout, wrapping, and rendering. It is
 | 
			
		||||
designed to avoid redundant work and minimize memory allocations wherever
 | 
			
		||||
posible in situations where the bounds of a section of text may change
 | 
			
		||||
frequently and its content semi-frequently. Text layout is performed by the
 | 
			
		||||
TypeSetter struct, which operates in a three-phase process:
 | 
			
		||||
 | 
			
		||||
The state of a text layout is stored in a TypeSetter, and it can be drawn to any
 | 
			
		||||
draw.Image using a Drawer which "extends" TypeSetter.
 | 
			
		||||
1. Tokenization
 | 
			
		||||
2. Measurement
 | 
			
		||||
3. Layout, alignment
 | 
			
		||||
 | 
			
		||||
The results of these phases are memoized. When the state of the TypeSetter is
 | 
			
		||||
queried, it will run through only the required phases before returning a value.
 | 
			
		||||
 | 
			
		||||
The contents of a TypeSetter can be drawn onto any draw.Image using the Draw
 | 
			
		||||
function included within this package, but it is entirely possible to create a
 | 
			
		||||
custom draw function that iterates over TypeSetter.Runes that uses some other
 | 
			
		||||
method of drawing that's faster than five gazillion virtual method calls.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										84
									
								
								old/drawer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								old/drawer.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								old/layout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								old/layout.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,250 @@
 | 
			
		||||
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
 | 
			
		||||
	// TODO: just have RecommendedHeight want aligned layout?
 | 
			
		||||
	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 tabStop (x, tabWidth fixed.Int26_6, delta int) fixed.Int26_6 {
 | 
			
		||||
	return fixed.I((tabWidth * 64 / x).Floor() + delta).Mul(tabWidth)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										428
									
								
								old/setter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										428
									
								
								old/setter.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,428 @@
 | 
			
		||||
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
 | 
			
		||||
	
 | 
			
		||||
	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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ForRunes is like For, but leaves out the fake null rune.
 | 
			
		||||
func (setter *TypeSetter) ForRunes (iterator RuneIterator) {
 | 
			
		||||
	setter.forInternal(iterator, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (setter *TypeSetter) forInternal (iterator RuneIterator, fakeNull bool) {
 | 
			
		||||
	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 ++
 | 
			
		||||
			lastCharRightBound = fixed.Int26_6(0)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
		}
 | 
			
		||||
		if line.BreakAfter {
 | 
			
		||||
			dot.Y += metrics.Height
 | 
			
		||||
			dot.X = 0
 | 
			
		||||
			firstWord = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return dot.Y.Round()
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user