diff --git a/old/drawer.go b/old/drawer.go new file mode 100644 index 0000000..044fb1e --- /dev/null +++ b/old/drawer.go @@ -0,0 +1,84 @@ +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 new file mode 100644 index 0000000..03cb9a3 --- /dev/null +++ b/old/layout.go @@ -0,0 +1,250 @@ +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 new file mode 100644 index 0000000..6176a4f --- /dev/null +++ b/old/setter.go @@ -0,0 +1,428 @@ +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() +}