os_js.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package wm
4
5 import (
6 "image"
7 "strings"
8 "sync"
9 "syscall/js"
10 "time"
11 "unicode"
12 "unicode/utf8"
13
14 "github.com/p9c/p9/pkg/gel/gio/f32"
15 "github.com/p9c/p9/pkg/gel/gio/io/clipboard"
16 "github.com/p9c/p9/pkg/gel/gio/io/key"
17 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
18 "github.com/p9c/p9/pkg/gel/gio/io/system"
19 "github.com/p9c/p9/pkg/gel/gio/unit"
20 )
21
22 type window struct {
23 window js.Value
24 document js.Value
25 clipboard js.Value
26 cnv js.Value
27 tarea js.Value
28 w Callbacks
29 redraw js.Func
30 clipboardCallback js.Func
31 requestAnimationFrame js.Value
32 browserHistory js.Value
33 visualViewport js.Value
34 cleanfuncs []func()
35 touches []js.Value
36 composing bool
37 requestFocus bool
38
39 chanAnimation chan struct{}
40 chanRedraw chan struct{}
41
42 mu sync.Mutex
43 size f32.Point
44 inset f32.Point
45 scale float32
46 animating bool
47 // animRequested tracks whether a requestAnimationFrame callback
48 // is pending.
49 animRequested bool
50 }
51
52 func NewWindow(win Callbacks, opts *Options) error {
53 doc := js.Global().Get("document")
54 cont := getContainer(doc)
55 cnv := createCanvas(doc)
56 cont.Call("appendChild", cnv)
57 tarea := createTextArea(doc)
58 cont.Call("appendChild", tarea)
59 w := &window{
60 cnv: cnv,
61 document: doc,
62 tarea: tarea,
63 window: js.Global().Get("window"),
64 clipboard: js.Global().Get("navigator").Get("clipboard"),
65 }
66 w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
67 w.browserHistory = w.window.Get("history")
68 w.visualViewport = w.window.Get("visualViewport")
69 if w.visualViewport.IsUndefined() {
70 w.visualViewport = w.window
71 }
72 w.chanAnimation = make(chan struct{}, 1)
73 w.chanRedraw = make(chan struct{}, 1)
74 w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
75 w.chanAnimation <- struct{}{}
76 return nil
77 })
78 w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
79 content := args[0].String()
80 win.Event(clipboard.Event{Text: content})
81 return nil
82 })
83 w.addEventListeners()
84 w.addHistory()
85 w.Option(opts)
86 w.w = win
87
88 go func() {
89 defer w.cleanup()
90 w.w.SetDriver(w)
91 w.blur()
92 w.w.Event(system.StageEvent{Stage: system.StageRunning})
93 w.resize()
94 w.draw(true)
95 for {
96 select {
97 case <-w.chanAnimation:
98 w.animCallback()
99 case <-w.chanRedraw:
100 w.draw(true)
101 }
102 }
103 }()
104 return nil
105 }
106
107 func getContainer(doc js.Value) js.Value {
108 cont := doc.Call("getElementById", "giowindow")
109 if !cont.IsNull() {
110 return cont
111 }
112 cont = doc.Call("createElement", "DIV")
113 doc.Get("body").Call("appendChild", cont)
114 return cont
115 }
116
117 func createTextArea(doc js.Value) js.Value {
118 tarea := doc.Call("createElement", "input")
119 style := tarea.Get("style")
120 style.Set("width", "1px")
121 style.Set("height", "1px")
122 style.Set("opacity", "0")
123 style.Set("border", "0")
124 style.Set("padding", "0")
125 tarea.Set("autocomplete", "off")
126 tarea.Set("autocorrect", "off")
127 tarea.Set("autocapitalize", "off")
128 tarea.Set("spellcheck", false)
129 return tarea
130 }
131
132 func createCanvas(doc js.Value) js.Value {
133 cnv := doc.Call("createElement", "canvas")
134 style := cnv.Get("style")
135 style.Set("position", "fixed")
136 style.Set("width", "100%")
137 style.Set("height", "100%")
138 return cnv
139 }
140
141 func (w *window) cleanup() {
142 // Cleanup in the opposite order of
143 // construction.
144 for i := len(w.cleanfuncs) - 1; i >= 0; i-- {
145 w.cleanfuncs[i]()
146 }
147 w.cleanfuncs = nil
148 }
149
150 func (w *window) addEventListeners() {
151 w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
152 w.resize()
153 w.chanRedraw <- struct{}{}
154 return nil
155 })
156 w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
157 args[0].Call("preventDefault")
158 return nil
159 })
160 w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
161 ev := &system.CommandEvent{Type: system.CommandBack}
162 w.w.Event(ev)
163 if ev.Cancel {
164 return w.browserHistory.Call("forward")
165 }
166
167 return w.browserHistory.Call("back")
168 })
169 w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
170 ev := system.StageEvent{}
171 switch w.document.Get("visibilityState").String() {
172 case "hidden", "prerender", "unloaded":
173 ev.Stage = system.StagePaused
174 default:
175 ev.Stage = system.StageRunning
176 }
177 w.w.Event(ev)
178 return nil
179 })
180 w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
181 w.pointerEvent(pointer.Move, 0, 0, args[0])
182 return nil
183 })
184 w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} {
185 w.pointerEvent(pointer.Press, 0, 0, args[0])
186 if w.requestFocus {
187 w.focus()
188 w.requestFocus = false
189 }
190 return nil
191 })
192 w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} {
193 w.pointerEvent(pointer.Release, 0, 0, args[0])
194 return nil
195 })
196 w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} {
197 e := args[0]
198 dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float()
199 mode := e.Get("deltaMode").Int()
200 switch mode {
201 case 0x01: // DOM_DELTA_LINE
202 dx *= 10
203 dy *= 10
204 case 0x02: // DOM_DELTA_PAGE
205 dx *= 120
206 dy *= 120
207 }
208 w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e)
209 return nil
210 })
211 w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} {
212 w.touchEvent(pointer.Press, args[0])
213 if w.requestFocus {
214 w.focus() // iOS can only focus inside a Touch event.
215 w.requestFocus = false
216 }
217 return nil
218 })
219 w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} {
220 w.touchEvent(pointer.Release, args[0])
221 return nil
222 })
223 w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} {
224 w.touchEvent(pointer.Move, args[0])
225 return nil
226 })
227 w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} {
228 // Cancel all touches even if only one touch was cancelled.
229 for i := range w.touches {
230 w.touches[i] = js.Null()
231 }
232 w.touches = w.touches[:0]
233 w.w.Event(pointer.Event{
234 Type: pointer.Cancel,
235 Source: pointer.Touch,
236 })
237 return nil
238 })
239 w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
240 w.w.Event(key.FocusEvent{Focus: true})
241 return nil
242 })
243 w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
244 w.w.Event(key.FocusEvent{Focus: false})
245 w.blur()
246 return nil
247 })
248 w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} {
249 w.keyEvent(args[0], key.Press)
250 return nil
251 })
252 w.addEventListener(w.tarea, "keyup", func(this js.Value, args []js.Value) interface{} {
253 w.keyEvent(args[0], key.Release)
254 return nil
255 })
256 w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} {
257 w.composing = true
258 return nil
259 })
260 w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} {
261 w.composing = false
262 w.flushInput()
263 return nil
264 })
265 w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} {
266 if w.composing {
267 return nil
268 }
269 w.flushInput()
270 return nil
271 })
272 w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} {
273 if w.clipboard.IsUndefined() {
274 return nil
275 }
276 // Prevents duplicated-paste, since "paste" is already handled through Clipboard API.
277 args[0].Call("preventDefault")
278 return nil
279 })
280 }
281
282 func (w *window) addHistory() {
283 w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href"))
284 }
285
286 func (w *window) flushInput() {
287 val := w.tarea.Get("value").String()
288 w.tarea.Set("value", "")
289 w.w.Event(key.EditEvent{Text: string(val)})
290 }
291
292 func (w *window) blur() {
293 w.tarea.Call("blur")
294 w.requestFocus = false
295 }
296
297 func (w *window) focus() {
298 w.tarea.Call("focus")
299 w.requestFocus = true
300 }
301
302 func (w *window) keyEvent(e js.Value, ks key.State) {
303 k := e.Get("key").String()
304 if n, ok := translateKey(k); ok {
305 cmd := key.Event{
306 Name: n,
307 Modifiers: modifiersFor(e),
308 State: ks,
309 }
310 w.w.Event(cmd)
311 }
312 }
313
314 // modifiersFor returns the modifier set for a DOM MouseEvent or
315 // KeyEvent.
316 func modifiersFor(e js.Value) key.Modifiers {
317 var mods key.Modifiers
318 if e.Get("getModifierState").IsUndefined() {
319 // Some browsers doesn't support getModifierState.
320 return mods
321 }
322 if e.Call("getModifierState", "Alt").Bool() {
323 mods |= key.ModAlt
324 }
325 if e.Call("getModifierState", "Control").Bool() {
326 mods |= key.ModCtrl
327 }
328 if e.Call("getModifierState", "Shift").Bool() {
329 mods |= key.ModShift
330 }
331 return mods
332 }
333
334 func (w *window) touchEvent(typ pointer.Type, e js.Value) {
335 e.Call("preventDefault")
336 t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
337 changedTouches := e.Get("changedTouches")
338 n := changedTouches.Length()
339 rect := w.cnv.Call("getBoundingClientRect")
340 w.mu.Lock()
341 scale := w.scale
342 w.mu.Unlock()
343 var mods key.Modifiers
344 if e.Get("shiftKey").Bool() {
345 mods |= key.ModShift
346 }
347 if e.Get("altKey").Bool() {
348 mods |= key.ModAlt
349 }
350 if e.Get("ctrlKey").Bool() {
351 mods |= key.ModCtrl
352 }
353 for i := 0; i < n; i++ {
354 touch := changedTouches.Index(i)
355 pid := w.touchIDFor(touch)
356 x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float()
357 x -= rect.Get("left").Float()
358 y -= rect.Get("top").Float()
359 pos := f32.Point{
360 X: float32(x) * scale,
361 Y: float32(y) * scale,
362 }
363 w.w.Event(pointer.Event{
364 Type: typ,
365 Source: pointer.Touch,
366 Position: pos,
367 PointerID: pid,
368 Time: t,
369 Modifiers: mods,
370 })
371 }
372 }
373
374 func (w *window) touchIDFor(touch js.Value) pointer.ID {
375 id := touch.Get("identifier")
376 for i, id2 := range w.touches {
377 if id2.Equal(id) {
378 return pointer.ID(i)
379 }
380 }
381 pid := pointer.ID(len(w.touches))
382 w.touches = append(w.touches, id)
383 return pid
384 }
385
386 func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
387 e.Call("preventDefault")
388 x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
389 rect := w.cnv.Call("getBoundingClientRect")
390 x -= rect.Get("left").Float()
391 y -= rect.Get("top").Float()
392 w.mu.Lock()
393 scale := w.scale
394 w.mu.Unlock()
395 pos := f32.Point{
396 X: float32(x) * scale,
397 Y: float32(y) * scale,
398 }
399 scroll := f32.Point{
400 X: dx * scale,
401 Y: dy * scale,
402 }
403 t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
404 jbtns := e.Get("buttons").Int()
405 var btns pointer.Buttons
406 if jbtns&1 != 0 {
407 btns |= pointer.ButtonPrimary
408 }
409 if jbtns&2 != 0 {
410 btns |= pointer.ButtonSecondary
411 }
412 if jbtns&4 != 0 {
413 btns |= pointer.ButtonTertiary
414 }
415 w.w.Event(pointer.Event{
416 Type: typ,
417 Source: pointer.Mouse,
418 Buttons: btns,
419 Position: pos,
420 Scroll: scroll,
421 Time: t,
422 Modifiers: modifiersFor(e),
423 })
424 }
425
426 func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) {
427 jsf := w.funcOf(f)
428 this.Call("addEventListener", event, jsf)
429 w.cleanfuncs = append(w.cleanfuncs, func() {
430 this.Call("removeEventListener", event, jsf)
431 })
432 }
433
434 // funcOf is like js.FuncOf but adds the js.Func to a list of
435 // functions to be released during cleanup.
436 func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func {
437 jsf := js.FuncOf(f)
438 w.cleanfuncs = append(w.cleanfuncs, jsf.Release)
439 return jsf
440 }
441
442 func (w *window) animCallback() {
443 w.mu.Lock()
444 anim := w.animating
445 w.animRequested = anim
446 if anim {
447 w.requestAnimationFrame.Invoke(w.redraw)
448 }
449 w.mu.Unlock()
450 if anim {
451 w.draw(false)
452 }
453 }
454
455 func (w *window) SetAnimating(anim bool) {
456 w.mu.Lock()
457 defer w.mu.Unlock()
458 w.animating = anim
459 if anim && !w.animRequested {
460 w.animRequested = true
461 w.requestAnimationFrame.Invoke(w.redraw)
462 }
463 }
464
465 func (w *window) ReadClipboard() {
466 if w.clipboard.IsUndefined() {
467 return
468 }
469 if w.clipboard.Get("readText").IsUndefined() {
470 return
471 }
472 w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
473 }
474
475 func (w *window) WriteClipboard(s string) {
476 if w.clipboard.IsUndefined() {
477 return
478 }
479 if w.clipboard.Get("writeText").IsUndefined() {
480 return
481 }
482 w.clipboard.Call("writeText", s)
483 }
484
485 func (w *window) Option(opts *Options) {
486 if o := opts.WindowMode; o != nil {
487 w.windowMode(*o)
488 }
489 }
490
491 func (w *window) SetCursor(name pointer.CursorName) {
492 style := w.cnv.Get("style")
493 style.Set("cursor", string(name))
494 }
495
496 func (w *window) ShowTextInput(show bool) {
497 // Run in a goroutine to avoid a deadlock if the
498 // focus change result in an event.
499 go func() {
500 if show {
501 w.focus()
502 } else {
503 w.blur()
504 }
505 }()
506 }
507
508 // Close the window. Not implemented for js.
509 func (w *window) Close() {}
510
511 func (w *window) resize() {
512 w.mu.Lock()
513 defer w.mu.Unlock()
514
515 w.scale = float32(w.window.Get("devicePixelRatio").Float())
516
517 rect := w.cnv.Call("getBoundingClientRect")
518 w.size.X = float32(rect.Get("width").Float()) * w.scale
519 w.size.Y = float32(rect.Get("height").Float()) * w.scale
520
521 if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
522 w.inset.X = w.size.X - float32(vx.Float())*w.scale
523 w.inset.Y = w.size.Y - float32(vy.Float())*w.scale
524 }
525
526 if w.size.X == 0 || w.size.Y == 0 {
527 return
528 }
529
530 w.cnv.Set("width", int(w.size.X+.5))
531 w.cnv.Set("height", int(w.size.Y+.5))
532 }
533
534 func (w *window) draw(sync bool) {
535 width, height, insets, metric := w.config()
536 if metric == (unit.Metric{}) || width == 0 || height == 0 {
537 return
538 }
539
540 w.w.Event(FrameEvent{
541 FrameEvent: system.FrameEvent{
542 Now: time.Now(),
543 Size: image.Point{
544 X: width,
545 Y: height,
546 },
547 Insets: insets,
548 Metric: metric,
549 },
550 Sync: sync,
551 })
552 }
553
554 func (w *window) config() (int, int, system.Insets, unit.Metric) {
555 w.mu.Lock()
556 defer w.mu.Unlock()
557
558 return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{
559 Bottom: unit.Px(w.inset.Y),
560 Right: unit.Px(w.inset.X),
561 }, unit.Metric{
562 PxPerDp: w.scale,
563 PxPerSp: w.scale,
564 }
565 }
566
567 func (w *window) windowMode(mode WindowMode) {
568 switch mode {
569 case Windowed:
570 if fs := w.document.Get("fullscreenElement"); !fs.Truthy() {
571 return // Browser is already Windowed.
572 }
573 if !w.document.Get("exitFullscreen").Truthy() {
574 return // Browser doesn't support such feature.
575 }
576 w.document.Call("exitFullscreen")
577 case Fullscreen:
578 elem := w.document.Get("documentElement")
579 if !elem.Get("requestFullscreen").Truthy() {
580 return // Browser doesn't support such feature.
581 }
582 elem.Call("requestFullscreen")
583 }
584 }
585
586 func Main() {
587 select {}
588 }
589
590 func translateKey(k string) (string, bool) {
591 var n string
592 switch k {
593 case "ArrowUp":
594 n = key.NameUpArrow
595 case "ArrowDown":
596 n = key.NameDownArrow
597 case "ArrowLeft":
598 n = key.NameLeftArrow
599 case "ArrowRight":
600 n = key.NameRightArrow
601 case "Escape":
602 n = key.NameEscape
603 case "Enter":
604 n = key.NameReturn
605 case "Backspace":
606 n = key.NameDeleteBackward
607 case "Delete":
608 n = key.NameDeleteForward
609 case "Home":
610 n = key.NameHome
611 case "End":
612 n = key.NameEnd
613 case "PageUp":
614 n = key.NamePageUp
615 case "PageDown":
616 n = key.NamePageDown
617 case "Tab":
618 n = key.NameTab
619 case " ":
620 n = key.NameSpace
621 case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12":
622 n = k
623 default:
624 r, s := utf8.DecodeRuneInString(k)
625 // If there is exactly one printable character, return that.
626 if s == len(k) && unicode.IsPrint(r) {
627 return strings.ToUpper(k), true
628 }
629 return "", false
630 }
631 return n, true
632 }
633