package renderer import ( "encoding/json" "fmt" "math/rand" "strings" "sync" "time" "github.com/wailsapp/wails/runtime" "github.com/go-playground/colors" "github.com/wailsapp/wails/lib/interfaces" "github.com/wailsapp/wails/lib/logger" "github.com/wailsapp/wails/lib/messages" wv "github.com/wailsapp/wails/lib/renderer/webview" ) // WebView defines the main webview application window // Default values in [] // UseFirebug indicates whether to inject the firebug console var UseFirebug = "" type WebView struct { window wv.WebView // The webview object ipc interfaces.IPCManager log *logger.CustomLogger config interfaces.AppConfig eventManager interfaces.EventManager bindingCache []string maximumSizeSet bool } // NewWebView returns a new WebView struct func NewWebView() *WebView { return &WebView{} } // Initialise sets up the WebView func (w *WebView) Initialise(config interfaces.AppConfig, ipc interfaces.IPCManager, eventManager interfaces.EventManager) error { // Store reference to eventManager w.eventManager = eventManager // Set up logger w.log = logger.NewCustomLogger("WebView") // Set up the dispatcher function w.ipc = ipc ipc.BindRenderer(w) // Save the config w.config = config width := config.GetWidth() height := config.GetHeight() // Clamp width and height minWidth, minHeight := config.GetMinWidth(), config.GetMinHeight() maxWidth, maxHeight := config.GetMaxWidth(), config.GetMaxHeight() setMinSize := minWidth != -1 && minHeight != -1 setMaxSize := maxWidth != -1 && maxHeight != -1 if setMinSize { if width < minWidth { width = minWidth } if height < minHeight { height = minHeight } } if setMaxSize { if width > maxWidth { width = maxWidth } if height > maxHeight { height = maxHeight } } // Create the WebView instance w.window = wv.NewWebview(wv.Settings{ Width: width, Height: height, Title: config.GetTitle(), Resizable: config.GetResizable(), URL: config.GetHTML(), Debug: !config.GetDisableInspector(), ExternalInvokeCallback: func(_ wv.WebView, message string) { w.ipc.Dispatch(message, w.callback) }, }) // Set minimum and maximum sizes if setMinSize { w.SetMinSize(minWidth, minHeight) } if setMaxSize { w.SetMaxSize(maxWidth, maxHeight) } // Set minimum and maximum sizes if setMinSize { w.SetMinSize(minWidth, minHeight) } if setMaxSize { w.SetMaxSize(maxWidth, maxHeight) } // SignalManager.OnExit(w.Exit) // Set colour color := config.GetColour() if color != "" { err := w.SetColour(color) if err != nil { return err } } w.log.Info("Initialised") return nil } // SetColour sets the window colour func (w *WebView) SetColour(colour string) error { color, err := colors.Parse(colour) if err != nil { return err } rgba := color.ToRGBA() alpha := uint8(255 * rgba.A) w.window.Dispatch(func() { w.window.SetColor(rgba.R, rgba.G, rgba.B, alpha) }) return nil } // evalJS evaluates the given js in the WebView // I should rename this to evilJS lol func (w *WebView) evalJS(js string) error { outputJS := fmt.Sprintf("%.45s", js) if len(js) > 45 { outputJS += "..." } w.log.DebugFields("Eval", logger.Fields{"js": outputJS}) // w.window.Dispatch(func() { w.window.Eval(js) }) return nil } // Escape the Javascripts! func escapeJS(js string) (string, error) { result := strings.Replace(js, "\\", "\\\\", -1) result = strings.Replace(result, "'", "\\'", -1) result = strings.Replace(result, "\n", "\\n", -1) return result, nil } // evalJSSync evaluates the given js in the WebView synchronously // Do not call this from the main thread or you'll nuke your app because // you won't get the callback. func (w *WebView) evalJSSync(js string) error { minified, err := escapeJS(js) if err != nil { return err } outputJS := fmt.Sprintf("%.45s", js) if len(js) > 45 { outputJS += "..." } w.log.DebugFields("EvalSync", logger.Fields{"js": outputJS}) ID := fmt.Sprintf("syncjs:%d:%d", time.Now().Unix(), rand.Intn(9999)) var wg sync.WaitGroup wg.Add(1) go func() { exit := false // We are done when we receive the Callback ID w.log.Debug("SyncJS: sending with ID = " + ID) w.eventManager.On(ID, func(...interface{}) { w.log.Debug("SyncJS: Got callback ID = " + ID) wg.Done() exit = true }) command := fmt.Sprintf("wails._.AddScript('%s', '%s')", minified, ID) w.window.Dispatch(func() { w.window.Eval(command) }) for exit == false { time.Sleep(time.Millisecond * 1) } }() wg.Wait() return nil } // injectCSS adds the given CSS to the WebView func (w *WebView) injectCSS(css string) { w.window.Dispatch(func() { w.window.InjectCSS(css) }) } // Exit closes the window func (w *WebView) Exit() { w.window.Exit() } // Run the window main loop func (w *WebView) Run() error { w.log.Info("Running...") // Inject firebug in debug mode on Windows if UseFirebug != "" { w.log.Debug("Injecting Firebug") w.evalJS(`window.usefirebug=true;`) } // Runtime assets w.log.DebugFields("Injecting wails JS runtime", logger.Fields{"js": runtime.WailsJS}) w.evalJS(runtime.WailsJS) // Ping the wait channel when the wails runtime is loaded w.eventManager.On("wails:loaded", func(...interface{}) { // Run this in a different go routine to free up the main process go func() { // Inject Bindings for _, binding := range w.bindingCache { w.evalJSSync(binding) } // Inject user CSS if w.config.GetCSS() != "" { outputCSS := fmt.Sprintf("%.45s", w.config.GetCSS()) if len(outputCSS) > 45 { outputCSS += "..." } w.log.DebugFields("Inject User CSS", logger.Fields{"css": outputCSS}) w.injectCSS(w.config.GetCSS()) } else { // Use default wails css w.log.Debug("Injecting Default Wails CSS: " + runtime.WailsCSS) w.injectCSS(runtime.WailsCSS) } // Inject user JS if w.config.GetJS() != "" { outputJS := fmt.Sprintf("%.45s", w.config.GetJS()) if len(outputJS) > 45 { outputJS += "..." } w.log.DebugFields("Inject User JS", logger.Fields{"js": outputJS}) w.evalJSSync(w.config.GetJS()) } // Emit that everything is loaded and ready w.eventManager.Emit("wails:ready") }() }) // Kick off main window loop w.window.Run() return nil } // NewBinding registers a new binding with the frontend func (w *WebView) NewBinding(methodName string) error { objectCode := fmt.Sprintf("window.wails._.NewBinding('%s');", methodName) w.bindingCache = append(w.bindingCache, objectCode) return nil } // SelectFile opens a dialog that allows the user to select a file func (w *WebView) SelectFile(title string, filter string) string { var result string // We need to run this on the main thread, however Dispatch is // non-blocking so we launch this in a goroutine and wait for // dispatch to finish before returning the result var wg sync.WaitGroup wg.Add(1) go func() { w.window.Dispatch(func() { result = w.window.Dialog(wv.DialogTypeOpen, 0, title, "", filter) wg.Done() }) }() defer w.focus() // Ensure the main window is put back into focus afterwards wg.Wait() return result } // SelectDirectory opens a dialog that allows the user to select a directory func (w *WebView) SelectDirectory() string { var result string // We need to run this on the main thread, however Dispatch is // non-blocking so we launch this in a goroutine and wait for // dispatch to finish before returning the result var wg sync.WaitGroup wg.Add(1) go func() { w.window.Dispatch(func() { result = w.window.Dialog(wv.DialogTypeOpen, wv.DialogFlagDirectory, "Select Directory", "", "") wg.Done() }) }() defer w.focus() // Ensure the main window is put back into focus afterwards wg.Wait() return result } // SelectSaveFile opens a dialog that allows the user to select a file to save func (w *WebView) SelectSaveFile(title string, filter string) string { var result string // We need to run this on the main thread, however Dispatch is // non-blocking so we launch this in a goroutine and wait for // dispatch to finish before returning the result var wg sync.WaitGroup wg.Add(1) go func() { w.window.Dispatch(func() { result = w.window.Dialog(wv.DialogTypeSave, 0, title, "", filter) wg.Done() }) }() defer w.focus() // Ensure the main window is put back into focus afterwards wg.Wait() return result } // focus puts the main window into focus func (w *WebView) focus() { w.window.Dispatch(func() { w.window.Focus() }) } // callback sends a callback to the frontend func (w *WebView) callback(data string) error { callbackCMD := fmt.Sprintf("window.wails._.Callback('%s');", data) return w.evalJS(callbackCMD) } // NotifyEvent notifies the frontend about a backend runtime event func (w *WebView) NotifyEvent(event *messages.EventData) error { // Look out! Nils about! var err error if event == nil { err = fmt.Errorf("Sent nil event to renderer.WebView") w.log.Error(err.Error()) return err } // Default data is a blank array data := []byte("[]") // Process event data if event.Data != nil { // Marshall the data data, err = json.Marshal(event.Data) if err != nil { w.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error()) return err } } // Double encode data to ensure everything is escaped correctly. data, err = json.Marshal(string(data)) if err != nil { w.log.Errorf("Cannot marshal JSON data in event: %s ", err.Error()) return err } message := "window.wails._.Notify('" + event.Name + "'," + string(data) + ")" return w.evalJS(message) } // SetMinSize sets the minimum size of a resizable window func (w *WebView) SetMinSize(width, height int) { if w.config.GetResizable() == false { w.log.Warn("Cannot call SetMinSize() - App.Resizable = false") return } w.window.Dispatch(func() { w.window.SetMinSize(width, height) }) } // SetMaxSize sets the maximum size of a resizable window func (w *WebView) SetMaxSize(width, height int) { if w.config.GetResizable() == false { w.log.Warn("Cannot call SetMaxSize() - App.Resizable = false") return } w.maximumSizeSet = true w.window.Dispatch(func() { w.window.SetMaxSize(width, height) }) } // Fullscreen makes the main window go fullscreen func (w *WebView) Fullscreen() { if w.config.GetResizable() == false { w.log.Warn("Cannot call Fullscreen() - App.Resizable = false") return } else if w.maximumSizeSet { w.log.Warn("Cannot call Fullscreen() - Maximum size of window set") return } w.window.Dispatch(func() { w.window.SetFullscreen(true) }) } // UnFullscreen returns the window to the position prior to a fullscreen call func (w *WebView) UnFullscreen() { if w.config.GetResizable() == false { w.log.Warn("Cannot call UnFullscreen() - App.Resizable = false") return } w.window.Dispatch(func() { w.window.SetFullscreen(false) }) } // SetTitle sets the window title func (w *WebView) SetTitle(title string) { w.window.Dispatch(func() { w.window.SetTitle(title) }) } // Close closes the window func (w *WebView) Close() { w.window.Dispatch(func() { w.window.Terminate() }) }