Replace TextDrawer with more capable system

This commit is contained in:
Sasha Koshka 2023-02-15 18:17:17 -05:00
parent 0c22977693
commit ae551c47ea
6 changed files with 608 additions and 389 deletions

View File

@ -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
View 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
View 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
View 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
View 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
View 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()
}