diff --git a/README.md b/README.md index f389524..4551455 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ edit [filename.json] play [filename.json] Open a map in Play Mode. +echo + Flash a message to the console. + +clear + Clear the console output history. + exit quit Close the developer console. @@ -111,18 +117,39 @@ As a rough idea of the milestones needed for this game to work: * [ ] Add support for the shell to pop itself open and ask the user for input prompts. -## Platformer +## Alpha Platformer -* [ ] Inflate the pixel history from the map file into a full lookup grid +* [x] Inflate the pixel history from the map file into a full lookup grid of `(X,Y)` coordinates. This will be useful for collision detection. -* [ ] Create a dummy player character sprite, probably just a +* [x] Create a dummy player character sprite, probably just a `render.Circle()`. In **Play Mode** run collision checks and gravity on the player sprite. -* [ ] Get basic movement and collision working. With a cleanup this can + * [x] Create the concept of the Doodad and make the player character + implement one. +* [x] Get basic movement and collision working. With a cleanup this can make a workable **ALPHA RELEASE** -* [ ] Wrap a Qt GUI around the SDL window to make the Edit Mode easier to - work with, with toolbars to select brushes and doodads and junk. -* [ ] Work on support for solid vs. transparent, fire, etc. geometry. + * [x] Ability to move laterally along the ground. + * [x] Ability to walk up reasonable size slopes but be stopped when + running against a steeper wall. + * [x] Basic gravity + +## UI Overhaul + +* [x] Create a user interface toolkit which will be TREMENDOUSLY helpful + for the rest of this program. + * [x] Labels + * [ ] Buttons (text only is OK) + * [x] Buttons wrap their Label and dynamically compute their size based + on how wide the label will render, plus padding and border. + * [x] Border colors and widths and paddings are all configurable. + * [ ] Buttons should interact with the cursor and be hoverable and + clickable. + * [ ] UI Manager that will keep track of buttons to know when the mouse + is interacting with them. + * [ ] Frames + * [ ] Windows (fixed, non-draggable is OK) +* [ ] Expand the Palette support in levels for solid vs. transparent, fire, + etc. with UI toolbar to choose palettes. * [ ] ??? # Building diff --git a/doodle.go b/doodle.go index 79c8dda..404f7e1 100644 --- a/doodle.go +++ b/doodle.go @@ -114,7 +114,7 @@ func (d *Doodle) Run() error { } // Draw the debug overlay over all scenes. - d.DrawDebugOverlay() + // d.DrawDebugOverlay() // Render the pixels to the screen. err = d.Engine.Present() diff --git a/editor_scene.go b/editor_scene.go index 6491415..c1d1fd7 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/ui" ) // EditorScene manages the "Edit Level" game mode. @@ -132,6 +133,59 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { func (s *EditorScene) Draw(d *Doodle) error { s.canvas.Draw(d.Engine) + label := ui.NewLabel(render.Text{ + Text: "Hello UI toolkit!", + Size: 26, + Color: render.Pink, + Stroke: render.SkyBlue, + Shadow: render.Black, + }) + label.SetPoint(render.NewPoint(128, 128)) + label.Compute(d.Engine) + log.Info("Label rect: %+v", label.Size()) + log.Info("Label at: %s", label.Point()) + label.Present(d.Engine) + + button := ui.NewButton(*ui.NewLabel(render.Text{ + Text: "Hello", + Size: 14, + Color: render.Black, + })) + button.SetPoint(render.NewPoint(200, 200)) + button.Present(d.Engine) + + // Point and size of that button + point := button.Point() + size := button.Size() + + button2 := ui.NewButton(*ui.NewLabel(render.Text{ + Text: "World!", + Size: 14, + Color: render.Blue, + })) + button2.SetPoint(render.Point{ + X: point.X + size.W, + Y: point.Y, + }) + button2.Present(d.Engine) + + button.SetText("Buttons that don't click yet") + button.SetPoint(render.NewPoint(250, 300)) + button.Label.Text.Size = 24 + button.Border = 8 + button.Outline = 4 + button.Present(d.Engine) + + button2.SetText("Multiple colors, too") + button2.Label.Text.Color = render.White + button2.Background = render.RGBA(0, 153, 255, 255) + button2.HighlightColor = render.RGBA(100, 200, 255, 255) + button2.ShadowColor = render.RGBA(0, 100, 153, 255) + button2.SetPoint(render.NewPoint(10, 300)) + button2.Present(d.Engine) + + _ = label + return nil } diff --git a/render/interface.go b/render/interface.go index d048b0a..ff31400 100644 --- a/render/interface.go +++ b/render/interface.go @@ -26,6 +26,7 @@ type Engine interface { DrawRect(Color, Rect) DrawBox(Color, Rect) DrawText(Text, Point) error + ComputeTextRect(Text) (Rect, error) // Delay for a moment using the render engine's delay method, // implemented by sdl.Delay(uint32) @@ -68,6 +69,14 @@ type Point struct { Y int32 } +// NewPoint makes a new Point at an X,Y coordinate. +func NewPoint(x, y int32) Point { + return Point{ + X: x, + Y: y, + } +} + func (p Point) String() string { return fmt.Sprintf("Point<%d,%d>", p.X, p.Y) } diff --git a/render/sdl/text.go b/render/sdl/text.go index b841037..20b72c2 100644 --- a/render/sdl/text.go +++ b/render/sdl/text.go @@ -40,6 +40,31 @@ func (r *Renderer) Keysym(ev *events.State) string { return "" } +// ComputeTextRect computes and returns a Rect for how large the text would +// appear if rendered. +func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) { + var ( + rect render.Rect + font *ttf.Font + surface *sdl.Surface + color = ColorToSDL(text.Color) + err error + ) + + if font, err = LoadFont(text.Size); err != nil { + return rect, err + } + + if surface, err = font.RenderUTF8Blended(text.Text, color); err != nil { + return rect, err + } + defer surface.Free() + + rect.W = surface.W + rect.H = surface.H + return rect, err +} + // DrawText draws text on the canvas. func (r *Renderer) DrawText(text render.Text, point render.Point) error { var ( diff --git a/ui/button.go b/ui/button.go new file mode 100644 index 0000000..801e6d8 --- /dev/null +++ b/ui/button.go @@ -0,0 +1,98 @@ +package ui + +import ( + "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/ui/theme" +) + +// Button is a clickable button. +type Button struct { + BaseWidget + Label Label + Padding int32 + Border int32 + Outline int32 + + // Color options. + Background render.Color + HighlightColor render.Color + ShadowColor render.Color + OutlineColor render.Color +} + +// NewButton creates a new Button. +func NewButton(label Label) *Button { + return &Button{ + Label: label, + Padding: 4, // TODO magic number + Border: 2, + Outline: 1, + + // Default theme colors. + Background: theme.ButtonBackgroundColor, + HighlightColor: theme.ButtonHighlightColor, + ShadowColor: theme.ButtonShadowColor, + OutlineColor: theme.ButtonOutlineColor, + } +} + +// SetText quickly changes the text of the label. +func (w *Button) SetText(text string) { + w.Label.Text.Text = text +} + +// Compute the size of the button. +func (w *Button) Compute(e render.Engine) { + // Compute the size of the inner widget first. + w.Label.Compute(e) + size := w.Label.Size() + w.Resize(render.Rect{ + W: size.W + (w.Padding * 2) + (w.Border * 2) + (w.Outline * 2), + H: size.H + (w.Padding * 2) + (w.Border * 2) + (w.Outline * 2), + }) +} + +// Present the button. +func (w *Button) Present(e render.Engine) { + w.Compute(e) + P := w.Point() + S := w.Size() + + box := render.Rect{ + X: P.X, + Y: P.Y, + W: S.W, + H: S.H, + } + + // Draw the outline layer as the full size of the widget. + e.DrawBox(w.OutlineColor, render.Rect{ + X: P.X - w.Outline, + Y: P.Y - w.Outline, + W: S.W + (w.Outline * 2), + H: S.H + (w.Outline * 2), + }) + + // Highlight on the top left edge. + e.DrawBox(w.HighlightColor, box) + box.W = S.W + + // Shadow on the bottom right edge. + box.X += w.Border + box.Y += w.Border + box.W -= w.Border + box.H -= w.Border + e.DrawBox(w.ShadowColor, box) + + // Background color of the button. + box.W -= w.Border + box.H -= w.Border + e.DrawBox(w.Background, box) + + // Draw the text label inside. + w.Label.SetPoint(render.Point{ + X: P.X + w.Padding + w.Border + w.Outline, + Y: P.Y + w.Padding + w.Border + w.Outline, + }) + w.Label.Present(e) +} diff --git a/ui/label.go b/ui/label.go new file mode 100644 index 0000000..76bb6bf --- /dev/null +++ b/ui/label.go @@ -0,0 +1,31 @@ +package ui + +import "git.kirsle.net/apps/doodle/render" + +// Label is a simple text label widget. +type Label struct { + BaseWidget + width int32 + height int32 + Text render.Text +} + +// NewLabel creates a new label. +func NewLabel(t render.Text) *Label { + return &Label{ + Text: t, + } +} + +// Compute the size of the label widget. +func (w *Label) Compute(e render.Engine) { + rect, err := e.ComputeTextRect(w.Text) + w.Resize(rect) + _ = rect + _ = err +} + +// Present the label widget. +func (w *Label) Present(e render.Engine) { + e.DrawText(w.Text, w.Point()) +} diff --git a/ui/theme/theme.go b/ui/theme/theme.go new file mode 100644 index 0000000..c81f415 --- /dev/null +++ b/ui/theme/theme.go @@ -0,0 +1,11 @@ +package theme + +import "git.kirsle.net/apps/doodle/render" + +// Color schemes. +var ( + ButtonBackgroundColor = render.RGBA(250, 250, 250, 255) + ButtonHighlightColor = render.RGBA(128, 128, 128, 255) + ButtonShadowColor = render.RGBA(20, 20, 20, 255) + ButtonOutlineColor = render.Black +) diff --git a/ui/widget.go b/ui/widget.go new file mode 100644 index 0000000..2e6b2a9 --- /dev/null +++ b/ui/widget.go @@ -0,0 +1,56 @@ +package ui + +import "git.kirsle.net/apps/doodle/render" + +// Widget is a user interface element. +type Widget interface { + Width() int32 // Get width + Height() int32 // Get height + SetWidth(int32) // Set + SetHeight(int32) // Set + Point() render.Point + SetPoint(render.Point) + Size() render.Rect // Return the Width and Height of the widget. + Resize(render.Rect) + + // Run any render computations; by the end the widget must know its + // Width and Height. For example the Label widget will render itself onto + // an SDL Surface and then it will know its bounding box, but not before. + Compute(render.Engine) + + // Render the final widget onto the drawing engine. + Present(render.Engine) +} + +// BaseWidget holds common functionality for all widgets, such as managing +// their widths and heights. +type BaseWidget struct { + width int32 + height int32 + point render.Point +} + +// Point returns the X,Y position of the widget on the window. +func (w *BaseWidget) Point() render.Point { + return w.point +} + +// SetPoint updates the X,Y position of the widget relative to the window. +func (w *BaseWidget) SetPoint(v render.Point) { + w.point = v +} + +// Size returns the box with W and H attributes containing the size of the +// widget. The X,Y attributes of the box are ignored and zero. +func (w *BaseWidget) Size() render.Rect { + return render.Rect{ + W: w.width, + H: w.height, + } +} + +// Resize sets the size of the widget to the .W and .H attributes of a rect. +func (w *BaseWidget) Resize(v render.Rect) { + w.width = v.W + w.height = v.H +}