// +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 { 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 }