Noah Petherbridge
7d9ba79cd2
* Adds Window Manager support to the Supervisor, so that Window widgets can be dragged by their title bar, clicked to focus, etc. * Create a ui.Window as normal, but instead of Packing or Placing it into a parent container as before, you call .Supervise() and give it your Supervisor. The window registers itself to be managed and drawn by the Supervisor itself. * Supervisor manages the focused window order using a doubly linked list. When a window takes focus it moves to the top of the list. Widgets in the active window take event priority. * Extended DragDrop API to support holding a widget pointer in the drag operation. * Changed widget event Handle functions to return an error: so that they could return ErrStopPropagation to prevent events going to more widgets once handled (for important events). Some bugs remain around overlapping windows and event propagation.
212 lines
5.2 KiB
Go
212 lines
5.2 KiB
Go
// +build !js
|
|
|
|
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/render/sdl"
|
|
)
|
|
|
|
// Target frames per second for the MainWindow to render at.
|
|
var (
|
|
FPS = 60
|
|
)
|
|
|
|
// Default width and height for MainWindow.
|
|
var (
|
|
DefaultWidth = 640
|
|
DefaultHeight = 480
|
|
)
|
|
|
|
// MainWindow is the parent window of a UI application.
|
|
type MainWindow struct {
|
|
Engine render.Engine
|
|
supervisor *Supervisor
|
|
frame *Frame
|
|
loopCallbacks []func(*event.State)
|
|
w int
|
|
h int
|
|
}
|
|
|
|
// NewMainWindow initializes the MainWindow. You should probably only have one
|
|
// of these per application. Dimensions are the width and height of the window.
|
|
//
|
|
// Example: NewMainWindow("Title Bar") // default 640x480 window
|
|
// NewMainWindow("Title", 800, 600) // both required
|
|
func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) {
|
|
var (
|
|
width = DefaultWidth
|
|
height = DefaultHeight
|
|
)
|
|
|
|
if len(dimensions) > 0 {
|
|
if len(dimensions) != 2 {
|
|
return nil, fmt.Errorf("provide width and height dimensions, like NewMainWindow(title, 800, 600)")
|
|
}
|
|
width, height = dimensions[0], dimensions[1]
|
|
}
|
|
|
|
mw := &MainWindow{
|
|
w: width,
|
|
h: height,
|
|
supervisor: NewSupervisor(),
|
|
loopCallbacks: []func(*event.State){},
|
|
}
|
|
|
|
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))
|
|
|
|
// Compute initial window size.
|
|
mw.resized()
|
|
|
|
return mw, nil
|
|
}
|
|
|
|
// SetTitle changes the title of the window.
|
|
func (mw *MainWindow) SetTitle(title string) {
|
|
mw.Engine.SetTitle(title)
|
|
}
|
|
|
|
// Add a child widget to the window's supervisor. This alone does not make the
|
|
// child widget render each frame; use Pack, Place or Attach for that.
|
|
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)
|
|
}
|
|
|
|
// Place a child widget into the window's default frame.
|
|
func (mw *MainWindow) Place(w Widget, config Place) {
|
|
mw.Add(w)
|
|
mw.frame.Place(w, config)
|
|
}
|
|
|
|
// Attach a child widget to the window without its position managed. The
|
|
// widget's Present() method will be called each time the window Presents, but
|
|
// the positioning of the child widget must be handled manually by the caller.
|
|
//
|
|
// Pack and Place are usually the methods you want to use to put a child widget
|
|
// into the window. One example use case for Attach is when you want to create
|
|
// child Window widgets which can be dragged by their title bars; their dynamic
|
|
// drag-drop positioning is best managed manually, and Pack or Place would
|
|
// interfere with their positioning otherwise.
|
|
//
|
|
// This also calls .Add() to add the widget to the MainWindow's Supervisor.
|
|
//
|
|
// Implementation details:
|
|
// - Adds the widget to the MainWindow's Supervisor.
|
|
// - Calls Frame.Add(w) so it will Present each time the main frame Presents.
|
|
// - Calls w.Compute() on your widget so it can calculate its initial size.
|
|
func (mw *MainWindow) Attach(w Widget) {
|
|
mw.Add(w)
|
|
mw.frame.Add(w)
|
|
w.Compute(mw.Engine)
|
|
}
|
|
|
|
// Frame returns the window's main frame, if needed.
|
|
func (mw *MainWindow) Frame() *Frame {
|
|
return mw.frame
|
|
}
|
|
|
|
// Supervisor returns the window's Supervisor instance.
|
|
func (mw *MainWindow) Supervisor() *Supervisor {
|
|
return mw.supervisor
|
|
}
|
|
|
|
// resized handles the window being resized.
|
|
func (mw *MainWindow) resized() {
|
|
mw.frame.Resize(render.Rect{
|
|
W: mw.w,
|
|
H: mw.h,
|
|
})
|
|
}
|
|
|
|
// SetBackground changes the window's frame's background color.
|
|
func (mw *MainWindow) SetBackground(color render.Color) {
|
|
mw.frame.SetBackground(color)
|
|
}
|
|
|
|
// OnLoop registers a function to be called on every loop of the main window.
|
|
// This enables your application to register global event handlers or whatnot.
|
|
// The function is called between the event polling and the updating of any UI
|
|
// elements.
|
|
func (mw *MainWindow) OnLoop(callback func(*event.State)) {
|
|
mw.loopCallbacks = append(mw.loopCallbacks, callback)
|
|
}
|
|
|
|
// 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 {
|
|
fmt.Printf("------ MAIN LOOP\n")
|
|
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.WindowResized {
|
|
w, h := mw.Engine.WindowSize()
|
|
if w != mw.w || h != mw.h {
|
|
mw.w = w
|
|
mw.h = h
|
|
mw.resized()
|
|
}
|
|
}
|
|
|
|
// Ping any loop callbacks.
|
|
for _, cb := range mw.loopCallbacks {
|
|
cb(ev)
|
|
}
|
|
|
|
mw.frame.Compute(mw.Engine)
|
|
|
|
// Render the child widgets.
|
|
mw.supervisor.Loop(ev)
|
|
mw.frame.Present(mw.Engine, mw.frame.Point())
|
|
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
|
|
}
|