Global UI Popup Modals
* Adds global modal support in the pkg/modal/ package. It has easy Alert() and Confirm() methods to prompt the user before calling a callback function on affirmative response. * Modals have global app state: they're processed in the main loop in pkg/doodle.go similar to the global command shell. * When a modal is active, a semitransparent black frame covers the screen (gameplay loop paused, last game frame rendered below) and the modal window appears on top. * The developer console retains higher priority than the modal system and always renders on top. * Editor Mode: track when the level pixels have been modified, and confirm the user about unsaved changes when they attempt to close the level (New, Open, Close, etc.) * Global: the Escape key no longer immediately shuts down the game, but will confirm the user's intent via a modal. * File->Quit in the Editor Mode also invokes the confirm shutdown modal.
This commit is contained in:
parent
bc02f2c685
commit
336a949ed0
4
go.mod
4
go.mod
|
@ -2,6 +2,9 @@ module git.kirsle.net/apps/doodle
|
|||
|
||||
go 1.15
|
||||
|
||||
replace git.kirsle.net/go/render => /home/kirsle/SketchyMaze/render
|
||||
replace git.kirsle.net/go/ui => /home/kirsle/SketchyMaze/ui
|
||||
|
||||
require (
|
||||
git.kirsle.net/go/audio v0.0.0-20200429055451-ae3b0695ba6f
|
||||
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b
|
||||
|
@ -19,4 +22,5 @@ require (
|
|||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 // indirect
|
||||
)
|
||||
|
|
23
go.sum
23
go.sum
|
@ -16,9 +16,13 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
|||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac h1:kYPjbEN6YPYWWHI6ky1J813KzIq/8+Wg4TO4xU7A/KU=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
|
@ -34,25 +38,44 @@ github.com/veandco/go-sdl2 v0.4.4 h1:coOJGftOdvNvGoUIZmm4XD+ZRQF4mg9ZVHmH3/42zFQ
|
|||
github.com/veandco/go-sdl2 v0.4.4/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible h1:wapg9xDUZDzGCNFlwc5SqI1rvcciqcxEHac4CYj89xI=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b h1:zSzQJAznWxAh9fZxiPy2FZo+ZZEYoYFYYDYdOrU7AaM=
|
||||
golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
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-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
|
@ -33,6 +34,9 @@ func (c Command) Run(d *Doodle) error {
|
|||
case "echo":
|
||||
d.Flash(c.ArgsLiteral)
|
||||
return nil
|
||||
case "alert":
|
||||
modal.Alert(c.ArgsLiteral)
|
||||
return nil
|
||||
case "new":
|
||||
return c.New(d)
|
||||
case "save":
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||
"git.kirsle.net/apps/doodle/pkg/native"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
golog "git.kirsle.net/go/log"
|
||||
|
@ -84,10 +85,15 @@ func (d *Doodle) Title() string {
|
|||
|
||||
// SetupEngine sets up the rendering engine.
|
||||
func (d *Doodle) SetupEngine() error {
|
||||
// Set up the rendering engine (SDL2, etc.)
|
||||
if err := d.Engine.Setup(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.engineReady = true
|
||||
|
||||
// Initialize the UI modal manager.
|
||||
modal.Initialize(d.Engine)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -113,7 +119,6 @@ func (d *Doodle) Run() error {
|
|||
|
||||
// Poll for events.
|
||||
ev, err := d.Engine.Poll()
|
||||
// log.Error("Button1 is: %+v", ev.Button1)
|
||||
shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY)
|
||||
if err != nil {
|
||||
log.Error("event poll error: %s", err)
|
||||
|
@ -132,8 +137,8 @@ func (d *Doodle) Run() error {
|
|||
// Global event handlers.
|
||||
if ev.Escape {
|
||||
log.Error("Escape key pressed, shutting down")
|
||||
d.running = false
|
||||
break
|
||||
d.ConfirmExit()
|
||||
continue
|
||||
}
|
||||
|
||||
if ev.KeyDown("F1") {
|
||||
|
@ -148,17 +153,24 @@ func (d *Doodle) Run() error {
|
|||
ev.SetKeyDown("F4", false)
|
||||
}
|
||||
|
||||
// Run the scene's logic.
|
||||
err = d.Scene.Loop(d, ev)
|
||||
if err != nil {
|
||||
return err
|
||||
// Is a UI modal active?
|
||||
if modal.Handled(ev) == false {
|
||||
// Run the scene's logic.
|
||||
err = d.Scene.Loop(d, ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draw the scene.
|
||||
d.Scene.Draw(d)
|
||||
|
||||
// Draw the shell.
|
||||
// Draw modals on top of the game UI.
|
||||
modal.Draw()
|
||||
|
||||
// Draw the shell, always on top of UI and modals.
|
||||
err = d.shell.Draw(d, ev)
|
||||
if err != nil {
|
||||
log.Error("shell error: %s", err)
|
||||
|
@ -199,6 +211,15 @@ func (d *Doodle) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ConfirmExit may shut down Doodle gracefully after showing the user a
|
||||
// confirmation modal.
|
||||
func (d *Doodle) ConfirmExit() {
|
||||
modal.Confirm("Are you sure you want to quit %s?", branding.AppName).
|
||||
WithTitle("Confirm Quit").Then(func() {
|
||||
d.running = false
|
||||
})
|
||||
}
|
||||
|
||||
// NewMap loads a new map in Edit Mode.
|
||||
func (d *Doodle) NewMap() {
|
||||
log.Info("Starting a new map")
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
|
@ -160,6 +161,19 @@ func (s *EditorScene) Playtest() {
|
|||
})
|
||||
}
|
||||
|
||||
// ConfirmUnload may pop up a confirmation modal to save the level before the
|
||||
// user performs an action that may close the level, such as click File->New.
|
||||
func (s *EditorScene) ConfirmUnload(fn func()) {
|
||||
if !s.UI.Canvas.Modified() {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
|
||||
modal.Confirm(
|
||||
"This level has unsaved changes. Are you sure you\nwant to continue and lose your changes?",
|
||||
).WithTitle("Confirm Closing Level").Then(fn)
|
||||
}
|
||||
|
||||
// Loop the editor scene.
|
||||
func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
|
||||
// Update debug overlay values.
|
||||
|
@ -265,6 +279,9 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
|||
m.Palette = s.UI.Canvas.Palette
|
||||
m.Chunker = s.UI.Canvas.Chunker()
|
||||
|
||||
// Clear the modified flag on the level.
|
||||
s.UI.Canvas.SetModified(false)
|
||||
|
||||
return m.WriteFile(filename)
|
||||
}
|
||||
|
||||
|
@ -307,6 +324,9 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
|||
d.Palette = s.UI.Canvas.Palette
|
||||
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
|
||||
|
||||
// Clear the modified flag on the level.
|
||||
s.UI.Canvas.SetModified(false)
|
||||
|
||||
// Save it to their profile directory.
|
||||
filename = userdir.DoodadPath(filename)
|
||||
log.Info("Write Doodad: %s", filename)
|
||||
|
|
|
@ -2,7 +2,6 @@ package doodle
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
|
@ -483,21 +482,25 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
|||
// File menu
|
||||
fileMenu := menu.AddMenu("File")
|
||||
fileMenu.AddItemAccel("New level", "Ctrl-N", func() {
|
||||
d.GotoNewMenu()
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.GotoNewMenu()
|
||||
})
|
||||
})
|
||||
if !balance.FreeVersion {
|
||||
fileMenu.AddItem("New doodad", func() {
|
||||
d.Prompt("Doodad size [100]>", func(answer string) {
|
||||
size := balance.DoodadSize
|
||||
if answer != "" {
|
||||
i, err := strconv.Atoi(answer)
|
||||
if err != nil {
|
||||
d.Flash("Error: Doodad size must be a number.")
|
||||
return
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.Prompt("Doodad size [100]>", func(answer string) {
|
||||
size := balance.DoodadSize
|
||||
if answer != "" {
|
||||
i, err := strconv.Atoi(answer)
|
||||
if err != nil {
|
||||
d.Flash("Error: Doodad size must be a number.")
|
||||
return
|
||||
}
|
||||
size = i
|
||||
}
|
||||
size = i
|
||||
}
|
||||
d.NewDoodad(size)
|
||||
d.NewDoodad(size)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -520,15 +523,18 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
|||
})
|
||||
})
|
||||
fileMenu.AddItemAccel("Open...", "Ctrl-O", func() {
|
||||
d.GotoLoadMenu()
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.GotoLoadMenu()
|
||||
})
|
||||
})
|
||||
fileMenu.AddSeparator()
|
||||
fileMenu.AddItem("Close "+drawingType, func() {
|
||||
d.Goto(&MainScene{})
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.Goto(&MainScene{})
|
||||
})
|
||||
})
|
||||
fileMenu.AddItemAccel("Quit", "Ctrl-Q", func() {
|
||||
// TODO graceful shutdown
|
||||
os.Exit(0)
|
||||
d.ConfirmExit()
|
||||
})
|
||||
|
||||
////////
|
||||
|
|
74
pkg/modal/alert.go
Normal file
74
pkg/modal/alert.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package modal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Alert pops up an alert box modal.
|
||||
func Alert(message string, args ...interface{}) *Modal {
|
||||
if !ready {
|
||||
panic("modal.Alert(): not ready")
|
||||
} else if current != nil {
|
||||
return current
|
||||
}
|
||||
|
||||
// Reset the supervisor.
|
||||
supervisor = ui.NewSupervisor()
|
||||
|
||||
m := &Modal{
|
||||
title: "Alert",
|
||||
message: fmt.Sprintf(message, args...),
|
||||
}
|
||||
m.window = makeAlert(m)
|
||||
|
||||
center(m.window)
|
||||
current = m
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// alertWindow creates the ui.Window for the Alert modal.
|
||||
func makeAlert(m *Modal) *ui.Window {
|
||||
win := ui.NewWindow("Alert")
|
||||
_, title := win.TitleBar()
|
||||
title.TextVariable = &m.title
|
||||
|
||||
msgFrame := ui.NewFrame("Alert Message")
|
||||
win.Pack(msgFrame, ui.Pack{
|
||||
Side: ui.N,
|
||||
})
|
||||
|
||||
msg := ui.NewLabel(ui.Label{
|
||||
TextVariable: &m.message,
|
||||
Font: balance.UIFont,
|
||||
})
|
||||
msgFrame.Pack(msg, ui.Pack{
|
||||
Side: ui.N,
|
||||
})
|
||||
|
||||
button := ui.NewButton("Ok Button", ui.NewLabel(ui.Label{
|
||||
Text: "Ok",
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
button.Handle(ui.Click, func(ev ui.EventData) error {
|
||||
log.Info("clicked!")
|
||||
m.Dismiss(true)
|
||||
return nil
|
||||
})
|
||||
win.Pack(button, ui.Pack{
|
||||
Side: ui.N,
|
||||
PadY: 4,
|
||||
})
|
||||
|
||||
button.Compute(engine)
|
||||
supervisor.Add(button)
|
||||
|
||||
win.Compute(engine)
|
||||
win.Supervise(supervisor)
|
||||
|
||||
return win
|
||||
}
|
91
pkg/modal/confirm.go
Normal file
91
pkg/modal/confirm.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package modal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Confirm pops up an Ok/Cancel modal.
|
||||
func Confirm(message string, args ...interface{}) *Modal {
|
||||
if !ready {
|
||||
panic("modal.Confirm(): not ready")
|
||||
} else if current != nil {
|
||||
return current
|
||||
}
|
||||
|
||||
// Reset the supervisor.
|
||||
supervisor = ui.NewSupervisor()
|
||||
|
||||
m := &Modal{
|
||||
title: "Confirm",
|
||||
message: fmt.Sprintf(message, args...),
|
||||
}
|
||||
m.window = makeConfirm(m)
|
||||
|
||||
center(m.window)
|
||||
current = m
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// makeConfirm creates the ui.Window for the Confirm modal.
|
||||
func makeConfirm(m *Modal) *ui.Window {
|
||||
win := ui.NewWindow("Confirm")
|
||||
_, title := win.TitleBar()
|
||||
title.TextVariable = &m.title
|
||||
|
||||
msgFrame := ui.NewFrame("Confirm Message")
|
||||
win.Pack(msgFrame, ui.Pack{
|
||||
Side: ui.N,
|
||||
})
|
||||
|
||||
msg := ui.NewLabel(ui.Label{
|
||||
TextVariable: &m.message,
|
||||
Font: balance.UIFont,
|
||||
})
|
||||
msgFrame.Pack(msg, ui.Pack{
|
||||
Side: ui.N,
|
||||
})
|
||||
|
||||
// Ok/Cancel button bar.
|
||||
btnBar := ui.NewFrame("Button Bar")
|
||||
msgFrame.Pack(btnBar, ui.Pack{
|
||||
Side: ui.N,
|
||||
PadY: 4,
|
||||
})
|
||||
|
||||
for _, btn := range []struct {
|
||||
Label string
|
||||
F func(ui.EventData) error
|
||||
}{
|
||||
{"Ok", func(ev ui.EventData) error {
|
||||
m.Dismiss(true)
|
||||
return nil
|
||||
}},
|
||||
{"Cancel", func(ev ui.EventData) error {
|
||||
m.Dismiss(false)
|
||||
return nil
|
||||
}},
|
||||
} {
|
||||
btn := btn
|
||||
button := ui.NewButton(btn.Label+"Button", ui.NewLabel(ui.Label{
|
||||
Text: btn.Label,
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
button.Handle(ui.Click, btn.F)
|
||||
button.Compute(engine)
|
||||
supervisor.Add(button)
|
||||
|
||||
btnBar.Pack(button, ui.Pack{
|
||||
Side: ui.W,
|
||||
PadX: 2,
|
||||
})
|
||||
}
|
||||
|
||||
win.Compute(engine)
|
||||
win.Supervise(supervisor)
|
||||
|
||||
return win
|
||||
}
|
112
pkg/modal/modal.go
Normal file
112
pkg/modal/modal.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Package modal provides UI pop-up modals for Doodle.
|
||||
package modal
|
||||
|
||||
import (
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Package global variables.
|
||||
var (
|
||||
ready bool // Has been initialized with a render.Engine
|
||||
current *Modal // Current modal object, nil if no modal active.
|
||||
engine render.Engine
|
||||
window render.Rect // cached window dimensions
|
||||
|
||||
supervisor *ui.Supervisor
|
||||
screen *ui.Frame
|
||||
)
|
||||
|
||||
// Initialize the modal package.
|
||||
func Initialize(e render.Engine) {
|
||||
engine = e
|
||||
supervisor = ui.NewSupervisor()
|
||||
|
||||
width, height := engine.WindowSize()
|
||||
window = render.NewRect(width, height)
|
||||
|
||||
screen = ui.NewFrame("Modal Screen")
|
||||
screen.SetBackground(render.RGBA(1, 1, 1, 128))
|
||||
screen.Resize(window)
|
||||
screen.Compute(e)
|
||||
|
||||
ready = true
|
||||
}
|
||||
|
||||
// Reset the modal state (closing all modals).
|
||||
func Reset() {
|
||||
supervisor = nil
|
||||
current = nil
|
||||
}
|
||||
|
||||
// Handled runs the modal manager's logic. Returns true if a modal
|
||||
// is presently active, to signal to Doodle not to run game logic.
|
||||
func Handled(ev *event.State) bool {
|
||||
if !ready || current == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
supervisor.Loop(ev)
|
||||
|
||||
// Has the window changed size?
|
||||
size := render.NewRect(engine.WindowSize())
|
||||
if size != window {
|
||||
window = size
|
||||
screen.Resize(window)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Draw the modal UI to the screen.
|
||||
func Draw() {
|
||||
if ready && current != nil {
|
||||
screen.Present(engine, render.Origin)
|
||||
supervisor.Present(engine)
|
||||
}
|
||||
}
|
||||
|
||||
// Center the window on screen.
|
||||
func center(win *ui.Window) {
|
||||
var modSize = win.Size()
|
||||
var moveTo = render.Point{
|
||||
X: (window.W / 2) - (modSize.W / 2),
|
||||
Y: (window.H / 4) - (modSize.H / 2),
|
||||
}
|
||||
win.MoveTo(moveTo)
|
||||
|
||||
// HACK: ideally the modal should auto-size itself, but currently
|
||||
// the body of the window juts out the right and bottom side by
|
||||
// a few pixels. Fix the underlying problem later, for now we
|
||||
// set the modal size to big enough to hide the problem.
|
||||
win.Children()[0].Resize(render.NewRect(modSize.W+12, modSize.H+12))
|
||||
}
|
||||
|
||||
// Modal is an instance of a modal, i.e. Alert or Confirm.
|
||||
type Modal struct {
|
||||
title string
|
||||
message string
|
||||
window *ui.Window
|
||||
callback func()
|
||||
}
|
||||
|
||||
// WithTitle sets the title of the modal.
|
||||
func (m *Modal) WithTitle(title string) *Modal {
|
||||
m.title = title
|
||||
return m
|
||||
}
|
||||
|
||||
// Then calls a function after the modal is answered.
|
||||
func (m *Modal) Then(f func()) *Modal {
|
||||
m.callback = f
|
||||
return m
|
||||
}
|
||||
|
||||
// Dismiss the modal and optionally call the callback function.
|
||||
func (m *Modal) Dismiss(call bool) {
|
||||
if call && m.callback != nil {
|
||||
m.callback()
|
||||
}
|
||||
Reset()
|
||||
}
|
15
pkg/modal/modal_test.go
Normal file
15
pkg/modal/modal_test.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package modal_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
modal "git.kirsle.net/apps/doodle/pkg/modal"
|
||||
)
|
||||
|
||||
func ExampleAlert() {
|
||||
alert := modal.Alert("Permission Denied").WithTitle("Error").Then(func() {
|
||||
fmt.Println("Alert button answered!")
|
||||
})
|
||||
|
||||
_ = alert
|
||||
}
|
|
@ -48,8 +48,9 @@ type Canvas struct {
|
|||
NoLimitScroll bool
|
||||
|
||||
// Underlying chunk data for the drawing.
|
||||
level *level.Level
|
||||
chunks *level.Chunker
|
||||
level *level.Level
|
||||
chunks *level.Chunker
|
||||
modified bool // set to True when the drawing has been modified, like in Editor Mode.
|
||||
|
||||
// Actors to superimpose on top of the drawing.
|
||||
actor *Actor // if this canvas IS an actor
|
||||
|
@ -131,6 +132,7 @@ func NewCanvas(size int, editable bool) *Canvas {
|
|||
func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
|
||||
w.Palette = p
|
||||
w.chunks = g
|
||||
w.modified = false
|
||||
|
||||
if len(w.Palette.Swatches) > 0 {
|
||||
w.SetSwatch(w.Palette.Swatches[0])
|
||||
|
|
|
@ -8,6 +8,18 @@ import (
|
|||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Modified returns whether the canvas has been modified since it was last
|
||||
// loaded. Methods like Load and LoadFile will set modified to false, and
|
||||
// commitStroke sets it to true.
|
||||
func (w *Canvas) Modified() bool {
|
||||
return w.modified
|
||||
}
|
||||
|
||||
// SetModified sets the modified bit on the canvas.
|
||||
func (w *Canvas) SetModified(v bool) {
|
||||
w.modified = v
|
||||
}
|
||||
|
||||
// commitStroke is the common function that applies a stroke the user is
|
||||
// actively drawing onto the canvas. This is for Edit Mode.
|
||||
func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) {
|
||||
|
@ -16,6 +28,9 @@ func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) {
|
|||
return
|
||||
}
|
||||
|
||||
// Mark the canvas as modified.
|
||||
w.modified = true
|
||||
|
||||
var (
|
||||
deleting = w.currentStroke.Shape == drawtool.Eraser
|
||||
dedupe = map[render.Point]interface{}{} // don't revisit the same point twice
|
||||
|
|
Loading…
Reference in New Issue
Block a user