2017-07-21 17:10:22 +02:00
|
|
|
// 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 {
|
2017-07-22 20:21:02 +02:00
|
|
|
Rect image.Rectangle
|
|
|
|
Text string
|
2017-07-21 17:10:22 +02:00
|
|
|
|
|
|
|
mouseDown bool
|
|
|
|
pressed bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Button) Update() {
|
|
|
|
b.pressed = false
|
|
|
|
if theInput.IsMouseButtonPressed() {
|
|
|
|
x, y := ebiten.CursorPosition()
|
2017-07-22 20:21:02 +02:00
|
|
|
if b.Rect.Min.X <= x && x < b.Rect.Max.X && b.Rect.Min.Y <= y && y < b.Rect.Max.Y {
|
2017-07-21 17:10:22 +02:00
|
|
|
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
|
|
|
|
}
|
2017-07-22 20:21:02 +02:00
|
|
|
drawNinePatches(dst, b.Rect, imageSrcRects[t])
|
2017-07-21 17:10:22 +02:00
|
|
|
|
|
|
|
bounds, _ := font.BoundString(uiFont, b.Text)
|
|
|
|
w := (bounds.Max.X - bounds.Min.X).Ceil()
|
2017-07-22 20:21:02 +02:00
|
|
|
x := b.Rect.Min.X + (b.Rect.Dx()-w)/2
|
|
|
|
y := b.Rect.Max.Y - (b.Rect.Dy()-uiFontMHeight)/2
|
2017-07-21 17:10:22 +02:00
|
|
|
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 {
|
2017-07-22 20:21:02 +02:00
|
|
|
Rect image.Rectangle
|
|
|
|
Text string
|
2017-07-21 17:10:22 +02:00
|
|
|
|
|
|
|
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{}
|
|
|
|
}
|
2017-07-22 20:21:02 +02:00
|
|
|
t.vScrollBar.X = t.Rect.Max.X - VScrollBarWidth
|
|
|
|
t.vScrollBar.Y = t.Rect.Min.Y
|
|
|
|
t.vScrollBar.Height = t.Rect.Dy()
|
2017-07-21 17:10:22 +02:00
|
|
|
|
|
|
|
_, 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
|
2017-07-22 20:21:02 +02:00
|
|
|
return t.Rect.Dx(), h
|
2017-07-21 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TextBox) viewSize() (int, int) {
|
2017-07-22 20:21:02 +02:00
|
|
|
return t.Rect.Dx() - VScrollBarWidth - textBoxPaddingLeft, t.Rect.Dy()
|
2017-07-21 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TextBox) contentOffset() (int, int) {
|
|
|
|
return t.offsetX, t.offsetY
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TextBox) Draw(dst *ebiten.Image) {
|
2017-07-22 20:21:02 +02:00
|
|
|
drawNinePatches(dst, t.Rect, imageSrcRects[imageTypeTextBox])
|
2017-07-21 17:10:22 +02:00
|
|
|
|
|
|
|
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{}
|
2017-07-22 20:21:02 +02:00
|
|
|
op.GeoM.Translate(float64(t.Rect.Min.X), float64(t.Rect.Min.Y))
|
2017-07-21 17:10:22 +02:00
|
|
|
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{
|
2017-07-22 20:21:02 +02:00
|
|
|
Rect: image.Rect(16, 16, 144, 48),
|
|
|
|
Text: "Button 1",
|
2017-07-21 17:10:22 +02:00
|
|
|
}
|
|
|
|
button2 = &Button{
|
2017-07-22 20:21:02 +02:00
|
|
|
Rect: image.Rect(160, 16, 288, 48),
|
|
|
|
Text: "Button 2",
|
2017-07-21 17:10:22 +02:00
|
|
|
}
|
|
|
|
checkBox = &CheckBox{
|
|
|
|
X: 16,
|
|
|
|
Y: 64,
|
|
|
|
Text: "Check Box!",
|
|
|
|
}
|
|
|
|
textBoxLog = &TextBox{
|
2017-07-22 20:21:02 +02:00
|
|
|
Rect: image.Rect(16, 96, 624, 464),
|
2017-07-21 17:10:22 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|