From 20771fbe13c37e33f536e66bb1ee3fc48f3cc7bd Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 19 Oct 2018 13:31:58 -0700 Subject: [PATCH] Draw Actors Embedded in Levels in Edit Mode Add the JSON format for embedding Actors (Doodad instances) inside of a Level. I made a test map that manually inserted a couple of actors. Actors are given to the Canvas responsible for the Level via the function `InstallActors()`. So it means you'll call LoadLevel and then InstallActors to hook everything up. The Canvas creates sub-Canvas widgets from each Actor. After drawing the main level geometry from the Canvas.Chunker, it calls the drawActors() function which does the same but for Actors. Levels keep a global map of all Actors that exist. For any Actors that are visible within the Viewport, their sub-Canvas widgets are presented appropriately on top of the parent Canvas. In case their sub-Canvas overlaps the parent's boundaries, their sub-Canvas is resized and moved appropriately. - Allow the MainWindow to be resized at run time, and the UI recalculates its sizing and position. - Made the in-game Shell properties editable via environment variables. The kirsle.env file sets a blue and pink color scheme. - Begin the ground work for Levels and Doodads to embed files inside their data via the level.FileSystem type. - UI: Labels can now contain line break characters. It will appropriately render multiple lines of render.Text and take into account the proper BoxSize to contain them all. - Add environment variable DOODLE_DEBUG_ALL=true that will turn on ALL debug overlay and visualization options. - Add debug overlay to "tag" each Canvas widget with some of its details, like its Name and World Position. Can be enabled with the environment variable DEBUG_CANVAS_LABEL=true - Improved the FPS debug overlay to show in labeled columns and multiple colors, with easy ability to add new data points to it. --- balance/README.md | 4 + balance/debug.go | 36 ++++++- balance/shell.go | 2 +- config.go | 134 +----------------------- doodle.go | 14 ++- editor_scene.go | 41 ++++++-- editor_ui.go | 118 ++++++++++++++------- enum/enum.go | 6 ++ events/events.go | 4 + fps.go | 76 ++++++++++---- guitest_scene.go | 4 +- kirsle.env | 5 + level/actors.go | 25 +++++ level/filesystem.go | 9 ++ level/json.go | 1 + level/types.go | 6 ++ main_scene.go | 4 +- pkg/userdir/userdir.go | 124 ++++++++++++++++++++++ play_scene.go | 2 +- render/interface.go | 22 ++++ render/point.go | 6 +- render/point_test.go | 26 +++-- render/rect_test.go | 71 +++++++++++++ render/sdl/events.go | 127 ++++++++++++++++++++++ render/sdl/log.go | 7 +- render/sdl/sdl.go | 112 +------------------- shell.go | 10 +- ui/label.go | 48 ++++++--- uix/canvas.go | 232 ++++++++++++++++++++++++++++++++++++----- 29 files changed, 906 insertions(+), 370 deletions(-) create mode 100644 kirsle.env create mode 100644 level/actors.go create mode 100644 level/filesystem.go create mode 100644 pkg/userdir/userdir.go create mode 100644 render/rect_test.go create mode 100644 render/sdl/events.go diff --git a/balance/README.md b/balance/README.md index 7ece450..c2373e2 100644 --- a/balance/README.md +++ b/balance/README.md @@ -21,12 +21,16 @@ like `#FF00FF99` for 153 ($99) on the alpha channel. * `D_SHELL_FS=16`: font size for both the shell and on-screen flashed messages. * Debug Colors and Hitboxes (default invisible=off): + * `DOODLE_DEBUG_ALL=false`: turn on all debug colors and hitboxes to their + default colors and settings. * `DEBUG_CHUNK_COLOR=#FFFFFF`: background color when caching a chunk to bitmap. Helps visualize where the chunks and caching are happening. * `DEBUG_CANVAS_BORDER`: draw a border color around every uix.Canvas widget. This effectively draws the bounds of every Doodad drawn on top of a level or inside a button and the bounds of the level space itself. + * `DEBUG_CANVAS_LABEL=false`: draw a label in the corner of every Canvas + with details about the Canvas. * Tuning constants (may not be available in production builds): * `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows in the Editor Mode, in pixels per tick. diff --git a/balance/debug.go b/balance/debug.go index 12548e9..1d630ab 100644 --- a/balance/debug.go +++ b/balance/debug.go @@ -1,9 +1,9 @@ package balance import ( - "fmt" "os" "strconv" + "strings" "git.kirsle.net/apps/doodle/render" ) @@ -15,13 +15,21 @@ var ( * Visualizers * ***************/ + // Debug overlay (FPS etc.) settings. + DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf" + DebugFontSize = 15 + DebugLabelColor = render.MustHexColor("#FF9900") + DebugValueColor = render.MustHexColor("#00CCFF") + DebugStrokeDarken int32 = 80 + // Background color to use when exporting a drawing Chunk as a bitmap image // on disk. Default is white. Setting this to translucent yellow is a great // way to visualize the chunks loaded from cache on your screen. DebugChunkBitmapBackground = render.White // XXX: export $DEBUG_CHUNK_COLOR // Put a border around all Canvas widgets. - DebugCanvasBorder = render.Red + DebugCanvasBorder = render.Invisible + DebugCanvasLabel = false // Tag the canvas with a label. ) func init() { @@ -45,24 +53,33 @@ func init() { // Visualizers "DEBUG_CHUNK_COLOR": &DebugChunkBitmapBackground, "DEBUG_CANVAS_BORDER": &DebugCanvasBorder, + "DEBUG_CANVAS_LABEL": &DebugCanvasLabel, } for name, value := range config { switch v := value.(type) { case *int: *v = IntEnv(name, *(v)) + case *bool: + *v = BoolEnv(name, *(v)) case *int32: *v = int32(IntEnv(name, int(*(v)))) case *render.Color: *v = ColorEnv(name, *(v)) } } + + // Debug all? + if BoolEnv("DOODLE_DEBUG_ALL", false) { + DebugChunkBitmapBackground = render.RGBA(255, 255, 0, 128) + DebugCanvasBorder = render.Red + DebugCanvasLabel = true + } } // ColorEnv gets a color value from environment variable or returns a default. // This will panic if the color is not valid, so only do this on startup time. func ColorEnv(name string, v render.Color) render.Color { if color := os.Getenv(name); color != "" { - fmt.Printf("set %s to %s\n", name, color) return render.MustHexColor(color) } return v @@ -79,3 +96,16 @@ func IntEnv(name string, v int) int { } return v } + +// BoolEnv gets a bool from the environment with a default. +func BoolEnv(name string, v bool) bool { + if env := os.Getenv(name); env != "" { + switch strings.ToLower(env) { + case "true", "t", "1", "on", "yes", "y": + return true + case "false", "f", "0", "off", "no", "n": + return false + } + } + return v +} diff --git a/balance/shell.go b/balance/shell.go index 1657836..12ef517 100644 --- a/balance/shell.go +++ b/balance/shell.go @@ -6,13 +6,13 @@ import ( // Shell related variables. var ( - // TODO: why not renders transparent ShellFontFilename = "./fonts/DejaVuSansMono.ttf" ShellBackgroundColor = render.RGBA(0, 20, 40, 200) ShellForegroundColor = render.RGBA(0, 153, 255, 255) ShellPromptColor = render.White ShellPadding int32 = 8 ShellFontSize = 16 + ShellFontSizeSmall = 10 ShellCursorBlinkRate uint64 = 20 ShellHistoryLineCount = 8 diff --git a/config.go b/config.go index 15ad85c..3500f5f 100644 --- a/config.go +++ b/config.go @@ -2,106 +2,16 @@ package doodle import ( "fmt" - "io/ioutil" "os" "path/filepath" "regexp" "strings" - "git.kirsle.net/apps/doodle/render" - "github.com/kirsle/configdir" + "git.kirsle.net/apps/doodle/pkg/userdir" ) -// Configuration constants. -var ( - DebugTextPadding int32 = 8 - DebugTextSize = 24 - DebugTextColor = render.SkyBlue - DebugTextStroke = render.Grey - DebugTextShadow = render.Black -) - -// Profile Directory settings. -var ( - ConfigDirectoryName = "doodle" - - ProfileDirectory string - LevelDirectory string - DoodadDirectory string - - CacheDirectory string - FontDirectory string - - // Regexp to match simple filenames for maps and doodads. - reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`) -) - -// File extensions -const ( - extLevel = ".level" - extDoodad = ".doodad" -) - -func init() { - // Profile directory contains the user's levels and doodads. - ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName) - LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels") - DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads") - - // Cache directory to extract font files to. - CacheDirectory = configdir.LocalCache(ConfigDirectoryName) - FontDirectory = configdir.LocalCache(ConfigDirectoryName, "fonts") - - // Ensure all the directories exist. - configdir.MakePath(LevelDirectory) - configdir.MakePath(DoodadDirectory) - configdir.MakePath(FontDirectory) -} - -// LevelPath will turn a "simple" filename into an absolute path in the user's -// local levels folder. If the filename already contains slashes, it is returned -// as-is as an absolute or relative path. -func LevelPath(filename string) string { - return resolvePath(LevelDirectory, filename, extLevel) -} - -// DoodadPath is like LevelPath but for Doodad files. -func DoodadPath(filename string) string { - return resolvePath(DoodadDirectory, filename, extDoodad) -} - -// ListDoodads returns a listing of all available doodads. -func ListDoodads() ([]string, error) { - var names []string - - files, err := ioutil.ReadDir(DoodadDirectory) - if err != nil { - return names, err - } - - for _, file := range files { - name := file.Name() - if strings.HasSuffix(strings.ToLower(name), extDoodad) { - names = append(names, name) - } - } - - return names, nil -} - -// resolvePath is the inner logic for LevelPath and DoodadPath. -func resolvePath(directory, filename, extension string) string { - if strings.Contains(filename, "/") { - return filename - } - - // Attach the file extension? - if strings.ToLower(filepath.Ext(filename)) != extension { - filename += extension - } - - return filepath.Join(directory, filename) -} +// Regexp to match simple filenames for maps and doodads. +var reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`) /* EditFile opens a drawing file (Level or Doodad) in the EditorScene. @@ -123,7 +33,7 @@ func (d *Doodle) EditFile(filename string) error { if m := reSimpleFilename.FindStringSubmatch(filename); len(m) > 0 { log.Debug("EditFile: simple filename %s", filename) extension := strings.ToLower(filepath.Ext(filename)) - if foundFilename := d.ResolvePath(filename, extension, false); foundFilename != "" { + if foundFilename := userdir.ResolvePath(filename, extension, false); foundFilename != "" { log.Info("EditFile: resolved name '%s' to path %s", filename, foundFilename) absPath = foundFilename } else { @@ -141,39 +51,3 @@ func (d *Doodle) EditFile(filename string) error { return nil } - -// ResolvePath takes an ambiguous simple filename and searches for a Level or -// Doodad that matches. Returns a blank string if no files found. -// -// Pass a true value for `one` if you are intending to create the file. It will -// only test one filepath and return the first one, regardless if the file -// existed. So the filename should have a ".level" or ".doodad" extension and -// then this path will resolve the ProfileDirectory of the file. -func (d *Doodle) ResolvePath(filename, extension string, one bool) string { - // If the filename exists outright, return it. - if _, err := os.Stat(filename); !os.IsNotExist(err) { - return filename - } - - var paths []string - if extension == extLevel { - paths = append(paths, filepath.Join(LevelDirectory, filename)) - } else if extension == extDoodad { - paths = append(paths, filepath.Join(DoodadDirectory, filename)) - } else { - paths = append(paths, - filepath.Join(LevelDirectory, filename+".level"), - filepath.Join(DoodadDirectory, filename+".doodad"), - ) - } - - for _, test := range paths { - log.Debug("findFilename: try to find '%s' as %s", filename, test) - if _, err := os.Stat(test); os.IsNotExist(err) { - continue - } - return test - } - - return "" -} diff --git a/doodle.go b/doodle.go index 14942fc..4f129cf 100644 --- a/doodle.go +++ b/doodle.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/enum" + "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "github.com/kirsle/golog" ) @@ -28,11 +29,15 @@ type Doodle struct { Engine render.Engine engineReady bool + // Easy access to the event state, for the debug overlay to use. + // Might not be thread safe. + event *events.State + startTime time.Time running bool ticks uint64 - width int32 - height int32 + width int + height int // Command line shell options. shell Shell @@ -47,8 +52,8 @@ func New(debug bool, engine render.Engine) *Doodle { Engine: engine, startTime: time.Now(), running: true, - width: int32(balance.Width), - height: int32(balance.Height), + width: balance.Width, + height: balance.Height, } d.shell = NewShell(d) @@ -97,6 +102,7 @@ func (d *Doodle) Run() error { d.running = false break } + d.event = ev // Command line shell. if d.shell.Open { diff --git a/editor_scene.go b/editor_scene.go index cbb9905..f67f6b8 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/render" ) @@ -103,6 +104,18 @@ func (s *EditorScene) Setup(d *Doodle) error { // Loop the editor scene. func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { + // Has the window been resized? + if resized := ev.Resized.Read(); resized { + w, h := d.Engine.WindowSize() + if w != d.width || h != d.height { + // Not a false alarm. + d.width = w + d.height = h + s.UI.Resized(d) + return nil + } + } + s.UI.Loop(ev) // Switching to Play Mode? @@ -133,6 +146,7 @@ func (s *EditorScene) LoadLevel(filename string) error { s.filename = filename level, err := level.LoadJSON(filename) + fmt.Printf("%+v\n", level) if err != nil { return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err) } @@ -140,6 +154,20 @@ func (s *EditorScene) LoadLevel(filename string) error { s.DrawingType = enum.LevelDrawing s.Level = level s.UI.Canvas.LoadLevel(s.Level) + + // TODO: debug + for i, actor := range level.Actors { + log.Info("Actor %s is a %s", i, actor.ID()) + } + for name, file := range level.Files { + log.Info("File %s has: %s", name, file.Data) + } + + log.Info("Installing %d actors into the drawing", len(level.Actors)) + if err := s.UI.Canvas.InstallActors(level.Actors); err != nil { + return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err) + } + return nil } @@ -150,8 +178,8 @@ func (s *EditorScene) SaveLevel(filename string) error { return errors.New("SaveLevel: current drawing is not a Level type") } - if !strings.HasSuffix(filename, extLevel) { - filename += extLevel + if !strings.HasSuffix(filename, enum.LevelExt) { + filename += enum.LevelExt } s.filename = filename @@ -173,7 +201,7 @@ func (s *EditorScene) SaveLevel(filename string) error { } // Save it to their profile directory. - filename = LevelPath(filename) + filename = userdir.LevelPath(filename) log.Info("Write Level: %s", filename) err = ioutil.WriteFile(filename, json, 0644) if err != nil { @@ -205,8 +233,8 @@ func (s *EditorScene) SaveDoodad(filename string) error { return errors.New("SaveDoodad: current drawing is not a Doodad type") } - if !strings.HasSuffix(filename, extDoodad) { - filename += extDoodad + if !strings.HasSuffix(filename, enum.DoodadExt) { + filename += enum.DoodadExt } s.filename = filename @@ -223,7 +251,7 @@ func (s *EditorScene) SaveDoodad(filename string) error { d.Layers[0].Chunker = s.UI.Canvas.Chunker() // Save it to their profile directory. - filename = DoodadPath(filename) + filename = userdir.DoodadPath(filename) log.Info("Write Doodad: %s", filename) err := d.WriteJSON(filename) return err @@ -231,5 +259,6 @@ func (s *EditorScene) SaveDoodad(filename string) error { // Destroy the scene. func (s *EditorScene) Destroy() error { + debugWorldIndex = render.Origin return nil } diff --git a/editor_ui.go b/editor_ui.go index 45bbd1b..8691410 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -9,11 +9,15 @@ import ( "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/uix" ) +// Width of the panel frame. +var paletteWidth int32 = 150 + // EditorUI manages the user interface for the Editor Scene. type EditorUI struct { d *Doodle @@ -70,6 +74,8 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { u.Palette = u.SetupPalette(d) u.Workspace = u.SetupWorkspace(d) // important that this is last! + u.Resized(d) + // Position the Canvas inside the frame. u.Workspace.Pack(u.Canvas, ui.Pack{ Anchor: ui.N, @@ -84,14 +90,75 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { return u } +// Resized handles the window being resized so we can recompute the widgets. +func (u *EditorUI) Resized(d *Doodle) { + // Menu Bar frame. + { + u.MenuBar.Configure(ui.Config{ + Width: int32(d.width), + Background: render.Black, + }) + u.MenuBar.Compute(d.Engine) + } + + // Status Bar. + { + u.StatusBar.Configure(ui.Config{ + Width: int32(d.width), + }) + u.StatusBar.MoveTo(render.Point{ + X: 0, + Y: int32(d.height) - u.StatusBar.Size().H, + }) + u.StatusBar.Compute(d.Engine) + } + + // Palette panel. + { + u.Palette.Configure(ui.Config{ + Width: paletteWidth, + Height: int32(u.d.height) - u.StatusBar.Size().H, + }) + u.Palette.MoveTo(render.NewPoint( + int32(u.d.width)-u.Palette.BoxSize().W, + u.MenuBar.BoxSize().H, + )) + u.Palette.Compute(d.Engine) + } + + // Position the workspace around with the other widgets. + { + frame := u.Workspace + frame.MoveTo(render.NewPoint( + 0, + u.MenuBar.Size().H, + )) + frame.Resize(render.NewRect( + int32(d.width)-u.Palette.Size().W, + int32(d.height)-u.MenuBar.Size().H-u.StatusBar.Size().H, + )) + frame.Compute(d.Engine) + + u.ExpandCanvas(d.Engine) + } +} + // Loop to process events and update the UI. func (u *EditorUI) Loop(ev *events.State) { u.Supervisor.Loop(ev) - u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)", - ev.CursorX.Now, - ev.CursorY.Now, - ) + { + var P = u.Workspace.Point() + debugWorldIndex = render.NewPoint( + ev.CursorX.Now-P.X-u.Canvas.Scroll.X, + ev.CursorY.Now-P.Y-u.Canvas.Scroll.Y, + ) + u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d) Px: (%s)", + ev.CursorX.Now, + ev.CursorY.Now, + debugWorldIndex, + ) + } u.StatusPaletteText = fmt.Sprintf("Swatch: %s", u.Canvas.Palette.ActiveSwatch, ) @@ -139,18 +206,6 @@ func (u *EditorUI) Present(e render.Engine) { // Canvas, so it can easily full-screen it or center it for Doodad editing. func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame { frame := ui.NewFrame("Workspace") - - // Position and size the frame around the other main widgets. - frame.MoveTo(render.NewPoint( - 0, - u.MenuBar.Size().H, - )) - frame.Resize(render.NewRect( - d.width-u.Palette.Size().W, - d.height-u.MenuBar.Size().H-u.StatusBar.Size().H, - )) - frame.Compute(d.Engine) - return frame } @@ -170,17 +225,17 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { // in its frame, but that would artificially expand the Canvas also when it // _wanted_ to be smaller, as in Doodad Editing Mode. func (u *EditorUI) ExpandCanvas(e render.Engine) { - u.Canvas.Resize(u.Workspace.Size()) + if u.Scene.DrawingType == enum.LevelDrawing { + u.Canvas.Resize(u.Workspace.Size()) + } else { + // Size is managed externally. + } u.Workspace.Compute(e) } // SetupMenuBar sets up the menu bar. func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { frame := ui.NewFrame("MenuBar") - frame.Configure(ui.Config{ - Width: d.width, - Background: render.Black, - }) type menuButton struct { Text string @@ -293,21 +348,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { // SetupPalette sets up the palette panel. func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { - var paletteWidth int32 = 150 - window := ui.NewWindow("Palette") window.ConfigureTitle(balance.TitleConfig) window.TitleBar().Font = balance.TitleFont window.Configure(ui.Config{ - Width: paletteWidth, - Height: u.d.height - u.StatusBar.Size().H, Background: balance.WindowBackground, BorderColor: balance.WindowBorder, }) - window.MoveTo(render.NewPoint( - u.d.width-window.BoxSize().W, - u.MenuBar.BoxSize().H, - )) // Frame that holds the tab buttons in Level Edit mode. tabFrame := ui.NewFrame("Palette Tabs") @@ -355,7 +402,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { Fill: true, }) - doodadsAvailable, err := ListDoodads() + doodadsAvailable, err := userdir.ListDoodads() if err != nil { d.Flash("ListDoodads: %s", err) } @@ -376,7 +423,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { }) } - doodad, err := doodads.LoadJSON(DoodadPath(filename)) + doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename)) if err != nil { log.Error(err.Error()) doodad = doodads.New(balance.DoodadSize) @@ -459,7 +506,6 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { BorderStyle: ui.BorderRaised, Background: render.Grey, BorderSize: 2, - Width: d.width, }) style := ui.Config{ @@ -503,15 +549,13 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { Anchor: ui.E, }) + // Set the initial good frame size to have the height secured, + // so when resizing the application window we can just adjust for width. frame.Resize(render.Rect{ - W: d.width, + W: int32(d.width), H: labelHeight + frame.BoxThickness(1), }) frame.Compute(d.Engine) - frame.MoveTo(render.Point{ - X: 0, - Y: d.height - frame.Size().H, - }) return frame } diff --git a/enum/enum.go b/enum/enum.go index e71074c..ba71e35 100644 --- a/enum/enum.go +++ b/enum/enum.go @@ -10,3 +10,9 @@ const ( LevelDrawing DrawingType = iota DoodadDrawing ) + +// File extensions +const ( + LevelExt = ".level" + DoodadExt = ".doodad" +) diff --git a/events/events.go b/events/events.go index 67c86ed..eca4e23 100644 --- a/events/events.go +++ b/events/events.go @@ -25,6 +25,9 @@ type State struct { // Cursor positions. CursorX *Int32Tick CursorY *Int32Tick + + // Window events: window has changed size. + Resized *BoolTick } // New creates a new event state manager. @@ -43,6 +46,7 @@ func New() *State { Down: &BoolTick{}, CursorX: &Int32Tick{}, CursorY: &Int32Tick{}, + Resized: &BoolTick{}, } } diff --git a/fps.go b/fps.go index 6d2295e..f1b6147 100644 --- a/fps.go +++ b/fps.go @@ -2,9 +2,12 @@ package doodle import ( "fmt" + "strings" + "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/ui" ) // Frames to cache for FPS calculation. @@ -15,6 +18,12 @@ const maxSamples = 100 var ( DebugOverlay = true DebugCollision = true + + DebugTextPadding int32 = 8 + DebugTextSize = 24 + DebugTextColor = render.SkyBlue + DebugTextStroke = render.Grey + DebugTextShadow = render.Black ) var ( @@ -24,6 +33,11 @@ var ( fpsFrames int fpsSkipped uint32 fpsInterval uint32 = 1000 + + // XXX: some opt-in WorldIndex variables for the debug overlay. + // This is the world pixel that the mouse cursor is over, + // the Cursor + Scroll position of the canvas. + debugWorldIndex render.Point ) // DrawDebugOverlay draws the debug FPS text on the SDL canvas. @@ -32,29 +46,53 @@ func (d *Doodle) DrawDebugOverlay() { return } - label := fmt.Sprintf( - "FPS: %d (%dms) S:%s F12=screenshot", - fpsCurrent, - fpsSkipped, - d.Scene.Name(), + var ( + darken = balance.DebugStrokeDarken + Yoffset int32 = 20 // leave room for the menu bar + Xoffset int32 = 5 + keys = []string{ + " FPS:", + "Scene:", + "Pixel:", + "Mouse:", + } + values = []string{ + fmt.Sprintf("%d (skip: %dms)", fpsCurrent, fpsSkipped), + d.Scene.Name(), + debugWorldIndex.String(), + fmt.Sprintf("%d,%d", d.event.CursorX.Now, d.event.CursorY.Now), + } ) - err := d.Engine.DrawText( - render.Text{ - Text: label, - Size: 24, - Color: DebugTextColor, - Stroke: DebugTextStroke, - Shadow: DebugTextShadow, + key := ui.NewLabel(ui.Label{ + Text: strings.Join(keys, "\n"), + Font: render.Text{ + Size: balance.DebugFontSize, + FontFilename: balance.ShellFontFilename, + Color: balance.DebugLabelColor, + Stroke: balance.DebugLabelColor.Darken(darken), }, - render.Point{ - X: DebugTextPadding, - Y: DebugTextPadding + 32, // extra padding to not overlay menu bars + }) + key.Compute(d.Engine) + key.Present(d.Engine, render.NewPoint( + DebugTextPadding+Xoffset, + DebugTextPadding+Yoffset, + )) + + value := ui.NewLabel(ui.Label{ + Text: strings.Join(values, "\n"), + Font: render.Text{ + Size: balance.DebugFontSize, + FontFilename: balance.DebugFontFilename, + Color: balance.DebugValueColor, + Stroke: balance.DebugValueColor.Darken(darken), }, - ) - if err != nil { - log.Error("DrawDebugOverlay: text error: %s", err.Error()) - } + }) + value.Compute(d.Engine) + value.Present(d.Engine, render.NewPoint( + DebugTextPadding+Xoffset+key.Size().W+DebugTextPadding, + DebugTextPadding+Yoffset, // padding to not overlay menu bar + )) } // DrawCollisionBox draws the collision box around a Doodad. diff --git a/guitest_scene.go b/guitest_scene.go index 2e42db5..cd1722d 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -275,14 +275,14 @@ func (s *GUITestScene) Draw(d *Doodle) error { }) label.Compute(d.Engine) label.MoveTo(render.Point{ - X: (d.width / 2) - (label.Size().W / 2), + X: (int32(d.width) / 2) - (label.Size().W / 2), Y: 40, }) label.Present(d.Engine, label.Point()) s.Window.Compute(d.Engine) s.Window.MoveTo(render.Point{ - X: (d.width / 2) - (s.Window.Size().W / 2), + X: (int32(d.width) / 2) - (s.Window.Size().W / 2), Y: 100, }) s.Window.Present(d.Engine, s.Window.Point()) diff --git a/kirsle.env b/kirsle.env new file mode 100644 index 0000000..e83754d --- /dev/null +++ b/kirsle.env @@ -0,0 +1,5 @@ +#!/bin/bash + +export D_SHELL_BG="#001133DD" +export D_SHELL_FG="#FF99FF" +export D_SHELL_PC="#FF9900" diff --git a/level/actors.go b/level/actors.go new file mode 100644 index 0000000..c912e14 --- /dev/null +++ b/level/actors.go @@ -0,0 +1,25 @@ +package level + +import "git.kirsle.net/apps/doodle/render" + +// ActorMap holds the doodad information by their ID in the level data. +type ActorMap map[string]*Actor + +// Inflate assigns each actor its ID from the hash map for their self reference. +func (m ActorMap) Inflate() { + for id, actor := range m { + actor.id = id + } +} + +// Actor is an instance of a Doodad in the level. +type Actor struct { + id string // NOTE: read only, use ID() to access. + Filename string `json:"filename"` // like "exit.doodad" + Point render.Point `json:"point"` +} + +// ID returns the actor's ID. +func (a *Actor) ID() string { + return a.id +} diff --git a/level/filesystem.go b/level/filesystem.go new file mode 100644 index 0000000..6459216 --- /dev/null +++ b/level/filesystem.go @@ -0,0 +1,9 @@ +package level + +// FileSystem embeds a map of files inside a parent drawing. +type FileSystem map[string]File + +// File holds details about a file in the FileSystem. +type File struct { + Data []byte `json:"data"` +} diff --git a/level/json.go b/level/json.go index 7cd89f6..d666e30 100644 --- a/level/json.go +++ b/level/json.go @@ -50,6 +50,7 @@ func LoadJSON(filename string) (*Level, error) { // Inflate the chunk metadata to map the pixels to their palette indexes. m.Chunker.Inflate(m.Palette) + m.Actors.Inflate() // Inflate the private instance values. m.Palette.Inflate() diff --git a/level/types.go b/level/types.go index e318d10..a396cbf 100644 --- a/level/types.go +++ b/level/types.go @@ -15,6 +15,9 @@ type Base struct { GameVersion string `json:"gameVersion"` // Game version that created the level. Title string `json:"title"` Author string `json:"author"` + + // Every drawing type is able to embed other files inside of itself. + Files FileSystem `json:"files"` } // Level is the container format for Doodle map drawings. @@ -29,6 +32,9 @@ type Level struct { // The Palette holds the unique "colors" used in this map file, and their // properties (solid, fire, slippery, etc.) Palette *Palette `json:"palette"` + + // Actors keep a list of the doodad instances in this map. + Actors ActorMap `json:"actors"` } // New creates a blank level object with all its members initialized. diff --git a/main_scene.go b/main_scene.go index 8717c6e..b559382 100644 --- a/main_scene.go +++ b/main_scene.go @@ -77,14 +77,14 @@ func (s *MainScene) Draw(d *Doodle) error { }) label.Compute(d.Engine) label.MoveTo(render.Point{ - X: (d.width / 2) - (label.Size().W / 2), + X: (int32(d.width) / 2) - (label.Size().W / 2), Y: 120, }) label.Present(d.Engine, label.Point()) s.frame.Compute(d.Engine) s.frame.MoveTo(render.Point{ - X: (d.width / 2) - (s.frame.Size().W / 2), + X: (int32(d.width) / 2) - (s.frame.Size().W / 2), Y: 200, }) s.frame.Present(d.Engine, s.frame.Point()) diff --git a/pkg/userdir/userdir.go b/pkg/userdir/userdir.go new file mode 100644 index 0000000..ac3b814 --- /dev/null +++ b/pkg/userdir/userdir.go @@ -0,0 +1,124 @@ +package userdir + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/kirsle/configdir" +) + +// Profile Directory settings. +var ( + ConfigDirectoryName = "doodle" + + ProfileDirectory string + LevelDirectory string + DoodadDirectory string + + CacheDirectory string + FontDirectory string +) + +// File extensions +const ( + extLevel = ".level" + extDoodad = ".doodad" +) + +func init() { + // Profile directory contains the user's levels and doodads. + ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName) + LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels") + DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads") + + // Cache directory to extract font files to. + CacheDirectory = configdir.LocalCache(ConfigDirectoryName) + FontDirectory = configdir.LocalCache(ConfigDirectoryName, "fonts") + + // Ensure all the directories exist. + configdir.MakePath(LevelDirectory) + configdir.MakePath(DoodadDirectory) + configdir.MakePath(FontDirectory) +} + +// LevelPath will turn a "simple" filename into an absolute path in the user's +// local levels folder. If the filename already contains slashes, it is returned +// as-is as an absolute or relative path. +func LevelPath(filename string) string { + return resolvePath(LevelDirectory, filename, extLevel) +} + +// DoodadPath is like LevelPath but for Doodad files. +func DoodadPath(filename string) string { + return resolvePath(DoodadDirectory, filename, extDoodad) +} + +// ListDoodads returns a listing of all available doodads. +func ListDoodads() ([]string, error) { + var names []string + + files, err := ioutil.ReadDir(DoodadDirectory) + if err != nil { + return names, err + } + + for _, file := range files { + name := file.Name() + if strings.HasSuffix(strings.ToLower(name), extDoodad) { + names = append(names, name) + } + } + + return names, nil +} + +// resolvePath is the inner logic for LevelPath and DoodadPath. +func resolvePath(directory, filename, extension string) string { + if strings.Contains(filename, "/") { + return filename + } + + // Attach the file extension? + if strings.ToLower(filepath.Ext(filename)) != extension { + filename += extension + } + + return filepath.Join(directory, filename) +} + +// ResolvePath takes an ambiguous simple filename and searches for a Level or +// Doodad that matches. Returns a blank string if no files found. +// +// Pass a true value for `one` if you are intending to create the file. It will +// only test one filepath and return the first one, regardless if the file +// existed. So the filename should have a ".level" or ".doodad" extension and +// then this path will resolve the ProfileDirectory of the file. +func ResolvePath(filename, extension string, one bool) string { + // If the filename exists outright, return it. + if _, err := os.Stat(filename); !os.IsNotExist(err) { + return filename + } + + var paths []string + if extension == extLevel { + paths = append(paths, filepath.Join(LevelDirectory, filename)) + } else if extension == extDoodad { + paths = append(paths, filepath.Join(DoodadDirectory, filename)) + } else { + paths = append(paths, + filepath.Join(LevelDirectory, filename+".level"), + filepath.Join(DoodadDirectory, filename+".doodad"), + ) + } + + for _, test := range paths { + if _, err := os.Stat(test); os.IsNotExist(err) { + continue + } + return test + } + + return "" +} diff --git a/play_scene.go b/play_scene.go index dc2d227..659d70a 100644 --- a/play_scene.go +++ b/play_scene.go @@ -33,7 +33,7 @@ func (s *PlayScene) Name() string { func (s *PlayScene) Setup(d *Doodle) error { s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing.MoveTo(render.Origin) - s.drawing.Resize(render.NewRect(d.width, d.height)) + s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) s.drawing.Compute(d.Engine) // Given a filename or map data to play? diff --git a/render/interface.go b/render/interface.go index f051e8c..1a30ad9 100644 --- a/render/interface.go +++ b/render/interface.go @@ -15,6 +15,7 @@ type Engine interface { // Poll for events like keypresses and mouse clicks. Poll() (*events.State, error) GetTicks() uint32 + WindowSize() (w, h int) // Present presents the current state to the screen. Present() error @@ -88,6 +89,27 @@ func (r Rect) Bigger(other Rect) bool { other.H > r.H) // Taller } +// Intersects with the other rectangle in any way. +func (r Rect) Intersects(other Rect) bool { + // Do a bidirectional compare. + compare := func(a, b Rect) bool { + var corners = []Point{ + NewPoint(b.X, b.Y), + NewPoint(b.X, b.Y+b.H), + NewPoint(b.X+b.W, b.Y), + NewPoint(b.X+b.W, b.Y+b.H), + } + for _, pt := range corners { + if pt.Inside(a) { + return true + } + } + return false + } + + return compare(r, other) || compare(other, r) || false +} + // IsZero returns if the Rect is uninitialized. func (r Rect) IsZero() bool { return r.X == 0 && r.Y == 0 && r.W == 0 && r.H == 0 diff --git a/render/point.go b/render/point.go index ac10d12..272238c 100644 --- a/render/point.go +++ b/render/point.go @@ -55,6 +55,9 @@ func (p Point) IsZero() bool { } // Inside returns whether the Point falls inside the rect. +// +// NOTICE: the W and H are zero-relative, so a 100x100 box at coordinate +// X,Y would still have W,H of 100. func (p Point) Inside(r Rect) bool { var ( x1 = r.X @@ -62,7 +65,8 @@ func (p Point) Inside(r Rect) bool { x2 = r.X + r.W y2 = r.Y + r.H ) - return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2 + return ((p.X >= x1 && p.X <= x2) && + (p.Y >= y1 && p.Y <= y2)) } // Add (or subtract) the other point to your current point. diff --git a/render/point_test.go b/render/point_test.go index 770ce57..d56a70a 100644 --- a/render/point_test.go +++ b/render/point_test.go @@ -8,13 +8,9 @@ import ( ) func TestPointInside(t *testing.T) { - var p = render.Point{ - X: 128, - Y: 256, - } - type testCase struct { rect render.Rect + p render.Point shouldPass bool } tests := []testCase{ @@ -25,6 +21,7 @@ func TestPointInside(t *testing.T) { W: 500, H: 500, }, + p: render.NewPoint(128, 256), shouldPass: true, }, testCase{ @@ -34,14 +31,27 @@ func TestPointInside(t *testing.T) { W: 40, H: 60, }, + p: render.NewPoint(128, 256), shouldPass: false, }, + testCase{ + // true values when debugging why Doodads weren't + // considered inside the viewport. + rect: render.Rect{ + X: 0, + Y: -232, + H: 874, + W: 490, + }, + p: render.NewPoint(509, 260), + shouldPass: true, + }, } for _, test := range tests { - if p.Inside(test.rect) != test.shouldPass { - t.Errorf("Failed: %s inside %s should %s", - p, + if test.p.Inside(test.rect) != test.shouldPass { + t.Errorf("Failed: %s inside %s should be %s", + test.p, test.rect, strconv.FormatBool(test.shouldPass), ) diff --git a/render/rect_test.go b/render/rect_test.go new file mode 100644 index 0000000..882b6fc --- /dev/null +++ b/render/rect_test.go @@ -0,0 +1,71 @@ +package render_test + +import ( + "strconv" + "testing" + + "git.kirsle.net/apps/doodle/render" +) + +func TestIntersection(t *testing.T) { + newRect := func(x, y, w, h int) render.Rect { + return render.Rect{ + X: int32(x), + Y: int32(y), + W: int32(w), + H: int32(h), + } + } + + type TestCase struct { + A render.Rect + B render.Rect + Expect bool + } + var tests = []TestCase{ + { + A: newRect(0, 0, 1000, 1000), + B: newRect(200, 200, 100, 100), + Expect: true, + }, + { + A: newRect(200, 200, 100, 100), + B: newRect(0, 0, 1000, 1000), + Expect: true, + }, + { + A: newRect(0, 0, 100, 100), + B: newRect(100, 0, 100, 100), + Expect: true, + }, + { + A: newRect(0, 0, 99, 99), + B: newRect(100, 0, 99, 99), + Expect: false, + }, + { + // Real coords of a test doodad! + A: newRect(183, 256, 283, 356), + B: newRect(0, -232, 874, 490), + Expect: true, + }, + { + A: newRect(183, 256, 283, 356), + B: newRect(0, -240, 874, 490), + Expect: false, // XXX: must be true + }, + } + + for _, test := range tests { + actual := test.A.Intersects(test.B) + if actual != test.Expect { + t.Errorf( + "%s collision with %s: expected %s, got %s", + test.A, + test.B, + strconv.FormatBool(test.Expect), + strconv.FormatBool(actual), + ) + } + } +} diff --git a/render/sdl/events.go b/render/sdl/events.go new file mode 100644 index 0000000..ea54187 --- /dev/null +++ b/render/sdl/events.go @@ -0,0 +1,127 @@ +package sdl + +import ( + "errors" + + "git.kirsle.net/apps/doodle/events" + "github.com/veandco/go-sdl2/sdl" +) + +// Poll for events. +func (r *Renderer) Poll() (*events.State, error) { + s := r.events + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch t := event.(type) { + case *sdl.QuitEvent: + return s, errors.New("quit") + case *sdl.WindowEvent: + if DebugWindowEvents { + if t.Event == sdl.WINDOWEVENT_RESIZED { + log.Debug("[%d ms] tick:%d Window Resized to %dx%d", + t.Timestamp, + r.ticks, + t.Data1, + t.Data2, + ) + } + } + s.Resized.Push(true) + case *sdl.MouseMotionEvent: + if DebugMouseEvents { + log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, + ) + } + + // Push the cursor position. + s.CursorX.Push(t.X) + s.CursorY.Push(t.Y) + s.Button1.Push(t.State == 1) + case *sdl.MouseButtonEvent: + if DebugClickEvents { + log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State, + ) + } + + // Push the cursor position. + s.CursorX.Push(t.X) + s.CursorY.Push(t.Y) + + // Is a mouse button pressed down? + if t.Button == 1 { + var eventName string + if t.State == 1 && s.Button1.Now == false { + eventName = "DOWN" + } else if t.State == 0 && s.Button1.Now == true { + eventName = "UP" + } + + if eventName != "" { + s.Button1.Push(eventName == "DOWN") + + // Return the event immediately. + return s, nil + } + } + + // s.Button2.Push(t.Button == 3 && t.State == 1) + case *sdl.MouseWheelEvent: + if DebugMouseEvents { + log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, + ) + } + case *sdl.KeyboardEvent: + if DebugKeyEvents { + log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", + t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, + ) + } + + switch t.Keysym.Scancode { + case sdl.SCANCODE_ESCAPE: + if t.Repeat == 1 { + continue + } + s.EscapeKey.Push(t.State == 1) + case sdl.SCANCODE_RETURN: + if t.Repeat == 1 { + continue + } + s.EnterKey.Push(t.State == 1) + case sdl.SCANCODE_F12: + s.ScreenshotKey.Push(t.State == 1) + case sdl.SCANCODE_UP: + s.Up.Push(t.State == 1) + case sdl.SCANCODE_LEFT: + s.Left.Push(t.State == 1) + case sdl.SCANCODE_RIGHT: + s.Right.Push(t.State == 1) + case sdl.SCANCODE_DOWN: + s.Down.Push(t.State == 1) + case sdl.SCANCODE_LSHIFT: + case sdl.SCANCODE_RSHIFT: + s.ShiftActive.Push(t.State == 1) + continue + case sdl.SCANCODE_LALT: + case sdl.SCANCODE_RALT: + case sdl.SCANCODE_LCTRL: + case sdl.SCANCODE_RCTRL: + continue + case sdl.SCANCODE_BACKSPACE: + // Make it a key event with "\b" as the sequence. + if t.State == 1 || t.Repeat == 1 { + s.KeyName.Push(`\b`) + } + default: + // Push the string value of the key. + if t.State == 1 { + s.KeyName.Push(string(t.Keysym.Sym)) + } + } + } + } + + return s, nil +} diff --git a/render/sdl/log.go b/render/sdl/log.go index 1564f5d..00c5fef 100644 --- a/render/sdl/log.go +++ b/render/sdl/log.go @@ -6,9 +6,10 @@ var log *golog.Logger // Verbose debug logging. var ( - DebugMouseEvents = false - DebugClickEvents = false - DebugKeyEvents = false + DebugMouseEvents = false + DebugClickEvents = false + DebugKeyEvents = false + DebugWindowEvents = false ) func init() { diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go index b23cc4f..2b6bb8e 100644 --- a/render/sdl/sdl.go +++ b/render/sdl/sdl.go @@ -2,7 +2,6 @@ package sdl import ( - "errors" "time" "git.kirsle.net/apps/doodle/events" @@ -69,7 +68,7 @@ func (r *Renderer) Setup() error { sdl.WINDOWPOS_CENTERED, r.width, r.height, - sdl.WINDOW_SHOWN, + sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE, ) if err != nil { return err @@ -93,111 +92,10 @@ func (r *Renderer) GetTicks() uint32 { return sdl.GetTicks() } -// Poll for events. -func (r *Renderer) Poll() (*events.State, error) { - s := r.events - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch t := event.(type) { - case *sdl.QuitEvent: - return s, errors.New("quit") - case *sdl.MouseMotionEvent: - if DebugMouseEvents { - log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, - ) - } - - // Push the cursor position. - s.CursorX.Push(t.X) - s.CursorY.Push(t.Y) - s.Button1.Push(t.State == 1) - case *sdl.MouseButtonEvent: - if DebugClickEvents { - log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State, - ) - } - - // Push the cursor position. - s.CursorX.Push(t.X) - s.CursorY.Push(t.Y) - - // Is a mouse button pressed down? - if t.Button == 1 { - var eventName string - if t.State == 1 && s.Button1.Now == false { - eventName = "DOWN" - } else if t.State == 0 && s.Button1.Now == true { - eventName = "UP" - } - - if eventName != "" { - s.Button1.Push(eventName == "DOWN") - - // Return the event immediately. - return s, nil - } - } - - // s.Button2.Push(t.Button == 3 && t.State == 1) - case *sdl.MouseWheelEvent: - if DebugMouseEvents { - log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, - ) - } - case *sdl.KeyboardEvent: - if DebugKeyEvents { - log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, - ) - } - - switch t.Keysym.Scancode { - case sdl.SCANCODE_ESCAPE: - if t.Repeat == 1 { - continue - } - s.EscapeKey.Push(t.State == 1) - case sdl.SCANCODE_RETURN: - if t.Repeat == 1 { - continue - } - s.EnterKey.Push(t.State == 1) - case sdl.SCANCODE_F12: - s.ScreenshotKey.Push(t.State == 1) - case sdl.SCANCODE_UP: - s.Up.Push(t.State == 1) - case sdl.SCANCODE_LEFT: - s.Left.Push(t.State == 1) - case sdl.SCANCODE_RIGHT: - s.Right.Push(t.State == 1) - case sdl.SCANCODE_DOWN: - s.Down.Push(t.State == 1) - case sdl.SCANCODE_LSHIFT: - case sdl.SCANCODE_RSHIFT: - s.ShiftActive.Push(t.State == 1) - continue - case sdl.SCANCODE_LALT: - case sdl.SCANCODE_RALT: - case sdl.SCANCODE_LCTRL: - case sdl.SCANCODE_RCTRL: - continue - case sdl.SCANCODE_BACKSPACE: - // Make it a key event with "\b" as the sequence. - if t.State == 1 || t.Repeat == 1 { - s.KeyName.Push(`\b`) - } - default: - // Push the string value of the key. - if t.State == 1 { - s.KeyName.Push(string(t.Keysym.Sym)) - } - } - } - } - - return s, nil +// WindowSize returns the SDL window size. +func (r *Renderer) WindowSize() (int, int) { + w, h := r.window.GetSize() + return int(w), int(h) } // Present the current frame. diff --git a/shell.go b/shell.go index ca0228a..1ce49d5 100644 --- a/shell.go +++ b/shell.go @@ -281,14 +281,14 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error { balance.ShellBackgroundColor, render.Rect{ X: 0, - Y: d.height - boxHeight, - W: d.width, + Y: int32(d.height) - boxHeight, + W: int32(d.width), H: boxHeight, }, ) // Draw the recent commands. - outputY := d.height - int32(lineHeight*2) + outputY := int32(d.height - (lineHeight * 2)) for i := 0; i < balance.ShellHistoryLineCount; i++ { if len(s.Output) > i { line := s.Output[len(s.Output)-1-i] @@ -318,14 +318,14 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error { }, render.Point{ X: balance.ShellPadding, - Y: d.height - int32(balance.ShellFontSize) - balance.ShellPadding, + Y: int32(d.height-balance.ShellFontSize) - balance.ShellPadding, }, ) } else if len(s.Flashes) > 0 { // Otherwise, just draw flashed messages. valid := false // Did we actually draw any? - outputY := d.height - int32(lineHeight*2) + outputY := int32(d.height - (lineHeight * 2)) for i := len(s.Flashes); i > 0; i-- { flash := s.Flashes[i-1] if d.ticks >= flash.Expires { diff --git a/ui/label.go b/ui/label.go index c7d191b..8f3cfdf 100644 --- a/ui/label.go +++ b/ui/label.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "strings" "git.kirsle.net/apps/doodle/render" ) @@ -21,8 +22,9 @@ type Label struct { TextVariable *string Font render.Text - width int32 - height int32 + width int32 + height int32 + lineHeight int } // NewLabel creates a new label. @@ -60,10 +62,24 @@ func (w *Label) Value() string { // Compute the size of the label widget. func (w *Label) Compute(e render.Engine) { - rect, err := e.ComputeTextRect(w.text()) - if err != nil { - log.Error("%s: failed to compute text rect: %s", w, err) - return + text := w.text() + lines := strings.Split(text.Text, "\n") + + // Max rect to encompass all lines of text. + var maxRect = render.Rect{} + for _, line := range lines { + text.Text = line // only this line at this time. + rect, err := e.ComputeTextRect(text) + if err != nil { + log.Error("%s: failed to compute text rect: %s", w, err) + return + } + + if rect.W > maxRect.W { + maxRect.W = rect.W + } + maxRect.H += rect.H + w.lineHeight = int(rect.H) } var ( @@ -73,14 +89,14 @@ func (w *Label) Compute(e render.Engine) { if !w.FixedSize() { w.resizeAuto(render.Rect{ - W: rect.W + (padX * 2), - H: rect.H + (padY * 2), + W: maxRect.W + (padX * 2), + H: maxRect.H + (padY * 2), }) } w.MoveTo(render.Point{ - X: rect.X + w.BoxThickness(1), - Y: rect.Y + w.BoxThickness(1), + X: maxRect.X + w.BoxThickness(1), + Y: maxRect.Y + w.BoxThickness(1), }) } @@ -93,13 +109,17 @@ func (w *Label) Present(e render.Engine, P render.Point) { border := w.BoxThickness(1) var ( + text = w.text() padX = w.Font.Padding + w.Font.PadX padY = w.Font.Padding + w.Font.PadY ) w.DrawBox(e, P) - e.DrawText(w.text(), render.Point{ - X: P.X + border + padX, - Y: P.Y + border + padY, - }) + for i, line := range strings.Split(text.Text, "\n") { + text.Text = line + e.DrawText(text, render.Point{ + X: P.X + border + padX, + Y: P.Y + border + padY + int32(i*w.lineHeight), + }) + } } diff --git a/uix/canvas.go b/uix/canvas.go index 82bce3e..8b9ef78 100644 --- a/uix/canvas.go +++ b/uix/canvas.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" ) @@ -21,7 +22,14 @@ type Canvas struct { Editable bool Scrollable bool - chunks *level.Chunker + // Underlying chunk data for the drawing. + chunks *level.Chunker + + // Actors to superimpose on top of the drawing. + actor *level.Actor // if this canvas IS an actor + actors []*Actor + + // Tracking pixels while editing. TODO: get rid of pixelHistory? pixelHistory []*level.Pixel lastPixel *level.Pixel @@ -29,6 +37,12 @@ type Canvas struct { Scroll render.Point // Scroll offset for which parts of canvas are visible. } +// Actor is an instance of an actor with a Canvas attached. +type Actor struct { + Actor *level.Actor + Canvas *Canvas +} + // NewCanvas initializes a Canvas widget. // // If editable is true, Scrollable is also set to true, which means the arrow @@ -39,6 +53,7 @@ func NewCanvas(size int, editable bool) *Canvas { Scrollable: editable, Palette: level.NewPalette(), chunks: level.NewChunker(size), + actors: make([]*Actor, 0), } w.setup() w.IDFunc(func() string { @@ -80,6 +95,32 @@ func (w *Canvas) LoadDoodad(d *doodads.Doodad) { w.Load(d.Palette, d.Layers[0].Chunker) } +// InstallActors adds external Actors to the canvas to be superimposed on top +// of the drawing. +func (w *Canvas) InstallActors(actors level.ActorMap) error { + w.actors = make([]*Actor, 0) + for id, actor := range actors { + log.Info("InstallActors: %s", id) + + doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename)) + if err != nil { + return fmt.Errorf("InstallActors: %s", err) + } + + size := int32(doodad.Layers[0].Chunker.Size) + can := NewCanvas(int(size), false) + can.Name = id + can.actor = actor + can.LoadDoodad(doodad) + can.Resize(render.NewRect(size, size)) + w.actors = append(w.actors, &Actor{ + Actor: actor, + Canvas: can, + }) + } + return nil +} + // SetSwatch changes the currently selected swatch for editing. func (w *Canvas) SetSwatch(s *level.Swatch) { w.Palette.ActiveSwatch = s @@ -88,6 +129,16 @@ func (w *Canvas) SetSwatch(s *level.Swatch) { // setup common configs between both initializers of the canvas. func (w *Canvas) setup() { w.SetBackground(render.White) + + // XXX: Debug code. + if balance.DebugCanvasBorder != render.Invisible { + w.Configure(ui.Config{ + BorderColor: balance.DebugCanvasBorder, + BorderSize: 2, + BorderStyle: ui.BorderSolid, + }) + } + w.Handle(ui.MouseOver, func(p render.Point) { w.SetBackground(render.Yellow) }) @@ -145,14 +196,6 @@ func (w *Canvas) Loop(ev *events.State) error { Swatch: w.Palette.ActiveSwatch, } - log.Warn( - "real cursor: %d,%d translated: %s widget pos: %s scroll: %s", - ev.CursorX.Now, ev.CursorY.Now, - cursor, - P, - w.Scroll, - ) - // Append unique new pixels. if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel { if lastPixel != nil { @@ -168,7 +211,6 @@ func (w *Canvas) Loop(ev *events.State) error { w.pixelHistory = append(w.pixelHistory, pixel) // Save in the pixel canvas map. - log.Info("Set: %s %s", cursor, pixel.Swatch.Color) w.chunks.Set(cursor, pixel.Swatch) } } else { @@ -181,6 +223,15 @@ func (w *Canvas) Loop(ev *events.State) error { // Viewport returns a rect containing the viewable drawing coordinates in this // canvas. The X,Y values are the scroll offset (top left) and the W,H values // are the scroll offset plus the width/height of the Canvas widget. +// +// The Viewport rect are the Absolute World Coordinates of the drawing that are +// visible inside the Canvas. The X,Y is the top left World Coordinate and the +// W,H are the bottom right World Coordinate, making this rect an absolute +// slice of the world. For a normal rect with a relative width and height, +// use ViewportRelative(). +// +// The rect X,Y are the negative Scroll Value. +// The rect W,H are the Canvas widget size minus the Scroll Value. func (w *Canvas) Viewport() render.Rect { var S = w.Size() return render.Rect{ @@ -191,6 +242,22 @@ func (w *Canvas) Viewport() render.Rect { } } +// ViewportRelative returns a relative viewport where the Width and Height +// values are zero-relative: so you can use it with point.Inside(viewport) +// to see if a World Index point should be visible on screen. +// +// The rect X,Y are the negative Scroll Value +// The rect W,H are the Canvas widget size. +func (w *Canvas) ViewportRelative() render.Rect { + var S = w.Size() + return render.Rect{ + X: -w.Scroll.X, + Y: -w.Scroll.Y, + W: S.W, + H: S.H, + } +} + // Chunker returns the underlying Chunker object. func (w *Canvas) Chunker() *level.Chunker { return w.chunks @@ -254,8 +321,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { // src.W and src.H will be AT MOST the full width and height of // a Canvas widget. Subtract the scroll offset to keep it bounded // visually on its right and bottom sides. - W: src.W, // - w.Scroll.X, - H: src.H, // - w.Scroll.Y, + W: src.W, + H: src.H, } // If the destination width will cause it to overflow the widget @@ -273,13 +340,13 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { if dst.X+src.W > p.X+S.W { // NOTE: delta is a negative number, // so it will subtract from the width. - delta := (S.W + p.X) - (dst.W + dst.X) + delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X) src.W += delta dst.W += delta } if dst.Y+src.H > p.Y+S.H { // NOTE: delta is a negative number - delta := (S.H + p.Y) - (dst.H + dst.Y) + delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y) src.H += delta dst.H += delta } @@ -298,30 +365,141 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { // NOTE: delta is a positive number, // so it will add to the destination coordinates. delta := p.X - dst.X - dst.X = p.X + dst.X = p.X + w.BoxThickness(1) dst.W -= delta src.X += delta } if dst.Y < p.Y { delta := p.Y - dst.Y - dst.Y = p.Y + dst.Y = p.Y + w.BoxThickness(1) dst.H -= delta src.Y += delta } + // Trim the destination width so it doesn't overlap the Canvas border. + if dst.W >= S.W-w.BoxThickness(1) { + dst.W = S.W - w.BoxThickness(1) + } + e.Copy(tex, src, dst) } } - // for px := range w.chunks.IterViewport(Viewport) { - // // This pixel is visible in the canvas, but offset it by the - // // scroll height. - // px.X -= Viewport.X - // px.Y -= Viewport.Y - // color := render.Cyan // px.Swatch.Color - // e.DrawPoint(color, render.Point{ - // X: p.X + w.BoxThickness(1) + px.X, - // Y: p.Y + w.BoxThickness(1) + px.Y, - // }) - // } + w.drawActors(e, p) + + // XXX: Debug, show label in canvas corner. + if balance.DebugCanvasLabel { + rows := []string{ + w.Name, + + // XXX: debug options, uncomment for more details + + // Size of the canvas + // fmt.Sprintf("S=%d,%d", S.W, S.H), + + // Viewport of the canvas + // fmt.Sprintf("V=%d,%d:%d,%d", + // Viewport.X, Viewport.Y, + // Viewport.W, Viewport.H, + // ), + } + if w.actor != nil { + rows = append(rows, + fmt.Sprintf("WP=%s", w.actor.Point), + ) + } + label := ui.NewLabel(ui.Label{ + Text: strings.Join(rows, "\n"), + Font: render.Text{ + FontFilename: balance.ShellFontFilename, + Size: balance.ShellFontSizeSmall, + Color: render.White, + }, + }) + label.SetBackground(render.RGBA(0, 0, 50, 150)) + label.Compute(e) + label.Present(e, render.Point{ + X: p.X + S.W - label.Size().W - w.BoxThickness(1), + Y: p.Y + w.BoxThickness(1), + }) + } +} + +// drawActors superimposes the actors on top of the drawing. +func (w *Canvas) drawActors(e render.Engine, p render.Point) { + var ( + Viewport = w.ViewportRelative() + S = w.Size() + ) + + // See if each Actor is in range of the Viewport. + for _, a := range w.actors { + var ( + actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE + can = a.Canvas // Canvas widget that draws the actor + actorPoint = actor.Point // XXX TODO: DO NOT CHANGE + actorSize = can.Size() + ) + + // Create a box of World Coordinates that this actor occupies. The + // Actor X,Y from level data is already a World Coordinate; + // accomodate for the size of the Actor. + actorBox := render.Rect{ + X: actorPoint.X, + Y: actorPoint.Y, + W: actorSize.W, + H: actorSize.H, + } + + // Is any part of the actor visible? + if !Viewport.Intersects(actorBox) { + continue // not visible on screen + } + + drawAt := render.Point{ + X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1), + Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1), + } + resizeTo := actorSize + + // XXX TODO: when an Actor hits the left or top edge and shrinks, + // scrolling to offset that shrink is currently hard to solve. + scrollTo := render.Origin + + // Handle cropping and scaling if this Actor's canvas can't be + // completely visible within the parent. + if drawAt.X+resizeTo.W > p.X+S.W { + // Hitting the right edge, shrunk the width now. + delta := (drawAt.X + resizeTo.W) - (p.X + S.W) + resizeTo.W -= delta + } else if drawAt.X < p.X { + // Hitting the left edge. Cap the X coord and shrink the width. + delta := p.X - drawAt.X // positive number + drawAt.X = p.X + // scrollTo.X -= delta // TODO + resizeTo.W -= delta + } + + if drawAt.Y+resizeTo.H > p.Y+S.H { + // Hitting the bottom edge, shrink the height. + delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H) + resizeTo.H -= delta + } else if drawAt.Y < p.Y { + // Hitting the top edge. Cap the Y coord and shrink the height. + delta := p.Y - drawAt.Y + drawAt.Y = p.Y + // scrollTo.Y -= delta // TODO + resizeTo.H -= delta + } + + if resizeTo != actorSize { + can.Resize(resizeTo) + can.ScrollTo(scrollTo) + } + can.Present(e, drawAt) + + // Clean up the canvas size and offset. + can.Resize(actorSize) // restore original size in case cropped + can.ScrollTo(render.Origin) + } }