Noah Petherbridge
0a8bce708e
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.
582 lines
12 KiB
Go
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
|
|
}
|