9 Commits

3 changed files with 105 additions and 76 deletions

View File

@@ -13,11 +13,13 @@ 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,
color color.RGBA,
color color.Color,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
source := image.NewUniform(color)
drawer.For (func (
index int,
char rune,
@@ -39,7 +41,7 @@ func (drawer Drawer) Draw (
draw.DrawMask (
destination,
destinationRectangle,
image.NewUniform(color), image.Point { },
source, image.Point { },
mask, maskPoint,
draw.Over)

View File

@@ -108,10 +108,10 @@ type LineLayout struct {
// 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) {
// 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)
lastWord := WordLayout { }
@@ -122,9 +122,9 @@ func DoLine (text []rune, face font.Face, maxWidth fixed.Int26_6) (line LineLayo
word.X = x
x += word.Width
// if we have gone over the maximum width, stop processing
// words (if maxWidth is even specified)
if !isFirstWord && maxWidth > 0 && x > maxWidth {
// if we have gone over the preferred width, stop processing
// words (if wrap is enabled)
if !isFirstWord && wrap && x > width {
break
}
@@ -150,18 +150,26 @@ func DoLine (text []rune, face font.Face, maxWidth fixed.Int26_6) (line LineLayo
// set the width of the line's content.
line.ContentWidth = lastWord.X + lastWord.Width
// 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
if wrap {
line.Width = width
} else {
line.Width = line.ContentWidth
line.SpaceAfter = lastWord.SpaceAfter
line.Width = line.ContentWidth
}
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) {

139
setter.go
View File

@@ -18,7 +18,9 @@ type TypeSetter struct {
face font.Face
maxWidth int
maxHeight int
wrap bool
minWidth fixed.Int26_6
layoutBounds image.Rectangle
layoutBoundsSpace image.Rectangle
}
@@ -28,51 +30,52 @@ func (setter *TypeSetter) needLayout () {
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 }
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)
for len(remaining) > 0 {
// process one line
line, remainingFromLine := DoLine (
remaining, setter.face, fixed.I(setter.maxWidth))
remaining = remainingFromLine
// add the line
metrics := setter.face.Metrics()
remaining := setter.text
y := fixed.Int26_6(0)
addLine := func (line LineLayout) {
line.Y = y
y += metrics.Height
if line.Width > horizontalExtent {
horizontalExtent = line.Width
if line.ContentWidth > horizontalExtent {
horizontalExtent = line.ContentWidth
}
lineWidthSpace := line.Width + line.SpaceAfter
lineWidthSpace := line.ContentWidth + 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
// process every line until there are no more remaining runes
for len(remaining) > 0 {
line, remainingFromLine := DoLine (
remaining, setter.face, setter.wrap,
fixed.I(setter.maxWidth))
remaining = remainingFromLine
addLine(line)
}
// if there were no lines processed or the last line has a break after
// it, add a blank line at the end
needBlankLine :=
len(setter.lines) == 0 ||
setter.lines[len(setter.lines) - 1].BreakAfter
if needBlankLine { addLine(LineLayout { }) }
// calculate layout boundaries
setter.minWidth = horizontalExtentSpace
setter.layoutBounds.Max.X = horizontalExtent.Round()
setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round()
y -= metrics.Height
if setter.maxHeight == 0 {
setter.layoutBounds.Min.Y = -metrics.Ascent.Round()
@@ -96,18 +99,28 @@ func (setter *TypeSetter) needAlignedLayout () {
for index := range setter.lines {
align := setter.align
if index == len(setter.lines) - 1 && align == AlignJustify {
align = AlignLeft
if align == AlignJustify {
except :=
index == len(setter.lines) - 1 ||
setter.lines[index].BreakAfter
if except { align = AlignLeft }
}
setter.lines[index].Align(align)
}
}
// SetWrap sets whether or not the text wraps around and forms new lines.
func (setter *TypeSetter) SetWrap (wrap bool) {
if setter.wrap == wrap { return }
setter.layoutClean = false
setter.wrap = wrap
}
// SetAlign sets the alignment method of the typesetter.
func (setter *TypeSetter) SetAlign (align Align) {
if setter.align == align { return }
func (setter *TypeSetter) SetAlign (horizontal, vertical Align) {
if setter.align == horizontal { return }
setter.alignClean = false
setter.align = align
setter.align = horizontal
}
// SetText sets the text content of the typesetter.
@@ -125,20 +138,19 @@ func (setter *TypeSetter) SetFace (face font.Face) {
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) {
// 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.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) {
// 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.maxHeight == heignt { return }
setter.layoutClean = false
setter.alignClean = false
@@ -159,14 +171,13 @@ func (setter *TypeSetter) LineHeight () fixed.Int26_6 {
return setter.face.Metrics().Height
}
// MaxWidth returns the maximum width of the typesetter as set by SetMaxWidth.
func (setter *TypeSetter) MaxWidth () int {
// Width returns the height of the typesetter as set by SetWidth.
func (setter *TypeSetter) Width () int {
return setter.maxWidth
}
// MaxHeight returns the maximum height of the typesetter as set by
// SetMaxHeight.
func (setter *TypeSetter) MaxHeight () int {
// Height returns the height of the typesetter as set by SetHeight.
func (setter *TypeSetter) Height () int {
return setter.maxHeight
}
@@ -238,27 +249,21 @@ func (setter *TypeSetter) AtPosition (position fixed.Point26_6) (index int) {
// 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()
line := setter.lines[len(setter.lines) - 1]
lineSize := 0
for _, curLine := range setter.lines {
for _, curWord := range curLine.Words {
lineSize += len(curWord.Runes)
}
if curLine.BreakAfter { lineSize ++ }
index += lineSize
metrics := setter.face.Metrics()
lastLine := setter.lines[len(setter.lines) - 1]
for _, curLine := range setter.lines {
if curLine.Y + metrics.Descent > position.Y {
line = curLine
lastLine = curLine
break
}
index += curLine.Length()
}
index -= lineSize
if line.Words == nil { return }
if lastLine.Words == nil { return }
// find the first rune who's right bound is greater than position.X.
for _, curWord := range line.Words {
for _, curWord := range lastLine.Words {
for _, curChar := range curWord.Runes {
x := curWord.X + curChar.X + curChar.Width
if x > position.X { goto foundRune }
@@ -296,6 +301,20 @@ func (setter *TypeSetter) LayoutBoundsSpace () (image.Rectangle) {
return setter.layoutBoundsSpace
}
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return (Em(), 0)
func (setter *TypeSetter) MinimumSize () image.Point {
setter.needLayout()
if setter.wrap {
return image.Pt(setter.Em().Round(), 0)
}
width := setter.minWidth
height := fixed.Int26_6(len(setter.lines)) * setter.LineHeight()
return image.Pt(width.Round(), height.Round())
}
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not alter the
// typesetter's state.