main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. // Copyright 2017 The Ebiten 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 main
  15. import (
  16. "bytes"
  17. "image"
  18. "image/color"
  19. _ "image/png"
  20. "log"
  21. "strings"
  22. "golang.org/x/image/font/gofont/goregular"
  23. "github.com/hajimehoshi/ebiten/v2"
  24. "github.com/hajimehoshi/ebiten/v2/examples/resources/images"
  25. "github.com/hajimehoshi/ebiten/v2/inpututil"
  26. "github.com/hajimehoshi/ebiten/v2/text/v2"
  27. )
  28. const (
  29. uiFontSize = 12
  30. lineSpacingInPixels = 16
  31. )
  32. var (
  33. uiImage *ebiten.Image
  34. uiFaceSource *text.GoTextFaceSource
  35. )
  36. func init() {
  37. // Decode an image from the image file's byte slice.
  38. img, _, err := image.Decode(bytes.NewReader(images.UI_png))
  39. if err != nil {
  40. log.Fatal(err)
  41. }
  42. uiImage = ebiten.NewImageFromImage(img)
  43. }
  44. func init() {
  45. s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
  46. if err != nil {
  47. log.Fatal(err)
  48. }
  49. uiFaceSource = s
  50. }
  51. type imageType int
  52. const (
  53. imageTypeButton imageType = iota
  54. imageTypeButtonPressed
  55. imageTypeTextBox
  56. imageTypeVScrollBarBack
  57. imageTypeVScrollBarFront
  58. imageTypeCheckBox
  59. imageTypeCheckBoxPressed
  60. imageTypeCheckBoxMark
  61. )
  62. var imageSrcRects = map[imageType]image.Rectangle{
  63. imageTypeButton: image.Rect(0, 0, 16, 16),
  64. imageTypeButtonPressed: image.Rect(16, 0, 32, 16),
  65. imageTypeTextBox: image.Rect(0, 16, 16, 32),
  66. imageTypeVScrollBarBack: image.Rect(16, 16, 24, 32),
  67. imageTypeVScrollBarFront: image.Rect(24, 16, 32, 32),
  68. imageTypeCheckBox: image.Rect(0, 32, 16, 48),
  69. imageTypeCheckBoxPressed: image.Rect(16, 32, 32, 48),
  70. imageTypeCheckBoxMark: image.Rect(32, 32, 48, 48),
  71. }
  72. const (
  73. screenWidth = 640
  74. screenHeight = 480
  75. )
  76. type Input struct {
  77. mouseButtonState int
  78. }
  79. func drawNinePatches(dst *ebiten.Image, dstRect image.Rectangle, srcRect image.Rectangle) {
  80. srcX := srcRect.Min.X
  81. srcY := srcRect.Min.Y
  82. srcW := srcRect.Dx()
  83. srcH := srcRect.Dy()
  84. dstX := dstRect.Min.X
  85. dstY := dstRect.Min.Y
  86. dstW := dstRect.Dx()
  87. dstH := dstRect.Dy()
  88. op := &ebiten.DrawImageOptions{}
  89. for j := 0; j < 3; j++ {
  90. for i := 0; i < 3; i++ {
  91. op.GeoM.Reset()
  92. sx := srcX
  93. sy := srcY
  94. sw := srcW / 4
  95. sh := srcH / 4
  96. dx := 0
  97. dy := 0
  98. dw := sw
  99. dh := sh
  100. switch i {
  101. case 1:
  102. sx = srcX + srcW/4
  103. sw = srcW / 2
  104. dx = srcW / 4
  105. dw = dstW - 2*srcW/4
  106. case 2:
  107. sx = srcX + 3*srcW/4
  108. dx = dstW - srcW/4
  109. }
  110. switch j {
  111. case 1:
  112. sy = srcY + srcH/4
  113. sh = srcH / 2
  114. dy = srcH / 4
  115. dh = dstH - 2*srcH/4
  116. case 2:
  117. sy = srcY + 3*srcH/4
  118. dy = dstH - srcH/4
  119. }
  120. op.GeoM.Scale(float64(dw)/float64(sw), float64(dh)/float64(sh))
  121. op.GeoM.Translate(float64(dx), float64(dy))
  122. op.GeoM.Translate(float64(dstX), float64(dstY))
  123. dst.DrawImage(uiImage.SubImage(image.Rect(sx, sy, sx+sw, sy+sh)).(*ebiten.Image), op)
  124. }
  125. }
  126. }
  127. type Button struct {
  128. Rect image.Rectangle
  129. Text string
  130. mouseDown bool
  131. onPressed func(b *Button)
  132. }
  133. func (b *Button) Update() {
  134. if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
  135. x, y := ebiten.CursorPosition()
  136. if b.Rect.Min.X <= x && x < b.Rect.Max.X && b.Rect.Min.Y <= y && y < b.Rect.Max.Y {
  137. b.mouseDown = true
  138. } else {
  139. b.mouseDown = false
  140. }
  141. } else {
  142. if b.mouseDown {
  143. if b.onPressed != nil {
  144. b.onPressed(b)
  145. }
  146. }
  147. b.mouseDown = false
  148. }
  149. }
  150. func (b *Button) Draw(dst *ebiten.Image) {
  151. t := imageTypeButton
  152. if b.mouseDown {
  153. t = imageTypeButtonPressed
  154. }
  155. drawNinePatches(dst, b.Rect, imageSrcRects[t])
  156. op := &text.DrawOptions{}
  157. op.GeoM.Translate(float64(b.Rect.Min.X+b.Rect.Max.X)/2, float64(b.Rect.Min.Y+b.Rect.Max.Y)/2)
  158. op.ColorScale.ScaleWithColor(color.Black)
  159. op.LineSpacing = lineSpacingInPixels
  160. op.PrimaryAlign = text.AlignCenter
  161. op.SecondaryAlign = text.AlignCenter
  162. text.Draw(dst, b.Text, &text.GoTextFace{
  163. Source: uiFaceSource,
  164. Size: uiFontSize,
  165. }, op)
  166. }
  167. func (b *Button) SetOnPressed(f func(b *Button)) {
  168. b.onPressed = f
  169. }
  170. const VScrollBarWidth = 16
  171. type VScrollBar struct {
  172. X int
  173. Y int
  174. Height int
  175. thumbRate float64
  176. thumbOffset int
  177. dragging bool
  178. draggingStartOffset int
  179. draggingStartY int
  180. contentOffset int
  181. }
  182. func (v *VScrollBar) thumbSize() int {
  183. const minThumbSize = VScrollBarWidth
  184. r := v.thumbRate
  185. if r > 1 {
  186. r = 1
  187. }
  188. s := int(float64(v.Height) * r)
  189. if s < minThumbSize {
  190. return minThumbSize
  191. }
  192. return s
  193. }
  194. func (v *VScrollBar) thumbRect() image.Rectangle {
  195. if v.thumbRate >= 1 {
  196. return image.Rectangle{}
  197. }
  198. s := v.thumbSize()
  199. return image.Rect(v.X, v.Y+v.thumbOffset, v.X+VScrollBarWidth, v.Y+v.thumbOffset+s)
  200. }
  201. func (v *VScrollBar) maxThumbOffset() int {
  202. return v.Height - v.thumbSize()
  203. }
  204. func (v *VScrollBar) ContentOffset() int {
  205. return v.contentOffset
  206. }
  207. func (v *VScrollBar) Update(contentHeight int) {
  208. v.thumbRate = float64(v.Height) / float64(contentHeight)
  209. if !v.dragging && inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
  210. x, y := ebiten.CursorPosition()
  211. tr := v.thumbRect()
  212. if tr.Min.X <= x && x < tr.Max.X && tr.Min.Y <= y && y < tr.Max.Y {
  213. v.dragging = true
  214. v.draggingStartOffset = v.thumbOffset
  215. v.draggingStartY = y
  216. }
  217. }
  218. if v.dragging {
  219. if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
  220. _, y := ebiten.CursorPosition()
  221. v.thumbOffset = v.draggingStartOffset + (y - v.draggingStartY)
  222. if v.thumbOffset < 0 {
  223. v.thumbOffset = 0
  224. }
  225. if v.thumbOffset > v.maxThumbOffset() {
  226. v.thumbOffset = v.maxThumbOffset()
  227. }
  228. } else {
  229. v.dragging = false
  230. }
  231. }
  232. v.contentOffset = 0
  233. if v.thumbRate < 1 {
  234. v.contentOffset = int(float64(contentHeight) * float64(v.thumbOffset) / float64(v.Height))
  235. }
  236. }
  237. func (v *VScrollBar) Draw(dst *ebiten.Image) {
  238. sd := image.Rect(v.X, v.Y, v.X+VScrollBarWidth, v.Y+v.Height)
  239. drawNinePatches(dst, sd, imageSrcRects[imageTypeVScrollBarBack])
  240. if v.thumbRate < 1 {
  241. drawNinePatches(dst, v.thumbRect(), imageSrcRects[imageTypeVScrollBarFront])
  242. }
  243. }
  244. const (
  245. textBoxPaddingLeft = 8
  246. textBoxPaddingTop = 4
  247. )
  248. type TextBox struct {
  249. Rect image.Rectangle
  250. Text string
  251. vScrollBar *VScrollBar
  252. offsetX int
  253. offsetY int
  254. }
  255. func (t *TextBox) AppendLine(line string) {
  256. if t.Text == "" {
  257. t.Text = line
  258. } else {
  259. t.Text += "\n" + line
  260. }
  261. }
  262. func (t *TextBox) Update() {
  263. if t.vScrollBar == nil {
  264. t.vScrollBar = &VScrollBar{}
  265. }
  266. t.vScrollBar.X = t.Rect.Max.X - VScrollBarWidth
  267. t.vScrollBar.Y = t.Rect.Min.Y
  268. t.vScrollBar.Height = t.Rect.Dy()
  269. _, h := t.contentSize()
  270. t.vScrollBar.Update(h)
  271. t.offsetX = 0
  272. t.offsetY = t.vScrollBar.ContentOffset()
  273. }
  274. func (t *TextBox) contentSize() (int, int) {
  275. h := len(strings.Split(t.Text, "\n"))*lineSpacingInPixels + textBoxPaddingTop
  276. return t.Rect.Dx(), h
  277. }
  278. func (t *TextBox) viewSize() (int, int) {
  279. return t.Rect.Dx() - VScrollBarWidth - textBoxPaddingLeft, t.Rect.Dy()
  280. }
  281. func (t *TextBox) contentOffset() (int, int) {
  282. return t.offsetX, t.offsetY
  283. }
  284. func (t *TextBox) Draw(dst *ebiten.Image) {
  285. drawNinePatches(dst, t.Rect, imageSrcRects[imageTypeTextBox])
  286. textOp := &text.DrawOptions{}
  287. x := -float64(t.offsetX) + textBoxPaddingLeft
  288. y := -float64(t.offsetY) + textBoxPaddingTop
  289. textOp.GeoM.Translate(x, y)
  290. textOp.GeoM.Translate(float64(t.Rect.Min.X), float64(t.Rect.Min.Y))
  291. textOp.ColorScale.ScaleWithColor(color.Black)
  292. textOp.LineSpacing = lineSpacingInPixels
  293. text.Draw(dst.SubImage(t.Rect).(*ebiten.Image), t.Text, &text.GoTextFace{
  294. Source: uiFaceSource,
  295. Size: uiFontSize,
  296. }, textOp)
  297. t.vScrollBar.Draw(dst)
  298. }
  299. const (
  300. checkboxWidth = 16
  301. checkboxHeight = 16
  302. checkboxPaddingLeft = 8
  303. )
  304. type CheckBox struct {
  305. X int
  306. Y int
  307. Text string
  308. checked bool
  309. mouseDown bool
  310. onCheckChanged func(c *CheckBox)
  311. }
  312. func (c *CheckBox) width() int {
  313. w := text.Advance(c.Text, &text.GoTextFace{
  314. Source: uiFaceSource,
  315. Size: uiFontSize,
  316. })
  317. return checkboxWidth + checkboxPaddingLeft + int(w)
  318. }
  319. func (c *CheckBox) Update() {
  320. if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
  321. x, y := ebiten.CursorPosition()
  322. if c.X <= x && x < c.X+c.width() && c.Y <= y && y < c.Y+checkboxHeight {
  323. c.mouseDown = true
  324. } else {
  325. c.mouseDown = false
  326. }
  327. } else {
  328. if c.mouseDown {
  329. c.checked = !c.checked
  330. if c.onCheckChanged != nil {
  331. c.onCheckChanged(c)
  332. }
  333. }
  334. c.mouseDown = false
  335. }
  336. }
  337. func (c *CheckBox) Draw(dst *ebiten.Image) {
  338. t := imageTypeCheckBox
  339. if c.mouseDown {
  340. t = imageTypeCheckBoxPressed
  341. }
  342. r := image.Rect(c.X, c.Y, c.X+checkboxWidth, c.Y+checkboxHeight)
  343. drawNinePatches(dst, r, imageSrcRects[t])
  344. if c.checked {
  345. drawNinePatches(dst, r, imageSrcRects[imageTypeCheckBoxMark])
  346. }
  347. x := c.X + checkboxWidth + checkboxPaddingLeft
  348. y := c.Y + checkboxHeight/2
  349. op := &text.DrawOptions{}
  350. op.GeoM.Translate(float64(x), float64(y))
  351. op.ColorScale.ScaleWithColor(color.Black)
  352. op.LineSpacing = lineSpacingInPixels
  353. op.PrimaryAlign = text.AlignStart
  354. op.SecondaryAlign = text.AlignCenter
  355. text.Draw(dst, c.Text, &text.GoTextFace{
  356. Source: uiFaceSource,
  357. Size: uiFontSize,
  358. }, op)
  359. }
  360. func (c *CheckBox) Checked() bool {
  361. return c.checked
  362. }
  363. func (c *CheckBox) SetOnCheckChanged(f func(c *CheckBox)) {
  364. c.onCheckChanged = f
  365. }
  366. type Game struct {
  367. button1 *Button
  368. button2 *Button
  369. checkBox *CheckBox
  370. textBoxLog *TextBox
  371. }
  372. func NewGame() *Game {
  373. g := &Game{}
  374. g.button1 = &Button{
  375. Rect: image.Rect(16, 16, 144, 48),
  376. Text: "Button 1",
  377. }
  378. g.button2 = &Button{
  379. Rect: image.Rect(160, 16, 288, 48),
  380. Text: "Button 2",
  381. }
  382. g.checkBox = &CheckBox{
  383. X: 16,
  384. Y: 64,
  385. Text: "Check Box!",
  386. }
  387. g.textBoxLog = &TextBox{
  388. Rect: image.Rect(16, 96, 624, 464),
  389. }
  390. g.button1.SetOnPressed(func(b *Button) {
  391. g.textBoxLog.AppendLine("Button 1 Pressed")
  392. })
  393. g.button2.SetOnPressed(func(b *Button) {
  394. g.textBoxLog.AppendLine("Button 2 Pressed")
  395. })
  396. g.checkBox.SetOnCheckChanged(func(c *CheckBox) {
  397. msg := "Check box check changed"
  398. if c.Checked() {
  399. msg += " (Checked)"
  400. } else {
  401. msg += " (Unchecked)"
  402. }
  403. g.textBoxLog.AppendLine(msg)
  404. })
  405. return g
  406. }
  407. func (g *Game) Update() error {
  408. g.button1.Update()
  409. g.button2.Update()
  410. g.checkBox.Update()
  411. g.textBoxLog.Update()
  412. return nil
  413. }
  414. func (g *Game) Draw(screen *ebiten.Image) {
  415. screen.Fill(color.RGBA{0xeb, 0xeb, 0xeb, 0xff})
  416. g.button1.Draw(screen)
  417. g.button2.Draw(screen)
  418. g.checkBox.Draw(screen)
  419. g.textBoxLog.Draw(screen)
  420. }
  421. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
  422. return screenWidth, screenHeight
  423. }
  424. func main() {
  425. ebiten.SetWindowSize(screenWidth, screenHeight)
  426. ebiten.SetWindowTitle("UI (Ebitengine Demo)")
  427. if err := ebiten.RunGame(NewGame()); err != nil {
  428. log.Fatal(err)
  429. }
  430. }