5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 18:29:53 +08:00
wails/v2/internal/frontend/desktop/windows/frontend.go
stffabi 664f6a952c
[v2, windows] Unlock OSThread after native calls have been finished (#1441)
* [v2, windows] Remove unnecessary LockOSThread

Form.Invoke makes sure the call is on the correct thread and does
lock the OSThread during the call.

* [v2, windows] Unlock OSThread after native calls have been finished

This makes sure the OSThread can be reused by other go
routines after a native call has been finished. Otherwise the
OSThread will be destroyed as soon as the goroutine has
finished.
2022-06-08 20:56:07 +10:00

708 lines
17 KiB
Go

//go:build windows
// +build windows
package windows
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"runtime"
"strconv"
"strings"
"text/template"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
const startURL = "http://wails.localhost/"
type Frontend struct {
// Context
ctx context.Context
frontendOptions *options.App
logger *logger.Logger
chromium *edge.Chromium
debug bool
// Assets
assets *assetserver.AssetServer
startURL *url.URL
// main window handle
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
hasStarted bool
// Windows build number
versionInfo *operatingsystem.WindowsVersionInfo
}
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
// Get Windows build number
versionInfo, _ := operatingsystem.GetWindowsVersionInfo()
result := &Frontend{
frontendOptions: appoptions,
logger: myLogger,
bindings: appBindings,
dispatcher: dispatcher,
ctx: ctx,
versionInfo: versionInfo,
}
// We currently can't use wails://wails/ as other platforms do, therefore we map the assets sever onto the following url.
result.startURL, _ = url.Parse(startURL)
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
return result
}
bindingsJSON, err := appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindingsJSON)
if err != nil {
log.Fatal(err)
}
result.assets = assets
return result
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
}
func (f *Frontend) WindowSetSystemDefaultTheme() {
f.mainWindow.frontendOptions.Windows.Theme = windows.SystemDefault
f.mainWindow.Invoke(func() {
f.mainWindow.updateTheme()
})
}
func (f *Frontend) WindowSetLightTheme() {
if f.mainWindow.frontendOptions != nil && f.mainWindow.frontendOptions.Windows != nil {
f.mainWindow.frontendOptions.Windows.Theme = windows.Light
f.mainWindow.Invoke(func() {
f.mainWindow.updateTheme()
})
}
}
func (f *Frontend) WindowSetDarkTheme() {
if f.mainWindow.frontendOptions != nil && f.mainWindow.frontendOptions.Windows != nil {
f.mainWindow.frontendOptions.Windows.Theme = windows.Dark
f.mainWindow.Invoke(func() {
f.mainWindow.updateTheme()
})
}
}
func (f *Frontend) Run(ctx context.Context) error {
f.ctx = context.WithValue(ctx, "frontend", f)
mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo)
f.mainWindow = mainWindow
var _debug = ctx.Value("debug")
if _debug != nil {
f.debug = _debug.(bool)
}
f.WindowCenter()
f.setupChromium()
f.mainWindow.notifyParentWindowPositionChanged = f.chromium.NotifyParentWindowPositionChanged
mainWindow.OnSize().Bind(func(arg *winc.Event) {
if f.frontendOptions.Frameless {
// If the window is frameless and we are minimizing, then we need to suppress the Resize on the
// WebView2. If we don't do this, restoring does not work as expected and first restores with some wrong
// size during the restore animation and only fully renders when the animation is done. This highly
// depends on the content in the WebView, see https://github.com/wailsapp/wails/issues/1319
event, _ := arg.Data.(*winc.SizeEventData)
if event != nil && event.Type == w32.SIZE_MINIMIZED {
return
}
}
f.chromium.Resize()
})
mainWindow.OnClose().Bind(func(arg *winc.Event) {
if f.frontendOptions.HideWindowOnClose {
f.WindowHide()
} else {
f.Quit()
}
})
go func() {
if f.frontendOptions.OnStartup != nil {
f.frontendOptions.OnStartup(f.ctx)
}
}()
mainWindow.Run()
mainWindow.Close()
return nil
}
func (f *Frontend) WindowCenter() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.Center()
}
func (f *Frontend) WindowSetPosition(x, y int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.SetPos(x, y)
}
func (f *Frontend) WindowGetPosition() (int, int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
return f.mainWindow.Pos()
}
func (f *Frontend) WindowSetSize(width, height int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.SetSize(width, height)
}
func (f *Frontend) WindowGetSize() (int, int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
return f.mainWindow.Size()
}
func (f *Frontend) WindowSetTitle(title string) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.SetText(title)
}
func (f *Frontend) WindowFullscreen() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = false;")
}
f.mainWindow.Fullscreen()
}
func (f *Frontend) WindowReloadApp() {
f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL))
}
func (f *Frontend) WindowUnfullscreen() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = true;")
}
f.mainWindow.UnFullscreen()
}
func (f *Frontend) WindowShow() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.ShowWindow()
}
func (f *Frontend) WindowHide() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.Hide()
}
func (f *Frontend) WindowMaximise() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if f.hasStarted {
if !f.frontendOptions.DisableResize {
f.mainWindow.Maximise()
}
} else {
f.frontendOptions.WindowStartState = options.Maximised
}
}
func (f *Frontend) WindowToggleMaximise() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if !f.hasStarted {
return
}
if f.mainWindow.IsMaximised() {
f.WindowUnmaximise()
} else {
f.WindowMaximise()
}
}
func (f *Frontend) WindowUnmaximise() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.Restore()
}
func (f *Frontend) WindowMinimise() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if f.hasStarted {
f.mainWindow.Minimise()
} else {
f.frontendOptions.WindowStartState = options.Minimised
}
}
func (f *Frontend) WindowUnminimise() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.Restore()
}
func (f *Frontend) WindowSetMinSize(width int, height int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.SetMinSize(width, height)
}
func (f *Frontend) WindowSetMaxSize(width int, height int) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
f.mainWindow.SetMaxSize(width, height)
}
func (f *Frontend) WindowSetRGBA(col *options.RGBA) {
if col == nil {
return
}
f.mainWindow.Invoke(func() {
controller := f.chromium.GetController()
controller2 := controller.GetICoreWebView2Controller2()
backgroundCol := edge.COREWEBVIEW2_COLOR{
A: col.A,
R: col.R,
G: col.G,
B: col.B,
}
// Webview2 only has 0 and 255 as valid values.
if backgroundCol.A > 0 && backgroundCol.A < 255 {
backgroundCol.A = 255
}
if f.frontendOptions.Windows != nil && f.frontendOptions.Windows.WebviewIsTransparent {
backgroundCol.A = 0
}
err := controller2.PutDefaultBackgroundColor(backgroundCol)
if err != nil {
log.Fatal(err)
}
})
}
func (f *Frontend) Quit() {
if f.frontendOptions.OnBeforeClose != nil && f.frontendOptions.OnBeforeClose(f.ctx) {
return
}
// Exit must be called on the Main-Thread. It calls PostQuitMessage which sends the WM_QUIT message to the thread's
// message queue and our message queue runs on the Main-Thread.
f.mainWindow.Invoke(winc.Exit)
}
func (f *Frontend) setupChromium() {
chromium := edge.NewChromium()
f.chromium = chromium
if opts := f.frontendOptions.Windows; opts != nil && opts.WebviewUserDataPath != "" {
chromium.DataPath = opts.WebviewUserDataPath
}
chromium.MessageCallback = f.processMessage
chromium.WebResourceRequestedCallback = f.processRequest
chromium.NavigationCompletedCallback = f.navigationCompleted
chromium.AcceleratorKeyCallback = func(vkey uint) bool {
w32.PostMessage(f.mainWindow.Handle(), w32.WM_KEYDOWN, uintptr(vkey), 0)
return false
}
chromium.Embed(f.mainWindow.Handle())
chromium.Resize()
settings, err := chromium.GetSettings()
if err != nil {
log.Fatal(err)
}
err = settings.PutAreDefaultContextMenusEnabled(f.debug)
if err != nil {
log.Fatal(err)
}
err = settings.PutAreDevToolsEnabled(f.debug)
if err != nil {
log.Fatal(err)
}
err = settings.PutIsZoomControlEnabled(false)
if err != nil {
log.Fatal(err)
}
err = settings.PutIsStatusBarEnabled(false)
if err != nil {
log.Fatal(err)
}
err = settings.PutAreBrowserAcceleratorKeysEnabled(false)
if err != nil {
log.Fatal(err)
}
err = settings.PutIsSwipeNavigationEnabled(false)
if err != nil {
log.Fatal(err)
}
// Setup focus event handler
onFocus := f.mainWindow.OnSetFocus()
onFocus.Bind(f.onFocus)
// Set background colour
f.WindowSetRGBA(f.frontendOptions.RGBA)
chromium.SetGlobalPermission(edge.CoreWebView2PermissionStateAllow)
chromium.AddWebResourceRequestedFilter("*", edge.COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL)
chromium.Navigate(f.startURL.String())
}
type EventNotify struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
func (f *Frontend) Notify(name string, data ...interface{}) {
notification := EventNotify{
Name: name,
Data: data,
}
payload, err := json.Marshal(notification)
if err != nil {
f.logger.Error(err.Error())
return
}
f.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`)
}
func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, args *edge.ICoreWebView2WebResourceRequestedEventArgs) {
// Setting the UserAgent on the CoreWebView2Settings clears the whole default UserAgent of the Edge browser, but
// we want to just append our ApplicationIdentifier. So we adjust the UserAgent for every request.
if reqHeaders, err := req.GetHeaders(); err == nil {
useragent, _ := reqHeaders.GetHeader(assetserver.HeaderUserAgent)
useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ")
reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent)
reqHeaders.Release()
}
if f.assets == nil {
// We are using the devServer let the WebView2 handle the request with its default handler
return
}
//Get the request
uri, _ := req.GetUri()
reqUri, err := url.ParseRequestURI(uri)
if err != nil {
f.logger.Error("Unable to parse equest uri %s: %s", uri, err)
return
}
if reqUri.Scheme != f.startURL.Scheme {
// Let the WebView2 handle the request with its default handler
return
} else if reqUri.Host != f.startURL.Host {
// Let the WebView2 handle the request with its default handler
return
}
logInfo := strings.Replace(uri, f.startURL.String(), "", 1)
rw := httptest.NewRecorder()
f.assets.ProcessHTTPRequest(logInfo, rw, coreWebview2RequestToHttpRequest(req))
headers := []string{}
for k, v := range rw.Header() {
headers = append(headers, fmt.Sprintf("%s: %s", k, strings.Join(v, ",")))
}
env := f.chromium.Environment()
response, err := env.CreateWebResourceResponse(rw.Body.Bytes(), rw.Code, http.StatusText(rw.Code), strings.Join(headers, "\n"))
if err != nil {
f.logger.Error("CreateWebResourceResponse Error: %s", err)
return
}
defer response.Release()
// Send response back
err = args.PutResponse(response)
if err != nil {
f.logger.Error("PutResponse Error: %s", err)
return
}
}
var edgeMap = map[string]uintptr{
"n-resize": w32.HTTOP,
"ne-resize": w32.HTTOPRIGHT,
"e-resize": w32.HTRIGHT,
"se-resize": w32.HTBOTTOMRIGHT,
"s-resize": w32.HTBOTTOM,
"sw-resize": w32.HTBOTTOMLEFT,
"w-resize": w32.HTLEFT,
"nw-resize": w32.HTTOPLEFT,
}
func (f *Frontend) processMessage(message string) {
if message == "drag" {
if !f.mainWindow.IsFullScreen() {
err := f.startDrag()
if err != nil {
f.logger.Error(err.Error())
}
}
return
}
if strings.HasPrefix(message, "resize:") {
if !f.mainWindow.IsFullScreen() {
sl := strings.Split(message, ":")
if len(sl) != 2 {
f.logger.Info("Unknown message returned from dispatcher: %+v", message)
return
}
edge := edgeMap[sl[1]]
err := f.startResize(edge)
if err != nil {
f.logger.Error(err.Error())
}
}
return
}
go func() {
result, err := f.dispatcher.ProcessMessage(message, f)
if err != nil {
f.logger.Error(err.Error())
f.Callback(result)
return
}
if result == "" {
return
}
switch result[0] {
case 'c':
// Callback from a method call
f.Callback(result[1:])
default:
f.logger.Info("Unknown message returned from dispatcher: %+v", result)
}
}()
}
func (f *Frontend) Callback(message string) {
f.mainWindow.Invoke(func() {
f.chromium.Eval(`window.wails.Callback(` + strconv.Quote(message) + `);`)
})
}
func (f *Frontend) startDrag() error {
if !w32.ReleaseCapture() {
return fmt.Errorf("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until dragging has been finished.
w32.PostMessage(f.mainWindow.Handle(), w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0)
return nil
}
func (f *Frontend) startResize(border uintptr) error {
if !w32.ReleaseCapture() {
return fmt.Errorf("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until resizing has been finished.
w32.PostMessage(f.mainWindow.Handle(), w32.WM_NCLBUTTONDOWN, border, 0)
return nil
}
func (f *Frontend) ExecJS(js string) {
f.mainWindow.Invoke(func() {
f.chromium.Eval(js)
})
}
func (f *Frontend) navigationCompleted(sender *edge.ICoreWebView2, args *edge.ICoreWebView2NavigationCompletedEventArgs) {
if f.frontendOptions.OnDomReady != nil {
go f.frontendOptions.OnDomReady(f.ctx)
}
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = true;")
}
if f.hasStarted {
return
}
f.hasStarted = true
// Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026
err := f.chromium.Hide()
if err != nil {
log.Fatal(err)
}
err = f.chromium.Show()
if err != nil {
log.Fatal(err)
}
if f.frontendOptions.StartHidden {
return
}
switch f.frontendOptions.WindowStartState {
case options.Maximised:
if !f.frontendOptions.DisableResize {
f.mainWindow.Maximise()
} else {
f.mainWindow.Show()
}
f.ShowWindow()
case options.Minimised:
f.mainWindow.Minimise()
case options.Fullscreen:
f.mainWindow.Fullscreen()
f.ShowWindow()
default:
if f.frontendOptions.Fullscreen {
f.mainWindow.Fullscreen()
}
f.ShowWindow()
}
}
func (f *Frontend) ShowWindow() {
f.mainWindow.Invoke(func() {
f.mainWindow.Restore()
w32.SetForegroundWindow(f.mainWindow.Handle())
w32.SetFocus(f.mainWindow.Handle())
})
}
func (f *Frontend) onFocus(arg *winc.Event) {
f.chromium.Focus()
}
func coreWebview2RequestToHttpRequest(coreReq *edge.ICoreWebView2WebResourceRequest) func() (*http.Request, error) {
return func() (r *http.Request, err error) {
header := http.Header{}
headers, err := coreReq.GetHeaders()
if err != nil {
return nil, fmt.Errorf("GetHeaders Error: %s", err)
}
defer headers.Release()
headersIt, err := headers.GetIterator()
if err != nil {
return nil, fmt.Errorf("GetIterator Error: %s", err)
}
defer headersIt.Release()
for {
has, err := headersIt.HasCurrentHeader()
if err != nil {
return nil, fmt.Errorf("HasCurrentHeader Error: %s", err)
}
if !has {
break
}
name, value, err := headersIt.GetCurrentHeader()
if err != nil {
return nil, fmt.Errorf("GetCurrentHeader Error: %s", err)
}
header.Set(name, value)
if _, err := headersIt.MoveNext(); err != nil {
return nil, fmt.Errorf("MoveNext Error: %s", err)
}
}
method, err := coreReq.GetMethod()
if err != nil {
return nil, fmt.Errorf("GetMethod Error: %s", err)
}
uri, err := coreReq.GetUri()
if err != nil {
return nil, fmt.Errorf("GetUri Error: %s", err)
}
var body io.ReadCloser
if content, err := coreReq.GetContent(); err != nil {
return nil, fmt.Errorf("GetContent Error: %s", err)
} else if content != nil {
body = &iStreamReleaseCloser{stream: content}
}
req, err := http.NewRequest(method, uri, body)
if err != nil {
if body != nil {
body.Close()
}
return nil, err
}
req.Header = header
return req, nil
}
}
type iStreamReleaseCloser struct {
stream *edge.IStream
closed bool
}
func (i *iStreamReleaseCloser) Read(p []byte) (int, error) {
if i.closed {
return 0, io.ErrClosedPipe
}
return i.stream.Read(p)
}
func (i *iStreamReleaseCloser) Close() error {
if i.closed {
return nil
}
i.closed = true
return i.stream.Release()
}