github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/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 }