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  }