Compare commits
11 Commits
v0.8.0
...
0c9d50ebcd
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c9d50ebcd | |||
| 943fc57080 | |||
| 0592fe32b6 | |||
| 56cf7e3fb8 | |||
| 650ecf0c2e | |||
| aa00b93bd3 | |||
| 8a22afe95a | |||
| ba1438b700 | |||
| 2aa1d355ec | |||
| 6e3e288628 | |||
| f2da861f1b |
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
|
||||
19
README.md
19
README.md
@@ -2,7 +2,20 @@
|
||||
|
||||
[](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.
|
||||
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user