From da832315599343fd9407854d286d69e833773e9f Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 8 Dec 2023 19:48:02 -0800 Subject: [PATCH] Level Screenshots and Thumbnails Adds some support for "less giant" level screenshots. * In the Editor, the Level->Take Screenshot menu will render a cropped screen shot of just the level viewport on screen. Note: it is not an SDL2 screen copy but generated from scratch from the level data. * In levels themselves, screenshots can be stored inside the level data in three different sizes: large (1280x720), medium and small (each a halved size of the previous). * The first screenshot is created when the level is saved, starting from wherever the scroll position in the editor is at, and recording the 720p view of the level from there. * The level screenshot can be previewed and updated in the Level Properties window of the editor: so you can scroll the editor to just the right position and take a good screenshot to represent your level. * In the future: these embedded level screenshots will be displayed on the Story Mode and other screens to see a preview of each level. Other tweaks: * When taking a Giant Screenshot: a confirm modal will warn the player that it may take a while. And during the screenshot, show the new Wait Modal to block player interaction until the screenshot has finished. --- pkg/balance/numbers.go | 8 + pkg/balance/theme.go | 7 + pkg/editor_scene.go | 20 ++ pkg/editor_ui_menubar.go | 29 ++- pkg/editor_ui_popups.go | 3 + pkg/level/filesystem.go | 22 ++ .../giant_screenshot/giant_screenshot.go | 50 +++-- .../giant_screenshot/regular_screenshot.go | 196 ++++++++++++++++++ pkg/level/screenshot.go | 61 ++++++ pkg/level/types.go | 11 + pkg/windows/add_edit_level.go | 87 ++++++++ 11 files changed, 476 insertions(+), 18 deletions(-) create mode 100644 pkg/level/giant_screenshot/regular_screenshot.go create mode 100644 pkg/level/screenshot.go diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index cfb4da4..1644a4b 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -153,6 +153,14 @@ var ( // variable is the tolerance offset - if they are only this far out of bounds, put them // back in bounds but further out and they're OK. OutOfBoundsMargin = 40 + + // Level screenshot dimensions saved within the level data. + LevelScreenshotLargeFilename = "large.png" + LevelScreenshotMediumFilename = "medium.png" + LevelScreenshotSmallFilename = "small.png" + LevelScreenshotLargeSize = render.NewRect(1280, 720) + LevelScreenshotMediumSize = render.NewRect(640, 360) + LevelScreenshotSmallSize = render.NewRect(320, 180) ) // Edit Mode Values diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 6f7ddc8..78bce9f 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -141,6 +141,13 @@ var ( Color: render.Black, } + // DangerFont is a red version of UIFont. + DangerFont = render.Text{ + Size: 12, + Padding: 4, + Color: render.Red, + } + // LabelFont is the font for strong labels in UI. LabelFont = render.Text{ Size: 12, diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index db94e60..07cc711 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -13,6 +13,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/enum" "git.kirsle.net/SketchyMaze/doodle/pkg/keybind" "git.kirsle.net/SketchyMaze/doodle/pkg/level" + "git.kirsle.net/SketchyMaze/doodle/pkg/level/giant_screenshot" "git.kirsle.net/SketchyMaze/doodle/pkg/level/publishing" "git.kirsle.net/SketchyMaze/doodle/pkg/license" "git.kirsle.net/SketchyMaze/doodle/pkg/log" @@ -553,9 +554,28 @@ func (s *EditorScene) SaveLevel(filename string) error { } s.lastAutosaveAt = time.Now() + + // Save screenshots into the level file. + if !m.HasScreenshot() { + s.UpdateLevelScreenshot(m) + } + return m.WriteFile(filename) } +// UpdateLevelScreenshot updates a level screenshot in its zipfile. +func (s *EditorScene) UpdateLevelScreenshot(lvl *level.Level) error { + // The level must have been saved and have a filename to update in. + if s.filename == "" { + return errors.New("Save your level to disk before updating its screenshot.") + } + + if err := giant_screenshot.UpdateLevelScreenshots(lvl, s.UI.Canvas.Viewport().Point()); err != nil { + return fmt.Errorf("Error saving level screenshots: %s", err) + } + return nil +} + // AutoSave takes an autosave snapshot of the level or drawing. func (s *EditorScene) AutoSave() error { var ( diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index b0a6ff4..8c3cbc3 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/level/giant_screenshot" "git.kirsle.net/SketchyMaze/doodle/pkg/license" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/SketchyMaze/doodle/pkg/userdir" "git.kirsle.net/SketchyMaze/doodle/pkg/windows" @@ -115,19 +116,41 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { }) levelMenu.AddSeparator() - levelMenu.AddItem("Giant Screenshot", func() { + levelMenu.AddItem("Screenshot", func() { // It takes a LONG TIME to render for medium+ maps. // Do so on a background thread. go func() { - filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level) + filename, err := giant_screenshot.SaveCroppedScreenshot(u.Scene.Level, u.Scene.GetDrawing().Viewport()) if err != nil { d.FlashError("Error: %s", err.Error()) return } - d.FlashError("Giant screenshot saved as: %s", filename) + d.FlashError("Screenshot saved as: %s", filename) }() }) + levelMenu.AddItem("Giant Screenshot", func() { + // It takes a LONG TIME to render for medium+ maps. + modal.Confirm( + "Do you want to make a 'Giant Screenshot' of\n" + + "your WHOLE level? Note: this may take several\n" + + "seconds for very large maps!", + ).WithTitle("Giant Screenshot").Then(func() { + // Show the wait modal and generate the screenshot on a background thread. + m := modal.Wait("Generating a giant screenshot...").WithTitle("Please hold") + go func() { + defer m.Dismiss(true) + + filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level) + if err != nil { + d.FlashError("Error: %s", err.Error()) + return + } + + d.FlashError("Giant screenshot saved as: %s", filename) + }() + }) + }) levelMenu.AddItem("Open screenshot folder", func() { native.OpenLocalURL(userdir.ScreenshotDirectory) }) diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index b7820e7..ffbe44d 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -188,6 +188,9 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.Canvas.Destroy() // clean up old textures u.Canvas.LoadLevel(scene.Level) }, + OnUpdateScreenshot: func() error { + return scene.UpdateLevelScreenshot(scene.Level) + }, OnReload: func() { log.Warn("RELOAD LEVEL") scene.Reset() diff --git a/pkg/level/filesystem.go b/pkg/level/filesystem.go index 5af8a5c..2aafbfd 100644 --- a/pkg/level/filesystem.go +++ b/pkg/level/filesystem.go @@ -37,6 +37,28 @@ func NewFileSystem() *FileSystem { } } +// Exists checks if a file exists. +func (fs *FileSystem) Exists(filename string) bool { + if fs.filemap == nil { + fs.filemap = map[string]File{} + } + + // Legacy file map. + if _, ok := fs.filemap[filename]; ok { + return ok + } + + // Check in the zipfile. + if fs.Zipfile != nil { + file, err := fs.Zipfile.Open(filename) + if err == nil { + defer file.Close() + return true + } + } + return false +} + // Get a file from the FileSystem. func (fs *FileSystem) Get(filename string) ([]byte, error) { if fs.filemap == nil { diff --git a/pkg/level/giant_screenshot/giant_screenshot.go b/pkg/level/giant_screenshot/giant_screenshot.go index 88f26d4..5ae11ae 100644 --- a/pkg/level/giant_screenshot/giant_screenshot.go +++ b/pkg/level/giant_screenshot/giant_screenshot.go @@ -72,7 +72,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) { // Render the wallpaper onto it. log.Debug("GiantScreenshot: Render wallpaper to image (%s)...", size) - img = WallpaperToImage(lvl, img, size.W, size.H) + img = WallpaperToImage(lvl, img, size.W, size.H, render.Origin) // Render the chunks. log.Debug("GiantScreenshot: Render level chunks...") @@ -158,7 +158,9 @@ func SaveGiantScreenshot(level *level.Level) (string, error) { // // The image is assumed to have a rect of (0,0,width,height) and that // width and height are positive. Used for the Giant Screenshot feature. -func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int) *image.RGBA { +// +// Pass an offset point to 'scroll' the wallpaper (for the Cropped Screenshot). +func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int, offset render.Point) *image.RGBA { wp, err := wallpaper.FromFile("assets/wallpapers/"+lvl.Wallpaper, lvl) if err != nil { log.Error("GiantScreenshot: wallpaper load: %s", err) @@ -167,30 +169,48 @@ func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int) * var size = wp.QuarterRect() var resultImage = target - // Tile the repeat texture. - for x := 0; x < width; x += size.W { - for y := 0; y < height; y += size.H { - offset := image.Pt(x, y) - resultImage = blotImage(resultImage, wp.Repeat(), offset) + // Handling the scroll offsets: wallpapers are made up of small-ish + // repeating squares so the scroll offset needs only be a modulus within + // the size of one square. + absOffset := offset + if offset != render.Origin { + offset.X = render.AbsInt(offset.X % size.W) + offset.Y = render.AbsInt(offset.Y % size.H) + } + + // Tile the repeat texture. Go one tile extra in case of offset. + for x := 0; x < width+size.W; x += size.W { + for y := 0; y < height+size.H; y += size.H { + dst := image.Pt(x-offset.X, y-offset.Y) + resultImage = blotImage(resultImage, wp.Repeat(), dst) } } // Tile the left edge for bounded lvls. if lvl.PageType > level.Unbounded { - // The left edge. - for y := 0; y < height; y += size.H { - offset := image.Pt(0, y) - resultImage = blotImage(resultImage, wp.Left(), offset) + // The left edge (unless off screen) + if absOffset.X < size.W { + for y := 0; y < height; y += size.H { + dst := image.Pt(0-offset.X, y-offset.Y) + resultImage = blotImage(resultImage, wp.Left(), dst) + } } // The top edge. - for x := 0; x < width; x += size.W { - offset := image.Pt(x, 0) - resultImage = blotImage(resultImage, wp.Top(), offset) + if absOffset.Y < size.H { + for x := 0; x < width; x += size.W { + dst := image.Pt(x-offset.X, 0-offset.Y) + resultImage = blotImage(resultImage, wp.Top(), dst) + } } // The top left corner. - resultImage = blotImage(resultImage, wp.Corner(), image.Point{}) + if absOffset.X < size.W && absOffset.Y < size.H { + resultImage = blotImage(resultImage, wp.Corner(), image.Pt( + -offset.X, + -offset.Y, + )) + } } return resultImage diff --git a/pkg/level/giant_screenshot/regular_screenshot.go b/pkg/level/giant_screenshot/regular_screenshot.go new file mode 100644 index 0000000..3ebcbd8 --- /dev/null +++ b/pkg/level/giant_screenshot/regular_screenshot.go @@ -0,0 +1,196 @@ +package giant_screenshot + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/png" + "os" + "path/filepath" + "time" + + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/SketchyMaze/doodle/pkg/doodads" + "git.kirsle.net/SketchyMaze/doodle/pkg/level" + "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/userdir" + "git.kirsle.net/go/render" + "golang.org/x/image/draw" +) + +// CroppedScreenshot returns a rendered RGBA image of the level. +func CroppedScreenshot(lvl *level.Level, viewport render.Rect) (image.Image, error) { + // Lock this to one user at a time. + if locked { + return nil, errors.New("a screenshot is still being processed; try later...") + } + locked = true + defer func() { + locked = false + }() + + // How big will our image be? + var ( + size = render.NewRect(viewport.W-viewport.X, viewport.H-viewport.Y) + chunkSize = int(lvl.Chunker.Size) + // worldSize = viewport + ) + + // Create the image. + img := image.NewRGBA(image.Rect(0, 0, size.W, size.H)) + + // Render the wallpaper onto it. + log.Debug("CroppedScreenshot: Render wallpaper to image (%s)...", size) + img = WallpaperToImage(lvl, img, size.W, size.H, viewport.Point()) + + // Render the chunks. + log.Debug("CroppedScreenshot: Render level chunks...") + for coord := range lvl.Chunker.IterViewportChunks(viewport) { + if chunk, ok := lvl.Chunker.GetChunk(coord); ok { + + // Get this chunk's rendered bitmap. + rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA) + if !ok { + log.Error("CroppedScreenshot: couldn't turn chunk to RGBA") + } + log.Debug("Blot chunk %s onto image", coord) + + // Compute where on the output image to copy this bitmap to. + dst := image.Pt( + // (X * W) multiplies the chunk coord by its size, + // Then subtract the Viewport (level scroll position) + (coord.X*chunkSize)-viewport.X, + (coord.Y*chunkSize)-viewport.Y, + ) + + log.Debug("Copy chunk: %s to %s", coord, dst) + img = blotImage(img, rgba, dst) + } + } + + // Render the doodads. + log.Debug("CroppedScreenshot: Render actors...") + for _, actor := range lvl.Actors { + doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false) + if err != nil { + log.Error("CroppedScreenshot: Load doodad: %s", err) + continue + } + + // Offset the doodad position by the viewport (scroll position). + drawAt := render.NewPoint(actor.Point.X, actor.Point.Y) + drawAt.X -= viewport.X + drawAt.Y -= viewport.Y + + // TODO: usually doodad sprites start at 0,0 and the chunkSize + // is the same as their sprite size. + if len(doodad.Layers) > 0 && doodad.Layers[0].Chunker != nil { + var chunker = doodad.Layers[0].Chunker + chunk, ok := chunker.GetChunk(render.Origin) + if !ok { + continue + } + + // TODO: we always use RGBA but is risky: + rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA) + if !ok { + log.Error("CroppedScreenshot: couldn't turn chunk to RGBA") + } + img = blotImage(img, rgba, image.Pt(drawAt.X, drawAt.Y)) + } + + } + + return img, nil +} + +// SaveCroppedScreenshot will take a screenshot and write it to a file on disk, +// returning the filename relative to ~/.config/doodle/screenshots +func SaveCroppedScreenshot(level *level.Level, viewport render.Rect) (string, error) { + var filename = time.Now().Format("2006-01-02_15-04-05.png") + + img, err := CroppedScreenshot(level, viewport) + if err != nil { + return "", err + } + + fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename)) + if err != nil { + return "", err + } + + png.Encode(fh, img) + return filename, nil +} + +// UpdateLevelScreenshots will generate and embed the screenshot PNGs into the level data. +func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error { + // Take screenshots. + large, medium, small, err := CreateLevelScreenshots(lvl, scroll) + if err != nil { + return err + } + + // Save the images into the level's filesystem. + for filename, img := range map[string]image.Image{ + balance.LevelScreenshotLargeFilename: large, + balance.LevelScreenshotMediumFilename: medium, + balance.LevelScreenshotSmallFilename: small, + } { + var fh = bytes.NewBuffer([]byte{}) + if err := png.Encode(fh, img); err != nil { + return fmt.Errorf("encode %s: %s", filename, err) + } + + log.Debug("UpdateLevelScreenshots: add %s", filename) + lvl.Files.Set( + fmt.Sprintf("assets/screenshots/%s", filename), + fh.Bytes(), + ) + } + + return nil +} + +// CreateLevelScreenshots generates a screenshot to save with the level data. +// +// This is called by the editor upon level save, and outputs the screenshots that +// will be embedded within the level data itself. +// +// Returns the large, medium and small images. +func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small image.Image, err error) { + // Viewport to screenshot. + viewport := render.Rect{ + X: scroll.X, + W: scroll.X + balance.LevelScreenshotLargeSize.W, + Y: scroll.Y, + H: scroll.Y + balance.LevelScreenshotLargeSize.H, + } + + // Get the full size screenshot as an image. + large, err = CroppedScreenshot(lvl, viewport) + if err != nil { + return + } + + // Scale the medium and small versions. + medium = Scale(large, image.Rect(0, 0, balance.LevelScreenshotMediumSize.W, balance.LevelScreenshotMediumSize.H), draw.ApproxBiLinear) + small = Scale(large, image.Rect(0, 0, balance.LevelScreenshotSmallSize.W, balance.LevelScreenshotSmallSize.H), draw.ApproxBiLinear) + return large, medium, small, nil +} + +// Scale down an image. Example: +// +// scaled := Scale(src, image.Rect(0, 0, 200, 200), draw.ApproxBiLinear) +func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image { + dst := image.NewRGBA(rect) + copyRect := image.Rect( + rect.Min.X, + rect.Min.Y, + rect.Min.X+rect.Max.X, + rect.Min.Y+rect.Max.Y, + ) + scale.Scale(dst, copyRect, src, src.Bounds(), draw.Over, nil) + return dst +} diff --git a/pkg/level/screenshot.go b/pkg/level/screenshot.go new file mode 100644 index 0000000..d25985e --- /dev/null +++ b/pkg/level/screenshot.go @@ -0,0 +1,61 @@ +package level + +import ( + "bytes" + "image" + "image/png" + + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/go/ui" +) + +// Helper functions to get a level's embedded screenshot PNGs as textures. + +// HasScreenshot returns whether screenshots exist for the level. +func (lvl *Level) HasScreenshot() bool { + var filenames = []string{ + balance.LevelScreenshotLargeFilename, + balance.LevelScreenshotMediumFilename, + balance.LevelScreenshotSmallFilename, + } + for _, filename := range filenames { + if lvl.Files.Exists("assets/screenshots/" + filename) { + return true + } + } + return false +} + +// GetScreenshotImage returns a screenshot from the level's data as a Go Image. +// The filename is like "large.png" or "medium.png" and is appended to "assets/screenshots" +func (lvl *Level) GetScreenshotImage(filename string) (image.Image, error) { + data, err := lvl.Files.Get("assets/screenshots/" + filename) + if err != nil { + return nil, err + } + + return png.Decode(bytes.NewBuffer(data)) +} + +// GetScreenshotImageAsUIImage returns a ui.Image texture of a screenshot. +func (lvl *Level) GetScreenshotImageAsUIImage(filename string) (*ui.Image, error) { + // Have it cached recently? + if lvl.cacheImages == nil { + lvl.cacheImages = map[string]*ui.Image{} + } else if img, ok := lvl.cacheImages[filename]; ok { + return img, nil + } + + img, err := lvl.GetScreenshotImage(filename) + if err != nil { + return nil, err + } + + result, err := ui.ImageFromImage(img) + if err != nil { + return nil, err + } + + lvl.cacheImages[filename] = result + return result, nil +} diff --git a/pkg/level/types.go b/pkg/level/types.go index c1411da..e32b536 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" ) // Useful variables. @@ -73,6 +74,9 @@ type Level struct { // Undo history, temporary live data not persisted to the level file. UndoHistory *drawtool.History `json:"-"` + + // Cache of loaded images (e.g. screenshots). + cacheImages map[string]*ui.Image } // GameRule @@ -118,6 +122,13 @@ func (m *Level) Teardown() { textures += freed } + // Free any cached images (screenshots) + if m.cacheImages != nil { + for _, img := range m.cacheImages { + img.Destroy() + } + } + log.Debug("Teardown level (%s): Freed %d textures across %d level chunks", m.Title, textures, chunks) } diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index 2087fca..3c42625 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -34,6 +34,7 @@ type AddEditLevel struct { OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string) OnCreateNewLevel func(*level.Level) OnCreateNewDoodad func(width, height int) + OnUpdateScreenshot func() error OnReload func() OnCancel func() } @@ -74,6 +75,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { } else { // Additional Level tabs (existing level only) config.setupGameRuleFrame(tabframe) + config.setupScreenshotFrame(tabframe) config.setupAdvancedFrame(tabframe) } @@ -564,6 +566,91 @@ func (config AddEditLevel) setupGameRuleFrame(tf *ui.TabFrame) { form.Create(frame, fields) } +// Level Screenshot management frame. +func (config AddEditLevel) setupScreenshotFrame(tf *ui.TabFrame) { + frame := tf.AddTab("Screenshot", ui.NewLabel(ui.Label{ + Text: "Screenshot", + Font: balance.TabFont, + })) + + var image *ui.Image + + // Have a screenshot already? + if config.EditLevel.HasScreenshot() { + if img, err := config.EditLevel.GetScreenshotImageAsUIImage(balance.LevelScreenshotSmallFilename); err != nil { + lbl := ui.NewLabel(ui.Label{ + Text: err.Error(), + Font: balance.DangerFont, + }) + frame.Pack(lbl, ui.Pack{ + Side: ui.N, + }) + } else { + // Draw the image. + log.Error("Got img: %+v", img) + frame.Pack(img, ui.Pack{ + Side: ui.N, + }) + + // Hold onto it in case we need to refresh it. + image = img + } + } else { + lbl := ui.NewLabel(ui.Label{ + Text: "This level has no screenshot available. If this is\n" + + "a new drawing, its first screenshot will be created\n" + + "upon save; playtest or close and re-open the level\n" + + "then and its screenshot should appear here and can\n" + + "be refreshed.", + Font: balance.DangerFont, + }) + frame.Pack(lbl, ui.Pack{ + Side: ui.N, + }) + + // Exit now - don't add the Refresh button in case of nil pointer exception + // while we are in this state. + return + } + + // Image refresh button. + btn := ui.NewButton("Refresh", ui.NewLabel(ui.Label{ + Text: "Update Screenshot", + Font: balance.UIFont, + })) + ui.NewTooltip(btn, ui.Tooltip{ + Text: "Update the screenshot from your current scroll point\nin the level editor.", + }) + + btn.Handle(ui.Click, func(ed ui.EventData) error { + // Take a new screenshot. + if config.OnUpdateScreenshot == nil { + shmem.FlashError("OnUpdateScreenshot handler not configured!") + } else { + if err := config.OnUpdateScreenshot(); err != nil { + modal.Alert(err.Error()).WithTitle("Error updating screenshot") + } else { + // Get the updated screenshot image from the level. + if img, err := config.EditLevel.GetScreenshotImage(balance.LevelScreenshotSmallFilename); err != nil { + shmem.FlashError("Couldn't reload screenshot from level: %s", err) + } else { + image.ReplaceFromImage(img) + } + } + } + return nil + }) + + btn.Compute(config.Engine) + + frame.Pack(btn, ui.Pack{ + Side: ui.N, + Expand: true, + }) + + config.Supervisor.Add(btn) +} + // Creates the Game Rules frame for existing level (set difficulty, etc.) func (config AddEditLevel) setupAdvancedFrame(tf *ui.TabFrame) { frame := tf.AddTab("Advanced", ui.NewLabel(ui.Label{