doodle/pkg/windows/settings.go
Noah Petherbridge 0a8bce708e Actor Zoom + Experimental Settings GUI
Improvements to the Zoom feature:
* Actor position and size within your level scales up and down
  appropriately. The canvas size of the actor is scaled and its canvas
  is told the Zoom number of the parent so it will render its own
  graphic scaled correctly too.

Other features:
* "Experimental" tab added to the Settings window as a UI version of the
  --experimental CLI option. The option saves persistently to disk.
* The "Replace Palette" experimental feature now works better. Debating
  whether it's a useful feature to even have.
2021-09-11 21:18:22 -07:00

582 lines
12 KiB
Go

package windows
import (
"strings"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
)
// Settings window.
type Settings struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
// Boolean bindings.
DebugOverlay *bool
DebugCollision *bool
HorizontalToolbars *bool
EnableFeatures *bool
// Configuration options.
SceneName string // name of scene which called this window
ActiveTab string // specify the tab to open
OnApply func()
}
// MakeSettingsWindow initializes a settings window for any scene.
// The window width/height are the actual SDL2 window dimensions.
func MakeSettingsWindow(windowWidth, windowHeight int, cfg Settings) *ui.Window {
win := NewSettingsWindow(cfg)
win.Compute(cfg.Engine)
win.Supervise(cfg.Supervisor)
// Center the window.
size := win.Size()
win.MoveTo(render.Point{
X: (windowWidth / 2) - (size.W / 2),
Y: (windowHeight / 2) - (size.H / 2),
})
return win
}
// NewSettingsWindow initializes the window.
func NewSettingsWindow(cfg Settings) *ui.Window {
var (
Width = 400
Height = 400
)
window := ui.NewWindow("Settings")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: Width,
Height: Height,
Background: render.Grey,
})
///////////
// Tab Bar
tabFrame := ui.NewTabFrame("Tab Frame")
tabFrame.SetBackground(render.DarkGrey)
window.Pack(tabFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
// Make the tabs
cfg.makeOptionsTab(tabFrame, Width, Height)
cfg.makeControlsTab(tabFrame, Width, Height)
cfg.makeExperimentalTab(tabFrame, Width, Height)
tabFrame.Supervise(cfg.Supervisor)
return window
}
// saveGameSettings controls pkg/usercfg to write the user settings
// to disk, based on the settings toggle-able from the UI window.
func saveGameSettings() {
log.Info("Saving game settings")
if err := usercfg.Save(); err != nil {
log.Error("Couldn't save game settings: %s", err)
}
}
// Settings Window "Options" Tab
func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Options", ui.NewLabel(ui.Label{
Text: "Options",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
// Common click handler for all settings,
// so we can write the updated info to disk.
onClick := func(ed ui.EventData) error {
saveGameSettings()
return nil
}
rows := []struct {
Header string
Text string
Boolean *bool
TextVariable *string
PadY int
PadX int
name string // for special cases
}{
{
Text: "Notice: all settings are temporary and controls are not editable.",
PadY: 2,
},
{
Header: "Game Options",
},
{
Boolean: c.HorizontalToolbars,
Text: "Editor: Horizontal instead of vertical toolbars",
PadX: 4,
name: "toolbars",
},
{
Header: "Debug Options (temporary)",
},
{
Boolean: c.DebugOverlay,
Text: "Show debug text overlay (F3)",
PadX: 4,
},
{
Boolean: c.DebugCollision,
Text: "Show collision hitboxes (F4)",
PadX: 4,
},
{
Header: "My Custom Content",
},
{
Text: "Levels and doodads you create in-game are placed in your\n" +
"Profile Directory. This is also where you can place content made\n" +
"by others to use them in your game. Click on the button below\n" +
"to (hopefully) be taken to your Profile Directory:",
},
}
for _, row := range rows {
row := row
frame := ui.NewFrame("Frame")
tab.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: row.PadY,
})
// Headers get their own row to themselves.
if row.Header != "" {
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Checkboxes get their own row.
if row.Boolean != nil {
cb := ui.NewCheckbox(row.Text, row.Boolean, ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
}))
cb.Handle(ui.Click, onClick)
cb.Supervise(c.Supervisor)
// Add warning to the toolbars option if the EditMode is currently active.
if row.name == "toolbars" && c.SceneName == "Edit" {
ui.NewTooltip(cb, ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.",
Edge: ui.Top,
})
}
frame.Pack(cb, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Any leftover Text gets packed to the left.
if row.Text != "" {
tf := ui.NewFrame("TextFrame")
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
})
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
}
// Button toolbar.
btnFrame := ui.NewFrame("Button Frame")
tab.Pack(btnFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 4,
})
for _, button := range []struct {
Label string
Fn func()
Style *style.Button
}{
{
Label: "Open profile directory",
Fn: func() {
path := strings.ReplaceAll(userdir.ProfileDirectory, "\\", "/")
if path[0] != '/' {
path = "/" + path
}
native.OpenURL("file://" + path)
},
Style: &balance.ButtonPrimary,
},
} {
btn := ui.NewButton(button.Label, ui.NewLabel(ui.Label{
Text: button.Label,
Font: balance.UIFont,
}))
if button.Style != nil {
btn.SetStyle(button.Style)
}
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.Fn()
return nil
})
c.Supervisor.Add(btn)
btnFrame.Pack(btn, ui.Pack{
Side: ui.W,
Expand: true,
})
}
return tab
}
// Settings Window "Controls" Tab
func (c Settings) makeControlsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
frame := tabFrame.AddTab("Controls", ui.NewLabel(ui.Label{
Text: "Controls",
Font: balance.TabFont,
}))
frame.Resize(render.NewRect(Width-4, Height-frame.Size().H-46))
var (
halfWidth = (Width - 4) / 2 // the 4 is for window borders, TODO
shortcutTabWidth = float64(halfWidth) * 0.5
infoTabWidth = float64(halfWidth) * 0.5
rowHeight = 20
shortcutTabSize = render.NewRect(int(shortcutTabWidth), rowHeight)
infoTabSize = render.NewRect(int(infoTabWidth), rowHeight)
)
controls := []struct {
Header string
Label string
Shortcut string
}{
{
Header: "Universal Shortcut Keys",
},
{
Shortcut: "Escape",
Label: "Exit game",
},
{
Shortcut: "F1",
Label: "Guidebook",
},
{
Shortcut: "`",
Label: "Dev console",
},
{
Header: "Gameplay Controls (Play Mode)",
},
{
Shortcut: "Up or W",
Label: "Jump",
},
{
Shortcut: "Space",
Label: "Activate",
},
{
Shortcut: "Left or A",
Label: "Move left",
},
{
Shortcut: "Right or D",
Label: "Move right",
},
{
Header: "Level Editor Shortcuts",
},
{
Shortcut: "Ctrl-N",
Label: "New level",
},
{
Shortcut: "Ctrl-O",
Label: "Open drawing",
},
{
Shortcut: "Ctrl-S",
Label: "Save drawing",
},
{
Shortcut: "Shift-Ctrl-S",
Label: "Save a copy",
},
{
Shortcut: "Ctrl-Z",
Label: "Undo stroke",
},
{
Shortcut: "Ctrl-Y",
Label: "Redo stroke",
},
{
Shortcut: "P",
Label: "Playtest",
},
{
Shortcut: "0",
Label: "Scroll to origin",
},
{
Shortcut: "q",
Label: "Doodads",
},
{
Shortcut: "f",
Label: "Pencil Tool",
},
{
Shortcut: "l",
Label: "Line Tool",
},
{
Shortcut: "r",
Label: "Rectangle Tool",
},
{
Shortcut: "c",
Label: "Ellipse Tool",
},
{
Shortcut: "x",
Label: "Eraser Tool",
},
}
var curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
var i = -1 // manually controlled
for _, row := range controls {
i++
row := row
if row.Header != "" {
// Close out a previous Frame?
if i != 0 {
curFrame = ui.NewFrame("Header Row")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
}
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont,
})
curFrame.Pack(label, ui.Pack{
Side: ui.W,
})
// Set up the next series of shortcut keys.
i = -1
curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
continue
}
// Cut a new frame every 2 items.
if i > 0 && i%2 == 0 {
curFrame = ui.NewFrame("Frame")
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 2,
})
}
keyLabel := ui.NewLabel(ui.Label{
Text: row.Shortcut,
Font: balance.CodeLiteralFont,
})
keyLabel.Configure(ui.Config{
Background: render.RGBA(255, 255, 220, 255),
BorderSize: 1,
BorderStyle: ui.BorderSunken,
BorderColor: render.DarkGrey,
})
keyLabel.Resize(shortcutTabSize)
curFrame.Pack(keyLabel, ui.Pack{
Side: ui.W,
PadX: 1,
})
helpLabel := ui.NewLabel(ui.Label{
Text: row.Label,
Font: balance.UIFont,
})
helpLabel.Resize(infoTabSize)
curFrame.Pack(helpLabel, ui.Pack{
Side: ui.W,
PadX: 1,
})
}
return frame
}
// Settings Window "Experimental" Tab
func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Experimental", ui.NewLabel(ui.Label{
Text: "Experimental",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
// Common click handler for all settings,
// so we can write the updated info to disk.
onClick := func(ed ui.EventData) error {
saveGameSettings()
return nil
}
rows := []struct {
Header string
Text string
Boolean *bool
TextVariable *string
PadY int
PadX int
name string // for special cases
}{
{
Header: "Enable Experimental Features",
},
{
Text: "The setting below can enable experimental features in this\n" +
"game. These are features which are still in development and\n" +
"may have unstable or buggy behavior.",
PadY: 2,
},
{
Header: "Zoom In/Out",
},
{
Text: "This adds Zoom options to the level editor. It has a few\n" +
"bugs around scrolling but may be useful already.",
PadY: 2,
},
{
Header: "Replace Level Palette",
},
{
Text: "This adds an option to the Level Properties dialog to\n" +
"replace your level palette with one of the defaults,\n" +
"like on the New Level screen. It might not actually work.",
PadY: 2,
},
{
Boolean: c.EnableFeatures,
Text: "Enable experimental features",
PadX: 4,
},
{
Text: "Restart the game for changes to take effect.",
PadY: 2,
},
}
for _, row := range rows {
row := row
frame := ui.NewFrame("Frame")
tab.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: row.PadY,
})
// Headers get their own row to themselves.
if row.Header != "" {
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Checkboxes get their own row.
if row.Boolean != nil {
cb := ui.NewCheckbox(row.Text, row.Boolean, ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
}))
cb.Handle(ui.Click, onClick)
cb.Supervise(c.Supervisor)
// Add warning to the toolbars option if the EditMode is currently active.
if row.name == "toolbars" && c.SceneName == "Edit" {
ui.NewTooltip(cb, ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.",
Edge: ui.Top,
})
}
frame.Pack(cb, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Any leftover Text gets packed to the left.
if row.Text != "" {
tf := ui.NewFrame("TextFrame")
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
})
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
}
return tab
}