Compare commits
50 Commits
v0.5.3
...
4be5154df0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4be5154df0 | |||
| 197346d730 | |||
| ef8944f2a6 | |||
| 4c42b12cc1 | |||
| 6efe40efc2 | |||
| 17385c4c9a | |||
| e38cac8e3b | |||
| 90b2e49664 | |||
| 2ae07af710 | |||
| ce21b34f86 | |||
| ff8f86e034 | |||
| 288a1fb9ef | |||
| a91816df6c | |||
| aa486fe660 | |||
| cde84b8756 | |||
| 013b121d46 | |||
| 85c48461c7 | |||
| 0342e25456 | |||
| f0adca5c37 | |||
| 56024caaf5 | |||
| 5171cbac16 | |||
| 300c28853d | |||
| 6fabfd9fd0 | |||
| 6b6e485aca | |||
| 89c23a8947 | |||
| 0cb6e28542 | |||
| 5dee53b8a9 | |||
| 9c7732c95b | |||
| 37554dd719 | |||
| 569defdb36 | |||
| 0c9d50ebcd | |||
| 943fc57080 | |||
| 0592fe32b6 | |||
| 56cf7e3fb8 | |||
| 650ecf0c2e | |||
| aa00b93bd3 | |||
| 8a22afe95a | |||
| ba1438b700 | |||
| 2aa1d355ec | |||
| 6e3e288628 | |||
| f2da861f1b | |||
| 0beef86c58 | |||
| 6a60458484 | |||
| 021dd288b6 | |||
| b0e80ce961 | |||
| a1bd411e43 | |||
| a54d40b52c | |||
| 8d9e0e1340 | |||
| 92cb318972 | |||
| 14deec24f5 |
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
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
output.png
|
||||
21
README.md
21
README.md
@@ -1,6 +1,21 @@
|
||||
# typeset
|
||||
|
||||
Typeset provides utilities for text layout, wrapping, and rendering.
|
||||
[](https://pkg.go.dev/git.tebibyte.media/tomo/typeset)
|
||||
|
||||
The state of a text layout is stored in a TypeSetter, and it can be drawn to any
|
||||
image.Image using a Drawer which "extends" TypeSetter.
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
106
draw.go
Normal file
106
draw.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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 using these colors:
|
||||
// - Red: LayoutBounds
|
||||
// - Green: MinimumSize
|
||||
// - Blue: LayoutBoundsSpace
|
||||
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())
|
||||
}
|
||||
52
drawer.go
52
drawer.go
@@ -1,52 +0,0 @@
|
||||
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,
|
||||
color color.Color,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
source := image.NewUniform(color)
|
||||
|
||||
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,
|
||||
source, image.Point { },
|
||||
mask, maskPoint,
|
||||
draw.Over)
|
||||
|
||||
updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
164
examples/test/main.go
Normal file
164
examples/test/main.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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))
|
||||
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
|
||||
drawText(img, &setter, image.Rect(256, 256, 512, 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`
|
||||
239
flow.go
Normal file
239
flow.go
Normal file
@@ -0,0 +1,239 @@
|
||||
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
|
||||
const notSeen = -1
|
||||
firstWord := notSeen
|
||||
lineStart := 0
|
||||
lineEnd := 0
|
||||
lastWord := 0
|
||||
lastNonLineBreak := notSeen
|
||||
nLines := 0
|
||||
firstLine := true
|
||||
|
||||
newline := func (wrapped bool) {
|
||||
// if the line isn't empty
|
||||
if lineStart != lineEnd {
|
||||
// align line
|
||||
alignLine (
|
||||
tokens[lineStart:lineEnd],
|
||||
size.X, xAlign, lineEnd == len(tokens))
|
||||
|
||||
// calculate extents
|
||||
var lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6
|
||||
|
||||
lineMinSpace = tokens[lineStart].position.X
|
||||
if firstWord == notSeen {
|
||||
lineMin = lineMinSpace
|
||||
} else {
|
||||
lineMin = tokens[firstWord].position.X
|
||||
}
|
||||
|
||||
lastWordTok := tokens[lastWord]
|
||||
if lastWordTok.kind == tokenKindWord {
|
||||
// the line had a word in it
|
||||
lineMax = lastWordTok.position.X + lastWordTok.width
|
||||
}
|
||||
if wrapped || lastNonLineBreak == notSeen {
|
||||
lineMaxSpace = lineMax
|
||||
} else {
|
||||
lastTokenTok := tokens[lastNonLineBreak]
|
||||
lineMaxSpace = lastTokenTok.position.X + lastTokenTok.width
|
||||
}
|
||||
// println(lineMax.String(), lineMaxSpace.String())
|
||||
|
||||
if lineMin < extents.Min.X || firstLine { extents.Min.X = lineMin }
|
||||
if lineMinSpace < extentsSpace.Min.X || firstLine { extentsSpace.Min.X = lineMinSpace }
|
||||
if lineMax > extents.Max.X { extents.Max.X = lineMax }
|
||||
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
|
||||
firstLine = false
|
||||
}
|
||||
|
||||
// update dot
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
|
||||
// update indices, counts
|
||||
lineStart = lineEnd
|
||||
lastWord = lineEnd
|
||||
lastNonLineBreak = notSeen
|
||||
firstWord = notSeen
|
||||
nLines ++
|
||||
}
|
||||
|
||||
// for each line, arrange and align while calculating effective
|
||||
// bounds/extents
|
||||
sawLineBreak := false
|
||||
for index, token := range tokens {
|
||||
lineEnd = index
|
||||
updateIndices := func () {
|
||||
if token.kind != tokenKindLineBreak {
|
||||
lastNonLineBreak = index
|
||||
}
|
||||
if token.kind == tokenKindWord {
|
||||
lastWord = index
|
||||
if firstWord == notSeen {
|
||||
firstWord = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// demarcate lines
|
||||
if sawLineBreak {
|
||||
newline(false)
|
||||
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(true)
|
||||
}
|
||||
updateIndices()
|
||||
tokens[index].position = dot
|
||||
dot.X += token.width
|
||||
}
|
||||
}
|
||||
lineEnd ++ // make lineEnd equal to len(tokens)
|
||||
newline(false)
|
||||
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
|
||||
|
||||
// second, vertical alignment pass
|
||||
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
|
||||
|
||||
// calculate extents
|
||||
extentsVerticalOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
|
||||
extents.Max.Y = dot.Y + metrics.Descent
|
||||
extentsSpace.Max.Y = dot.Y + metrics.Descent
|
||||
minimumSize.X = fixedRectDx(extents)
|
||||
minimumSize.Y = fixedRectDy(extents)
|
||||
extents = extents.Sub(extentsVerticalOffset)
|
||||
extentsSpace = extentsSpace.Sub(extentsVerticalOffset)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func fixedRectDx (rect fixed.Rectangle26_6) fixed.Int26_6 {
|
||||
return rect.Max.X - rect.Min.X
|
||||
}
|
||||
|
||||
func fixedRectDy (rect fixed.Rectangle26_6) fixed.Int26_6 {
|
||||
return rect.Max.Y - rect.Min.Y
|
||||
}
|
||||
|
||||
func calculateLineExtents (
|
||||
firstWord, firstToken, lastWord, lastToken token,
|
||||
) (
|
||||
lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6,
|
||||
) {
|
||||
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
|
||||
}
|
||||
39
flow_test.go
Normal file
39
flow_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package typeset
|
||||
|
||||
import "testing"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
|
||||
func BenchmarkFlowLipsumLeftTop (benchmark *testing.B) {
|
||||
benchmarkFlow(benchmark, false, AlignStart, AlignStart)
|
||||
}
|
||||
|
||||
func BenchmarkFlowLipsumWrapLeftTop (benchmark *testing.B) {
|
||||
benchmarkFlow(benchmark, true, AlignStart, AlignStart)
|
||||
}
|
||||
|
||||
func BenchmarkFlowLipsumCenterTop (benchmark *testing.B) {
|
||||
benchmarkFlow(benchmark, false, AlignMiddle, AlignStart)
|
||||
}
|
||||
|
||||
func BenchmarkFlowLipsumWrapCenterTop (benchmark *testing.B) {
|
||||
benchmarkFlow(benchmark, true, AlignMiddle, AlignStart)
|
||||
}
|
||||
|
||||
func BenchmarkFlowLipsumWrapJustifyTop (benchmark *testing.B) {
|
||||
benchmarkFlow(benchmark, true, AlignEven, AlignStart)
|
||||
}
|
||||
|
||||
func benchmarkFlow (benchmark *testing.B, wrap bool, xAlign, yAlign Align) {
|
||||
_, tokens := parseString(lipsumLt)
|
||||
measure(tokens, basicfont.Face7x13)
|
||||
benchmark.ReportAllocs()
|
||||
benchmark.ResetTimer()
|
||||
for i := 0; i < benchmark.N; i ++ {
|
||||
reflow (
|
||||
tokens,
|
||||
basicfont.Face7x13,
|
||||
fixed.P(256, 256),
|
||||
wrap, xAlign, yAlign)
|
||||
}
|
||||
}
|
||||
45
measure.go
Normal file
45
measure.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package typeset
|
||||
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
|
||||
func measure (tokens []token, face font.Face) {
|
||||
var lastRune rune
|
||||
for index, token := range tokens {
|
||||
var x fixed.Int26_6
|
||||
for index, runl := range token.runes {
|
||||
advance, ok := face.GlyphAdvance(runl.run)
|
||||
if !ok { advance = tofuAdvance(face) }
|
||||
advance += face.Kern(lastRune, runl.run)
|
||||
|
||||
runl.x = x
|
||||
x += advance
|
||||
lastRune = runl.run
|
||||
token.runes[index] = runl
|
||||
}
|
||||
token.width = x
|
||||
tokens[index] = token
|
||||
}
|
||||
}
|
||||
|
||||
const tofuStandinRune = 'M'
|
||||
const fallbackTofuAdvance = 16
|
||||
const fallbackTofuWidth = 14
|
||||
const fallbackTofuAscend = 16
|
||||
|
||||
func tofuAdvance (face font.Face) fixed.Int26_6 {
|
||||
if advance, ok := face.GlyphAdvance(tofuStandinRune); ok {
|
||||
return advance
|
||||
} else {
|
||||
return fallbackTofuAdvance
|
||||
}
|
||||
}
|
||||
|
||||
func tofuBounds (face font.Face) (fixed.Rectangle26_6, fixed.Int26_6) {
|
||||
if bounds, advance, ok := face.GlyphBounds(tofuStandinRune); ok {
|
||||
return bounds, advance
|
||||
} else {
|
||||
return fixed.R(0, -fallbackTofuAscend, fallbackTofuWidth, 0),
|
||||
fallbackTofuAdvance
|
||||
}
|
||||
}
|
||||
70
measure_test.go
Normal file
70
measure_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package typeset
|
||||
|
||||
import "testing"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
|
||||
const basicfontFace7x13advance = 7
|
||||
|
||||
func tkw (kind tokenKind, value string, width fixed.Int26_6) token {
|
||||
tok := tk(kind, value)
|
||||
tok.width = width
|
||||
for index, runl := range tok.runes {
|
||||
runl.x = fixed.I(basicfontFace7x13advance * index)
|
||||
tok.runes[index] = runl
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func TestMeasure (test *testing.T) {
|
||||
// ---- processing ----
|
||||
tokens := []token {
|
||||
tk(tokenKindWord, "hello"),
|
||||
tk(tokenKindSpace, " "),
|
||||
tk(tokenKindWord, "\rworld!"),
|
||||
tk(tokenKindLineBreak, "\n"),
|
||||
tk(tokenKindWord, "foo"),
|
||||
tk(tokenKindLineBreak, "\n"),
|
||||
tk(tokenKindLineBreak, "\r\n"),
|
||||
tk(tokenKindWord, "bar"),
|
||||
tk(tokenKindTab, "\t"),
|
||||
tk(tokenKindWord, "baz"),
|
||||
tk(tokenKindTab, "\t\t"),
|
||||
tk(tokenKindWord, "something"),
|
||||
}
|
||||
measure(tokens, basicfont.Face7x13)
|
||||
|
||||
// ---- correct data ----
|
||||
correctTokens := []token {
|
||||
tkw(tokenKindWord, "hello", fixed.I(35)),
|
||||
tkw(tokenKindSpace, " ", fixed.I( 7)),
|
||||
tkw(tokenKindWord, "\rworld!", fixed.I(49)),
|
||||
tkw(tokenKindLineBreak, "\n", fixed.I( 7)),
|
||||
tkw(tokenKindWord, "foo", fixed.I(21)),
|
||||
tkw(tokenKindLineBreak, "\n", fixed.I( 7)),
|
||||
tkw(tokenKindLineBreak, "\r\n", fixed.I(14)),
|
||||
tkw(tokenKindWord, "bar", fixed.I(21)),
|
||||
tkw(tokenKindTab, "\t", fixed.I( 7)),
|
||||
tkw(tokenKindWord, "baz", fixed.I(21)),
|
||||
tkw(tokenKindTab, "\t\t", fixed.I(14)),
|
||||
tkw(tokenKindWord, "something", fixed.I(63)),
|
||||
}
|
||||
|
||||
// ---- testing ----
|
||||
if len(tokens) != len(correctTokens) {
|
||||
test.Logf("len(tokens) != len(correctTokens): %d, %d", len(tokens), len(correctTokens))
|
||||
test.Log("GOT")
|
||||
logTokens(test, tokens)
|
||||
test.Log("CORRECT")
|
||||
logTokens(test, correctTokens)
|
||||
test.FailNow()
|
||||
}
|
||||
if !compareTokens(tokens, correctTokens) {
|
||||
test.Log("tokens != correctTokens:")
|
||||
test.Log("GOT")
|
||||
logTokens(test, tokens)
|
||||
test.Log("CORRECT")
|
||||
logTokens(test, correctTokens)
|
||||
test.FailNow()
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,11 @@ import "golang.org/x/image/math/fixed"
|
||||
type Align int
|
||||
|
||||
const (
|
||||
// AlignLeft aligns the start of each line to the beginning point
|
||||
// of each dot.
|
||||
AlignLeft Align = iota
|
||||
AlignRight
|
||||
AlignCenter
|
||||
AlignJustify
|
||||
// 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
|
||||
@@ -59,13 +58,16 @@ func DoWord (text []rune, face font.Face) (word WordLayout, remaining []rune) {
|
||||
|
||||
// consume and process the rune
|
||||
remaining = remaining[1:]
|
||||
_, advance, ok := face.GlyphBounds(char)
|
||||
if !ok { continue }
|
||||
word.Runes = append (word.Runes, RuneLayout {
|
||||
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 {
|
||||
@@ -114,12 +116,10 @@ type LineLayout struct {
|
||||
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 { }
|
||||
isFirstWord := true
|
||||
for {
|
||||
// process one word
|
||||
word, remainingFromWord := DoWord(remaining, face)
|
||||
word.X = x
|
||||
x += word.Width
|
||||
|
||||
// if we have gone over the preferred width, stop processing
|
||||
@@ -133,7 +133,6 @@ func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line
|
||||
|
||||
// if the word actually has contents, add it
|
||||
if word.Runes != nil {
|
||||
lastWord = word
|
||||
line.Words = append(line.Words, word)
|
||||
}
|
||||
|
||||
@@ -149,13 +148,13 @@ func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line
|
||||
}
|
||||
|
||||
// set the width of the line's content.
|
||||
line.ContentWidth = lastWord.X + lastWord.Width
|
||||
if wrap {
|
||||
line.Width = width
|
||||
} else {
|
||||
line.Width = line.ContentWidth
|
||||
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
|
||||
}
|
||||
line.SpaceAfter = lastWord.SpaceAfter
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,30 +171,45 @@ func (line *LineLayout) Length () int {
|
||||
|
||||
// Align aligns the text in the line according to the specified alignment
|
||||
// method.
|
||||
func (line *LineLayout) Align (align Align) {
|
||||
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
|
||||
}
|
||||
|
||||
if align == AlignJustify {
|
||||
line.justify()
|
||||
return
|
||||
}
|
||||
|
||||
leftOffset := -line.Words[0].X
|
||||
|
||||
if align == AlignCenter {
|
||||
leftOffset += (line.Width - line.ContentWidth) / 2
|
||||
} else if align == AlignRight {
|
||||
leftOffset += line.Width - line.ContentWidth
|
||||
}
|
||||
|
||||
for index := range line.Words {
|
||||
line.Words[index].X += leftOffset
|
||||
for index := range line.Words {
|
||||
line.Words[index].X += leftOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (line *LineLayout) justify () {
|
||||
if len(line.Words) < 2 {
|
||||
line.Align(AlignLeft)
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -206,11 +220,31 @@ func (line *LineLayout) justify () {
|
||||
trueContentWidth += word.Width
|
||||
}
|
||||
|
||||
spaceCount := fixed.Int26_6(len(line.Words) - 1)
|
||||
spacePerWord := (line.Width - trueContentWidth) / spaceCount
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@ type TypeSetter struct {
|
||||
layoutClean bool
|
||||
alignClean bool
|
||||
|
||||
align Align
|
||||
face font.Face
|
||||
maxWidth int
|
||||
maxHeight int
|
||||
wrap bool
|
||||
hAlign, vAlign Align
|
||||
face font.Face
|
||||
width, height int
|
||||
wrap bool
|
||||
tabWidth fixed.Int26_6
|
||||
|
||||
minWidth fixed.Int26_6
|
||||
layoutBounds image.Rectangle
|
||||
@@ -34,14 +34,15 @@ func (setter *TypeSetter) needLayout () {
|
||||
setter.layoutBounds = image.Rectangle { }
|
||||
setter.layoutBoundsSpace = image.Rectangle { }
|
||||
setter.minWidth = 0
|
||||
if setter.face == nil { 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)
|
||||
|
||||
|
||||
// function to add line and update bounds statistics
|
||||
addLine := func (line LineLayout) {
|
||||
line.Y = y
|
||||
y += metrics.Height
|
||||
@@ -59,7 +60,7 @@ func (setter *TypeSetter) needLayout () {
|
||||
for len(remaining) > 0 {
|
||||
line, remainingFromLine := DoLine (
|
||||
remaining, setter.face, setter.wrap,
|
||||
fixed.I(setter.maxWidth))
|
||||
fixed.I(setter.width))
|
||||
remaining = remainingFromLine
|
||||
addLine(line)
|
||||
}
|
||||
@@ -71,23 +72,22 @@ func (setter *TypeSetter) needLayout () {
|
||||
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
|
||||
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.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
|
||||
}
|
||||
@@ -97,15 +97,73 @@ func (setter *TypeSetter) needAlignedLayout () {
|
||||
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.align
|
||||
if align == AlignJustify {
|
||||
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 = AlignLeft }
|
||||
if except { align = AlignStart }
|
||||
}
|
||||
setter.lines[index].Align(align)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +175,11 @@ func (setter *TypeSetter) SetWrap (wrap bool) {
|
||||
}
|
||||
|
||||
// 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.hAlign == horizontal && setter.vAlign == vertical { return }
|
||||
setter.alignClean = false
|
||||
setter.align = align
|
||||
setter.hAlign = horizontal
|
||||
setter.vAlign = vertical
|
||||
}
|
||||
|
||||
// SetText sets the text content of the typesetter.
|
||||
@@ -138,25 +197,31 @@ func (setter *TypeSetter) SetFace (face font.Face) {
|
||||
setter.face = face
|
||||
}
|
||||
|
||||
// TODO rename to SetWidth and SetHeight
|
||||
|
||||
// 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) SetMaxWidth (width int) {
|
||||
if setter.maxWidth == width { return }
|
||||
func (setter *TypeSetter) SetWidth (width int) {
|
||||
if setter.width == width { return }
|
||||
setter.layoutClean = false
|
||||
setter.alignClean = false
|
||||
setter.maxWidth = width
|
||||
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) SetMaxHeight (heignt int) {
|
||||
if setter.maxHeight == heignt { return }
|
||||
func (setter *TypeSetter) SetHeight (heignt int) {
|
||||
if setter.height == heignt { return }
|
||||
setter.layoutClean = false
|
||||
setter.alignClean = false
|
||||
setter.maxHeight = heignt
|
||||
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
|
||||
@@ -175,12 +240,12 @@ func (setter *TypeSetter) LineHeight () fixed.Int26_6 {
|
||||
|
||||
// Width returns the height of the typesetter as set by SetWidth.
|
||||
func (setter *TypeSetter) Width () int {
|
||||
return setter.maxWidth
|
||||
return setter.width
|
||||
}
|
||||
|
||||
// Height returns the height of the typesetter as set by SetHeight.
|
||||
func (setter *TypeSetter) Height () int {
|
||||
return setter.maxHeight
|
||||
return setter.height
|
||||
}
|
||||
|
||||
// Face returns the TypeSetter's font face as set by SetFace.
|
||||
@@ -203,8 +268,18 @@ type RuneIterator func (
|
||||
)
|
||||
|
||||
// For calls the specified iterator for every rune in the typesetter. If the
|
||||
// iterator returns false, the loop will immediately stop.
|
||||
// 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
|
||||
@@ -215,7 +290,7 @@ func (setter *TypeSetter) For (iterator RuneIterator) {
|
||||
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 {
|
||||
keepGoing := iterator(index, char.Rune, fixed.Point26_6 {
|
||||
X: word.X + char.X,
|
||||
Y: line.Y,
|
||||
})
|
||||
@@ -224,21 +299,24 @@ func (setter *TypeSetter) For (iterator RuneIterator) {
|
||||
}}
|
||||
|
||||
if line.BreakAfter {
|
||||
keepGoing := iterator (index, '\n', fixed.Point26_6 {
|
||||
keepGoing := iterator(index, '\n', fixed.Point26_6 {
|
||||
X: lastCharRightBound,
|
||||
Y: line.Y,
|
||||
})
|
||||
if !keepGoing { return }
|
||||
index ++
|
||||
lastCharRightBound = fixed.Int26_6(0)
|
||||
}
|
||||
}
|
||||
|
||||
keepGoing := iterator (index, '\000', fixed.Point26_6 {
|
||||
X: lastCharRightBound,
|
||||
Y: lastLineY,
|
||||
})
|
||||
if !keepGoing { return }
|
||||
index ++
|
||||
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.
|
||||
@@ -317,10 +395,10 @@ func (setter *TypeSetter) MinimumSize () image.Point {
|
||||
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
|
||||
// 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) ReccomendedHeightFor (width int) (height int) {
|
||||
func (setter *TypeSetter) RecommendedHeight (width int) (height int) {
|
||||
setter.needLayout()
|
||||
|
||||
if setter.lines == nil { return }
|
||||
78
parse.go
Normal file
78
parse.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package typeset
|
||||
|
||||
import "unicode"
|
||||
|
||||
// TODO perhaps follow https://unicode.org/reports/tr14/
|
||||
|
||||
func parseString (text string) ([]runeLayout, []token) {
|
||||
// alloc initial rune slice
|
||||
runes := make([]runeLayout, 0, len(text) * 2 / 3)
|
||||
|
||||
// build the rune slice
|
||||
// we need to do this before parsing into tokens, because otherwise
|
||||
// a realloc will occur in the middle of it and the tokens at the start
|
||||
// will be referencing old memory
|
||||
for _, run := range text {
|
||||
runes = append(runes, runeLayout {
|
||||
run: run,
|
||||
})
|
||||
}
|
||||
|
||||
// alloc initial token slice
|
||||
tokens := make([]token, 0, len(runes) / 5)
|
||||
|
||||
var index int
|
||||
var startingIndex int
|
||||
var runl runeLayout
|
||||
var lastRune rune
|
||||
|
||||
var tok token
|
||||
tokenBoundary := func () {
|
||||
if startingIndex != index {
|
||||
tok.runes = runes[startingIndex:index]
|
||||
startingIndex = index
|
||||
tokens = append(tokens, tok)
|
||||
}
|
||||
tok = token { }
|
||||
}
|
||||
mustBeInToken := func (kind tokenKind) {
|
||||
if tok.kind != kind {
|
||||
tokenBoundary()
|
||||
tok.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
// parse tokens
|
||||
for index, runl = range runes {
|
||||
switch {
|
||||
case runl.run == '\r':
|
||||
tokenBoundary()
|
||||
// we don't know the token type yet. if next rune is a
|
||||
// \n then this is a CRLF line break. if not, this is
|
||||
// just a word.
|
||||
|
||||
case runl.run == '\n':
|
||||
if lastRune == '\r' {
|
||||
// continue the \r to make a CRLF line break
|
||||
tok.kind = tokenKindLineBreak
|
||||
} else {
|
||||
tokenBoundary()
|
||||
tok.kind = tokenKindLineBreak
|
||||
}
|
||||
|
||||
case runl.run == '\t':
|
||||
mustBeInToken(tokenKindTab)
|
||||
|
||||
case unicode.IsSpace(runl.run):
|
||||
mustBeInToken(tokenKindSpace)
|
||||
|
||||
default:
|
||||
mustBeInToken(tokenKindWord)
|
||||
}
|
||||
lastRune = runl.run
|
||||
}
|
||||
index ++ // make index equal to len([]rune(text))
|
||||
|
||||
tokenBoundary()
|
||||
return runes, tokens
|
||||
}
|
||||
224
parse_test.go
Normal file
224
parse_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package typeset
|
||||
|
||||
import "slices"
|
||||
import "testing"
|
||||
|
||||
func rl (run rune) runeLayout { return runeLayout { run: run } }
|
||||
func tk (kind tokenKind, value string) token {
|
||||
tok := token {
|
||||
kind: kind,
|
||||
}
|
||||
runeValue := []rune(value)
|
||||
tok.runes = make([]runeLayout, len(runeValue))
|
||||
for index, run := range runeValue {
|
||||
tok.runes[index] = rl(run)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
func compareTokens (got, correct []token) bool {
|
||||
for index, tok := range got {
|
||||
correctTok := correct[index]
|
||||
isCorrect :=
|
||||
correctTok.kind == tok.kind &&
|
||||
correctTok.width == tok.width &&
|
||||
slices.Equal(correctTok.runes, tok.runes)
|
||||
if !isCorrect { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
func logTokens (test *testing.T, tokens []token) {
|
||||
for _, token := range tokens {
|
||||
test.Logf("- %-40v | %v", token, token.runes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseString (test *testing.T) {
|
||||
// ---- processing ----
|
||||
runes, tokens := parseString("hello \rworld!\nfoo\n\r\nbar\tbaz\t\tsomething")
|
||||
|
||||
// ---- correct data ----
|
||||
correctRunes := []runeLayout {
|
||||
rl('h'),
|
||||
rl('e'),
|
||||
rl('l'),
|
||||
rl('l'),
|
||||
rl('o'),
|
||||
rl(' '),
|
||||
rl('\r'),
|
||||
rl('w'),
|
||||
rl('o'),
|
||||
rl('r'),
|
||||
rl('l'),
|
||||
rl('d'),
|
||||
rl('!'),
|
||||
rl('\n'),
|
||||
rl('f'),
|
||||
rl('o'),
|
||||
rl('o'),
|
||||
rl('\n'),
|
||||
rl('\r'),
|
||||
rl('\n'),
|
||||
rl('b'),
|
||||
rl('a'),
|
||||
rl('r'),
|
||||
rl('\t'),
|
||||
rl('b'),
|
||||
rl('a'),
|
||||
rl('z'),
|
||||
rl('\t'),
|
||||
rl('\t'),
|
||||
rl('s'),
|
||||
rl('o'),
|
||||
rl('m'),
|
||||
rl('e'),
|
||||
rl('t'),
|
||||
rl('h'),
|
||||
rl('i'),
|
||||
rl('n'),
|
||||
rl('g'),
|
||||
}
|
||||
correctTokens := []token {
|
||||
tk(tokenKindWord, "hello"),
|
||||
tk(tokenKindSpace, " "),
|
||||
tk(tokenKindWord, "\rworld!"),
|
||||
tk(tokenKindLineBreak, "\n"),
|
||||
tk(tokenKindWord, "foo"),
|
||||
tk(tokenKindLineBreak, "\n"),
|
||||
tk(tokenKindLineBreak, "\r\n"),
|
||||
tk(tokenKindWord, "bar"),
|
||||
tk(tokenKindTab, "\t"),
|
||||
tk(tokenKindWord, "baz"),
|
||||
tk(tokenKindTab, "\t\t"),
|
||||
tk(tokenKindWord, "something"),
|
||||
}
|
||||
|
||||
// ---- testing ----
|
||||
if len(runes) != len(correctRunes) {
|
||||
test.Logf("len(runes) != len(correctRunes): %d, %d", len(runes), len(correctRunes))
|
||||
test.Log(runes)
|
||||
test.Log(correctRunes)
|
||||
test.FailNow()
|
||||
}
|
||||
if !slices.Equal(runes, correctRunes) {
|
||||
test.Log("runes != correctRunes:")
|
||||
test.Log(runes)
|
||||
test.Log(correctRunes)
|
||||
test.FailNow()
|
||||
}
|
||||
if len(tokens) != len(correctTokens) {
|
||||
test.Logf("len(tokens) != len(correctTokens): %d, %d", len(tokens), len(correctTokens))
|
||||
test.Log("GOT")
|
||||
logTokens(test, tokens)
|
||||
test.Log("CORRECT")
|
||||
logTokens(test, correctTokens)
|
||||
test.FailNow()
|
||||
}
|
||||
if !compareTokens(tokens, correctTokens) {
|
||||
test.Log("tokens != correctTokens:")
|
||||
test.Log("GOT")
|
||||
logTokens(test, tokens)
|
||||
test.Log("CORRECT")
|
||||
logTokens(test, correctTokens)
|
||||
test.FailNow()
|
||||
}
|
||||
test.Logf("changing first rune from %c to x", runes[0].run)
|
||||
runes[0].run = 'x'
|
||||
test.Logf("first rune is now %c", runes[0].run)
|
||||
tokenRune := tokens[0].runes[0].run
|
||||
if tokenRune != 'x' {
|
||||
test.Fatalf (
|
||||
"tokens does not reference the same memory as runes after changing runes: %c, %c",
|
||||
runes[0].run, tokenRune)
|
||||
}
|
||||
runeIndex := 0
|
||||
for tokenIndex, token := range tokens {
|
||||
tokenRunePtr := &token.runes[0]
|
||||
runePtr := &runes[runeIndex]
|
||||
|
||||
if runePtr != tokenRunePtr {
|
||||
test.Fatalf (
|
||||
"tokens[%d] does not reference runes[%d]: %p, %p",
|
||||
tokenIndex, runeIndex, tokenRunePtr, runePtr)
|
||||
}
|
||||
|
||||
runeIndex += len(token.runes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseStringLatin (benchmark *testing.B) {
|
||||
benchmark.ReportAllocs()
|
||||
var rmeanLen, rmeanCap int
|
||||
var tmeanLen, tmeanCap int
|
||||
for i := 0; i < benchmark.N; i ++ {
|
||||
runes, tokens := parseString(lipsumLt)
|
||||
rmeanLen += len(runes)
|
||||
rmeanCap += cap(runes)
|
||||
tmeanLen += len(tokens)
|
||||
tmeanCap += cap(tokens)
|
||||
}
|
||||
rmeanLen /= benchmark.N
|
||||
rmeanCap /= benchmark.N
|
||||
tmeanLen /= benchmark.N
|
||||
tmeanCap /= benchmark.N
|
||||
benchmark.ReportMetric(float64(rmeanCap) / float64(rmeanLen), "rune-waste")
|
||||
benchmark.ReportMetric(float64(tmeanCap) / float64(tmeanLen), "token-waste")
|
||||
}
|
||||
|
||||
func BenchmarkParseStringChinese (benchmark *testing.B) {
|
||||
benchmark.ReportAllocs()
|
||||
var rmeanLen, rmeanCap int
|
||||
var tmeanLen, tmeanCap int
|
||||
for i := 0; i < benchmark.N; i ++ {
|
||||
runes, tokens := parseString(lipsumCn)
|
||||
rmeanLen += len(runes)
|
||||
rmeanCap += cap(runes)
|
||||
tmeanLen += len(tokens)
|
||||
tmeanCap += cap(tokens)
|
||||
}
|
||||
rmeanLen /= benchmark.N
|
||||
rmeanCap /= benchmark.N
|
||||
tmeanLen /= benchmark.N
|
||||
tmeanCap /= benchmark.N
|
||||
benchmark.ReportMetric(float64(rmeanCap) / float64(rmeanLen), "rune-waste")
|
||||
benchmark.ReportMetric(float64(tmeanCap) / float64(tmeanLen), "token-waste")
|
||||
}
|
||||
|
||||
const lipsumLt =
|
||||
`Voluptatem impedit id id facilis et. Sit eligendi aspernatur dicta vitae ipsa officia enim harum. Occaecati quod harum quos temporibus officiis provident enim neque. Odio totam ducimus commodi quis minima ea.
|
||||
|
||||
Ut delectus quis a rem consectetur laudantium hic sequi. Vel sunt neque nisi excepturi id sit id ut. Dolores expedita et odio. Quibusdam sed et quam nostrum. Sed perspiciatis voluptatibus et.
|
||||
|
||||
Omnis qui tempore corrupti alias ut repellendus est. A officiis molestias perspiciatis ut dolores nihil. Ut officiis hic quo aut aut dolorum. Modi at molestiae praesentium ea eveniet aut porro.
|
||||
|
||||
Similique facere cum amet nesciunt dolorem nemo. Rerum temporibus iure maiores. Facere quam nihil quia debitis nihil est officia aliquam. Magnam aut alias consectetur. Velit cumque eligendi assumenda magni ratione. Est dolorem modi a unde.
|
||||
|
||||
Illo reprehenderit est sunt quaerat cum nihil non. Quia nihil placeat qui ex hic molestiae eligendi. Asperiores optio et nobis et.`
|
||||
|
||||
const lipsumCn =
|
||||
`这很容易并且妨碍快乐。让我们很难为这些人选择上述生活的职责。他们被这样的事实蒙蔽了双眼:他们现在不提供办公室。我讨厌我们给小孩子们带来所有的好处。
|
||||
|
||||
作为被选中的人,他将跟随这里的赞美。或者除非他们被排除在外,否则他们不是。意想不到的痛苦和仇恨。但对某些人来说,以及我们自己。但让我们看看其中的乐趣和
|
||||
|
||||
当时所有腐败的人都必须被击退。办公室的麻烦被视为无痛。至于这里的服务无论是哪里还是让人心疼。但目前的麻烦将会发生或持续下去。
|
||||
|
||||
当没有人知道其中的痛苦时,也要做同样的事情。事物的时代确实更加伟大。无所作为,因为你不欠任何东西,这是一些责任。这将是伟大的或其他方面。无论他选择什么,他都必须非常小心。有一种痛从何而来。
|
||||
|
||||
他责怪他们在什么都没有的情况下才问。因为没有人喜欢从这里选择麻烦。对我们来说,这是一个更艰难的选择。
|
||||
|
||||
这很容易并且妨碍快乐。让我们很难为这些人选择上述生活的职责。他们被这样的事实蒙蔽了双眼:他们现在不提供办公室。我讨厌我们给小孩子们带来所有的好处。
|
||||
|
||||
作为被选中的人,他将跟随这里的赞美。或者除非他们被排除在外,否则他们不是。意想不到的痛苦和仇恨。但对某些人来说,以及我们自己。但让我们看看其中的乐趣和
|
||||
|
||||
当时所有腐败的人都必须被击退。办公室的麻烦被视为无痛。至于这里的服务无论是哪里还是让人心疼。但目前的麻烦将会发生或持续下去。
|
||||
|
||||
当没有人知道其中的痛苦时,也要做同样的事情。事物的时代确实更加伟大。无所作为,因为你不欠任何东西,这是一些责任。这将是伟大的或其他方面。无论他选择什么,他都必须非常小心。有一种痛从何而来。
|
||||
|
||||
他责怪他们在什么都没有的情况下才问。因为没有人喜欢从这里选择麻烦。对我们来说,这是一个更艰难的选择。
|
||||
|
||||
这很容易并且妨碍快乐。让我们很难为这些人选择上述生活的职责。他们被这样的事实蒙蔽了双眼:他们现在不提供办公室。我讨厌我们给小孩子们带来所有的好处。
|
||||
|
||||
作为被选中的人,他将跟随这里的赞美。或者除非他们被排除在外,否则他们不是。意想不到的痛苦和仇恨。但对某些人来说,以及我们自己。但让我们看看其中的乐趣和
|
||||
|
||||
当时所有腐败的人都必须被击退。办公室的麻烦被视为无痛。至于这里的服务无论是哪里还是让人心疼。但目前的麻烦将会发生或持续下去。
|
||||
|
||||
当没有人知道其中的痛苦时,也要做同样的事情。事物的`
|
||||
37
recommend.go
Normal file
37
recommend.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package typeset
|
||||
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
|
||||
func recommendHeight (tokens []token, face font.Face, width fixed.Int26_6) fixed.Int26_6 {
|
||||
metrics := face.Metrics()
|
||||
var dot fixed.Point26_6
|
||||
|
||||
newline := func () {
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
}
|
||||
|
||||
sawLineBreak := false
|
||||
for _, token := range tokens {
|
||||
// demarcate lines
|
||||
if sawLineBreak {
|
||||
newline()
|
||||
sawLineBreak = false
|
||||
}
|
||||
if token.kind == tokenKindLineBreak {
|
||||
sawLineBreak = true
|
||||
} else {
|
||||
needWrap :=
|
||||
token.kind == tokenKindWord &&
|
||||
dot.X + token.width > width
|
||||
if needWrap {
|
||||
newline()
|
||||
}
|
||||
dot.X += token.width
|
||||
}
|
||||
}
|
||||
newline()
|
||||
|
||||
return dot.Y
|
||||
}
|
||||
257
typesetter.go
Normal file
257
typesetter.go
Normal file
@@ -0,0 +1,257 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// TODO: perhaps rename this to just "glyph"
|
||||
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 their positions.
|
||||
func (this *TypeSetter) Runes () RuneIter {
|
||||
this.needFlow()
|
||||
return func (yield func (fixed.Point26_6, rune) bool) {
|
||||
for _, tok := range this.tokens {
|
||||
for _, runl := range tok.runes {
|
||||
pos := tok.position
|
||||
pos.X += runl.x
|
||||
if !yield(pos, runl.run) { return }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunesWithNull returns an iterator for all runes in the TypeSetter, plus an
|
||||
// additional null rune at the end. This is useful for calculating the positions
|
||||
// of things.
|
||||
func (this *TypeSetter) RunesWithNull () RuneIter {
|
||||
this.needFlow()
|
||||
return func (yield func (fixed.Point26_6, rune) bool) {
|
||||
var tok token
|
||||
for _, tok = range this.tokens {
|
||||
for _, runl := range tok.runes {
|
||||
pos := tok.position
|
||||
pos.X += runl.x
|
||||
if !yield(pos, runl.run) { return }
|
||||
}
|
||||
}
|
||||
|
||||
pos := tok.position
|
||||
pos.X += tok.width
|
||||
yield(pos, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Face returns the font face as set by SetFace.
|
||||
func (this *TypeSetter) Face () font.Face {
|
||||
return this.face
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.RunesWithNull()(func (pos fixed.Point26_6, run rune) bool {
|
||||
if index == idx {
|
||||
position = pos
|
||||
return false
|
||||
}
|
||||
idx ++
|
||||
return true
|
||||
})
|
||||
return position
|
||||
}
|
||||
|
||||
// ReccomendedHeightFor returns the reccomended max height if the text were to
|
||||
// have its maximum width set to the given width. This does not actually move
|
||||
// any text, it only simulates it.
|
||||
func (this *TypeSetter) RecommendedHeight (width fixed.Int26_6) fixed.Int26_6 {
|
||||
this.needMeasurement()
|
||||
return recommendHeight(this.tokens, this.face, width)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// SetText sets the text of the TypeSetter.
|
||||
func (this *TypeSetter) SetText (text string) {
|
||||
if this.text == text { return }
|
||||
this.text = text
|
||||
this.invalidate(validationLevelTokens)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user