mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 02:38:53 +01:00
Add examples/ui (#367)
This commit is contained in:
parent
585529d276
commit
7607ad1660
BIN
examples/_resources/images/ui.png
Normal file
BIN
examples/_resources/images/ui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 599 B |
529
examples/ui/main.go
Normal file
529
examples/ui/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user