diff --git a/commands.go b/commands.go index 5ba1952..1bd78bd 100644 --- a/commands.go +++ b/commands.go @@ -37,6 +37,12 @@ func (c Command) Run(d *Doodle) error { return c.Quit() case "help": return c.Help(d) + case "reload": + d.Goto(d.Scene) + return nil + case "guitest": + d.Goto(&GUITestScene{}) + return nil case "eval": case "$": out, err := d.shell.js.Run(c.ArgsLiteral) diff --git a/guitest_scene.go b/guitest_scene.go index 61b0224..8a4e6c2 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -1,6 +1,8 @@ package doodle import ( + "fmt" + "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" @@ -9,8 +11,10 @@ import ( // GUITestScene implements the main menu of Doodle. type GUITestScene struct { Supervisor *ui.Supervisor - frame *ui.Frame - window *ui.Frame + + // Private widgets. + Frame *ui.Frame + Window *ui.Frame } // Name of the scene. @@ -22,64 +26,151 @@ func (s *GUITestScene) Name() string { func (s *GUITestScene) Setup(d *Doodle) error { s.Supervisor = ui.NewSupervisor() - window := ui.NewFrame() - s.window = window + window := ui.NewFrame("window") + s.Window = window window.Configure(ui.Config{ - Width: 400, - Height: 400, + Width: 750, + Height: 450, Background: render.Grey, BorderStyle: ui.BorderRaised, BorderSize: 2, }) + // Title Bar titleBar := ui.NewLabel(render.Text{ - Text: "Alert", + Text: "Widget Toolkit", Size: 12, Color: render.White, - Stroke: render.Black, + Stroke: render.DarkBlue, }) titleBar.Configure(ui.Config{ - Background: render.Blue, - OutlineSize: 1, - OutlineColor: render.Black, + Background: render.Blue, }) window.Pack(titleBar, ui.Pack{ Anchor: ui.N, - FillX: true, + Fill: true, }) - msgFrame := ui.NewFrame() - msgFrame.Configure(ui.Config{ + // Window Body + body := ui.NewFrame("Window Body") + body.Configure(ui.Config{ + Background: render.Yellow, + }) + window.Pack(body, ui.Pack{ + Anchor: ui.N, + Expand: true, + }) + + // Left Frame + leftFrame := ui.NewFrame("Left Frame") + leftFrame.Configure(ui.Config{ Background: render.Grey, - BorderStyle: ui.BorderRaised, - BorderSize: 1, + BorderStyle: ui.BorderSolid, + BorderSize: 4, + Width: 100, }) - window.Pack(msgFrame, ui.Pack{ - Anchor: ui.N, - Fill: true, - Padding: 4, + body.Pack(leftFrame, ui.Pack{ + Anchor: ui.W, + FillY: true, }) - btnFrame := ui.NewFrame() - btnFrame.Configure(ui.Config{ - Background: render.DarkRed, + // Some left frame buttons. + for _, label := range []string{"New", "Edit", "Play", "Help"} { + btn := ui.NewButton("dummy "+label, ui.NewLabel(render.Text{ + Text: label, + Size: 12, + Color: render.Black, + })) + s.Supervisor.Add(btn) + leftFrame.Pack(btn, ui.Pack{ + Anchor: ui.N, + Fill: true, + PadY: 2, + }) + } + + // Main Frame + frame := ui.NewFrame("Main Frame") + frame.Configure(ui.Config{ + Background: render.White, + BorderSize: 0, }) - window.Pack(btnFrame, ui.Pack{ - Anchor: ui.N, - Padding: 4, + body.Pack(frame, ui.Pack{ + Anchor: ui.W, + Expand: true, }) - msg := ui.NewLabel(render.Text{ + // Right Frame + rightFrame := ui.NewFrame("Right Frame") + rightFrame.Configure(ui.Config{ + Background: render.SkyBlue, + BorderStyle: ui.BorderSunken, + BorderSize: 2, + Width: 80, + }) + body.Pack(rightFrame, ui.Pack{ + Anchor: ui.W, + Fill: true, + }) + + // A grid of buttons. + for row := 0; row < 3; row++ { + rowFrame := ui.NewFrame(fmt.Sprintf("Row%d", row)) + for col := 0; col < 3; col++ { + btn := ui.NewButton("X", + ui.NewFrame(fmt.Sprintf("Col%d", col)), + ) + btn.Configure(ui.Config{ + Height: 20, + BorderStyle: ui.BorderRaised, + }) + rowFrame.Pack(btn, ui.Pack{ + Anchor: ui.W, + Expand: true, + }) + s.Supervisor.Add(btn) + } + rightFrame.Pack(rowFrame, ui.Pack{ + Anchor: ui.N, + Fill: true, + }) + } + + frame.Pack(ui.NewLabel(render.Text{ Text: "Hello World!", Size: 14, Color: render.Black, - }) - msgFrame.Pack(msg, ui.Pack{ + }), ui.Pack{ Anchor: ui.NW, Padding: 2, }) + frame.Pack(ui.NewLabel(render.Text{ + Text: "Like Tk!", + Size: 16, + Color: render.Red, + }), ui.Pack{ + Anchor: ui.SE, + Padding: 8, + }) + frame.Pack(ui.NewLabel(render.Text{ + Text: "Frame widget for pack layouts", + Size: 14, + Color: render.Blue, + }), ui.Pack{ + Anchor: ui.SE, + Padding: 8, + }) - button1 := ui.NewButton(*ui.NewLabel(render.Text{ + // Buttom Frame + btnFrame := ui.NewFrame("btnFrame") + btnFrame.Configure(ui.Config{ + Background: render.Grey, + }) + window.Pack(btnFrame, ui.Pack{ + Anchor: ui.N, + }) + + button1 := ui.NewButton("Button1", ui.NewLabel(render.Text{ Text: "New Map", Size: 14, Color: render.Black, @@ -91,7 +182,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { log.Info("Button1 bg: %s", button1.Background()) - button2 := ui.NewButton(*ui.NewLabel(render.Text{ + button2 := ui.NewButton("Button2", ui.NewLabel(render.Text{ Text: "New Map", Size: 14, Color: render.Black, @@ -102,12 +193,10 @@ func (s *GUITestScene) Setup(d *Doodle) error { btnFrame.Pack(button1, ui.Pack{ Anchor: align, Padding: 20, - Fill: true, }) btnFrame.Pack(button2, ui.Pack{ Anchor: align, Padding: 20, - Fill: true, }) s.Supervisor.Add(button1) @@ -141,15 +230,17 @@ func (s *GUITestScene) Draw(d *Doodle) error { }) label.Present(d.Engine) - s.window.Compute(d.Engine) - s.window.MoveTo(render.Point{ - X: (d.width / 2) - (s.window.Size().W / 2), + s.Window.Compute(d.Engine) + s.Window.MoveTo(render.Point{ + X: (d.width / 2) - (s.Window.Size().W / 2), Y: 100, }) - s.window.Present(d.Engine) + s.Window.Present(d.Engine) s.Supervisor.Present(d.Engine) + // os.Exit(1) + return nil } diff --git a/main_scene.go b/main_scene.go index 073be25..5927270 100644 --- a/main_scene.go +++ b/main_scene.go @@ -21,7 +21,7 @@ func (s *MainScene) Name() string { func (s *MainScene) Setup(d *Doodle) error { s.Supervisor = ui.NewSupervisor() - frame := ui.NewFrame() + frame := ui.NewFrame("frame") s.frame = frame s.frame.Configure(ui.Config{ // Width: 400, @@ -32,7 +32,7 @@ func (s *MainScene) Setup(d *Doodle) error { BorderColor: render.Blue, }) - button1 := ui.NewButton(*ui.NewLabel(render.Text{ + button1 := ui.NewButton("Button1", ui.NewLabel(render.Text{ Text: "New Map", Size: 14, Color: render.Black, @@ -46,7 +46,7 @@ func (s *MainScene) Setup(d *Doodle) error { d.NewMap() }) - button2 := ui.NewButton(*ui.NewLabel(render.Text{ + button2 := ui.NewButton("Button2", ui.NewLabel(render.Text{ Text: "New Map", Size: 14, Color: render.Black, diff --git a/render/interface.go b/render/interface.go index 8ae222d..b0aea4a 100644 --- a/render/interface.go +++ b/render/interface.go @@ -125,12 +125,35 @@ type Rect struct { H int32 } +// NewRect creates a rectangle of size `width` and `height`. The X,Y values +// are initialized to zero. +func NewRect(width, height int32) Rect { + return Rect{ + W: width, + H: height, + } +} + func (r Rect) String() string { return fmt.Sprintf("Rect<%d,%d,%d,%d>", r.X, r.Y, r.W, r.H, ) } +// Bigger returns if the given rect is larger than the current one. +func (r Rect) Bigger(other Rect) bool { + // TODO: don't know why this is ! + return !(other.X < r.X || // Lefter + other.Y < r.Y || // Higher + other.W > r.W || // Wider + other.H > r.H) // Taller +} + +// 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 +} + // Text holds information for drawing text. type Text struct { Text string diff --git a/shell.go b/shell.go index 2827506..77f2353 100644 --- a/shell.go +++ b/shell.go @@ -65,6 +65,7 @@ func NewShell(d *Doodle) Shell { "log": log, "RGBA": render.RGBA, "Point": render.NewPoint, + "Rect": render.NewRect, } for name, v := range bindings { err := s.js.Set(name, v) diff --git a/ui/button.go b/ui/button.go index f8ab5c1..d69718c 100644 --- a/ui/button.go +++ b/ui/button.go @@ -1,6 +1,7 @@ package ui import ( + "errors" "fmt" "git.kirsle.net/apps/doodle/render" @@ -10,7 +11,7 @@ import ( // Button is a clickable button. type Button struct { BaseWidget - Label Label + child Widget // Private options. hovering bool @@ -18,10 +19,13 @@ type Button struct { } // NewButton creates a new Button. -func NewButton(label Label) *Button { +func NewButton(name string, child Widget) *Button { w := &Button{ - Label: label, + child: child, } + w.IDFunc(func() string { + return fmt.Sprintf("Button<%s>", name) + }) w.Configure(Config{ Padding: 4, @@ -50,33 +54,40 @@ func NewButton(label Label) *Button { w.SetBorderStyle(BorderRaised) }) - w.IDFunc(func() string { - return fmt.Sprintf("Button<%s>", w.Label.Text.Text) - }) - return w } -// 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.BoxThickness(2), - H: size.H + w.BoxThickness(2), - }) + w.child.Compute(e) + + // Auto-resize only if we haven't been given a fixed size. + if !w.FixedSize() { + size := w.child.Size() + w.Resize(render.Rect{ + W: size.W + w.BoxThickness(2), + H: size.H + w.BoxThickness(2), + }) + } +} + +// SetText conveniently sets the button text, for Label children only. +func (w *Button) SetText(text string) error { + if label, ok := w.child.(*Label); ok { + label.Text.Text = text + } + return errors.New("child is not a Label widget") } // Present the button. func (w *Button) Present(e render.Engine) { w.Compute(e) - P := w.Point() + var ( + P = w.Point() + S = w.Size() + ChildSize = w.child.Size() + ) // Draw the widget's border and everything. w.DrawBox(e) @@ -87,10 +98,20 @@ func (w *Button) Present(e render.Engine) { clickOffset++ } - // Draw the text label inside. - w.Label.MoveTo(render.Point{ + // Where to place the child widget. + moveTo := render.Point{ X: P.X + w.BoxThickness(1) + clickOffset, Y: P.Y + w.BoxThickness(1) + clickOffset, - }) - w.Label.Present(e) + } + + // If we're bigger than we need to be, center the child widget. + if S.Bigger(ChildSize) { + moveTo.X = P.X + (S.W / 2) - (ChildSize.W / 2) + } + _ = S + _ = ChildSize + + // Draw the text label inside. + w.child.MoveTo(moveTo) + w.child.Present(e) } diff --git a/ui/frame.go b/ui/frame.go index 66f19ee..779b7f6 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -2,47 +2,52 @@ package ui import ( "fmt" - "time" "git.kirsle.net/apps/doodle/render" ) // Frame is a widget that contains other widgets. type Frame struct { + Name string BaseWidget packs map[Anchor][]packedWidget widgets []Widget } // NewFrame creates a new Frame. -func NewFrame() *Frame { - return &Frame{ +func NewFrame(name string) *Frame { + w := &Frame{ + Name: name, packs: map[Anchor][]packedWidget{}, widgets: []Widget{}, } -} - -func (w *Frame) String() string { - return fmt.Sprintf("Frame<%d widgets>", - len(w.widgets), - ) + w.IDFunc(func() string { + return fmt.Sprintf("Frame<%s; %d widgets>", + name, + len(w.widgets), + ) + }) + return w } // Compute the size of the Frame. func (w *Frame) Compute(e render.Engine) { var ( frameSize = w.Size() + + // maxWidth and maxHeight are always the computed minimum dimensions + // that the Frame must be to contain all of its children. If the Frame + // was configured with an explicit Size, the Frame will be that Size, + // but we still calculate how much space the widgets _actually_ take + // so we can expand them to fill remaining space in fixed size widgets. maxWidth int32 maxHeight int32 visited = []packedWidget{} + expanded = []packedWidget{} ) - // Initialize the dimensions? - if w.FixedSize() { - maxWidth = frameSize.W - maxHeight = frameSize.H - } - + // Iterate through all anchored directions and compute how much space to + // reserve to contain all of their widgets. for anchor := AnchorMin; anchor <= AnchorMax; anchor++ { if _, ok := w.packs[anchor]; !ok { continue @@ -57,10 +62,10 @@ func (w *Frame) Compute(e render.Engine) { if anchor.IsSouth() { y = frameSize.H - yDirection = -1 - w.BoxThickness(1) + yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2 } else if anchor == E { x = frameSize.W - xDirection = -1 + xDirection = -1 - w.BoxThickness(2) } for _, packedWidget := range w.packs[anchor] { @@ -74,24 +79,11 @@ func (w *Frame) Compute(e render.Engine) { xStep = x * xDirection ) - if !w.FixedSize() { - log.Warn("not fixed") - if xStep+size.W+(pack.PadX*2) > maxWidth { - var old = maxWidth - maxWidth = xStep + size.W + (pack.PadX * 2) - log.Error("%s %s Upgrading maxWidth %d -> %d (size %s) xstep %d", - w, - child, - old, - maxWidth, - size, - xStep, - ) - time.Sleep(5) - } - if yStep+size.H+(pack.PadY*2) > maxHeight { - maxHeight = yStep + size.H + (pack.PadY * 2) - } + if xStep+size.W+(pack.PadX*2) > maxWidth { + maxWidth = xStep + size.W + (pack.PadX * 2) + } + if yStep+size.H+(pack.PadY*2) > maxHeight { + maxHeight = yStep + size.H + (pack.PadY * 2) } if anchor.IsSouth() { @@ -114,9 +106,32 @@ func (w *Frame) Compute(e render.Engine) { } visited = append(visited, packedWidget) + if pack.Expand { + expanded = append(expanded, packedWidget) + } } } + // If we have extra space in the Frame and any expanding widgets, let the + // expanding widgets grow and share the remaining space. + computedSize := render.NewRect(maxWidth, maxHeight) + if len(expanded) > 0 && !frameSize.IsZero() && frameSize.Bigger(computedSize) { + // Divy up the size available. + growBy := render.Rect{ + W: ((frameSize.W - computedSize.W) / int32(len(expanded))) - w.BoxThickness(2), + H: ((frameSize.H - computedSize.H) / int32(len(expanded))) - w.BoxThickness(2), + } + for _, pw := range expanded { + pw.widget.ResizeBy(growBy) + pw.widget.Compute(e) + } + } + + // If we're not using a fixed Frame size, use the dynamically computed one. + if !w.FixedSize() { + frameSize = render.NewRect(maxWidth, maxHeight) + } + // Rescan all the widgets in this anchor to re-center them // in their space. for _, pw := range visited { @@ -125,38 +140,43 @@ func (w *Frame) Compute(e render.Engine) { pack = pw.pack point = child.Point() size = child.Size() + resize = size resized bool moved bool ) if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() { - if pack.FillX && size.W < maxWidth { - size.W = maxWidth - w.BoxThickness(2) + if pack.FillX && resize.W < frameSize.W { + resize.W = frameSize.W - w.BoxThickness(2) resized = true } - if size.W < maxWidth { + if resize.W < frameSize.W-w.BoxThickness(4) { if pack.Anchor.IsCenter() { - point.X = (maxWidth / 2) - (size.W / 2) + point.X = (frameSize.W / 2) - (resize.W / 2) } else if pack.Anchor.IsWest() { point.X = pack.PadX } else if pack.Anchor.IsEast() { - point.X = maxWidth - size.W - pack.PadX + point.X = frameSize.W - resize.W - pack.PadX } moved = true } } else if pack.Anchor.IsWest() || pack.Anchor.IsEast() { - if pack.FillY && size.H < maxHeight { - size.H = maxHeight - w.BoxThickness(2) + if pack.FillY && resize.H < frameSize.H { + resize.H = frameSize.H - w.BoxThickness(2) // BoxThickness(2) for parent + child + // point.Y -= (w.BoxThickness(4) + child.BoxThickness(2)) + moved = true resized = true } - if size.H < maxHeight { + + // Vertically align the widgets. + if resize.H < frameSize.H { if pack.Anchor.IsMiddle() { - point.Y = (maxHeight / 2) - (size.H / 2) + point.Y = (frameSize.H / 2) - (resize.H / 2) } else if pack.Anchor.IsNorth() { - point.Y = pack.PadY + point.Y = pack.PadY - w.BoxThickness(4) } else if pack.Anchor.IsSouth() { - point.Y = maxHeight - size.H - pack.PadY + point.Y = frameSize.H - resize.H - pack.PadY } moved = true } @@ -164,8 +184,9 @@ func (w *Frame) Compute(e render.Engine) { log.Error("unsupported pack.Anchor") } - if resized { - child.Resize(size) + if resized && size != resize { + child.Resize(resize) + child.Compute(e) } if moved { child.MoveTo(point) @@ -173,10 +194,7 @@ func (w *Frame) Compute(e render.Engine) { } if !w.FixedSize() { - w.Resize(render.Rect{ - W: maxWidth + w.BoxThickness(2), - H: maxHeight + w.BoxThickness(2), - }) + w.Resize(frameSize) } } @@ -224,7 +242,7 @@ type Pack struct { Padding int32 // Equal padding on X and Y. PadX int32 PadY int32 - // Expand bool // Widget should grow to fill any remaining space. + Expand bool // Widget should grow its allocated space to better fill the parent. } // Anchor is a cardinal direction. @@ -299,6 +317,12 @@ func (w *Frame) Pack(child Widget, config ...Pack) { C.PadX += C.Padding C.PadY += C.Padding + // Fill: true implies both directions. + if C.Fill { + C.FillX = true + C.FillY = true + } + w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{ widget: child, pack: C, diff --git a/ui/label.go b/ui/label.go index 2c7cc4c..2452c88 100644 --- a/ui/label.go +++ b/ui/label.go @@ -31,10 +31,14 @@ func NewLabel(t render.Text) *Label { // Compute the size of the label widget. func (w *Label) Compute(e render.Engine) { rect, _ := e.ComputeTextRect(w.Text) - w.Resize(render.Rect{ - W: rect.W + w.Padding(), - H: rect.H + w.Padding(), - }) + + if !w.FixedSize() { + w.resizeAuto(render.Rect{ + W: rect.W + w.Padding(), + H: rect.H + w.Padding(), + }) + } + w.MoveTo(render.Point{ X: rect.X + w.BoxThickness(1), Y: rect.Y + w.BoxThickness(1), diff --git a/ui/widget.go b/ui/widget.go index ac44592..e031228 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -10,6 +10,7 @@ type BorderStyle string // Styles for a widget border. const ( + BorderNone BorderStyle = "" BorderSolid BorderStyle = "solid" BorderRaised = "raised" BorderSunken = "sunken" @@ -26,6 +27,7 @@ type Widget interface { Size() render.Rect // Return the Width and Height of the widget. FixedSize() bool // Return whether the size is fixed (true) or automatic (false) Resize(render.Rect) + ResizeBy(render.Rect) Handle(string, func(render.Point)) Event(string, render.Point) // called internally to trigger an event @@ -130,10 +132,14 @@ func (w *BaseWidget) String() string { // Configure the base widget with all the common properties at once. Any // property left as the zero value will not update the widget. func (w *BaseWidget) Configure(c Config) { - if c.Width != 0 && c.Height != 0 { + if c.Width != 0 || c.Height != 0 { w.fixedSize = !c.AutoResize - w.width = c.Width - w.height = c.Height + if c.Width != 0 { + w.width = c.Width + } + if c.Height != 0 { + w.height = c.Height + } } if c.Padding != 0 { @@ -155,7 +161,7 @@ func (w *BaseWidget) Configure(c Config) { if c.BorderSize != 0 { w.borderSize = c.BorderSize } - if c.BorderStyle != BorderSolid { + if c.BorderStyle != BorderNone { w.borderStyle = c.BorderStyle } if c.OutlineSize != 0 { @@ -201,6 +207,13 @@ func (w *BaseWidget) Resize(v render.Rect) { w.height = v.H } +// ResizeBy resizes by a relative amount. +func (w *BaseWidget) ResizeBy(v render.Rect) { + w.fixedSize = true + w.width += v.W + w.height += v.H +} + // resizeAuto sets the size of the widget but doesn't set the fixedSize flag. func (w *BaseWidget) resizeAuto(v render.Rect) { w.width = v.W