Spit and polish

UI improvements specifically for mobile (running the game with the
`-w mobile` or `-w landscape` options) screen sizes.

* Rework the Settings window to be mobile friendly to landscape
  oriented screens (`doodle -w landscape`) and migrate Options tab
  to magicform.
* The toolbar in the Editor will be a single column of buttons
  on small screens, such as `-w mobile` (375x812) portrait mode
  smartphone. On larger screens the toolbar shows in two columns
  of buttons.
* Fix tooltips not drawing on top.
* Centralize the hard-coded references to specific font filenames
* Add cheat code: `test load screen` to bring a sample loading screen up
  for a few seconds. It needs improvement on `-w landscape`
pull/84/head
Noah 2022-03-05 22:44:54 -08:00
parent 77297fd60d
commit 44122d4130
12 changed files with 238 additions and 329 deletions

View File

@ -31,4 +31,5 @@ var (
CheatPlayAsThief = "play as thief"
CheatPlayAsAnvil = "megaton weight"
CheatGodMode = "god mode"
CheatDebugLoadScreen = "test load screen"
)

View File

@ -16,7 +16,7 @@ var (
***************/
// Debug overlay (FPS etc.) settings.
DebugFontFilename = "DejaVuSans-Bold.ttf"
DebugFontFilename = SansBoldFont
DebugFontSize = 16
DebugLabelColor = render.MustHexColor("#FF9900")
DebugValueColor = render.MustHexColor("#00CCFF")

9
pkg/balance/fonts.go Normal file
View File

@ -0,0 +1,9 @@
package balance
// Font filenames used as part of game configuration.
const (
// Main UI fonts (sans-serif and monospace)
SansSerifFont = "DejaVuSans.ttf"
MonospaceFont = "DejaVuSansMono.ttf"
SansBoldFont = "DejaVuSans-Bold.ttf"
)

View File

@ -61,7 +61,7 @@ var (
// Default font filename selected for Text Tool in the editor.
// TODO: better centralize font filenames, here and in theme.go
TextToolDefaultFont = "DejaVuSans.ttf"
TextToolDefaultFont = SansSerifFont
// Interval for auto-save in the editor
AutoSaveInterval = 5 * time.Minute

View File

@ -4,7 +4,7 @@ import "git.kirsle.net/go/render"
// Shell related variables.
var (
ShellFontFilename = "DejaVuSansMono.ttf"
ShellFontFilename = MonospaceFont
ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
ShellPromptColor = render.White

View File

@ -23,7 +23,7 @@ var (
Shadow: render.Black,
}
TitleScreenSubtitleFont = render.Text{
FontFilename: "DejaVuSans.ttf",
FontFilename: SansSerifFont,
Size: 18,
Color: render.SkyBlue,
Shadow: render.SkyBlue.Darken(128),
@ -44,7 +44,7 @@ var (
Shadow: render.Black,
}
LoadScreenSecondaryFont = render.Text{
FontFilename: "DejaVuSans.ttf",
FontFilename: SansSerifFont,
Size: 18,
Color: render.SkyBlue,
Shadow: render.SkyBlue.Darken(128),
@ -54,7 +54,7 @@ var (
// Play Mode Touch UI Hints Font
TouchHintsFont = render.Text{
FontFilename: "DejaVuSans.ttf",
FontFilename: SansSerifFont,
Size: 14,
Color: render.SkyBlue,
Shadow: render.SkyBlue.Darken(128),
@ -69,7 +69,7 @@ var (
OutlineColor: render.Black,
}
TitleFont = render.Text{
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: SansBoldFont,
Size: 9,
Padding: 4,
Color: render.White,
@ -99,7 +99,7 @@ var (
PadX: 4,
}
MenuFontBold = render.Text{
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: SansBoldFont,
Size: 12,
PadX: 4,
}
@ -131,7 +131,7 @@ var (
// LabelFont is the font for strong labels in UI.
LabelFont = render.Text{
Size: 12,
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: SansBoldFont,
Padding: 4,
Color: render.Black,
}
@ -148,7 +148,7 @@ var (
LargeLabelFont = render.Text{
Size: 18,
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: SansBoldFont,
Padding: 4,
Color: render.Black,
}
@ -157,7 +157,7 @@ var (
SmallMonoFont = render.Text{
Size: 14,
PadX: 3,
FontFilename: "DejaVuSansMono.ttf",
FontFilename: MonospaceFont,
Color: render.Black,
}
@ -165,7 +165,7 @@ var (
CodeLiteralFont = render.Text{
Size: 11,
PadX: 3,
FontFilename: "DejaVuSansMono.ttf",
FontFilename: MonospaceFont,
Color: render.Magenta,
}
@ -185,7 +185,7 @@ var (
LinkAnimSpeed uint64 = 30 // ticks
PlayButtonFont = render.Text{
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: SansBoldFont,
Size: 16,
Padding: 4,
Color: render.RGBA(255, 255, 0, 255),
@ -194,7 +194,7 @@ var (
// In-game level timer font.
TimerFont = render.Text{
FontFilename: "DejaVuSansMono.ttf",
FontFilename: MonospaceFont,
Size: 16,
Color: render.Cyan,
Stroke: render.DarkCyan,

View File

@ -1,7 +1,10 @@
package doodle
import (
"time"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
)
// IsDefaultPlayerCharacter checks whether the DefaultPlayerCharacter doodad has
@ -146,6 +149,18 @@ func (c Command) cheatCommand(d *Doodle) bool {
d.FlashError("Use this cheat in Play Mode to toggle invincibility.")
}
case balance.CheatDebugLoadScreen:
loadscreen.ShowWithProgress()
loadscreen.SetSubtitle("Loading: /dev/null", "Loadscreen testing.")
go func() {
var i float64
for i = 0; i < 100; i++ {
time.Sleep(100 * time.Millisecond)
loadscreen.SetProgress(i / 100)
}
loadscreen.Hide()
}()
default:
return false
}

View File

@ -53,8 +53,16 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
// We can draw 2 buttons per row, but for very small screens
// e.g. mobile in portrait orientation, draw 1 button per row.
buttonsPerRow = 1
if isHoz || d.width >= enum.ScreenWidthSmall {
buttonsPerRow = 2
if isHoz {
if d.width < enum.ScreenWidthSmall {
// Narrow screens
buttonsPerRow = 2
}
} else {
if d.width >= enum.ScreenWidthSmall {
// Screen wider than 600px = can spare room for 2 buttons per row.
buttonsPerRow = 2
}
}
// Compute toolbar size to accommodate all buttons (+10 for borders/padding)
@ -229,10 +237,11 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
})
u.Supervisor.Add(btn)
ui.NewTooltip(btn, ui.Tooltip{
tt := ui.NewTooltip(btn, ui.Tooltip{
Text: button.Tooltip,
Edge: tooltipEdge,
})
tt.Supervise(u.Supervisor)
btnRow.Pack(btn, btnPack)
}
@ -352,13 +361,22 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Text: button.Label,
Font: balance.SmallMonoFont,
}))
btn.SetBorderSize(1)
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.F()
return nil
})
u.Supervisor.Add(btn)
// Which side to pack on?
var side = ui.W
if !isHoz && buttonsPerRow == 1 {
// Vertical layout w/ narrow one-button-per-row, the +-
// buttons stick out so stack them vertically.
side = ui.S
}
sizeBtnFrame.Pack(btn, ui.Pack{
Side: ui.W,
Side: side,
})
}
}

View File

@ -113,7 +113,7 @@ func (s *MainScene) Setup(d *Doodle) error {
s.updateButton = ui.NewButton("Update Button", ui.NewLabel(ui.Label{
Text: "An update is available!",
Font: render.Text{
FontFilename: "DejaVuSans-Bold.ttf",
FontFilename: balance.SansBoldFont,
Size: 16,
Color: render.Blue,
Padding: 4,

View File

@ -5,6 +5,7 @@ import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
@ -21,6 +22,7 @@ const (
Checkbox
Radiobox
Selectbox
Color
)
// Form configuration.
@ -61,11 +63,12 @@ type Field struct {
Frame *ui.Frame
// Variable bindings, the type may infer to be:
BoolVariable *bool // Checkbox
TextVariable *string // Textbox
IntVariable *int // Textbox
Options []Option // Selectbox
SelectValue interface{} // Selectbox default choice
BoolVariable *bool // Checkbox
TextVariable *string // Textbox
IntVariable *int // Textbox
Options []Option // Selectbox
SelectValue interface{} // Selectbox default choice
Color *render.Color // Color
// Tooltip to add to a form control.
// Checkbox only for now.
@ -181,6 +184,77 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
})
}
// Color picker button.
if row.Type == Color && row.Color != nil {
btn := ui.NewButton("ColorPicker", ui.NewLabel(ui.Label{
Text: " ",
Font: row.Font,
}))
style := style.DefaultButton
style.Background = *row.Color
style.HoverBackground = style.Background.Lighten(20)
btn.SetStyle(&style)
form.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
btn.Handle(ui.Click, func(ed ui.EventData) error {
// Open a ColorPicker widget.
picker, err := ui.NewColorPicker(ui.ColorPicker{
Title: "Select a color",
Supervisor: form.Supervisor,
Engine: form.Engine,
Color: *row.Color,
OnManualInput: func(callback func(render.Color)) {
// Prompt the user to enter a hex color using the developer shell.
shmem.Prompt("New color in hex notation: ", func(answer string) {
if answer != "" {
// XXX: pure white renders as invisible, fudge it a bit.
if answer == "FFFFFF" {
answer = "FFFFFE"
}
color, err := render.HexColor(answer)
if err != nil {
shmem.Flash("Error with that color code: %s", err)
return
}
// Reconfigure the button now.
style.Background = color
style.HoverBackground = style.Background.Lighten(20)
callback(color)
}
})
},
})
if err != nil {
log.Error("Couldn't open ColorPicker: %s", err)
return err
}
picker.Then(func(color render.Color) {
*row.Color = color
style.Background = color
style.HoverBackground = style.Background.Lighten(20)
// call onClick to save change to disk now
if row.OnClick != nil {
row.OnClick()
}
})
picker.Center(shmem.CurrentRenderEngine.WindowSize())
picker.Show()
return nil
})
}
// Buttons and Text fields (for now).
if row.Type == Button || row.Type == Textbox {
btn := ui.NewButton("Button", ui.NewLabel(ui.Label{
@ -198,7 +272,8 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
ui.NewTooltip(btn, row.Tooltip)
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
// Handlers
@ -224,7 +299,8 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
ui.NewTooltip(cb, row.Tooltip)
tt := ui.NewTooltip(cb, row.Tooltip)
tt.Supervise(form.Supervisor)
}
// Handlers

View File

@ -244,10 +244,11 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
})
// Tooltip hover to show the doodad's name.
ui.NewTooltip(btn, ui.Tooltip{
tt := ui.NewTooltip(btn, ui.Tooltip{
Text: doodad.Title,
Edge: ui.Top,
})
tt.Supervise(config.Supervisor)
// Begin the drag event to grab this Doodad.
// NOTE: The drag target is the EditorUI.Canvas in

View File

@ -1,20 +1,17 @@
package windows
import (
"strconv"
"strings"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/gamepad"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/shmem"
magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form"
"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.
@ -61,7 +58,7 @@ func MakeSettingsWindow(windowWidth, windowHeight int, cfg Settings) *ui.Window
func NewSettingsWindow(cfg Settings) *ui.Window {
var (
Width = 400
Height = 400
Height = 360
)
window := ui.NewWindow("Settings")
@ -111,305 +108,98 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
// Common click handler for all settings,
// so we can write the updated info to disk.
onClick := func(ed ui.EventData) error {
onClick := func() {
saveGameSettings()
return nil
}
var inputBoxWidth = 120
rows := []struct {
Header string
Text string
Boolean *bool
Integer *int
TextVariable *string
Color *render.Color
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",
},
{
Boolean: c.HideTouchHints,
Text: "Hide touchscreen control hints during Play Mode",
PadX: 4,
name: "toolbars",
},
{
Boolean: c.DisableAutosave,
Text: "Disable auto-save in the Editor",
PadX: 4,
name: "autosave",
},
{
Integer: c.CrosshairSize,
Text: "Editor: Crosshair size (0 to disable):",
PadX: 4,
},
{
Color: c.CrosshairColor,
Text: "Editor: Crosshair color:",
PadX: 4,
},
{
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, which you can access below:",
},
// The CrosshairSize is ideally a 0-100 (percent) how big the editor
// crosshair is, but options now are only 0% or 100% so it presents
// this as a checkbox for now.
var crosshairEnabled = *c.CrosshairSize > 0
form := magicform.Form{
Supervisor: c.Supervisor,
Engine: c.Engine,
Vertical: true,
LabelWidth: 150,
}
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
} else {
// Reserve indented space where the checkbox would have gone.
spacer := ui.NewFrame("Spacer")
spacer.Resize(render.NewRect(9, 9)) // TODO: ugly UI hack ;)
frame.Pack(spacer, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
}
// 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,
})
}
// Int variables draw as a button to prompt for new value.
// In future: TextVariable works here too.
if row.Integer != nil {
varButton := ui.NewButton("VarButton", ui.NewLabel(ui.Label{
IntVariable: row.Integer,
Font: ui.MenuFont,
}))
varButton.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt(row.Text+" ", func(answer string) {
if answer == "" {
return
}
a, err := strconv.Atoi(answer)
if err != nil {
shmem.FlashError(err.Error())
return
}
if a < 0 {
a = 0
} else if a > 100 {
a = 100
}
*row.Integer = a
shmem.Flash("Crosshair size set to %d%% (WIP)", a)
// call onClick to save change to disk now
onClick(ed)
})
return nil
})
varButton.Compute(c.Engine)
varButton.Resize(render.Rect{
W: inputBoxWidth,
H: varButton.Size().H,
})
c.Supervisor.Add(varButton)
frame.Pack(varButton, ui.Pack{
Side: ui.E,
PadX: row.PadX,
})
}
// Color picker button.
if row.Color != nil {
btn := ui.NewButton("ColorBtn", ui.NewFrame(""))
style := style.DefaultButton
style.Background = *row.Color
style.HoverBackground = style.Background.Lighten(20)
btn.SetStyle(&style)
btn.Handle(ui.Click, func(ed ui.EventData) error {
// Open a ColorPicker widget.
picker, err := ui.NewColorPicker(ui.ColorPicker{
Title: "Select a color",
Supervisor: c.Supervisor,
Engine: c.Engine,
Color: *row.Color,
OnManualInput: func(callback func(render.Color)) {
// Prompt the user to enter a hex color using the developer shell.
shmem.Prompt("New color in hex notation: ", func(answer string) {
if answer != "" {
// XXX: pure white renders as invisible, fudge it a bit.
if answer == "FFFFFF" {
answer = "FFFFFE"
}
color, err := render.HexColor(answer)
if err != nil {
shmem.Flash("Error with that color code: %s", err)
return
}
callback(color)
}
})
},
})
if err != nil {
log.Error("Couldn't open ColorPicker: %s", err)
return err
}
picker.Then(func(color render.Color) {
*row.Color = color
style.Background = color
style.HoverBackground = style.Background.Lighten(20)
// call onClick to save change to disk now
onClick(ed)
})
picker.Center(shmem.CurrentRenderEngine.WindowSize())
picker.Show()
return nil
})
btn.Compute(c.Engine)
btn.Resize(render.Rect{
W: inputBoxWidth,
H: 20, // TODO
})
c.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.E,
PadX: row.PadX,
})
}
}
// 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
}{
form.Create(tab, []magicform.Field{
{
Label: "Open profile directory",
Fn: func() {
path := strings.ReplaceAll(userdir.ProfileDirectory, "\\", "/")
if path[0] != '/' {
path = "/" + path
}
native.OpenURL("file://" + path)
Label: "Game Options",
Font: balance.LabelFont,
},
{
Label: "Hide touchscreen control hints during Play Mode",
Font: balance.UIFont,
BoolVariable: c.HideTouchHints,
},
{
Label: "Level & Doodad Editor",
Font: balance.LabelFont,
},
{
Label: "Horizontal instead of vertical toolbars",
Font: balance.UIFont,
BoolVariable: c.HorizontalToolbars,
Tooltip: ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.",
Edge: ui.Top,
},
Style: &balance.ButtonPrimary,
},
} {
btn := ui.NewButton(button.Label, ui.NewLabel(ui.Label{
Text: button.Label,
{
Label: "Disable auto-save in the Editor",
Font: balance.UIFont,
BoolVariable: c.DisableAutosave,
},
{
Label: "Draw a crosshair at the mouse cursor.",
Font: balance.UIFont,
BoolVariable: &crosshairEnabled,
OnClick: func() {
if crosshairEnabled {
*c.CrosshairSize = 100
} else {
*c.CrosshairSize = 0
}
onClick()
},
},
{
Type: magicform.Color,
Label: "Crosshair color:",
Font: balance.UIFont,
Color: c.CrosshairColor,
OnClick: func() {
onClick()
},
},
{
Label: "My Custom Content",
Font: balance.LabelFont,
},
{
Label: "Levels and doodads you create in-game are placed in your\n" +
"Profile Directory, which you can access below:",
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,
})
}
},
{
Buttons: []magicform.Field{
{
Label: "Open profile directory",
Font: balance.UIFont,
ButtonStyle: &balance.ButtonPrimary,
OnClick: func() {
path := strings.ReplaceAll(userdir.ProfileDirectory, "\\", "/")
if path[0] != '/' {
path = "/" + path
}
native.OpenURL("file://" + path)
},
},
},
},
})
return tab
}
@ -575,7 +365,7 @@ func (c Settings) makeControlsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.
frame.Pack(curFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 2,
PadY: 1,
})
}
@ -743,14 +533,13 @@ func (c Settings) makeControllerTab(tabFrame *ui.TabFrame, Width, Height int) *u
}
form.Create(tab, []magicform.Field{
{
Label: "About",
Label: "Play with an Xbox or Nintendo controller!",
Font: balance.LabelFont,
},
{
Label: "Play Sketchy Maze with an Xbox or Nintendo controller!\n\n" +
"Full customization options aren't here yet, but you can\n" +
"choose between the 'X Style' or 'N Style' profile below.\n" +
"'N Style' will swap the A/B and X/Y buttons.",
Label: "If you have a Nintendo-style controller (your A button is on\n" +
"the right and B button on bottom), pick 'N Style' to reverse\n" +
"the A/B and X/Y buttons.",
Font: balance.UIFont,
},
{
@ -776,7 +565,7 @@ func (c Settings) makeControllerTab(tabFrame *ui.TabFrame, Width, Height int) *u
},
},
{
Label: "\nThe gamepad controls vary between two modes:",
Label: "The gamepad controls vary between two modes:",
Font: balance.UIFont,
},
{