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