gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/app/os_js.go (about)

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