github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/renderer_bounds.go (about) 1 package etxt 2 3 import "golang.org/x/image/math/fixed" 4 5 import "github.com/Kintar/etxt/efixed" 6 7 // This file contains the definitions of the text and glyph 8 // bounding functions for Renderer objects. 9 10 // Get the dimensions of the area taken by the given text. 11 // Intuitively, this matches the shaded area that you see when 12 // highlighting or selecting text in browsers and text editors. 13 // 14 // The results are affected by the renderer's font, size, quantization 15 // mode, sizer and text direction. If the input text contains \n line 16 // breaks, then line height and line spacing will also affect the results. 17 // 18 // Notice that spilling (content falling outside the returned rect) 19 // is possible. In general it will be non-existent or very minor, but 20 // some fancy display or script fonts can really go to crazy places. 21 // You should also be careful with italics. 22 func (self *Renderer) SelectionRect(text string) RectSize { 23 // (notice that SelectionRect is different from the BoundString() methods 24 // offered by golang/x/image/font or ebiten/text, which give you a tight 25 // bounding box around the glyph segments composing the text instead 26 // through individual glyph control box union, very technical to 27 // understand in all its subtleties) 28 29 if text == "" { 30 return RectSize{} 31 } 32 width := fixed.Int26_6(0) 33 absX := fixed.Int26_6(0) 34 lineBreaksOnly := true 35 measureFn := 36 func(currentDot fixed.Point26_6, codePoint rune, _ GlyphIndex) { 37 absX = fixedAbs(currentDot.X) 38 if absX > width { 39 width = absX 40 } 41 if codePoint != '\n' { 42 lineBreaksOnly = false 43 } 44 } 45 46 // traverse the string 47 vAlign, hAlign := self.tempMeasuringStart() 48 dot := self.Traverse(text, fixed.Point26_6{}, measureFn) 49 absX = fixedAbs(dot.X) 50 if absX > width { 51 width = absX 52 } 53 self.tempMeasuringEnd(vAlign, hAlign) 54 55 // obtain height and return 56 if self.metrics == nil { 57 self.updateMetrics() 58 } 59 height := fixedAbs(dot.Y) 60 if !lineBreaksOnly { 61 height += self.metrics.Height 62 } 63 return RectSize{width, height} 64 } 65 66 // Same as [Renderer.SelectionRect](), but taking a slice of glyph indices 67 // instead of a string. 68 func (self *Renderer) SelectionRectGlyphs(glyphIndices []GlyphIndex) RectSize { 69 if len(glyphIndices) == 0 { 70 return RectSize{} 71 } 72 vAlign, hAlign := self.tempMeasuringStart() 73 dot := self.TraverseGlyphs(glyphIndices, fixed.Point26_6{}, func(fixed.Point26_6, GlyphIndex) {}) 74 self.tempMeasuringEnd(vAlign, hAlign) 75 if self.metrics == nil { 76 self.updateMetrics() 77 } 78 return RectSize{fixedAbs(dot.X), self.metrics.Height} 79 } 80 81 // During selection rect measuring, using certain aligns like the centered 82 // ones is extremely inefficient and pointless. So, we disable those 83 // temporarily and optimize a bit around it. In fact, now Traverse* 84 // operations depend on centered aligns being used in SelectionRect* 85 // methods (infinite recursive loops would happen otherwise). 86 func (self *Renderer) tempMeasuringStart() (VertAlign, HorzAlign) { 87 v, h := self.vertAlign, self.horzAlign 88 self.vertAlign = Baseline 89 self.horzAlign = Left 90 if self.direction == RightToLeft { 91 self.horzAlign = Right 92 } 93 return v, h 94 } 95 96 // The counterpart of tempMeasuringStart(). 97 func (self *Renderer) tempMeasuringEnd(origVertAlign VertAlign, origHorzAlign HorzAlign) { 98 self.vertAlign = origVertAlign 99 self.horzAlign = origHorzAlign 100 } 101 102 // Line breaks are always considered for height, whether they are leading, 103 // trailing, or even if the input text only contains line breaks. 104 func (self *Renderer) textHeight(text string) fixed.Int26_6 { 105 if text == "" { 106 return 0 107 } 108 109 // count line breaks 110 lineBreakCount := 0 111 for _, codePoint := range text { 112 if codePoint == '\n' { 113 lineBreakCount += 1 114 } 115 } 116 117 // handle simple no line breaks case 118 if self.metrics == nil { 119 self.updateMetrics() 120 } 121 if lineBreakCount == 0 { 122 return self.metrics.Height 123 } 124 125 // we have line breaks, get line advance 126 lineAdvance := self.GetLineAdvance() 127 if lineAdvance < 0 { 128 lineAdvance = -lineAdvance 129 } 130 131 lineAdvance = efixed.QuantizeFractUp(lineAdvance, self.vertQuantStep) 132 advance := fixed.Int26_6(lineBreakCount) * lineAdvance 133 return self.metrics.Height + advance 134 } 135 136 // --- helper methods --- 137 138 func fixedAbs(x fixed.Int26_6) fixed.Int26_6 { 139 if x >= 0 { 140 return x 141 } 142 return -x 143 }