Add Initial "Doodad Palette" UX

* Add a tab bar to the top of the Palette window that has two
  radiobuttons for "Palette" and "Doodads"
* UI: add the concept of a Hidden() widget and the corresponding Hide()
  and Show() methods. Hidden widgets are skipped over when evaluating
  Frame packing, rendering, and event supervision.
* The Palette Window in editor mode now displays one of two tabs:
  * Palette: the old color swatch palette now lives here.
  * Doodads: the new Doodad palette.
* The Doodad Palette shows a grid of buttons (2 per row) showing the
  available Doodad drawings in the user's config folder.
* The Doodad buttons act as radiobuttons for now and have no other
  effect. TODO will be making them react to drag-drop events.
* UI: added a `Children()` method as the inverse of `Parent()` for
  container widgets (like Frame, Window and Button) to expose their
  children. The BaseWidget just returns an empty []Widget.
* Console: added a `repl` command that keeps the dev console open and
  prefixes every command with `$` filled out -- for rapid JavaScript
  console evaluation.
bitmap-cache
Noah 2018-10-08 13:06:42 -07:00
parent f18dcf9c2c
commit b67c4b67b2
13 changed files with 356 additions and 82 deletions

View File

@ -52,6 +52,9 @@ func (c Command) Run(d *Doodle) error {
out, err := d.shell.js.Run(c.ArgsLiteral)
d.Flash("%+v", out)
return err
case "repl":
d.shell.Repl = true
d.shell.Text = "$ "
case "boolProp":
return c.BoolProp(d)
default:

View File

@ -2,6 +2,7 @@ package doodle
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@ -69,6 +70,25 @@ 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, "/") {

View File

@ -5,6 +5,7 @@ import (
"strconv"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
@ -23,14 +24,22 @@ type EditorUI struct {
StatusPaletteText string
StatusFilenameText string
selectedSwatch string // name of selected swatch in palette
selectedDoodad string
// Widgets
Supervisor *ui.Supervisor
Canvas *uix.Canvas
Workspace *ui.Frame
MenuBar *ui.Frame
Palette *ui.Window
StatusBar *ui.Frame
// Palette window.
Palette *ui.Window
PaletteTab *ui.Frame
DoodadTab *ui.Frame
// Palette variables.
paletteTab string // selected tab, Palette or Doodads
}
// NewEditorUI initializes the Editor UI.
@ -268,11 +277,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: 150,
Width: paletteWidth,
Height: u.d.height - u.StatusBar.Size().H,
Background: balance.WindowBackground,
BorderColor: balance.WindowBorder,
@ -282,36 +293,142 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
u.MenuBar.BoxSize().H,
))
// Handler function for the radio buttons being clicked.
onClick := func(p render.Point) {
name := u.selectedSwatch
swatch, ok := u.Canvas.Palette.Get(name)
if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return
// Frame that holds the tab buttons in Level Edit mode.
tabFrame := ui.NewFrame("Palette Tabs")
if u.Scene.DrawingType != enum.LevelDrawing {
// Don't show the tab bar except in Level Edit mode.
tabFrame.Hide()
}
for _, name := range []string{"Palette", "Doodads"} {
if u.paletteTab == "" {
u.paletteTab = name
}
tab := ui.NewRadioButton("Palette Tab", &u.paletteTab, name, ui.NewLabel(ui.Label{
Text: name,
}))
tab.Handle(ui.Click, func(p render.Point) {
if u.paletteTab == "Palette" {
u.PaletteTab.Show()
u.DoodadTab.Hide()
} else {
u.PaletteTab.Hide()
u.DoodadTab.Show()
}
window.Compute(d.Engine)
})
u.Supervisor.Add(tab)
tabFrame.Pack(tab, ui.Pack{
Anchor: ui.W,
Fill: true,
Expand: true,
})
}
window.Pack(tabFrame, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
// Doodad frame.
{
u.DoodadTab = ui.NewFrame("Doodad Tab")
u.DoodadTab.Hide()
window.Pack(u.DoodadTab, ui.Pack{
Anchor: ui.N,
Fill: true,
})
doodadsAvailable, err := ListDoodads()
if err != nil {
d.Flash("ListDoodads: %s", err)
}
var buttonSize = (paletteWidth - window.BoxThickness(2)) / 2
// Draw the doodad buttons in a grid 2 wide.
var row *ui.Frame
for i, filename := range doodadsAvailable {
si := fmt.Sprintf("%d", i)
if row == nil || i%2 == 0 {
row = ui.NewFrame("Doodad Row " + si)
row.SetBackground(balance.WindowBackground)
u.DoodadTab.Pack(row, ui.Pack{
Anchor: ui.N,
Fill: true,
// Expand: true,
})
}
doodad, err := doodads.LoadJSON(DoodadPath(filename))
if err != nil {
log.Error(err.Error())
doodad = doodads.New(balance.DoodadSize)
}
can := uix.NewCanvas(int(buttonSize), true)
can.LoadDoodad(doodad)
btn := ui.NewRadioButton(filename, &u.selectedDoodad, si, can)
btn.Resize(render.NewRect(
buttonSize-2, // TODO: without the -2 the button border
buttonSize-2, // rests on top of the window border.
))
u.Supervisor.Add(btn)
row.Pack(btn, ui.Pack{
Anchor: ui.W,
})
// Resize the canvas to fill the button interior.
btnSize := btn.Size()
can.Resize(render.NewRect(
btnSize.W-btn.BoxThickness(2),
btnSize.H-btn.BoxThickness(2),
))
btn.Compute(d.Engine)
}
log.Info("Set swatch: %s", swatch)
u.Canvas.SetSwatch(swatch)
}
// Draw the radio buttons for the palette.
if u.Canvas != nil && u.Canvas.Palette != nil {
for _, swatch := range u.Canvas.Palette.Swatches {
label := ui.NewLabel(ui.Label{
Text: swatch.Name,
Font: balance.StatusFont,
})
label.Font.Color = swatch.Color.Darken(40)
// Color Palette Frame.
{
u.PaletteTab = ui.NewFrame("Palette Tab")
u.PaletteTab.SetBackground(balance.WindowBackground)
window.Pack(u.PaletteTab, ui.Pack{
Anchor: ui.N,
Fill: true,
})
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn)
// Handler function for the radio buttons being clicked.
onClick := func(p render.Point) {
name := u.selectedSwatch
swatch, ok := u.Canvas.Palette.Get(name)
if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return
}
log.Info("Set swatch: %s", swatch)
u.Canvas.SetSwatch(swatch)
}
window.Pack(btn, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
// Draw the radio buttons for the palette.
if u.Canvas != nil && u.Canvas.Palette != nil {
for _, swatch := range u.Canvas.Palette.Swatches {
label := ui.NewLabel(ui.Label{
Text: swatch.Name,
Font: balance.StatusFont,
})
label.Font.Color = swatch.Color.Darken(40)
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn)
u.PaletteTab.Pack(btn, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
}
}
}

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"github.com/robertkrimen/otto"
)
@ -30,6 +31,7 @@ type Shell struct {
Open bool
Prompt string
Repl bool
callback func(string) // for prompt answers only
Text string
History []string
@ -75,6 +77,12 @@ func NewShell(d *Doodle) Shell {
"RGBA": render.RGBA,
"Point": render.NewPoint,
"Rect": render.NewRect,
"Tree": func(w ui.Widget) string {
for _, row := range ui.WidgetTree(w) {
d.Flash(row)
}
return ""
},
}
for name, v := range bindings {
err := s.js.Set(name, v)
@ -90,6 +98,7 @@ func NewShell(d *Doodle) Shell {
func (s *Shell) Close() {
log.Debug("Shell: closing shell")
s.Open = false
s.Repl = false
s.Prompt = ">"
s.callback = nil
s.Text = ""
@ -100,6 +109,7 @@ func (s *Shell) Close() {
// Execute a command in the shell.
func (s *Shell) Execute(input string) {
command := s.Parse(input)
if command.Raw != "" {
s.Output = append(s.Output, s.Prompt+command.Raw)
s.History = append(s.History, command.Raw)
@ -123,7 +133,11 @@ func (s *Shell) Execute(input string) {
}
// Reset the text buffer in the shell.
s.Text = ""
if s.Repl {
s.Text = "$ "
} else {
s.Text = ""
}
}
// Write a line of output text to the console.
@ -197,7 +211,12 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
return nil
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
s.Execute(s.Text)
s.Close()
// Auto-close the console unless in REPL mode.
if !s.Repl {
s.Close()
}
return nil
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
// Paging through history.

View File

@ -56,6 +56,11 @@ func NewButton(name string, child Widget) *Button {
return w
}
// Children returns the button's child widget.
func (w *Button) Children() []Widget {
return []Widget{w.child}
}
// Compute the size of the button.
func (w *Button) Compute(e render.Engine) {
// Compute the size of the inner widget first.
@ -81,6 +86,10 @@ func (w *Button) SetText(text string) error {
// Present the button.
func (w *Button) Present(e render.Engine, P render.Point) {
if w.Hidden() {
return
}
w.Compute(e)
w.MoveTo(P)
var (

23
ui/debug.go Normal file
View File

@ -0,0 +1,23 @@
package ui
import "strings"
// WidgetTree returns a string representing the tree of widgets starting
// at a given widget.
func WidgetTree(root Widget) []string {
var crawl func(int, Widget) []string
crawl = func(depth int, node Widget) []string {
var (
prefix = strings.Repeat(" ", depth)
lines = []string{prefix + node.ID()}
)
for _, child := range node.Children() {
lines = append(lines, crawl(depth+1, child)...)
}
return lines
}
return crawl(0, root)
}

View File

@ -39,6 +39,11 @@ func (w *Frame) Setup() {
}
}
// Children returns all of the child widgets.
func (w *Frame) Children() []Widget {
return w.widgets
}
// Compute the size of the Frame.
func (w *Frame) Compute(e render.Engine) {
w.computePacked(e)
@ -46,6 +51,10 @@ func (w *Frame) Compute(e render.Engine) {
// Present the Frame.
func (w *Frame) Present(e render.Engine, P render.Point) {
if w.Hidden() {
return
}
var (
S = w.Size()
)

View File

@ -2,6 +2,58 @@ package ui
import "git.kirsle.net/apps/doodle/render"
// Pack provides configuration fields for Frame.Pack().
type Pack struct {
// Side of the parent to anchor the position to, like N, SE, W. Default
// is Center.
Anchor Anchor
// If the widget is smaller than its allocated space, grow the widget
// to fill its space in the Frame.
Fill bool
FillX bool
FillY bool
Padding int32 // Equal padding on X and Y.
PadX int32
PadY int32
Expand bool // Widget should grow its allocated space to better fill the parent.
}
// Pack a widget along a side of the frame.
func (w *Frame) Pack(child Widget, config ...Pack) {
var C Pack
if len(config) > 0 {
C = config[0]
}
// Initialize the pack list for this anchor?
if _, ok := w.packs[C.Anchor]; !ok {
w.packs[C.Anchor] = []packedWidget{}
}
// Padding: if the user only provided Padding add it to both
// the X and Y value. If the user additionally provided the X
// and Y value, it will add to the base padding as you'd expect.
C.PadX += C.Padding
C.PadY += C.Padding
// Fill: true implies both directions.
if C.Fill {
C.FillX = true
C.FillY = true
}
// Adopt the child widget so it can access the Frame.
child.Adopt(w)
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
// computePacked processes all the Pack layout widgets in the Frame.
func (w *Frame) computePacked(e render.Engine) {
var (
@ -46,6 +98,10 @@ func (w *Frame) computePacked(e render.Engine) {
pack := packedWidget.pack
child.Compute(e)
if child.Hidden() {
continue
}
x += pack.PadX
y += pack.PadY
@ -187,24 +243,6 @@ func (w *Frame) computePacked(e render.Engine) {
// }
}
// Pack provides configuration fields for Frame.Pack().
type Pack struct {
// Side of the parent to anchor the position to, like N, SE, W. Default
// is Center.
Anchor Anchor
// If the widget is smaller than its allocated space, grow the widget
// to fill its space in the Frame.
Fill bool
FillX bool
FillY bool
Padding int32 // Equal padding on X and Y.
PadX int32
PadY int32
Expand bool // Widget should grow its allocated space to better fill the parent.
}
// Anchor is a cardinal direction.
type Anchor uint8
@ -259,40 +297,6 @@ func (a Anchor) IsMiddle() bool {
return a == Center || a == W || a == E
}
// Pack a widget along a side of the frame.
func (w *Frame) Pack(child Widget, config ...Pack) {
var C Pack
if len(config) > 0 {
C = config[0]
}
// Initialize the pack list for this anchor?
if _, ok := w.packs[C.Anchor]; !ok {
w.packs[C.Anchor] = []packedWidget{}
}
// Padding: if the user only provided Padding add it to both
// the X and Y value. If the user additionally provided the X
// and Y value, it will add to the base padding as you'd expect.
C.PadX += C.Padding
C.PadY += C.Padding
// Fill: true implies both directions.
if C.Fill {
C.FillX = true
C.FillY = true
}
// Adopt the child widget so it can access the Frame.
child.Adopt(w)
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
type packLayout struct {
widgets []packedWidget
}

View File

@ -86,6 +86,10 @@ func (w *Label) Compute(e render.Engine) {
// Present the label widget.
func (w *Label) Present(e render.Engine, P render.Point) {
if w.Hidden() {
return
}
border := w.BoxThickness(1)
var (

View File

@ -53,6 +53,12 @@ func (s *Supervisor) Loop(ev *events.State) {
// See if we are hovering over any widgets.
for id, w := range s.widgets {
if w.Hidden() {
// TODO: somehow the Supervisor wasn't triggering hidden widgets
// anyway, but I don't know why. Adding this check for safety.
continue
}
var (
P = w.Point()
S = w.Size()

View File

@ -56,10 +56,16 @@ type Widget interface {
OutlineSize() int32 // Outline size (default 0)
SetOutlineSize(int32) //
// Visibility
Hide()
Show()
Hidden() bool
// Container widgets like Frames can wire up associations between the
// child widgets and the parent.
Parent() (parent Widget, ok bool)
Adopt(parent Widget) // for the container to assign itself the parent
Children() []Widget // for containers to return their children
// Run any render computations; by the end the widget must know its
// Width and Height. For example the Label widget will render itself onto
@ -98,6 +104,7 @@ type BaseWidget struct {
id string
idFunc func() string
fixedSize bool
hidden bool
width int32
height int32
point render.Point
@ -276,6 +283,37 @@ func (w *BaseWidget) Adopt(parent Widget) {
}
}
// Children returns the widget's children, to be implemented by containers.
// The default implementation returns an empty slice.
func (w *BaseWidget) Children() []Widget {
return []Widget{}
}
// Hide the widget from being rendered.
func (w *BaseWidget) Hide() {
w.hidden = true
}
// Show the widget.
func (w *BaseWidget) Show() {
w.hidden = false
}
// Hidden returns whether the widget is hidden. If this widget is not hidden,
// but it has a parent, this will recursively crawl the parents to see if any
// of them are hidden.
func (w *BaseWidget) Hidden() bool {
if w.hidden {
return true
}
if parent, ok := w.Parent(); ok {
return parent.Hidden()
}
return false
}
// DrawBox draws the border and outline.
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
var (

View File

@ -69,6 +69,13 @@ func NewWindow(title string) *Window {
return w
}
// Children returns the window's child widgets.
func (w *Window) Children() []Widget {
return []Widget{
w.body,
}
}
// TitleBar returns the title bar widget.
func (w *Window) TitleBar() *Label {
return w.titleBar

View File

@ -1,6 +1,9 @@
package uix
import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/events"
@ -39,7 +42,19 @@ func NewCanvas(size int, editable bool) *Canvas {
}
w.setup()
w.IDFunc(func() string {
return "Canvas"
var attrs []string
if w.Editable {
attrs = append(attrs, "editable")
} else {
attrs = append(attrs, "read-only")
}
if w.Scrollable {
attrs = append(attrs, "scrollable")
}
return fmt.Sprintf("Canvas<%d; %s>", size, strings.Join(attrs, "; "))
})
return w
}