github.com/kintar/etxt@v0.0.9/renderer_traverse.go (about)

     1  package etxt
     2  
     3  import "golang.org/x/image/math/fixed"
     4  
     5  import "github.com/kintar/etxt/efixed"
     6  
     7  // Returns a [Feed] object linked to the Renderer.
     8  //
     9  // Feeds are the lowest level mechanism to draw text in etxt, as they
    10  // expose and allow one to modify the drawing position manually.
    11  //
    12  // Unlike Traverse* methods, though, Feeds can't automatically align
    13  // text because the content to work with isn't known ahead of time.
    14  // Only vertical align will be applied to the starting position as if
    15  // the content had a single line.
    16  func (self *Renderer) NewFeed(xy fixed.Point26_6) *Feed {
    17  	xy.Y = self.alignGlyphsDotY(xy.Y)
    18  	return &Feed{renderer: self, Position: xy, LineBreakX: xy.X}
    19  }
    20  
    21  // During traversal processes, having both the current and previous
    22  // glyph is important in order to be able to apply kerning.
    23  type glyphPair struct {
    24  	// Current glyph index.
    25  	CurrentIndex GlyphIndex
    26  
    27  	// Previous glyph index.
    28  	PreviousIndex GlyphIndex
    29  
    30  	// Whether the PreviousIndex is valid and can be used
    31  	// for purposes like kerning.
    32  	HasPrevious bool
    33  }
    34  
    35  // Low-level method that can be used to implement your own drawing and
    36  // bounding operations.
    37  //
    38  // The given function is called for each character in the input string.
    39  // On line breaks, the function is called with '\n' and glyph index 0.
    40  //
    41  // The returned coordinates correspond to the baseline position of the
    42  // next glyph that would have to be drawn if the input was longer. The
    43  // coordinates are unquantized and haven't had kerning applied (as the
    44  // next glyph is not known yet). Notice that align and text direction
    45  // can affect the returned coordinate:
    46  //   - If [HorzAlign] is etxt.[Left], the returned coordinate will
    47  //     be on the right side of the last character drawn.
    48  //   - If [HorzAlign] is etxt.[Right], the returned coordinate will
    49  //     be on the left side of the last character drawn.
    50  //   - If [HorzAlign] is etxt.[XCenter], the returned coordinate will
    51  //     be on the right side of the last character drawn if the text direction
    52  //     is [LeftToRight], or on the left side otherwise.
    53  //
    54  // This returned coordinate can be useful when implementing bidirectional
    55  // text renderers, custom multi-style renderers and similar, though for
    56  // heterogeneous styling using a [Feed] is often more appropriate.
    57  func (self *Renderer) Traverse(text string, xy fixed.Point26_6, operation func(fixed.Point26_6, rune, GlyphIndex)) fixed.Point26_6 {
    58  	// NOTE: the spec says the returned coordinates are unquantized,
    59  	//       but the y will be quantized if another character has been
    60  	//       written earlier in the same line. So, it's more like
    61  	//       unquantized relative to the last position. This is expected
    62  	//       behavior, and I doubt anyone will care, but at least it's
    63  	//       written down.
    64  
    65  	if text == "" {
    66  		return xy
    67  	} // empty case
    68  
    69  	// prepare helper variables
    70  	hasPrevGlyph := false
    71  	previousIndex := GlyphIndex(0)
    72  	dir, traverseInReverse := self.traversalMode()
    73  	traverseFunc := self.getTraverseFunc(dir)
    74  	xy.Y = self.alignTextDotY(text, xy.Y)
    75  	lineHorzResetPoint := xy.X
    76  
    77  	// create iterator and adjust for centered align
    78  	iterator := newStrIterator(text, traverseInReverse)
    79  	if self.horzAlign == XCenter {
    80  		xy.X = self.centerLineStringX(iterator, lineHorzResetPoint, dir)
    81  	}
    82  	dot := xy
    83  	self.preFractPositionNotify(dot)
    84  
    85  	// iterate text code points
    86  	for {
    87  		codePoint := iterator.Next()
    88  		if codePoint == -1 {
    89  			return dot
    90  		} // end condition
    91  
    92  		// handle special line break case by moving the dot
    93  		if codePoint == '\n' {
    94  			dot.X = self.quantizeX(dot.X, dir)
    95  			if !hasPrevGlyph {
    96  				dot.Y = self.quantizeY(dot.Y)
    97  			}
    98  			operation(dot, '\n', 0)
    99  			dot.Y = self.applyLineAdvance(dot)
   100  			if self.horzAlign == XCenter {
   101  				dot.X = self.centerLineStringX(iterator, lineHorzResetPoint, dir)
   102  			} else {
   103  				dot.X = lineHorzResetPoint
   104  			}
   105  			hasPrevGlyph = false
   106  			continue
   107  		}
   108  
   109  		// quantize now (subtle consistency concerns)
   110  		if !hasPrevGlyph {
   111  			dot.X = self.quantizeX(dot.X, dir)
   112  			dot.Y = self.quantizeY(dot.Y)
   113  		}
   114  
   115  		// get the glyph index for the current character and traverse it
   116  		index := self.getGlyphIndex(codePoint)
   117  		dot = traverseFunc(dot, glyphPair{index, previousIndex, hasPrevGlyph},
   118  			func(opDot fixed.Point26_6) { operation(opDot, codePoint, index) })
   119  		hasPrevGlyph = true
   120  		previousIndex = index
   121  	}
   122  }
   123  
   124  // Same as [Renderer.Traverse](), but taking glyph indices instead of a string.
   125  // This method can be used as a building block for creating other methods or
   126  // types that operate with glyphs, as demonstrated by [eglyr.Renderer].
   127  //
   128  // This method is only relevant when working with complex scripts and using
   129  // [text shaping].
   130  //
   131  // [text shaping]: https://github.com/kintar/etxt/blob/main/docs/shaping.md
   132  // [eglyr.Renderer]: https://pkg.go.dev/github.com/kintar/etxt/eglyr#Renderer
   133  func (self *Renderer) TraverseGlyphs(glyphIndices []GlyphIndex, xy fixed.Point26_6, operation func(fixed.Point26_6, GlyphIndex)) fixed.Point26_6 {
   134  	if len(glyphIndices) == 0 {
   135  		return xy
   136  	} // empty case
   137  
   138  	// prepare helper variables, aligns, etc
   139  	previousIndex := GlyphIndex(0)
   140  	dir, traverseInReverse := self.traversalMode()
   141  	traverseFunc := self.getTraverseFunc(dir)
   142  	xy.Y = self.alignGlyphsDotY(xy.Y)
   143  	if self.horzAlign == XCenter { // consider xcenter align
   144  		hw := (self.SelectionRectGlyphs(glyphIndices).Width >> 1)
   145  		if dir == LeftToRight {
   146  			xy.X -= hw
   147  		} else {
   148  			xy.X += hw
   149  		}
   150  	}
   151  	self.preFractPositionNotify(xy)
   152  	xy.X = self.quantizeX(xy.X, dir)
   153  	xy.Y = self.quantizeY(xy.Y)
   154  	dot := xy
   155  
   156  	// iterate first glyph (with prevGlyph == false)
   157  	iterator := newGlyphsIterator(glyphIndices, traverseInReverse)
   158  	index, _ := iterator.Next()
   159  	dot = traverseFunc(dot, glyphPair{index, previousIndex, false},
   160  		func(opDot fixed.Point26_6) { operation(opDot, index) })
   161  	previousIndex = index
   162  
   163  	// iterate all remaining glyphs
   164  	for {
   165  		index, done := iterator.Next()
   166  		if done {
   167  			return dot
   168  		}
   169  		dot = traverseFunc(dot, glyphPair{index, previousIndex, true},
   170  			func(dot fixed.Point26_6) { operation(dot, index) })
   171  		previousIndex = index
   172  	}
   173  }
   174  
   175  // --- helper methods ---
   176  
   177  func (self *Renderer) quantizeY(y fixed.Int26_6) fixed.Int26_6 {
   178  	if self.GetLineAdvance() >= 0 {
   179  		return efixed.QuantizeFractUp(y, self.vertQuantStep)
   180  	} else {
   181  		return efixed.QuantizeFractDown(y, self.vertQuantStep)
   182  	}
   183  }
   184  
   185  func (self *Renderer) quantizeX(x fixed.Int26_6, dir Direction) fixed.Int26_6 {
   186  	if dir == LeftToRight {
   187  		return efixed.QuantizeFractUp(x, self.horzQuantStep)
   188  	} else { // RightToLeft
   189  		return efixed.QuantizeFractDown(x, self.horzQuantStep)
   190  	}
   191  }
   192  
   193  func (self *Renderer) centerLineStringX(iterator strIterator, lineHorzResetPoint fixed.Int26_6, dir Direction) fixed.Int26_6 {
   194  	line := iterator.UntilNextLineBreak()
   195  	halfWidth := (self.SelectionRect(line).Width >> 1)
   196  	if dir == LeftToRight {
   197  		return lineHorzResetPoint - halfWidth
   198  	} else {
   199  		return lineHorzResetPoint + halfWidth
   200  	}
   201  }
   202  
   203  // Notify fractional position change at the start of traversal.
   204  func (self *Renderer) preFractPositionNotify(dot fixed.Point26_6) {
   205  	if self.cacheHandler != nil {
   206  		if self.vertQuantStep != 64 {
   207  			self.cacheHandler.NotifyFractChange(dot) // only required for Y
   208  		} else {
   209  			// X is expected to be notified later too, so
   210  			// this works for QuantizeVert without issue
   211  			self.cacheHandler.NotifyFractChange(fixed.Point26_6{})
   212  		}
   213  	}
   214  }
   215  
   216  func (self *Renderer) getGlyphIndex(codePoint rune) GlyphIndex {
   217  	index, err := self.font.GlyphIndex(&self.buffer, codePoint)
   218  	if err != nil {
   219  		panic("font.GlyphIndex error: " + err.Error())
   220  	}
   221  	if index == 0 {
   222  		msg := "glyph index for '" + string(codePoint) + "' ["
   223  		msg += runeToUnicodeCode(codePoint) + "] missing"
   224  		panic(msg)
   225  	}
   226  	return index
   227  }
   228  
   229  // The returned bool is true when the traversal needs to be done
   230  // in reverse (from last line character to first).
   231  func (self *Renderer) traversalMode() (Direction, bool) {
   232  	switch self.horzAlign {
   233  	case Left:
   234  		return LeftToRight, (self.direction == RightToLeft)
   235  	case Right:
   236  		return RightToLeft, (self.direction == LeftToRight)
   237  	}
   238  	return self.direction, false
   239  }
   240  
   241  type traverseFuncType func(fixed.Point26_6, glyphPair, func(fixed.Point26_6)) fixed.Point26_6
   242  
   243  func (self *Renderer) getTraverseFunc(dir Direction) traverseFuncType {
   244  	if dir == LeftToRight {
   245  		return self.traverseGlyphLTR
   246  	}
   247  	return self.traverseGlyphRTL
   248  }
   249  
   250  func (self *Renderer) traverseGlyphLTR(dot fixed.Point26_6, glyphSeq glyphPair, operation func(fixed.Point26_6)) fixed.Point26_6 {
   251  	// kern
   252  	if glyphSeq.HasPrevious { // apply kerning
   253  		prev, curr := glyphSeq.PreviousIndex, glyphSeq.CurrentIndex
   254  		dot.X += self.sizer.Kern(self.font, prev, curr, self.sizePx)
   255  	}
   256  
   257  	// quantize
   258  	dot.X = efixed.QuantizeFractUp(dot.X, self.horzQuantStep)
   259  	if self.cacheHandler != nil && self.horzQuantStep != 64 {
   260  		self.cacheHandler.NotifyFractChange(dot)
   261  	}
   262  
   263  	// operate
   264  	operation(dot)
   265  
   266  	// advance
   267  	dot.X += self.sizer.Advance(self.font, glyphSeq.CurrentIndex, self.sizePx)
   268  	return dot
   269  }
   270  
   271  func (self *Renderer) traverseGlyphRTL(dot fixed.Point26_6, glyphSeq glyphPair, operation func(fixed.Point26_6)) fixed.Point26_6 {
   272  	// advance
   273  	dot.X -= self.sizer.Advance(self.font, glyphSeq.CurrentIndex, self.sizePx)
   274  
   275  	// kern and pad
   276  	if glyphSeq.HasPrevious {
   277  		prev, curr := glyphSeq.PreviousIndex, glyphSeq.CurrentIndex
   278  		dot.X -= self.sizer.Kern(self.font, curr, prev, self.sizePx)
   279  	}
   280  
   281  	// quantize position
   282  	dot.X = efixed.QuantizeFractDown(dot.X, self.horzQuantStep)
   283  	if self.cacheHandler != nil && self.horzQuantStep != 64 {
   284  		self.cacheHandler.NotifyFractChange(dot)
   285  	}
   286  
   287  	// operate
   288  	operation(dot)
   289  	return dot
   290  }
   291  
   292  // Apply line advance to the given coordinate applying quantization if
   293  // relevant, notifying the cache handler fractional pixel change, etc.
   294  func (self *Renderer) applyLineAdvance(dot fixed.Point26_6) fixed.Int26_6 {
   295  	// handle non-quantized case (notifying fractional position change)
   296  	if self.vertQuantStep != 64 {
   297  		dot.Y += self.GetLineAdvance()
   298  		if self.cacheHandler != nil {
   299  			self.cacheHandler.NotifyFractChange(dot)
   300  		}
   301  		return dot.Y
   302  	}
   303  
   304  	// handle quantized case (round but don't notify fractional change)
   305  	lineAdvance := self.GetLineAdvance()
   306  	if lineAdvance >= 0 {
   307  		return efixed.RoundHalfUp(dot.Y + lineAdvance)
   308  	} else {
   309  		return efixed.RoundHalfDown(dot.Y + lineAdvance)
   310  	}
   311  }