diff --git a/pkg/balance/runtime.go b/pkg/balance/runtime.go index 2864832..27effb3 100644 --- a/pkg/balance/runtime.go +++ b/pkg/balance/runtime.go @@ -3,7 +3,11 @@ package balance import "runtime" // Runtime environment settings. -var Runtime rtc +var ( + Runtime rtc + + GuidebookPath = "./guidebook/index.html" +) type rtc struct { Platform platform diff --git a/pkg/branding/branding.go b/pkg/branding/branding.go index f7508e7..fc75262 100644 --- a/pkg/branding/branding.go +++ b/pkg/branding/branding.go @@ -2,9 +2,11 @@ package branding // Constants for branding and version information. const ( - AppName = "Project: Doodle" - Summary = "A drawing-based maze game" - Version = "0.1.0-alpha" + AppName = "Project: Doodle" + Summary = "A drawing-based maze game" + Version = "0.1.0-alpha" + Website = "https://www.kirsle.net/tagged/Doodle" + Copyright = "2020 Noah Petherbridge" // Update check URL UpdateCheckJSON = "https://download.sketchymaze.com/version.json" diff --git a/pkg/doodle.go b/pkg/doodle.go index b097e53..0494b4f 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -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/native" "git.kirsle.net/apps/doodle/pkg/shmem" golog "git.kirsle.net/go/log" "git.kirsle.net/go/render" @@ -134,7 +135,11 @@ func (d *Doodle) Run() error { 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 ev.SetKeyDown("F3", false) } else if ev.KeyDown("F4") { diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 7aa9270..fe66bde 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -2,6 +2,7 @@ package doodle import ( "fmt" + "os" "path/filepath" "strconv" @@ -11,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/native" "git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/go/render" @@ -36,16 +38,18 @@ type EditorUI struct { cursor render.Point // remember the cursor position in Loop // Widgets + screen *ui.Frame // full-window parent frame for layout Supervisor *ui.Supervisor Canvas *uix.Canvas Workspace *ui.Frame - MenuBar *ui.Frame + MenuBar *ui.MenuBar StatusBar *ui.Frame ToolBar *ui.Frame PlayButton *ui.Button // Popup windows. levelSettingsWindow *ui.Window + aboutWindow *ui.Window // Palette window. Palette *ui.Window @@ -81,6 +85,11 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { 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. u.activeTool = drawtool.PencilTool.String() @@ -98,6 +107,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { u.ToolBar = u.SetupToolbar(d) 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{ Text: "Play (P)", 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. func (u *EditorUI) Resized(d *Doodle) { - // Menu Bar frame. - { - u.MenuBar.Configure(ui.Config{ - Width: d.width, - Background: render.Black, - }) - u.MenuBar.Compute(d.Engine) - } + // Resize the screen frame to fill the window. + u.screen.Resize(render.NewRect(d.width, d.height)) + u.screen.Compute(d.Engine) + menuHeight := 20 // TODO: ideally the MenuBar should know its own height and we can ask // Status Bar. { @@ -161,14 +172,14 @@ func (u *EditorUI) Resized(d *Doodle) { }) u.Palette.MoveTo(render.NewPoint( u.d.width-u.Palette.BoxSize().W, - u.MenuBar.BoxSize().H, + menuHeight, )) u.Palette.Compute(d.Engine) 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. { @@ -178,7 +189,7 @@ func (u *EditorUI) Resized(d *Doodle) { }) u.ToolBar.MoveTo(render.NewPoint( 0, - u.MenuBar.BoxSize().H, + menuHeight, )) u.ToolBar.Compute(d.Engine) } @@ -189,11 +200,11 @@ func (u *EditorUI) Resized(d *Doodle) { frame := u.Workspace frame.MoveTo(render.NewPoint( u.ToolBar.Size().W, - u.MenuBar.Size().H, + menuHeight, )) frame.Resize(render.NewRect( 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) @@ -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. // 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) } return nil @@ -298,6 +310,8 @@ func (u *EditorUI) Present(e render.Engine) { u.ToolBar.Present(e, u.ToolBar.Point()) u.PlayButton.Present(e, u.PlayButton.Point()) + u.screen.Present(e, render.Origin) + // Are we dragging a Doodad canvas? if u.Supervisor.IsDragging() { if actor := u.DraggableActor; actor != nil { @@ -421,14 +435,18 @@ func (u *EditorUI) ExpandCanvas(e render.Engine) { } // SetupMenuBar sets up the menu bar. -func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { - frame := ui.NewFrame("MenuBar") +func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { + menu := ui.NewMenuBar("Main Menu") // Save and Save As common menu handler - var saveFunc func(filename string) + var ( + drawingType string + saveFunc func(filename string) + ) switch u.Scene.DrawingType { case enum.LevelDrawing: + drawingType = "level" saveFunc = func(filename string) { if err := u.Scene.SaveLevel(filename); err != nil { d.Flash("Error: %s", err) @@ -437,6 +455,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { } } case enum.DoodadDrawing: + drawingType = "doodad" saveFunc = func(filename string) { if err := u.Scene.SaveDoodad(filename); err != nil { d.Flash("Error: %s", err) @@ -448,136 +467,155 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { d.Flash("Error: Scene.DrawingType is not a valid type") } - type menuButton struct { - Text string - Click func(ui.EventData) error - } - buttons := []menuButton{ - menuButton{ - Text: "New Level", - Click: func(ed ui.EventData) error { - d.GotoNewMenu() - return nil - }, - }, - menuButton{ - Text: "New Doodad", - Click: func(ed ui.EventData) error { - 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 + //////// + // File menu + fileMenu := menu.AddMenu("File") + fileMenu.AddItemAccel("New level", "Ctrl-N", 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 } - d.NewDoodad(size) - }) - - return nil - }, - }, - menuButton{ - Text: "Save", - Click: func(ed ui.EventData) error { - if u.Scene.filename != "" { - saveFunc(u.Scene.filename) - } else { - d.Prompt("Save filename>", func(answer string) { - if answer != "" { - saveFunc(answer) - } - }) + size = i } - return nil - }, - }, - menuButton{ - Text: "Save as...", - Click: func(ed ui.EventData) error { - d.Prompt("Save as filename>", func(answer string) { - if answer != "" { - saveFunc(answer) - } - }) - return nil - }, - }, - menuButton{ - Text: "Load", - Click: func(ed ui.EventData) error { - d.GotoLoadMenu() - return nil - }, - }, - menuButton{ - Text: "Options", - Click: func(ed ui.EventData) error { - scene, _ := d.Scene.(*EditorScene) - log.Info("Opening the window") - - // Open the New Level window in edit-settings mode. - if u.levelSettingsWindow == nil { - u.levelSettingsWindow = windows.NewAddEditLevel(windows.AddEditLevel{ - Supervisor: u.Supervisor, - Engine: d.Engine, - EditLevel: scene.Level, - - OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { - log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) - scene.Level.PageType = pageType - scene.Level.Wallpaper = wallpaper - u.Canvas.LoadLevel(d.Engine, scene.Level) - }, - OnCancel: func() { - u.levelSettingsWindow.Hide() - }, - }) - - u.levelSettingsWindow.Compute(d.Engine) - u.levelSettingsWindow.Supervise(u.Supervisor) - - // Center the window. - u.levelSettingsWindow.MoveTo(render.Point{ - X: (d.width / 2) - (u.levelSettingsWindow.Size().W / 2), - Y: 60, - }) - } else { - u.levelSettingsWindow.Show() - } - - return nil - }, - }, + d.NewDoodad(size) + }) + }) } - - for _, btn := range buttons { - if balance.FreeVersion { - if btn.Text == "New Doodad" { - continue - } + fileMenu.AddItemAccel("Save", "Ctrl-S", func() { + if u.Scene.filename != "" { + saveFunc(u.Scene.filename) + } else { + d.Prompt("Save filename>", func(answer string) { + if answer != "" { + saveFunc(answer) + } + }) } - - w := ui.NewButton(btn.Text, ui.NewLabel(ui.Label{ - Text: btn.Text, - Font: balance.MenuFont, - })) - w.Configure(ui.Config{ - BorderSize: 1, - OutlineSize: 0, + }) + fileMenu.AddItem("Save as...", func() { + d.Prompt("Save as filename>", func(answer string) { + if answer != "" { + saveFunc(answer) + } }) - w.Handle(ui.MouseUp, btn.Click) - u.Supervisor.Add(w) - frame.Pack(w, ui.Pack{ - Side: ui.W, - PadX: 1, + }) + fileMenu.AddItemAccel("Open...", "Ctrl-O", func() { + d.GotoLoadMenu() + }) + fileMenu.AddSeparator() + fileMenu.AddItem("Close "+drawingType, func() { + d.Goto(&MainScene{}) + }) + 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) + log.Info("Opening the window") + + // Open the New Level window in edit-settings mode. + if u.levelSettingsWindow == nil { + u.levelSettingsWindow = windows.NewAddEditLevel(windows.AddEditLevel{ + Supervisor: u.Supervisor, + Engine: d.Engine, + EditLevel: scene.Level, + + OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { + log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) + scene.Level.PageType = pageType + scene.Level.Wallpaper = wallpaper + u.Canvas.LoadLevel(d.Engine, scene.Level) + }, + OnCancel: func() { + u.levelSettingsWindow.Hide() + }, + }) + + u.levelSettingsWindow.Compute(d.Engine) + u.levelSettingsWindow.Supervise(u.Supervisor) + + // Center the window. + u.levelSettingsWindow.MoveTo(render.Point{ + X: (d.width / 2) - (u.levelSettingsWindow.Size().W / 2), + Y: 60, + }) + } else { + u.levelSettingsWindow.Show() + } + }) + + //////// + // Level menu + if drawingType == "level" { + 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. diff --git a/pkg/windows/about.go b/pkg/windows/about.go new file mode 100644 index 0000000..02e9e82 --- /dev/null +++ b/pkg/windows/about.go @@ -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 +}