diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index d256efd..0e9d625 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -20,6 +20,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/sound" "git.kirsle.net/apps/doodle/pkg/usercfg" + "git.kirsle.net/go/render" "git.kirsle.net/go/render/sdl" "github.com/urfave/cli/v2" @@ -62,6 +63,11 @@ func main() { log.Error("Error loading user settings (defaults will be used): %s", err) } + // Set default user settings. + if usercfg.Current.CrosshairColor == render.Invisible { + usercfg.Current.CrosshairColor = balance.DefaultCrosshairColor + } + app.Version = fmt.Sprintf("%s build %s%s. Built on %s", branding.Version, Build, diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index e741005..116587e 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -187,6 +187,8 @@ var ( ButtonBabyBlue = style.DefaultButton ButtonPink = style.DefaultButton ButtonLightRed = style.DefaultButton + + DefaultCrosshairColor = render.RGBA(0, 153, 255, 255) ) // Customize the various button styles. diff --git a/pkg/doodle.go b/pkg/doodle.go index 63bbb4a..f395ea5 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -69,6 +69,7 @@ func New(debug bool, engine render.Engine) *Doodle { // Make the render engine globally available. TODO: for wasm/ToBitmap shmem.CurrentRenderEngine = engine shmem.Flash = d.Flash + shmem.FlashError = d.FlashError shmem.Prompt = d.Prompt if debug { @@ -240,6 +241,8 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window { DebugCollision: &DebugCollision, HorizontalToolbars: &usercfg.Current.HorizontalToolbars, EnableFeatures: &usercfg.Current.EnableFeatures, + CrosshairSize: &usercfg.Current.CrosshairSize, + CrosshairColor: &usercfg.Current.CrosshairColor, } return windows.MakeSettingsWindow(d.width, d.height, cfg) } diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 579fd34..8a26264 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -351,6 +351,9 @@ func (u *EditorUI) Present(e render.Engine) { u.screen.Present(e, render.Origin) + // Draw the crosshair over the level canvas, but not over UI popups. + uix.DrawCrosshair(e, u.Canvas, usercfg.Current.CrosshairColor, usercfg.Current.CrosshairSize) + // Draw any windows being managed by Supervisor. u.Supervisor.Present(e) diff --git a/pkg/shmem/globals.go b/pkg/shmem/globals.go index 08379e3..e5611b3 100644 --- a/pkg/shmem/globals.go +++ b/pkg/shmem/globals.go @@ -23,7 +23,8 @@ var ( OfflineMode bool // Globally available Flash() function so we can emit text to the Doodle UI. - Flash func(string, ...interface{}) + Flash func(string, ...interface{}) + FlashError func(string, ...interface{}) // Globally available Prompt() function. Prompt func(string, func(string)) diff --git a/pkg/uix/canvas_scrolling.go b/pkg/uix/canvas_scrolling.go index 1a05a4c..744800d 100644 --- a/pkg/uix/canvas_scrolling.go +++ b/pkg/uix/canvas_scrolling.go @@ -116,6 +116,11 @@ func (w *Canvas) loopConstrainScroll() error { return errors.New("NoLimitScroll enabled") } + // Levels only. + if w.level == nil { + return nil + } + var ( capped bool maxWidth = w.level.MaxWidth diff --git a/pkg/uix/crosshair.go b/pkg/uix/crosshair.go new file mode 100644 index 0000000..6fc9e13 --- /dev/null +++ b/pkg/uix/crosshair.go @@ -0,0 +1,70 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +/* +Functions for the Crosshair feature of the game. + +NOT dependent on Canvas! +*/ + +type Crosshair struct { + LengthPct float64 // between 0 and 1 + Widget ui.Widget +} + +func NewCrosshair(child ui.Widget, length float64) *Crosshair { + return &Crosshair{ + LengthPct: length, + Widget: child, + } +} + +// DrawCrosshair renders a crosshair on the screen. It appears while the mouse cursor is +// over the child widget and draws within the bounds of the child widget. +// +// The lengthPct is an integer ranged 0 to 100 to be a percentage length of the crosshair. +// A value of zero will not draw anything and just return. +func DrawCrosshair(e render.Engine, child ui.Widget, color render.Color, lengthPct int) { + // Get our window boundaries based on our widget. + var ( + Position = ui.AbsolutePosition(child) + Size = child.Size() + Cursor = shmem.Cursor + VertLine = []render.Point{ + {X: Cursor.X, Y: Position.Y}, + {X: Cursor.X, Y: Position.Y + Size.H}, + } + HozLine = []render.Point{ + {X: Position.X, Y: Cursor.Y}, + {X: Position.X + Size.W, Y: Cursor.Y}, + } + ) + + if lengthPct > 100 { + lengthPct = 100 + } else if lengthPct <= 0 { + return + } + + // Mouse outside our box. + if Cursor.X < Position.X || Cursor.X > Position.X+Size.W || + Cursor.Y < Position.Y || Cursor.Y > Position.Y+Size.H { + return + } + + e.DrawLine( + color, + VertLine[0], + VertLine[1], + ) + e.DrawLine( + color, + HozLine[0], + HozLine[1], + ) +} diff --git a/pkg/usercfg/usercfg.go b/pkg/usercfg/usercfg.go index 802da9b..041aa76 100644 --- a/pkg/usercfg/usercfg.go +++ b/pkg/usercfg/usercfg.go @@ -18,6 +18,7 @@ import ( "time" "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/go/render" ) // Settings are the available game settings. @@ -30,6 +31,8 @@ type Settings struct { // Configurable settings (pkg/windows/settings.go) HorizontalToolbars bool `json:",omitempty"` EnableFeatures bool `json:",omitempty"` + CrosshairSize int `json:",omitempty"` + CrosshairColor render.Color // Secret boolprops from balance/boolprops.go ShowHiddenDoodads bool `json:",omitempty"` diff --git a/pkg/windows/settings.go b/pkg/windows/settings.go index 91c7768..a222fb0 100644 --- a/pkg/windows/settings.go +++ b/pkg/windows/settings.go @@ -1,11 +1,13 @@ package windows import ( + "strconv" "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/shmem" "git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/go/render" @@ -24,6 +26,8 @@ type Settings struct { DebugCollision *bool HorizontalToolbars *bool EnableFeatures *bool + CrosshairSize *int + CrosshairColor *render.Color // Configuration options. SceneName string // name of scene which called this window @@ -106,11 +110,14 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F 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 @@ -128,6 +135,16 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F PadX: 4, name: "toolbars", }, + { + 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)", }, @@ -196,6 +213,14 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F 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. @@ -212,6 +237,94 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F 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 { + shmem.Prompt("Enter color in hexadecimal notation: ", func(answer string) { + if answer == "" { + return + } + + color, err := render.HexColor(answer) + if err != nil { + shmem.FlashError("Invalid color value: %s", err) + } + + *row.Color = color + style.Background = color + style.HoverBackground = style.Background.Lighten(20) + + // call onClick to save change to disk now + onClick(ed) + }) + 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.