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() {}