Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bindings and expose Navigation Starting and Navigation Completed #53

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
21 changes: 21 additions & 0 deletions cmd/demo/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"log"

"github.com/jchv/go-webview2"
Expand All @@ -23,6 +24,26 @@ func main() {
}
defer w.Destroy()
w.SetSize(800, 600, webview2.HintFixed)
_ = w.NavigationStarting(onNavigationStarting)
_ = w.NavigationCompleted(onNavigationCompleted)
w.Navigate("https://en.m.wikipedia.org/wiki/Main_Page")
w.Run()
}

func onNavigationCompleted(httpStatusCode int32, isSuccess bool, navigationId uint64, webErrorStatus int32) {
fmt.Println("navigation completed:")
fmt.Println("http status code: ", httpStatusCode)
fmt.Println("success: ", isSuccess)
fmt.Println("navigation id: ", navigationId)
fmt.Println("web error status: ", webErrorStatus)
}

func onNavigationStarting(additionalAllowedFrameAncestors string, isRedirected bool, isUserInitiated bool, navigationId uint64, uri string) bool {
fmt.Println("navigation starting:")
fmt.Println("redirected: ", isRedirected)
fmt.Println("user initiated: ", isUserInitiated)
fmt.Println("navigation id: ", navigationId)
fmt.Println("additional allowed frame ancestors: ", additionalAllowedFrameAncestors)
fmt.Println("uri: ", uri)
return false
}
14 changes: 14 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,18 @@ type WebView interface {
// f must be a function
// f must return either value and error or just error
Bind(name string, f interface{}) error

// NavigationStarting binds a callback function for right before the webview2
// perform a navigation
//
// f must be a function
// f must return true, to proceed with the navigation
// f must return false, to cancel the navigation
NavigationStarting(f func(additionalAllowedFrameAncestors string, isRedirected bool, isUserInitiated bool, navigationId uint64, uri string) bool) error
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere, the WebView interface is frozen to preserve compatibility with webview/webview. The best way to add new functionality like this right now is to allow the user to pass a handler into the options struct of NewWithOptions.


// NavigationCompleted binds a callback function for right after the webview2
// has performed a navigation
//
// f must be a function
NavigationCompleted(f func(httpStatusCode int32, isSuccess bool, navigationId uint64, webErrorStatus int32)) error
}
1 change: 1 addition & 0 deletions pkg/edge/ICoreWebView2NavigationCompletedEventArgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type _ICoreWebView2NavigationCompletedEventArgsVtbl struct {
GetIsSuccess ComProc
GetWebErrorStatus ComProc
GetNavigationId ComProc
GetHttpStatusCode ComProc
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unsafe; GetHttpStatusCode is only available on the extended ICoreWebView2NavigationCompletedEventArgs2 class. You need to define a new COM wrapper for ICoreWebView2NavigationCompletedEventArgs2 instead.

Take a look at how it is done for ICoreWebView2_3: https://github.com/jchv/go-webview2/blob/2269a8d58f7e49372b823d9171948b3ae1993539/pkg/edge/ICoreWebView2_3.go

Notice the function GetICoreWebView2_3 which allows you to get a ICoreWebView2_3 from a ICoreWebView2. The same thing is needed for ICoreWebView2NavigationCompletedEventArgs2.

It should wind up looking something like this:

package edge

...

type _ICoreWebView2NavigationCompletedEventArgs2Vtbl struct {
	_ICoreWebView2NavigationCompletedEventArgsVtbl
	GetHttpStatusCode ComProc
}

type ICoreWebView2NavigationCompletedEventArgs2 struct {
	vtbl *_ICoreWebView2NavigationCompletedEventArgs2Vtbl
}

func (i *ICoreWebView2NavigationCompletedEventArgs) GetICoreWebView2NavigationCompletedEventArgs2() *ICoreWebView2NavigationCompletedEventArgs2 {
	var result *ICoreWebView2NavigationCompletedEventArgs2

	iidICoreWebView2NavigationCompletedEventArgs2 := NewGUID("{FDF8B738-EE1E-4DB2-A329-8D7D7B74D792}")
	_, _, _ = i.vtbl.QueryInterface.Call(
		uintptr(unsafe.Pointer(i)),
		uintptr(unsafe.Pointer(iidICoreWebView2NavigationCompletedEventArgs2)),
		uintptr(unsafe.Pointer(&result)))

	return result
}

}

type ICoreWebView2NavigationCompletedEventArgs struct {
Expand Down
21 changes: 21 additions & 0 deletions pkg/edge/ICoreWebView2NavigationStartingEventArgs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package edge

type _ICoreWebView2NavigationStartingEventArgsVtbl struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two problems here:

  1. Like ICoreWebView2NavigationCompletedEventArgs, there is a corresponding extended event args object for ICoreWebView2NavigationStartingEventArgs called, predictably, ICoreWebView2NavigationStartingEventArgs2. As I outlined with GetHttpStatusCode, it is not safe to include these directly without calling QueryInterface first. The GUID for ICoreWebView2NavigationStartingEventArgs2 is 9086BE93-91AA-472D-A7E0-579F2BA006AD.
  2. This Vtable is out of order, which will lead to the wrong functions being called.

The Vtable order for ICoreWebView2NavigationStartingEventArgs is:

[propget] HRESULT Uri([out, retval] LPWSTR* uri);
[propget] HRESULT IsUserInitiated([out, retval] BOOL* isUserInitiated);
[propget] HRESULT IsRedirected([out, retval] BOOL* isRedirected);
[propget] HRESULT RequestHeaders([out, retval] ICoreWebView2HttpRequestHeaders** requestHeaders);
[propget] HRESULT Cancel([out, retval] BOOL* cancel);
[propput] HRESULT Cancel([in] BOOL cancel);
[propget] HRESULT NavigationId([out, retval] UINT64* navigationId);

(Note that your names are fine. The propget/propput is syntax sugar in COM.)

And ICoreWebView2NavigationStartingEventArgs2:

// (... everything from the original ICoreWebView2NavigationStartingEventArgs)
[propget] HRESULT AdditionalAllowedFrameAncestors([out, retval] LPWSTR* value);
[propput] HRESULT AdditionalAllowedFrameAncestors([in] LPCWSTR value);

(Note that there is a put from AdditionalAllowedFrameAncestors as well as the get you already have.)

If you are wondering where this information comes from, it comes from the WebView2 NuGet package. You can download that here:

https://www.nuget.org/packages/Microsoft.Web.WebView2

Just click "Download Package". The corresponding nupkg file is a ZIP archive, and once you unzip it you will see WebView2.idl which contains the full COM IDL for all WebView2-related classes.

MSDN alone is not good enough, as MSDN displays the functions in a different order from their VTable ordering. We need these functions to be in VTable order for our memory map to align.

_IUnknownVtbl
GetUri ComProc
GetIsUserInitiated ComProc
GetIsRedirected ComProc
GetRequestHeaders ComProc
GetAdditionalAllowedFrameAncestors ComProc
PutCancel ComProc
GetNavigationId ComProc
}

type ICoreWebView2NavigationStartingEventArgs struct {
vtbl *_ICoreWebView2NavigationStartingEventArgsVtbl
}

func (i *ICoreWebView2NavigationStartingEventArgs) AddRef() uintptr {
r, _, _ := i.vtbl.AddRef.Call()
return r
}
48 changes: 48 additions & 0 deletions pkg/edge/ICoreWebView2NavigationStartingEventHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package edge

type _ICoreWebView2NavigationStartingEventHandlerVtbl struct {
_IUnknownVtbl
Invoke ComProc
}

type ICoreWebView2NavigationStartingEventHandler struct {
vtbl *_ICoreWebView2NavigationStartingEventHandlerVtbl
impl _ICoreWebView2NavigationStartingEventHandlerImpl
}

func _ICoreWebView2NavigationStartingEventHandlerIUnknownQueryInterface(this *ICoreWebView2NavigationStartingEventHandler, refiid, object uintptr) uintptr {
return this.impl.QueryInterface(refiid, object)
}

func _ICoreWebView2NavigationStartingEventHandlerIUnknownAddRef(this *ICoreWebView2NavigationStartingEventHandler) uintptr {
return this.impl.AddRef()
}

func _ICoreWebView2NavigationStartingEventHandlerIUnknownRelease(this *ICoreWebView2NavigationStartingEventHandler) uintptr {
return this.impl.Release()
}

func _ICoreWebView2NavigationStartingEventHandlerInvoke(this *ICoreWebView2NavigationStartingEventHandler, sender *ICoreWebView2, args *ICoreWebView2NavigationStartingEventArgs) uintptr {
return this.impl.NavigationStarting(sender, args)
}

type _ICoreWebView2NavigationStartingEventHandlerImpl interface {
_IUnknownImpl
NavigationStarting(sender *ICoreWebView2, args *ICoreWebView2NavigationStartingEventArgs) uintptr
}

var _ICoreWebView2NavigationStartingEventHandlerFn = _ICoreWebView2NavigationStartingEventHandlerVtbl{
_IUnknownVtbl{
NewComProc(_ICoreWebView2NavigationStartingEventHandlerIUnknownQueryInterface),
NewComProc(_ICoreWebView2NavigationStartingEventHandlerIUnknownAddRef),
NewComProc(_ICoreWebView2NavigationStartingEventHandlerIUnknownRelease),
},
NewComProc(_ICoreWebView2NavigationStartingEventHandlerInvoke),
}

func newICoreWebView2NavigationStartingEventHandler(impl _ICoreWebView2NavigationStartingEventHandlerImpl) *ICoreWebView2NavigationStartingEventHandler {
return &ICoreWebView2NavigationStartingEventHandler{
vtbl: &_ICoreWebView2NavigationStartingEventHandlerFn,
impl: impl,
}
}
87 changes: 85 additions & 2 deletions pkg/edge/chromium.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Chromium struct {
webResourceRequested *iCoreWebView2WebResourceRequestedEventHandler
acceleratorKeyPressed *ICoreWebView2AcceleratorKeyPressedEventHandler
navigationCompleted *ICoreWebView2NavigationCompletedEventHandler
navigationStarting *ICoreWebView2NavigationStartingEventHandler

environment *ICoreWebView2Environment

Expand All @@ -40,7 +41,8 @@ type Chromium struct {
// Callbacks
MessageCallback func(string)
WebResourceRequestedCallback func(request *ICoreWebView2WebResourceRequest, args *ICoreWebView2WebResourceRequestedEventArgs)
NavigationCompletedCallback func(sender *ICoreWebView2, args *ICoreWebView2NavigationCompletedEventArgs)
NavigationCompletedCallback func(httpStatusCode int32, isSuccess bool, navigationId uint64, webErrorStatus int32)
NavigationStartingCallback func(additionalAllowedFrameAncestors string, isRedirected bool, isUserInitiated bool, navigationId uint64, uri string) bool
AcceleratorKeyCallback func(uint) bool
}

Expand All @@ -64,6 +66,7 @@ func NewChromium() *Chromium {
e.webResourceRequested = newICoreWebView2WebResourceRequestedEventHandler(e)
e.acceleratorKeyPressed = newICoreWebView2AcceleratorKeyPressedEventHandler(e)
e.navigationCompleted = newICoreWebView2NavigationCompletedEventHandler(e)
e.navigationStarting = newICoreWebView2NavigationStartingEventHandler(e)
e.permissions = make(map[CoreWebView2PermissionKind]CoreWebView2PermissionState)

return e
Expand Down Expand Up @@ -211,6 +214,11 @@ func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller
uintptr(unsafe.Pointer(e.navigationCompleted)),
uintptr(unsafe.Pointer(&token)),
)
_, _, _ = e.webview.vtbl.AddNavigationStarting.Call(
uintptr(unsafe.Pointer(e.webview)),
uintptr(unsafe.Pointer(e.navigationStarting)),
uintptr(unsafe.Pointer(&token)),
)

_ = e.controller.AddAcceleratorKeyPressed(e.acceleratorKeyPressed, &token)

Expand Down Expand Up @@ -331,8 +339,83 @@ func boolToInt(input bool) int {

func (e *Chromium) NavigationCompleted(sender *ICoreWebView2, args *ICoreWebView2NavigationCompletedEventArgs) uintptr {
if e.NavigationCompletedCallback != nil {
e.NavigationCompletedCallback(sender, args)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Chromium abstraction should continue passing the raw ICoreWebView2NavigationCompletedEventArgs object down. Some users may already depend on this functionality.

This logic could be moved to webview.go instead. Although webview.go mostly avoids having actual Edge-related code in it, I do not see it as a huge problem to add it in this case.

var isSuccess uint
var _, _, _ = args.vtbl.GetIsSuccess.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&isSuccess)),
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it is not strictly necessary, a way this code could be improved is by adding helpers into the ICoreWebView2NavigationCompletedEventArgs struct. For example,

func (a *ICoreWebView2NavigationCompletedEventArgs) IsSuccess() bool {
	var isSuccess uint
	var _, _, _ = args.vtbl.GetIsSuccess.Call(
		uintptr(unsafe.Pointer(args)),
		uintptr(unsafe.Pointer(&isSuccess)),
	)
	return isSuccess == 1
}

(These should also error-check, but unfortunately that's already missing in a lot of places, so it's not really a big deal.)


var webErrorStatus int32
var _, _, _ = args.vtbl.GetWebErrorStatus.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&webErrorStatus)),
)

var navigationId uint64
var _, _, _ = args.vtbl.GetNavigationId.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&navigationId)),
)

var httpStatusCode int32
var _, _, _ = args.vtbl.GetHttpStatusCode.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&httpStatusCode)),
)

e.NavigationCompletedCallback(httpStatusCode, isSuccess == 1, navigationId, webErrorStatus)
}
return 0
}

func (e *Chromium) NavigationStarting(sender *ICoreWebView2, args *ICoreWebView2NavigationStartingEventArgs) uintptr {
if e.NavigationStartingCallback != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the Chromium interfaces uniform, as above, this should pass the raw ICoreWebView2NavigationStartingEvent rather than fetching the information directly.


var navigationId uint64
var _, _, _ = args.vtbl.GetNavigationId.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&navigationId)),
)

var uriPtr *uint16
var _, _, _ = args.vtbl.GetUri.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&uriPtr)),
)
var uri = w32.Utf16PtrToString(uriPtr)

var additionalAllowedFrameAncestorsPtr *uint16
var _, _, _ = args.vtbl.GetAdditionalAllowedFrameAncestors.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&additionalAllowedFrameAncestorsPtr)),
)
var additionalAllowedFrameAncestors = w32.Utf16PtrToString(additionalAllowedFrameAncestorsPtr)

var isRedirected uint
var _, _, _ = args.vtbl.GetIsRedirected.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&isRedirected)),
)

var isUserInitiated uint
var _, _, _ = args.vtbl.GetIsUserInitiated.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&isUserInitiated)),
)

var proceed = e.NavigationStartingCallback(additionalAllowedFrameAncestors, isRedirected == 1, isUserInitiated == 1, navigationId, uri)

if proceed {
return 0
}

var res uint
var _, _, _ = args.vtbl.PutCancel.Call(
uintptr(unsafe.Pointer(args)),
uintptr(unsafe.Pointer(&res)),
)
}

return 0
}

Expand Down
73 changes: 73 additions & 0 deletions webview.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import (
"golang.org/x/sys/windows"
)

const (
NavigationStartingBindingName = "navigation-starting"
NavigationCompletedBindingName = "navigation-completed"
)

var (
windowContext = map[uintptr]interface{}{}
windowContextSync sync.RWMutex
Expand Down Expand Up @@ -100,6 +105,8 @@ func NewWithOptions(options WebViewOptions) WebView {

chromium := edge.NewChromium()
chromium.MessageCallback = w.msgcb
chromium.NavigationCompletedCallback = w.navigationCompletedCallback
chromium.NavigationStartingCallback = w.navigationStartingCallback
chromium.DataPath = options.DataPath
chromium.SetPermission(edge.CoreWebView2PermissionKindClipboardRead, edge.CoreWebView2PermissionStateAllow)

Expand Down Expand Up @@ -474,3 +481,69 @@ func (w *webview) Bind(name string, f interface{}) error {

return nil
}

func (w *webview) navigationCompletedCallback(httpStatusCode int32, isSuccess bool, navigationId uint64, webErrorStatus int32) {
w.m.Lock()
var f, ok = w.bindings[NavigationCompletedBindingName]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This binding mechanism is meant for the JavaScript-Go binding system. Instead, just use normal typed function pointers inside the webview struct, like is done in the chromium struct.

These handlers (navigationCompletedCallback, navigationStartingCallback) would be the ideal place to put the logic to grab the event args and throw them into a Go struct for the callback.

w.m.Unlock()

if !ok {
return
}

var fValue = reflect.ValueOf(f)
var fArgs = []reflect.Value{
reflect.ValueOf(httpStatusCode),
reflect.ValueOf(isSuccess),
reflect.ValueOf(navigationId),
reflect.ValueOf(webErrorStatus),
}
fValue.Call(fArgs)
}

func (w *webview) NavigationCompleted(f func(httpStatusCode int32, isSuccess bool, navigationId uint64, webErrorStatus int32)) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a new API for this, please use the WebViewOptions function to add this kind of event handler. The function pointer passed in from NewWithOptions can be saved to the webview struct. This is preferred especially because the go-webview2 Webview interface is frozen to remain compatible with webview/webview.

w.m.Lock()
w.bindings[NavigationCompletedBindingName] = f
w.m.Unlock()

return nil
}

func (w *webview) navigationStartingCallback(additionalAllowedFrameAncestors string, isRedirected bool, isUserInitiated bool, navigationId uint64, uri string) bool {
w.m.Lock()
var f, ok = w.bindings[NavigationStartingBindingName]
w.m.Unlock()

if !ok {
return true
}

var fValue = reflect.ValueOf(f)
var fArgs = []reflect.Value{
reflect.ValueOf(additionalAllowedFrameAncestors),
reflect.ValueOf(isRedirected),
reflect.ValueOf(isUserInitiated),
reflect.ValueOf(navigationId),
reflect.ValueOf(uri),
}
var returnedValues = fValue.Call(fArgs)

if len(returnedValues) == 0 {
return true
}

var firstValue = returnedValues[0]
if firstValue.Kind() != reflect.Bool {
return true
}

return firstValue.Bool()
}

func (w *webview) NavigationStarting(f func(additionalAllowedFrameAncestors string, isRedirected bool, isUserInitiated bool, navigationId uint64, uri string) bool) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of the callbacks have rather long strings of arguments. I think it would be easier and more future-proof to make structs to hold these arguments, like:

type NavigationStartingEventArgs struct {
    AdditionalAllowedFrameAncestors string
    IsRedirected                    bool
    IsUserInitiated                 bool
    NavigationID                    uint64
    URI                             string
}

Then the closure type could just be

func(args NavigationStartingEventArgs) bool

Ditto for NavigationCompleted.

w.m.Lock()
w.bindings[NavigationStartingBindingName] = f
w.m.Unlock()

return nil
}