initial commit

This commit is contained in:
milarin 2023-01-22 12:38:03 +01:00
commit b832628da8
27 changed files with 2051 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
old
*.otf

92
app.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

19
tmpl_view.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}