diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 1103de0..031ebe6 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "log" "os" "regexp" "runtime" @@ -15,6 +14,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/sound" "git.kirsle.net/go/render/sdl" "github.com/urfave/cli" @@ -71,7 +71,7 @@ func main() { &cli.StringFlag{ Name: "window", Aliases: []string{"w"}, - Usage: "set the window size (e.g. -w 1024x768) or special value: desktop, mobile, landscape", + Usage: "set the window size (e.g. -w 1024x768) or special value: desktop, mobile, landscape, maximized", }, &cli.BoolFlag{ Name: "guitest", @@ -127,6 +127,13 @@ func main() { game.PlayLevel(filename) } } + + // Maximizing the window? with `-w maximized` + if c.String("window") == "maximized" { + log.Info("Maximize main window") + engine.Maximize() + } + game.Run() return nil } @@ -136,13 +143,13 @@ func main() { err := app.Run(os.Args) if err != nil { - log.Fatal(err) + log.Error(err.Error()) } } func setResolution(value string) error { switch value { - case "desktop": + case "desktop", "maximized": return nil case "mobile": balance.Width = 375 diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 6a47ca8..be1b308 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -23,7 +23,7 @@ var ( } TitleFont = render.Text{ FontFilename: "DejaVuSans-Bold.ttf", - Size: 12, + Size: 9, Padding: 4, Color: render.White, Stroke: render.Red, @@ -37,6 +37,11 @@ var ( Size: 12, PadX: 4, } + MenuFontBold = render.Text{ + FontFilename: "DejaVuSans-Bold.ttf", + Size: 12, + PadX: 4, + } // StatusFont is the font for the status bar. StatusFont = render.Text{ @@ -83,4 +88,10 @@ var ( Color: render.RGBA(255, 255, 0, 255), Stroke: render.RGBA(100, 100, 0, 255), } + + // Doodad Dropper Window settings. + DoodadButtonBackground = render.RGBA(255, 255, 200, 255) + DoodadButtonSize = 64 + DoodadDropperCols = 6 // rows/columns of buttons + DoodadDropperRows = 3 ) diff --git a/pkg/doodle.go b/pkg/doodle.go index 0494b4f..25b7f69 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -61,6 +61,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.Prompt = d.Prompt if debug { log.Logger.Config.Level = golog.DebugLevel diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index fe66bde..692353f 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -20,9 +20,6 @@ import ( "git.kirsle.net/go/ui" ) -// Width of the panel frame. -var paletteWidth = 160 - // EditorUI manages the user interface for the Editor Scene. type EditorUI struct { d *Doodle @@ -50,6 +47,8 @@ type EditorUI struct { // Popup windows. levelSettingsWindow *ui.Window aboutWindow *ui.Window + doodadWindow *ui.Window + paletteEditor *ui.Window // Palette window. Palette *ui.Window @@ -107,6 +106,9 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { u.ToolBar = u.SetupToolbar(d) u.Workspace = u.SetupWorkspace(d) // important that this is last! + // Preload pop-up windows before they're needed. + u.SetupPopups(d) + log.Error("menu size: %s", u.MenuBar.Rect()) u.screen.Pack(u.MenuBar, ui.Pack{ Side: ui.N, @@ -175,8 +177,6 @@ func (u *EditorUI) Resized(d *Doodle) { menuHeight, )) u.Palette.Compute(d.Engine) - - u.scrollDoodadFrame(0) } var innerHeight = u.d.height - menuHeight - u.StatusBar.Size().H @@ -312,6 +312,9 @@ func (u *EditorUI) Present(e render.Engine) { u.screen.Present(e, render.Origin) + // Draw any windows being managed by Supervisor. + u.Supervisor.Present(e) + // Are we dragging a Doodad canvas? if u.Supervisor.IsDragging() { if actor := u.DraggableActor; actor != nil { @@ -322,9 +325,6 @@ func (u *EditorUI) Present(e render.Engine) { )) } } - - // Draw any windows being managed by Supervisor. - u.Supervisor.Present(e) } // SetupWorkspace configures the main Workspace frame that takes up the full @@ -359,6 +359,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { // mode when you click an existing Doodad and it "pops" out of the canvas // and onto the cursor to be repositioned. drawing.OnDragStart = func(actor *level.Actor) { + log.Warn("drawing.OnDragStart: grab actor %s", actor) u.startDragActor(nil, actor) } @@ -384,11 +385,22 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { // Was it an actor from the Doodad Palette? if actor := u.DraggableActor; actor != nil { log.Info("Actor is a %s", actor.doodad.Filename) + + // The actor has been dropped so null it out. + defer func() { + u.DraggableActor = nil + }() + if u.Scene.Level == nil { u.d.Flash("Can't drop doodads onto doodad drawings!") return nil } + // If they dropped it onto a UI window, ignore it. + if u.Supervisor.IsPointInWindow(ed.Point) { + return nil + } + var ( // Uncenter the drawing from the cursor. size = actor.canvas.Size() @@ -530,38 +542,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { }) 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() - } + u.levelSettingsWindow.Hide() + u.levelSettingsWindow = nil + u.SetupPopups(u.d) + u.levelSettingsWindow.Show() }) //////// @@ -585,6 +572,10 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { toolMenu.AddItemAccel("Command shell", "Enter", func() { d.shell.Open = true }) + toolMenu.AddItemAccel("Doodads", "d", func() { + log.Info("Open the DoodadDropper") + u.doodadWindow.Show() + }) //////// // Help menu @@ -618,6 +609,96 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { return menu } +// SetupPopups preloads popup windows like the DoodadDropper. +func (u *EditorUI) SetupPopups(d *Doodle) { + // Common window configure function. + var configure = func(window *ui.Window) { + var size = window.Size() + window.Compute(d.Engine) + window.Supervise(u.Supervisor) + + // Center the window. + window.MoveTo(render.Point{ + X: (d.width / 2) - (size.W / 2), + Y: (d.height / 2) - (size.H / 2), + }) + } + + // Doodad Dropper. + if u.doodadWindow == nil { + u.doodadWindow = windows.NewDoodadDropper(windows.DoodadDropper{ + Supervisor: u.Supervisor, + Engine: d.Engine, + + OnStartDragActor: u.startDragActor, + OnCancel: func() { + u.doodadWindow.Hide() + }, + }) + configure(u.doodadWindow) + } + + // Page Settings + if u.levelSettingsWindow == nil { + scene, _ := d.Scene.(*EditorScene) + + 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() + }, + }) + configure(u.levelSettingsWindow) + } + + // Palette Editor. + if u.paletteEditor == nil { + scene, _ := d.Scene.(*EditorScene) + + u.paletteEditor = windows.NewPaletteEditor(windows.PaletteEditor{ + Supervisor: u.Supervisor, + Engine: d.Engine, + EditLevel: scene.Level, + + OnChange: func() { + // Reload the level. + log.Warn("RELOAD LEVEL") + u.Canvas.LoadLevel(d.Engine, scene.Level) + scene.Level.Chunker.Redraw() + + // Reload the palette frame to reflect the changed data. + u.Palette.Hide() + u.Palette = u.SetupPalette(d) + u.Resized(d) + }, + OnAddColor: func() { + // Adding a new color to the palette. + sw := scene.Level.Palette.AddSwatch() + log.Info("Added new palette color: %+v", sw) + + // Awkward but... reload this very same window. + u.paletteEditor.Hide() + u.paletteEditor = nil + u.SetupPopups(d) + u.paletteEditor.Show() + }, + OnCancel: func() { + u.paletteEditor.Hide() + }, + }) + configure(u.paletteEditor) + } +} + // SetupStatusBar sets up the status bar widget along the bottom of the window. func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { frame := ui.NewFrame("Status Bar") diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index 83aa751..01bb662 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -5,15 +5,12 @@ package doodle // refactor into a subpackage if EditorUI itself can ever be decoupled. import ( - "fmt" - "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/go/render" - "git.kirsle.net/go/ui" ) // DraggableActor is a Doodad being dragged from the Doodad palette. @@ -54,225 +51,3 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) { actor: actor, } } - -// setupDoodadFrame configures the Doodad Palette tab for Edit Mode. -// This is a subroutine of editor_ui.go#SetupPalette() -// -// Can return an error if userdir.ListDoodads() returns an error (like directory -// not found), but it will *ALWAYS* return a valid ui.Frame -- it will just be -// empty and uninitialized. -func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Frame, error) { - var ( - frame = ui.NewFrame("Doodad Tab") - perRow = balance.UIDoodadsPerRow - ) - - // Pager buttons on top of the doodad list. - pager := ui.NewFrame("Doodad Pager") - pager.SetBackground(render.RGBA(255, 0, 0, 20)) // TODO: if I don't set a background color, - // this frame will light up the same color as the Link button on mouse - // over. somewhere some memory might be shared between the recent widgets - { - leftBtn := ui.NewButton("Prev Page", ui.NewLabel(ui.Label{ - Text: "<", - Font: balance.MenuFont, - })) - leftBtn.Handle(ui.Click, func(ed ui.EventData) error { - u.scrollDoodadFrame(-1) - return nil - }) - u.Supervisor.Add(leftBtn) - pager.Pack(leftBtn, ui.Pack{ - Side: ui.W, - }) - - scroller := ui.NewFrame("Doodad Scroll Progressbar") - scroller.Configure(ui.Config{ - Width: 20, - Height: 20, - Background: render.RGBA(128, 128, 128, 128), - }) - pager.Pack(scroller, ui.Pack{ - Side: ui.W, - }) - u.doodadScroller = scroller - - rightBtn := ui.NewButton("Next Page", ui.NewLabel(ui.Label{ - Text: ">", - Font: balance.MenuFont, - })) - rightBtn.Handle(ui.Click, func(ed ui.EventData) error { - u.scrollDoodadFrame(1) - return nil - }) - u.Supervisor.Add(rightBtn) - pager.Pack(rightBtn, ui.Pack{ - Side: ui.E, - }) - } - u.doodadPager = pager - frame.Pack(pager, ui.Pack{ - Side: ui.N, - Fill: true, - PadY: 5, - }) - - doodadsAvailable, err := doodads.ListDoodads() - if err != nil { - return frame, fmt.Errorf( - "setupDoodadFrame: doodads.ListDoodads: %s", - err, - ) - } - - var buttonSize = (paletteWidth - window.BoxThickness(2)) / perRow - u.doodadButtonSize = buttonSize - - // Load all the doodads, skip hidden ones. - var items []*doodads.Doodad - for _, filename := range doodadsAvailable { - doodad, err := doodads.LoadFile(filename) - if err != nil { - log.Error(err.Error()) - doodad = doodads.New(balance.DoodadSize) - } - - // Skip hidden doodads. - if doodad.Hidden && !balance.ShowHiddenDoodads { - log.Info("skip %s: hidden doodad", filename) - continue - } - - doodad.Filename = filename - items = append(items, doodad) - } - - // Draw the doodad buttons in a grid `perRow` buttons wide. - var ( - row *ui.Frame - rowCount int // for labeling the ui.Frame for each row - btnRows = []*ui.Frame{} // Collect the row frames for the buttons. - ) - for i, doodad := range items { - doodad := doodad - - if row == nil || i%perRow == 0 { - rowCount++ - row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount)) - row.SetBackground(balance.WindowBackground) - btnRows = append(btnRows, row) - frame.Pack(row, ui.Pack{ - Side: ui.N, - Fill: true, - }) - } - - can := uix.NewCanvas(int(buttonSize), true) - can.Name = doodad.Title - can.SetBackground(render.White) - can.LoadDoodad(doodad) - - btn := ui.NewButton(doodad.Title, can) - btn.Resize(render.NewRect( - buttonSize-2, // TODO: without the -2 the button border - buttonSize-2, // rests on top of the window border. - )) - row.Pack(btn, ui.Pack{ - Side: ui.W, - }) - - // Tooltip hover to show the doodad's name. - ui.NewTooltip(btn, ui.Tooltip{ - Text: doodad.Title, - Edge: ui.Top, - }) - - // Begin the drag event to grab this Doodad. - // NOTE: The drag target is the EditorUI.Canvas in - // editor_ui.go#SetupCanvas() - btn.Handle(ui.MouseDown, func(ed ui.EventData) error { - log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title) - u.startDragActor(doodad, nil) - return nil - }) - u.Supervisor.Add(btn) - - // Resize the canvas to fill the button interior. - btnSize := btn.Size() - can.Resize(render.NewRect( - btnSize.W-btn.BoxThickness(2), - btnSize.H-btn.BoxThickness(2), - ), - ) - - btn.Compute(e) - } - - u.doodadRows = btnRows - u.scrollDoodadFrame(0) - - return frame, nil -} - -// scrollDoodadFrame handles the Page Up/Down buttons to adjust the number of -// Doodads visible on screen. -// -// rows is the number of rows to scroll. Positive values mean scroll *down* -// the list. -func (u *EditorUI) scrollDoodadFrame(rows int) { - u.doodadSkip += rows - if u.doodadSkip < 0 { - u.doodadSkip = 0 - } - - // Calculate about how many rows we can see given our current window size. - var ( - maxVisibleHeight = u.d.height - 86 - calculatedHeight int - rowsBefore int // count of rows hidden before - rowsVisible int - rowsAfter int // count of rows hidden after - rowsEstimated = maxVisibleHeight / u.doodadButtonSize // estimated number rows shown - maxSkip = ((len(u.doodadRows) * int(u.doodadButtonSize)) - int(u.doodadButtonSize*rowsEstimated)) / int(u.doodadButtonSize) - ) - - if maxSkip < 0 { - maxSkip = 0 - } - - if u.doodadSkip > maxSkip { - u.doodadSkip = maxSkip - } - - // If the window is big enough to encompass all the doodads, don't show - // the pager toolbar, its just confusing. - if maxSkip == 0 { - u.doodadPager.Hide() - } else { - u.doodadPager.Show() - } - - // Comb through the doodads and show/hide the relevant buttons. - for i, row := range u.doodadRows { - if i < u.doodadSkip { - row.Hide() - rowsBefore++ - continue - } - - calculatedHeight += u.doodadButtonSize - if calculatedHeight > maxVisibleHeight { - row.Hide() - rowsAfter++ - } else { - row.Show() - rowsVisible++ - } - } - - var viewPercent = float64(rowsBefore+rowsVisible) / float64(len(u.doodadRows)) - u.doodadScroller.Configure(ui.Config{ - Width: int(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number - }) - -} diff --git a/pkg/editor_ui_palette.go b/pkg/editor_ui_palette.go index 024fac8..77b2d00 100644 --- a/pkg/editor_ui_palette.go +++ b/pkg/editor_ui_palette.go @@ -8,34 +8,20 @@ import ( "git.kirsle.net/go/ui" ) +// Width of the panel frame. +var paletteWidth = 50 + // SetupPalette sets up the palette panel. func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { window := ui.NewWindow("Palette") window.ConfigureTitle(balance.TitleConfig) - // window.TitleBar().Font = balance.TitleFont + _, label := window.TitleBar() + label.Font = balance.TitleFont window.Configure(ui.Config{ Background: balance.WindowBackground, BorderColor: balance.WindowBorder, }) - // Doodad frame. - { - frame, err := u.setupDoodadFrame(d.Engine, window) - if err != nil { - d.Flash(err.Error()) - } - - // Even if there was an error (userdir.ListDoodads couldn't read the - // config folder on disk or whatever) the Frame is still valid but - // empty, which is still the intended behavior. - u.DoodadTab = frame - u.DoodadTab.Hide() - window.Pack(u.DoodadTab, ui.Pack{ - Side: ui.N, - Fill: true, - }) - } - // Color Palette Frame. u.PaletteTab = u.setupPaletteFrame(window) window.Pack(u.PaletteTab, ui.Pack{ @@ -65,30 +51,16 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { return nil } + var buttonSize = 32 + // Draw the radio buttons for the palette. if u.Canvas != nil && u.Canvas.Palette != nil { for _, swatch := range u.Canvas.Palette.Swatches { swFrame := ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name)) - - colorFrame := ui.NewFrame(fmt.Sprintf("Swatch(%s) Color Box", swatch.Name)) - colorFrame.Configure(ui.Config{ - Width: 16, - Height: 16, - Background: swatch.Color, - BorderSize: 1, - BorderStyle: ui.BorderSunken, - }) - swFrame.Pack(colorFrame, ui.Pack{ - Side: ui.W, - }) - - label := ui.NewLabel(ui.Label{ - Text: swatch.Name, - Font: balance.StatusFont, - }) - label.Font.Color = swatch.Color.Darken(128) - swFrame.Pack(label, ui.Pack{ - Side: ui.W, + swFrame.Configure(ui.Config{ + Width: buttonSize, + Height: buttonSize, + Background: swatch.Color, }) btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, swFrame) @@ -97,18 +69,11 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { // Add a tooltip showing the swatch attributes. ui.NewTooltip(btn, ui.Tooltip{ - Text: "Attributes: " + swatch.Attributes(), + Text: fmt.Sprintf("Name: %s\nAttributes: %s", swatch.Name, swatch.Attributes()), Edge: ui.Left, }) btn.Compute(u.d.Engine) - swFrame.Configure(ui.Config{ - Height: label.Size().H, - - // TODO: magic number, trying to left-align - // the label by making the frame as wide as possible. - Width: paletteWidth - 16, - }) frame.Pack(btn, ui.Pack{ Side: ui.N, @@ -118,5 +83,27 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { } } + // Draw the Edit Palette button. + btn := ui.NewButton("Edit Palette", ui.NewLabel(ui.Label{ + Text: "Edit", + Font: balance.MenuFont, + })) + btn.Handle(ui.Click, func(ed ui.EventData) error { + // TODO: recompute the window so the actual loaded level palette gets in + u.paletteEditor.Hide() + u.paletteEditor = nil + u.SetupPopups(u.d) + u.paletteEditor.Show() + return nil + }) + u.Supervisor.Add(btn) + + btn.Compute(u.d.Engine) + frame.Pack(btn, ui.Pack{ + Side: ui.N, + Fill: true, + PadY: 4, + }) + return frame } diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 35cedef..0c71b91 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -27,18 +27,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Side: ui.N, }) - // Helper functions to toggle the correct palette panel. - var ( - showSwatchPalette = func() { - u.DoodadTab.Hide() - u.PaletteTab.Show() - } - showDoodadPalette = func() { - u.PaletteTab.Hide() - u.DoodadTab.Show() - } - ) - // Buttons. var buttons = []struct { Value string @@ -52,7 +40,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Pencil Tool", Click: func() { u.Canvas.Tool = drawtool.PencilTool - showSwatchPalette() d.Flash("Pencil Tool selected.") }, }, @@ -63,7 +50,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Line Tool", Click: func() { u.Canvas.Tool = drawtool.LineTool - showSwatchPalette() d.Flash("Line Tool selected.") }, }, @@ -74,7 +60,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Rectangle Tool", Click: func() { u.Canvas.Tool = drawtool.RectTool - showSwatchPalette() d.Flash("Rectangle Tool selected.") }, }, @@ -85,7 +70,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Ellipse Tool", Click: func() { u.Canvas.Tool = drawtool.EllipseTool - showSwatchPalette() d.Flash("Ellipse Tool selected.") }, }, @@ -96,7 +80,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Doodad Tool\nDrag-and-drop objects into your map", Click: func() { u.Canvas.Tool = drawtool.ActorTool - showDoodadPalette() + u.doodadWindow.Show() d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.") }, }, @@ -107,7 +91,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Tooltip: "Link Tool\nConnect doodads to each other", Click: func() { u.Canvas.Tool = drawtool.LinkTool - showDoodadPalette() + u.doodadWindow.Show() d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") }, }, @@ -126,7 +110,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { u.Canvas.BrushSize = balance.MaxEraserBrushSize } - showSwatchPalette() d.Flash("Eraser Tool selected.") }, }, diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index 233c6a5..2a69813 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -104,6 +104,12 @@ func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texture return c.textureMasked } +// SetDirty sets the `dirty` flag to true and forces the texture to be +// re-computed next frame. +func (c *Chunk) SetDirty() { + c.dirty = true +} + // toBitmap puts the texture in a well named bitmap path in the cache folder. func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) { // Generate a unique name for this chunk cache. diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 8f0648d..648fcee 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -180,6 +180,14 @@ func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) { return chunk, ok } +// Redraw marks every chunk as dirty and invalidates all their texture caches, +// forcing the drawing to re-generate from scratch. +func (c *Chunker) Redraw() { + for _, chunk := range c.Chunks { + chunk.SetDirty() + } +} + // Get a pixel at the given coordinate. Returns the Palette entry for that // pixel or else returns an error if not found. func (c *Chunker) Get(p render.Point) (*Swatch, error) { diff --git a/pkg/level/palette.go b/pkg/level/palette.go index dc65c2c..eb85ff4 100644 --- a/pkg/level/palette.go +++ b/pkg/level/palette.go @@ -1,6 +1,10 @@ package level -import "git.kirsle.net/go/render" +import ( + "fmt" + + "git.kirsle.net/go/render" +) // DefaultPalette returns a sensible default palette. func DefaultPalette() *Palette { @@ -85,6 +89,25 @@ func (p *Palette) Inflate() { p.update() } +// AddSwatch adds a new swatch to the palette. +func (p *Palette) AddSwatch() *Swatch { + p.update() + + var ( + index = len(p.Swatches) + name = fmt.Sprintf("color %d", len(p.Swatches)+1) + ) + + p.Swatches = append(p.Swatches, &Swatch{ + Name: name, + Color: render.Magenta, + index: index, + }) + p.byName[name] = index + + return p.Swatches[index] +} + // Get a swatch by name. func (p *Palette) Get(name string) (result *Swatch, exists bool) { p.update() diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 44fad5c..cbc3f32 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -146,6 +146,8 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error { }, }) s.window = window + window.SetButtons(0) + window.Show() return nil } diff --git a/pkg/shmem/globals.go b/pkg/shmem/globals.go index f687091..5556252 100644 --- a/pkg/shmem/globals.go +++ b/pkg/shmem/globals.go @@ -22,6 +22,9 @@ var ( // Globally available Flash() function so we can emit text to the Doodle UI. Flash func(string, ...interface{}) + // Globally available Prompt() function. + Prompt func(string, func(string)) + // Ajax file cache for WASM use. AjaxCache map[string][]byte ) diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index bb12858..2d14c15 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -351,6 +351,11 @@ func (w *Canvas) loopEditable(ev *event.State) error { if err := w.LinkAdd(actor); err != nil { return err } + + // TODO: reset the Button1 state so we don't finish a + // link and then LinkAdd the clicked doodad immediately + // (causing link chaining) + ev.Button1 = false break } } else { diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index a52410e..121483d 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -42,8 +42,8 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { window := ui.NewWindow(title) window.SetButtons(ui.CloseButton) window.Configure(ui.Config{ - Width: 540, - Height: 350, + Width: 400, + Height: 180, Background: render.Grey, }) @@ -234,5 +234,6 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { } } + window.Hide() return window } diff --git a/pkg/windows/doodad_dropper.go b/pkg/windows/doodad_dropper.go new file mode 100644 index 0000000..ff78b5a --- /dev/null +++ b/pkg/windows/doodad_dropper.go @@ -0,0 +1,244 @@ +package windows + +import ( + "fmt" + "math" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/uix" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// DoodadDropper is the doodad palette pop-up window for Editor Mode. +type DoodadDropper struct { + Supervisor *ui.Supervisor + Engine render.Engine + + // Editing settings for an existing level? + EditLevel *level.Level + + // Callback functions. + OnStartDragActor func(doodad *doodads.Doodad, actor *level.Actor) + OnCancel func() +} + +// NewDoodadDropper initializes the window. +func NewDoodadDropper(config DoodadDropper) *ui.Window { + // Default options. + var ( + title = "Doodads" + + buttonSize = balance.DoodadButtonSize + columns = balance.DoodadDropperCols + rows = balance.DoodadDropperRows + + // size of the doodad window + width = buttonSize * columns + height = (buttonSize * rows) + 64 // account for button borders :( + + // pagination values + page = 1 + pages int + perPage = 20 + ) + + window := ui.NewWindow(title) + window.SetButtons(ui.CloseButton) + window.Configure(ui.Config{ + Width: width, + Height: height, + Background: render.Grey, + }) + + frame := ui.NewFrame("Window Body Frame") + window.Pack(frame, ui.Pack{ + Side: ui.N, + Fill: true, + Expand: true, + }) + + /******* + * Display the Doodads in rows of buttons + *******/ + + doodadsAvailable, err := doodads.ListDoodads() + if err != nil { + log.Error("NewDoodadDropper: doodads.ListDoodads: %s", err) + } + + // Load all the doodads, skip hidden ones. + var items []*doodads.Doodad + for _, filename := range doodadsAvailable { + doodad, err := doodads.LoadFile(filename) + if err != nil { + log.Error(err.Error()) + doodad = doodads.New(balance.DoodadSize) + } + + // Skip hidden doodads. + if doodad.Hidden && !balance.ShowHiddenDoodads { + continue + } + + doodad.Filename = filename + items = append(items, doodad) + } + + // Compute the number of pages for the pager widget. + pages = int( + math.Ceil( + float64(len(items)) / float64(columns*rows), + ), + ) + + // Draw the doodad buttons in rows. + var btnRows = []*ui.Frame{} + { + var ( + row *ui.Frame + rowCount int // for labeling the ui.Frame for each row + + // TODO: pre-size btnRows by calculating how many needed + ) + + for i, doodad := range items { + doodad := doodad + + if row == nil || i%columns == 0 { + var hidden = rowCount >= rows + rowCount++ + + row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount)) + row.SetBackground(balance.DoodadButtonBackground) + btnRows = append(btnRows, row) + frame.Pack(row, ui.Pack{ + Side: ui.N, + // Fill: true, + }) + + // Hide overflowing rows until we scroll to them. + if hidden { + row.Hide() + } + } + + can := uix.NewCanvas(int(buttonSize), true) + can.Name = doodad.Title + can.SetBackground(balance.DoodadButtonBackground) + can.LoadDoodad(doodad) + + btn := ui.NewButton(doodad.Title, can) + btn.Resize(render.NewRect( + buttonSize-2, // TODO: without the -2 the button border + buttonSize-2, // rests on top of the window border + )) + row.Pack(btn, ui.Pack{ + Side: ui.W, + }) + + // Tooltip hover to show the doodad's name. + ui.NewTooltip(btn, ui.Tooltip{ + Text: doodad.Title, + Edge: ui.Top, + }) + + // Begin the drag event to grab this Doodad. + // NOTE: The drag target is the EditorUI.Canvas in + // editor_ui.go#SetupCanvas() + btn.Handle(ui.MouseDown, func(ed ui.EventData) error { + log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title) + config.OnStartDragActor(doodad, nil) + return nil + }) + config.Supervisor.Add(btn) + + // Resize the canvas to fill the button interior. + btnSize := btn.Size() + can.Resize(render.NewRect( + btnSize.W-btn.BoxThickness(2), + btnSize.H-btn.BoxThickness(2), + )) + + btn.Compute(config.Engine) + } + } + + { + /****************** + * Confirm/cancel buttons. + ******************/ + + bottomFrame := ui.NewFrame("Button Frame") + frame.Pack(bottomFrame, ui.Pack{ + Side: ui.S, + FillX: true, + }) + + // Pager for the doodads. + pager := ui.NewPager(ui.Pager{ + Page: page, + Pages: pages, + PerPage: perPage, + Font: balance.MenuFont, + OnChange: func(newPage, perPage int) { + page = newPage + log.Info("Page: %d, %d", page, perPage) + + // Re-evaluate which rows are shown/hidden for the page we're on. + var ( + minRow = (page - 1) * rows + visible = 0 + ) + for i, row := range btnRows { + if visible >= rows { + row.Hide() + continue + } + + if i < minRow { + row.Hide() + } else { + row.Show() + visible++ + } + } + }, + }) + pager.Compute(config.Engine) + pager.Supervise(config.Supervisor) + bottomFrame.Place(pager, ui.Place{ + Top: 20, + Left: 20, + }) + + var buttons = []struct { + Label string + F func(ui.EventData) error + }{ + // OK button is for editing an existing level. + {"Close", func(ed ui.EventData) error { + config.OnCancel() + return nil + }}, + } + for _, t := range buttons { + btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{ + Text: t.Label, + Font: balance.MenuFont, + })) + btn.Handle(ui.Click, t.F) + config.Supervisor.Add(btn) + bottomFrame.Place(btn, ui.Place{ + Top: 20, + Right: 20, + }) + } + } + + window.Hide() + return window +} diff --git a/pkg/windows/palette_editor.go b/pkg/windows/palette_editor.go new file mode 100644 index 0000000..2ebba97 --- /dev/null +++ b/pkg/windows/palette_editor.go @@ -0,0 +1,365 @@ +package windows + +import ( + "fmt" + "math" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/level" + "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" +) + +// PaletteEditor lets you customize the level palette in Edit Mode. +type PaletteEditor struct { + Supervisor *ui.Supervisor + Engine render.Engine + + // Pointer to the currently edited level. + EditLevel *level.Level + + // Callback functions. + OnChange func() + OnAddColor func() + OnCancel func() +} + +// NewPaletteEditor initializes the window. +func NewPaletteEditor(config PaletteEditor) *ui.Window { + // Default options. + var ( + title = "Level Palette" + + buttonSize = balance.DoodadButtonSize + columns = balance.DoodadDropperCols + rows = []*ui.Frame{} + + // size of the popup window + width = buttonSize * columns + height = (buttonSize * balance.DoodadDropperRows) + 64 // account for button borders :( + + // Column sizes of the palette table. + col1 = 30 // ID no. + col2 = 24 // Color + col3 = 120 // Name + col4 = 140 // Attributes + // col5 = 150 // Delete + + // pagination values + page = 1 + perPage = 5 + ) + + window := ui.NewWindow(title) + window.SetButtons(ui.CloseButton) + window.Configure(ui.Config{ + Width: width, + Height: height, + Background: render.Grey, + }) + + frame := ui.NewFrame("Window Body Frame") + window.Pack(frame, ui.Pack{ + Side: ui.N, + Fill: true, + Expand: true, + }) + + log.Info("SETUP PALETTE WINDOW") + + // Draw the header row. + headers := []struct { + Name string + Size int + }{ + {"ID", col1}, + {"Col", col2}, + {"Name", col3}, + {"Attributes", col4}, + // {"Delete", col5}, + } + header := ui.NewFrame("Palette Header") + for _, col := range headers { + labelFrame := ui.NewFrame(col.Name) + labelFrame.Configure(ui.Config{ + Width: col.Size, + Height: 24, + }) + + label := ui.NewLabel(ui.Label{ + Text: col.Name, + Font: balance.MenuFontBold, + }) + labelFrame.Pack(label, ui.Pack{ + Side: ui.N, + }) + + header.Pack(labelFrame, ui.Pack{ + Side: ui.W, + Padding: 2, + }) + } + + header.Compute(config.Engine) + frame.Pack(header, ui.Pack{ + Side: ui.N, + }) + + // Draw the main table of Palette rows. + if level := config.EditLevel; level != nil { + for i, swatch := range level.Palette.Swatches { + var idStr = fmt.Sprintf("%d", i) + swatch := swatch + + row := ui.NewFrame("Swatch " + idStr) + rows = append(rows, row) + + // Off the end of the first page? + if i >= perPage { + row.Hide() + } + + // ID label. + idLabel := ui.NewLabel(ui.Label{ + Text: idStr + ".", + Font: balance.MenuFont, + }) + idLabel.Configure(ui.Config{ + Width: col1, + Height: 24, + }) + + // Name button (click to rename the swatch) + btnName := ui.NewButton("Name", ui.NewLabel(ui.Label{ + TextVariable: &swatch.Name, + })) + btnName.Configure(ui.Config{ + Width: col3, + Height: 24, + }) + btnName.Handle(ui.Click, func(ed ui.EventData) error { + shmem.Prompt("New swatch name ["+swatch.Name+"]: ", func(answer string) { + log.Warn("Answer: %s", answer) + if answer != "" { + swatch.Name = answer + if config.OnChange != nil { + config.OnChange() + } + } + }) + return nil + }) + config.Supervisor.Add(btnName) + + // Color Choice button. + btnColor := ui.NewButton("Color", ui.NewFrame("Color Frame")) + btnColor.SetStyle(&style.Button{ + Background: swatch.Color, + HoverBackground: swatch.Color.Lighten(40), + OutlineColor: render.Black, + OutlineSize: 1, + BorderStyle: style.BorderRaised, + BorderSize: 2, + }) + btnColor.Configure(ui.Config{ + Background: swatch.Color, + Width: col2, + Height: 24, + }) + btnColor.Handle(ui.Click, func(ed ui.EventData) error { + shmem.Prompt(fmt.Sprintf( + "New color in hex notation [%s]: ", swatch.Color.ToHex()), func(answer string) { + if answer != "" { + color, err := render.HexColor(answer) + if err != nil { + shmem.Flash("Error with that color code: %s", err) + return + } + + swatch.Color = color + + // TODO: redundant from above, consolidate these + fmt.Printf("Set button style to: %s\n", swatch.Color) + btnColor.SetStyle(&style.Button{ + Background: swatch.Color, + HoverBackground: swatch.Color.Lighten(40), + OutlineColor: render.Black, + OutlineSize: 1, + BorderStyle: style.BorderRaised, + BorderSize: 2, + }) + + if config.OnChange != nil { + config.OnChange() + } + } + }) + return nil + }) + config.Supervisor.Add(btnColor) + + // Attribute flags + attrFrame := ui.NewFrame("Attributes") + attrFrame.Configure(ui.Config{ + Width: col4, + Height: 24, + }) + attributes := []struct { + Label string + Var *bool + }{ + { + Label: "Solid", + Var: &swatch.Solid, + }, + { + Label: "Fire", + Var: &swatch.Fire, + }, + { + Label: "Water", + Var: &swatch.Water, + }, + } + for _, attr := range attributes { + attr := attr + btn := ui.NewCheckButton(attr.Label, attr.Var, ui.NewLabel(ui.Label{ + Text: attr.Label, + Font: balance.MenuFont, + })) + btn.Handle(ui.Click, func(ed ui.EventData) error { + if config.OnChange != nil { + config.OnChange() + } + return nil + }) + config.Supervisor.Add(btn) + attrFrame.Pack(btn, ui.Pack{ + Side: ui.W, + }) + } + + // Pack all the widgets. + row.Pack(idLabel, ui.Pack{ + Side: ui.W, + PadX: 2, + }) + row.Pack(btnColor, ui.Pack{ + Side: ui.W, + PadX: 2, + }) + row.Pack(btnName, ui.Pack{ + Side: ui.W, + PadX: 2, + }) + row.Pack(attrFrame, ui.Pack{ + Side: ui.W, + PadX: 2, + }) + + row.Compute(config.Engine) + frame.Pack(row, ui.Pack{ + Side: ui.N, + PadY: 2, + }) + } + } + + { + /****************** + * Confirm/cancel buttons. + ******************/ + + bottomFrame := ui.NewFrame("Button Frame") + frame.Pack(bottomFrame, ui.Pack{ + Side: ui.S, + FillX: true, + }) + + // Pager for the doodads. + pager := ui.NewPager(ui.Pager{ + Page: page, + Pages: int(math.Ceil( + float64(len(rows)) / float64(perPage), + )), + PerPage: perPage, + Font: balance.MenuFont, + OnChange: func(newPage, perPage int) { + page = newPage + log.Info("Page: %d, %d", page, perPage) + + // Re-evaluate which rows are shown/hidden for this page. + var ( + minRow = (page - 1) * perPage + visible = 0 + ) + for i, row := range rows { + if visible >= perPage { + row.Hide() + continue + } + + if i < minRow { + row.Hide() + } else { + row.Show() + visible++ + } + } + }, + }) + pager.Compute(config.Engine) + pager.Supervise(config.Supervisor) + bottomFrame.Place(pager, ui.Place{ + Top: 20, + Left: 20, + }) + + btnFrame := ui.NewFrame("Window Buttons") + var buttons = []struct { + Label string + F func(ui.EventData) error + }{ + {"Add Color", func(ed ui.EventData) error { + if config.OnAddColor != nil { + config.OnAddColor() + } + + if config.OnChange != nil { + config.OnChange() + } + return nil + }}, + {"Close", func(ed ui.EventData) error { + if config.OnCancel != nil { + config.OnCancel() + } + return nil + }}, + } + for _, t := range buttons { + btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{ + Text: t.Label, + Font: balance.MenuFont, + })) + btn.Handle(ui.Click, t.F) + btn.Compute(config.Engine) + config.Supervisor.Add(btn) + + btnFrame.Pack(btn, ui.Pack{ + Side: ui.W, + PadX: 4, + }) + } + bottomFrame.Place(btnFrame, ui.Place{ + Top: 20, + Right: 20, + }) + } + + window.Hide() + return window +}