From aceb7e7a7e68fc04b06294161d71f59046d1393c Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 8 Jun 2019 17:03:59 -0700 Subject: [PATCH] UI: Add MainWindow Widget and start an example app * MainWindow is ideal for apps that just want a UI and don't manage their own SDL windows. * The example app will grow into a series of demos that test the UI toolkit to help fix bugs and grow features. --- eg/layout/layout.go | 7 +++ eg/layout/main.go | 12 +++++ eg/main.go | 47 +++++++++++++++++ main_window.go | 126 ++++++++++++++++++++++++++++++++++++++++++++ menu.go | 99 ++++++++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 eg/layout/layout.go create mode 100644 eg/layout/main.go create mode 100644 eg/main.go create mode 100644 main_window.go create mode 100644 menu.go diff --git a/eg/layout/layout.go b/eg/layout/layout.go new file mode 100644 index 0000000..511c885 --- /dev/null +++ b/eg/layout/layout.go @@ -0,0 +1,7 @@ +package layout + +import "fmt" + +func main() { + fmt.Println("Hello world") +} diff --git a/eg/layout/main.go b/eg/layout/main.go new file mode 100644 index 0000000..5bb48d7 --- /dev/null +++ b/eg/layout/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/ui/eg/layout" +) + +func main() { + fmt.Println("Hello world") + layout.main() +} diff --git a/eg/main.go b/eg/main.go new file mode 100644 index 0000000..6116cfc --- /dev/null +++ b/eg/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" +) + +func main() { + mw, err := ui.NewMainWindow("UI Toolkit Demo") + if err != nil { + panic(err) + } + + leftFrame := ui.NewFrame("Left Frame") + leftFrame.Configure(ui.Config{ + Width: 200, + BorderSize: 1, + BorderStyle: ui.BorderRaised, + Background: render.Grey, + }) + mw.Pack(leftFrame, ui.Pack{ + Anchor: ui.W, + FillY: true, + }) + + mainFrame := ui.NewFrame("Main Frame") + mainFrame.Configure(ui.Config{ + Background: render.RGBA(255, 255, 255, 180), + }) + mw.Pack(mainFrame, ui.Pack{ + Anchor: ui.W, + Expand: true, + PadX: 10, + }) + + label := ui.NewLabel(ui.Label{ + Text: "Hello world", + }) + leftFrame.Pack(label, ui.Pack{ + Anchor: ui.SE, + }) + + err = mw.MainLoop() + if err != nil { + panic("MainLoop:" + err.Error()) + } +} diff --git a/main_window.go b/main_window.go new file mode 100644 index 0000000..e12c154 --- /dev/null +++ b/main_window.go @@ -0,0 +1,126 @@ +package ui + +import ( + "fmt" + "time" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/render/sdl" +) + +// Target frames per second for the MainWindow to render at. +var ( + FPS = 60 +) + +// MainWindow is the parent window of a UI application. +type MainWindow struct { + engine render.Engine + supervisor *Supervisor + frame *Frame + w int + h int +} + +// NewMainWindow initializes the MainWindow. You should probably only have one +// of these per application. +func NewMainWindow(title string) (*MainWindow, error) { + mw := &MainWindow{ + w: 800, + h: 600, + supervisor: NewSupervisor(), + } + + mw.engine = sdl.New( + title, + mw.w, + mw.h, + ) + if err := mw.engine.Setup(); err != nil { + return nil, err + } + + // Add a default frame to the window. + mw.frame = NewFrame("MainWindow Body") + mw.frame.SetBackground(render.RGBA(0, 153, 255, 100)) + mw.Add(mw.frame) + + // Compute initial window size. + mw.resized() + + return mw, nil +} + +// Add a child widget to the window. +func (mw *MainWindow) Add(w Widget) { + mw.supervisor.Add(w) +} + +// Pack a child widget into the window's default frame. +func (mw *MainWindow) Pack(w Widget, pack Pack) { + mw.Add(w) + mw.frame.Pack(w, pack) +} + +// resized handles the window being resized. +func (mw *MainWindow) resized() { + mw.frame.Resize(render.Rect{ + W: int32(mw.w), + H: int32(mw.h), + }) +} + +// Present the window. +func (mw *MainWindow) Present() { + mw.supervisor.Present(mw.engine) +} + +// MainLoop starts the main event loop and blocks until there's an error. +func (mw *MainWindow) MainLoop() error { + for true { + if err := mw.Loop(); err != nil { + return err + } + } + return nil +} + +// Loop does one loop of the UI. +func (mw *MainWindow) Loop() error { + mw.engine.Clear(render.White) + + // Record how long this loop took. + start := time.Now() + + // Poll for events. + ev, err := mw.engine.Poll() + if err != nil { + return fmt.Errorf("event poll error: %s", err) + } + + if ev.Resized.Now { + w, h := mw.engine.WindowSize() + if w != mw.w || h != mw.h { + mw.w = w + mw.h = h + mw.resized() + } + } + + mw.frame.Compute(mw.engine) + + // Render the child widgets. + mw.supervisor.Present(mw.engine) + mw.engine.Present() + + // Delay to maintain target frames per second. + var delay uint32 + var targetFPS = 1000 / FPS + elapsed := time.Now().Sub(start) / time.Millisecond + if targetFPS-int(elapsed) > 0 { + delay = uint32(targetFPS - int(elapsed)) + } + mw.engine.Delay(delay) + + return nil +} diff --git a/menu.go b/menu.go new file mode 100644 index 0000000..adebbaa --- /dev/null +++ b/menu.go @@ -0,0 +1,99 @@ +package ui + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" +) + +// Menu is a rectangle that holds menu items. +type Menu struct { + BaseWidget + Name string + + body *Frame +} + +// NewMenu creates a new Menu. It is hidden by default. Usually you'll +// use it with a MenuButton or in a right-click handler. +func NewMenu(name string) *Menu { + w := &Menu{ + Name: name, + body: NewFrame(name + ":Body"), + } + w.body.Configure(Config{ + Width: 150, + BorderSize: 12, + BorderStyle: BorderRaised, + Background: render.Grey, + }) + w.IDFunc(func() string { + return fmt.Sprintf("Menu<%s>", w.Name) + }) + return w +} + +// Compute the menu +func (w *Menu) Compute(e render.Engine) { + w.body.Compute(e) +} + +// Present the menu +func (w *Menu) Present(e render.Engine, p render.Point) { + w.body.Present(e, p) +} + +// AddItem quickly adds an item to a menu. +func (w *Menu) AddItem(label string, command func()) *MenuItem { + menu := NewMenuItem(label, command) + w.Pack(menu) + return menu +} + +// Pack a menu item onto the menu. +func (w *Menu) Pack(item *MenuItem) { + w.body.Pack(item, Pack{ + Anchor: NE, + // Expand: true, + // Padding: 8, + FillX: true, + }) +} + +// MenuItem is an item in a Menu. +type MenuItem struct { + Button + Label string + Accelerator string + Command func() + button *Button +} + +// NewMenuItem creates a new menu item. +func NewMenuItem(label string, command func()) *MenuItem { + w := &MenuItem{ + Label: label, + Command: command, + } + w.IDFunc(func() string { + return fmt.Sprintf("MenuItem<%s>", w.Label) + }) + + font := DefaultFont + font.Color = render.White + font.PadX = 12 + w.Button.child = NewLabel(Label{ + Text: label, + Font: font, + }) + w.Button.Configure(Config{ + Background: render.Blue, + }) + + w.Button.Handle(Click, func(p render.Point) { + w.Command() + }) + + // Assign the button + return w +}