layout.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // Copyright 2023 The Ebitengine Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package text
  15. import (
  16. "strings"
  17. "github.com/hajimehoshi/ebiten/v2"
  18. "github.com/hajimehoshi/ebiten/v2/vector"
  19. )
  20. // Align is the alignment that determines how to put a text.
  21. type Align int
  22. const (
  23. AlignStart Align = iota
  24. AlignCenter
  25. AlignEnd
  26. )
  27. // DrawOptions represents options for the Draw function.
  28. //
  29. // DrawOption embeds ebiten.DrawImageOptions.
  30. // DrawImageOptions.GeoM is an additional geometry transformation
  31. // after putting the rendering region along with the specified alignments.
  32. // DrawImageOptions.ColorScale scales the text color.
  33. type DrawOptions struct {
  34. ebiten.DrawImageOptions
  35. LayoutOptions
  36. }
  37. // LayoutOptions represents options for layouting texts.
  38. //
  39. // PrimaryAlign and SecondaryAlign determine where to put the text in the given region at Draw.
  40. // Draw might render the text outside of the specified image bounds, so you might have to specify GeoM to make the text visible.
  41. type LayoutOptions struct {
  42. // LineSpacing is a distance between two adjacent lines's baselines.
  43. // The unit is in pixels.
  44. LineSpacing float64
  45. // PrimaryAlign is an alignment of the primary direction, in which a text in one line is rendered.
  46. // The primary direction is the horizontal direction for a horizontal-direction face,
  47. // and the vertical direction for a vertical-direction face.
  48. // The meaning of the start and the end depends on the face direction.
  49. PrimaryAlign Align
  50. // SecondaryAlign is an alignment of the secondary direction, in which multiple lines are rendered.
  51. // The secondary direction is the vertical direction for a horizontal-direction face,
  52. // and the horizontal direction for a vertical-direction face.
  53. // The meaning of the start and the end depends on the face direction.
  54. SecondaryAlign Align
  55. }
  56. // Draw draws a given text on a given destination image dst.
  57. // face is the font for text rendering.
  58. //
  59. // The '\n' newline character puts the following text on the next line.
  60. //
  61. // Glyphs used for rendering are cached in least-recently-used way.
  62. // Then old glyphs might be evicted from the cache.
  63. // As the cache capacity has limit, it is not guaranteed that all the glyphs for runes given at Draw are cached.
  64. //
  65. // It is OK to call Draw with a same text and a same face at every frame in terms of performance.
  66. //
  67. // Draw is concurrent-safe.
  68. //
  69. // # Rendering region
  70. //
  71. // A rectangle region where a text is put is called a 'rendering region'.
  72. // The position of the text in the rendering region is determined by the specified primary and secondary alignments.
  73. //
  74. // The actual rendering position of the rendering region depends on the alignments in DrawOptions.
  75. // By default, if the face's primary direction is left-to-right, the rendering region's upper-left position is (0, 0).
  76. // Note that this is different from text v1. In text v1, (0, 0) is always the origin position.
  77. //
  78. // # Alignments
  79. //
  80. // For horizontal directions, the start and end depends on the face.
  81. // If the face is GoTextFace, the start and the end depend on the Direction property.
  82. // If the face is GoXFace, the start and the end are always left and right respectively.
  83. //
  84. // For vertical directions, the start and end are top and bottom respectively.
  85. //
  86. // If the horizontal alignment is left, the rendering region's left X comes to the destination image's origin (0, 0).
  87. // If the horizontal alignment is center, the rendering region's middle X comes to the origin.
  88. // If the horizontal alignment is right, the rendering region's right X comes to the origin.
  89. //
  90. // If the vertical alignment is top, the rendering region's top Y comes to the destination image's origin (0, 0).
  91. // If the vertical alignment is center, the rendering region's middle Y comes to the origin.
  92. // If the vertical alignment is bottom, the rendering region's bottom Y comes to the origin.
  93. func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
  94. var layoutOp LayoutOptions
  95. var drawOp ebiten.DrawImageOptions
  96. if options != nil {
  97. layoutOp = options.LayoutOptions
  98. drawOp = options.DrawImageOptions
  99. }
  100. geoM := drawOp.GeoM
  101. for _, g := range AppendGlyphs(nil, text, face, &layoutOp) {
  102. if g.Image == nil {
  103. continue
  104. }
  105. drawOp.GeoM.Reset()
  106. drawOp.GeoM.Translate(g.X, g.Y)
  107. drawOp.GeoM.Concat(geoM)
  108. dst.DrawImage(g.Image, &drawOp)
  109. }
  110. }
  111. // AppendGlyphs appends glyphs to the given slice and returns a slice.
  112. //
  113. // AppendGlyphs is a low-level API, and you can use AppendGlyphs to have more control than Draw.
  114. // AppendGlyphs is also available to precache glyphs.
  115. //
  116. // For the details of options, see Draw function.
  117. //
  118. // AppendGlyphs is concurrent-safe.
  119. func AppendGlyphs(glyphs []Glyph, text string, face Face, options *LayoutOptions) []Glyph {
  120. return appendGlyphs(glyphs, text, face, 0, 0, options)
  121. }
  122. // AppndVectorPath appends a vector path for glyphs to the given path.
  123. //
  124. // AppendVectorPath works only when the face is *GoTextFace or a composite face using *GoTextFace so far.
  125. // For other types, AppendVectorPath does nothing.
  126. func AppendVectorPath(path *vector.Path, text string, face Face, options *LayoutOptions) {
  127. forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
  128. face.appendVectorPathForLine(path, line, originX, originY)
  129. })
  130. }
  131. // appendGlyphs appends glyphs to the given slice and returns a slice.
  132. //
  133. // appendGlyphs assumes the text is rendered with the position (x, y).
  134. // (x, y) might affect the subpixel rendering results.
  135. func appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options *LayoutOptions) []Glyph {
  136. forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
  137. glyphs = face.appendGlyphsForLine(glyphs, line, indexOffset, originX+x, originY+y)
  138. })
  139. return glyphs
  140. }
  141. // forEachLine interates lines.
  142. func forEachLine(text string, face Face, options *LayoutOptions, f func(text string, indexOffset int, originX, originY float64)) {
  143. if text == "" {
  144. return
  145. }
  146. if options == nil {
  147. options = &LayoutOptions{}
  148. }
  149. // Calculate the advances for each line.
  150. var advances []float64
  151. var longestAdvance float64
  152. var lineCount int
  153. for t := text; ; {
  154. lineCount++
  155. line, rest, found := strings.Cut(t, "\n")
  156. a := face.advance(line)
  157. advances = append(advances, a)
  158. if longestAdvance < a {
  159. longestAdvance = a
  160. }
  161. if !found {
  162. break
  163. }
  164. t = rest
  165. }
  166. d := face.direction()
  167. m := face.Metrics()
  168. var boundaryWidth, boundaryHeight float64
  169. if d.isHorizontal() {
  170. boundaryWidth = longestAdvance
  171. boundaryHeight = float64(lineCount-1)*options.LineSpacing + m.HAscent + m.HDescent
  172. } else {
  173. // TODO: Perhaps HAscent and HDescent should be used for sideways glyphs.
  174. boundaryWidth = float64(lineCount-1)*options.LineSpacing + m.VAscent + m.VDescent
  175. boundaryHeight = longestAdvance
  176. }
  177. var offsetX, offsetY float64
  178. // Adjust the offset based on the secondary alignments.
  179. h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
  180. switch d {
  181. case DirectionLeftToRight, DirectionRightToLeft:
  182. offsetY += m.HAscent
  183. switch v {
  184. case verticalAlignTop:
  185. case verticalAlignCenter:
  186. offsetY -= boundaryHeight / 2
  187. case verticalAlignBottom:
  188. offsetY -= boundaryHeight
  189. }
  190. case DirectionTopToBottomAndLeftToRight:
  191. // TODO: Perhaps HDescent should be used for sideways glyphs.
  192. offsetX += m.VDescent
  193. switch h {
  194. case horizontalAlignLeft:
  195. case horizontalAlignCenter:
  196. offsetX -= boundaryWidth / 2
  197. case horizontalAlignRight:
  198. offsetX -= boundaryWidth
  199. }
  200. case DirectionTopToBottomAndRightToLeft:
  201. // TODO: Perhaps HAscent should be used for sideways glyphs.
  202. offsetX -= m.VAscent
  203. switch h {
  204. case horizontalAlignLeft:
  205. offsetX += boundaryWidth
  206. case horizontalAlignCenter:
  207. offsetX += boundaryWidth / 2
  208. case horizontalAlignRight:
  209. }
  210. }
  211. var indexOffset int
  212. var originX, originY float64
  213. var i int
  214. for t := text; ; {
  215. line, rest, found := strings.Cut(t, "\n")
  216. // Adjust the origin position based on the primary alignments.
  217. switch d {
  218. case DirectionLeftToRight, DirectionRightToLeft:
  219. switch h {
  220. case horizontalAlignLeft:
  221. originX = 0
  222. case horizontalAlignCenter:
  223. originX = -advances[i] / 2
  224. case horizontalAlignRight:
  225. originX = -advances[i]
  226. }
  227. case DirectionTopToBottomAndLeftToRight, DirectionTopToBottomAndRightToLeft:
  228. switch v {
  229. case verticalAlignTop:
  230. originY = 0
  231. case verticalAlignCenter:
  232. originY = -advances[i] / 2
  233. case verticalAlignBottom:
  234. originY = -advances[i]
  235. }
  236. }
  237. f(line, indexOffset, originX+offsetX, originY+offsetY)
  238. if !found {
  239. break
  240. }
  241. t = rest
  242. indexOffset += len(line) + 1
  243. i++
  244. // Advance the origin position in the secondary direction.
  245. switch face.direction() {
  246. case DirectionLeftToRight:
  247. originY += options.LineSpacing
  248. case DirectionRightToLeft:
  249. originY += options.LineSpacing
  250. case DirectionTopToBottomAndLeftToRight:
  251. originX += options.LineSpacing
  252. case DirectionTopToBottomAndRightToLeft:
  253. originX -= options.LineSpacing
  254. }
  255. }
  256. }
  257. type horizontalAlign int
  258. const (
  259. horizontalAlignLeft horizontalAlign = iota
  260. horizontalAlignCenter
  261. horizontalAlignRight
  262. )
  263. type verticalAlign int
  264. const (
  265. verticalAlignTop verticalAlign = iota
  266. verticalAlignCenter
  267. verticalAlignBottom
  268. )
  269. func calcAligns(direction Direction, primaryAlign, secondaryAlign Align) (horizontalAlign, verticalAlign) {
  270. var h horizontalAlign
  271. var v verticalAlign
  272. switch direction {
  273. case DirectionLeftToRight:
  274. switch primaryAlign {
  275. case AlignStart:
  276. h = horizontalAlignLeft
  277. case AlignCenter:
  278. h = horizontalAlignCenter
  279. case AlignEnd:
  280. h = horizontalAlignRight
  281. }
  282. switch secondaryAlign {
  283. case AlignStart:
  284. v = verticalAlignTop
  285. case AlignCenter:
  286. v = verticalAlignCenter
  287. case AlignEnd:
  288. v = verticalAlignBottom
  289. }
  290. case DirectionRightToLeft:
  291. switch primaryAlign {
  292. case AlignStart:
  293. h = horizontalAlignRight
  294. case AlignCenter:
  295. h = horizontalAlignCenter
  296. case AlignEnd:
  297. h = horizontalAlignLeft
  298. }
  299. switch secondaryAlign {
  300. case AlignStart:
  301. v = verticalAlignTop
  302. case AlignCenter:
  303. v = verticalAlignCenter
  304. case AlignEnd:
  305. v = verticalAlignBottom
  306. }
  307. case DirectionTopToBottomAndLeftToRight:
  308. switch primaryAlign {
  309. case AlignStart:
  310. v = verticalAlignTop
  311. case AlignCenter:
  312. v = verticalAlignCenter
  313. case AlignEnd:
  314. v = verticalAlignBottom
  315. }
  316. switch secondaryAlign {
  317. case AlignStart:
  318. h = horizontalAlignLeft
  319. case AlignCenter:
  320. h = horizontalAlignCenter
  321. case AlignEnd:
  322. h = horizontalAlignRight
  323. }
  324. case DirectionTopToBottomAndRightToLeft:
  325. switch primaryAlign {
  326. case AlignStart:
  327. v = verticalAlignTop
  328. case AlignCenter:
  329. v = verticalAlignCenter
  330. case AlignEnd:
  331. v = verticalAlignBottom
  332. }
  333. switch secondaryAlign {
  334. case AlignStart:
  335. h = horizontalAlignRight
  336. case AlignCenter:
  337. h = horizontalAlignCenter
  338. case AlignEnd:
  339. h = horizontalAlignLeft
  340. }
  341. }
  342. return h, v
  343. }