github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/app/internal/window/os_js.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package window
     4  
     5  import (
     6  	"image"
     7  	"strings"
     8  	"sync"
     9  	"syscall/js"
    10  	"time"
    11  	"unicode"
    12  	"unicode/utf8"
    13  
    14  	"github.com/gop9/olt/gio/f32"
    15  	"github.com/gop9/olt/gio/io/key"
    16  	"github.com/gop9/olt/gio/io/pointer"
    17  	"github.com/gop9/olt/gio/io/system"
    18  )
    19  
    20  type window struct {
    21  	window                js.Value
    22  	cnv                   js.Value
    23  	tarea                 js.Value
    24  	w                     Callbacks
    25  	redraw                js.Func
    26  	requestAnimationFrame js.Value
    27  	cleanfuncs            []func()
    28  	touches               []js.Value
    29  	composing             bool
    30  
    31  	mu        sync.Mutex
    32  	scale     float32
    33  	animating bool
    34  }
    35  
    36  var mainDone = make(chan struct{})
    37  
    38  func NewWindow(win Callbacks, opts *Options) error {
    39  	doc := js.Global().Get("document")
    40  	cont := getContainer(doc)
    41  	cnv := createCanvas(doc)
    42  	cont.Call("appendChild", cnv)
    43  	tarea := createTextArea(doc)
    44  	cont.Call("appendChild", tarea)
    45  	w := &window{
    46  		cnv:    cnv,
    47  		tarea:  tarea,
    48  		window: js.Global().Get("window"),
    49  	}
    50  	w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
    51  	w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
    52  		w.animCallback()
    53  		return nil
    54  	})
    55  	w.addEventListeners()
    56  	w.w = win
    57  	go func() {
    58  		w.w.SetDriver(w)
    59  		w.focus()
    60  		w.w.Event(system.StageEvent{Stage: system.StageRunning})
    61  		w.draw(true)
    62  		select {}
    63  		w.cleanup()
    64  		close(mainDone)
    65  	}()
    66  	return nil
    67  }
    68  
    69  func getContainer(doc js.Value) js.Value {
    70  	cont := doc.Call("getElementById", "giowindow")
    71  	if !cont.IsNull() {
    72  		return cont
    73  	}
    74  	cont = doc.Call("createElement", "DIV")
    75  	doc.Get("body").Call("appendChild", cont)
    76  	return cont
    77  }
    78  
    79  func createTextArea(doc js.Value) js.Value {
    80  	tarea := doc.Call("createElement", "input")
    81  	style := tarea.Get("style")
    82  	style.Set("width", "1px")
    83  	style.Set("height", "1px")
    84  	style.Set("opacity", "0")
    85  	style.Set("border", "0")
    86  	style.Set("padding", "0")
    87  	tarea.Set("autocomplete", "off")
    88  	tarea.Set("autocorrect", "off")
    89  	tarea.Set("autocapitalize", "off")
    90  	tarea.Set("spellcheck", false)
    91  	return tarea
    92  }
    93  
    94  func createCanvas(doc js.Value) js.Value {
    95  	cnv := doc.Call("createElement", "canvas")
    96  	style := cnv.Get("style")
    97  	style.Set("position", "fixed")
    98  	style.Set("width", "100%")
    99  	style.Set("height", "100%")
   100  	return cnv
   101  }
   102  
   103  func (w *window) cleanup() {
   104  	// Cleanup in the opposite order of
   105  	// construction.
   106  	for i := len(w.cleanfuncs) - 1; i >= 0; i-- {
   107  		w.cleanfuncs[i]()
   108  	}
   109  	w.cleanfuncs = nil
   110  }
   111  
   112  func (w *window) addEventListeners() {
   113  	w.addEventListener(w.window, "resize", func(this js.Value, args []js.Value) interface{} {
   114  		w.draw(true)
   115  		return nil
   116  	})
   117  	w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
   118  		w.pointerEvent(pointer.Move, 0, 0, args[0])
   119  		return nil
   120  	})
   121  	w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} {
   122  		w.pointerEvent(pointer.Press, 0, 0, args[0])
   123  		return nil
   124  	})
   125  	w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} {
   126  		w.pointerEvent(pointer.Release, 0, 0, args[0])
   127  		return nil
   128  	})
   129  	w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} {
   130  		e := args[0]
   131  		dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float()
   132  		mode := e.Get("deltaMode").Int()
   133  		switch mode {
   134  		case 0x01: // DOM_DELTA_LINE
   135  			dx *= 10
   136  			dy *= 10
   137  		case 0x02: // DOM_DELTA_PAGE
   138  			dx *= 120
   139  			dy *= 120
   140  		}
   141  		w.pointerEvent(pointer.Move, float32(dx), float32(dy), e)
   142  		return nil
   143  	})
   144  	w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} {
   145  		w.touchEvent(pointer.Press, args[0])
   146  		return nil
   147  	})
   148  	w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} {
   149  		w.touchEvent(pointer.Release, args[0])
   150  		return nil
   151  	})
   152  	w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} {
   153  		w.touchEvent(pointer.Move, args[0])
   154  		return nil
   155  	})
   156  	w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} {
   157  		// Cancel all touches even if only one touch was cancelled.
   158  		for i := range w.touches {
   159  			w.touches[i] = js.Null()
   160  		}
   161  		w.touches = w.touches[:0]
   162  		w.w.Event(pointer.Event{
   163  			Type:   pointer.Cancel,
   164  			Source: pointer.Touch,
   165  		})
   166  		return nil
   167  	})
   168  	w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
   169  		w.w.Event(key.FocusEvent{Focus: true})
   170  		return nil
   171  	})
   172  	w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
   173  		w.w.Event(key.FocusEvent{Focus: false})
   174  		return nil
   175  	})
   176  	w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} {
   177  		w.keyEvent(args[0])
   178  		return nil
   179  	})
   180  	w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} {
   181  		w.composing = true
   182  		return nil
   183  	})
   184  	w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} {
   185  		w.composing = false
   186  		w.flushInput()
   187  		return nil
   188  	})
   189  	w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} {
   190  		if w.composing {
   191  			return nil
   192  		}
   193  		w.flushInput()
   194  		return nil
   195  	})
   196  }
   197  
   198  func (w *window) flushInput() {
   199  	val := w.tarea.Get("value").String()
   200  	w.tarea.Set("value", "")
   201  	w.w.Event(key.EditEvent{Text: string(val)})
   202  }
   203  
   204  func (w *window) blur() {
   205  	w.tarea.Call("blur")
   206  }
   207  
   208  func (w *window) focus() {
   209  	w.tarea.Call("focus")
   210  }
   211  
   212  func (w *window) keyEvent(e js.Value) {
   213  	k := e.Get("key").String()
   214  	if n, ok := translateKey(k); ok {
   215  		cmd := key.Event{Name: n}
   216  		if e.Call("getModifierState", "Alt").Bool() {
   217  			cmd.Modifiers |= key.ModAlt
   218  		}
   219  		if e.Call("getModifierState", "Control").Bool() {
   220  			cmd.Modifiers |= key.ModCtrl
   221  		}
   222  		if e.Call("getModifierState", "Shift").Bool() {
   223  			cmd.Modifiers |= key.ModShift
   224  		}
   225  		w.w.Event(cmd)
   226  	}
   227  }
   228  
   229  func (w *window) touchEvent(typ pointer.Type, e js.Value) {
   230  	e.Call("preventDefault")
   231  	t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
   232  	changedTouches := e.Get("changedTouches")
   233  	n := changedTouches.Length()
   234  	rect := w.cnv.Call("getBoundingClientRect")
   235  	w.mu.Lock()
   236  	scale := w.scale
   237  	w.mu.Unlock()
   238  	for i := 0; i < n; i++ {
   239  		touch := changedTouches.Index(i)
   240  		pid := w.touchIDFor(touch)
   241  		x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float()
   242  		x -= rect.Get("left").Float()
   243  		y -= rect.Get("top").Float()
   244  		pos := f32.Point{
   245  			X: float32(x) * scale,
   246  			Y: float32(y) * scale,
   247  		}
   248  		w.w.Event(pointer.Event{
   249  			Type:      typ,
   250  			Source:    pointer.Touch,
   251  			Position:  pos,
   252  			PointerID: pid,
   253  			Time:      t,
   254  		})
   255  	}
   256  }
   257  
   258  func (w *window) touchIDFor(touch js.Value) pointer.ID {
   259  	id := touch.Get("identifier")
   260  	for i, id2 := range w.touches {
   261  		if id2.Equal(id) {
   262  			return pointer.ID(i)
   263  		}
   264  	}
   265  	pid := pointer.ID(len(w.touches))
   266  	w.touches = append(w.touches, id)
   267  	return pid
   268  }
   269  
   270  func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
   271  	e.Call("preventDefault")
   272  	x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
   273  	rect := w.cnv.Call("getBoundingClientRect")
   274  	x -= rect.Get("left").Float()
   275  	y -= rect.Get("top").Float()
   276  	w.mu.Lock()
   277  	scale := w.scale
   278  	w.mu.Unlock()
   279  	pos := f32.Point{
   280  		X: float32(x) * scale,
   281  		Y: float32(y) * scale,
   282  	}
   283  	scroll := f32.Point{
   284  		X: dx * scale,
   285  		Y: dy * scale,
   286  	}
   287  	t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
   288  	jbtns := e.Get("buttons").Int()
   289  	var btns pointer.Buttons
   290  	if jbtns&1 != 0 {
   291  		btns |= pointer.ButtonLeft
   292  	}
   293  	if jbtns&2 != 0 {
   294  		btns |= pointer.ButtonRight
   295  	}
   296  	if jbtns&4 != 0 {
   297  		btns |= pointer.ButtonMiddle
   298  	}
   299  	w.w.Event(pointer.Event{
   300  		Type:     typ,
   301  		Source:   pointer.Mouse,
   302  		Buttons:  btns,
   303  		Position: pos,
   304  		Scroll:   scroll,
   305  		Time:     t,
   306  	})
   307  }
   308  
   309  func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) {
   310  	jsf := w.funcOf(f)
   311  	this.Call("addEventListener", event, jsf)
   312  	w.cleanfuncs = append(w.cleanfuncs, func() {
   313  		this.Call("removeEventListener", event, jsf)
   314  	})
   315  }
   316  
   317  // funcOf is like js.FuncOf but adds the js.Func to a list of
   318  // functions to be released up.
   319  func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func {
   320  	jsf := js.FuncOf(f)
   321  	w.cleanfuncs = append(w.cleanfuncs, jsf.Release)
   322  	return jsf
   323  }
   324  
   325  func (w *window) animCallback() {
   326  	w.mu.Lock()
   327  	anim := w.animating
   328  	if anim {
   329  		w.requestAnimationFrame.Invoke(w.redraw)
   330  	}
   331  	w.mu.Unlock()
   332  	if anim {
   333  		w.draw(false)
   334  	}
   335  }
   336  
   337  func (w *window) SetAnimating(anim bool) {
   338  	w.mu.Lock()
   339  	defer w.mu.Unlock()
   340  	if anim && !w.animating {
   341  		w.requestAnimationFrame.Invoke(w.redraw)
   342  	}
   343  	w.animating = anim
   344  }
   345  
   346  func (w *window) ShowTextInput(show bool) {
   347  	// Run in a goroutine to avoid a deadlock if the
   348  	// focus change result in an event.
   349  	go func() {
   350  		if show {
   351  			w.focus()
   352  		} else {
   353  			w.blur()
   354  		}
   355  	}()
   356  }
   357  
   358  func (w *window) draw(sync bool) {
   359  	width, height, scale, cfg := w.config()
   360  	if cfg == (config{}) || width == 0 || height == 0 {
   361  		return
   362  	}
   363  	w.mu.Lock()
   364  	w.scale = float32(scale)
   365  	w.mu.Unlock()
   366  	cfg.now = time.Now()
   367  	w.w.Event(FrameEvent{
   368  		FrameEvent: system.FrameEvent{
   369  			Size: image.Point{
   370  				X: width,
   371  				Y: height,
   372  			},
   373  			Config: &cfg,
   374  		},
   375  		Sync: sync,
   376  	})
   377  }
   378  
   379  func (w *window) config() (int, int, float32, config) {
   380  	rect := w.cnv.Call("getBoundingClientRect")
   381  	width, height := rect.Get("width").Float(), rect.Get("height").Float()
   382  	scale := w.window.Get("devicePixelRatio").Float()
   383  	width *= scale
   384  	height *= scale
   385  	iw, ih := int(width+.5), int(height+.5)
   386  	// Adjust internal size of canvas if necessary.
   387  	if cw, ch := w.cnv.Get("width").Int(), w.cnv.Get("height").Int(); iw != cw || ih != ch {
   388  		w.cnv.Set("width", iw)
   389  		w.cnv.Set("height", ih)
   390  	}
   391  	return iw, ih, float32(scale), config{
   392  		pxPerDp: float32(scale),
   393  		pxPerSp: float32(scale),
   394  	}
   395  }
   396  
   397  func Main() {
   398  	<-mainDone
   399  }
   400  
   401  func translateKey(k string) (string, bool) {
   402  	var n string
   403  	switch k {
   404  	case "ArrowUp":
   405  		n = key.NameUpArrow
   406  	case "ArrowDown":
   407  		n = key.NameDownArrow
   408  	case "ArrowLeft":
   409  		n = key.NameLeftArrow
   410  	case "ArrowRight":
   411  		n = key.NameRightArrow
   412  	case "Escape":
   413  		n = key.NameEscape
   414  	case "Enter":
   415  		n = key.NameReturn
   416  	case "Backspace":
   417  		n = key.NameDeleteBackward
   418  	case "Delete":
   419  		n = key.NameDeleteForward
   420  	case "Home":
   421  		n = key.NameHome
   422  	case "End":
   423  		n = key.NameEnd
   424  	case "PageUp":
   425  		n = key.NamePageUp
   426  	case "PageDown":
   427  		n = key.NamePageDown
   428  	case "Tab":
   429  		n = key.NameTab
   430  	case " ":
   431  		n = "Space"
   432  	case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12":
   433  		n = k
   434  	default:
   435  		r, s := utf8.DecodeRuneInString(k)
   436  		// If there is exactly one printable character, return that.
   437  		if s == len(k) && unicode.IsPrint(r) {
   438  			return strings.ToUpper(k), true
   439  		}
   440  		return "", false
   441  	}
   442  	return n, true
   443  }