mirror of
https://github.com/taigrr/wails.git
synced 2026-04-17 12:15:02 -07:00
feat: major refactor
This commit is contained in:
254
lib/renderer/headless.go
Normal file
254
lib/renderer/headless.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dchest/htmlmin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/leaanthony/mewn"
|
||||
"github.com/wailsapp/wails/lib/interfaces"
|
||||
"github.com/wailsapp/wails/lib/logger"
|
||||
"github.com/wailsapp/wails/lib/messages"
|
||||
)
|
||||
|
||||
type messageType int
|
||||
|
||||
const (
|
||||
jsMessage messageType = iota
|
||||
cssMessage
|
||||
htmlMessage
|
||||
notifyMessage
|
||||
bindingMessage
|
||||
callbackMessage
|
||||
wailsRuntimeMessage
|
||||
)
|
||||
|
||||
func (m messageType) toString() string {
|
||||
return [...]string{"j", "s", "h", "n", "b", "c", "w"}[m]
|
||||
}
|
||||
|
||||
// Headless is a backend that opens a local web server
|
||||
// and renders the files over a websocket
|
||||
type Headless struct {
|
||||
// Common
|
||||
log *logger.CustomLogger
|
||||
ipcManager interfaces.IPCManager
|
||||
appConfig interfaces.AppConfig
|
||||
eventManager interfaces.EventManager
|
||||
bindingCache []string
|
||||
|
||||
// Headless specific
|
||||
initialisationJS []string
|
||||
server *http.Server
|
||||
theConnection *websocket.Conn
|
||||
|
||||
// Mutex for writing to the socket
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Initialise the Headless Renderer
|
||||
func (h *Headless) Initialise(appConfig interfaces.AppConfig, ipcManager interfaces.IPCManager, eventManager interfaces.EventManager) error {
|
||||
h.ipcManager = ipcManager
|
||||
h.appConfig = appConfig
|
||||
h.eventManager = eventManager
|
||||
ipcManager.BindRenderer(h)
|
||||
h.log = logger.NewCustomLogger("Bridge")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Headless) evalJS(js string, mtype messageType) error {
|
||||
|
||||
message := mtype.toString() + js
|
||||
|
||||
if h.theConnection == nil {
|
||||
h.initialisationJS = append(h.initialisationJS, message)
|
||||
} else {
|
||||
// Prepend message type to message
|
||||
h.sendMessage(h.theConnection, message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Headless) injectCSS(css string) {
|
||||
// Minify css to overcome issues in the browser with carriage returns
|
||||
minified, err := htmlmin.Minify([]byte(css), &htmlmin.Options{
|
||||
MinifyStyles: true,
|
||||
})
|
||||
if err != nil {
|
||||
h.log.Fatal("Unable to minify CSS: " + css)
|
||||
}
|
||||
minifiedCSS := string(minified)
|
||||
minifiedCSS = strings.Replace(minifiedCSS, "\\", "\\\\", -1)
|
||||
minifiedCSS = strings.Replace(minifiedCSS, "'", "\\'", -1)
|
||||
minifiedCSS = strings.Replace(minifiedCSS, "\n", " ", -1)
|
||||
inject := fmt.Sprintf("wails._.injectCSS('%s')", minifiedCSS)
|
||||
h.evalJS(inject, cssMessage)
|
||||
}
|
||||
|
||||
func (h *Headless) wsBridgeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||
}
|
||||
h.theConnection = conn
|
||||
h.log.Infof("Connection from frontend accepted [%p].", h.theConnection)
|
||||
conn.SetCloseHandler(func(int, string) error {
|
||||
h.log.Infof("Connection dropped [%p].", h.theConnection)
|
||||
h.theConnection = nil
|
||||
return nil
|
||||
})
|
||||
go h.start(conn)
|
||||
}
|
||||
|
||||
func (h *Headless) sendMessage(conn *websocket.Conn, msg string) {
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
|
||||
h.log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Headless) start(conn *websocket.Conn) {
|
||||
|
||||
// set external.invoke
|
||||
h.log.Infof("Connected to frontend.")
|
||||
|
||||
wailsRuntime := mewn.String("../../runtime/js/dist/wails.js")
|
||||
h.evalJS(wailsRuntime, wailsRuntimeMessage)
|
||||
|
||||
// Inject bindings
|
||||
for _, binding := range h.bindingCache {
|
||||
h.evalJS(binding, bindingMessage)
|
||||
}
|
||||
|
||||
// Emit that everything is loaded and ready
|
||||
h.eventManager.Emit("wails:ready")
|
||||
|
||||
for {
|
||||
messageType, buffer, err := conn.ReadMessage()
|
||||
if messageType == -1 {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
h.log.Errorf("Error reading message: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
h.log.Debugf("Got message: %#v\n", string(buffer))
|
||||
|
||||
h.ipcManager.Dispatch(string(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
// Run the app in headless mode!
|
||||
func (h *Headless) Run() error {
|
||||
h.server = &http.Server{Addr: ":34115"}
|
||||
http.HandleFunc("/bridge", h.wsBridgeHandler)
|
||||
|
||||
h.log.Info("Bridge mode started.")
|
||||
h.log.Info("The frontend will connect automatically.")
|
||||
|
||||
err := h.server.ListenAndServe()
|
||||
if err != nil {
|
||||
h.log.Fatal(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewBinding creates a new binding with the frontend
|
||||
func (h *Headless) NewBinding(methodName string) error {
|
||||
h.bindingCache = append(h.bindingCache, methodName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectFile is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) SelectFile() string {
|
||||
h.log.Warn("SelectFile() unsupported in bridge mode")
|
||||
return ""
|
||||
}
|
||||
|
||||
// SelectDirectory is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) SelectDirectory() string {
|
||||
h.log.Warn("SelectDirectory() unsupported in bridge mode")
|
||||
return ""
|
||||
}
|
||||
|
||||
// SelectSaveFile is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) SelectSaveFile() string {
|
||||
h.log.Warn("SelectSaveFile() unsupported in bridge mode")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Callback sends a callback to the frontend
|
||||
func (h *Headless) Callback(data string) error {
|
||||
return h.evalJS(data, callbackMessage)
|
||||
}
|
||||
|
||||
// NotifyEvent notifies the frontend of an event
|
||||
func (h *Headless) NotifyEvent(event *messages.EventData) error {
|
||||
|
||||
// Look out! Nils about!
|
||||
var err error
|
||||
if event == nil {
|
||||
err = fmt.Errorf("Sent nil event to renderer.webViewRenderer")
|
||||
h.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 {
|
||||
h.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("window.wails._.notify('%s','%s')", event.Name, data)
|
||||
return h.evalJS(message, notifyMessage)
|
||||
}
|
||||
|
||||
// SetColour is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) SetColour(colour string) error {
|
||||
h.log.WarnFields("SetColour ignored for headless more", logger.Fields{"col": colour})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fullscreen is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) Fullscreen() {
|
||||
h.log.Warn("Fullscreen() unsupported in bridge mode")
|
||||
}
|
||||
|
||||
// UnFullscreen is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) UnFullscreen() {
|
||||
h.log.Warn("UnFullscreen() unsupported in bridge mode")
|
||||
}
|
||||
|
||||
// SetTitle is currently unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) SetTitle(title string) {
|
||||
h.log.WarnFields("SetTitle() unsupported in bridge mode", logger.Fields{"title": title})
|
||||
}
|
||||
|
||||
// Close is unsupported for Headless but required
|
||||
// for the Renderer interface
|
||||
func (h *Headless) Close() {
|
||||
h.log.Warn("Close() unsupported in bridge mode")
|
||||
}
|
||||
10
lib/renderer/renderer-mewn.go
Normal file
10
lib/renderer/renderer-mewn.go
Normal file
File diff suppressed because one or more lines are too long
357
lib/renderer/webview.go
Normal file
357
lib/renderer/webview.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/colors"
|
||||
"github.com/leaanthony/mewn"
|
||||
"github.com/wailsapp/wails/lib/logger"
|
||||
"github.com/wailsapp/wails/lib/messages"
|
||||
"github.com/wailsapp/wails/lib/interfaces"
|
||||
"github.com/wailsapp/webview"
|
||||
)
|
||||
|
||||
// WebView defines the main webview application window
|
||||
// Default values in []
|
||||
type WebView struct {
|
||||
window webview.WebView // The webview object
|
||||
ipc interfaces.IPCManager
|
||||
log *logger.CustomLogger
|
||||
config interfaces.AppConfig
|
||||
eventManager interfaces.EventManager
|
||||
bindingCache []string
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Create the WebView instance
|
||||
w.window = webview.NewWebview(webview.Settings{
|
||||
Width: config.GetWidth(),
|
||||
Height: config.GetHeight(),
|
||||
Title: config.GetTitle(),
|
||||
Resizable: config.GetResizable(),
|
||||
URL: config.GetDefaultHTML(),
|
||||
Debug: !config.GetDisableInspector(),
|
||||
ExternalInvokeCallback: func(_ webview.WebView, message string) {
|
||||
w.ipc.Dispatch(message)
|
||||
},
|
||||
})
|
||||
|
||||
// SignalManager.OnExit(w.Exit)
|
||||
|
||||
// Set colour
|
||||
err := w.SetColour(config.GetColour())
|
||||
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("Run()")
|
||||
|
||||
// Runtime assets
|
||||
wailsRuntime := mewn.String("../../runtime/js/dist/wails.js")
|
||||
w.evalJS(wailsRuntime)
|
||||
|
||||
// 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")
|
||||
defaultCSS := mewn.String("../../runtime/assets/wails.css")
|
||||
|
||||
w.injectCSS(defaultCSS)
|
||||
}
|
||||
|
||||
// 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() 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(webview.DialogTypeOpen, 0, "Select File", "")
|
||||
wg.Done()
|
||||
})
|
||||
}()
|
||||
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(webview.DialogTypeOpen, webview.DialogFlagDirectory, "Select Directory", "")
|
||||
wg.Done()
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
return result
|
||||
}
|
||||
|
||||
// SelectSaveFile opens a dialog that allows the user to select a file to save
|
||||
func (w *WebView) SelectSaveFile() 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(webview.DialogTypeSave, 0, "Save file", "")
|
||||
wg.Done()
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
return result
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("wails._.notify('%s','%s')", event.Name, data)
|
||||
return w.evalJS(message)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user