github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/app/window.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package app
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"image"
     9  	"image/color"
    10  	"time"
    11  
    12  	"github.com/cybriq/giocore/io/event"
    13  	"github.com/cybriq/giocore/io/pointer"
    14  	"github.com/cybriq/giocore/io/profile"
    15  	"github.com/cybriq/giocore/io/router"
    16  	"github.com/cybriq/giocore/io/system"
    17  	"github.com/cybriq/giocore/op"
    18  	"github.com/cybriq/giocore/unit"
    19  
    20  	_ "github.com/cybriq/giocore/app/internal/log"
    21  	"github.com/cybriq/giocore/app/internal/wm"
    22  )
    23  
    24  // WindowOption configures a wm.
    25  type Option func(opts *wm.Options)
    26  
    27  // Window represents an operating system window.
    28  type Window struct {
    29  	ctx  wm.Context
    30  	loop *renderLoop
    31  
    32  	// driverFuncs is a channel of functions to run when
    33  	// the Window has a valid driver.
    34  	driverFuncs chan func(d wm.Driver)
    35  	// wakeups wakes up the native event loop to send a
    36  	// wm.WakeupEvent that flushes driverFuncs.
    37  	wakeups chan struct{}
    38  
    39  	out         chan event.Event
    40  	in          chan event.Event
    41  	ack         chan struct{}
    42  	invalidates chan struct{}
    43  	frames      chan *op.Ops
    44  	frameAck    chan struct{}
    45  	// dead is closed when the window is destroyed.
    46  	dead          chan struct{}
    47  	notifyAnimate chan struct{}
    48  
    49  	stage        system.Stage
    50  	animating    bool
    51  	hasNextFrame bool
    52  	nextFrame    time.Time
    53  	delayedDraw  *time.Timer
    54  
    55  	queue  queue
    56  	cursor pointer.CursorName
    57  
    58  	callbacks callbacks
    59  
    60  	nocontext bool
    61  }
    62  
    63  type callbacks struct {
    64  	w *Window
    65  	d wm.Driver
    66  }
    67  
    68  // queue is an event.Queue implementation that distributes system events
    69  // to the input handlers declared in the most recent frame.
    70  type queue struct {
    71  	q router.Router
    72  }
    73  
    74  // driverEvent is sent when the underlying driver changes.
    75  type driverEvent struct {
    76  	driver wm.Driver
    77  }
    78  
    79  // Pre-allocate the ack event to avoid garbage.
    80  var ackEvent event.Event
    81  
    82  // NewWindow creates a new window for a set of window
    83  // options. The options are hints; the platform is free to
    84  // ignore or adjust them.
    85  //
    86  // If the current program is running on iOS and Android,
    87  // NewWindow returns the window previously created by the
    88  // platform.
    89  //
    90  // Calling NewWindow more than once is not supported on
    91  // iOS, Android, WebAssembly.
    92  func NewWindow(options ...Option) *Window {
    93  	opts := new(wm.Options)
    94  	// Default options.
    95  	Size(unit.Dp(800), unit.Dp(600))(opts)
    96  	Title("Gio")(opts)
    97  
    98  	for _, o := range options {
    99  		o(opts)
   100  	}
   101  
   102  	w := &Window{
   103  		in:            make(chan event.Event),
   104  		out:           make(chan event.Event),
   105  		ack:           make(chan struct{}),
   106  		invalidates:   make(chan struct{}, 1),
   107  		frames:        make(chan *op.Ops),
   108  		frameAck:      make(chan struct{}),
   109  		driverFuncs:   make(chan func(d wm.Driver), 1),
   110  		wakeups:       make(chan struct{}, 1),
   111  		dead:          make(chan struct{}),
   112  		notifyAnimate: make(chan struct{}, 1),
   113  		nocontext:     opts.CustomRenderer,
   114  	}
   115  	w.callbacks.w = w
   116  	go w.run(opts)
   117  	return w
   118  }
   119  
   120  // Events returns the channel where events are delivered.
   121  func (w *Window) Events() <-chan event.Event {
   122  	return w.out
   123  }
   124  
   125  // update updates the wm. Paint operations updates the
   126  // window contents, input operations declare input handlers,
   127  // and so on. The supplied operations list completely replaces
   128  // the window state from previous calls.
   129  func (w *Window) update(frame *op.Ops) {
   130  	w.frames <- frame
   131  	<-w.frameAck
   132  }
   133  
   134  func (w *Window) validateAndProcess(driver wm.Driver, frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error {
   135  	for {
   136  		if w.loop != nil {
   137  			if err := w.loop.Flush(); err != nil {
   138  				w.destroyGPU()
   139  				if err == wm.ErrDeviceLost {
   140  					continue
   141  				}
   142  				return err
   143  			}
   144  		}
   145  		if w.loop == nil && !w.nocontext {
   146  			var err error
   147  			w.ctx, err = driver.NewContext()
   148  			if err != nil {
   149  				return err
   150  			}
   151  			w.loop, err = newLoop(w.ctx)
   152  			if err != nil {
   153  				w.ctx.Release()
   154  				return err
   155  			}
   156  		}
   157  		w.processFrame(frameStart, size, frame)
   158  		if sync && w.loop != nil {
   159  			if err := w.loop.Flush(); err != nil {
   160  				w.destroyGPU()
   161  				if err == wm.ErrDeviceLost {
   162  					continue
   163  				}
   164  				return err
   165  			}
   166  		}
   167  		return nil
   168  	}
   169  }
   170  
   171  func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.Ops) {
   172  	var sync <-chan struct{}
   173  	if w.loop != nil {
   174  		sync = w.loop.Draw(size, frame)
   175  	} else {
   176  		s := make(chan struct{}, 1)
   177  		s <- struct{}{}
   178  		sync = s
   179  	}
   180  	w.queue.q.Frame(frame)
   181  	switch w.queue.q.TextInputState() {
   182  	case router.TextInputOpen:
   183  		go w.driverRun(func(d wm.Driver) { d.ShowTextInput(true) })
   184  	case router.TextInputClose:
   185  		go w.driverRun(func(d wm.Driver) { d.ShowTextInput(false) })
   186  	}
   187  	if hint, ok := w.queue.q.TextInputHint(); ok {
   188  		go w.driverRun(func(d wm.Driver) { d.SetInputHint(hint) })
   189  	}
   190  	if txt, ok := w.queue.q.WriteClipboard(); ok {
   191  		go w.WriteClipboard(txt)
   192  	}
   193  	if w.queue.q.ReadClipboard() {
   194  		go w.ReadClipboard()
   195  	}
   196  	if w.queue.q.Profiling() && w.loop != nil {
   197  		frameDur := time.Since(frameStart)
   198  		frameDur = frameDur.Truncate(100 * time.Microsecond)
   199  		q := 100 * time.Microsecond
   200  		timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.loop.Summary())
   201  		w.queue.q.Queue(profile.Event{Timings: timings})
   202  	}
   203  	if t, ok := w.queue.q.WakeupTime(); ok {
   204  		w.setNextFrame(t)
   205  	}
   206  	// Opportunistically check whether Invalidate has been called, to avoid
   207  	// stopping and starting animation mode.
   208  	select {
   209  	case <-w.invalidates:
   210  		w.setNextFrame(time.Time{})
   211  	default:
   212  	}
   213  	w.updateAnimation()
   214  	// Wait for the GPU goroutine to finish processing frame.
   215  	<-sync
   216  }
   217  
   218  // Invalidate the window such that a FrameEvent will be generated immediately.
   219  // If the window is inactive, the event is sent when the window becomes active.
   220  //
   221  // Note that Invalidate is intended for externally triggered updates, such as a
   222  // response from a network request. InvalidateOp is more efficient for animation
   223  // and similar internal updates.
   224  //
   225  // Invalidate is safe for concurrent use.
   226  func (w *Window) Invalidate() {
   227  	select {
   228  	case w.invalidates <- struct{}{}:
   229  	default:
   230  	}
   231  }
   232  
   233  // Option applies the options to the window.
   234  func (w *Window) Option(opts ...Option) {
   235  	go w.driverRun(func(d wm.Driver) {
   236  		o := new(wm.Options)
   237  		for _, opt := range opts {
   238  			opt(o)
   239  		}
   240  		d.Option(o)
   241  	})
   242  }
   243  
   244  // ReadClipboard initiates a read of the clipboard in the form
   245  // of a clipboard.Event. Multiple reads may be coalesced
   246  // to a single event.
   247  func (w *Window) ReadClipboard() {
   248  	go w.driverRun(func(d wm.Driver) {
   249  		d.ReadClipboard()
   250  	})
   251  }
   252  
   253  // WriteClipboard writes a string to the clipboard.
   254  func (w *Window) WriteClipboard(s string) {
   255  	go w.driverRun(func(d wm.Driver) {
   256  		d.WriteClipboard(s)
   257  	})
   258  }
   259  
   260  // SetCursorName changes the current window cursor to name.
   261  func (w *Window) SetCursorName(name pointer.CursorName) {
   262  	go w.driverRun(func(d wm.Driver) {
   263  		d.SetCursor(name)
   264  	})
   265  }
   266  
   267  // Close the wm. The window's event loop should exit when it receives
   268  // system.DestroyEvent.
   269  //
   270  // Currently, only macOS, Windows and X11 drivers implement this functionality,
   271  // all others are stubbed.
   272  func (w *Window) Close() {
   273  	go w.driverRun(func(d wm.Driver) {
   274  		d.Close()
   275  	})
   276  }
   277  
   278  // Run f in the same thread as the native window event loop, and wait for f to
   279  // return or the window to close. Run is guaranteed not to deadlock if it is
   280  // invoked during the handling of a ViewEvent, system.FrameEvent,
   281  // system.StageEvent; call Run in a separate goroutine to avoid deadlock in all
   282  // other cases.
   283  //
   284  // Note that most programs should not call Run; configuring a Window with
   285  // CustomRenderer is a notable exception.
   286  func (w *Window) Run(f func()) {
   287  	w.driverRun(func(_ wm.Driver) {
   288  		f()
   289  	})
   290  }
   291  
   292  func (w *Window) driverRun(f func(d wm.Driver)) {
   293  	done := make(chan struct{})
   294  	wrapper := func(d wm.Driver) {
   295  		defer close(done)
   296  		f(d)
   297  	}
   298  	select {
   299  	case w.driverFuncs <- wrapper:
   300  		w.wakeup()
   301  		select {
   302  		case <-done:
   303  		case <-w.dead:
   304  		}
   305  	case <-w.dead:
   306  	}
   307  }
   308  
   309  func (w *Window) updateAnimation() {
   310  	animate := false
   311  	if w.delayedDraw != nil {
   312  		w.delayedDraw.Stop()
   313  		w.delayedDraw = nil
   314  	}
   315  	if w.stage >= system.StageRunning && w.hasNextFrame {
   316  		if dt := time.Until(w.nextFrame); dt <= 0 {
   317  			animate = true
   318  		} else {
   319  			w.delayedDraw = time.NewTimer(dt)
   320  		}
   321  	}
   322  	if animate != w.animating {
   323  		w.animating = animate
   324  		select {
   325  		case w.notifyAnimate <- struct{}{}:
   326  			w.wakeup()
   327  		default:
   328  		}
   329  	}
   330  }
   331  
   332  func (w *Window) wakeup() {
   333  	select {
   334  	case w.wakeups <- struct{}{}:
   335  	default:
   336  	}
   337  }
   338  
   339  func (w *Window) setNextFrame(at time.Time) {
   340  	if !w.hasNextFrame || at.Before(w.nextFrame) {
   341  		w.hasNextFrame = true
   342  		w.nextFrame = at
   343  	}
   344  }
   345  
   346  func (c *callbacks) SetDriver(d wm.Driver) {
   347  	c.d = d
   348  	c.Event(driverEvent{d})
   349  }
   350  
   351  func (c *callbacks) Event(e event.Event) {
   352  	select {
   353  	case c.w.in <- e:
   354  		c.w.runFuncs(c.d)
   355  	case <-c.w.dead:
   356  	}
   357  }
   358  
   359  func (w *Window) runFuncs(d wm.Driver) {
   360  	// Don't run driver functions if there's no driver.
   361  	if d == nil {
   362  		<-w.ack
   363  		return
   364  	}
   365  	// Flush pending runnnables.
   366  loop:
   367  	for {
   368  		select {
   369  		case <-w.notifyAnimate:
   370  			d.SetAnimating(w.animating)
   371  		case f := <-w.driverFuncs:
   372  			f(d)
   373  		default:
   374  			break loop
   375  		}
   376  	}
   377  	// Wait for ack while running incoming runnables.
   378  	for {
   379  		select {
   380  		case <-w.notifyAnimate:
   381  			d.SetAnimating(w.animating)
   382  		case f := <-w.driverFuncs:
   383  			f(d)
   384  		case <-w.ack:
   385  			return
   386  		}
   387  	}
   388  }
   389  
   390  func (c *callbacks) Run(f func()) {
   391  	c.w.Run(f)
   392  }
   393  
   394  func (w *Window) waitAck() {
   395  	// Send a dummy event; when it gets through we
   396  	// know the application has processed the previous event.
   397  	w.out <- ackEvent
   398  }
   399  
   400  // Prematurely destroy the window and wait for the native window
   401  // destroy event.
   402  func (w *Window) destroy(err error) {
   403  	w.destroyGPU()
   404  	// Ack the current event.
   405  	w.ack <- struct{}{}
   406  	w.out <- system.DestroyEvent{Err: err}
   407  	close(w.dead)
   408  	close(w.out)
   409  	for e := range w.in {
   410  		w.ack <- struct{}{}
   411  		if _, ok := e.(system.DestroyEvent); ok {
   412  			return
   413  		}
   414  	}
   415  }
   416  
   417  func (w *Window) refresh() {
   418  	w.driverRun(func(_ wm.Driver) {
   419  		w.ctx.Refresh()
   420  	})
   421  	w.loop.Refresh()
   422  }
   423  
   424  func (w *Window) destroyGPU() {
   425  	if w.loop != nil {
   426  		w.loop.Release()
   427  		w.loop = nil
   428  	}
   429  	if w.ctx != nil {
   430  		w.ctx.Release()
   431  		w.ctx = nil
   432  	}
   433  }
   434  
   435  // waitFrame waits for the client to either call FrameEvent.Frame
   436  // or to continue event handling. It returns whether the client
   437  // called Frame or not.
   438  func (w *Window) waitFrame() (*op.Ops, bool) {
   439  	select {
   440  	case frame := <-w.frames:
   441  		// The client called FrameEvent.Frame.
   442  		return frame, true
   443  	case w.out <- ackEvent:
   444  		// The client ignored FrameEvent and continued processing
   445  		// events.
   446  		return nil, false
   447  	}
   448  }
   449  
   450  func (w *Window) run(opts *wm.Options) {
   451  	defer close(w.out)
   452  	defer close(w.dead)
   453  	if err := wm.NewWindow(&w.callbacks, opts); err != nil {
   454  		w.out <- system.DestroyEvent{Err: err}
   455  		return
   456  	}
   457  	var driver wm.Driver
   458  	for {
   459  		var wakeups chan struct{}
   460  		if driver != nil {
   461  			wakeups = w.wakeups
   462  		}
   463  		var timer <-chan time.Time
   464  		if w.delayedDraw != nil {
   465  			timer = w.delayedDraw.C
   466  		}
   467  		select {
   468  		case <-timer:
   469  			w.setNextFrame(time.Time{})
   470  			w.updateAnimation()
   471  		case <-w.invalidates:
   472  			w.setNextFrame(time.Time{})
   473  			w.updateAnimation()
   474  		case <-wakeups:
   475  			driver.Wakeup()
   476  		case e := <-w.in:
   477  			switch e2 := e.(type) {
   478  			case system.StageEvent:
   479  				if w.loop != nil {
   480  					if e2.Stage < system.StageRunning {
   481  						w.destroyGPU()
   482  					} else {
   483  						w.refresh()
   484  					}
   485  				}
   486  				w.stage = e2.Stage
   487  				w.updateAnimation()
   488  				w.out <- e
   489  				w.waitAck()
   490  			case wm.FrameEvent:
   491  				if e2.Size == (image.Point{}) {
   492  					panic(errors.New("internal error: zero-sized Draw"))
   493  				}
   494  				if w.stage < system.StageRunning {
   495  					// No drawing if not visible.
   496  					break
   497  				}
   498  				frameStart := time.Now()
   499  				w.hasNextFrame = false
   500  				e2.Frame = w.update
   501  				e2.Queue = &w.queue
   502  				w.out <- e2.FrameEvent
   503  				if w.loop != nil {
   504  					if e2.Sync {
   505  						w.refresh()
   506  					}
   507  				}
   508  				frame, gotFrame := w.waitFrame()
   509  				err := w.validateAndProcess(driver, frameStart, e2.Size, e2.Sync, frame)
   510  				if gotFrame {
   511  					// We're done with frame, let the client continue.
   512  					w.frameAck <- struct{}{}
   513  				}
   514  				if err != nil {
   515  					w.destroyGPU()
   516  					w.destroy(err)
   517  					return
   518  				}
   519  				w.updateCursor()
   520  			case *system.CommandEvent:
   521  				w.out <- e
   522  				w.waitAck()
   523  			case driverEvent:
   524  				driver = e2.driver
   525  			case system.DestroyEvent:
   526  				w.destroyGPU()
   527  				w.out <- e2
   528  				w.ack <- struct{}{}
   529  				return
   530  			case ViewEvent:
   531  				w.out <- e2
   532  				w.waitAck()
   533  			case wm.WakeupEvent:
   534  			case event.Event:
   535  				if w.queue.q.Queue(e2) {
   536  					w.setNextFrame(time.Time{})
   537  					w.updateAnimation()
   538  				}
   539  				w.updateCursor()
   540  				w.out <- e
   541  			}
   542  			w.ack <- struct{}{}
   543  		}
   544  	}
   545  }
   546  
   547  func (w *Window) updateCursor() {
   548  	if c := w.queue.q.Cursor(); c != w.cursor {
   549  		w.cursor = c
   550  		w.SetCursorName(c)
   551  	}
   552  }
   553  
   554  func (q *queue) Events(k event.Tag) []event.Event {
   555  	return q.q.Events(k)
   556  }
   557  
   558  var (
   559  	// Windowed is the normal window mode with OS specific window decorations.
   560  	Windowed = windowMode(wm.Windowed)
   561  	// Fullscreen is the full screen window mode.
   562  	Fullscreen = windowMode(wm.Fullscreen)
   563  )
   564  
   565  // windowMode sets the window mode.
   566  //
   567  // Supported platforms are macOS, X11, Windows and JS.
   568  func windowMode(mode wm.WindowMode) Option {
   569  	return func(opts *wm.Options) {
   570  		opts.WindowMode = &mode
   571  	}
   572  }
   573  
   574  var (
   575  	// AnyOrientation allows the window to be freely orientated.
   576  	AnyOrientation = orientation(wm.AnyOrientation)
   577  	// LandscapeOrientation constrains the window to landscape orientations.
   578  	LandscapeOrientation = orientation(wm.LandscapeOrientation)
   579  	// PortraitOrientation constrains the window to portrait orientations.
   580  	PortraitOrientation = orientation(wm.PortraitOrientation)
   581  )
   582  
   583  // orientation sets the orientation of the app.
   584  //
   585  // Supported platforms are Android and JS.
   586  func orientation(mode wm.Orientation) Option {
   587  	return func(opts *wm.Options) {
   588  		opts.Orientation = &mode
   589  	}
   590  }
   591  
   592  // Title sets the title of the wm.
   593  func Title(t string) Option {
   594  	return func(opts *wm.Options) {
   595  		opts.Title = &t
   596  	}
   597  }
   598  
   599  // Size sets the size of the wm.
   600  func Size(w, h unit.Value) Option {
   601  	if w.V <= 0 {
   602  		panic("width must be larger than or equal to 0")
   603  	}
   604  	if h.V <= 0 {
   605  		panic("height must be larger than or equal to 0")
   606  	}
   607  	return func(opts *wm.Options) {
   608  		opts.Size = &wm.Size{
   609  			Width:  w,
   610  			Height: h,
   611  		}
   612  	}
   613  }
   614  
   615  // MaxSize sets the maximum size of the wm.
   616  func MaxSize(w, h unit.Value) Option {
   617  	if w.V <= 0 {
   618  		panic("width must be larger than or equal to 0")
   619  	}
   620  	if h.V <= 0 {
   621  		panic("height must be larger than or equal to 0")
   622  	}
   623  	return func(opts *wm.Options) {
   624  		opts.MaxSize = &wm.Size{
   625  			Width:  w,
   626  			Height: h,
   627  		}
   628  	}
   629  }
   630  
   631  // MinSize sets the minimum size of the wm.
   632  func MinSize(w, h unit.Value) Option {
   633  	if w.V <= 0 {
   634  		panic("width must be larger than or equal to 0")
   635  	}
   636  	if h.V <= 0 {
   637  		panic("height must be larger than or equal to 0")
   638  	}
   639  	return func(opts *wm.Options) {
   640  		opts.MinSize = &wm.Size{
   641  			Width:  w,
   642  			Height: h,
   643  		}
   644  	}
   645  }
   646  
   647  // StatusColor sets the color of the Android status bar.
   648  func StatusColor(color color.NRGBA) Option {
   649  	return func(opts *wm.Options) {
   650  		opts.StatusColor = &color
   651  	}
   652  }
   653  
   654  // NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers.
   655  func NavigationColor(color color.NRGBA) Option {
   656  	return func(opts *wm.Options) {
   657  		opts.NavigationColor = &color
   658  	}
   659  }
   660  
   661  // CustomRenderer controls whether the the window contents is
   662  // rendered by the client. If true, no GPU context is created.
   663  func CustomRenderer(custom bool) Option {
   664  	return func(opts *wm.Options) {
   665  		opts.CustomRenderer = custom
   666  	}
   667  }
   668  
   669  func (driverEvent) ImplementsEvent() {}