// SPDX-License-Identifier: Unlicense OR MIT package app import ( "fmt" "image" "image/color" "io" "strings" "syscall/js" "time" "unicode" "unicode/utf8" "gioui.org/internal/f32color" "gioui.org/op" "gioui.org/f32" "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/io/transfer" "gioui.org/unit" ) type JSViewEvent struct { Element js.Value } type contextStatus int const ( contextStatusOkay contextStatus = iota contextStatusLost contextStatusRestored ) type window struct { window js.Value document js.Value head js.Value clipboard js.Value cnv js.Value tarea js.Value w *callbacks redraw js.Func clipboardCallback js.Func requestAnimationFrame js.Value browserHistory js.Value visualViewport js.Value screenOrientation js.Value cleanfuncs []func() touches []js.Value composing int lastCursor int requestFocus bool config Config inset f32.Point scale float32 animating bool // animRequested tracks whether a requestAnimationFrame callback // is pending. animRequested bool wakeups chan struct{} contextStatus contextStatus } func newWindow(win *callbacks, options []Option) { doc := js.Global().Get("document") cont := getContainer(doc) cnv := createCanvas(doc) cont.Call("appendChild", cnv) tarea := createTextArea(doc) cont.Call("appendChild", tarea) w := &window{ cnv: cnv, document: doc, tarea: tarea, window: js.Global().Get("window"), head: doc.Get("head"), clipboard: js.Global().Get("navigator").Get("clipboard"), wakeups: make(chan struct{}, 1), w: win, composing: -1, } w.w.SetDriver(w) w.requestAnimationFrame = w.window.Get("requestAnimationFrame") w.browserHistory = w.window.Get("history") w.visualViewport = w.window.Get("visualViewport") if w.visualViewport.IsUndefined() { w.visualViewport = w.window } if screen := w.window.Get("screen"); screen.Truthy() { w.screenOrientation = screen.Get("orientation") } w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { w.draw(false) return nil }) w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { content := args[0].String() w.processEvent(transfer.DataEvent{ Type: "application/text", Open: func() io.ReadCloser { return io.NopCloser(strings.NewReader(content)) }, }) return nil }) w.addEventListeners() w.addHistory() w.Configure(options) w.blur() w.processEvent(JSViewEvent{Element: cont}) w.resize() w.draw(true) } func getContainer(doc js.Value) js.Value { cont := doc.Call("getElementById", "giowindow") if !cont.IsNull() { return cont } cont = doc.Call("createElement", "DIV") doc.Get("body").Call("appendChild", cont) return cont } func createTextArea(doc js.Value) js.Value { tarea := doc.Call("createElement", "textarea") style := tarea.Get("style") // Position absolute so left/top coordinates actually place the element style.Set("position", "absolute") style.Set("width", "1px") style.Set("height", "1px") style.Set("opacity", "0") style.Set("border", "0") style.Set("padding", "0") tarea.Set("autocomplete", "off") tarea.Set("autocorrect", "off") tarea.Set("autocapitalize", "off") tarea.Set("spellcheck", false) // Enable multiline text input for better composition support on some browsers. tarea.Set("rows", 1) style.Set("resize", "none") style.Set("overflow", "hidden") style.Set("white-space", "pre-wrap") style.Set("word-break", "normal") return tarea } func createCanvas(doc js.Value) js.Value { cnv := doc.Call("createElement", "canvas") style := cnv.Get("style") style.Set("position", "fixed") style.Set("width", "100%") style.Set("height", "100%") return cnv } func (w *window) cleanup() { // Cleanup in the opposite order of // construction. for i := len(w.cleanfuncs) - 1; i >= 0; i-- { w.cleanfuncs[i]() } w.cleanfuncs = nil } func (w *window) addEventListeners() { w.addEventListener(w.cnv, "webglcontextlost", func(this js.Value, args []js.Value) interface{} { args[0].Call("preventDefault") w.contextStatus = contextStatusLost return nil }) w.addEventListener(w.cnv, "webglcontextrestored", func(this js.Value, args []js.Value) interface{} { args[0].Call("preventDefault") w.contextStatus = contextStatusRestored // Resize is required to force update the canvas content when restored. w.cnv.Set("width", 0) w.cnv.Set("height", 0) w.resize() w.draw(true) return nil }) w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} { w.resize() w.draw(true) return nil }) w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} { args[0].Call("preventDefault") return nil }) w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} { if w.processEvent(key.Event{Name: key.NameBack}) { return w.browserHistory.Call("forward") } return w.browserHistory.Call("back") }) w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} { w.pointerEvent(pointer.Move, 0, 0, args[0]) return nil }) w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} { w.pointerEvent(pointer.Press, 0, 0, args[0]) if w.requestFocus { w.focus() w.requestFocus = false } return nil }) w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} { w.pointerEvent(pointer.Release, 0, 0, args[0]) return nil }) w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} { e := args[0] dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() // horizontal scroll if shift is pressed. if e.Get("shiftKey").Bool() { dx, dy = dy, dx } mode := e.Get("deltaMode").Int() switch mode { case 0x01: // DOM_DELTA_LINE dx *= 10 dy *= 10 case 0x02: // DOM_DELTA_PAGE dx *= 120 dy *= 120 } w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e) return nil }) w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} { w.touchEvent(pointer.Press, args[0]) if w.requestFocus { w.focus() // iOS can only focus inside a Touch event. w.requestFocus = false } return nil }) w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} { w.touchEvent(pointer.Release, args[0]) return nil }) w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} { w.touchEvent(pointer.Move, args[0]) return nil }) w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} { // Cancel all touches even if only one touch was cancelled. for i := range w.touches { w.touches[i] = js.Null() } w.touches = w.touches[:0] w.processEvent(pointer.Event{ Kind: pointer.Cancel, Source: pointer.Touch, }) return nil }) w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { w.config.Focused = true w.processEvent(ConfigEvent{Config: w.config}) return nil }) w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { if w.composing != -1 { // If we're composing, try to cancel. // On Javascript is not possible to cancel the composition once started. w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) w.composing = -1 } w.config.Focused = false w.lastCursor = 0 // Reset cursor tracking on blur w.processEvent(ConfigEvent{Config: w.config}) w.blur() return nil }) w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} { w.keyEvent(args[0], key.Press) return nil }) w.addEventListener(w.tarea, "keyup", func(this js.Value, args []js.Value) interface{} { w.keyEvent(args[0], key.Release) return nil }) w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} { st := w.w.EditorState() sel := st.Selection.Range if sel.Start == -1 { sel.Start = 0 sel.End = 0 } w.w.SetEditorSnippet(key.Range{Start: sel.Start, End: sel.End}) w.composing = sel.Start return nil }) w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} { finalText := w.tarea.Get("value").String() if w.composing != -1 && finalText != "" { // Replace the entire composition range with the final text. compEnd := w.composing + utf8.RuneCountInString(finalText) replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, finalText) w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) // Position cursor after the final composition text. newEnd := w.composing + utf8.RuneCountInString(finalText) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) } w.composing = -1 w.tarea.Set("value", "") return nil }) w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} { e := args[0] inputType := e.Get("inputType").String() dataVal := e.Get("data") var data string if dataVal.Truthy() { data = dataVal.String() } // Get the current textarea value. tareaValue := w.tarea.Get("value").String() st := w.w.EditorState() sel := st.Selection.Range var absStart, absEnd int snippetStart := st.Snippet.Range.Start snippetEnd := st.Snippet.Range.End cursorPos := sel.Start selectionEnd := sel.End if cursorPos < 0 { cursorPos = 0 selectionEnd = 0 } // Check if we need to expand the snippet to include the range. if st.Snippet.Range.Start == 0 && st.Snippet.Range.End == 0 && tareaValue != "" { // Empty snippet - set it to include the selection/cursor. w.w.SetEditorSnippet(key.Range{Start: cursorPos, End: selectionEnd}) absStart = cursorPos absEnd = selectionEnd } else if cursorPos < snippetStart || selectionEnd > snippetEnd { // Selection is outside the snippet newStart := snippetStart newEnd := snippetEnd if cursorPos < newStart { newStart = cursorPos } if selectionEnd > newEnd { newEnd = selectionEnd } w.w.SetEditorSnippet(key.Range{Start: newStart, End: newEnd}) // Refresh state after snippet update. st = w.w.EditorState() // Use the selection range directly. absStart = cursorPos absEnd = selectionEnd } else { // Selection is within snippet to absolute positions. absStart = cursorPos absEnd = selectionEnd } switch inputType { case "insertCompositionText": if w.composing == -1 { break } compEnd := absEnd if compEnd < w.composing { compEnd = w.composing } replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, data) newEnd := w.composing + utf8.RuneCountInString(data) w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd}) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) case "deleteContentBackward", "deleteContentForward", "deleteByCut": if w.composing != -1 { compEnd := w.composing + utf8.RuneCountInString(tareaValue) replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, tareaValue) newEnd := w.composing + utf8.RuneCountInString(tareaValue) w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd}) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) } else { replaceRange := key.Range{Start: absStart, End: absEnd} w.w.EditorReplace(replaceRange, "") w.w.SetEditorSelection(key.Range{Start: absStart, End: absStart}) } case "insertReplacementText": if w.composing != -1 { // During composition, replace the entire composition. compEnd := w.composing + utf8.RuneCountInString(data) replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, data) newEnd := w.composing + utf8.RuneCountInString(data) w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) w.composing = -1 w.lastCursor = newEnd } else { // Safari sends "insertReplacementText" for autocorrect, but the cursor is at the end of the word, so we need to find the word start. insertLen := utf8.RuneCountInString(data) wordStart := absStart if absStart > snippetStart { relPos := absStart - snippetStart snippetRunes := []rune(st.Snippet.Text) for i := relPos - 1; i >= 0; i-- { if i >= len(snippetRunes) { continue } r := snippetRunes[i] if r == ' ' || r == '\t' || r == '\n' || r == '\r' { break } wordStart = snippetStart + i } } replaceRange := key.Range{Start: wordStart, End: absStart} w.w.EditorReplace(replaceRange, data) newCursor := wordStart + insertLen w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor}) w.lastCursor = newCursor } case "insertText": if w.composing != -1 { compEnd := w.composing + utf8.RuneCountInString(data) replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, data) newEnd := w.composing + utf8.RuneCountInString(data) w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) w.composing = -1 w.lastCursor = newEnd } else { insertLen := utf8.RuneCountInString(data) replaceRange := key.Range{Start: absStart, End: absStart} if absStart != absEnd { replaceRange = key.Range{Start: absStart, End: absEnd} } newCursor := replaceRange.Start + insertLen w.w.EditorReplace(replaceRange, data) w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor}) w.lastCursor = newCursor } default: // paste and other input types if w.composing != -1 { compEnd := w.composing + utf8.RuneCountInString(tareaValue) replaceRange := key.Range{Start: w.composing, End: compEnd} w.w.EditorReplace(replaceRange, tareaValue) newEnd := w.composing + utf8.RuneCountInString(tareaValue) w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd}) w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd}) } else { replaceRange := key.Range{Start: absStart, End: absEnd} w.w.EditorReplace(replaceRange, tareaValue) newCursor := absStart + utf8.RuneCountInString(tareaValue) w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor}) } } return nil }) w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} { if w.clipboard.IsUndefined() { return nil } // Prevents duplicated-paste, since "paste" is already handled through Clipboard API. args[0].Call("preventDefault") return nil }) } func (w *window) addHistory() { w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href")) } func (w *window) blur() { w.tarea.Call("blur") w.requestFocus = false } func (w *window) focus() { w.tarea.Call("focus") w.requestFocus = true } func (w *window) keyboard(hint key.InputHint) { var m string switch hint { case key.HintAny: m = "text" case key.HintText: m = "text" case key.HintNumeric: m = "decimal" case key.HintEmail: m = "email" case key.HintURL: m = "url" case key.HintTelephone: m = "tel" case key.HintPassword: m = "password" default: m = "text" } w.tarea.Set("inputMode", m) // Update autocomplete / autocorrect attributes. var autocomplete, autocorrect, autocapitalize string var spellcheck bool switch hint { case key.HintAny, key.HintText: autocomplete, autocorrect, autocapitalize, spellcheck = "on", "on", "on", true case key.HintEmail: autocomplete, autocorrect, autocapitalize, spellcheck = "email", "off", "off", false case key.HintURL: autocomplete, autocorrect, autocapitalize, spellcheck = "url", "off", "off", false case key.HintTelephone: autocomplete, autocorrect, autocapitalize, spellcheck = "tel", "off", "off", false case key.HintPassword: autocomplete, autocorrect, autocapitalize, spellcheck = "current-password", "off", "off", false default: // key.HintNumeric and others autocomplete, autocorrect, autocapitalize, spellcheck = "off", "off", "off", false } w.tarea.Set("autocomplete", autocomplete) w.tarea.Set("autocorrect", autocorrect) w.tarea.Set("autocapitalize", autocapitalize) w.tarea.Set("spellcheck", spellcheck) } func (w *window) keyEvent(e js.Value, ks key.State) { k := e.Get("key").String() if n, ok := translateKey(k); ok { if ks == key.Press { isMod := n == key.NameAlt || n == key.NameCommand || n == key.NameCtrl || n == key.NameShift || n == key.NameSuper isFunc := n == key.NameUpArrow || n == key.NameDownArrow || n == key.NameLeftArrow || n == key.NameRightArrow || n == key.NamePageUp || n == key.NamePageDown || n == key.NameHome || n == key.NameEnd || n == key.NameEscape || n == key.NameReturn || n == key.NameEnter || n == key.NameTab || n == key.NameDeleteBackward || n == key.NameDeleteForward if isMod || isFunc { // Gio will request the browser to change the selection/carret position natively. e.Call("preventDefault") } } cmd := key.Event{ Name: n, Modifiers: modifiersFor(e), State: ks, } w.processEvent(cmd) } } func (w *window) ProcessEvent(e event.Event) { w.processEvent(e) } func (w *window) processEvent(e event.Event) bool { if !w.w.ProcessEvent(e) { return false } select { case w.wakeups <- struct{}{}: default: } return true } func (w *window) Event() event.Event { for { evt, ok := w.w.nextEvent() if ok { if _, destroy := evt.(DestroyEvent); destroy { w.cleanup() } return evt } <-w.wakeups } } func (w *window) Invalidate() { w.w.Invalidate() } func (w *window) Run(f func()) { f() } func (w *window) Frame(frame *op.Ops) { w.w.ProcessFrame(frame, nil) } // modifiersFor returns the modifier set for a DOM MouseEvent or // KeyEvent. func modifiersFor(e js.Value) key.Modifiers { var mods key.Modifiers if e.Get("getModifierState").IsUndefined() { // Some browsers doesn't support getModifierState. return mods } if e.Call("getModifierState", "Alt").Bool() { mods |= key.ModAlt } if e.Call("getModifierState", "Control").Bool() { mods |= key.ModCtrl } if e.Call("getModifierState", "Shift").Bool() { mods |= key.ModShift } if e.Call("getModifierState", "Meta").Bool() { mods |= key.ModCommand } if e.Call("getModifierState", "OS").Bool() { mods |= key.ModSuper } return mods } func (w *window) touchEvent(kind pointer.Kind, e js.Value) { e.Call("preventDefault") t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond changedTouches := e.Get("changedTouches") n := changedTouches.Length() rect := w.cnv.Call("getBoundingClientRect") scale := w.scale var mods key.Modifiers if e.Get("shiftKey").Bool() { mods |= key.ModShift } if e.Get("altKey").Bool() { mods |= key.ModAlt } if e.Get("ctrlKey").Bool() { mods |= key.ModCtrl } if e.Get("metaKey").Bool() { mods |= key.ModCommand } for i := 0; i < n; i++ { touch := changedTouches.Index(i) pid := w.touchIDFor(touch) x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() x -= rect.Get("left").Float() y -= rect.Get("top").Float() pos := f32.Point{ X: float32(x) * scale, Y: float32(y) * scale, } w.processEvent(pointer.Event{ Kind: kind, Source: pointer.Touch, Position: pos, PointerID: pid, Time: t, Modifiers: mods, }) } } func (w *window) touchIDFor(touch js.Value) pointer.ID { id := touch.Get("identifier") for i, id2 := range w.touches { if id2.Equal(id) { return pointer.ID(i) } } pid := pointer.ID(len(w.touches)) w.touches = append(w.touches, id) return pid } func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) { e.Call("preventDefault") x, y := e.Get("clientX").Float(), e.Get("clientY").Float() rect := w.cnv.Call("getBoundingClientRect") x -= rect.Get("left").Float() y -= rect.Get("top").Float() scale := w.scale pos := f32.Point{ X: float32(x) * scale, Y: float32(y) * scale, } scroll := f32.Point{ X: dx * scale, Y: dy * scale, } t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond jbtns := e.Get("buttons").Int() var btns pointer.Buttons if jbtns&1 != 0 { btns |= pointer.ButtonPrimary } if jbtns&2 != 0 { btns |= pointer.ButtonSecondary } if jbtns&4 != 0 { btns |= pointer.ButtonTertiary } w.processEvent(pointer.Event{ Kind: kind, Source: pointer.Mouse, Buttons: btns, Position: pos, Scroll: scroll, Time: t, Modifiers: modifiersFor(e), }) } func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) { jsf := w.funcOf(f) this.Call("addEventListener", event, jsf) w.cleanfuncs = append(w.cleanfuncs, func() { this.Call("removeEventListener", event, jsf) }) } // funcOf is like js.FuncOf but adds the js.Func to a list of // functions to be released during cleanup. func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func { jsf := js.FuncOf(f) w.cleanfuncs = append(w.cleanfuncs, jsf.Release) return jsf } func (w *window) EditorStateChanged(old, new editorState) { if w.composing != -1 { // Do not interfere with browser state while composing. // On Javascript is not possible to cancel the composition once started! return } // Update textarea value to match the snippet. if old.Snippet != new.Snippet { w.tarea.Set("value", new.Snippet.Text) } // Update selection to match Gio's selection. if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet { if new.Selection.Range.Start != -1 && new.Selection.Range.End != -1 { // Calculate selection positions relative to snippet start. // The textarea contains only the snippet text. snippetStart := new.Snippet.Range.Start snippetEnd := new.Snippet.Range.End selStart := new.Selection.Range.Start selEnd := new.Selection.Range.End if selStart < snippetStart { selStart = snippetStart } if selStart > snippetEnd { selStart = snippetEnd } if selEnd < snippetStart { selEnd = snippetStart } if selEnd > snippetEnd { selEnd = snippetEnd } // Convert absolute rune positions to UTF-16 positions for the textarea. startUTF16 := new.UTF16Index(selStart) endUTF16 := new.UTF16Index(selEnd) // Convert to snippet-relative UTF-16 positions. snippetStartUTF16 := new.UTF16Index(snippetStart) start := startUTF16 - snippetStartUTF16 end := endUTF16 - snippetStartUTF16 if start < 0 { start = 0 } if end < 0 { end = 0 } // Calculate max UTF-16 length of snippet text. textLen := new.UTF16Index(snippetEnd) - snippetStartUTF16 if start > textLen { start = textLen } if end > textLen { end = textLen } if start > end { start, end = end, start } w.tarea.Set("selectionStart", start) w.tarea.Set("selectionEnd", end) } } // Move DOM element to position the caret. if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform { pos := new.Selection.Transform.Transform(new.Selection.Caret.Pos.Add(f32.Pt(0, new.Selection.Caret.Descent))) bounds := w.cnv.Call("getBoundingClientRect") left := bounds.Get("left").Float() + float64(pos.X)/float64(w.scale) top := bounds.Get("top").Float() + float64(pos.Y-new.Selection.Caret.Ascent)/float64(w.scale) height := float64(new.Selection.Caret.Ascent+new.Selection.Caret.Descent) / float64(w.scale) style := w.tarea.Get("style") style.Set("left", fmt.Sprintf("%fpx", left)) style.Set("top", fmt.Sprintf("%fpx", top)) style.Set("height", fmt.Sprintf("%fpx", height)) style.Set("width", "1px") } } func (w *window) SetAnimating(anim bool) { w.animating = anim if anim && !w.animRequested { w.animRequested = true w.requestAnimationFrame.Invoke(w.redraw) } } func (w *window) ReadClipboard() { if w.clipboard.IsUndefined() { return } if w.clipboard.Get("readText").Truthy() { w.clipboard.Call("readText").Call("then", w.clipboardCallback) } } func (w *window) WriteClipboard(mime string, s []byte) { if w.clipboard.IsUndefined() { return } if w.clipboard.Get("writeText").IsUndefined() { return } w.clipboard.Call("writeText", string(s)) } func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) // Decorations are never disabled. cnf.Decorated = true if prev.Title != cnf.Title { w.config.Title = cnf.Title w.document.Set("title", cnf.Title) } if prev.Mode != cnf.Mode { w.windowMode(cnf.Mode) } if prev.NavigationColor != cnf.NavigationColor { w.config.NavigationColor = cnf.NavigationColor w.navigationColor(cnf.NavigationColor) } if prev.Orientation != cnf.Orientation { w.config.Orientation = cnf.Orientation w.orientation(cnf.Orientation) } if cnf.Decorated != prev.Decorated { w.config.Decorated = cnf.Decorated } w.processEvent(ConfigEvent{Config: w.config}) } func (w *window) Perform(system.Action) {} var webCursor = [...]string{ pointer.CursorDefault: "default", pointer.CursorNone: "none", pointer.CursorText: "text", pointer.CursorVerticalText: "vertical-text", pointer.CursorPointer: "pointer", pointer.CursorCrosshair: "crosshair", pointer.CursorAllScroll: "all-scroll", pointer.CursorColResize: "col-resize", pointer.CursorRowResize: "row-resize", pointer.CursorGrab: "grab", pointer.CursorGrabbing: "grabbing", pointer.CursorNotAllowed: "not-allowed", pointer.CursorWait: "wait", pointer.CursorProgress: "progress", pointer.CursorNorthWestResize: "nw-resize", pointer.CursorNorthEastResize: "ne-resize", pointer.CursorSouthWestResize: "sw-resize", pointer.CursorSouthEastResize: "se-resize", pointer.CursorNorthSouthResize: "ns-resize", pointer.CursorEastWestResize: "ew-resize", pointer.CursorWestResize: "w-resize", pointer.CursorEastResize: "e-resize", pointer.CursorNorthResize: "n-resize", pointer.CursorSouthResize: "s-resize", pointer.CursorNorthEastSouthWestResize: "nesw-resize", pointer.CursorNorthWestSouthEastResize: "nwse-resize", } func (w *window) SetCursor(cursor pointer.Cursor) { style := w.cnv.Get("style") style.Set("cursor", webCursor[cursor]) } func (w *window) ShowTextInput(show bool) { // Run in a goroutine to avoid a deadlock if the // focus change result in an event. if show { w.focus() } else { // If we're composing, end composition first by clearing the textarea. // That is a attempt to force the browser to end composition. if w.composing != -1 { w.tarea.Set("value", "") w.composing = -1 w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) } w.blur() } } func (w *window) SetInputHint(mode key.InputHint) { w.keyboard(mode) } func (w *window) resize() { w.scale = float32(w.window.Get("devicePixelRatio").Float()) rect := w.cnv.Call("getBoundingClientRect") size := image.Point{ X: int(float32(rect.Get("width").Float()) * w.scale), Y: int(float32(rect.Get("height").Float()) * w.scale), } if size != w.config.Size { w.config.Size = size w.processEvent(ConfigEvent{Config: w.config}) } if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { w.inset.X = float32(w.config.Size.X) - float32(vx.Float())*w.scale w.inset.Y = float32(w.config.Size.Y) - float32(vy.Float())*w.scale } if w.config.Size.X == 0 || w.config.Size.Y == 0 { return } w.cnv.Set("width", w.config.Size.X) w.cnv.Set("height", w.config.Size.Y) } func (w *window) draw(sync bool) { if w.contextStatus == contextStatusLost { return } anim := w.animating w.animRequested = anim if anim { w.requestAnimationFrame.Invoke(w.redraw) } else if !sync { return } size, insets, metric := w.getConfig() if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 { return } w.processEvent(frameEvent{ FrameEvent: FrameEvent{ Now: time.Now(), Size: size, Insets: insets, Metric: metric, }, Sync: sync, }) } func (w *window) getConfig() (image.Point, Insets, unit.Metric) { invscale := unit.Dp(1. / w.scale) return image.Pt(w.config.Size.X, w.config.Size.Y), Insets{ Bottom: unit.Dp(w.inset.Y) * invscale, Right: unit.Dp(w.inset.X) * invscale, }, unit.Metric{ PxPerDp: w.scale, PxPerSp: w.scale, } } func (w *window) windowMode(mode WindowMode) { switch mode { case Windowed: if !w.document.Get("fullscreenElement").Truthy() { return // Browser is already Windowed. } if !w.document.Get("exitFullscreen").Truthy() { return // Browser doesn't support such feature. } w.document.Call("exitFullscreen") w.config.Mode = Windowed case Fullscreen: elem := w.document.Get("documentElement") if !elem.Get("requestFullscreen").Truthy() { return // Browser doesn't support such feature. } elem.Call("requestFullscreen") w.config.Mode = Fullscreen } } func (w *window) orientation(mode Orientation) { if j := w.screenOrientation; !j.Truthy() || !j.Get("unlock").Truthy() || !j.Get("lock").Truthy() { return // Browser don't support Screen Orientation API. } switch mode { case AnyOrientation: w.screenOrientation.Call("unlock") case LandscapeOrientation: w.screenOrientation.Call("lock", "landscape").Call("then", w.redraw) case PortraitOrientation: w.screenOrientation.Call("lock", "portrait").Call("then", w.redraw) } } func (w *window) navigationColor(c color.NRGBA) { theme := w.head.Call("querySelector", `meta[name="theme-color"]`) if !theme.Truthy() { theme = w.document.Call("createElement", "meta") theme.Set("name", "theme-color") w.head.Call("appendChild", theme) } rgba := f32color.NRGBAToRGBA(c) theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B})) } func osMain() { select {} } func translateKey(k string) (key.Name, bool) { var n key.Name switch k { case "ArrowUp": n = key.NameUpArrow case "ArrowDown": n = key.NameDownArrow case "ArrowLeft": n = key.NameLeftArrow case "ArrowRight": n = key.NameRightArrow case "Escape": n = key.NameEscape case "Enter": n = key.NameReturn case "Backspace": n = key.NameDeleteBackward case "Delete": n = key.NameDeleteForward case "Home": n = key.NameHome case "End": n = key.NameEnd case "PageUp": n = key.NamePageUp case "PageDown": n = key.NamePageDown case "Tab": n = key.NameTab case " ": n = key.NameSpace case "F1": n = key.NameF1 case "F2": n = key.NameF2 case "F3": n = key.NameF3 case "F4": n = key.NameF4 case "F5": n = key.NameF5 case "F6": n = key.NameF6 case "F7": n = key.NameF7 case "F8": n = key.NameF8 case "F9": n = key.NameF9 case "F10": n = key.NameF10 case "F11": n = key.NameF11 case "F12": n = key.NameF12 case "Control": n = key.NameCtrl case "Shift": n = key.NameShift case "Alt": n = key.NameAlt case "OS": n = key.NameSuper default: r, s := utf8.DecodeRuneInString(k) // If there is exactly one printable character, return that. if s == len(k) && unicode.IsPrint(r) { return key.Name(strings.ToUpper(k)), true } return "", false } return n, true } func (JSViewEvent) implementsViewEvent() {} func (JSViewEvent) ImplementsEvent() {} func (j JSViewEvent) Valid() bool { return !(j.Element.IsNull() || j.Element.IsUndefined()) }