Browse Source

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.
menus
Noah Petherbridge 2 years ago
parent
commit
aceb7e7a7e
5 changed files with 291 additions and 0 deletions
  1. +7
    -0
      eg/layout/layout.go
  2. +12
    -0
      eg/layout/main.go
  3. +47
    -0
      eg/main.go
  4. +126
    -0
      main_window.go
  5. +99
    -0
      menu.go

+ 7
- 0
eg/layout/layout.go View File

@@ -0,0 +1,7 @@
package layout

import "fmt"

func main() {
fmt.Println("Hello world")
}

+ 12
- 0
eg/layout/main.go 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
- 0
eg/main.go 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
- 0
main_window.go 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
- 0
menu.go 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
}

Loading…
Cancel
Save