Add initial User Interface Toolkit

With Labels and Buttons so far.

* Labels are pretty much complete, they wrap a render.Text and have a
  Compute() method that returns their Width and Height when rendered
  onto an SDL Surface.
* Buttons wrap a Label widget and Compute() its size and takes that into
  consideration when rendering itself. Buttons render themselves from
  scratch in a "Windows 95" themed way, with configurable colors, border
  widths and outline.
This commit is contained in:
Noah 2018-07-25 09:03:49 -07:00
parent 0efb2ab24f
commit 94c1df050b
9 changed files with 319 additions and 8 deletions

View File

@ -78,6 +78,12 @@ edit [filename.json]
play [filename.json]
Open a map in Play Mode.
echo <text>
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

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 (

98
ui/button.go Normal file
View File

@ -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)
}

31
ui/label.go Normal file
View File

@ -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())
}

11
ui/theme/theme.go Normal file
View File

@ -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
)

56
ui/widget.go Normal file
View File

@ -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
}