diff --git a/examples/_resources/images/ui.png b/examples/_resources/images/ui.png new file mode 100644 index 000000000..60cbdd9ec Binary files /dev/null and b/examples/_resources/images/ui.png differ diff --git a/examples/ui/main.go b/examples/ui/main.go new file mode 100644 index 000000000..bf9b9bc34 --- /dev/null +++ b/examples/ui/main.go @@ -0,0 +1,529 @@ +// Copyright 2017 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build example + +package main + +import ( + "image" + "image/color" + "log" + "strings" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/ebitenutil" + "github.com/hajimehoshi/ebiten/text" +) + +const ( + lineHeight = 16 +) + +var ( + uiImage *ebiten.Image + uiFont font.Face + uiFontMHeight int +) + +func init() { + var err error + uiImage, _, err = ebitenutil.NewImageFromFile("_resources/images/ui.png", ebiten.FilterNearest) + if err != nil { + log.Fatal(err) + } + + tt, err := truetype.Parse(goregular.TTF) + if err != nil { + log.Fatal(err) + } + uiFont = truetype.NewFace(tt, &truetype.Options{ + Size: 12, + DPI: 72, + Hinting: font.HintingFull, + }) + b, _, _ := uiFont.GlyphBounds('M') + uiFontMHeight = (b.Max.Y - b.Min.Y).Ceil() +} + +type imageType int + +const ( + imageTypeButton imageType = iota + imageTypeButtonPressed + imageTypeTextBox + imageTypeVScollBarBack + imageTypeVScollBarFront + imageTypeCheckBox + imageTypeCheckBoxPressed + imageTypeCheckBoxMark +) + +var imageSrcRects = map[imageType]image.Rectangle{ + imageTypeButton: image.Rect(0, 0, 16, 16), + imageTypeButtonPressed: image.Rect(16, 0, 32, 16), + imageTypeTextBox: image.Rect(0, 16, 16, 32), + imageTypeVScollBarBack: image.Rect(16, 16, 24, 32), + imageTypeVScollBarFront: image.Rect(24, 16, 32, 32), + imageTypeCheckBox: image.Rect(0, 32, 16, 48), + imageTypeCheckBoxPressed: image.Rect(16, 32, 32, 48), + imageTypeCheckBoxMark: image.Rect(32, 32, 48, 48), +} + +const ( + screenWidth = 640 + screenHeight = 480 +) + +type Input struct { + mouseButtonState int +} + +var theInput = &Input{} + +func (i *Input) Update() { + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + i.mouseButtonState++ + } else { + i.mouseButtonState = 0 + } +} + +func (i *Input) IsMouseButtonPressed() bool { + return i.mouseButtonState > 0 +} + +func (i *Input) IsMouseButtonTriggered() bool { + return i.mouseButtonState == 1 +} + +func drawNinePatches(dst *ebiten.Image, dstRect image.Rectangle, srcRect image.Rectangle) { + srcX := srcRect.Min.X + srcY := srcRect.Min.Y + srcW := srcRect.Dx() + srcH := srcRect.Dy() + + dstX := dstRect.Min.X + dstY := dstRect.Min.Y + dstW := dstRect.Dx() + dstH := dstRect.Dy() + + op := &ebiten.DrawImageOptions{} + for j := 0; j < 3; j++ { + for i := 0; i < 3; i++ { + op.GeoM.Reset() + + sx := srcX + sy := srcY + sw := srcW / 4 + sh := srcH / 4 + dx := 0 + dy := 0 + dw := sw + dh := sh + switch i { + case 1: + sx = srcX + srcW/4 + sw = srcW / 2 + dx = srcW / 4 + dw = dstW - 2*srcW/4 + case 2: + sx = srcX + 3*srcW/4 + dx = dstW - srcW/4 + } + switch j { + case 1: + sy = srcY + srcH/4 + sh = srcH / 2 + dy = srcH / 4 + dh = dstH - 2*srcH/4 + case 2: + sy = srcY + 3*srcH/4 + dy = dstH - srcH/4 + } + + op.GeoM.Scale(float64(dw)/float64(sw), float64(dh)/float64(sh)) + op.GeoM.Translate(float64(dx), float64(dy)) + op.GeoM.Translate(float64(dstX), float64(dstY)) + r := image.Rect(sx, sy, sx+sw, sy+sh) + op.SourceRect = &r + dst.DrawImage(uiImage, op) + } + } +} + +type Button struct { + X int + Y int + Width int + Height int + Text string + + mouseDown bool + pressed bool +} + +func (b *Button) Update() { + b.pressed = false + if theInput.IsMouseButtonPressed() { + x, y := ebiten.CursorPosition() + if b.X <= x && x < (b.X+b.Width) && b.Y <= y && y < (b.Y+b.Height) { + b.mouseDown = true + } else { + b.mouseDown = false + } + } else { + if b.mouseDown { + b.pressed = true + } + b.mouseDown = false + } +} + +func (b *Button) Draw(dst *ebiten.Image) { + t := imageTypeButton + if b.mouseDown { + t = imageTypeButtonPressed + } + r := image.Rect(b.X, b.Y, b.X+b.Width, b.Y+b.Height) + drawNinePatches(dst, r, imageSrcRects[t]) + + bounds, _ := font.BoundString(uiFont, b.Text) + w := (bounds.Max.X - bounds.Min.X).Ceil() + x := b.X + (b.Width-w)/2 + y := (b.Y + b.Height) - (b.Height-uiFontMHeight)/2 + text.Draw(dst, b.Text, uiFont, x, y, color.Black) +} + +func (b *Button) Pressed() bool { + return b.pressed +} + +const VScrollBarWidth = 16 + +type VScrollBar struct { + X int + Y int + Height int + + thumbRate float64 + thumbOffset int + dragging bool + draggingStartOffset int + draggingStartY int + contentOffset int +} + +func (v *VScrollBar) thumbSize() int { + const minThumbSize = VScrollBarWidth + + r := v.thumbRate + if r > 1 { + r = 1 + } + s := int(float64(v.Height) * r) + if s < minThumbSize { + return minThumbSize + } + return s +} + +func (v *VScrollBar) thumbRect() image.Rectangle { + if v.thumbRate >= 1 { + return image.Rectangle{} + } + + s := v.thumbSize() + return image.Rect(v.X, v.Y+v.thumbOffset, v.X+VScrollBarWidth, v.Y+v.thumbOffset+s) +} + +func (v *VScrollBar) maxThumbOffset() int { + return v.Height - v.thumbSize() +} + +func (v *VScrollBar) ContentOffset() int { + return v.contentOffset +} + +func (v *VScrollBar) Update(contentHeight int) { + v.thumbRate = float64(v.Height) / float64(contentHeight) + + if !v.dragging && theInput.IsMouseButtonTriggered() { + x, y := ebiten.CursorPosition() + tr := v.thumbRect() + if tr.Min.X <= x && x < tr.Max.X && tr.Min.Y <= y && y < tr.Max.Y { + v.dragging = true + v.draggingStartOffset = v.thumbOffset + v.draggingStartY = y + } + } + if v.dragging { + if theInput.IsMouseButtonPressed() { + _, y := ebiten.CursorPosition() + v.thumbOffset = v.draggingStartOffset + (y - v.draggingStartY) + if v.thumbOffset < 0 { + v.thumbOffset = 0 + } + if v.thumbOffset > v.maxThumbOffset() { + v.thumbOffset = v.maxThumbOffset() + } + } else { + v.dragging = false + } + } + + v.contentOffset = 0 + if v.thumbRate < 1 { + v.contentOffset = int(float64(contentHeight) * float64(v.thumbOffset) / float64(v.Height)) + } +} + +func (v *VScrollBar) Draw(dst *ebiten.Image) { + sd := image.Rect(v.X, v.Y, v.X+VScrollBarWidth, v.Y+v.Height) + drawNinePatches(dst, sd, imageSrcRects[imageTypeVScollBarBack]) + + if v.thumbRate < 1 { + drawNinePatches(dst, v.thumbRect(), imageSrcRects[imageTypeVScollBarFront]) + } +} + +const ( + textBoxPaddingLeft = 8 +) + +type TextBox struct { + X int + Y int + Width int + Height int + Text string + + contentBuf *ebiten.Image + vScrollBar *VScrollBar + offsetX int + offsetY int +} + +func (t *TextBox) AppendLine(line string) { + if t.Text == "" { + t.Text = line + } else { + t.Text += "\n" + line + } +} + +func (t *TextBox) Update() { + if t.vScrollBar == nil { + t.vScrollBar = &VScrollBar{} + } + t.vScrollBar.X = t.X + t.Width - VScrollBarWidth + t.vScrollBar.Y = t.Y + t.vScrollBar.Height = t.Height + + _, h := t.contentSize() + t.vScrollBar.Update(h) + + t.offsetX = 0 + t.offsetY = t.vScrollBar.ContentOffset() +} + +func (t *TextBox) contentSize() (int, int) { + h := len(strings.Split(t.Text, "\n")) * lineHeight + return t.Width, h +} + +func (t *TextBox) viewSize() (int, int) { + return t.Width - VScrollBarWidth - textBoxPaddingLeft, t.Height +} + +func (t *TextBox) contentOffset() (int, int) { + return t.offsetX, t.offsetY +} + +func (t *TextBox) Draw(dst *ebiten.Image) { + r := image.Rect(t.X, t.Y, t.X+t.Width, t.Y+t.Height) + drawNinePatches(dst, r, imageSrcRects[imageTypeTextBox]) + + if t.contentBuf != nil { + vw, vh := t.viewSize() + w, h := t.contentBuf.Size() + if vw > w || vh > h { + t.contentBuf.Dispose() + t.contentBuf = nil + } + } + if t.contentBuf == nil { + w, h := t.viewSize() + t.contentBuf, _ = ebiten.NewImage(w, h, ebiten.FilterNearest) + } + + t.contentBuf.Clear() + for i, line := range strings.Split(t.Text, "\n") { + x := -t.offsetX + textBoxPaddingLeft + y := -t.offsetY + i*lineHeight + lineHeight - (lineHeight-uiFontMHeight)/2 + if y < -lineHeight { + continue + } + _, h := t.viewSize() + if y >= h+lineHeight { + continue + } + text.Draw(t.contentBuf, line, uiFont, x, y, color.Black) + } + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(t.X), float64(t.Y)) + dst.DrawImage(t.contentBuf, op) + + t.vScrollBar.Draw(dst) +} + +const ( + checkboxWidth = 16 + checkboxHeight = 16 + checkboxPaddingLeft = 8 +) + +type CheckBox struct { + X int + Y int + Text string + + checked bool + mouseDown bool + checkChanged bool +} + +func (c *CheckBox) width() int { + b, _ := font.BoundString(uiFont, c.Text) + w := (b.Max.X - b.Min.X).Ceil() + return checkboxWidth + checkboxPaddingLeft + w +} + +func (c *CheckBox) Update() { + c.checkChanged = false + if theInput.IsMouseButtonPressed() { + x, y := ebiten.CursorPosition() + if c.X <= x && x < c.X+c.width() && c.Y <= y && y < c.Y+checkboxHeight { + c.mouseDown = true + } else { + c.mouseDown = false + } + } else { + if c.mouseDown { + c.checkChanged = true + } + c.mouseDown = false + } + if c.checkChanged { + c.checked = !c.checked + } +} + +func (c *CheckBox) Draw(dst *ebiten.Image) { + t := imageTypeCheckBox + if c.mouseDown { + t = imageTypeCheckBoxPressed + } + r := image.Rect(c.X, c.Y, c.X+checkboxWidth, c.Y+checkboxHeight) + drawNinePatches(dst, r, imageSrcRects[t]) + if c.checked { + drawNinePatches(dst, r, imageSrcRects[imageTypeCheckBoxMark]) + } + + x := c.X + checkboxWidth + checkboxPaddingLeft + y := (c.Y + 16) - (16-uiFontMHeight)/2 + text.Draw(dst, c.Text, uiFont, x, y, color.Black) +} + +func (c *CheckBox) Checked() bool { + return c.checked +} + +func (c *CheckBox) CheckChanged() bool { + return c.checkChanged +} + +var ( + button1 = &Button{ + X: 16, + Y: 16, + Width: 128, + Height: 32, + Text: "Button 1", + } + button2 = &Button{ + X: 160, + Y: 16, + Width: 128, + Height: 32, + Text: "Button 2", + } + checkBox = &CheckBox{ + X: 16, + Y: 64, + Text: "Check Box!", + } + textBoxLog = &TextBox{ + X: 16, + Y: 96, + Width: 608, + Height: 160, + } +) + +func update(screen *ebiten.Image) error { + theInput.Update() + + button1.Update() + button2.Update() + checkBox.Update() + textBoxLog.Update() + + if button1.Pressed() { + textBoxLog.AppendLine("Button 1 Pressed") + } + if button2.Pressed() { + textBoxLog.AppendLine("Button 2 Pressed") + } + if checkBox.CheckChanged() { + msg := "Check box check changed" + if checkBox.Checked() { + msg += " (Checked)" + } else { + msg += " (Unchecked)" + } + textBoxLog.AppendLine(msg) + } + + if ebiten.IsRunningSlowly() { + return nil + } + + screen.Fill(color.RGBA{0xeb, 0xeb, 0xeb, 0xff}) + button1.Draw(screen) + button2.Draw(screen) + checkBox.Draw(screen) + textBoxLog.Draw(screen) + return nil +} + +func main() { + if err := ebiten.Run(update, screenWidth, screenHeight, 1, "UI (Ebiten Demo)"); err != nil { + log.Fatal(err) + } +}