Files
gio/ui/app/window.go
T
Elias Naur b494b3c8c0 ui/app: remove IsAlive
IsAlive races with the StageDead event: if the client checks IsAlive
after the stage is updated to StageDead but before having received
the StageDead it will exit the event loop, blocking the StageDead
to ever arrive.

Remove IsAlive and let the client rely only on the StageDead to exit
its event loop.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-07-08 15:46:48 +02:00

292 lines
5.3 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package app
import (
"errors"
"fmt"
"image"
"sync"
"time"
"gioui.org/ui"
"gioui.org/ui/app/internal/gpu"
"gioui.org/ui/input"
"gioui.org/ui/internal/ops"
"gioui.org/ui/key"
)
type WindowOptions struct {
Width ui.Value
Height ui.Value
Title string
}
type Window struct {
Profiling bool
driver *window
lastFrame time.Time
drawStart time.Time
gpu *gpu.GPU
timings string
inputState key.TextInputState
err error
events chan Event
eventLock sync.Mutex
mu sync.Mutex
stage Stage
size image.Point
syncGPU bool
animating bool
hasNextFrame bool
nextFrame time.Time
delayedDraw *time.Timer
reader ui.OpsReader
}
// driver is the interface for the platform implementation
// of a Window.
var _ interface {
// setAnimating sets the animation flag. When the window is animating,
// Draw events are delivered as fast as the display can handle them.
setAnimating(anim bool)
// setTextInput updates the virtual keyboard state.
setTextInput(s key.TextInputState)
} = (*window)(nil)
var ackEvent Event
func newWindow(nw *window) *Window {
w := &Window{
driver: nw,
events: make(chan Event),
stage: StagePaused,
}
return w
}
func (w *Window) Events() <-chan Event {
return w.events
}
func (w *Window) Timings() string {
return w.timings
}
func (w *Window) SetTextInput(s key.TextInputState) {
w.mu.Lock()
defer w.mu.Unlock()
if !w.isAlive() {
return
}
if s != w.inputState && (s == key.TextInputClose || s == key.TextInputOpen) {
w.driver.setTextInput(s)
}
if s == key.TextInputFocus {
w.setNextFrame(time.Time{})
w.updateAnimation()
}
w.inputState = s
}
func (w *Window) Err() error {
return w.err
}
func (w *Window) Draw(root *ui.Ops) {
w.mu.Lock()
var drawDur time.Duration
if !w.drawStart.IsZero() {
drawDur = time.Since(w.drawStart)
w.drawStart = time.Time{}
}
stage := w.stage
sync := w.syncGPU
w.syncGPU = false
alive := w.isAlive()
w.mu.Unlock()
if !alive || stage < StageRunning {
return
}
if w.gpu != nil {
if sync {
w.gpu.Refresh()
}
if err := w.gpu.Flush(); err != nil {
w.gpu.Release()
w.gpu = nil
}
}
if w.gpu == nil {
ctx, err := newContext(w.driver)
if err != nil {
w.err = err
return
}
w.gpu, err = gpu.NewGPU(ctx)
if err != nil {
w.err = err
return
}
}
w.reader.Reset(root)
redrawTime, redraw := collectRedraws(&w.reader)
now := time.Now()
w.mu.Lock()
size := w.size
frameDur := now.Sub(w.lastFrame)
frameDur = frameDur.Truncate(100 * time.Microsecond)
w.lastFrame = now
if w.Profiling {
q := 100 * time.Microsecond
w.timings = fmt.Sprintf("tot:%7s cpu:%7s %s", frameDur.Round(q), drawDur.Round(q), w.gpu.Timings())
w.setNextFrame(time.Time{})
}
if redraw {
w.setNextFrame(redrawTime)
}
w.updateAnimation()
w.mu.Unlock()
w.gpu.Draw(w.Profiling, size, root)
}
func collectRedraws(r *ui.OpsReader) (time.Time, bool) {
var t time.Time
redraw := false
for {
encOp, ok := r.Decode()
if !ok {
break
}
switch ops.OpType(encOp.Data[0]) {
case ops.TypeInvalidate:
var op ui.InvalidateOp
op.Decode(encOp.Data)
if !redraw || op.At.Before(t) {
redraw = true
t = op.At
}
}
}
return t, redraw
}
func (w *Window) Redraw() {
w.mu.Lock()
defer w.mu.Unlock()
if !w.isAlive() {
return
}
w.setNextFrame(time.Time{})
w.updateAnimation()
}
func (w *Window) updateAnimation() {
animate := false
if w.delayedDraw != nil {
w.delayedDraw.Stop()
w.delayedDraw = nil
}
if w.stage >= StageRunning && w.hasNextFrame {
if dt := time.Until(w.nextFrame); dt <= 0 {
animate = true
} else {
w.delayedDraw = time.AfterFunc(dt, w.Redraw)
}
}
if animate != w.animating {
w.animating = animate
w.driver.setAnimating(animate)
}
}
func (w *Window) setNextFrame(at time.Time) {
if !w.hasNextFrame || at.Before(w.nextFrame) {
w.hasNextFrame = true
w.nextFrame = at
}
}
func (w *Window) Size() image.Point {
w.mu.Lock()
defer w.mu.Unlock()
return w.size
}
func (w *Window) Stage() Stage {
w.mu.Lock()
defer w.mu.Unlock()
return w.stage
}
func (w *Window) isAlive() bool {
return w.stage > StageDead && w.err == nil
}
func (w *Window) contextDriver() interface{} {
return w.driver
}
func (w *Window) event(e Event) {
w.eventLock.Lock()
defer w.eventLock.Unlock()
w.mu.Lock()
needAck := false
switch e := e.(type) {
case input.Event:
w.setNextFrame(time.Time{})
case *CommandEvent:
needAck = true
w.setNextFrame(time.Time{})
case StageEvent:
w.stage = e.Stage
if w.stage > StageDead {
needAck = true
w.syncGPU = true
}
case DrawEvent:
if e.Size == (image.Point{}) {
panic(errors.New("internal error: zero-sized Draw"))
}
if w.stage < StageRunning {
// No drawing if not visible.
break
}
w.drawStart = time.Now()
needAck = true
w.hasNextFrame = false
w.syncGPU = e.sync
w.size = e.Size
}
stage := w.stage
w.updateAnimation()
w.mu.Unlock()
w.events <- e
if needAck {
// Send a dummy event; when it gets through we
// know the application has processed the actual event.
w.events <- ackEvent
}
if w.gpu != nil {
w.mu.Lock()
sync := w.syncGPU
w.syncGPU = false
w.mu.Unlock()
switch {
case stage < StageRunning:
w.gpu.Release()
w.gpu = nil
case sync:
w.gpu.Refresh()
}
}
if stage == StageDead {
close(w.events)
}
}