11 Commits

5 changed files with 83 additions and 29 deletions

12
.editorconfig Normal file
View 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

View File

@@ -2,7 +2,20 @@
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/typeset.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/typeset)
Typeset provides utilities for text layout, wrapping, and rendering.
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:
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.
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.

View File

@@ -116,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
@@ -135,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)
}
@@ -151,9 +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
line.Width = width
line.SpaceAfter = lastWord.SpaceAfter
// 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
}
return
}
@@ -170,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()
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
}
@@ -213,6 +229,10 @@ func (line *LineLayout) justify () {
}
}
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

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) {
@@ -386,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 }