Menu Bar Update

* Integrate the new ui.MenuBar into the Editor Scene.
  * File: New Level/Doodad, Save [as], Open, Close, Exit
  * Edit: Undo, Redo, Level options
  * Level: Playtest
  * Tools: Debug overlay, Command shell
  * Help: User Manual, About
* Add an About dialog accessible from the Help menu.
This commit is contained in:
Noah 2020-06-04 21:55:54 -07:00
parent 82d50f1c91
commit 2c032f1df7
5 changed files with 280 additions and 145 deletions

View File

@ -3,7 +3,11 @@ package balance
import "runtime" import "runtime"
// Runtime environment settings. // Runtime environment settings.
var Runtime rtc var (
Runtime rtc
GuidebookPath = "./guidebook/index.html"
)
type rtc struct { type rtc struct {
Platform platform Platform platform

View File

@ -5,6 +5,8 @@ const (
AppName = "Project: Doodle" AppName = "Project: Doodle"
Summary = "A drawing-based maze game" Summary = "A drawing-based maze game"
Version = "0.1.0-alpha" Version = "0.1.0-alpha"
Website = "https://www.kirsle.net/tagged/Doodle"
Copyright = "2020 Noah Petherbridge"
// Update check URL // Update check URL
UpdateCheckJSON = "https://download.sketchymaze.com/version.json" UpdateCheckJSON = "https://download.sketchymaze.com/version.json"

View File

@ -10,6 +10,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
golog "git.kirsle.net/go/log" golog "git.kirsle.net/go/log"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -134,7 +135,11 @@ func (d *Doodle) Run() error {
break break
} }
if ev.KeyDown("F3") { if ev.KeyDown("F1") {
// TODO: launch the guidebook.
native.OpenURL(balance.GuidebookPath)
ev.SetKeyDown("F1", false)
} else if ev.KeyDown("F3") {
DebugOverlay = !DebugOverlay DebugOverlay = !DebugOverlay
ev.SetKeyDown("F3", false) ev.SetKeyDown("F3", false)
} else if ev.KeyDown("F4") { } else if ev.KeyDown("F4") {

View File

@ -2,6 +2,7 @@ package doodle
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -11,6 +12,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -36,16 +38,18 @@ type EditorUI struct {
cursor render.Point // remember the cursor position in Loop cursor render.Point // remember the cursor position in Loop
// Widgets // Widgets
screen *ui.Frame // full-window parent frame for layout
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
Canvas *uix.Canvas Canvas *uix.Canvas
Workspace *ui.Frame Workspace *ui.Frame
MenuBar *ui.Frame MenuBar *ui.MenuBar
StatusBar *ui.Frame StatusBar *ui.Frame
ToolBar *ui.Frame ToolBar *ui.Frame
PlayButton *ui.Button PlayButton *ui.Button
// Popup windows. // Popup windows.
levelSettingsWindow *ui.Window levelSettingsWindow *ui.Window
aboutWindow *ui.Window
// Palette window. // Palette window.
Palette *ui.Window Palette *ui.Window
@ -81,6 +85,11 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusScrollText: "Hello world", StatusScrollText: "Hello world",
} }
// The screen is a full-window-sized frame for laying out the UI.
u.screen = ui.NewFrame("screen")
u.screen.Resize(render.NewRect(d.width, d.height))
u.screen.Compute(d.Engine)
// Default tool in the toolbox. // Default tool in the toolbox.
u.activeTool = drawtool.PencilTool.String() u.activeTool = drawtool.PencilTool.String()
@ -98,6 +107,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
u.ToolBar = u.SetupToolbar(d) u.ToolBar = u.SetupToolbar(d)
u.Workspace = u.SetupWorkspace(d) // important that this is last! u.Workspace = u.SetupWorkspace(d) // important that this is last!
log.Error("menu size: %s", u.MenuBar.Rect())
u.screen.Pack(u.MenuBar, ui.Pack{
Side: ui.N,
FillX: true,
})
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{ u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
Text: "Play (P)", Text: "Play (P)",
Font: balance.PlayButtonFont, Font: balance.PlayButtonFont,
@ -132,14 +147,10 @@ func (u *EditorUI) FinishSetup(d *Doodle) {
// Resized handles the window being resized so we can recompute the widgets. // Resized handles the window being resized so we can recompute the widgets.
func (u *EditorUI) Resized(d *Doodle) { func (u *EditorUI) Resized(d *Doodle) {
// Menu Bar frame. // Resize the screen frame to fill the window.
{ u.screen.Resize(render.NewRect(d.width, d.height))
u.MenuBar.Configure(ui.Config{ u.screen.Compute(d.Engine)
Width: d.width, menuHeight := 20 // TODO: ideally the MenuBar should know its own height and we can ask
Background: render.Black,
})
u.MenuBar.Compute(d.Engine)
}
// Status Bar. // Status Bar.
{ {
@ -161,14 +172,14 @@ func (u *EditorUI) Resized(d *Doodle) {
}) })
u.Palette.MoveTo(render.NewPoint( u.Palette.MoveTo(render.NewPoint(
u.d.width-u.Palette.BoxSize().W, u.d.width-u.Palette.BoxSize().W,
u.MenuBar.BoxSize().H, menuHeight,
)) ))
u.Palette.Compute(d.Engine) u.Palette.Compute(d.Engine)
u.scrollDoodadFrame(0) u.scrollDoodadFrame(0)
} }
var innerHeight = u.d.height - u.MenuBar.Size().H - u.StatusBar.Size().H var innerHeight = u.d.height - menuHeight - u.StatusBar.Size().H
// Tool Bar. // Tool Bar.
{ {
@ -178,7 +189,7 @@ func (u *EditorUI) Resized(d *Doodle) {
}) })
u.ToolBar.MoveTo(render.NewPoint( u.ToolBar.MoveTo(render.NewPoint(
0, 0,
u.MenuBar.BoxSize().H, menuHeight,
)) ))
u.ToolBar.Compute(d.Engine) u.ToolBar.Compute(d.Engine)
} }
@ -189,11 +200,11 @@ func (u *EditorUI) Resized(d *Doodle) {
frame := u.Workspace frame := u.Workspace
frame.MoveTo(render.NewPoint( frame.MoveTo(render.NewPoint(
u.ToolBar.Size().W, u.ToolBar.Size().W,
u.MenuBar.Size().H, menuHeight,
)) ))
frame.Resize(render.NewRect( frame.Resize(render.NewRect(
d.width-u.Palette.Size().W-u.ToolBar.Size().W, d.width-u.Palette.Size().W-u.ToolBar.Size().W,
d.height-u.MenuBar.Size().H-u.StatusBar.Size().H, d.height-menuHeight-u.StatusBar.Size().H,
)) ))
frame.Compute(d.Engine) frame.Compute(d.Engine)
@ -274,7 +285,8 @@ func (u *EditorUI) Loop(ev *event.State) error {
// Only forward events to the Canvas if the UI hasn't stopped them. // Only forward events to the Canvas if the UI hasn't stopped them.
// Also ignore events if a managed ui.Window is overlapping the canvas. // Also ignore events if a managed ui.Window is overlapping the canvas.
if !(stopPropagation || u.Supervisor.IsPointInWindow(u.cursor)) { // Also ignore if an active modal (popup menu) is on screen.
if !(stopPropagation || u.Supervisor.IsPointInWindow(u.cursor) || u.Supervisor.GetModal() != nil) {
u.Canvas.Loop(ev) u.Canvas.Loop(ev)
} }
return nil return nil
@ -298,6 +310,8 @@ func (u *EditorUI) Present(e render.Engine) {
u.ToolBar.Present(e, u.ToolBar.Point()) u.ToolBar.Present(e, u.ToolBar.Point())
u.PlayButton.Present(e, u.PlayButton.Point()) u.PlayButton.Present(e, u.PlayButton.Point())
u.screen.Present(e, render.Origin)
// Are we dragging a Doodad canvas? // Are we dragging a Doodad canvas?
if u.Supervisor.IsDragging() { if u.Supervisor.IsDragging() {
if actor := u.DraggableActor; actor != nil { if actor := u.DraggableActor; actor != nil {
@ -421,14 +435,18 @@ func (u *EditorUI) ExpandCanvas(e render.Engine) {
} }
// SetupMenuBar sets up the menu bar. // SetupMenuBar sets up the menu bar.
func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
frame := ui.NewFrame("MenuBar") menu := ui.NewMenuBar("Main Menu")
// Save and Save As common menu handler // Save and Save As common menu handler
var saveFunc func(filename string) var (
drawingType string
saveFunc func(filename string)
)
switch u.Scene.DrawingType { switch u.Scene.DrawingType {
case enum.LevelDrawing: case enum.LevelDrawing:
drawingType = "level"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := u.Scene.SaveLevel(filename); err != nil { if err := u.Scene.SaveLevel(filename); err != nil {
d.Flash("Error: %s", err) d.Flash("Error: %s", err)
@ -437,6 +455,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
} }
} }
case enum.DoodadDrawing: case enum.DoodadDrawing:
drawingType = "doodad"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := u.Scene.SaveDoodad(filename); err != nil { if err := u.Scene.SaveDoodad(filename); err != nil {
d.Flash("Error: %s", err) d.Flash("Error: %s", err)
@ -448,21 +467,14 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
d.Flash("Error: Scene.DrawingType is not a valid type") d.Flash("Error: Scene.DrawingType is not a valid type")
} }
type menuButton struct { ////////
Text string // File menu
Click func(ui.EventData) error fileMenu := menu.AddMenu("File")
} fileMenu.AddItemAccel("New level", "Ctrl-N", func() {
buttons := []menuButton{
menuButton{
Text: "New Level",
Click: func(ed ui.EventData) error {
d.GotoNewMenu() d.GotoNewMenu()
return nil })
}, if !balance.FreeVersion {
}, fileMenu.AddItem("New doodad", func() {
menuButton{
Text: "New Doodad",
Click: func(ed ui.EventData) error {
d.Prompt("Doodad size [100]>", func(answer string) { d.Prompt("Doodad size [100]>", func(answer string) {
size := balance.DoodadSize size := balance.DoodadSize
if answer != "" { if answer != "" {
@ -475,13 +487,9 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
} }
d.NewDoodad(size) d.NewDoodad(size)
}) })
})
return nil }
}, fileMenu.AddItemAccel("Save", "Ctrl-S", func() {
},
menuButton{
Text: "Save",
Click: func(ed ui.EventData) error {
if u.Scene.filename != "" { if u.Scene.filename != "" {
saveFunc(u.Scene.filename) saveFunc(u.Scene.filename)
} else { } else {
@ -491,30 +499,37 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
} }
}) })
} }
return nil })
}, fileMenu.AddItem("Save as...", func() {
},
menuButton{
Text: "Save as...",
Click: func(ed ui.EventData) error {
d.Prompt("Save as filename>", func(answer string) { d.Prompt("Save as filename>", func(answer string) {
if answer != "" { if answer != "" {
saveFunc(answer) saveFunc(answer)
} }
}) })
return nil })
}, fileMenu.AddItemAccel("Open...", "Ctrl-O", func() {
},
menuButton{
Text: "Load",
Click: func(ed ui.EventData) error {
d.GotoLoadMenu() d.GotoLoadMenu()
return nil })
}, fileMenu.AddSeparator()
}, fileMenu.AddItem("Close "+drawingType, func() {
menuButton{ d.Goto(&MainScene{})
Text: "Options", })
Click: func(ed ui.EventData) error { fileMenu.AddItemAccel("Quit", "Ctrl-Q", func() {
// TODO graceful shutdown
os.Exit(0)
})
////////
// Edit menu
editMenu := menu.AddMenu("Edit")
editMenu.AddItemAccel("Undo", "Ctrl-Z", func() {
u.Canvas.UndoStroke()
})
editMenu.AddItemAccel("Redo", "Shift-Ctrl-Y", func() {
u.Canvas.RedoStroke()
})
editMenu.AddSeparator()
editMenu.AddItem("Level options", func() {
scene, _ := d.Scene.(*EditorScene) scene, _ := d.Scene.(*EditorScene)
log.Info("Opening the window") log.Info("Opening the window")
@ -547,37 +562,60 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
} else { } else {
u.levelSettingsWindow.Show() u.levelSettingsWindow.Show()
} }
return nil
},
},
}
for _, btn := range buttons {
if balance.FreeVersion {
if btn.Text == "New Doodad" {
continue
}
}
w := ui.NewButton(btn.Text, ui.NewLabel(ui.Label{
Text: btn.Text,
Font: balance.MenuFont,
}))
w.Configure(ui.Config{
BorderSize: 1,
OutlineSize: 0,
}) })
w.Handle(ui.MouseUp, btn.Click)
u.Supervisor.Add(w) ////////
frame.Pack(w, ui.Pack{ // Level menu
Side: ui.W, if drawingType == "level" {
PadX: 1, levelMenu := menu.AddMenu("Level")
levelMenu.AddItemAccel("Playtest", "P", func() {
u.Scene.Playtest()
}) })
} }
frame.Compute(d.Engine) ////////
return frame // Tools menu
toolMenu := menu.AddMenu("Tools")
toolMenu.AddItemAccel("Debug overlay", "F3", func() {
DebugOverlay = !DebugOverlay
if DebugOverlay {
d.Flash("Debug overlay enabled. Press F3 to turn it off.")
}
})
toolMenu.AddItemAccel("Command shell", "Enter", func() {
d.shell.Open = true
})
////////
// Help menu
helpMenu := menu.AddMenu("Help")
helpMenu.AddItemAccel("User Manual", "F1", func() {
// TODO: launch the guidebook.
native.OpenURL(balance.GuidebookPath)
})
helpMenu.AddItem("About", func() {
if u.aboutWindow == nil {
u.aboutWindow = windows.NewAboutWindow(windows.About{
Supervisor: u.Supervisor,
Engine: d.Engine,
})
u.aboutWindow.Compute(d.Engine)
u.aboutWindow.Supervise(u.Supervisor)
// Center the window.
u.aboutWindow.MoveTo(render.Point{
X: (d.width / 2) - (u.aboutWindow.Size().W / 2),
Y: 60,
})
}
u.aboutWindow.Show()
})
menu.Supervise(u.Supervisor)
menu.Compute(d.Engine)
log.Error("Setup MenuBar: %s\n", menu.Size())
return menu
} }
// SetupStatusBar sets up the status bar widget along the bottom of the window. // SetupStatusBar sets up the status bar widget along the bottom of the window.

86
pkg/windows/about.go Normal file
View File

@ -0,0 +1,86 @@
package windows
import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// About window.
type About struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
}
// NewAboutWindow initializes the window.
func NewAboutWindow(cfg About) *ui.Window {
window := ui.NewWindow("About " + branding.AppName)
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: 400,
Height: 170,
Background: render.Grey,
})
text := ui.NewLabel(ui.Label{
Text: fmt.Sprintf("%s is a drawing-based maze game.\n\n"+
"Copyright © %s.\nAll rights reserved.\n\n"+
"Version %s",
branding.AppName,
branding.Copyright,
branding.Version,
),
})
window.Pack(text, ui.Pack{
Side: ui.N,
Padding: 8,
})
frame := ui.NewFrame("Button frame")
buttons := []struct {
label string
f func()
}{
{"Website", func() {
native.OpenURL(branding.Website)
}},
{"Open Source Licenses", func() {
// TODO: open file
native.OpenURL("./Open Source Licenses.md")
}},
}
for _, button := range buttons {
button := button
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
Text: button.label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.f()
return nil
})
btn.Compute(cfg.Engine)
cfg.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 4,
Expand: true,
Fill: true,
})
}
window.Pack(frame, ui.Pack{
Side: ui.N,
Padding: 8,
})
return window
}