From b67c4b67b2c6685d6cfe9b3f88f52e3deabf45a6 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 8 Oct 2018 13:06:42 -0700 Subject: [PATCH] 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. --- commands.go | 3 + config.go | 20 ++++++ editor_ui.go | 171 +++++++++++++++++++++++++++++++++++++++-------- shell.go | 23 ++++++- ui/button.go | 9 +++ ui/debug.go | 23 +++++++ ui/frame.go | 9 +++ ui/frame_pack.go | 108 ++++++++++++++++-------------- ui/label.go | 4 ++ ui/supervisor.go | 6 ++ ui/widget.go | 38 +++++++++++ ui/window.go | 7 ++ uix/canvas.go | 17 ++++- 13 files changed, 356 insertions(+), 82 deletions(-) create mode 100644 ui/debug.go diff --git a/commands.go b/commands.go index 796f84d..d3d314a 100644 --- a/commands.go +++ b/commands.go @@ -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: diff --git a/config.go b/config.go index 466bb42..15ad85c 100644 --- a/config.go +++ b/config.go @@ -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, "/") { diff --git a/editor_ui.go b/editor_ui.go index 00dfaba..3f43411 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -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, + }) + } } } diff --git a/shell.go b/shell.go index 36e7f1c..ca0228a 100644 --- a/shell.go +++ b/shell.go @@ -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. diff --git a/ui/button.go b/ui/button.go index 7e60db7..e7240ca 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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 ( diff --git a/ui/debug.go b/ui/debug.go new file mode 100644 index 0000000..2111668 --- /dev/null +++ b/ui/debug.go @@ -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) +} diff --git a/ui/frame.go b/ui/frame.go index 76735c6..ee15af6 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -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() ) diff --git a/ui/frame_pack.go b/ui/frame_pack.go index d2fb6d2..228f8fc 100644 --- a/ui/frame_pack.go +++ b/ui/frame_pack.go @@ -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 } diff --git a/ui/label.go b/ui/label.go index ef464b8..c7d191b 100644 --- a/ui/label.go +++ b/ui/label.go @@ -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 ( diff --git a/ui/supervisor.go b/ui/supervisor.go index 8351537..cb9914b 100644 --- a/ui/supervisor.go +++ b/ui/supervisor.go @@ -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() diff --git a/ui/widget.go b/ui/widget.go index d5710ab..bcc6db8 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -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 ( diff --git a/ui/window.go b/ui/window.go index 5f17803..4347c63 100644 --- a/ui/window.go +++ b/ui/window.go @@ -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 diff --git a/uix/canvas.go b/uix/canvas.go index f6936b1..394fc91 100644 --- a/uix/canvas.go +++ b/uix/canvas.go @@ -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 }