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.
This commit is contained in:
Noah 2019-06-08 17:03:59 -07:00
parent aaa795245a
commit aceb7e7a7e
5 changed files with 291 additions and 0 deletions

7
eg/layout/layout.go Normal file
View File

@ -0,0 +1,7 @@
package layout
import "fmt"
func main() {
fmt.Println("Hello world")
}

12
eg/layout/main.go Normal file
View File

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

47
eg/main.go Normal file
View File

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

126
main_window.go Normal file
View File

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

99
menu.go Normal file
View File

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