Compare commits

..

10 Commits
v0.7.1 ... main

4 changed files with 137 additions and 49 deletions

View File

@ -5,4 +5,4 @@
Typeset provides utilities for text layout, wrapping, and rendering.
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.
draw.Image using a Drawer which "extends" TypeSetter.

View File

@ -13,25 +13,31 @@ 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,
col color.Color,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
source := image.NewUniform(color)
source := image.NewUniform(col)
drawer.For (func (
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 (
fixed.P (
offset.X + position.X.Round(),
offset.Y + position.Y.Round()),
char)
if !ok || unicode.IsSpace(char) || char == 0 {
mask, maskPoint, _, ok := drawer.face.Glyph(dot, char)
// tofu
if !ok {
drawer.drawTofu(char, destination, col, dot)
return true
}
@ -50,3 +56,29 @@ func (drawer Drawer) Draw (
})
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)
}
}

View File

@ -58,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 {
@ -113,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
@ -132,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)
}
@ -148,9 +148,12 @@ 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
line.Width = width
line.SpaceAfter = lastWord.SpaceAfter
if len(line.Words) > 0 {
lastWord := line.Words[len(line.Words) - 1]
line.ContentWidth = x - lastWord.SpaceAfter
line.SpaceAfter = lastWord.SpaceAfter
}
return
}
@ -167,30 +170,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()
return
}
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
}
leftOffset := -line.Words[0].X
if align == AlignMiddle {
leftOffset += (line.Width - line.ContentWidth) / 2
} else if align == AlignEnd {
leftOffset += line.Width - line.ContentWidth
}
for index := range line.Words {
line.Words[index].X += leftOffset
for index := range line.Words {
line.Words[index].X += leftOffset
}
}
}
func (line *LineLayout) justify () {
if len(line.Words) < 2 {
line.Align(AlignStart)
// 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
}
@ -209,3 +227,19 @@ func (line *LineLayout) justify () {
x += spacePerWord + word.Width
}
}
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
}
}

View File

@ -18,6 +18,7 @@ type TypeSetter struct {
face font.Face
width, height int
wrap bool
tabWidth fixed.Int26_6
minWidth fixed.Int26_6
layoutBounds image.Rectangle
@ -117,7 +118,7 @@ func (setter *TypeSetter) alignHorizontally () {
}
// align line
setter.lines[index].Align(align)
setter.lines[index].Align(align, setter.tabWidth)
}
}
@ -215,6 +216,14 @@ func (setter *TypeSetter) SetHeight (heignt int) {
setter.height = heignt
}
// SetTabWidth sets the distance between tab stops.
func (setter *TypeSetter) SetTabWidth (tabWidth fixed.Int26_6) {
if setter.tabWidth == tabWidth { return }
setter.layoutClean = false
setter.alignClean = false
setter.tabWidth = tabWidth
}
// Em returns the width of one emspace according to the typesetter's font, which
// is the width of the capital letter 'M'.
func (setter *TypeSetter) Em () (width fixed.Int26_6) {
@ -259,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
@ -271,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,
})
@ -280,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.
@ -373,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 }