4 Commits

3 changed files with 103 additions and 48 deletions

View File

@@ -1,5 +1,7 @@
# typeset
[![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.
The state of a text layout is stored in a TypeSetter, and it can be drawn to any

View File

@@ -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
@@ -150,11 +149,7 @@ 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
line.SpaceAfter = lastWord.SpaceAfter
return
}
@@ -174,17 +169,17 @@ func (line *LineLayout) Length () int {
// method.
func (line *LineLayout) Align (align Align) {
if len(line.Words) == 0 { return }
if align == AlignJustify {
if align == AlignEven {
line.justify()
return
}
leftOffset := -line.Words[0].X
if align == AlignCenter {
if align == AlignMiddle {
leftOffset += (line.Width - line.ContentWidth) / 2
} else if align == AlignRight {
} else if align == AlignEnd {
leftOffset += line.Width - line.ContentWidth
}
@@ -195,7 +190,7 @@ func (line *LineLayout) Align (align Align) {
func (line *LineLayout) justify () {
if len(line.Words) < 2 {
line.Align(AlignLeft)
line.Align(AlignStart)
return
}
@@ -206,8 +201,8 @@ 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

118
setter.go
View File

@@ -14,11 +14,10 @@ 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
minWidth fixed.Int26_6
layoutBounds image.Rectangle
@@ -34,14 +33,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 +59,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 +71,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,18 +96,76 @@ 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 }
}
// align line
setter.lines[index].Align(align)
}
}
// 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()
}
}
// SetWrap sets whether or not the text wraps around and forms new lines.
func (setter *TypeSetter) SetWrap (wrap bool) {
if setter.wrap == wrap { return }
@@ -118,9 +175,10 @@ func (setter *TypeSetter) SetWrap (wrap bool) {
// SetAlign sets the alignment method of the typesetter.
func (setter *TypeSetter) SetAlign (horizontal, vertical Align) {
if setter.align == horizontal { return }
if setter.hAlign == horizontal && setter.vAlign == vertical { return }
setter.alignClean = false
setter.align = horizontal
setter.hAlign = horizontal
setter.vAlign = vertical
}
// SetText sets the text content of the typesetter.
@@ -141,20 +199,20 @@ func (setter *TypeSetter) SetFace (face font.Face) {
// 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) SetWidth (width int) {
if setter.maxWidth == width { return }
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) SetHeight (heignt int) {
if setter.maxHeight == heignt { return }
if setter.height == heignt { return }
setter.layoutClean = false
setter.alignClean = false
setter.maxHeight = heignt
setter.height = heignt
}
// Em returns the width of one emspace according to the typesetter's font, which
@@ -173,12 +231,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.