initial commit
This commit is contained in:
commit
b832628da8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
old
|
||||||
|
*.otf
|
92
app.go
Normal file
92
app.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Root View
|
||||||
|
Background color.Color
|
||||||
|
|
||||||
|
defaultCtx *appCtx
|
||||||
|
|
||||||
|
oldMouseEvent MouseEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(root View) (*App, error) {
|
||||||
|
fnt, err := LoadFontFromBytes(fonts.MPlus1pRegular_ttf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
Root: root,
|
||||||
|
Background: color.White,
|
||||||
|
|
||||||
|
defaultCtx: &appCtx{
|
||||||
|
font: fnt,
|
||||||
|
fontSize: 16,
|
||||||
|
fg: color.Black,
|
||||||
|
bg: color.Transparent,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Update() error {
|
||||||
|
if a.Root == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
winw, winh := ebiten.WindowSize()
|
||||||
|
window := D(0, 0, winw, winh)
|
||||||
|
|
||||||
|
// handle mouse events
|
||||||
|
mouseEvent := makeMouseEvent()
|
||||||
|
if mouseEvent.IsScrollEvent() {
|
||||||
|
a.Root.OnMouseScroll(mouseEvent)
|
||||||
|
}
|
||||||
|
if mouseEvent.HasMouseMoved(a.oldMouseEvent) && mouseEvent.Position.In(window) {
|
||||||
|
a.Root.OnMouseMove(mouseEvent)
|
||||||
|
}
|
||||||
|
if mouseEvent.Buttons.anyClicked() {
|
||||||
|
a.Root.OnMouseClick(mouseEvent)
|
||||||
|
}
|
||||||
|
a.oldMouseEvent = mouseEvent
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Draw(screen *ebiten.Image) {
|
||||||
|
if a.Root == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout phase
|
||||||
|
a.Root.Layout(a.defaultCtx)
|
||||||
|
|
||||||
|
// draw phase
|
||||||
|
screen.Fill(a.Background)
|
||||||
|
a.Root.Draw(newImage(screen), a.defaultCtx)
|
||||||
|
|
||||||
|
// show fps
|
||||||
|
ebitenutil.DebugPrint(screen, "") //fmt.Sprintf("fps: %g", ebiten.CurrentFPS()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
|
||||||
|
return outsideWidth, outsideHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Start() error {
|
||||||
|
ebiten.SetScreenTransparent(true)
|
||||||
|
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
|
||||||
|
//ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMinimum)
|
||||||
|
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum)
|
||||||
|
ebiten.SetRunnableOnUnfocused(true)
|
||||||
|
return ebiten.RunGame(a)
|
||||||
|
}
|
46
ctx.go
Normal file
46
ctx.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/text"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppContext interface {
|
||||||
|
Font() *Font
|
||||||
|
FontSize() int
|
||||||
|
Foreground() color.Color
|
||||||
|
Background() color.Color
|
||||||
|
|
||||||
|
MeasureString(str string, f font.Face) image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
type appCtx struct {
|
||||||
|
font *Font
|
||||||
|
fontSize int
|
||||||
|
fg, bg color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AppContext = &appCtx{}
|
||||||
|
|
||||||
|
func (c *appCtx) Font() *Font {
|
||||||
|
return c.font
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appCtx) FontSize() int {
|
||||||
|
return c.fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appCtx) Foreground() color.Color {
|
||||||
|
return c.fg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appCtx) Background() color.Color {
|
||||||
|
return c.bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appCtx) MeasureString(str string, f font.Face) image.Rectangle {
|
||||||
|
return text.BoundString(f, str)
|
||||||
|
}
|
82
events.go
Normal file
82
events.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
type Events interface {
|
||||||
|
OnMouseClick(event MouseEvent) (consumed bool)
|
||||||
|
OnMouseMove(event MouseEvent) (consumed bool)
|
||||||
|
OnMouseScroll(event MouseEvent) (consumed bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventTmpl struct {
|
||||||
|
MouseClick func(event MouseEvent) (consumed bool)
|
||||||
|
MouseMove func(event MouseEvent) (consumed bool)
|
||||||
|
MouseScroll func(event MouseEvent) (consumed bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Events = &EventTmpl{}
|
||||||
|
|
||||||
|
func (e *EventTmpl) OnMouseClick(event MouseEvent) (consumed bool) {
|
||||||
|
if e.MouseClick == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.MouseClick(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EventTmpl) OnMouseMove(event MouseEvent) (consumed bool) {
|
||||||
|
if e.MouseMove == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.MouseMove(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EventTmpl) OnMouseScroll(event MouseEvent) (consumed bool) {
|
||||||
|
if e.MouseScroll == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.MouseScroll(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MouseEvent struct {
|
||||||
|
Position Point
|
||||||
|
Buttons ButtonMask
|
||||||
|
Wheel Point
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMouseEvent() MouseEvent {
|
||||||
|
return MouseEvent{
|
||||||
|
Position: P(ebiten.CursorPosition()),
|
||||||
|
Buttons: Buttons(),
|
||||||
|
Wheel: Pf(ebiten.Wheel()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) SubtractPos(p Point) MouseEvent {
|
||||||
|
e.Position = e.Position.Sub(p)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) AddPos(p Point) MouseEvent {
|
||||||
|
e.Position = e.Position.Add(p)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) IsScrollEvent() bool {
|
||||||
|
return e.Wheel.X != 0 || e.Wheel.Y != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) HasMouseMoved(o MouseEvent) bool {
|
||||||
|
return e.Position != o.Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) Pressed(button MouseButton) bool {
|
||||||
|
return e.Buttons.Pressed(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) JustPressed(button MouseButton) bool {
|
||||||
|
return e.Buttons.JustPressed(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) Clicked(button MouseButton) bool {
|
||||||
|
return e.Buttons.Clicked(button)
|
||||||
|
}
|
75
font.go
Normal file
75
font.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
|
"golang.org/x/image/font/sfnt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Font struct {
|
||||||
|
font *sfnt.Font
|
||||||
|
faces map[int]font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadOpenTypeFont(otfFile string) (*Font, error) {
|
||||||
|
ff, err := os.Open(otfFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return LoadFontFromReaderAt(ff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFontFromBytes(data []byte) (*Font, error) {
|
||||||
|
return LoadFontFromReaderAt(bytes.NewReader(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFontFromReaderAt(r io.ReaderAt) (*Font, error) {
|
||||||
|
fnt, err := opentype.ParseReaderAt(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Font{
|
||||||
|
font: fnt,
|
||||||
|
faces: map[int]font.Face{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) FaceErr(fontSize int) (face font.Face, err error) {
|
||||||
|
if v, ok := f.faces[fontSize]; ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
face, err = opentype.NewFace(f.font, &opentype.FaceOptions{
|
||||||
|
Size: float64(fontSize),
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingFull,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
f.faces[fontSize] = face
|
||||||
|
}
|
||||||
|
|
||||||
|
return face, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) Face(fontSize int) font.Face {
|
||||||
|
face, err := f.FaceErr(fontSize)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return face
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) Close() error {
|
||||||
|
for _, face := range f.faces {
|
||||||
|
if err := face.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
20
go.mod
Normal file
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module git.milar.in/milarin/gui
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hajimehoshi/ebiten v1.12.12
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.3.1
|
||||||
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // indirect
|
||||||
|
github.com/gofrs/flock v0.8.1 // indirect
|
||||||
|
github.com/jezek/xgb v1.0.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
)
|
114
go.sum
Normal file
114
go.sum
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/hajimehoshi/bitmapfont v1.3.0 h1:h6+HJQ+2MKT3lEVEArjVC4/h0qcFXlVsMTGuRijEnVA=
|
||||||
|
github.com/hajimehoshi/bitmapfont v1.3.0/go.mod h1:/Qb7yVjHYNUV4JdqNkPs6BSZwLjKqkZOMIp6jZD0KgE=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v2 v2.2.0 h1:E6vzlchynZj6OVohVKFqWkKW348EmDW62K5zPXDi7A8=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v2 v2.2.0/go.mod h1:Llj2wTYXMuCTJEw2ATNIO6HbFPOoBYPs08qLdFAxOsQ=
|
||||||
|
github.com/hajimehoshi/ebiten v1.12.12 h1:JvmF1bXRa+t+/CcLWxrJCRsdjs2GyBYBSiFAfIqDFlI=
|
||||||
|
github.com/hajimehoshi/ebiten v1.12.12/go.mod h1:1XI25ImVCDPJiXox4h9yK/CvN5sjDYnbF4oZcFzPXHw=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.3.1 h1:1usmOQv+wxeBElKTYiB7UnJaIwx8+FflDfQA729uUr4=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.3.1/go.mod h1:MSVpCRgFyOsDA2HWNPEiBz7aOjESNulTWwD31Ud9n5c=
|
||||||
|
github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
|
||||||
|
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
|
||||||
|
github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||||
|
github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||||
|
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||||
|
github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||||
|
github.com/hajimehoshi/oto/v2 v2.1.0/go.mod h1:9i0oYbpJ8BhVGkXDKdXKfFthX1JUNfXjeTp944W8TGM=
|
||||||
|
github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
|
||||||
|
github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
|
||||||
|
github.com/jezek/xgb v1.0.0 h1:s2rRzAV8KQRlpsYA7Uyxoidv1nodMF0m6dIG6FhhVLQ=
|
||||||
|
github.com/jezek/xgb v1.0.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||||
|
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||||
|
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||||
|
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||||
|
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk=
|
||||||
|
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
||||||
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||||
|
golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 h1:ZDL7hDvJEQEcHVkoZawKmRUgbqn1pOIzb8EinBh5csU=
|
||||||
|
golang.org/x/mobile v0.0.0-20220325161704-447654d348e3/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||||
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
|
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||||
|
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
81
image.go
Normal file
81
image.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
img *ebiten.Image
|
||||||
|
bounds image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImage(img *ebiten.Image) *Image {
|
||||||
|
return &Image{
|
||||||
|
img: img,
|
||||||
|
bounds: img.Bounds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImage(width, height int) *Image {
|
||||||
|
return &Image{
|
||||||
|
img: ebiten.NewImage(width, height),
|
||||||
|
bounds: image.Rect(0, 0, width, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) SubImage(rect image.Rectangle) image.Image {
|
||||||
|
img := i.img.SubImage(rect).(*ebiten.Image)
|
||||||
|
return &Image{
|
||||||
|
img: img,
|
||||||
|
bounds: img.Bounds().Intersect(i.bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) At(x, y int) color.Color {
|
||||||
|
return i.image().At(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) Constrain(rect image.Rectangle) *Image {
|
||||||
|
return &Image{
|
||||||
|
img: i.img,
|
||||||
|
bounds: i.bounds.Intersect(rect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) ColorModel() color.Model {
|
||||||
|
return i.image().ColorModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) Const() image.Rectangle {
|
||||||
|
return i.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) Bounds() image.Rectangle {
|
||||||
|
return i.img.Bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) image() *ebiten.Image {
|
||||||
|
return i.img.SubImage(i.bounds).(*ebiten.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) Fill(c color.Color) {
|
||||||
|
i.image().Fill(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) DrawString(s string, f font.Face, x, y int, c color.Color) {
|
||||||
|
text.Draw(i.image(), s, f, i.img.Bounds().Min.X+x, i.img.Bounds().Min.Y+y, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) DrawImage(img *Image, op *ebiten.DrawImageOptions) {
|
||||||
|
i.image().DrawImage(img.img, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) DrawRect(rect image.Rectangle, colr color.Color) {
|
||||||
|
ebitenutil.DrawRect(i.image(), float64(rect.Min.X), float64(rect.Min.Y), float64(rect.Dx()), float64(rect.Dy()), colr)
|
||||||
|
}
|
183
layouts/layout_border.go
Normal file
183
layouts/layout_border.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BorderLayout ia a gui.Layout which places its children onto a given gui.Side
|
||||||
|
type BorderLayout struct {
|
||||||
|
gui.ViewTmpl
|
||||||
|
views map[Slot]gui.View
|
||||||
|
horizontalLayout *LayoutResult
|
||||||
|
verticalLayout *LayoutResult
|
||||||
|
|
||||||
|
viewDims map[Slot]gui.Dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Layout = &BorderLayout{}
|
||||||
|
|
||||||
|
func NewBorderLayout() *BorderLayout {
|
||||||
|
return &BorderLayout{
|
||||||
|
views: map[Slot]gui.View{},
|
||||||
|
viewDims: map[Slot]gui.Dimension{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) Views() []gui.View {
|
||||||
|
s := make([]gui.View, 0, len(g.views))
|
||||||
|
for _, view := range g.views {
|
||||||
|
s = append(s, view)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) SetView(v gui.View, slot Slot) {
|
||||||
|
g.views[slot] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) View(slot Slot) gui.View {
|
||||||
|
return g.views[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
g.ViewTmpl.Draw(img, ctx)
|
||||||
|
|
||||||
|
if g.verticalLayout == nil {
|
||||||
|
g.Layout(ctx)
|
||||||
|
}
|
||||||
|
verticalLayout := g.verticalLayout
|
||||||
|
|
||||||
|
if g.horizontalLayout == nil {
|
||||||
|
g.Layout(ctx)
|
||||||
|
}
|
||||||
|
horizontalLayout := g.horizontalLayout
|
||||||
|
|
||||||
|
remainingVerticalSpacePerView := (img.Bounds().Dy() - verticalLayout.Sum.Height)
|
||||||
|
if verticalLayout.VerticalNegativeCount > 0 {
|
||||||
|
remainingVerticalSpacePerView /= verticalLayout.VerticalNegativeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingHorizontalSpacePerView := (img.Bounds().Dx() - horizontalLayout.Sum.Width)
|
||||||
|
if horizontalLayout.HorizontalNegativeCount > 0 {
|
||||||
|
remainingHorizontalSpacePerView /= horizontalLayout.HorizontalNegativeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fitsVertically := img.Bounds().Dy() >= verticalLayout.Sum.Height
|
||||||
|
fitsHorizontally := img.Bounds().Dx() >= horizontalLayout.Sum.Width
|
||||||
|
|
||||||
|
var topHeight int
|
||||||
|
var bottomHeight int
|
||||||
|
var leftWidth int
|
||||||
|
var rightWidth int
|
||||||
|
|
||||||
|
if view, ok := g.views[Top]; ok {
|
||||||
|
_, topHeight = view.Layout(ctx)
|
||||||
|
|
||||||
|
if fitsVertically {
|
||||||
|
topHeight = iff(topHeight < 0, remainingVerticalSpacePerView, topHeight)
|
||||||
|
} else {
|
||||||
|
topHeight = int(float64(img.Bounds().Dy()) * float64(topHeight) / float64(verticalLayout.Sum.Height))
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[Top] = gui.D(0, 0, img.Bounds().Dx(), topHeight)
|
||||||
|
view.Draw(img.SubImage(image.Rect(0, 0, img.Bounds().Dx(), topHeight)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view, ok := g.views[Bottom]; ok {
|
||||||
|
_, bottomHeight = view.Layout(ctx)
|
||||||
|
|
||||||
|
if fitsVertically {
|
||||||
|
bottomHeight = iff(bottomHeight < 0, remainingVerticalSpacePerView, bottomHeight)
|
||||||
|
} else {
|
||||||
|
bottomHeight = int(float64(img.Bounds().Dy()) * float64(bottomHeight) / float64(verticalLayout.Sum.Height))
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[Bottom] = gui.D(0, img.Bounds().Dy()-bottomHeight, img.Bounds().Dx(), bottomHeight)
|
||||||
|
view.Draw(img.SubImage(image.Rect(0, img.Bounds().Dy()-bottomHeight, img.Bounds().Dx(), img.Bounds().Dy())).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view, ok := g.views[Left]; ok {
|
||||||
|
leftWidth, _ = view.Layout(ctx)
|
||||||
|
|
||||||
|
if fitsHorizontally {
|
||||||
|
leftWidth = iff(leftWidth < 0, remainingHorizontalSpacePerView, leftWidth)
|
||||||
|
} else {
|
||||||
|
leftWidth = int(float64(img.Bounds().Dx()) * float64(leftWidth) / float64(horizontalLayout.Sum.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[Left] = gui.D(0, topHeight, leftWidth, img.Bounds().Dy()-topHeight-bottomHeight)
|
||||||
|
view.Draw(img.SubImage(image.Rect(0, topHeight, leftWidth, img.Bounds().Dy()-bottomHeight)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view, ok := g.views[Right]; ok {
|
||||||
|
rightWidth, _ = view.Layout(ctx)
|
||||||
|
|
||||||
|
if fitsHorizontally {
|
||||||
|
rightWidth = iff(rightWidth < 0, remainingHorizontalSpacePerView, rightWidth)
|
||||||
|
} else {
|
||||||
|
rightWidth = int(float64(img.Bounds().Dx()) * float64(rightWidth) / float64(horizontalLayout.Sum.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[Right] = gui.D(img.Bounds().Dx()-rightWidth, topHeight, rightWidth, img.Bounds().Dy()-topHeight-bottomHeight)
|
||||||
|
view.Draw(img.SubImage(image.Rect(img.Bounds().Dx()-rightWidth, topHeight, img.Bounds().Dx(), img.Bounds().Dy()-bottomHeight)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view, ok := g.views[Center]; ok {
|
||||||
|
g.viewDims[Center] = gui.D(leftWidth, topHeight, img.Bounds().Dx()-leftWidth-rightWidth, img.Bounds().Dy()-topHeight-bottomHeight)
|
||||||
|
view.Draw(img.SubImage(image.Rect(leftWidth, topHeight, img.Bounds().Dx()-rightWidth, img.Bounds().Dy()-bottomHeight)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.verticalLayout = nil
|
||||||
|
g.horizontalLayout = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
g.verticalLayout = CalculateLayoutResult(ctx, []gui.View{g.View(Top), g.View(Center), g.View(Bottom)})
|
||||||
|
g.horizontalLayout = CalculateLayoutResult(ctx, []gui.View{g.View(Left), g.View(Center), g.View(Right)})
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) OnMouseClick(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for slot, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if g.views[slot].OnMouseClick(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) OnMouseMove(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for slot, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if g.views[slot].OnMouseMove(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BorderLayout) OnMouseScroll(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for slot, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if g.views[slot].OnMouseScroll(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Slot string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Top Slot = "top"
|
||||||
|
Bottom Slot = "bottom"
|
||||||
|
Left Slot = "left"
|
||||||
|
Right Slot = "right"
|
||||||
|
Center Slot = "center"
|
||||||
|
)
|
58
layouts/layout_coord.go
Normal file
58
layouts/layout_coord.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoordLayout is a gui.Layout which places its children on predefined coordinates
|
||||||
|
type CoordLayout struct {
|
||||||
|
gui.ViewTmpl
|
||||||
|
views map[gui.View]gui.Dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Layout = &CoordLayout{}
|
||||||
|
|
||||||
|
func NewCoordLayout() *CoordLayout {
|
||||||
|
return &CoordLayout{
|
||||||
|
views: map[gui.View]gui.Dimension{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *CoordLayout) Views() []gui.View {
|
||||||
|
s := make([]gui.View, 0, len(g.views))
|
||||||
|
for v := range g.views {
|
||||||
|
s = append(s, v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetView places v at the given coordinates with the given dimensions.
|
||||||
|
// v will be added to g's children if not already added before
|
||||||
|
func (g *CoordLayout) SetView(v gui.View, x, y, width, height int) {
|
||||||
|
g.views[v] = gui.Dimension{Point: gui.Point{X: x, Y: y}, Size: gui.Size{Width: width, Height: height}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *CoordLayout) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
for v, d := range g.views {
|
||||||
|
v.Draw(img.SubImage(image.Rect(d.X, d.Y, d.Width, d.Height)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *CoordLayout) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (g *CoordLayout) OnKeyPressed(event *gui.KeyEvent) (consumed bool) {
|
||||||
|
for _, view := range g.Views() {
|
||||||
|
if view.OnKeyPressed(event) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO OnMouseEvent
|
176
layouts/layout_flow.go
Normal file
176
layouts/layout_flow.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FlowLayout ia a gui.Layout which places its children in a linear layout
|
||||||
|
type FlowLayout struct {
|
||||||
|
gui.ViewTmpl
|
||||||
|
views []gui.View
|
||||||
|
lastLayoutPhase *LayoutResult
|
||||||
|
|
||||||
|
viewDims map[gui.View]gui.Dimension
|
||||||
|
|
||||||
|
// Orientation defines in which direction the children will be placed
|
||||||
|
Orientation gui.Orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Layout = &FlowLayout{}
|
||||||
|
|
||||||
|
func NewFlowLayout(orientation gui.Orientation) *FlowLayout {
|
||||||
|
return &FlowLayout{
|
||||||
|
views: make([]gui.View, 0),
|
||||||
|
viewDims: map[gui.View]gui.Dimension{},
|
||||||
|
Orientation: orientation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) Views() []gui.View {
|
||||||
|
return g.views
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) AppendViews(v ...gui.View) {
|
||||||
|
g.views = append(g.views, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) PrependViews(v ...gui.View) {
|
||||||
|
g.views = append(v, g.views...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) InsertView(v gui.View, index int) {
|
||||||
|
g.views = append(g.views[:index], append([]gui.View{v}, g.views[index:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) removeView(v gui.View) {
|
||||||
|
for index, view := range g.Views() {
|
||||||
|
if v == view {
|
||||||
|
delete(g.viewDims, view)
|
||||||
|
g.views = append(g.views[:index], g.views[index+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) RemoveViews(v ...gui.View) {
|
||||||
|
views := append(make([]gui.View, 0, len(v)), v...)
|
||||||
|
for _, view := range views {
|
||||||
|
g.removeView(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
g.ViewTmpl.Draw(img, ctx)
|
||||||
|
|
||||||
|
if g.lastLayoutPhase == nil {
|
||||||
|
g.Layout(ctx)
|
||||||
|
}
|
||||||
|
layout := g.lastLayoutPhase
|
||||||
|
|
||||||
|
if g.Orientation == gui.Horizontal {
|
||||||
|
remainingSpacePerView := img.Bounds().Dx() - layout.Sum.Width
|
||||||
|
if remainingSpacePerView < 0 {
|
||||||
|
remainingSpacePerView = 0
|
||||||
|
}
|
||||||
|
if layout.HorizontalNegativeCount > 0 {
|
||||||
|
remainingSpacePerView /= layout.HorizontalNegativeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
x := 0
|
||||||
|
for _, view := range g.views {
|
||||||
|
size := layout.Sizes[view]
|
||||||
|
|
||||||
|
size.Height = iff(size.Height < 0, img.Bounds().Dy(), size.Height)
|
||||||
|
if size.Width < 0 {
|
||||||
|
size.Width = iff(layout.Sum.Width > img.Bounds().Dx(), 0, remainingSpacePerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[view] = gui.D(x, 0, size.Width, size.Height)
|
||||||
|
view.Draw(img.SubImage(image.Rect(x, 0, x+size.Width, size.Height)).(*gui.Image), ctx)
|
||||||
|
|
||||||
|
x += size.Width
|
||||||
|
if x >= img.Bounds().Dx() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if g.Orientation == gui.Vertical {
|
||||||
|
remainingSpacePerView := img.Bounds().Dy() - layout.Sum.Height
|
||||||
|
if remainingSpacePerView < 0 {
|
||||||
|
remainingSpacePerView = 0
|
||||||
|
}
|
||||||
|
if layout.VerticalNegativeCount > 0 {
|
||||||
|
remainingSpacePerView /= layout.VerticalNegativeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
y := 0
|
||||||
|
for _, view := range g.views {
|
||||||
|
size := layout.Sizes[view]
|
||||||
|
|
||||||
|
size.Width = iff(size.Width < 0, img.Bounds().Dx(), size.Width)
|
||||||
|
if size.Height < 0 {
|
||||||
|
size.Height = iff(layout.Sum.Height > img.Bounds().Dy(), 0, remainingSpacePerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.viewDims[view] = gui.D(0, y, size.Width, size.Height)
|
||||||
|
view.Draw(img.SubImage(image.Rect(0, y, size.Width, y+size.Height)).(*gui.Image), ctx)
|
||||||
|
|
||||||
|
y += size.Height
|
||||||
|
if y >= img.Bounds().Dy() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.lastLayoutPhase = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
layout := CalculateLayoutResult(ctx, g.Views())
|
||||||
|
g.lastLayoutPhase = layout
|
||||||
|
|
||||||
|
if g.Orientation == gui.Horizontal {
|
||||||
|
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Sum.Width, -1)
|
||||||
|
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Max.Height, -1)
|
||||||
|
} else if g.Orientation == gui.Vertical {
|
||||||
|
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Max.Width, -1)
|
||||||
|
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Sum.Height, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.Pref = gui.Size{Width: prefWidth, Height: prefHeight}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) OnMouseClick(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for view, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if view.OnMouseClick(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) OnMouseMove(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for view, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if view.OnMouseMove(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FlowLayout) OnMouseScroll(event gui.MouseEvent) (consumed bool) {
|
||||||
|
for view, dim := range g.viewDims {
|
||||||
|
if event.Position.In(dim) {
|
||||||
|
if view.OnMouseScroll(event.SubtractPos(dim.Point)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
95
layouts/utils.go
Normal file
95
layouts/utils.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(x, y int) int {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func limit(v, minv, maxv int) int {
|
||||||
|
return min(max(v, minv), maxv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func iff[T any](condition bool, trueValue, falseValue T) T {
|
||||||
|
if condition {
|
||||||
|
return trueValue
|
||||||
|
}
|
||||||
|
return falseValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutResult struct {
|
||||||
|
Sizes map[gui.View]gui.Size
|
||||||
|
|
||||||
|
Sum gui.Size
|
||||||
|
Min gui.Size
|
||||||
|
Max gui.Size
|
||||||
|
Pref gui.Size
|
||||||
|
|
||||||
|
Count int
|
||||||
|
|
||||||
|
VerticalNegativeCount int
|
||||||
|
HorizontalNegativeCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateLayoutResult(ctx gui.AppContext, views []gui.View) *LayoutResult {
|
||||||
|
result := &LayoutResult{
|
||||||
|
Sizes: map[gui.View]gui.Size{},
|
||||||
|
|
||||||
|
Sum: gui.Size{Width: 0, Height: 0},
|
||||||
|
Min: gui.Size{Width: math.MaxInt, Height: math.MaxInt},
|
||||||
|
Max: gui.Size{Width: -1, Height: -1},
|
||||||
|
|
||||||
|
Count: 0,
|
||||||
|
|
||||||
|
VerticalNegativeCount: 0,
|
||||||
|
HorizontalNegativeCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, view := range views {
|
||||||
|
if view == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Count++
|
||||||
|
|
||||||
|
width, height := view.Layout(ctx)
|
||||||
|
result.Sizes[view] = gui.Size{Width: width, Height: height}
|
||||||
|
|
||||||
|
if width > 0 {
|
||||||
|
result.Sum.Width += width
|
||||||
|
result.Min.Width = min(result.Min.Width, width)
|
||||||
|
result.Max.Width = max(result.Max.Width, width)
|
||||||
|
} else if width < 0 {
|
||||||
|
result.HorizontalNegativeCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if height > 0 {
|
||||||
|
result.Sum.Height += height
|
||||||
|
result.Min.Height = min(result.Min.Height, height)
|
||||||
|
result.Max.Height = max(result.Max.Height, height)
|
||||||
|
} else if height < 0 {
|
||||||
|
result.VerticalNegativeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func rect(x, y, w, h int) image.Rectangle {
|
||||||
|
return image.Rect(x, y, x+w, y+h)
|
||||||
|
}
|
79
style.go
Normal file
79
style.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Style interface {
|
||||||
|
SetForeground(c color.Color)
|
||||||
|
Foreground(ctx AppContext) color.Color
|
||||||
|
|
||||||
|
SetBackground(c color.Color)
|
||||||
|
Background(ctx AppContext) color.Color
|
||||||
|
|
||||||
|
SetFont(f *Font)
|
||||||
|
Font(ctx AppContext) *Font
|
||||||
|
|
||||||
|
SetFontSize(fontSize int)
|
||||||
|
FontSize(ctx AppContext) int
|
||||||
|
|
||||||
|
FontFace(ctx AppContext) font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
type StyleTmpl struct {
|
||||||
|
foreground color.Color
|
||||||
|
background color.Color
|
||||||
|
|
||||||
|
font *Font
|
||||||
|
fontSize *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) SetForeground(c color.Color) {
|
||||||
|
s.foreground = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) Foreground(ctx AppContext) color.Color {
|
||||||
|
if s.foreground == nil {
|
||||||
|
return ctx.Foreground()
|
||||||
|
}
|
||||||
|
return s.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) SetBackground(c color.Color) {
|
||||||
|
s.background = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) Background(ctx AppContext) color.Color {
|
||||||
|
if s.background == nil {
|
||||||
|
return ctx.Background()
|
||||||
|
}
|
||||||
|
return s.background
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) SetFont(f *Font) {
|
||||||
|
s.font = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) Font(ctx AppContext) *Font {
|
||||||
|
if s.font == nil {
|
||||||
|
return ctx.Font()
|
||||||
|
}
|
||||||
|
return s.font
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) SetFontSize(fontSize int) {
|
||||||
|
s.fontSize = &fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) FontSize(ctx AppContext) int {
|
||||||
|
if s.fontSize == nil {
|
||||||
|
return ctx.FontSize()
|
||||||
|
}
|
||||||
|
return *s.fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StyleTmpl) FontFace(ctx AppContext) font.Face {
|
||||||
|
return s.Font(ctx).Face(s.FontSize(ctx))
|
||||||
|
}
|
153
tests/main.go
Normal file
153
tests/main.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
"git.milar.in/milarin/gui/layouts"
|
||||||
|
"git.milar.in/milarin/gui/views"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
//AnchorViewTest()
|
||||||
|
//FlowLayoutTest()
|
||||||
|
ScrollLayoutTest()
|
||||||
|
//BorderLayoutTest()
|
||||||
|
//TextViewTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnchorViewTest() {
|
||||||
|
textView := views.NewTextView("hello world")
|
||||||
|
textView.SetBackground(color.NRGBA{255, 0, 0, 255})
|
||||||
|
anchorView := views.NewAnchorView(textView, gui.AnchorBottomRight)
|
||||||
|
|
||||||
|
a, err := gui.NewApp(anchorView)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Background = color.Transparent
|
||||||
|
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScrollLayoutTest() {
|
||||||
|
textViews := make([]gui.View, 0, 100)
|
||||||
|
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||||
|
for i := 0; i < cap(textViews); i++ {
|
||||||
|
textView := views.NewTextView(strconv.Itoa(i + 1))
|
||||||
|
marginView := views.NewMarginView(textView, 10, 10, 10, 10)
|
||||||
|
growView := views.NewGrowView(marginView, true, false)
|
||||||
|
constrainView := views.NewConstrainView(growView, 500, -1)
|
||||||
|
|
||||||
|
textViews = append(textViews, constrainView)
|
||||||
|
textViews[i].SetBackground(color.NRGBA{
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
255,
|
||||||
|
})
|
||||||
|
/*textView.MouseEvent = func(event gui.MouseEvent) (consumed bool) {
|
||||||
|
textView.SetBackground(color.White)
|
||||||
|
return true
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
flowLayout := layouts.NewFlowLayout(gui.Vertical)
|
||||||
|
flowLayout.AppendViews(textViews...)
|
||||||
|
flowLayout.SetBackground(color.White)
|
||||||
|
|
||||||
|
scrollView := views.NewScrollView(flowLayout)
|
||||||
|
//scrollView.Scroll(0, 100)
|
||||||
|
|
||||||
|
a, err := gui.NewApp(scrollView)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Background = color.Transparent
|
||||||
|
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlowLayoutTest() {
|
||||||
|
textViews := make([]gui.View, 0, 10)
|
||||||
|
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||||
|
for i := 0; i < cap(textViews); i++ {
|
||||||
|
textView := views.NewTextView(strconv.Itoa(i + 1))
|
||||||
|
textViews = append(textViews, textView)
|
||||||
|
textViews[i].SetBackground(color.NRGBA{
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
uint8(rnd.Intn(math.MaxUint8)),
|
||||||
|
255,
|
||||||
|
})
|
||||||
|
textView.MouseMove = func(event gui.MouseEvent) (consumed bool) {
|
||||||
|
textView.SetBackground(color.White)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flowLayout := layouts.NewFlowLayout(gui.Vertical)
|
||||||
|
flowLayout.AppendViews(textViews...)
|
||||||
|
|
||||||
|
a, err := gui.NewApp(flowLayout)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TextViewTest() {
|
||||||
|
a, err := gui.NewApp(views.NewTextView("hello world"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BorderLayoutTest() {
|
||||||
|
top := views.NewTextView("top")
|
||||||
|
bottom := views.NewTextView("bottom")
|
||||||
|
left := views.NewTextView("left")
|
||||||
|
right := views.NewTextView("right")
|
||||||
|
center := views.NewTextView("center")
|
||||||
|
|
||||||
|
top.SetBackground(color.NRGBA{255, 200, 200, 255})
|
||||||
|
bottom.SetBackground(color.NRGBA{200, 255, 200, 255})
|
||||||
|
left.SetBackground(color.NRGBA{200, 200, 255, 255})
|
||||||
|
right.SetBackground(color.NRGBA{255, 255, 200, 255})
|
||||||
|
center.SetBackground(color.NRGBA{200, 255, 255, 255})
|
||||||
|
|
||||||
|
top.SetFontSize(48)
|
||||||
|
|
||||||
|
borderLayout := layouts.NewBorderLayout()
|
||||||
|
borderLayout.SetView(top, layouts.Top)
|
||||||
|
borderLayout.SetView(bottom, layouts.Bottom)
|
||||||
|
borderLayout.SetView(left, layouts.Left)
|
||||||
|
borderLayout.SetView(right, layouts.Right)
|
||||||
|
borderLayout.SetView(center, layouts.Center)
|
||||||
|
|
||||||
|
a, err := gui.NewApp(borderLayout)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
BIN
tests/tests
Executable file
BIN
tests/tests
Executable file
Binary file not shown.
19
tmpl_view.go
Normal file
19
tmpl_view.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
type ViewTmpl struct {
|
||||||
|
EventTmpl
|
||||||
|
StyleTmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ View = &ViewTmpl{}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) Draw(img *Image, ctx AppContext) {
|
||||||
|
bg := v.Background(ctx)
|
||||||
|
if _, _, _, a := bg.RGBA(); a > 0 {
|
||||||
|
img.DrawRect(img.Bounds(), v.Background(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ViewTmpl) Layout(ctx AppContext) (prefWidth, prefHeight int) {
|
||||||
|
return -1, -1
|
||||||
|
}
|
55
tmpl_wrapper.go
Normal file
55
tmpl_wrapper.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
type WrapperTmpl struct {
|
||||||
|
ViewTmpl
|
||||||
|
view View
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Wrapper = &WrapperTmpl{}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Layout(ctx AppContext) (prefWidth, prefHeight int) {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.Layout(ctx)
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.Layout(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Draw(img *Image, ctx AppContext) {
|
||||||
|
v.ViewTmpl.Draw(img, ctx)
|
||||||
|
if v.view != nil {
|
||||||
|
v.view.Draw(img, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) OnMouseClick(event MouseEvent) (consumed bool) {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.OnMouseClick(event)
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.OnMouseClick(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) OnMouseMove(event MouseEvent) (consumed bool) {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.OnMouseMove(event)
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.OnMouseMove(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) OnMouseScroll(event MouseEvent) (consumed bool) {
|
||||||
|
if v.view != nil {
|
||||||
|
return v.view.OnMouseScroll(event)
|
||||||
|
}
|
||||||
|
return v.ViewTmpl.OnMouseScroll(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) Views() []View {
|
||||||
|
return []View{v.view}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) View() View {
|
||||||
|
return v.view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *WrapperTmpl) SetView(view View) {
|
||||||
|
v.view = view
|
||||||
|
}
|
194
types.go
Normal file
194
types.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
X, Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func P(x, y int) Point {
|
||||||
|
return Point{x, y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) Add(o Point) Point {
|
||||||
|
return Point{p.X + o.X, p.Y + o.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) Sub(o Point) Point {
|
||||||
|
return Point{p.X - o.X, p.Y - o.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) Mul(o Point) Point {
|
||||||
|
return Point{p.X * o.X, p.Y * o.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) Div(o Point) Point {
|
||||||
|
return Point{p.X / o.X, p.Y / o.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) Pt() image.Point {
|
||||||
|
return image.Pt(p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pf(x, y float64) Point {
|
||||||
|
return Point{int(x), int(y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) In(d Dimension) bool {
|
||||||
|
return p.X >= d.X && p.X < d.X+d.Width && p.Y >= d.Y && p.Y < d.Y+d.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) String() string {
|
||||||
|
return fmt.Sprintf("P(%d, %d)", p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Size struct {
|
||||||
|
Width, Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func S(w, h int) Size {
|
||||||
|
return Size{w, h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Size) Pt() image.Point {
|
||||||
|
return image.Pt(s.Width, s.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Size) String() string {
|
||||||
|
return fmt.Sprintf("S(%d, %d)", s.Width, s.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dimension struct {
|
||||||
|
Point
|
||||||
|
Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func D(x, y, w, h int) Dimension {
|
||||||
|
return Dimension{P(x, y), S(w, h)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dimension) Rect() image.Rectangle {
|
||||||
|
return image.Rectangle{Min: d.Point.Pt(), Max: d.Point.Pt().Add(d.Size.Pt())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dimension) String() string {
|
||||||
|
return fmt.Sprintf("D(%d, %d, %d, %d)", d.X, d.Y, d.Width, d.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Orientation uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Horizontal Orientation = iota
|
||||||
|
Vertical
|
||||||
|
)
|
||||||
|
|
||||||
|
type Side uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
SideTop Side = iota
|
||||||
|
SideBottom
|
||||||
|
SideLeft
|
||||||
|
SideRight
|
||||||
|
)
|
||||||
|
|
||||||
|
// Horizontal returns true if s is either SideLeft or SideRight
|
||||||
|
func (s Side) Horizontal() bool {
|
||||||
|
return s == SideLeft || s == SideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical returns true if s is either SideTop or SideBottom
|
||||||
|
func (s Side) Vertical() bool {
|
||||||
|
return s == SideTop || s == SideBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
type Anchor uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnchorTopLeft Anchor = iota
|
||||||
|
AnchorTop
|
||||||
|
AnchorTopRight
|
||||||
|
AnchorLeft
|
||||||
|
AnchorCenter
|
||||||
|
AnchorRight
|
||||||
|
AnchorBottomLeft
|
||||||
|
AnchorBottom
|
||||||
|
AnchorBottomRight
|
||||||
|
)
|
||||||
|
|
||||||
|
type MouseButton uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
MouseButtonLeft = MouseButton(ebiten.MouseButtonLeft)
|
||||||
|
MouseButtonRight = MouseButton(ebiten.MouseButtonRight)
|
||||||
|
MouseButtonMiddle = MouseButton(ebiten.MouseButtonMiddle)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ButtonMask uint16
|
||||||
|
|
||||||
|
func Buttons() ButtonMask {
|
||||||
|
var mask ButtonMask
|
||||||
|
|
||||||
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
||||||
|
mask |= 1 << MouseButtonLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
|
||||||
|
mask |= 1 << MouseButtonRight
|
||||||
|
}
|
||||||
|
|
||||||
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
|
||||||
|
mask |= 1 << MouseButtonMiddle
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||||
|
mask |= 1 << (MouseButtonLeft + 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
|
||||||
|
mask |= 1 << (MouseButtonRight + 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonMiddle) {
|
||||||
|
mask |= 1 << (MouseButtonMiddle + 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
|
||||||
|
mask |= 1 << (MouseButtonLeft + 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
|
||||||
|
mask |= 1 << (MouseButtonRight + 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonMiddle) {
|
||||||
|
mask |= 1 << (MouseButtonMiddle + 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ButtonMask) Pressed(button MouseButton) bool {
|
||||||
|
return m&(1<<button) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ButtonMask) JustPressed(button MouseButton) bool {
|
||||||
|
return m&(1<<button+3) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ButtonMask) Clicked(button MouseButton) bool {
|
||||||
|
return m&(1<<button+6) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ButtonMask) anyPressed() bool {
|
||||||
|
return m.Pressed(MouseButtonLeft) || m.Pressed(MouseButtonMiddle) || m.Pressed(MouseButtonRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ButtonMask) anyClicked() bool {
|
||||||
|
return m.Clicked(MouseButtonLeft) || m.Clicked(MouseButtonMiddle) || m.Clicked(MouseButtonRight)
|
||||||
|
}
|
31
view.go
Normal file
31
view.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
// View defines the behavior of any element displayable on screen.
|
||||||
|
// To define custom Views, it is recommended to add ViewTmpl
|
||||||
|
// as the promoted anonymous field for your custom View struct.
|
||||||
|
// It implements the View interface with useful default behavior
|
||||||
|
type View interface {
|
||||||
|
Events
|
||||||
|
Style
|
||||||
|
|
||||||
|
Layout(ctx AppContext) (prefWidth, prefHeight int)
|
||||||
|
Draw(img *Image, ctx AppContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout defines the behavior of a View which can hold multiple sub views
|
||||||
|
type Layout interface {
|
||||||
|
View
|
||||||
|
|
||||||
|
Views() []View
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper defines the behavior of a Layout which can hold exactly one sub view.
|
||||||
|
// To define custom Wrappers, it is recommended to add WrapperTmpl
|
||||||
|
// as the promoted anonymous field for your custom Wrapper struct.
|
||||||
|
// It implements the Wrapper interface with useful default behavior
|
||||||
|
type Wrapper interface {
|
||||||
|
Layout
|
||||||
|
|
||||||
|
SetView(View)
|
||||||
|
View() View
|
||||||
|
}
|
26
views/utils.go
Normal file
26
views/utils.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
func min[T int | float64](x, y T) T {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func max[T int | float64](x, y T) T {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func limit[T int | float64](v, minv, maxv T) T {
|
||||||
|
return min(max(v, minv), maxv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func iff[T any](condition bool, trueValue, falseValue T) T {
|
||||||
|
if condition {
|
||||||
|
return trueValue
|
||||||
|
}
|
||||||
|
return falseValue
|
||||||
|
}
|
57
views/view_anchor.go
Normal file
57
views/view_anchor.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnchorView struct {
|
||||||
|
gui.WrapperTmpl
|
||||||
|
Anchor gui.Anchor
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.View = &AnchorView{}
|
||||||
|
|
||||||
|
func NewAnchorView(view gui.View, anchor gui.Anchor) *AnchorView {
|
||||||
|
v := &AnchorView{Anchor: anchor}
|
||||||
|
v.SetView(view)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AnchorView) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
var pos image.Point
|
||||||
|
|
||||||
|
w, h := v.View().Layout(ctx)
|
||||||
|
w = iff(w >= 0, w, img.Bounds().Dx())
|
||||||
|
h = iff(h >= 0, h, img.Bounds().Dy())
|
||||||
|
|
||||||
|
vw, vh := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
|
||||||
|
switch v.Anchor {
|
||||||
|
case gui.AnchorTopLeft:
|
||||||
|
pos = image.Pt(0, 0)
|
||||||
|
case gui.AnchorTop:
|
||||||
|
pos = image.Pt(vw/2-w/2, 0)
|
||||||
|
case gui.AnchorTopRight:
|
||||||
|
pos = image.Pt(vw-w, 0)
|
||||||
|
case gui.AnchorLeft:
|
||||||
|
pos = image.Pt(0, vh/2-h/2)
|
||||||
|
case gui.AnchorCenter:
|
||||||
|
pos = image.Pt(vw/2-w/2, vh/2-h/2)
|
||||||
|
case gui.AnchorRight:
|
||||||
|
pos = image.Pt(vw-w, vh/2-h/2)
|
||||||
|
case gui.AnchorBottomLeft:
|
||||||
|
pos = image.Pt(0, vh-h)
|
||||||
|
case gui.AnchorBottom:
|
||||||
|
pos = image.Pt(0, vh-h)
|
||||||
|
case gui.AnchorBottomRight:
|
||||||
|
pos = image.Pt(vw-w, vh-h)
|
||||||
|
default:
|
||||||
|
panic("invalid anchor in AnchorView")
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = pos.Add(img.Bounds().Min)
|
||||||
|
rect := image.Rect(pos.X, pos.Y, pos.X+w, pos.Y+h)
|
||||||
|
v.View().Draw(img.SubImage(rect).(*gui.Image), ctx)
|
||||||
|
}
|
47
views/view_constrain.go
Normal file
47
views/view_constrain.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConstrainView is a gui.Wrapper which constrains the dimensions of its View
|
||||||
|
type ConstrainView struct {
|
||||||
|
gui.WrapperTmpl
|
||||||
|
MaxWidth int
|
||||||
|
MaxHeight int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Wrapper = &ConstrainView{}
|
||||||
|
|
||||||
|
func NewConstrainView(view gui.View, maxWidth, maxHeight int) *ConstrainView {
|
||||||
|
v := new(ConstrainView)
|
||||||
|
v.SetView(view)
|
||||||
|
v.Constrain(maxWidth, maxHeight)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ConstrainView) Constrain(maxWidth, maxHeight int) {
|
||||||
|
v.MaxWidth, v.MaxHeight = maxWidth, maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ConstrainView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
if v.View() == nil {
|
||||||
|
return v.MaxWidth, v.MaxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
vw, vh := v.View().Layout(ctx)
|
||||||
|
|
||||||
|
if v.MaxWidth >= 0 {
|
||||||
|
prefWidth = iff(vw >= 0, min(vw, v.MaxWidth), v.MaxWidth)
|
||||||
|
} else {
|
||||||
|
prefWidth = vw
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.MaxHeight >= 0 {
|
||||||
|
prefHeight = iff(vh >= 0, min(vh, v.MaxHeight), v.MaxHeight)
|
||||||
|
} else {
|
||||||
|
prefHeight = vh
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
25
views/view_grow.go
Normal file
25
views/view_grow.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GrowView is a gui.Wrapper which always demands all available space in the given axes
|
||||||
|
type GrowView struct {
|
||||||
|
gui.WrapperTmpl
|
||||||
|
|
||||||
|
growH, growV bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.View = &GrowView{}
|
||||||
|
|
||||||
|
func NewGrowView(view gui.View, growH, growV bool) *GrowView {
|
||||||
|
g := &GrowView{growH: growH, growV: growV}
|
||||||
|
g.SetView(view)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *GrowView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
w, h := v.WrapperTmpl.Layout(ctx)
|
||||||
|
return iff(v.growH, -1, w), iff(v.growV, -1, h)
|
||||||
|
}
|
48
views/view_margin.go
Normal file
48
views/view_margin.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarginView is a gui.Wrapper which applies margin around its view
|
||||||
|
type MarginView struct {
|
||||||
|
gui.WrapperTmpl
|
||||||
|
Margin map[gui.Side]int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Wrapper = &MarginView{}
|
||||||
|
|
||||||
|
func NewMarginView(view gui.View, top, right, bottom, left int) *MarginView {
|
||||||
|
v := new(MarginView)
|
||||||
|
v.SetView(view)
|
||||||
|
v.SetMargin(top, right, bottom, left)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *MarginView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
w, h := v.View().Layout(ctx)
|
||||||
|
w = iff(w > 0, w+v.Margin[gui.SideLeft]+v.Margin[gui.SideRight], w)
|
||||||
|
h = iff(h > 0, h+v.Margin[gui.SideTop]+v.Margin[gui.SideBottom], h)
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MarginView) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
x := img.Bounds().Min.X + g.Margin[gui.SideLeft]
|
||||||
|
y := img.Bounds().Min.Y + g.Margin[gui.SideTop]
|
||||||
|
w := img.Bounds().Max.X - g.Margin[gui.SideRight]
|
||||||
|
h := img.Bounds().Max.Y - g.Margin[gui.SideBottom]
|
||||||
|
|
||||||
|
g.ViewTmpl.Draw(img, ctx)
|
||||||
|
g.View().Draw(img.SubImage(image.Rect(x, y, w, h)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *MarginView) SetMargin(top, right, bottom, left int) {
|
||||||
|
v.Margin = map[gui.Side]int{
|
||||||
|
gui.SideTop: top,
|
||||||
|
gui.SideRight: right,
|
||||||
|
gui.SideBottom: bottom,
|
||||||
|
gui.SideLeft: left,
|
||||||
|
}
|
||||||
|
}
|
190
views/view_scroll.go
Normal file
190
views/view_scroll.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScrollView is a gui.View which can hold an arbitrary large view.
|
||||||
|
// If the sub view does not fit into its bounds, scroll bars will be shown
|
||||||
|
type ScrollView struct {
|
||||||
|
gui.WrapperTmpl
|
||||||
|
|
||||||
|
buf *gui.Image
|
||||||
|
|
||||||
|
viewportWidth, viewportHeight int
|
||||||
|
viewWidth, viewHeight int
|
||||||
|
|
||||||
|
VerticalScrollOffset float64
|
||||||
|
HorizontalScrollOffset float64
|
||||||
|
ScrollStep int
|
||||||
|
|
||||||
|
targetOffset gui.Point
|
||||||
|
ScrollSpeed int
|
||||||
|
lastDraw time.Time
|
||||||
|
|
||||||
|
verticalScrollBar *ScrollbarView
|
||||||
|
horizontalScrollBar *ScrollbarView
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.Wrapper = &ScrollView{}
|
||||||
|
|
||||||
|
func NewScrollView(view gui.View) *ScrollView {
|
||||||
|
v := new(ScrollView)
|
||||||
|
v.SetView(view)
|
||||||
|
v.ScrollStep = 100
|
||||||
|
v.ScrollSpeed = 1000
|
||||||
|
v.lastDraw = time.Now()
|
||||||
|
v.verticalScrollBar = NewScrollbarView(gui.Vertical)
|
||||||
|
v.horizontalScrollBar = NewScrollbarView(gui.Horizontal)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) update(deltaTime float64) {
|
||||||
|
deltaSpeed := deltaTime * float64(v.ScrollSpeed)
|
||||||
|
|
||||||
|
if math.Abs(v.VerticalScrollOffset-float64(v.targetOffset.Y)) >= deltaSpeed {
|
||||||
|
vDir := iff(v.VerticalScrollOffset-float64(v.targetOffset.Y) < 0, 1.0, -1.0)
|
||||||
|
v.VerticalScrollOffset += deltaSpeed * vDir
|
||||||
|
} else {
|
||||||
|
v.VerticalScrollOffset = float64(v.targetOffset.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.Abs(v.HorizontalScrollOffset-float64(v.targetOffset.X)) >= deltaSpeed {
|
||||||
|
hDir := iff(v.HorizontalScrollOffset-float64(v.targetOffset.X) < 0, 1.0, -1.0)
|
||||||
|
v.HorizontalScrollOffset += deltaSpeed * hDir
|
||||||
|
} else {
|
||||||
|
v.HorizontalScrollOffset = float64(v.targetOffset.X)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
now := time.Now()
|
||||||
|
deltaTime := now.Sub(v.lastDraw).Seconds()
|
||||||
|
v.lastDraw = now
|
||||||
|
|
||||||
|
v.viewWidth = img.Bounds().Dx()
|
||||||
|
v.viewHeight = img.Bounds().Dy()
|
||||||
|
|
||||||
|
v.update(deltaTime)
|
||||||
|
|
||||||
|
v.ViewTmpl.Draw(img, ctx)
|
||||||
|
|
||||||
|
w, h := v.View().Layout(ctx)
|
||||||
|
w = iff(w >= 0, w, img.Bounds().Dx()-v.verticalScrollBar.Thickness)
|
||||||
|
h = iff(h >= 0, h, img.Bounds().Dy()-v.horizontalScrollBar.Thickness)
|
||||||
|
|
||||||
|
if v.buf == nil || v.buf.Bounds().Dx() != w || v.buf.Bounds().Dy() != h {
|
||||||
|
v.buf = gui.NewImage(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.limit()
|
||||||
|
v.View().Draw(v.buf, ctx)
|
||||||
|
|
||||||
|
scrollH, scrollV := v.determineViewportSize(img)
|
||||||
|
copyBufferWidth, copyBufferHeight := 0, 0
|
||||||
|
|
||||||
|
if scrollH {
|
||||||
|
copyBufferWidth = v.viewportWidth
|
||||||
|
} else {
|
||||||
|
copyBufferWidth = v.buf.Bounds().Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollV {
|
||||||
|
copyBufferHeight = v.viewportHeight
|
||||||
|
} else {
|
||||||
|
copyBufferHeight = v.buf.Bounds().Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy buffer
|
||||||
|
min := image.Pt(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))
|
||||||
|
size := image.Pt(copyBufferWidth, copyBufferHeight)
|
||||||
|
max := min.Add(size)
|
||||||
|
rect := image.Rectangle{min, max}
|
||||||
|
op := new(ebiten.DrawImageOptions)
|
||||||
|
//op.GeoM.Translate(-float64(min.X), -float64(min.Y))
|
||||||
|
img.DrawImage(v.buf.SubImage(rect).(*gui.Image), op) //-min.X, -min.Y)
|
||||||
|
|
||||||
|
if scrollV {
|
||||||
|
v.verticalScrollBar.ViewSize = v.buf.Bounds().Dy()
|
||||||
|
v.verticalScrollBar.ViewportSize = size.Y
|
||||||
|
v.verticalScrollBar.ViewportPos = int(v.VerticalScrollOffset)
|
||||||
|
v.verticalScrollBar.Draw(img.SubImage(image.Rect(img.Bounds().Max.X-v.verticalScrollBar.Thickness, 0, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollH {
|
||||||
|
v.horizontalScrollBar.ViewSize = v.buf.Bounds().Dx()
|
||||||
|
v.horizontalScrollBar.ViewportSize = size.X
|
||||||
|
v.horizontalScrollBar.ViewportPos = int(v.HorizontalScrollOffset)
|
||||||
|
v.horizontalScrollBar.Draw(img.SubImage(image.Rect(0, img.Bounds().Max.Y-v.horizontalScrollBar.Thickness, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) limit() {
|
||||||
|
if v.buf != nil {
|
||||||
|
v.targetOffset.Y = limit(v.targetOffset.Y, 0, max(v.buf.Bounds().Dy()-v.viewportHeight, 0))
|
||||||
|
v.targetOffset.X = limit(v.targetOffset.X, 0, max(v.buf.Bounds().Dx()-v.viewportWidth, 0))
|
||||||
|
v.VerticalScrollOffset = limit(v.VerticalScrollOffset, 0, float64(max(v.buf.Bounds().Dy()-v.viewportHeight, 0)))
|
||||||
|
v.HorizontalScrollOffset = limit(v.HorizontalScrollOffset, 0, float64(max(v.buf.Bounds().Dx()-v.viewportWidth, 0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) Scroll(horizontalOffset, verticalOffset int) {
|
||||||
|
v.targetOffset.Y = v.targetOffset.Y + verticalOffset
|
||||||
|
v.targetOffset.X = v.targetOffset.X + horizontalOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) determineViewportSize(img *gui.Image) (scrollbarH, scrollbarV bool) {
|
||||||
|
v.viewportWidth, v.viewportHeight = img.Bounds().Dx()-v.verticalScrollBar.Thickness, img.Bounds().Dy()-v.horizontalScrollBar.Thickness
|
||||||
|
scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight
|
||||||
|
scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth
|
||||||
|
|
||||||
|
if scrollbarV && !scrollbarH {
|
||||||
|
v.viewportHeight += v.horizontalScrollBar.Thickness
|
||||||
|
scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scrollbarV && scrollbarH {
|
||||||
|
v.viewportWidth += v.verticalScrollBar.Thickness
|
||||||
|
scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) OnMouseClick(event gui.MouseEvent) (consumed bool) {
|
||||||
|
return v.View().OnMouseClick(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) OnMouseMove(event gui.MouseEvent) (consumed bool) {
|
||||||
|
if event.Position.In(gui.D(v.viewportWidth, 0, v.viewWidth-v.viewportWidth, v.viewHeight)) {
|
||||||
|
return v.verticalScrollBar.OnMouseMove(event.SubtractPos(gui.P(v.viewportWidth, 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Position.In(gui.D(0, v.viewportHeight, v.viewWidth, v.viewHeight-v.viewportHeight)) {
|
||||||
|
return v.horizontalScrollBar.OnMouseMove(event.SubtractPos(gui.P(0, v.viewportHeight)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.View().OnMouseMove(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollView) OnMouseScroll(event gui.MouseEvent) (consumed bool) {
|
||||||
|
if v.View().OnMouseScroll(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Wheel.X != 0 || event.Wheel.Y != 0 {
|
||||||
|
v.Scroll(-event.Wheel.X*v.ScrollStep, -event.Wheel.Y*v.ScrollStep)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
73
views/view_scrollbar.go
Normal file
73
views/view_scrollbar.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScrollbarView struct {
|
||||||
|
gui.ViewTmpl
|
||||||
|
Orientation gui.Orientation
|
||||||
|
Thickness int
|
||||||
|
|
||||||
|
ViewSize int
|
||||||
|
ViewportPos int
|
||||||
|
ViewportSize int
|
||||||
|
|
||||||
|
width, height int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.View = &ScrollbarView{}
|
||||||
|
|
||||||
|
func NewScrollbarView(orientation gui.Orientation) *ScrollbarView {
|
||||||
|
return &ScrollbarView{
|
||||||
|
Orientation: orientation,
|
||||||
|
Thickness: 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollbarView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
if v.Orientation == gui.Horizontal {
|
||||||
|
return -1, v.Thickness
|
||||||
|
}
|
||||||
|
return v.Thickness, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollbarView) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
v.ViewTmpl.Draw(img, ctx)
|
||||||
|
v.width, v.height = img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
|
||||||
|
var x, y, w, h int
|
||||||
|
|
||||||
|
if v.Orientation == gui.Vertical {
|
||||||
|
x = img.Bounds().Min.X
|
||||||
|
y = img.Bounds().Min.Y + v.ViewportPos*img.Bounds().Dy()/v.ViewSize
|
||||||
|
w = img.Bounds().Max.X
|
||||||
|
h = v.ViewportSize*img.Bounds().Dy()/v.ViewSize + 1
|
||||||
|
} else if v.Orientation == gui.Horizontal {
|
||||||
|
x = img.Bounds().Min.X + v.ViewportPos*img.Bounds().Dx()/v.ViewSize
|
||||||
|
y = img.Bounds().Min.Y
|
||||||
|
w = v.ViewportSize*img.Bounds().Dx()/v.ViewSize + 1
|
||||||
|
h = img.Bounds().Max.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
img.DrawRect(image.Rect(x, y, x+w, y+h), v.Foreground(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ScrollbarView) OnMouseMove(event gui.MouseEvent) (consumed bool) {
|
||||||
|
if v.Orientation == gui.Vertical {
|
||||||
|
y := v.ViewportPos * v.height / v.ViewSize
|
||||||
|
h := v.ViewportSize*v.height/v.ViewSize + 1
|
||||||
|
|
||||||
|
fmt.Println(event.Buttons)
|
||||||
|
// TODO because MouseMove is only fired when mouse is moved, JustPressed button events don't work
|
||||||
|
|
||||||
|
if event.Position.Y > y && event.Position.Y < y+h && event.JustPressed(gui.MouseButtonLeft) {
|
||||||
|
fmt.Println("move start")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
30
views/view_text.go
Normal file
30
views/view_text.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.milar.in/milarin/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextView is a gui.View which prints text
|
||||||
|
type TextView struct {
|
||||||
|
gui.ViewTmpl
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ gui.View = &TextView{}
|
||||||
|
|
||||||
|
func NewTextView(text string) *TextView {
|
||||||
|
return &TextView{
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TextView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
|
||||||
|
s := ctx.MeasureString(v.Text, v.FontFace(ctx)).Size()
|
||||||
|
return s.X, s.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TextView) Draw(img *gui.Image, ctx gui.AppContext) {
|
||||||
|
v.ViewTmpl.Draw(img, ctx)
|
||||||
|
s := ctx.MeasureString(v.Text, v.FontFace(ctx))
|
||||||
|
img.DrawString(v.Text, v.FontFace(ctx), 0, -s.Min.Y, v.Foreground(ctx))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user