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