window.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package app
   4  
   5  import (
   6  	"errors"
   7  	"fmt"
   8  	"image"
   9  	"time"
  10  
  11  	"github.com/p9c/p9/pkg/gel/gio/io/event"
  12  	"github.com/p9c/p9/pkg/gel/gio/io/pointer"
  13  	"github.com/p9c/p9/pkg/gel/gio/io/profile"
  14  	"github.com/p9c/p9/pkg/gel/gio/io/router"
  15  	"github.com/p9c/p9/pkg/gel/gio/io/system"
  16  	"github.com/p9c/p9/pkg/gel/gio/op"
  17  	"github.com/p9c/p9/pkg/gel/gio/unit"
  18  
  19  	_ "github.com/p9c/p9/pkg/gel/gio/app/internal/log"
  20  	"github.com/p9c/p9/pkg/gel/gio/app/internal/wm"
  21  )
  22  
  23  // WindowOption configures a wm.
  24  type Option func(opts *wm.Options)
  25  
  26  // Window represents an operating system wm.
  27  type Window struct {
  28  	driver wm.Driver
  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()
  35  
  36  	out         chan event.Event
  37  	in          chan event.Event
  38  	ack         chan struct{}
  39  	invalidates chan struct{}
  40  	frames      chan *op.Ops
  41  	frameAck    chan struct{}
  42  	// dead is closed when the window is destroyed.
  43  	dead chan struct{}
  44  
  45  	stage        system.Stage
  46  	animating    bool
  47  	hasNextFrame bool
  48  	nextFrame    time.Time
  49  	delayedDraw  *time.Timer
  50  
  51  	queue  queue
  52  	cursor pointer.CursorName
  53  
  54  	callbacks callbacks
  55  }
  56  
  57  type callbacks struct {
  58  	w *Window
  59  }
  60  
  61  // queue is an event.Queue implementation that distributes system events
  62  // to the input handlers declared in the most recent frame.
  63  type queue struct {
  64  	q router.Router
  65  }
  66  
  67  // driverEvent is sent when a new native driver
  68  // is available for the wm.
  69  type driverEvent struct {
  70  	driver wm.Driver
  71  }
  72  
  73  // Pre-allocate the ack event to avoid garbage.
  74  var ackEvent event.Event
  75  
  76  // NewWindow creates a new window for a set of window
  77  // options. The options are hints; the platform is free to
  78  // ignore or adjust them.
  79  //
  80  // If the current program is running on iOS and Android,
  81  // NewWindow returns the window previously created by the
  82  // platform.
  83  //
  84  // Calling NewWindow more than once is not supported on
  85  // iOS, Android, WebAssembly.
  86  func NewWindow(options ...Option) *Window {
  87  	opts := new(wm.Options)
  88  	// Default options.
  89  	Size(unit.Px(800), unit.Px(600))(opts)
  90  	Title("Gio")(opts)
  91  
  92  	for _, o := range options {
  93  		o(opts)
  94  	}
  95  
  96  	w := &Window{
  97  		in:          make(chan event.Event),
  98  		out:         make(chan event.Event),
  99  		ack:         make(chan struct{}),
 100  		invalidates: make(chan struct{}, 1),
 101  		frames:      make(chan *op.Ops),
 102  		frameAck:    make(chan struct{}),
 103  		driverFuncs: make(chan func()),
 104  		dead:        make(chan struct{}),
 105  	}
 106  	w.callbacks.w = w
 107  	go w.run(opts)
 108  	return w
 109  }
 110  
 111  // Events returns the channel where events are delivered.
 112  func (w *Window) Events() <-chan event.Event {
 113  	return w.out
 114  }
 115  
 116  // update updates the wm. Paint operations updates the
 117  // window contents, input operations declare input handlers,
 118  // and so on. The supplied operations list completely replaces
 119  // the window state from previous calls.
 120  func (w *Window) update(frame *op.Ops) {
 121  	w.frames <- frame
 122  	<-w.frameAck
 123  }
 124  
 125  func (w *Window) validateAndProcess(frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error {
 126  	for {
 127  		if w.loop != nil {
 128  			if err := w.loop.Flush(); err != nil {
 129  				w.destroyGPU()
 130  				if err == wm.ErrDeviceLost {
 131  					continue
 132  				}
 133  				return err
 134  			}
 135  		}
 136  		if w.loop == nil {
 137  			var err error
 138  			w.ctx, err = w.driver.NewContext()
 139  			if err != nil {
 140  				return err
 141  			}
 142  			w.loop, err = newLoop(w.ctx)
 143  			if err != nil {
 144  				w.ctx.Release()
 145  				return err
 146  			}
 147  		}
 148  		w.processFrame(frameStart, size, frame)
 149  		if sync {
 150  			if err := w.loop.Flush(); err != nil {
 151  				w.destroyGPU()
 152  				if err == wm.ErrDeviceLost {
 153  					continue
 154  				}
 155  				return err
 156  			}
 157  		}
 158  		return nil
 159  	}
 160  }
 161  
 162  func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.Ops) {
 163  	sync := w.loop.Draw(size, frame)
 164  	w.queue.q.Frame(frame)
 165  	switch w.queue.q.TextInputState() {
 166  	case router.TextInputOpen:
 167  		w.driver.ShowTextInput(true)
 168  	case router.TextInputClose:
 169  		w.driver.ShowTextInput(false)
 170  	}
 171  	if txt, ok := w.queue.q.WriteClipboard(); ok {
 172  		go w.WriteClipboard(txt)
 173  	}
 174  	if w.queue.q.ReadClipboard() {
 175  		go w.ReadClipboard()
 176  	}
 177  	if w.queue.q.Profiling() {
 178  		frameDur := time.Since(frameStart)
 179  		frameDur = frameDur.Truncate(100 * time.Microsecond)
 180  		q := 100 * time.Microsecond
 181  		timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.loop.Summary())
 182  		w.queue.q.Queue(profile.Event{Timings: timings})
 183  	}
 184  	if t, ok := w.queue.q.WakeupTime(); ok {
 185  		w.setNextFrame(t)
 186  	}
 187  	// Opportunistically check whether Invalidate has been called, to avoid
 188  	// stopping and starting animation mode.
 189  	select {
 190  	case <-w.invalidates:
 191  		w.setNextFrame(time.Time{})
 192  	default:
 193  	}
 194  	w.updateAnimation()
 195  	// Wait for the GPU goroutine to finish processing frame.
 196  	<-sync
 197  }
 198  
 199  // Invalidate the window such that a FrameEvent will be generated immediately.
 200  // If the window is inactive, the event is sent when the window becomes active.
 201  //
 202  // Note that Invalidate is intended for externally triggered updates, such as a
 203  // response from a network request. InvalidateOp is more efficient for animation
 204  // and similar internal updates.
 205  //
 206  // Invalidate is safe for concurrent use.
 207  func (w *Window) Invalidate() {
 208  	select {
 209  	case w.invalidates <- struct{}{}:
 210  	default:
 211  	}
 212  }
 213  
 214  // Option applies the options to the window.
 215  func (w *Window) Option(opts ...Option) {
 216  	go w.driverDo(func() {
 217  		o := new(wm.Options)
 218  		for _, opt := range opts {
 219  			opt(o)
 220  		}
 221  		w.driver.Option(o)
 222  	})
 223  }
 224  
 225  // ReadClipboard initiates a read of the clipboard in the form
 226  // of a clipboard.Event. Multiple reads may be coalesced
 227  // to a single event.
 228  func (w *Window) ReadClipboard() {
 229  	go w.driverDo(func() {
 230  		w.driver.ReadClipboard()
 231  	})
 232  }
 233  
 234  // WriteClipboard writes a string to the clipboard.
 235  func (w *Window) WriteClipboard(s string) {
 236  	go w.driverDo(func() {
 237  		w.driver.WriteClipboard(s)
 238  	})
 239  }
 240  
 241  // SetCursorName changes the current window cursor to name.
 242  func (w *Window) SetCursorName(name pointer.CursorName) {
 243  	go w.driverDo(func() {
 244  		w.driver.SetCursor(name)
 245  	})
 246  }
 247  
 248  // Close the wm. The window's event loop should exit when it receives
 249  // system.DestroyEvent.
 250  //
 251  // Currently, only macOS, Windows and X11 drivers implement this functionality,
 252  // all others are stubbed.
 253  func (w *Window) Close() {
 254  	go w.driverDo(func() {
 255  		w.driver.Close()
 256  	})
 257  }
 258  
 259  // driverDo waits for the window to have a valid driver attached and calls f.
 260  // It does nothing if the if the window was destroyed while waiting.
 261  func (w *Window) driverDo(f func()) {
 262  	select {
 263  	case w.driverFuncs <- f:
 264  	case <-w.dead:
 265  	}
 266  }
 267  
 268  func (w *Window) updateAnimation() {
 269  	animate := false
 270  	if w.delayedDraw != nil {
 271  		w.delayedDraw.Stop()
 272  		w.delayedDraw = nil
 273  	}
 274  	if w.stage >= system.StageRunning && w.hasNextFrame {
 275  		if dt := time.Until(w.nextFrame); dt <= 0 {
 276  			animate = true
 277  		} else {
 278  			w.delayedDraw = time.NewTimer(dt)
 279  		}
 280  	}
 281  	if animate != w.animating {
 282  		w.animating = animate
 283  		w.driver.SetAnimating(animate)
 284  	}
 285  }
 286  
 287  func (w *Window) setNextFrame(at time.Time) {
 288  	if !w.hasNextFrame || at.Before(w.nextFrame) {
 289  		w.hasNextFrame = true
 290  		w.nextFrame = at
 291  	}
 292  }
 293  
 294  func (c *callbacks) SetDriver(d wm.Driver) {
 295  	c.Event(driverEvent{d})
 296  }
 297  
 298  func (c *callbacks) Event(e event.Event) {
 299  	select {
 300  	case c.w.in <- e:
 301  		<-c.w.ack
 302  	case <-c.w.dead:
 303  	}
 304  }
 305  
 306  func (w *Window) waitAck() {
 307  	// Send a dummy event; when it gets through we
 308  	// know the application has processed the previous event.
 309  	w.out <- ackEvent
 310  }
 311  
 312  // Prematurely destroy the window and wait for the native window
 313  // destroy event.
 314  func (w *Window) destroy(err error) {
 315  	w.destroyGPU()
 316  	// Ack the current event.
 317  	w.ack <- struct{}{}
 318  	w.out <- system.DestroyEvent{Err: err}
 319  	close(w.dead)
 320  	for e := range w.in {
 321  		w.ack <- struct{}{}
 322  		if _, ok := e.(system.DestroyEvent); ok {
 323  			return
 324  		}
 325  	}
 326  }
 327  
 328  func (w *Window) destroyGPU() {
 329  	if w.loop != nil {
 330  		w.loop.Release()
 331  		w.loop = nil
 332  	}
 333  	if w.ctx != nil {
 334  		w.ctx.Release()
 335  		w.ctx = nil
 336  	}
 337  }
 338  
 339  // waitFrame waits for the client to either call FrameEvent.Frame
 340  // or to continue event handling. It returns whether the client
 341  // called Frame or not.
 342  func (w *Window) waitFrame() (*op.Ops, bool) {
 343  	select {
 344  	case frame := <-w.frames:
 345  		// The client called FrameEvent.Frame.
 346  		return frame, true
 347  	case w.out <- ackEvent:
 348  		// The client ignored FrameEvent and continued processing
 349  		// events.
 350  		return nil, false
 351  	}
 352  }
 353  
 354  func (w *Window) run(opts *wm.Options) {
 355  	defer close(w.in)
 356  	defer close(w.out)
 357  	if err := wm.NewWindow(&w.callbacks, opts); err != nil {
 358  		w.out <- system.DestroyEvent{Err: err}
 359  		return
 360  	}
 361  	for {
 362  		var driverFuncs chan func()
 363  		if w.driver != nil {
 364  			driverFuncs = w.driverFuncs
 365  		}
 366  		var timer <-chan time.Time
 367  		if w.delayedDraw != nil {
 368  			timer = w.delayedDraw.C
 369  		}
 370  		select {
 371  		case <-timer:
 372  			w.setNextFrame(time.Time{})
 373  			w.updateAnimation()
 374  		case <-w.invalidates:
 375  			w.setNextFrame(time.Time{})
 376  			w.updateAnimation()
 377  		case f := <-driverFuncs:
 378  			f()
 379  		case e := <-w.in:
 380  			switch e2 := e.(type) {
 381  			case system.StageEvent:
 382  				if w.loop != nil {
 383  					if e2.Stage < system.StageRunning {
 384  						w.destroyGPU()
 385  					} else {
 386  						w.loop.Refresh()
 387  					}
 388  				}
 389  				w.stage = e2.Stage
 390  				w.updateAnimation()
 391  				w.out <- e
 392  				w.waitAck()
 393  			case wm.FrameEvent:
 394  				if e2.Size == (image.Point{}) {
 395  					panic(errors.New("internal error: zero-sized Draw"))
 396  				}
 397  				if w.stage < system.StageRunning {
 398  					// No drawing if not visible.
 399  					break
 400  				}
 401  				frameStart := time.Now()
 402  				w.hasNextFrame = false
 403  				e2.Frame = w.update
 404  				e2.Queue = &w.queue
 405  				w.out <- e2.FrameEvent
 406  				if w.loop != nil {
 407  					if e2.Sync {
 408  						w.loop.Refresh()
 409  					}
 410  				}
 411  				frame, gotFrame := w.waitFrame()
 412  				err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame)
 413  				if gotFrame {
 414  					// We're done with frame, let the client continue.
 415  					w.frameAck <- struct{}{}
 416  				}
 417  				if err != nil {
 418  					w.destroyGPU()
 419  					w.destroy(err)
 420  					return
 421  				}
 422  				w.updateCursor()
 423  			case *system.CommandEvent:
 424  				w.out <- e
 425  				w.waitAck()
 426  			case driverEvent:
 427  				w.driver = e2.driver
 428  			case system.DestroyEvent:
 429  				w.destroyGPU()
 430  				w.out <- e2
 431  				w.ack <- struct{}{}
 432  				return
 433  			case event.Event:
 434  				if w.queue.q.Queue(e2) {
 435  					w.setNextFrame(time.Time{})
 436  					w.updateAnimation()
 437  				}
 438  				w.updateCursor()
 439  				w.out <- e
 440  			}
 441  			w.ack <- struct{}{}
 442  		}
 443  	}
 444  }
 445  
 446  func (w *Window) updateCursor() {
 447  	if c := w.queue.q.Cursor(); c != w.cursor {
 448  		w.cursor = c
 449  		w.SetCursorName(c)
 450  	}
 451  }
 452  
 453  func (q *queue) Events(k event.Tag) []event.Event {
 454  	return q.q.Events(k)
 455  }
 456  
 457  const (
 458  	// Windowed is the normal window mode with OS specific window decorations.
 459  	Windowed = wm.Windowed
 460  	// Fullscreen is the full screen window mode.
 461  	Fullscreen = wm.Fullscreen
 462  )
 463  
 464  // WindowMode sets the window mode.
 465  //
 466  // Supported platforms are macOS, X11 and Windows.
 467  func WindowMode(mode wm.WindowMode) Option {
 468  	return func(opts *wm.Options) {
 469  		opts.WindowMode = &mode
 470  	}
 471  }
 472  
 473  // Title sets the title of the wm.
 474  func Title(t string) Option {
 475  	return func(opts *wm.Options) {
 476  		opts.Title = &t
 477  	}
 478  }
 479  
 480  // Size sets the size of the wm.
 481  func Size(w, h unit.Value) Option {
 482  	if w.V <= 0 {
 483  		panic("width must be larger than or equal to 0")
 484  	}
 485  	if h.V <= 0 {
 486  		panic("height must be larger than or equal to 0")
 487  	}
 488  	return func(opts *wm.Options) {
 489  		opts.Size = &wm.Size{
 490  			Width:  w,
 491  			Height: h,
 492  		}
 493  	}
 494  }
 495  
 496  // MaxSize sets the maximum size of the wm.
 497  func MaxSize(w, h unit.Value) Option {
 498  	if w.V <= 0 {
 499  		panic("width must be larger than or equal to 0")
 500  	}
 501  	if h.V <= 0 {
 502  		panic("height must be larger than or equal to 0")
 503  	}
 504  	return func(opts *wm.Options) {
 505  		opts.MaxSize = &wm.Size{
 506  			Width:  w,
 507  			Height: h,
 508  		}
 509  	}
 510  }
 511  
 512  // MinSize sets the minimum size of the wm.
 513  func MinSize(w, h unit.Value) Option {
 514  	if w.V <= 0 {
 515  		panic("width must be larger than or equal to 0")
 516  	}
 517  	if h.V <= 0 {
 518  		panic("height must be larger than or equal to 0")
 519  	}
 520  	return func(opts *wm.Options) {
 521  		opts.MinSize = &wm.Size{
 522  			Width:  w,
 523  			Height: h,
 524  		}
 525  	}
 526  }
 527  
 528  func (driverEvent) ImplementsEvent() {}
 529