mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge 9cbb3f64b1 into 5d6e9872d4
This commit is contained in:
commit
63e8e737cf
15 changed files with 1177 additions and 0 deletions
142
v3/WEBCONTENTSVIEW.md
Normal file
142
v3/WEBCONTENTSVIEW.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# WebContentsView for Wails v3
|
||||
|
||||
`WebContentsView` is an implementation of Electron's `WebContentsView` (formerly `BrowserView`) for Wails v3. It allows you to embed a fully native, secondary OS-level Webview directly over your Wails application UI.
|
||||
|
||||
Unlike a standard HTML `<iframe>`, this native view:
|
||||
- Bypasses restrictive `X-Frame-Options` and `Content-Security-Policy: frame-ancestors` headers.
|
||||
- Can have web security (CORS) disabled independently of the main Wails app context.
|
||||
- Maintains its own session, cookies, and caching behavior.
|
||||
- Renders with native performance using the OS's underlying web engine (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux).
|
||||
|
||||
## Architecture
|
||||
|
||||
The module is built with a clean separation between the Go API and the platform-specific native implementations.
|
||||
|
||||
### 1. Go API Layer
|
||||
The `webcontentsview` package exposes a structured API for managing the view lifecycle.
|
||||
- **`NewWebContentsView(options)`**: Initializes the native OS webview but does not display it.
|
||||
- **`Attach(window)`**: Mounts the webview to the provided Wails `application.Window` using its raw `NativeWindow()` pointer.
|
||||
- **`SetBounds(rect)`**: Dynamically positions and sizes the view.
|
||||
- **`SetURL(url)`**: Navigates the view.
|
||||
- **`ExecJS(js)`**: Evaluates JavaScript inside the context of the secondary view.
|
||||
- **`Detach()`**: Unmounts and hides the view.
|
||||
|
||||
### 2. Platform Specific Implementations
|
||||
* **macOS (`webcontentsview_darwin.m`)**: Creates a `WKWebView` via Objective-C. When attached, it gets added as a subview to the `NSWindow`'s `contentView`. To ensure it sits above the main Wails UI, it is backed by a CoreAnimation layer (`wantsLayer = YES`) and assigned an astronomical z-index (`zPosition = 9999.0`). It automatically adjusts web coordinates (top-left) to Cocoa coordinates (bottom-left).
|
||||
* **Windows (`webcontentsview_windows.go`)**: Leverages the `github.com/wailsapp/go-webview2/pkg/edge` package to create an `edge.Chromium` instance, embedding it directly into the parent window's `HWND`.
|
||||
* **Linux (`webcontentsview_linux.go`)**: Uses CGO and GTK to create a `WebKitSettings` and `GtkWidget` webview, packing it into the main `GtkBox` container.
|
||||
|
||||
### 3. Web Preferences
|
||||
Inspired by Electron, `WebContentsViewOptions` accepts a `WebPreferences` struct. This passes down to `WKPreferences` / `ICoreWebView2Settings` to configure behavior:
|
||||
- `DevTools`: Enable/disable the web inspector.
|
||||
- `Javascript`: Enable/disable JS execution.
|
||||
- `WebSecurity`: Disables cross-origin restrictions and allows local file URL access (crucial for local-dev previewing).
|
||||
- `ZoomFactor`: Scales the viewport.
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
|
||||
To use `WebContentsView`, you must coordinate between your Go backend and your JavaScript/React frontend.
|
||||
|
||||
### 1. Go Backend setup
|
||||
Add the bridge methods to your Wails `App` struct so the frontend can control the view:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/webcontentsview"
|
||||
)
|
||||
|
||||
var browserView *webcontentsview.WebContentsView
|
||||
|
||||
func (a *App) InitBrowserView(x, y, width, height int, url string) {
|
||||
// ALL UI creation MUST happen on the main thread
|
||||
application.InvokeSync(func() {
|
||||
browserView = webcontentsview.NewWebContentsView(webcontentsview.WebContentsViewOptions{
|
||||
URL: url,
|
||||
Bounds: application.Rect{ X: x, Y: y, Width: width, Height: height },
|
||||
WebPreferences: webcontentsview.WebPreferences{
|
||||
DevTools: application.Enabled,
|
||||
Javascript: application.Enabled,
|
||||
WebSecurity: application.Disabled, // Ideal for bypassing CORS during local dev
|
||||
},
|
||||
})
|
||||
browserView.Attach(a.mainWindow)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) SetBrowserViewBounds(x, y, width, height int) {
|
||||
application.InvokeSync(func() {
|
||||
browserView.SetBounds(application.Rect{ X: x, Y: y, Width: width, Height: height })
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. React Frontend setup
|
||||
Instead of an `<iframe>`, render an empty `<div>` in React to act as a placeholder. Use a `ResizeObserver` to track the exact screen coordinates of the `<div>` and send them to the Go backend. Go will physically move the native window over the empty space.
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect, useLayoutEffect } from "react";
|
||||
import { desktopAPI } from "@/features/desktop/api";
|
||||
|
||||
export default function BrowserTab({ active, url }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
const updateBounds = () => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
// Hide by setting dimensions to 0 when inactive
|
||||
if (!active || rect.width === 0) {
|
||||
desktopAPI.setBrowserViewBounds(0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
desktopAPI.setBrowserViewBounds(
|
||||
Math.round(rect.x),
|
||||
Math.round(rect.y),
|
||||
Math.round(rect.width),
|
||||
Math.round(rect.height)
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize on mount
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!isInitializedRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
desktopAPI.initBrowserView(
|
||||
Math.round(rect.x), Math.round(rect.y),
|
||||
active ? Math.round(rect.width) : 0,
|
||||
active ? Math.round(rect.height) : 0,
|
||||
url
|
||||
).then(() => { isInitializedRef.current = true; });
|
||||
} else {
|
||||
updateBounds();
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
// Track window resizing and layout shifting
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const observer = new ResizeObserver(() => setTimeout(updateBounds, 10));
|
||||
if (containerRef.current) observer.observe(containerRef.current);
|
||||
|
||||
window.addEventListener('resize', updateBounds);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', updateBounds);
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
// The native webview will "float" exactly over this transparent div
|
||||
<div className="flex-1 w-full relative bg-transparent" ref={containerRef} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This pattern ensures the native `WebContentsView` stays perfectly synchronized with your React layout, mimicking the behaviour of a built-in browser component.
|
||||
|
|
@ -55,6 +55,7 @@ func (b *BrowserWindow) Info(message string, args ...any) {
|
|||
|
||||
// No-op methods - these don't apply to browser windows
|
||||
|
||||
|
||||
func (b *BrowserWindow) Center() {}
|
||||
func (b *BrowserWindow) Close() {}
|
||||
func (b *BrowserWindow) DisableSizeConstraints() {}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
type Window interface {
|
||||
|
||||
Center()
|
||||
Close()
|
||||
DisableSizeConstraints()
|
||||
|
|
|
|||
142
v3/pkg/webcontentsview/README.md
Normal file
142
v3/pkg/webcontentsview/README.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# WebContentsView for Wails v3
|
||||
|
||||
`WebContentsView` is an implementation of Electron's `WebContentsView` (formerly `BrowserView`) for Wails v3. It allows you to embed a fully native, secondary OS-level Webview directly over your Wails application UI.
|
||||
|
||||
Unlike a standard HTML `<iframe>`, this native view:
|
||||
- Bypasses restrictive `X-Frame-Options` and `Content-Security-Policy: frame-ancestors` headers.
|
||||
- Can have web security (CORS) disabled independently of the main Wails app context.
|
||||
- Maintains its own session, cookies, and caching behavior.
|
||||
- Renders with native performance using the OS's underlying web engine (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux).
|
||||
|
||||
## Architecture
|
||||
|
||||
The module is built with a clean separation between the Go API and the platform-specific native implementations.
|
||||
|
||||
### 1. Go API Layer
|
||||
The `webcontentsview` package exposes a structured API for managing the view lifecycle.
|
||||
- **`NewWebContentsView(options)`**: Initializes the native OS webview but does not display it.
|
||||
- **`Attach(window)`**: Mounts the webview to the provided Wails `application.Window` using its raw `NativeWindow()` pointer.
|
||||
- **`SetBounds(rect)`**: Dynamically positions and sizes the view.
|
||||
- **`SetURL(url)`**: Navigates the view.
|
||||
- **`ExecJS(js)`**: Evaluates JavaScript inside the context of the secondary view.
|
||||
- **`Detach()`**: Unmounts and hides the view.
|
||||
|
||||
### 2. Platform Specific Implementations
|
||||
* **macOS (`webcontentsview_darwin.m`)**: Creates a `WKWebView` via Objective-C. When attached, it gets added as a subview to the `NSWindow`'s `contentView`. To ensure it sits above the main Wails UI, it is backed by a CoreAnimation layer (`wantsLayer = YES`) and assigned an astronomical z-index (`zPosition = 9999.0`). It automatically adjusts web coordinates (top-left) to Cocoa coordinates (bottom-left).
|
||||
* **Windows (`webcontentsview_windows.go`)**: Leverages the `github.com/wailsapp/go-webview2/pkg/edge` package to create an `edge.Chromium` instance, embedding it directly into the parent window's `HWND`.
|
||||
* **Linux (`webcontentsview_linux.go`)**: Uses CGO and GTK to create a `WebKitSettings` and `GtkWidget` webview, packing it into the main `GtkBox` container.
|
||||
|
||||
### 3. Web Preferences
|
||||
Inspired by Electron, `WebContentsViewOptions` accepts a `WebPreferences` struct. This passes down to `WKPreferences` / `ICoreWebView2Settings` to configure behavior:
|
||||
- `DevTools`: Enable/disable the web inspector.
|
||||
- `Javascript`: Enable/disable JS execution.
|
||||
- `WebSecurity`: Disables cross-origin restrictions and allows local file URL access (crucial for local-dev previewing).
|
||||
- `ZoomFactor`: Scales the viewport.
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
|
||||
To use `WebContentsView`, you must coordinate between your Go backend and your JavaScript/React frontend.
|
||||
|
||||
### 1. Go Backend setup
|
||||
Add the bridge methods to your Wails `App` struct so the frontend can control the view:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/webcontentsview"
|
||||
)
|
||||
|
||||
var browserView *webcontentsview.WebContentsView
|
||||
|
||||
func (a *App) InitBrowserView(x, y, width, height int, url string) {
|
||||
// ALL UI creation MUST happen on the main thread
|
||||
application.InvokeSync(func() {
|
||||
browserView = webcontentsview.NewWebContentsView(webcontentsview.WebContentsViewOptions{
|
||||
URL: url,
|
||||
Bounds: application.Rect{ X: x, Y: y, Width: width, Height: height },
|
||||
WebPreferences: webcontentsview.WebPreferences{
|
||||
DevTools: application.Enabled,
|
||||
Javascript: application.Enabled,
|
||||
WebSecurity: application.Disabled, // Ideal for bypassing CORS during local dev
|
||||
},
|
||||
})
|
||||
browserView.Attach(a.mainWindow)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) SetBrowserViewBounds(x, y, width, height int) {
|
||||
application.InvokeSync(func() {
|
||||
browserView.SetBounds(application.Rect{ X: x, Y: y, Width: width, Height: height })
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. React Frontend setup
|
||||
Instead of an `<iframe>`, render an empty `<div>` in React to act as a placeholder. Use a `ResizeObserver` to track the exact screen coordinates of the `<div>` and send them to the Go backend. Go will physically move the native window over the empty space.
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect, useLayoutEffect } from "react";
|
||||
import { desktopAPI } from "@/features/desktop/api";
|
||||
|
||||
export default function BrowserTab({ active, url }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
const updateBounds = () => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
// Hide by setting dimensions to 0 when inactive
|
||||
if (!active || rect.width === 0) {
|
||||
desktopAPI.setBrowserViewBounds(0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
desktopAPI.setBrowserViewBounds(
|
||||
Math.round(rect.x),
|
||||
Math.round(rect.y),
|
||||
Math.round(rect.width),
|
||||
Math.round(rect.height)
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize on mount
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!isInitializedRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
desktopAPI.initBrowserView(
|
||||
Math.round(rect.x), Math.round(rect.y),
|
||||
active ? Math.round(rect.width) : 0,
|
||||
active ? Math.round(rect.height) : 0,
|
||||
url
|
||||
).then(() => { isInitializedRef.current = true; });
|
||||
} else {
|
||||
updateBounds();
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
// Track window resizing and layout shifting
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const observer = new ResizeObserver(() => setTimeout(updateBounds, 10));
|
||||
if (containerRef.current) observer.observe(containerRef.current);
|
||||
|
||||
window.addEventListener('resize', updateBounds);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', updateBounds);
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
// The native webview will "float" exactly over this transparent div
|
||||
<div className="flex-1 w-full relative bg-transparent" ref={containerRef} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This pattern ensures the native `WebContentsView` stays perfectly synchronized with your React layout, mimicking the behaviour of a built-in browser component.
|
||||
22
v3/pkg/webcontentsview/snapshot.go
Normal file
22
v3/pkg/webcontentsview/snapshot.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package webcontentsview
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var snapshotCallbacks sync.Map
|
||||
var snapshotCallbackID uintptr
|
||||
|
||||
func registerSnapshotCallback(ch chan string) uintptr {
|
||||
id := atomic.AddUintptr(&snapshotCallbackID, 1)
|
||||
snapshotCallbacks.Store(id, ch)
|
||||
return id
|
||||
}
|
||||
|
||||
func dispatchSnapshotResult(id uintptr, data string) {
|
||||
if ch, ok := snapshotCallbacks.Load(id); ok {
|
||||
ch.(chan string) <- data
|
||||
snapshotCallbacks.Delete(id)
|
||||
}
|
||||
}
|
||||
88
v3/pkg/webcontentsview/webcontentsview.go
Normal file
88
v3/pkg/webcontentsview/webcontentsview.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package webcontentsview
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// WebContentsViewOptions represents the options for creating a WebContentsView.
|
||||
type WebContentsViewOptions struct {
|
||||
Name string
|
||||
URL string
|
||||
HTML string
|
||||
Bounds application.Rect
|
||||
WebPreferences WebPreferences
|
||||
}
|
||||
|
||||
// WebContentsView represents a native webview that can be embedded into a window.
|
||||
type WebContentsView struct {
|
||||
options WebContentsViewOptions
|
||||
id uint
|
||||
impl webContentsViewImpl
|
||||
}
|
||||
|
||||
var webContentsViewID uintptr
|
||||
|
||||
// NewWebContentsView creates a new WebContentsView with the given options.
|
||||
func NewWebContentsView(options WebContentsViewOptions) *WebContentsView {
|
||||
result := &WebContentsView{
|
||||
id: uint(atomic.AddUintptr(&webContentsViewID, 1)),
|
||||
options: options,
|
||||
}
|
||||
result.impl = newWebContentsViewImpl(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// SetBounds sets the position and size of the WebContentsView relative to its parent.
|
||||
func (v *WebContentsView) SetBounds(bounds application.Rect) {
|
||||
v.impl.setBounds(bounds)
|
||||
}
|
||||
|
||||
// SetURL loads the given URL into the WebContentsView.
|
||||
func (v *WebContentsView) SetURL(url string) {
|
||||
v.impl.setURL(url)
|
||||
}
|
||||
|
||||
// ExecJS executes the given javascript in the WebContentsView.
|
||||
func (v *WebContentsView) ExecJS(js string) {
|
||||
v.impl.execJS(js)
|
||||
}
|
||||
|
||||
// GoBack navigates to the previous page in history.
|
||||
func (v *WebContentsView) GoBack() {
|
||||
v.impl.goBack()
|
||||
}
|
||||
|
||||
// GetURL returns the current URL of the view.
|
||||
func (v *WebContentsView) GetURL() string {
|
||||
return v.impl.getURL()
|
||||
}
|
||||
|
||||
// TakeSnapshot returns a base64 encoded PNG of the current view.
|
||||
func (v *WebContentsView) TakeSnapshot() string {
|
||||
return v.impl.takeSnapshot()
|
||||
}
|
||||
|
||||
// Attach binds the WebContentsView to a Wails Window.
|
||||
func (v *WebContentsView) Attach(window application.Window) {
|
||||
v.impl.attach(window)
|
||||
}
|
||||
|
||||
// Detach removes the WebContentsView from the Wails Window.
|
||||
func (v *WebContentsView) Detach() {
|
||||
v.impl.detach()
|
||||
}
|
||||
|
||||
// webContentsViewImpl is the interface that platform-specific implementations must satisfy.
|
||||
type webContentsViewImpl interface {
|
||||
setBounds(bounds application.Rect)
|
||||
setURL(url string)
|
||||
execJS(js string)
|
||||
goBack()
|
||||
getURL() string
|
||||
takeSnapshot() string
|
||||
attach(window application.Window)
|
||||
detach()
|
||||
nativeView() unsafe.Pointer
|
||||
}
|
||||
28
v3/pkg/webcontentsview/webcontentsview_android.go
Normal file
28
v3/pkg/webcontentsview/webcontentsview_android.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//go:build android
|
||||
|
||||
package webcontentsview
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type androidWebContentsView struct {
|
||||
parent *WebContentsView
|
||||
}
|
||||
|
||||
func newWebContentsViewImpl(parent *WebContentsView) webContentsViewImpl {
|
||||
return &androidWebContentsView{parent: parent}
|
||||
}
|
||||
|
||||
func (w *androidWebContentsView) setBounds(bounds application.Rect) {}
|
||||
func (w *androidWebContentsView) setURL(url string) {}
|
||||
func (w *androidWebContentsView) execJS(js string) {}
|
||||
func (w *androidWebContentsView) goBack() {}
|
||||
func (w *androidWebContentsView) getURL() string { return "" }
|
||||
func (w *androidWebContentsView) takeSnapshot() string { return "" }
|
||||
|
||||
|
||||
func (w *androidWebContentsView) attach(window application.Window) {}
|
||||
func (w *androidWebContentsView) detach() {}
|
||||
func (w *androidWebContentsView) nativeView() unsafe.Pointer { return nil }
|
||||
130
v3/pkg/webcontentsview/webcontentsview_darwin.go
Normal file
130
v3/pkg/webcontentsview/webcontentsview_darwin.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//go:build darwin
|
||||
|
||||
package webcontentsview
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
|
||||
#import "webcontentsview_darwin.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type macosWebContentsView struct {
|
||||
parent *WebContentsView
|
||||
nsView unsafe.Pointer
|
||||
nsWindow unsafe.Pointer
|
||||
}
|
||||
|
||||
func newWebContentsViewImpl(parent *WebContentsView) webContentsViewImpl {
|
||||
var cUserAgent *C.char
|
||||
if parent.options.WebPreferences.UserAgent != "" {
|
||||
cUserAgent = C.CString(parent.options.WebPreferences.UserAgent)
|
||||
defer C.free(unsafe.Pointer(cUserAgent))
|
||||
}
|
||||
|
||||
prefs := C.WebContentsViewPreferences{
|
||||
devTools: C.bool(parent.options.WebPreferences.DevTools != application.Disabled),
|
||||
javascript: C.bool(parent.options.WebPreferences.Javascript != application.Disabled),
|
||||
webSecurity: C.bool(parent.options.WebPreferences.WebSecurity != application.Disabled),
|
||||
images: C.bool(parent.options.WebPreferences.Images != application.Disabled),
|
||||
plugins: C.bool(parent.options.WebPreferences.Plugins == application.Enabled),
|
||||
zoomFactor: C.double(parent.options.WebPreferences.ZoomFactor),
|
||||
defaultFontSize: C.int(parent.options.WebPreferences.DefaultFontSize),
|
||||
defaultMonospaceFontSize: C.int(parent.options.WebPreferences.DefaultMonospaceFontSize),
|
||||
minimumFontSize: C.int(parent.options.WebPreferences.MinimumFontSize),
|
||||
userAgent: cUserAgent,
|
||||
}
|
||||
|
||||
if prefs.zoomFactor == 0 {
|
||||
prefs.zoomFactor = 1.0
|
||||
}
|
||||
|
||||
var view = C.createWebContentsView(
|
||||
C.int(parent.options.Bounds.X),
|
||||
C.int(parent.options.Bounds.Y),
|
||||
C.int(parent.options.Bounds.Width),
|
||||
C.int(parent.options.Bounds.Height),
|
||||
prefs,
|
||||
)
|
||||
|
||||
result := &macosWebContentsView{
|
||||
parent: parent,
|
||||
nsView: view,
|
||||
}
|
||||
|
||||
if parent.options.URL != "" {
|
||||
result.setURL(parent.options.URL)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) setBounds(bounds application.Rect) {
|
||||
C.webContentsViewSetBounds(w.nsView, C.int(bounds.X), C.int(bounds.Y), C.int(bounds.Width), C.int(bounds.Height))
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) setURL(url string) {
|
||||
cUrl := C.CString(url)
|
||||
defer C.free(unsafe.Pointer(cUrl))
|
||||
C.webContentsViewSetURL(w.nsView, cUrl)
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) goBack() {
|
||||
C.webContentsViewGoBack(w.nsView)
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) takeSnapshot() string {
|
||||
ch := make(chan string, 1)
|
||||
id := registerSnapshotCallback(ch)
|
||||
application.InvokeSync(func() {
|
||||
C.webContentsViewTakeSnapshot(w.nsView, C.uintptr_t(id))
|
||||
})
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) getURL() string {
|
||||
cUrl := C.webContentsViewGetURL(w.nsView)
|
||||
if cUrl == nil {
|
||||
return ""
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cUrl))
|
||||
return C.GoString(cUrl)
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) execJS(js string) {
|
||||
cJs := C.CString(js)
|
||||
defer C.free(unsafe.Pointer(cJs))
|
||||
C.webContentsViewExecJS(w.nsView, cJs)
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) attach(window application.Window) {
|
||||
w.nsWindow = window.NativeWindow()
|
||||
if w.nsWindow != nil {
|
||||
C.windowAddWebContentsView(w.nsWindow, w.nsView)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) detach() {
|
||||
if w.nsWindow != nil {
|
||||
C.windowRemoveWebContentsView(w.nsWindow, w.nsView)
|
||||
w.nsWindow = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *macosWebContentsView) nativeView() unsafe.Pointer {
|
||||
return w.nsView
|
||||
}
|
||||
//export browserViewSnapshotCallback
|
||||
func browserViewSnapshotCallback(callbackID C.uintptr_t, base64 *C.char) {
|
||||
id := uintptr(callbackID)
|
||||
str := ""
|
||||
if base64 != nil {
|
||||
str = C.GoString(base64)
|
||||
}
|
||||
dispatchSnapshotResult(id, str)
|
||||
}
|
||||
35
v3/pkg/webcontentsview/webcontentsview_darwin.h
Normal file
35
v3/pkg/webcontentsview/webcontentsview_darwin.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef webcontentsview_darwin_h
|
||||
#define webcontentsview_darwin_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
typedef struct {
|
||||
bool devTools;
|
||||
bool javascript;
|
||||
bool webSecurity;
|
||||
bool images;
|
||||
bool plugins;
|
||||
double zoomFactor;
|
||||
int defaultFontSize;
|
||||
int defaultMonospaceFontSize;
|
||||
int minimumFontSize;
|
||||
const char* userAgent;
|
||||
|
||||
} WebContentsViewPreferences;
|
||||
|
||||
extern void* createWebContentsView(int x, int y, int w, int h, WebContentsViewPreferences prefs);
|
||||
extern void webContentsViewSetBounds(void* view, int x, int y, int w, int h);
|
||||
extern void webContentsViewSetURL(void* view, const char* url);
|
||||
extern void webContentsViewExecJS(void* view, const char* js);
|
||||
extern void webContentsViewGoBack(void* view);
|
||||
extern const char* webContentsViewGetURL(void* view);
|
||||
|
||||
extern void windowAddWebContentsView(void* nsWindow, void* view);
|
||||
extern void windowRemoveWebContentsView(void* nsWindow, void* view);
|
||||
|
||||
// Async snapshot
|
||||
extern void webContentsViewTakeSnapshot(void* view, uintptr_t callbackID);
|
||||
|
||||
#endif /* webcontentsview_darwin_h */
|
||||
168
v3/pkg/webcontentsview/webcontentsview_darwin.m
Normal file
168
v3/pkg/webcontentsview/webcontentsview_darwin.m
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#import "webcontentsview_darwin.h"
|
||||
|
||||
void* createWebContentsView(int x, int y, int w, int h, WebContentsViewPreferences prefs) {
|
||||
NSRect frame = NSMakeRect(x, y, w, h);
|
||||
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
|
||||
|
||||
WKPreferences *preferences = [[WKPreferences alloc] init];
|
||||
|
||||
@try {
|
||||
if (@available(macOS 10.11, *)) {
|
||||
[preferences setValue:@(prefs.devTools) forKey:@"developerExtrasEnabled"];
|
||||
}
|
||||
|
||||
if (@available(macOS 11.0, *)) {
|
||||
WKWebpagePreferences *webpagePreferences = [[WKWebpagePreferences alloc] init];
|
||||
webpagePreferences.allowsContentJavaScript = prefs.javascript;
|
||||
config.defaultWebpagePreferences = webpagePreferences;
|
||||
} else {
|
||||
preferences.javaScriptEnabled = prefs.javascript;
|
||||
}
|
||||
|
||||
if (!prefs.webSecurity) {
|
||||
@try {
|
||||
[config.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
|
||||
[config.preferences setValue:@YES forKey:@"allowUniversalAccessFromFileURLs"];
|
||||
} @catch (NSException *e) {}
|
||||
}
|
||||
config.preferences = preferences;
|
||||
} @catch (NSException *e) {}
|
||||
|
||||
WKWebView* webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
|
||||
[webView setValue:@(NO) forKey:@"drawsTransparentBackground"];
|
||||
|
||||
if (prefs.userAgent != NULL) {
|
||||
NSString* customUA = [NSString stringWithUTF8String:prefs.userAgent];
|
||||
[webView setCustomUserAgent:customUA];
|
||||
}
|
||||
|
||||
return webView;
|
||||
}
|
||||
|
||||
void webContentsViewSetBounds(void* view, int x, int y, int w, int h) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
NSView* superview = [webView superview];
|
||||
|
||||
if (superview != nil) {
|
||||
CGFloat superHeight = superview.bounds.size.height;
|
||||
CGFloat cocoaY = superHeight - y - h;
|
||||
[webView setFrame:NSMakeRect(x, cocoaY, w, h)];
|
||||
} else {
|
||||
[webView setFrame:NSMakeRect(x, y, w, h)];
|
||||
}
|
||||
}
|
||||
|
||||
void webContentsViewSetURL(void* view, const char* url) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
NSString* nsURL = [NSString stringWithUTF8String:url];
|
||||
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:nsURL]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[webView loadRequest:request];
|
||||
});
|
||||
}
|
||||
|
||||
void webContentsViewExecJS(void* view, const char* js) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
NSString* script = [NSString stringWithUTF8String:js];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[webView evaluateJavaScript:script completionHandler:nil];
|
||||
});
|
||||
}
|
||||
|
||||
void windowAddWebContentsView(void* nsWindow, void* view) {
|
||||
NSWindow* window = (NSWindow*)nsWindow;
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
[window.contentView addSubview:webView];
|
||||
[webView setWantsLayer:YES];
|
||||
webView.layer.zPosition = 9999.0;
|
||||
}
|
||||
|
||||
void windowRemoveWebContentsView(void* nsWindow, void* view) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
[webView removeFromSuperview];
|
||||
}
|
||||
|
||||
void webContentsViewGoBack(void* view) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([webView canGoBack]) {
|
||||
[webView goBack];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const char* webContentsViewGetURL(void* view) {
|
||||
__block const char* result = NULL;
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
|
||||
if ([NSThread isMainThread]) {
|
||||
if (webView.URL != nil) {
|
||||
result = strdup(webView.URL.absoluteString.UTF8String);
|
||||
}
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
if (webView.URL != nil) {
|
||||
result = strdup(webView.URL.absoluteString.UTF8String);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
extern void browserViewSnapshotCallback(uintptr_t callbackID, const char* base64);
|
||||
|
||||
void webContentsViewTakeSnapshot(void* view, uintptr_t callbackID) {
|
||||
WKWebView* webView = (WKWebView*)view;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@try {
|
||||
if (@available(macOS 10.13, *)) {
|
||||
WKSnapshotConfiguration *config = [[WKSnapshotConfiguration alloc] init];
|
||||
[webView takeSnapshotWithConfiguration:config completionHandler:^(NSImage *image, NSError *error) {
|
||||
if (error != nil || image == nil) {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
@try {
|
||||
CGImageRef cgRef = [image CGImageForProposedRect:NULL context:nil hints:nil];
|
||||
if (cgRef == NULL) {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
NSBitmapImageRep *newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
|
||||
if (newRep == nil) {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
[newRep setSize:[image size]];
|
||||
NSData *pngData = [newRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
||||
|
||||
if (pngData == nil) {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
||||
if (base64String == nil) {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *fullBase64 = [NSString stringWithFormat:@"data:image/png;base64,%@", base64String];
|
||||
browserViewSnapshotCallback(callbackID, [fullBase64 UTF8String]);
|
||||
} @catch (NSException *innerException) {
|
||||
NSLog(@"Error processing snapshot image: %@", innerException.reason);
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"Exception in snapshot configuration: %@", e.reason);
|
||||
browserViewSnapshotCallback(callbackID, NULL);
|
||||
}
|
||||
});
|
||||
}
|
||||
78
v3/pkg/webcontentsview/webcontentsview_darwin_test.go
Normal file
78
v3/pkg/webcontentsview/webcontentsview_darwin_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//go:build darwin
|
||||
|
||||
package webcontentsview
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// Dummy mock window that satisfies the interface
|
||||
// and returns a nil NativeWindow so we can test the Attach nil-handling safely
|
||||
// without spinning up the full NSApplication runloop in a headless test environment.
|
||||
type mockWindow struct {
|
||||
application.Window
|
||||
}
|
||||
|
||||
func (m *mockWindow) NativeWindow() unsafe.Pointer {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWebContentsView_APISurface(t *testing.T) {
|
||||
// We primarily want to ensure that the API surface compiles and functions
|
||||
// correctly at a struct level. Note: Full WKWebView instantiation without an NSApplication
|
||||
// runloop will crash on macOS, so we test the struct wiring here instead of the native allocations.
|
||||
|
||||
options := WebContentsViewOptions{
|
||||
Name: "TestBrowser",
|
||||
URL: "https://example.com",
|
||||
Bounds: application.Rect{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
},
|
||||
WebPreferences: WebPreferences{
|
||||
DevTools: application.Enabled,
|
||||
Javascript: application.Enabled,
|
||||
WebSecurity: application.Disabled, // Disable CORS
|
||||
ZoomFactor: 1.2,
|
||||
},
|
||||
}
|
||||
|
||||
// Because calling NewWebContentsView invokes C.createWebContentsView which
|
||||
// traps without a runloop during go test, we will just manually instantiate
|
||||
// the Go wrapper to verify the methods.
|
||||
view := &WebContentsView{
|
||||
id: 1,
|
||||
options: options,
|
||||
impl: &mockWebContentsViewImpl{}, // Mock the impl to bypass Objective-C in headless test
|
||||
}
|
||||
|
||||
// 2. Test SetBounds
|
||||
view.SetBounds(application.Rect{X: 10, Y: 10, Width: 400, Height: 400})
|
||||
|
||||
// 3. Test SetURL
|
||||
view.SetURL("https://google.com")
|
||||
|
||||
// 4. Test ExecJS
|
||||
view.ExecJS("console.log('test');")
|
||||
|
||||
// 5. Test Attach and Detach using a mock window
|
||||
win := &mockWindow{}
|
||||
view.Attach(win)
|
||||
view.Detach()
|
||||
|
||||
t.Log("macOS WebContentsView API surface tests passed successfully.")
|
||||
}
|
||||
|
||||
type mockWebContentsViewImpl struct{}
|
||||
|
||||
func (m *mockWebContentsViewImpl) setBounds(bounds application.Rect) {}
|
||||
func (m *mockWebContentsViewImpl) setURL(url string) {}
|
||||
func (m *mockWebContentsViewImpl) execJS(js string) {}
|
||||
func (m *mockWebContentsViewImpl) attach(window application.Window) {}
|
||||
func (m *mockWebContentsViewImpl) detach() {}
|
||||
func (m *mockWebContentsViewImpl) nativeView() unsafe.Pointer { return nil }
|
||||
28
v3/pkg/webcontentsview/webcontentsview_ios.go
Normal file
28
v3/pkg/webcontentsview/webcontentsview_ios.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//go:build ios
|
||||
|
||||
package webcontentsview
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type iosWebContentsView struct {
|
||||
parent *WebContentsView
|
||||
}
|
||||
|
||||
func newWebContentsViewImpl(parent *WebContentsView) webContentsViewImpl {
|
||||
return &iosWebContentsView{parent: parent}
|
||||
}
|
||||
|
||||
func (w *iosWebContentsView) setBounds(bounds application.Rect) {}
|
||||
func (w *iosWebContentsView) setURL(url string) {}
|
||||
func (w *iosWebContentsView) execJS(js string) {}
|
||||
func (w *iosWebContentsView) goBack() {}
|
||||
func (w *iosWebContentsView) getURL() string { return "" }
|
||||
func (w *iosWebContentsView) takeSnapshot() string { return "" }
|
||||
|
||||
|
||||
func (w *iosWebContentsView) attach(window application.Window) {}
|
||||
func (w *iosWebContentsView) detach() {}
|
||||
func (w *iosWebContentsView) nativeView() unsafe.Pointer { return nil }
|
||||
145
v3/pkg/webcontentsview/webcontentsview_linux.go
Normal file
145
v3/pkg/webcontentsview/webcontentsview_linux.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
//go:build linux && cgo && !gtk4 && !android && !server
|
||||
|
||||
package webcontentsview
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 gdk-3.0
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
static void* createWebContentsView_linux(int x, int y, int w, int h, int devTools, int js, int images) {
|
||||
WebKitSettings *settings = webkit_settings_new();
|
||||
|
||||
webkit_settings_set_enable_developer_extras(settings, devTools ? TRUE : FALSE);
|
||||
webkit_settings_set_enable_javascript(settings, js ? TRUE : FALSE);
|
||||
webkit_settings_set_auto_load_images(settings, images ? TRUE : FALSE);
|
||||
|
||||
GtkWidget *webview = webkit_web_view_new_with_settings(settings);
|
||||
gtk_widget_set_size_request(webview, w, h);
|
||||
return webview;
|
||||
}
|
||||
|
||||
static void webContentsViewSetBounds_linux(void* view, void* parentFixed, int x, int y, int w, int h) {
|
||||
GtkWidget *webview = (GtkWidget*)view;
|
||||
gtk_widget_set_size_request(webview, w, h);
|
||||
if (parentFixed != NULL) {
|
||||
gtk_fixed_move(GTK_FIXED(parentFixed), webview, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
static void webContentsViewSetURL_linux(void* view, const char* url) {
|
||||
webkit_web_view_load_uri(WEBKIT_WEB_VIEW((GtkWidget*)view), url);
|
||||
}
|
||||
|
||||
static void webContentsViewExecJS_linux(void* view, const char* js) {
|
||||
webkit_web_view_run_javascript(WEBKIT_WEB_VIEW((GtkWidget*)view), js, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
static void webContentsViewAttach_linux(void* window, void* view) {
|
||||
// Attempt to add to the main container. Wails v3 usually uses a vbox.
|
||||
GtkWindow *gtkWindow = GTK_WINDOW(window);
|
||||
GtkWidget *child = gtk_bin_get_child(GTK_BIN(gtkWindow));
|
||||
if (child != NULL && GTK_IS_BOX(child)) {
|
||||
gtk_box_pack_start(GTK_BOX(child), GTK_WIDGET(view), FALSE, FALSE, 0);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
}
|
||||
}
|
||||
|
||||
static void webContentsViewDetach_linux(void* view) {
|
||||
GtkWidget *webview = (GtkWidget*)view;
|
||||
GtkWidget *parent = gtk_widget_get_parent(webview);
|
||||
if (parent != NULL) {
|
||||
gtk_container_remove(GTK_CONTAINER(parent), webview);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type linuxWebContentsView struct {
|
||||
parent *WebContentsView
|
||||
widget unsafe.Pointer
|
||||
}
|
||||
|
||||
func newWebContentsViewImpl(parent *WebContentsView) webContentsViewImpl {
|
||||
devTools := 1
|
||||
if parent.options.WebPreferences.DevTools == application.Disabled {
|
||||
devTools = 0
|
||||
}
|
||||
|
||||
js := 1
|
||||
if parent.options.WebPreferences.Javascript == application.Disabled {
|
||||
js = 0
|
||||
}
|
||||
|
||||
images := 1
|
||||
if parent.options.WebPreferences.Images == application.Disabled {
|
||||
images = 0
|
||||
}
|
||||
|
||||
view := C.createWebContentsView_linux(
|
||||
C.int(parent.options.Bounds.X),
|
||||
C.int(parent.options.Bounds.Y),
|
||||
C.int(parent.options.Bounds.Width),
|
||||
C.int(parent.options.Bounds.Height),
|
||||
C.int(devTools),
|
||||
C.int(js),
|
||||
C.int(images),
|
||||
)
|
||||
|
||||
result := &linuxWebContentsView{
|
||||
parent: parent,
|
||||
widget: view,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) setBounds(bounds application.Rect) {
|
||||
C.webContentsViewSetBounds_linux(w.widget, nil, C.int(bounds.X), C.int(bounds.Y), C.int(bounds.Width), C.int(bounds.Height))
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) setURL(url string) {
|
||||
cUrl := C.CString(url)
|
||||
defer C.free(unsafe.Pointer(cUrl))
|
||||
C.webContentsViewSetURL_linux(w.widget, cUrl)
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) goBack() {
|
||||
// TODO: webkit_web_view_go_back
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) getURL() string {
|
||||
return ""
|
||||
}
|
||||
func (w *linuxWebContentsView) takeSnapshot() string {
|
||||
return ""
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) execJS(js string) {
|
||||
cJs := C.CString(js)
|
||||
defer C.free(unsafe.Pointer(cJs))
|
||||
C.webContentsViewExecJS_linux(w.widget, cJs)
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) attach(window application.Window) {
|
||||
if window.NativeWindow() != nil {
|
||||
C.webContentsViewAttach_linux(window.NativeWindow(), w.widget)
|
||||
if w.parent.options.URL != "" {
|
||||
w.setURL(w.parent.options.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) detach() {
|
||||
C.webContentsViewDetach_linux(w.widget)
|
||||
}
|
||||
|
||||
func (w *linuxWebContentsView) nativeView() unsafe.Pointer {
|
||||
return w.widget
|
||||
}
|
||||
118
v3/pkg/webcontentsview/webcontentsview_windows.go
Normal file
118
v3/pkg/webcontentsview/webcontentsview_windows.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
//go:build windows
|
||||
|
||||
package webcontentsview
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/go-webview2/pkg/edge"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
type windowsWebContentsView struct {
|
||||
parent *WebContentsView
|
||||
chromium *edge.Chromium
|
||||
hwnd w32.HWND
|
||||
}
|
||||
|
||||
func newWebContentsViewImpl(parent *WebContentsView) webContentsViewImpl {
|
||||
chromium := edge.NewChromium()
|
||||
|
||||
result := &windowsWebContentsView{
|
||||
parent: parent,
|
||||
chromium: chromium,
|
||||
}
|
||||
|
||||
settings, err := chromium.GetSettings()
|
||||
if err == nil {
|
||||
if parent.options.WebPreferences.DevTools != application.Disabled {
|
||||
settings.PutAreDevToolsEnabled(true)
|
||||
settings.PutAreDefaultContextMenusEnabled(true)
|
||||
} else {
|
||||
settings.PutAreDevToolsEnabled(false)
|
||||
settings.PutAreDefaultContextMenusEnabled(false)
|
||||
}
|
||||
|
||||
if parent.options.WebPreferences.Javascript != application.Disabled {
|
||||
settings.PutIsScriptEnabled(true)
|
||||
} else {
|
||||
settings.PutIsScriptEnabled(false)
|
||||
}
|
||||
|
||||
if parent.options.WebPreferences.ZoomFactor > 0 {
|
||||
chromium.PutZoomFactor(parent.options.WebPreferences.ZoomFactor)
|
||||
}
|
||||
|
||||
if parent.options.WebPreferences.UserAgent != "" {
|
||||
settings.PutUserAgent(parent.options.WebPreferences.UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) setBounds(bounds application.Rect) {
|
||||
if w.chromium != nil {
|
||||
edgeBounds := edge.Rect{
|
||||
Left: int32(bounds.X),
|
||||
Top: int32(bounds.Y),
|
||||
Right: int32(bounds.X + bounds.Width),
|
||||
Bottom: int32(bounds.Y + bounds.Height),
|
||||
}
|
||||
w.chromium.ResizeWithBounds(&edgeBounds)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) setURL(url string) {
|
||||
if w.chromium != nil {
|
||||
w.chromium.Navigate(url)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) goBack() {
|
||||
if w.chromium != nil {
|
||||
// w.chromium.GoBack() // Requires wrapping edge GoBack if available, or just execJS
|
||||
w.execJS("window.history.back();")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) getURL() string {
|
||||
// Synchronous URL reading isn't trivial without a dedicated edge mapping,
|
||||
// returning last known or empty for now.
|
||||
return w.parent.options.URL
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) execJS(js string) {
|
||||
if w.chromium != nil {
|
||||
w.chromium.Eval(js)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) takeSnapshot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) attach(window application.Window) {
|
||||
if window.NativeWindow() != nil {
|
||||
w.hwnd = w32.HWND(window.NativeWindow())
|
||||
w.chromium.Embed(w.hwnd)
|
||||
|
||||
w.chromium.Resize()
|
||||
w.chromium.Show()
|
||||
|
||||
if w.parent.options.URL != "" {
|
||||
w.chromium.Navigate(w.parent.options.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) detach() {
|
||||
if w.chromium != nil {
|
||||
w.chromium.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebContentsView) nativeView() unsafe.Pointer {
|
||||
return unsafe.Pointer(w.chromium)
|
||||
}
|
||||
51
v3/pkg/webcontentsview/webpreferences.go
Normal file
51
v3/pkg/webcontentsview/webpreferences.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package webcontentsview
|
||||
|
||||
import "github.com/leaanthony/u"
|
||||
|
||||
// WebPreferences closely mirrors Electron's webPreferences for WebContentsView.
|
||||
type WebPreferences struct {
|
||||
// DevTools enables or disables the developer tools. Default is true.
|
||||
DevTools u.Bool
|
||||
|
||||
// Javascript enables or disables javascript execution. Default is true.
|
||||
Javascript u.Bool
|
||||
|
||||
// WebSecurity enables or disables web security (CORS, etc.). Default is true.
|
||||
WebSecurity u.Bool
|
||||
|
||||
// AllowRunningInsecureContent allows an https page to run http code. Default is false.
|
||||
AllowRunningInsecureContent u.Bool
|
||||
|
||||
// Images enables or disables image loading. Default is true.
|
||||
Images u.Bool
|
||||
|
||||
// TextAreasAreResizable controls whether text areas can be resized. Default is true.
|
||||
TextAreasAreResizable u.Bool
|
||||
|
||||
// WebGL enables or disables WebGL. Default is true.
|
||||
WebGL u.Bool
|
||||
|
||||
// Plugins enables or disables plugins. Default is false.
|
||||
Plugins u.Bool
|
||||
|
||||
// ZoomFactor sets the default zoom factor of the page. Default is 1.0.
|
||||
ZoomFactor float64
|
||||
|
||||
// NavigateOnDragDrop controls whether dropping files triggers navigation. Default is false.
|
||||
NavigateOnDragDrop u.Bool
|
||||
|
||||
// DefaultFontSize sets the default font size. Default is 16.
|
||||
DefaultFontSize int
|
||||
|
||||
// DefaultMonospaceFontSize sets the default monospace font size. Default is 13.
|
||||
DefaultMonospaceFontSize int
|
||||
|
||||
// MinimumFontSize sets the minimum font size. Default is 0.
|
||||
MinimumFontSize int
|
||||
|
||||
// DefaultEncoding sets the default character encoding. Default is "UTF-8".
|
||||
DefaultEncoding string
|
||||
// UserAgent sets a custom user agent for the webview.
|
||||
UserAgent string
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue