Compare commits
4 Commits
37554dd719
...
89c23a8947
Author | SHA1 | Date | |
---|---|---|---|
89c23a8947 | |||
0cb6e28542 | |||
5dee53b8a9 | |||
9c7732c95b |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
output.png
|
103
draw.go
Normal file
103
draw.go
Normal file
@ -0,0 +1,103 @@
|
||||
package typeset
|
||||
|
||||
import "image"
|
||||
import "unicode"
|
||||
import "image/draw"
|
||||
import "image/color"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
|
||||
// Draw draws the contents of a TypeSetter to an image at the given offset. It
|
||||
// returns a rectangle containing all pixels in the image that were updated.
|
||||
func Draw (destination draw.Image, setter *TypeSetter, offset fixed.Point26_6, col color.Color) image.Rectangle {
|
||||
source := image.NewUniform(col)
|
||||
face := setter.Face()
|
||||
var updatedRegion image.Rectangle
|
||||
bounds := destination.Bounds()
|
||||
|
||||
setter.Runes()(func (position fixed.Point26_6, run rune) bool {
|
||||
// leave empty space for space characters
|
||||
if unicode.IsSpace(run) {
|
||||
return true
|
||||
}
|
||||
|
||||
dot := offset.Add(position)
|
||||
destinationRectangle, mask, maskPoint, _, ok := face.Glyph(dot, run)
|
||||
|
||||
if ok {
|
||||
// don't bother drawing runes that are out of bounds
|
||||
if destinationRectangle.Min.Y > bounds.Max.Y { return false }
|
||||
if destinationRectangle.Intersect(bounds).Empty() { return true }
|
||||
|
||||
// draw rune
|
||||
draw.DrawMask (
|
||||
destination,
|
||||
destinationRectangle,
|
||||
source, image.Point { },
|
||||
mask, maskPoint,
|
||||
draw.Over)
|
||||
} else {
|
||||
// draw tofu
|
||||
drawTofu(run, destination, dot, face, col)
|
||||
}
|
||||
|
||||
|
||||
updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
return true
|
||||
})
|
||||
|
||||
return updatedRegion
|
||||
}
|
||||
|
||||
// DrawBounds draws the LayoutBounds, MinimumSize, and LayoutBoundsSpace of a
|
||||
// TypeSetter to the given image.
|
||||
func DrawBounds (destination draw.Image, setter *TypeSetter, offset fixed.Point26_6) {
|
||||
blue := color.RGBA { B: 255, A: 255 }
|
||||
red := color.RGBA { R: 255, A: 255 }
|
||||
green := color.RGBA { G: 255, A: 255 }
|
||||
|
||||
layoutBoundsSpace := setter.LayoutBoundsSpace()
|
||||
layoutBounds := setter.LayoutBounds()
|
||||
|
||||
minimum := setter.MinimumSize()
|
||||
minimumRect := roundRect(fixed.Rectangle26_6 { Max: minimum }.Add(offset).Add(layoutBounds.Min))
|
||||
drawRectangleOutline(destination, minimumRect, green)
|
||||
|
||||
drawRectangleOutline(destination, roundRect(layoutBoundsSpace.Add(offset)), blue)
|
||||
drawRectangleOutline(destination, roundRect(layoutBounds.Add(offset)), red)
|
||||
}
|
||||
|
||||
func drawTofu (
|
||||
char rune,
|
||||
destination draw.Image,
|
||||
position fixed.Point26_6,
|
||||
face font.Face,
|
||||
col color.Color,
|
||||
) {
|
||||
bounds, _ := tofuBounds(face)
|
||||
rectBounds := roundRect(bounds).Add(image.Pt (
|
||||
position.X.Round(),
|
||||
position.Y.Round()))
|
||||
drawRectangleOutline(destination, rectBounds, col)
|
||||
}
|
||||
|
||||
func drawRectangleOutline (destination draw.Image, bounds image.Rectangle, col color.Color) {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
destination.Set(x, bounds.Min.Y, col)
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
destination.Set(bounds.Min.X, y, col)
|
||||
destination.Set(bounds.Max.X - 1, y, col)
|
||||
}
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
destination.Set(x, bounds.Max.Y - 1, col)
|
||||
}
|
||||
}
|
||||
|
||||
func roundRect (rectangle fixed.Rectangle26_6) image.Rectangle {
|
||||
return image.Rect (
|
||||
rectangle.Min.X.Round(),
|
||||
rectangle.Min.Y.Round(),
|
||||
rectangle.Max.X.Round(),
|
||||
rectangle.Max.Y.Round())
|
||||
}
|
162
examples/test/main.go
Normal file
162
examples/test/main.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Example test demonstrates a variety of ways that TypeSetter can arrange text.
|
||||
package main
|
||||
|
||||
import "os"
|
||||
import "image"
|
||||
import "image/png"
|
||||
import "image/draw"
|
||||
import "image/color"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "git.tebibyte.media/tomo/typeset"
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
|
||||
func main () {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 2048, 1024))
|
||||
|
||||
setter := typeset.TypeSetter { }
|
||||
setter.SetWrap(true)
|
||||
setter.SetFace(basicfont.Face7x13)
|
||||
setter.SetText(lipsum)
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Inset(4))
|
||||
|
||||
setter.SetText(dimple)
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 0)).Inset(4))
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(0, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(0, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(0, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(0, 512)).Inset(4))
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 512)).Inset(4))
|
||||
|
||||
setter.SetText(haiku)
|
||||
setter.SetWrap(false)
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 0)).Inset(4))
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 0)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1536, 0)).Inset(4))
|
||||
|
||||
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 512)).Inset(4))
|
||||
|
||||
setter.SetText(haikuAlt)
|
||||
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
|
||||
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 512)).Inset(4))
|
||||
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
|
||||
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 512)).Inset(4))
|
||||
|
||||
file, err := os.Create("output.png")
|
||||
if err != nil { panic(err) }
|
||||
defer file.Close()
|
||||
err = png.Encode(file, img)
|
||||
if err != nil { panic(err) }
|
||||
}
|
||||
|
||||
type subDrawImage interface {
|
||||
draw.Image
|
||||
SubImage (image.Rectangle) image.Image
|
||||
}
|
||||
|
||||
func drawText (destination subDrawImage, setter *typeset.TypeSetter, bounds image.Rectangle) {
|
||||
whiteRectangle(destination, bounds)
|
||||
subImage := destination.SubImage(bounds).(draw.Image)
|
||||
metrics := setter.Face().Metrics()
|
||||
bounds = bounds.Inset(16)
|
||||
grayRectangle(destination, bounds)
|
||||
size := fixed.P(bounds.Dx(), bounds.Dy())
|
||||
setter.SetSize(size)
|
||||
offset := fixed.Point26_6 {
|
||||
X: fixed.I(bounds.Min.X),
|
||||
Y: metrics.Ascent + fixed.I(bounds.Min.Y),
|
||||
}
|
||||
// typeset.Draw(destination, setter, offset, color.Black)
|
||||
typeset.DrawBounds(subImage, setter, offset)
|
||||
typeset.Draw(subImage, setter, offset, color.Black)
|
||||
|
||||
}
|
||||
|
||||
func whiteRectangle (destination draw.Image, rect image.Rectangle) {
|
||||
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 150, G: 150, B: 150, A: 255 }), image.Pt(0, 0))
|
||||
}
|
||||
|
||||
func grayRectangle (destination draw.Image, rect image.Rectangle) {
|
||||
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 200, G: 200, B: 200, A: 255 }), image.Pt(0, 0))
|
||||
}
|
||||
|
||||
const lipsum = `Eum officia beatae harum. Rem aut praesentium possimus dignissimos ea sed. Recusandae sint rerum ut. Qui delectus rerum ut ut. Nobis non veritatis consequatur quia explicabo id. Et aut qui reiciendis esse voluptatem.
|
||||
|
||||
Eaque rem incidunt porro unde quia expedita quia. Deleniti repellat modi placeat. Et beatae aut voluptatem. Veritatis perspiciatis et aperiam sit modi sequi.
|
||||
|
||||
Accusantium et fugit expedita consequatur incidunt explicabo ea voluptatibus. Debitis consectetur veniam ut et esse aspernatur. Quas occaecati explicabo consequuntur. Quae dolorem ea harum ut tempora. Corporis ducimus et voluptatem. Corporis distinctio quia velit accusantium sunt omnis.
|
||||
|
||||
Libero blanditiis aut aut exercitationem modi. Eum corporis quam facere. Perferendis sit nulla et qui repellat eaque neque in. Expedita quidem similique sunt delectus similique non assumenda.
|
||||
|
||||
Hic rerum earum sapiente et itaque harum. Itaque amet voluptatem aliquid. Et qui excepturi animi voluptatem debitis necessitatibus atque animi. Nemo voluptates delectus quisquam non. Ipsam error voluptas similique dolores odit quos.`
|
||||
|
||||
const dimple = `I have been trying to remember this for a while
|
||||
|
||||
I could never place it
|
||||
|
||||
My brain kept saying Dimple when I tried to remember. Ever closer yet unobtainable. Censored, even. It did not want me to remember the dumple...`
|
||||
|
||||
const haiku = `An ocean voyage.
|
||||
As waves break over the bow,
|
||||
the sea welcomes me.
|
||||
|
||||
This is a very long line that will probably get cut off.`
|
||||
|
||||
const haikuAlt = `An ocean voyage.
|
||||
As waves break over the bow,
|
||||
the sea welcomes me.
|
||||
|
||||
This is a short ending`
|
191
flow.go
Normal file
191
flow.go
Normal file
@ -0,0 +1,191 @@
|
||||
package typeset
|
||||
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
|
||||
// TODO perhaps follow https://unicode.org/reports/tr14/
|
||||
|
||||
func reflow (
|
||||
tokens []token,
|
||||
face font.Face, size fixed.Point26_6,
|
||||
wrap bool, xAlign, yAlign Align,
|
||||
) (
|
||||
extents, extentsSpace fixed.Rectangle26_6,
|
||||
minimumSize fixed.Point26_6,
|
||||
) {
|
||||
if len(tokens) == 0 { return }
|
||||
|
||||
metrics := face.Metrics()
|
||||
|
||||
var dot fixed.Point26_6
|
||||
lineStart := 0
|
||||
lineEnd := 0
|
||||
lastWord := 0
|
||||
lastToken := 0
|
||||
nLines := 0
|
||||
|
||||
newline := func () {
|
||||
// if the line isn't empty
|
||||
if lineStart != lineEnd {
|
||||
// align line
|
||||
alignLine (
|
||||
tokens[lineStart:lineEnd],
|
||||
size.X, xAlign, lineEnd == len(tokens))
|
||||
|
||||
// calculate extents
|
||||
lastWordTok := tokens[lastWord]
|
||||
lastTokenTok := tokens[lastToken]
|
||||
lineMax := lastWordTok.position.X + lastWordTok.width
|
||||
lineMaxSpace := lastTokenTok.position.X + lastTokenTok.width
|
||||
if lineMax > minimumSize.X { minimumSize.X = lineMax }
|
||||
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
|
||||
}
|
||||
|
||||
// update dot
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
|
||||
// update indices, counts
|
||||
lineStart = lineEnd
|
||||
lastWord = lineEnd
|
||||
nLines ++
|
||||
}
|
||||
|
||||
// for each line, arrange and align while calculating effective
|
||||
// bounds/extents
|
||||
sawLineBreak := false
|
||||
for index, token := range tokens {
|
||||
lineEnd = index
|
||||
updateIndices := func () {
|
||||
lastToken = index
|
||||
if token.kind == tokenKindWord {
|
||||
lastWord = index
|
||||
}
|
||||
}
|
||||
|
||||
// demarcate lines
|
||||
if sawLineBreak {
|
||||
newline()
|
||||
sawLineBreak = false
|
||||
}
|
||||
if token.kind == tokenKindLineBreak {
|
||||
updateIndices()
|
||||
tokens[index].position = dot
|
||||
sawLineBreak = true
|
||||
} else {
|
||||
needWrap :=
|
||||
wrap &&
|
||||
token.kind == tokenKindWord &&
|
||||
dot.X + token.width > size.X
|
||||
if needWrap {
|
||||
newline()
|
||||
}
|
||||
updateIndices()
|
||||
tokens[index].position = dot
|
||||
dot.X += token.width
|
||||
}
|
||||
}
|
||||
lineEnd ++ // make lineEnd equal to len(tokens)
|
||||
newline()
|
||||
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
|
||||
|
||||
// second, vertical alignment pass
|
||||
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
|
||||
|
||||
// calculate extents
|
||||
extentsOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
|
||||
extents.Max.X = minimumSize.X
|
||||
extents.Max.Y = dot.Y + metrics.Descent
|
||||
extentsSpace.Max.Y = dot.Y + metrics.Descent
|
||||
extents = extents.Sub(extentsOffset)
|
||||
extentsSpace = extentsSpace.Sub(extentsOffset)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func alignLinesVertically (tokens []token, height, contentHeight fixed.Int26_6, align Align) {
|
||||
if len(tokens) == 0 { return }
|
||||
if align == AlignStart { return }
|
||||
|
||||
var topOffset fixed.Int26_6
|
||||
switch align {
|
||||
case AlignMiddle: topOffset = (height - contentHeight) / 2
|
||||
case AlignEnd, AlignEven: topOffset = height - contentHeight
|
||||
}
|
||||
|
||||
for index := range tokens {
|
||||
tokens[index].position.Y += topOffset
|
||||
}
|
||||
}
|
||||
|
||||
func alignLine (tokens []token, width fixed.Int26_6, align Align, atEnd bool) {
|
||||
if len(tokens) == 0 { return }
|
||||
if align == AlignStart { return }
|
||||
|
||||
if align == AlignEven {
|
||||
alignLineJustify(tokens, width, atEnd)
|
||||
return
|
||||
}
|
||||
|
||||
var leftOffset fixed.Int26_6
|
||||
contentWidth := lineContentWidth(tokens)
|
||||
switch align {
|
||||
case AlignMiddle: leftOffset = (width - contentWidth) / 2
|
||||
case AlignEnd: leftOffset = width - contentWidth
|
||||
}
|
||||
|
||||
for index := range tokens {
|
||||
tokens[index].position.X += leftOffset
|
||||
}
|
||||
}
|
||||
|
||||
func alignLineJustify (tokens []token, width fixed.Int26_6, atEnd bool) {
|
||||
cantJustify :=
|
||||
len(tokens) < 2 ||
|
||||
atEnd ||
|
||||
tokens[len(tokens) - 1].kind == tokenKindLineBreak
|
||||
if cantJustify {
|
||||
alignLine(tokens, width, AlignStart, atEnd)
|
||||
return
|
||||
}
|
||||
|
||||
contentWidth, wordCount := lineContentWordWidth(tokens)
|
||||
spaceCount := wordCount - 1
|
||||
if spaceCount == 0 { return }
|
||||
spacePerWord := (width - contentWidth) / fixed.Int26_6(spaceCount)
|
||||
|
||||
var x fixed.Int26_6
|
||||
for index, token := range tokens {
|
||||
if token.kind == tokenKindWord {
|
||||
tokens[index].position.X = x
|
||||
x += spacePerWord + token.width
|
||||
} else {
|
||||
tokens[index].position.X = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lineContentWordWidth (tokens []token) (fixed.Int26_6, int) {
|
||||
var width fixed.Int26_6
|
||||
var count int
|
||||
for _, token := range tokens {
|
||||
if token.kind == tokenKindWord {
|
||||
width += token.width
|
||||
count ++
|
||||
}
|
||||
}
|
||||
return width, count
|
||||
}
|
||||
|
||||
func lineContentWidth (tokens []token) fixed.Int26_6 {
|
||||
var width, spaceWidth fixed.Int26_6
|
||||
for _, token := range tokens {
|
||||
if token.kind == tokenKindWord {
|
||||
width += spaceWidth + token.width
|
||||
spaceWidth = 0
|
||||
} else {
|
||||
spaceWidth = token.width
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
227
typesetter.go
Normal file
227
typesetter.go
Normal file
@ -0,0 +1,227 @@
|
||||
package typeset
|
||||
|
||||
import "fmt"
|
||||
import "strconv"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
|
||||
type validationLevel int; const (
|
||||
validationLevelNone validationLevel = iota
|
||||
validationLevelTokens
|
||||
validationLevelMeasurement
|
||||
validationLevelFlow
|
||||
)
|
||||
|
||||
type tokenKind int; const (
|
||||
tokenKindWord tokenKind = iota // contains everything that isn't:
|
||||
tokenKindSpace // only unicode space runes, except \r or \n
|
||||
tokenKindTab // only \t runes
|
||||
tokenKindLineBreak // either "\n", or "\r\n"
|
||||
)
|
||||
|
||||
func (kind tokenKind) String () string {
|
||||
switch kind {
|
||||
case tokenKindWord: return "Word"
|
||||
case tokenKindSpace: return "Space"
|
||||
case tokenKindTab: return "Tab"
|
||||
case tokenKindLineBreak: return "LineBreak"
|
||||
}
|
||||
return fmt.Sprintf("typeset.tokenKind(%d)", kind)
|
||||
}
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
width fixed.Int26_6
|
||||
position fixed.Point26_6
|
||||
runes []runeLayout
|
||||
}
|
||||
|
||||
func (tok token) String () string {
|
||||
str := ""
|
||||
for _, runl := range tok.runes {
|
||||
str += string(runl.run)
|
||||
}
|
||||
return fmt.Sprintf (
|
||||
"%v:%v{%v,%v-%v}",
|
||||
tok.kind, strconv.Quote(str),
|
||||
tok.position.X, tok.position.Y, tok.width)
|
||||
}
|
||||
|
||||
type runeLayout struct {
|
||||
x fixed.Int26_6
|
||||
run rune
|
||||
}
|
||||
|
||||
func (run runeLayout) String () string {
|
||||
return fmt.Sprintf("%s-{%v}", strconv.Quote(string([]rune { run.run })), run.x)
|
||||
}
|
||||
|
||||
// RuneIter is an iterator that iterates over positioned runes.
|
||||
type RuneIter func (yield func(fixed.Point26_6, rune) bool)
|
||||
|
||||
// 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 | (unsupported)
|
||||
)
|
||||
|
||||
// TypeSetter manages text, and can perform layout operations on it. It
|
||||
// automatically avoids performing redundant work. It has no constructor and its
|
||||
// zero value can be used safely, but it must not be copied after first use.
|
||||
type TypeSetter struct {
|
||||
text string
|
||||
runes []runeLayout
|
||||
tokens []token
|
||||
|
||||
validationLevel validationLevel
|
||||
|
||||
xAlign, yAlign Align
|
||||
face font.Face
|
||||
size fixed.Point26_6 // width, height
|
||||
wrap bool
|
||||
|
||||
minimumSize fixed.Point26_6
|
||||
layoutBounds fixed.Rectangle26_6
|
||||
layoutBoundsSpace fixed.Rectangle26_6
|
||||
}
|
||||
|
||||
// Runes returns an iterator for all runes in the TypeSetter, and thier positions.
|
||||
func (this *TypeSetter) Runes () RuneIter {
|
||||
this.needFlow()
|
||||
return func (yield func (fixed.Point26_6, rune) bool) {
|
||||
for _, token := range this.tokens {
|
||||
for _, runl := range token.runes {
|
||||
pos := token.position
|
||||
pos.X += runl.x
|
||||
if !yield(pos, runl.run) { return }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Em returns the width of one emspace according to the typesetter's typeface,
|
||||
// which is the width of the capital letter 'M'.
|
||||
func (this *TypeSetter) Em () fixed.Int26_6 {
|
||||
if this.face == nil { return 0 }
|
||||
width, _ := this.face.GlyphAdvance('M')
|
||||
return width
|
||||
}
|
||||
|
||||
// MinimumSize returns the minimum width and height needed to display text. If
|
||||
// wrapping is enabled, this method will return { X: Em(), Y: 0 }.
|
||||
func (this *TypeSetter) MinimumSize () fixed.Point26_6 {
|
||||
if this.wrap { return fixed.Point26_6{ X: this.Em(), Y: 0 } }
|
||||
this.needFlow()
|
||||
return this.minimumSize
|
||||
}
|
||||
|
||||
// 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 (this *TypeSetter) LayoutBounds () fixed.Rectangle26_6 {
|
||||
this.needFlow()
|
||||
return this.layoutBounds
|
||||
}
|
||||
|
||||
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
|
||||
// trailing whitespace at the end of each line (if it exists).
|
||||
func (this *TypeSetter) LayoutBoundsSpace () fixed.Rectangle26_6 {
|
||||
this.needFlow()
|
||||
return this.layoutBoundsSpace
|
||||
}
|
||||
|
||||
// PositionAt returns the position of the rune at the specified index.
|
||||
func (this *TypeSetter) PositionAt (index int) fixed.Point26_6 {
|
||||
idx := 0
|
||||
var position fixed.Point26_6
|
||||
this.Runes()(func (pos fixed.Point26_6, run rune) bool {
|
||||
if index == idx {
|
||||
position = pos
|
||||
return false
|
||||
}
|
||||
idx ++
|
||||
return true
|
||||
})
|
||||
return position
|
||||
}
|
||||
|
||||
// SetText sets the text of the TypeSetter.
|
||||
func (this *TypeSetter) SetText (text string) {
|
||||
if this.text == text { return }
|
||||
this.text = text
|
||||
this.invalidate(validationLevelTokens)
|
||||
}
|
||||
|
||||
// SetSize sets the width and height of the TypeSetter.
|
||||
func (this *TypeSetter) SetSize (size fixed.Point26_6) {
|
||||
if this.size == size { return }
|
||||
this.size = size
|
||||
this.invalidate(validationLevelFlow)
|
||||
}
|
||||
|
||||
// SetWrap sets whether the text will wrap to the width specified by SetSize.
|
||||
func (this *TypeSetter) SetWrap (wrap bool) {
|
||||
if this.wrap == wrap { return }
|
||||
this.wrap = wrap
|
||||
this.invalidate(validationLevelFlow)
|
||||
}
|
||||
|
||||
// SetAlign sets the horizontal and vertical alignment of the text.
|
||||
func (this *TypeSetter) SetAlign (x, y Align) {
|
||||
if this.xAlign == x && this.yAlign == y { return }
|
||||
this.xAlign = x
|
||||
this.yAlign = y
|
||||
this.invalidate(validationLevelFlow)
|
||||
}
|
||||
|
||||
// Face returns the font face as set by SetFace.
|
||||
func (this *TypeSetter) Face () font.Face {
|
||||
return this.face
|
||||
}
|
||||
|
||||
// SetFace sets the font face the text will be laid out according to.
|
||||
func (this *TypeSetter) SetFace (face font.Face) {
|
||||
if this.face == face { return }
|
||||
this.face = face
|
||||
this.invalidate(validationLevelMeasurement)
|
||||
}
|
||||
|
||||
func (this *TypeSetter) needTokens () {
|
||||
if this.valid(validationLevelTokens) { return }
|
||||
this.runes, this.tokens = parseString(this.text)
|
||||
this.validate(validationLevelTokens)
|
||||
}
|
||||
|
||||
func (this *TypeSetter) needMeasurement () {
|
||||
if this.valid(validationLevelMeasurement) { return }
|
||||
this.needTokens()
|
||||
measure(this.tokens, this.face)
|
||||
this.validate(validationLevelMeasurement)
|
||||
}
|
||||
|
||||
func (this *TypeSetter) needFlow () {
|
||||
if this.valid(validationLevelFlow) { return }
|
||||
this.needMeasurement()
|
||||
this.layoutBounds, this.layoutBoundsSpace, this.minimumSize = reflow (
|
||||
this.tokens,
|
||||
this.face, this.size,
|
||||
this.wrap, this.xAlign, this.yAlign)
|
||||
this.validate(validationLevelFlow)
|
||||
}
|
||||
|
||||
func (this *TypeSetter) validate (level validationLevel) {
|
||||
this.validationLevel = level
|
||||
}
|
||||
|
||||
func (this *TypeSetter) invalidate (level validationLevel) {
|
||||
if this.valid(level) {
|
||||
this.validationLevel = level - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (this *TypeSetter) valid (level validationLevel) bool {
|
||||
return this.validationLevel >= level
|
||||
}
|
Loading…
Reference in New Issue
Block a user