diff --git a/old/drawer.go b/old/drawer.go deleted file mode 100644 index 044fb1e..0000000 --- a/old/drawer.go +++ /dev/null @@ -1,84 +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, - 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) - } -} diff --git a/old/layout.go b/old/layout.go deleted file mode 100644 index 03cb9a3..0000000 --- a/old/layout.go +++ /dev/null @@ -1,250 +0,0 @@ -package typeset - -import "unicode" -import "golang.org/x/image/font" -import "golang.org/x/image/math/fixed" - -// 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 | evenly spaced -) - -// RuneLayout contains layout information for a single rune relative to its -// word. -type RuneLayout struct { - X fixed.Int26_6 - Width fixed.Int26_6 - Rune rune -} - -// WordLayout contains layout information for a single word relative to its -// line. -type WordLayout struct { - X fixed.Int26_6 - Width fixed.Int26_6 - SpaceAfter fixed.Int26_6 - Runes []RuneLayout -} - -// DoWord consumes exactly one word from the given string, and produces a word -// layout according to the given font. It returns the remaining text as well. -func DoWord (text []rune, face font.Face) (word WordLayout, remaining []rune) { - remaining = text - gettingSpace := false - x := fixed.Int26_6(0) - lastRune := rune(-1) - for _, char := range text { - // if we run into a line break, we must break out immediately - // because it is not DoWord's job to handle that. - if char == '\n' { break } - - // if we suddenly run into spaces, and then run into a word - // again, we must break out immediately. - if unicode.IsSpace(char) { - gettingSpace = true - } else if gettingSpace { - break - } - - // apply kerning - if lastRune >= 0 { x += face.Kern(lastRune, char) } - lastRune = char - - // consume and process the rune - remaining = remaining[1:] - 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 { - word.SpaceAfter += advance - } else { - word.Width += advance - } - x += advance - } - return -} - -// LastRune returns the last rune in the word. -func (word WordLayout) LastRune () rune { - if word.Runes == nil { - return -1 - } else { - return word.Runes[len(word.Runes) - 1].Rune - } -} - -// FirstRune returns the last rune in the word. -func (word WordLayout) FirstRune () rune { - if word.Runes == nil { - return -1 - } else { - return word.Runes[0].Rune - } -} - -// LineLayout contains layout information for a single line. -type LineLayout struct { - Y fixed.Int26_6 - Width fixed.Int26_6 - ContentWidth fixed.Int26_6 - SpaceAfter fixed.Int26_6 - Words []WordLayout - BreakAfter bool -} - -// DoLine consumes exactly one line from the given string, and produces a line -// layout according to the given font. It returns the remaining text as well. If -// wrap is set to true, this function will stop processing words once maxWidth -// is crossed. The word which would have crossed over the limit will not be -// processed. -func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line LineLayout, remaining []rune) { - remaining = text - x := fixed.Int26_6(0) - isFirstWord := true - for { - // process one word - word, remainingFromWord := DoWord(remaining, face) - x += word.Width - - // if we have gone over the preferred width, stop processing - // words (if wrap is enabled) - if !isFirstWord && wrap && x > width { - break - } - - x += word.SpaceAfter - remaining = remainingFromWord - - // if the word actually has contents, add it - if word.Runes != nil { - line.Words = append(line.Words, word) - } - - // if we have hit the end of the line, stop processing words - if len(remaining) == 0 { break } - if remaining[0] == '\n' { - line.BreakAfter = true - remaining = remaining[1:] - break - } - - isFirstWord = false - } - - // set the width of the line's content. - 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 - } - return -} - -// Length returns the amount of runes within the line, including the trailing -// line break if it exists. -func (line *LineLayout) Length () int { - lineSize := 0 - for _, word := range line.Words { - lineSize += len(word.Runes) - } - if line.BreakAfter { lineSize ++ } - return lineSize -} - -// Align aligns the text in the line according to the specified alignment -// method. -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 - } - - for index := range line.Words { - line.Words[index].X += leftOffset - } - } -} - -// 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 - } - - // We are going to be moving the words, so we can't take SpaceAfter into - // account. - trueContentWidth := fixed.Int26_6(0) - for _, word := range line.Words { - trueContentWidth += word.Width - } - - 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 - } -} diff --git a/old/setter.go b/old/setter.go deleted file mode 100644 index 6176a4f..0000000 --- a/old/setter.go +++ /dev/null @@ -1,428 +0,0 @@ -package typeset - -import "image" -import "golang.org/x/image/font" -import "golang.org/x/image/math/fixed" - -// TypeSetter manages several lines of text, and can perform layout operations -// on them. It automatically avoids performing redundant work. It has no -// constructor and its zero value can be used safely. -type TypeSetter struct { - lines []LineLayout - text []rune - - layoutClean bool - alignClean bool - - hAlign, vAlign Align - face font.Face - width, height int - wrap bool - tabWidth fixed.Int26_6 - - minWidth fixed.Int26_6 - layoutBounds image.Rectangle - layoutBoundsSpace image.Rectangle -} - -func (setter *TypeSetter) needLayout () { - if setter.layoutClean { return } - setter.layoutClean = true - setter.alignClean = false - - setter.lines = nil - setter.layoutBounds = image.Rectangle { } - setter.layoutBoundsSpace = image.Rectangle { } - setter.minWidth = 0 - 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 - if line.ContentWidth > horizontalExtent { - horizontalExtent = line.ContentWidth - } - lineWidthSpace := line.ContentWidth + line.SpaceAfter - if lineWidthSpace > horizontalExtentSpace { - horizontalExtentSpace = lineWidthSpace - } - setter.lines = append(setter.lines, line) - } - - // process every line until there are no more remaining runes - for len(remaining) > 0 { - line, remainingFromLine := DoLine ( - remaining, setter.face, setter.wrap, - fixed.I(setter.width)) - remaining = remainingFromLine - addLine(line) - } - - // if there were no lines processed or the last line has a break after - // it, add a blank line at the end - needBlankLine := - len(setter.lines) == 0 || - 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 - 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 -} - -func (setter *TypeSetter) needAlignedLayout () { - if setter.alignClean && setter.layoutClean { return } - 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.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 = AlignStart } - } - - // 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() - } -} - -// SetWrap sets whether or not the text wraps around and forms new lines. -func (setter *TypeSetter) SetWrap (wrap bool) { - if setter.wrap == wrap { return } - setter.layoutClean = false - setter.wrap = wrap -} - -// SetAlign sets the alignment method of the typesetter. -func (setter *TypeSetter) SetAlign (horizontal, vertical Align) { - if setter.hAlign == horizontal && setter.vAlign == vertical { return } - setter.alignClean = false - setter.hAlign = horizontal - setter.vAlign = vertical -} - -// SetText sets the text content of the typesetter. -func (setter *TypeSetter) SetText (text []rune) { - setter.layoutClean = false - setter.alignClean = false - setter.text = text -} - -// SetFace sets the font face of the typesetter. -func (setter *TypeSetter) SetFace (face font.Face) { - if setter.face == face { return } - setter.layoutClean = false - setter.alignClean = false - setter.face = 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.width == width { return } - setter.layoutClean = false - setter.alignClean = false - 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.height == heignt { return } - setter.layoutClean = false - setter.alignClean = false - 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) { - if setter.face == nil { return 0 } - width, _ = setter.face.GlyphAdvance('M') - return -} - -// LineHeight returns the height of one line according to the typesetter's font. -func (setter *TypeSetter) LineHeight () fixed.Int26_6 { - if setter.face == nil { return 0 } - return setter.face.Metrics().Height -} - -// Width returns the height of the typesetter as set by SetWidth. -func (setter *TypeSetter) Width () int { - return setter.width -} - -// Height returns the height of the typesetter as set by SetHeight. -func (setter *TypeSetter) Height () int { - return setter.height -} - -// Face returns the TypeSetter's font face as set by SetFace. -func (setter *TypeSetter) Face () font.Face { - return setter.face -} - -// Length returns the amount of runes in the typesetter. -func (setter *TypeSetter) Length () int { - return len(setter.text) -} - -// RuneIterator is a function that can iterate accross a typesetter's runes. -type RuneIterator func ( - index int, - char rune, - position fixed.Point26_6, -) ( - keepGoing bool, -) - -// For calls the specified iterator for every rune in the typesetter. If the -// 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 - lastLineY := fixed.Int26_6(0) - lastCharRightBound := fixed.Int26_6(0) - for _, line := range setter.lines { - lastLineY = line.Y - 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 { - X: word.X + char.X, - Y: line.Y, - }) - if !keepGoing { return } - index ++ - }} - - if line.BreakAfter { - keepGoing := iterator(index, '\n', fixed.Point26_6 { - X: lastCharRightBound, - Y: line.Y, - }) - if !keepGoing { return } - index ++ - lastCharRightBound = fixed.Int26_6(0) - } - } - - 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. -func (setter *TypeSetter) AtPosition (position fixed.Point26_6) (index int) { - setter.needAlignedLayout() - - if setter.lines == nil { return } - if setter.face == nil { return } - - // find the first line who's bottom bound is greater than position.Y. if - // we haven't found it, then dont set the line variable (defaults to the - // last line) - metrics := setter.face.Metrics() - lastLine := setter.lines[len(setter.lines) - 1] - for _, curLine := range setter.lines { - if curLine.Y + metrics.Descent > position.Y { - lastLine = curLine - break - } - - index += curLine.Length() - } - - if lastLine.Words == nil { return } - - // find the first rune who's right bound is greater than position.X. - for _, curWord := range lastLine.Words { - for _, curChar := range curWord.Runes { - x := curWord.X + curChar.X + curChar.Width - if x > position.X { goto foundRune } - index ++ - } - } - foundRune: - return -} - -// PositionAt returns the position of the rune at the specified index. -func (setter *TypeSetter) PositionAt (index int) (position fixed.Point26_6) { - setter.needAlignedLayout() - - setter.For (func (i int, r rune, p fixed.Point26_6) bool { - position = p - return i < index - }) - return -} - -// 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 (setter *TypeSetter) LayoutBounds () (image.Rectangle) { - setter.needLayout() - return setter.layoutBounds - -} - -// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the -// trailing whitespace at the end of each line (if it exists). -func (setter *TypeSetter) LayoutBoundsSpace () (image.Rectangle) { - setter.needLayout() - return setter.layoutBoundsSpace -} - -// MinimumSize returns the minimum width and height needed to display text. If -// wrapping is enabled, this method will return (Em(), 0) -func (setter *TypeSetter) MinimumSize () image.Point { - setter.needLayout() - if setter.wrap { - return image.Pt(setter.Em().Round(), 0) - } - - width := setter.minWidth - height := fixed.Int26_6(len(setter.lines)) * setter.LineHeight() - - return image.Pt(width.Round(), height.Round()) -} - -// 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) RecommendedHeight (width int) (height int) { - setter.needLayout() - - if setter.lines == nil { return } - if setter.face == nil { return } - - metrics := setter.face.Metrics() - dot := fixed.Point26_6 { X: 0, Y: metrics.Height } - firstWord := true - for _, line := range setter.lines { - for _, word := range line.Words { - if word.Width + dot.X > fixed.I(width) && !firstWord { - dot.Y += metrics.Height - dot.X = 0 - firstWord = true - } - dot.X += word.Width + word.SpaceAfter - firstWord = false - } - if line.BreakAfter { - dot.Y += metrics.Height - dot.X = 0 - firstWord = true - } - } - - return dot.Y.Round() -}