This commit is contained in:
Ch3nyang 2026-03-01 16:06:13 -05:00 committed by GitHub
commit e06fbf016f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3374 additions and 1 deletions

View file

@ -0,0 +1,189 @@
# WebView Panel Example
This example demonstrates the **WebviewPanel** feature - embedding multiple independent webview panels within a single window. This is similar to Electron's BrowserView/WebContentsView and addresses [GitHub issue #1997](https://github.com/wailsapp/wails/issues/1997).
## Features Demonstrated
- **URL Loading**: Load external websites (like <https://wails.io>) in an embedded panel
- **Responsive Layout**: Panel automatically resizes with the window using anchor settings
- **Dynamic Switching**: Switch panel content between different URLs at runtime
- **Custom UI Integration**: Panel embedded within a custom HTML interface
## Running
```bash
cd v3/examples/webview-panel
go run .
```
## What This Example Shows
1. **Embedded Webview**: The main window displays a custom UI with a header and navigation buttons
2. **Panel Inside Window**: An embedded webview panel shows <https://wails.io> inside the window
3. **URL Switching**: Click the "Wails.io" or "Google.com" buttons to switch the panel content
4. **Responsive Behavior**: Resize the window to see the panel automatically adjust its size
## Use Cases
WebviewPanel is ideal for:
- **IDE-like layouts**: Editor + preview + terminal panels
- **Browser-style apps**: Tab bar + content area
- **Dashboard apps**: Navigation sidebar + main content
- **Email clients**: Folder list + message list + preview pane
- **News readers**: Article list + external website viewer
- **Dev tools**: App preview + inspector panels
## API Overview
### Creating Panels
```go
// Create a panel with URL and positioning
panel := window.NewPanel(application.WebviewPanelOptions{
Name: "content",
URL: "https://example.com",
X: 20,
Y: 60,
Width: 800,
Height: 500,
})
// Panel with anchoring (responsive to window resize)
panel := window.NewPanel(application.WebviewPanelOptions{
Name: "sidebar",
URL: "/sidebar.html",
X: 0,
Y: 0,
Width: 200,
Height: 600,
Anchor: application.AnchorTop | application.AnchorBottom | application.AnchorLeft,
})
// Panel that fills the entire window
panel := window.NewPanel(application.WebviewPanelOptions{
Name: "fullscreen",
URL: "https://wails.io",
X: 0,
Y: 0,
Width: 800,
Height: 600,
Anchor: application.AnchorFill,
})
```
### Anchor Types
Anchors control how panels respond to window resizing:
| Anchor | Behavior |
|--------|----------|
| `AnchorNone` | Fixed position and size |
| `AnchorTop` | Maintains distance from top edge |
| `AnchorBottom` | Maintains distance from bottom edge |
| `AnchorLeft` | Maintains distance from left edge |
| `AnchorRight` | Maintains distance from right edge |
| `AnchorFill` | Anchored to all edges (fills window with margins) |
Combine anchors with `|` for complex layouts:
```go
// Left sidebar that stretches vertically
Anchor: application.AnchorTop | application.AnchorBottom | application.AnchorLeft
```
### Panel Options
```go
application.WebviewPanelOptions{
// Identity
Name: "panel-name", // Unique identifier
// Content
URL: "https://example.com", // URL to load
Headers: map[string]string{ // Custom HTTP headers (optional)
"Authorization": "Bearer token",
},
UserAgent: "Custom UA", // Custom user agent (optional)
// Position & Size
X: 100, // X position (CSS pixels)
Y: 50, // Y position (CSS pixels)
Width: 800, // Width (CSS pixels)
Height: 600, // Height (CSS pixels)
ZIndex: 1, // Stacking order
Anchor: application.AnchorFill, // Resize behavior
// Appearance
Visible: boolPtr(true), // Initially visible
BackgroundColour: application.NewRGB(255, 255, 255),
Transparent: false, // Transparent background
Zoom: 1.0, // Zoom level (1.0 = 100%)
// Developer
DevToolsEnabled: boolPtr(true),
OpenInspectorOnStartup: false,
}
```
### Panel Manipulation
```go
// Position and size
panel.SetBounds(application.Rect{X: 100, Y: 50, Width: 400, Height: 300})
bounds := panel.Bounds()
// Content
panel.SetURL("https://wails.io")
panel.Reload()
panel.ForceReload() // Bypass cache
// Visibility
panel.Show()
panel.Hide()
visible := panel.IsVisible()
// Stacking order
panel.SetZIndex(10)
// Focus
panel.Focus()
focused := panel.IsFocused()
// Zoom
panel.SetZoom(1.5)
zoom := panel.GetZoom()
// Developer tools
panel.OpenDevTools()
// Cleanup
panel.Destroy()
```
### Getting Panels
```go
// Get panel by name
panel := window.GetPanel("sidebar")
// Get panel by ID
panel := window.GetPanelByID(1)
// Get all panels
panels := window.GetPanels()
// Remove panel
window.RemovePanel("sidebar")
```
## Key Differences: WebviewWindow vs WebviewPanel
| Feature | WebviewWindow | WebviewPanel |
|---------|---------------|--------------|
| Has title bar | ✅ | ❌ |
| Can be minimized/maximized | ✅ | ❌ |
| Independent window | ✅ | ❌ (child of window) |
| Can show external URLs | ✅ | ✅ |
| Multiple per app | ✅ | ✅ (multiple per window) |
| Position relative to | Screen | Parent window |
| Responsive anchoring | ❌ | ✅ |

View file

@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebviewPanel Demo</title>
<script src="/wails/runtime.js" type="module"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: rgba(0, 0, 0, 0.3);
padding: 15px 25px;
display: flex;
align-items: center;
gap: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header h1 {
font-size: 18px;
font-weight: 600;
color: #e94560;
}
.header .subtitle {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}
.nav-buttons {
display: flex;
gap: 10px;
margin-left: auto;
}
.nav-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.nav-btn.wails {
background: linear-gradient(135deg, #e94560 0%, #c73e54 100%);
color: white;
}
.nav-btn.wails:hover {
background: linear-gradient(135deg, #ff5a75 0%, #e94560 100%);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);
}
.nav-btn.google {
background: linear-gradient(135deg, #4285f4 0%, #3367d6 100%);
color: white;
}
.nav-btn.google:hover {
background: linear-gradient(135deg, #5a9aff 0%, #4285f4 100%);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(66, 133, 244, 0.4);
}
.nav-btn.active {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
}
.nav-btn .icon {
font-size: 16px;
}
.content-area {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
}
.panel-container {
flex: 1;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
position: relative;
}
.panel-label {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
z-index: 100;
pointer-events: none;
}
.panel-label .url {
color: #e94560;
font-family: monospace;
}
.info-bar {
margin-top: 15px;
padding: 12px 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}
.info-bar .tip {
display: flex;
align-items: center;
gap: 8px;
}
.info-bar .tip::before {
content: "💡";
}
.info-bar .size {
font-family: monospace;
color: #4ade80;
}
</style>
</head>
<body>
<div class="header">
<div>
<h1>🪟 WebviewPanel Demo</h1>
<div class="subtitle">Embedded webviews with responsive layout</div>
</div>
<div class="nav-buttons">
<button class="nav-btn wails active" onclick="switchTo('wails')">
<span class="icon">🚀</span>
Wails.io
</button>
<button class="nav-btn google" onclick="switchTo('google')">
<span class="icon">🔍</span>
Google.com
</button>
</div>
</div>
<div class="content-area">
<div class="panel-container" id="panelContainer">
<div class="panel-label">
Embedded Panel: <span class="url" id="currentUrl">https://wails.io</span>
</div>
<!-- The WebviewPanel will be positioned here -->
</div>
<div class="info-bar">
<div class="tip">Resize the window to see the panel respond dynamically</div>
<div class="size" id="sizeInfo">Loading...</div>
</div>
</div>
<script>
let currentSite = 'wails';
async function switchTo(site) {
if (site === currentSite) return;
currentSite = site;
const url = site === 'wails' ? 'https://wails.io' : 'https://www.google.com';
// Update UI
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector(`.nav-btn.${site}`).classList.add('active');
document.getElementById('currentUrl').textContent = url;
// Call Go to switch the panel URL
try {
await wails.Call.ByName("main.App.SwitchPanel", url);
console.log('Switched to:', url);
} catch (err) {
console.error('Failed to switch panel:', err);
}
}
// Update size display
function updateSize() {
const container = document.getElementById('panelContainer');
const rect = container.getBoundingClientRect();
document.getElementById('sizeInfo').textContent =
`Panel area: ${Math.round(rect.width)} × ${Math.round(rect.height)} px`;
}
// Update on resize
window.addEventListener('resize', updateSize);
// Initial update
setTimeout(updateSize, 100);
</script>
</body>
</html>

View file

@ -0,0 +1,105 @@
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
//go:embed assets/*
var assets embed.FS
// App struct holds application state
type App struct {
panel *application.WebviewPanel
}
// SwitchPanel switches the embedded panel to a different URL
func (a *App) SwitchPanel(url string) {
if a.panel != nil {
log.Printf("🔄 Switching panel to: %s", url)
a.panel.SetURL(url)
}
}
func main() {
appState := &App{}
app := application.New(application.Options{
Name: "WebviewPanel Demo",
Description: "Demonstrates embedding multiple webviews with switching capability",
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
Services: []application.Service{
application.NewService(appState),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create the main window with our custom UI
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "WebviewPanel Demo - Embedded Webviews",
Width: 1200,
Height: 800,
BackgroundType: application.BackgroundTypeSolid,
BackgroundColour: application.NewRGB(26, 26, 46),
URL: "/index.html",
})
// Create the embedded panel after the window is shown (only once)
var panelCreated bool
window.OnWindowEvent(events.Common.WindowShow, func(*application.WindowEvent) {
// Guard against creating multiple panels on repeated WindowShow events
if panelCreated {
return
}
panelCreated = true
// Based on actual HTML measurements:
// - Header: 41px height (with 15px/25px padding already included)
// - Content area: padding 20px
// - Panel container: 1142×591, border 1px
// - Panel should fit inside container: 1140×589
const (
headerHeight = 41 + 15*2 // 41px header + vertical padding
contentPadding = 20
containerBorder = 1
// Panel container inner size (container size minus borders)
panelContainerWidth = 1140 // 1142 - 2
panelContainerHeight = 589 // 591 - 2
)
// Panel position: content padding + container border
panelX := contentPadding + containerBorder
panelY := headerHeight + contentPadding + containerBorder
// Create a panel positioned inside the content area
// Using AnchorFill means the panel will maintain these margins when the window is resized
appState.panel = window.NewPanel(application.WebviewPanelOptions{
Name: "embedded-content",
URL: "https://wails.io",
X: panelX,
Y: panelY,
Width: panelContainerWidth,
Height: panelContainerHeight,
Anchor: application.AnchorFill, // Maintain margins on all sides when resizing
Visible: boolPtr(true),
DevToolsEnabled: boolPtr(true),
})
})
// Run the application
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
func boolPtr(b bool) *bool {
return &b
}

View file

@ -29,6 +29,8 @@ import * as IOS from "./ios.js";
import Window, { handleDragEnter, handleDragLeave, handleDragOver } from "./window.js";
import * as WML from "./wml.js";
export { Panel } from "./panel.js";
export {
Application,
Browser,

View file

@ -0,0 +1,282 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
import { newRuntimeCaller, objectNames } from "./runtime.js";
// Panel method constants - must match messageprocessor_panel.go
const SetBoundsMethod = 0;
const GetBoundsMethod = 1;
const SetZIndexMethod = 2;
const SetURLMethod = 3;
const SetHTMLMethod = 4;
const ExecJSMethod = 5;
const ReloadMethod = 6;
const ForceReloadMethod = 7;
const ShowMethod = 8;
const HideMethod = 9;
const IsVisibleMethod = 10;
const SetZoomMethod = 11;
const GetZoomMethod = 12;
const FocusMethod = 13;
const IsFocusedMethod = 14;
const OpenDevToolsMethod = 15;
const DestroyMethod = 16;
const NameMethod = 17;
/**
* A record describing the bounds (position and size) of a panel.
*/
export interface Bounds {
/** The X position of the panel within the window. */
x: number;
/** The Y position of the panel within the window. */
y: number;
/** The width of the panel. */
width: number;
/** The height of the panel. */
height: number;
}
// Private field names
const callerSym = Symbol("caller");
const panelNameSym = Symbol("panelName");
/**
* Panel represents an embedded webview panel within a window.
* Panels allow embedding multiple webview instances in a single window,
* similar to Electron's BrowserView/WebContentsView.
*/
export class Panel {
// Private fields
private [callerSym]: (method: number, args?: any) => Promise<any>;
private [panelNameSym]: string;
/**
* Creates a new Panel instance.
*
* @param panelName - The name of the panel to control.
* @param windowName - The name of the parent window (optional, defaults to current window).
*/
constructor(panelName: string, windowName: string = '') {
this[panelNameSym] = panelName;
this[callerSym] = newRuntimeCaller(objectNames.Panel, windowName);
// Bind instance methods for use in event handlers
for (const method of Object.getOwnPropertyNames(Panel.prototype)) {
if (method !== "constructor" && typeof (this as any)[method] === "function") {
(this as any)[method] = (this as any)[method].bind(this);
}
}
}
/**
* Gets a reference to the specified panel.
*
* @param panelName - The name of the panel to get.
* @param windowName - The name of the parent window (optional).
* @returns A new Panel instance.
*/
static Get(panelName: string, windowName: string = ''): Panel {
return new Panel(panelName, windowName);
}
/**
* Sets the position and size of the panel.
*
* @param bounds - The new bounds for the panel.
*/
SetBounds(bounds: Bounds): Promise<void> {
return this[callerSym](SetBoundsMethod, {
panel: this[panelNameSym],
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
});
}
/**
* Gets the current position and size of the panel.
*
* @returns The current bounds of the panel.
*/
GetBounds(): Promise<Bounds> {
return this[callerSym](GetBoundsMethod, {
panel: this[panelNameSym]
});
}
/**
* Sets the z-index (stacking order) of the panel.
*
* @param zIndex - The z-index value (higher values appear on top).
*/
SetZIndex(zIndex: number): Promise<void> {
return this[callerSym](SetZIndexMethod, {
panel: this[panelNameSym],
zIndex
});
}
/**
* Navigates the panel to the specified URL.
*
* @param url - The URL to navigate to.
*/
SetURL(url: string): Promise<void> {
return this[callerSym](SetURLMethod, {
panel: this[panelNameSym],
url
});
}
/**
* Sets the HTML content of the panel directly.
*
* @param html - The HTML content to load.
*/
SetHTML(html: string): Promise<void> {
return this[callerSym](SetHTMLMethod, {
panel: this[panelNameSym],
html
});
}
/**
* Executes JavaScript code in the panel's context.
*
* @param js - The JavaScript code to execute.
*/
ExecJS(js: string): Promise<void> {
return this[callerSym](ExecJSMethod, {
panel: this[panelNameSym],
js
});
}
/**
* Reloads the current page in the panel.
*/
Reload(): Promise<void> {
return this[callerSym](ReloadMethod, {
panel: this[panelNameSym]
});
}
/**
* Forces a reload of the page, ignoring cached content.
*/
ForceReload(): Promise<void> {
return this[callerSym](ForceReloadMethod, {
panel: this[panelNameSym]
});
}
/**
* Shows the panel (makes it visible).
*/
Show(): Promise<void> {
return this[callerSym](ShowMethod, {
panel: this[panelNameSym]
});
}
/**
* Hides the panel (makes it invisible).
*/
Hide(): Promise<void> {
return this[callerSym](HideMethod, {
panel: this[panelNameSym]
});
}
/**
* Checks if the panel is currently visible.
*
* @returns True if the panel is visible, false otherwise.
*/
IsVisible(): Promise<boolean> {
return this[callerSym](IsVisibleMethod, {
panel: this[panelNameSym]
});
}
/**
* Sets the zoom level of the panel.
*
* @param zoom - The zoom level (1.0 = 100%).
*/
SetZoom(zoom: number): Promise<void> {
return this[callerSym](SetZoomMethod, {
panel: this[panelNameSym],
zoom
});
}
/**
* Gets the current zoom level of the panel.
*
* @returns The current zoom level.
*/
GetZoom(): Promise<number> {
return this[callerSym](GetZoomMethod, {
panel: this[panelNameSym]
});
}
/**
* Focuses the panel (gives it keyboard input focus).
*/
Focus(): Promise<void> {
return this[callerSym](FocusMethod, {
panel: this[panelNameSym]
});
}
/**
* Checks if the panel currently has keyboard focus.
*
* @returns True if the panel is focused, false otherwise.
*/
IsFocused(): Promise<boolean> {
return this[callerSym](IsFocusedMethod, {
panel: this[panelNameSym]
});
}
/**
* Opens the developer tools for the panel.
*/
OpenDevTools(): Promise<void> {
return this[callerSym](OpenDevToolsMethod, {
panel: this[panelNameSym]
});
}
/**
* Destroys the panel, removing it from the window.
*/
Destroy(): Promise<void> {
return this[callerSym](DestroyMethod, {
panel: this[panelNameSym]
});
}
/**
* Gets the name of the panel.
*
* @returns The name of the panel.
*/
Name(): Promise<string> {
return this[callerSym](NameMethod, {
panel: this[panelNameSym]
});
}
}

View file

@ -29,6 +29,8 @@ export const objectNames = Object.freeze({
Browser: 9,
CancelCall: 10,
IOS: 11,
Android: 12,
Panel: 13,
});
export let clientId = nanoid();

View file

@ -27,6 +27,7 @@ const (
cancelCallRequest = 10
iosRequest = 11
androidRequest = 12
panelRequest = 13
)
var objectNames = map[int]string{
@ -43,6 +44,7 @@ var objectNames = map[int]string{
cancelCallRequest: "CancelCall",
iosRequest: "iOS",
androidRequest: "Android",
panelRequest: "Panel",
}
type RuntimeRequest struct {
@ -127,6 +129,8 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(ctx context.Context, req *Ru
return m.processIOSMethod(req, targetWindow)
case androidRequest:
return m.processAndroidMethod(req, targetWindow)
case panelRequest:
return m.processPanelMethod(req, targetWindow)
default:
return nil, errs.NewInvalidRuntimeCallErrorf("unknown object %d", req.Object)
}

View file

@ -0,0 +1,207 @@
package application
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/errs"
)
// Panel method constants for frontend-to-backend communication
const (
PanelSetBounds = 0
PanelGetBounds = 1
PanelSetZIndex = 2
PanelSetURL = 3
PanelReload = 4
PanelForceReload = 5
PanelShow = 6
PanelHide = 7
PanelIsVisible = 8
PanelSetZoom = 9
PanelGetZoom = 10
PanelFocus = 11
PanelIsFocused = 12
PanelOpenDevTools = 13
PanelDestroy = 14
PanelName = 15
)
var panelMethodNames = map[int]string{
PanelSetBounds: "SetBounds",
PanelGetBounds: "GetBounds",
PanelSetZIndex: "SetZIndex",
PanelSetURL: "SetURL",
PanelReload: "Reload",
PanelForceReload: "ForceReload",
PanelShow: "Show",
PanelHide: "Hide",
PanelIsVisible: "IsVisible",
PanelSetZoom: "SetZoom",
PanelGetZoom: "GetZoom",
PanelFocus: "Focus",
PanelIsFocused: "IsFocused",
PanelOpenDevTools: "OpenDevTools",
PanelDestroy: "Destroy",
PanelName: "Name",
}
// panelMethodHandler handles a specific panel method
type panelMethodHandler func(panel *WebviewPanel, args *MapArgs) (any, error)
// panelMethodHandlers maps method IDs to their handlers
var panelMethodHandlers = map[int]panelMethodHandler{
PanelSetBounds: handlePanelSetBounds,
PanelGetBounds: handlePanelGetBounds,
PanelSetZIndex: handlePanelSetZIndex,
PanelSetURL: handlePanelSetURL,
PanelReload: handlePanelReload,
PanelForceReload: handlePanelForceReload,
PanelShow: handlePanelShow,
PanelHide: handlePanelHide,
PanelIsVisible: handlePanelIsVisible,
PanelSetZoom: handlePanelSetZoom,
PanelGetZoom: handlePanelGetZoom,
PanelFocus: handlePanelFocus,
PanelIsFocused: handlePanelIsFocused,
PanelOpenDevTools: handlePanelOpenDevTools,
PanelDestroy: handlePanelDestroy,
PanelName: handlePanelName,
}
func (m *MessageProcessor) processPanelMethod(
req *RuntimeRequest,
window Window,
) (any, error) {
args := req.Args.AsMap()
// Get the WebviewWindow to access panels
ww, ok := window.(*WebviewWindow)
if !ok {
return nil, errs.NewInvalidRuntimeCallErrorf("window is not a WebviewWindow")
}
// Get panel name from args
panelName := args.String("panel")
if panelName == nil || *panelName == "" {
return nil, errs.NewInvalidRuntimeCallErrorf("panel name is required")
}
// Get the panel by name or ID
panel := ww.GetPanel(*panelName)
if panel == nil {
panelID := args.UInt("panelId")
if panelID != nil && *panelID > 0 {
panel = ww.GetPanelByID(uint(*panelID))
}
}
if panel == nil {
return nil, errs.NewInvalidRuntimeCallErrorf("panel not found: %s", *panelName)
}
// Look up and execute the handler
handler, ok := panelMethodHandlers[req.Method]
if !ok {
return nil, fmt.Errorf("unknown panel method: %d", req.Method)
}
return handler(panel, args)
}
func handlePanelSetBounds(panel *WebviewPanel, args *MapArgs) (any, error) {
x := args.Int("x")
y := args.Int("y")
width := args.Int("width")
height := args.Int("height")
if x == nil || y == nil || width == nil || height == nil {
return nil, errs.NewInvalidRuntimeCallErrorf("x, y, width, and height are required")
}
panel.SetBounds(Rect{X: *x, Y: *y, Width: *width, Height: *height})
return unit, nil
}
func handlePanelGetBounds(panel *WebviewPanel, _ *MapArgs) (any, error) {
bounds := panel.Bounds()
return map[string]interface{}{
"x": bounds.X,
"y": bounds.Y,
"width": bounds.Width,
"height": bounds.Height,
}, nil
}
func handlePanelSetZIndex(panel *WebviewPanel, args *MapArgs) (any, error) {
zIndex := args.Int("zIndex")
if zIndex == nil {
return nil, errs.NewInvalidRuntimeCallErrorf("zIndex is required")
}
panel.SetZIndex(*zIndex)
return unit, nil
}
func handlePanelSetURL(panel *WebviewPanel, args *MapArgs) (any, error) {
url := args.String("url")
if url == nil {
return nil, errs.NewInvalidRuntimeCallErrorf("url is required")
}
panel.SetURL(*url)
return unit, nil
}
func handlePanelReload(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.Reload()
return unit, nil
}
func handlePanelForceReload(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.ForceReload()
return unit, nil
}
func handlePanelShow(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.Show()
return unit, nil
}
func handlePanelHide(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.Hide()
return unit, nil
}
func handlePanelIsVisible(panel *WebviewPanel, _ *MapArgs) (any, error) {
return panel.IsVisible(), nil
}
func handlePanelSetZoom(panel *WebviewPanel, args *MapArgs) (any, error) {
zoom := args.Float64("zoom")
if zoom == nil {
return nil, errs.NewInvalidRuntimeCallErrorf("zoom is required")
}
panel.SetZoom(*zoom)
return unit, nil
}
func handlePanelGetZoom(panel *WebviewPanel, _ *MapArgs) (any, error) {
return panel.GetZoom(), nil
}
func handlePanelFocus(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.Focus()
return unit, nil
}
func handlePanelIsFocused(panel *WebviewPanel, _ *MapArgs) (any, error) {
return panel.IsFocused(), nil
}
func handlePanelOpenDevTools(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.OpenDevTools()
return unit, nil
}
func handlePanelDestroy(panel *WebviewPanel, _ *MapArgs) (any, error) {
panel.Destroy()
return unit, nil
}
func handlePanelName(panel *WebviewPanel, _ *MapArgs) (any, error) {
return panel.Name(), nil
}

View file

@ -0,0 +1,603 @@
package application
import (
"fmt"
"sync"
"sync/atomic"
"github.com/wailsapp/wails/v3/internal/assetserver"
)
// webviewPanelImpl is the platform-specific interface for WebviewPanel
type webviewPanelImpl interface {
// Lifecycle
create()
destroy()
// Position and size
setBounds(bounds Rect)
bounds() Rect
setZIndex(zIndex int)
// Content
setURL(url string)
reload()
forceReload()
// Visibility
show()
hide()
isVisible() bool
// Zoom
setZoom(zoom float64)
getZoom() float64
// DevTools
openDevTools()
// Focus
focus()
isFocused() bool
}
var panelID uint32
func getNextPanelID() uint {
return uint(atomic.AddUint32(&panelID, 1))
}
// WebviewPanel represents an embedded webview within a window.
// Unlike WebviewWindow, a WebviewPanel is a child view that exists within
// a parent window and can be positioned anywhere within that window.
// This is similar to Electron's BrowserView or the deprecated webview tag.
type WebviewPanel struct {
id uint
name string
options WebviewPanelOptions
impl webviewPanelImpl
parent *WebviewWindow
// Track if the panel has been destroyed
destroyed bool
destroyedLock sync.RWMutex
// Original window size when panel was created (for anchor calculations)
originalWindowWidth int
originalWindowHeight int
// Original panel bounds (for anchor calculations)
originalBounds Rect
}
// NewPanel creates a new WebviewPanel with the given options.
// Typically called via window.NewPanel() to associate the panel with a parent window.
func NewPanel(options WebviewPanelOptions) *WebviewPanel {
id := getNextPanelID()
// Apply defaults
if options.Width == 0 {
options.Width = 400
}
if options.Height == 0 {
options.Height = 300
}
if options.ZIndex == 0 {
options.ZIndex = 1
}
if options.Zoom == 0 {
options.Zoom = 1.0
}
if options.Name == "" {
options.Name = fmt.Sprintf("panel-%d", id)
}
// Default to visible
if options.Visible == nil {
visible := true
options.Visible = &visible
}
// Normalize URL via asset server for local paths
if options.URL != "" {
if normalizedURL, err := assetserver.GetStartURL(options.URL); err == nil && normalizedURL != "" {
options.URL = normalizedURL
}
}
// Store original bounds for anchor calculations
originalBounds := Rect{
X: options.X,
Y: options.Y,
Width: options.Width,
Height: options.Height,
}
return &WebviewPanel{
id: id,
name: options.Name,
options: options,
originalBounds: originalBounds,
}
}
// ID returns the unique identifier for this panel
func (p *WebviewPanel) ID() uint {
return p.id
}
// Name returns the name of this panel
func (p *WebviewPanel) Name() string {
return p.name
}
// Parent returns the parent window of this panel
func (p *WebviewPanel) Parent() *WebviewWindow {
return p.parent
}
// SetBounds sets the position and size of the panel within its parent window.
// This also updates the anchor baseline so future window resizes calculate from the new position.
func (p *WebviewPanel) SetBounds(bounds Rect) *WebviewPanel {
p.options.X = bounds.X
p.options.Y = bounds.Y
p.options.Width = bounds.Width
p.options.Height = bounds.Height
// Update anchor baseline so future resizes calculate from the new position
p.updateAnchorBaseline(bounds)
if p.impl != nil && !p.isDestroyed() {
InvokeSync(func() {
p.impl.setBounds(bounds)
})
}
return p
}
// updateAnchorBaseline updates the original bounds and window size used for anchor calculations.
// Called when the user manually changes panel bounds.
func (p *WebviewPanel) updateAnchorBaseline(bounds Rect) {
p.originalBounds = bounds
if p.parent != nil {
p.originalWindowWidth, p.originalWindowHeight = p.parent.Size()
}
}
// Bounds returns the current bounds of the panel
func (p *WebviewPanel) Bounds() Rect {
if p.impl != nil && !p.isDestroyed() {
return InvokeSyncWithResult(p.impl.bounds)
}
return Rect{
X: p.options.X,
Y: p.options.Y,
Width: p.options.Width,
Height: p.options.Height,
}
}
// SetPosition sets the position of the panel within its parent window
func (p *WebviewPanel) SetPosition(x, y int) *WebviewPanel {
bounds := p.Bounds()
bounds.X = x
bounds.Y = y
return p.SetBounds(bounds)
}
// Position returns the current position of the panel
func (p *WebviewPanel) Position() (int, int) {
bounds := p.Bounds()
return bounds.X, bounds.Y
}
// SetSize sets the size of the panel
func (p *WebviewPanel) SetSize(width, height int) *WebviewPanel {
bounds := p.Bounds()
bounds.Width = width
bounds.Height = height
return p.SetBounds(bounds)
}
// Size returns the current size of the panel
func (p *WebviewPanel) Size() (int, int) {
bounds := p.Bounds()
return bounds.Width, bounds.Height
}
// SetZIndex sets the stacking order of the panel
func (p *WebviewPanel) SetZIndex(zIndex int) *WebviewPanel {
p.options.ZIndex = zIndex
if p.impl != nil && !p.isDestroyed() {
InvokeSync(func() {
p.impl.setZIndex(zIndex)
})
}
return p
}
// ZIndex returns the current z-index of the panel
func (p *WebviewPanel) ZIndex() int {
return p.options.ZIndex
}
// SetURL navigates the panel to the specified URL
// Local paths (e.g., "/panel.html") are normalized via the asset server.
func (p *WebviewPanel) SetURL(url string) *WebviewPanel {
// Normalize URL via asset server for local paths
normalizedURL := url
if normalized, err := assetserver.GetStartURL(url); err == nil && normalized != "" {
normalizedURL = normalized
}
p.options.URL = normalizedURL
if p.impl != nil && !p.isDestroyed() {
InvokeSync(func() {
p.impl.setURL(normalizedURL)
})
}
return p
}
// URL returns the current URL of the panel
func (p *WebviewPanel) URL() string {
return p.options.URL
}
// Reload reloads the current page
func (p *WebviewPanel) Reload() {
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.reload)
}
}
// ForceReload reloads the current page, bypassing the cache
func (p *WebviewPanel) ForceReload() {
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.forceReload)
}
}
// Show makes the panel visible
func (p *WebviewPanel) Show() *WebviewPanel {
visible := true
p.options.Visible = &visible
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.show)
}
return p
}
// Hide hides the panel
func (p *WebviewPanel) Hide() *WebviewPanel {
visible := false
p.options.Visible = &visible
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.hide)
}
return p
}
// IsVisible returns whether the panel is currently visible
func (p *WebviewPanel) IsVisible() bool {
if p.impl != nil && !p.isDestroyed() {
return InvokeSyncWithResult(p.impl.isVisible)
}
return p.options.Visible != nil && *p.options.Visible
}
// SetZoom sets the zoom level of the panel
func (p *WebviewPanel) SetZoom(zoom float64) *WebviewPanel {
p.options.Zoom = zoom
if p.impl != nil && !p.isDestroyed() {
InvokeSync(func() {
p.impl.setZoom(zoom)
})
}
return p
}
// GetZoom returns the current zoom level of the panel
func (p *WebviewPanel) GetZoom() float64 {
if p.impl != nil && !p.isDestroyed() {
return InvokeSyncWithResult(p.impl.getZoom)
}
return p.options.Zoom
}
// OpenDevTools opens the developer tools for this panel
func (p *WebviewPanel) OpenDevTools() {
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.openDevTools)
}
}
// Focus gives focus to this panel
func (p *WebviewPanel) Focus() {
if p.impl != nil && !p.isDestroyed() {
InvokeSync(p.impl.focus)
}
}
// IsFocused returns whether this panel currently has focus
func (p *WebviewPanel) IsFocused() bool {
if p.impl != nil && !p.isDestroyed() {
return InvokeSyncWithResult(p.impl.isFocused)
}
return false
}
// Destroy removes the panel from its parent window and releases resources
func (p *WebviewPanel) Destroy() {
if p.isDestroyed() {
return
}
p.destroyedLock.Lock()
p.destroyed = true
p.destroyedLock.Unlock()
if p.impl != nil {
InvokeSync(p.impl.destroy)
}
// Remove from parent
if p.parent != nil {
p.parent.removePanel(p.id)
}
}
// isDestroyed returns whether the panel has been destroyed
func (p *WebviewPanel) isDestroyed() bool {
p.destroyedLock.RLock()
defer p.destroyedLock.RUnlock()
return p.destroyed
}
// run initializes the platform-specific implementation
// This is called by the parent window when the panel is added
func (p *WebviewPanel) run() {
globalApplication.debug("[Panel] run() called", "panelID", p.id, "panelName", p.name)
p.destroyedLock.Lock()
if p.impl != nil || p.destroyed {
globalApplication.debug("[Panel] run() skipped - impl already exists or destroyed",
"panelID", p.id, "hasImpl", p.impl != nil, "destroyed", p.destroyed)
p.destroyedLock.Unlock()
return
}
// Check parent window state before creating impl
if p.parent == nil {
globalApplication.error("[Panel] run() failed - parent window is nil", "panelID", p.id)
p.destroyedLock.Unlock()
return
}
if p.parent.impl == nil {
globalApplication.error("[Panel] run() failed - parent window impl is nil", "panelID", p.id, "windowID", p.parent.id)
p.destroyedLock.Unlock()
return
}
globalApplication.debug("[Panel] Creating platform impl", "panelID", p.id, "parentWindowID", p.parent.id)
p.impl = newPanelImpl(p)
p.destroyedLock.Unlock()
if p.impl == nil {
globalApplication.error("[Panel] newPanelImpl returned nil", "panelID", p.id)
return
}
globalApplication.debug("[Panel] Calling impl.create()", "panelID", p.id)
InvokeSync(p.impl.create)
globalApplication.debug("[Panel] impl.create() completed", "panelID", p.id)
}
// =========================================================================
// Layout Helper Methods
// =========================================================================
// FillWindow makes the panel fill the entire parent window.
// This is a convenience method equivalent to setting position to (0,0)
// and size to the window's content size.
func (p *WebviewPanel) FillWindow() *WebviewPanel {
if p.parent == nil {
return p
}
width, height := p.parent.Size()
return p.SetBounds(Rect{X: 0, Y: 0, Width: width, Height: height})
}
// DockLeft positions the panel on the left side of the window with the specified width.
// Height fills the window. Useful for sidebars and navigation panels.
func (p *WebviewPanel) DockLeft(width int) *WebviewPanel {
if p.parent == nil {
return p
}
_, height := p.parent.Size()
return p.SetBounds(Rect{X: 0, Y: 0, Width: width, Height: height})
}
// DockRight positions the panel on the right side of the window with the specified width.
// Height fills the window. Useful for property panels and inspectors.
func (p *WebviewPanel) DockRight(width int) *WebviewPanel {
if p.parent == nil {
return p
}
windowWidth, height := p.parent.Size()
return p.SetBounds(Rect{X: windowWidth - width, Y: 0, Width: width, Height: height})
}
// DockTop positions the panel at the top of the window with the specified height.
// Width fills the window. Useful for toolbars and header areas.
func (p *WebviewPanel) DockTop(height int) *WebviewPanel {
if p.parent == nil {
return p
}
width, _ := p.parent.Size()
return p.SetBounds(Rect{X: 0, Y: 0, Width: width, Height: height})
}
// DockBottom positions the panel at the bottom of the window with the specified height.
// Width fills the window. Useful for status bars and terminal panels.
func (p *WebviewPanel) DockBottom(height int) *WebviewPanel {
if p.parent == nil {
return p
}
width, windowHeight := p.parent.Size()
return p.SetBounds(Rect{X: 0, Y: windowHeight - height, Width: width, Height: height})
}
// FillBeside fills the remaining space beside another panel.
// The direction specifies whether to fill to the right, left, above, or below the reference panel.
func (p *WebviewPanel) FillBeside(refPanel *WebviewPanel, direction string) *WebviewPanel {
if p.parent == nil || refPanel == nil {
return p
}
windowWidth, windowHeight := p.parent.Size()
refBounds := refPanel.Bounds()
var bounds Rect
switch direction {
case "right":
bounds = Rect{
X: refBounds.X + refBounds.Width,
Y: refBounds.Y,
Width: windowWidth - (refBounds.X + refBounds.Width),
Height: refBounds.Height,
}
case "left":
bounds = Rect{
X: 0,
Y: refBounds.Y,
Width: refBounds.X,
Height: refBounds.Height,
}
case "below":
bounds = Rect{
X: refBounds.X,
Y: refBounds.Y + refBounds.Height,
Width: refBounds.Width,
Height: windowHeight - (refBounds.Y + refBounds.Height),
}
case "above":
bounds = Rect{
X: refBounds.X,
Y: 0,
Width: refBounds.Width,
Height: refBounds.Y,
}
default:
return p
}
return p.SetBounds(bounds)
}
// =========================================================================
// Anchor/Responsive Layout Methods
// =========================================================================
// initializeAnchor stores the original window size for anchor calculations.
// This is called when the panel is first attached to a window.
func (p *WebviewPanel) initializeAnchor() {
if p.parent == nil {
return
}
p.originalWindowWidth, p.originalWindowHeight = p.parent.Size()
}
// handleWindowResize recalculates the panel's bounds based on its anchor settings.
// This is called automatically when the parent window is resized.
func (p *WebviewPanel) handleWindowResize(newWindowWidth, newWindowHeight int) {
if p.isDestroyed() || p.options.Anchor == AnchorNone {
return
}
newBounds := p.calculateAnchoredBounds(newWindowWidth, newWindowHeight)
// Use internal setBounds to avoid updating anchor baseline during resize
p.setBoundsInternal(newBounds)
}
// setBoundsInternal sets bounds without updating anchor baseline.
// Used internally during window resize handling.
func (p *WebviewPanel) setBoundsInternal(bounds Rect) {
p.options.X = bounds.X
p.options.Y = bounds.Y
p.options.Width = bounds.Width
p.options.Height = bounds.Height
if p.impl != nil && !p.isDestroyed() {
InvokeSync(func() {
p.impl.setBounds(bounds)
})
}
}
// calculateAnchoredBounds computes the new bounds based on anchor settings.
func (p *WebviewPanel) calculateAnchoredBounds(newWindowWidth, newWindowHeight int) Rect {
anchor := p.options.Anchor
orig := p.originalBounds
origWinW := p.originalWindowWidth
origWinH := p.originalWindowHeight
// If original window size was not recorded, use current bounds
if origWinW == 0 || origWinH == 0 {
return Rect{
X: p.options.X,
Y: p.options.Y,
Width: p.options.Width,
Height: p.options.Height,
}
}
// Calculate distances from edges
distanceFromRight := origWinW - (orig.X + orig.Width)
distanceFromBottom := origWinH - (orig.Y + orig.Height)
newX := orig.X
newY := orig.Y
newWidth := orig.Width
newHeight := orig.Height
// Handle horizontal anchoring
hasLeft := anchor.HasAnchor(AnchorLeft)
hasRight := anchor.HasAnchor(AnchorRight)
if hasLeft && hasRight {
// Anchored to both sides - stretch horizontally
newX = orig.X
newWidth = newWindowWidth - orig.X - distanceFromRight
} else if hasRight {
// Anchored to right only - maintain distance from right
newX = newWindowWidth - distanceFromRight - orig.Width
}
// If hasLeft only or no horizontal anchor, X stays the same
// Handle vertical anchoring
hasTop := anchor.HasAnchor(AnchorTop)
hasBottom := anchor.HasAnchor(AnchorBottom)
if hasTop && hasBottom {
// Anchored to both sides - stretch vertically
newY = orig.Y
newHeight = newWindowHeight - orig.Y - distanceFromBottom
} else if hasBottom {
// Anchored to bottom only - maintain distance from bottom
newY = newWindowHeight - distanceFromBottom - orig.Height
}
// If hasTop only or no vertical anchor, Y stays the same
// Ensure minimum dimensions
if newWidth < 1 {
newWidth = 1
}
if newHeight < 1 {
newHeight = 1
}
return Rect{
X: newX,
Y: newY,
Width: newWidth,
Height: newHeight,
}
}

View file

@ -0,0 +1,79 @@
//go:build android
package application
// Android stub implementation for WebviewPanel
// Panels are not yet supported on Android.
// All methods are no-ops until Android platform support is implemented.
type androidPanelImpl struct {
panel *WebviewPanel
}
func newPanelImpl(panel *WebviewPanel) webviewPanelImpl {
return &androidPanelImpl{panel: panel}
}
func (p *androidPanelImpl) create() {
// Not implemented on Android
}
func (p *androidPanelImpl) destroy() {
// Not implemented on Android
}
func (p *androidPanelImpl) setBounds(_ Rect) {
// Not implemented on Android
}
func (p *androidPanelImpl) bounds() Rect {
return Rect{}
}
func (p *androidPanelImpl) setZIndex(_ int) {
// Not implemented on Android
}
func (p *androidPanelImpl) setURL(_ string) {
// Not implemented on Android
}
func (p *androidPanelImpl) reload() {
// Not implemented on Android
}
func (p *androidPanelImpl) forceReload() {
// Not implemented on Android
}
func (p *androidPanelImpl) show() {
// Not implemented on Android
}
func (p *androidPanelImpl) hide() {
// Not implemented on Android
}
func (p *androidPanelImpl) isVisible() bool {
return false
}
func (p *androidPanelImpl) setZoom(_ float64) {
// Not implemented on Android
}
func (p *androidPanelImpl) getZoom() float64 {
return 1.0
}
func (p *androidPanelImpl) openDevTools() {
// Not implemented on Android
}
func (p *androidPanelImpl) focus() {
// Not implemented on Android
}
func (p *androidPanelImpl) isFocused() bool {
return false
}

View file

@ -0,0 +1,427 @@
//go:build darwin && !ios
package application
/*
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
#cgo LDFLAGS: -framework Cocoa -framework WebKit
#include <stdlib.h>
#include "Cocoa/Cocoa.h"
#import <WebKit/WebKit.h>
// WebviewPanel delegate for handling messages
@interface WebviewPanelDelegate : NSObject <WKScriptMessageHandler, WKNavigationDelegate>
@property unsigned int panelId;
@property unsigned int windowId;
@property (assign) WKWebView* webView;
@end
@implementation WebviewPanelDelegate
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// Handle messages from the panel's webview
// For now, log them - in future this could route to Go
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// Navigation completed callback
extern void panelNavigationCompleted(unsigned int windowId, unsigned int panelId);
panelNavigationCompleted(self.windowId, self.panelId);
}
@end
// Create a new WebviewPanel
void* panelNew(unsigned int panelId, unsigned int windowId, void* parentWindow, int x, int y, int width, int height, bool transparent) {
WebviewWindow* window = (WebviewWindow*)parentWindow;
NSView* contentView = [window contentView];
// Calculate frame (macOS uses bottom-left origin)
NSRect contentBounds = [contentView bounds];
NSRect frame = NSMakeRect(x, contentBounds.size.height - y - height, width, height);
// Create WKWebView configuration
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
[config autorelease];
config.suppressesIncrementalRendering = true;
config.applicationNameForUserAgent = @"wails.io";
// Setup user content controller
WKUserContentController* userContentController = [WKUserContentController new];
[userContentController autorelease];
WebviewPanelDelegate* delegate = [[WebviewPanelDelegate alloc] init];
[delegate autorelease];
delegate.panelId = panelId;
delegate.windowId = windowId;
[userContentController addScriptMessageHandler:delegate name:@"external"];
config.userContentController = userContentController;
// Create the WKWebView
WKWebView* webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
delegate.webView = webView;
// Set navigation delegate so didFinishNavigation callback fires
[webView setNavigationDelegate:delegate];
// Configure webview
[webView setAutoresizingMask:NSViewNotSizable];
if (transparent) {
[webView setValue:@NO forKey:@"drawsBackground"];
}
// Add to parent window's content view
[contentView addSubview:webView];
return webView;
}
// Destroy a WebviewPanel
void panelDestroy(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[webView removeFromSuperview];
[webView release];
}
// Set panel bounds
void panelSetBounds(void* panel, void* parentWindow, int x, int y, int width, int height) {
WKWebView* webView = (WKWebView*)panel;
WebviewWindow* window = (WebviewWindow*)parentWindow;
NSView* contentView = [window contentView];
// Calculate frame (macOS uses bottom-left origin)
NSRect contentBounds = [contentView bounds];
NSRect frame = NSMakeRect(x, contentBounds.size.height - y - height, width, height);
[webView setFrame:frame];
}
// Get panel bounds
void panelGetBounds(void* panel, void* parentWindow, int* x, int* y, int* width, int* height) {
WKWebView* webView = (WKWebView*)panel;
WebviewWindow* window = (WebviewWindow*)parentWindow;
NSView* contentView = [window contentView];
NSRect frame = [webView frame];
NSRect contentBounds = [contentView bounds];
*x = (int)frame.origin.x;
*y = (int)(contentBounds.size.height - frame.origin.y - frame.size.height);
*width = (int)frame.size.width;
*height = (int)frame.size.height;
}
// Set panel z-index (bring to front or send to back)
// Note: This is a binary implementation - panels are either on top (zIndex > 0)
// or at the bottom (zIndex <= 0). Granular z-index ordering would require tracking
// all panels and repositioning them relative to each other using NSWindowOrderingMode.
void panelSetZIndex(void* panel, void* parentWindow, int zIndex) {
WKWebView* webView = (WKWebView*)panel;
WebviewWindow* window = (WebviewWindow*)parentWindow;
NSView* contentView = [window contentView];
if (zIndex > 0) {
// Bring to front
[webView removeFromSuperview];
[contentView addSubview:webView positioned:NSWindowAbove relativeTo:nil];
} else {
// Send to back (but above main webview which is at index 0)
[webView removeFromSuperview];
[contentView addSubview:webView positioned:NSWindowBelow relativeTo:nil];
}
}
// Navigate to URL
void panelLoadURL(void* panel, const char* url) {
WKWebView* webView = (WKWebView*)panel;
NSURL* nsURL = [NSURL URLWithString:[NSString stringWithUTF8String:url]];
NSURLRequest* request = [NSURLRequest requestWithURL:nsURL];
[webView loadRequest:request];
}
// Reload
void panelReload(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[webView reload];
}
// Force reload (bypass cache)
void panelForceReload(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[webView reloadFromOrigin];
}
// Show panel
void panelShow(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[webView setHidden:NO];
}
// Hide panel
void panelHide(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[webView setHidden:YES];
}
// Check if visible
bool panelIsVisible(void* panel) {
WKWebView* webView = (WKWebView*)panel;
return ![webView isHidden];
}
// Set zoom
void panelSetZoom(void* panel, double zoom) {
WKWebView* webView = (WKWebView*)panel;
[webView setMagnification:zoom];
}
// Get zoom
double panelGetZoom(void* panel) {
WKWebView* webView = (WKWebView*)panel;
return [webView magnification];
}
// Open DevTools (inspector)
void panelOpenDevTools(void* panel) {
WKWebView* webView = (WKWebView*)panel;
// Note: Opening inspector programmatically requires private API
// This is a no-op for now - users can right-click -> Inspect Element if enabled
}
// Focus panel
void panelFocus(void* panel) {
WKWebView* webView = (WKWebView*)panel;
[[webView window] makeFirstResponder:webView];
}
// Check if focused
bool panelIsFocused(void* panel) {
WKWebView* webView = (WKWebView*)panel;
NSWindow* window = [webView window];
return [window firstResponder] == webView;
}
// Set background color
void panelSetBackgroundColour(void* panel, int r, int g, int b, int a) {
WKWebView* webView = (WKWebView*)panel;
if (a == 0) {
[webView setValue:@NO forKey:@"drawsBackground"];
} else {
[webView setValue:[NSColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a/255.0] forKey:@"backgroundColor"];
}
}
*/
import "C"
import (
"unsafe"
)
type darwinPanelImpl struct {
panel *WebviewPanel
webview unsafe.Pointer
parentNSWindow unsafe.Pointer
}
func newPanelImpl(panel *WebviewPanel) webviewPanelImpl {
parentWindow := panel.parent
if parentWindow == nil || parentWindow.impl == nil {
return nil
}
darwinParent, ok := parentWindow.impl.(*macosWebviewWindow)
if !ok {
return nil
}
return &darwinPanelImpl{
panel: panel,
parentNSWindow: darwinParent.nsWindow,
}
}
//export panelNavigationCompleted
func panelNavigationCompleted(windowID C.uint, panelID C.uint) {
// Navigation completed callback - could be used for future functionality
globalApplication.debug("panelNavigationCompleted", "windowID", uint(windowID), "panelID", uint(panelID))
}
func (p *darwinPanelImpl) create() {
options := p.panel.options
transparent := options.Transparent
p.webview = C.panelNew(
C.uint(p.panel.id),
C.uint(p.panel.parent.id),
p.parentNSWindow,
C.int(options.X),
C.int(options.Y),
C.int(options.Width),
C.int(options.Height),
C.bool(transparent),
)
// Set background colour if not transparent
if !transparent {
C.panelSetBackgroundColour(
p.webview,
C.int(options.BackgroundColour.Red),
C.int(options.BackgroundColour.Green),
C.int(options.BackgroundColour.Blue),
C.int(options.BackgroundColour.Alpha),
)
}
// Set initial visibility
if options.Visible != nil && !*options.Visible {
C.panelHide(p.webview)
}
// Set zoom if specified
if options.Zoom > 0 && options.Zoom != 1.0 {
C.panelSetZoom(p.webview, C.double(options.Zoom))
}
// Navigate to initial URL
if options.URL != "" {
// TODO: Add support for custom headers when WKWebView supports it
if len(options.Headers) > 0 {
globalApplication.debug("[Panel-Darwin] Custom headers specified (not yet supported)",
"panelID", p.panel.id,
"headers", options.Headers)
}
url := C.CString(options.URL)
C.panelLoadURL(p.webview, url)
C.free(unsafe.Pointer(url))
}
// Note: markRuntimeLoaded() is called in panelNavigationCompleted callback
// when the navigation completes
}
func (p *darwinPanelImpl) destroy() {
if p.webview != nil {
C.panelDestroy(p.webview)
p.webview = nil
}
}
func (p *darwinPanelImpl) setBounds(bounds Rect) {
if p.webview == nil {
return
}
C.panelSetBounds(
p.webview,
p.parentNSWindow,
C.int(bounds.X),
C.int(bounds.Y),
C.int(bounds.Width),
C.int(bounds.Height),
)
}
func (p *darwinPanelImpl) bounds() Rect {
if p.webview == nil {
return Rect{}
}
var x, y, width, height C.int
C.panelGetBounds(p.webview, p.parentNSWindow, &x, &y, &width, &height)
return Rect{
X: int(x),
Y: int(y),
Width: int(width),
Height: int(height),
}
}
func (p *darwinPanelImpl) setZIndex(zIndex int) {
if p.webview == nil {
return
}
C.panelSetZIndex(p.webview, p.parentNSWindow, C.int(zIndex))
}
func (p *darwinPanelImpl) setURL(url string) {
if p.webview == nil {
return
}
urlStr := C.CString(url)
defer C.free(unsafe.Pointer(urlStr))
C.panelLoadURL(p.webview, urlStr)
}
func (p *darwinPanelImpl) reload() {
if p.webview == nil {
return
}
C.panelReload(p.webview)
}
func (p *darwinPanelImpl) forceReload() {
if p.webview == nil {
return
}
C.panelForceReload(p.webview)
}
func (p *darwinPanelImpl) show() {
if p.webview == nil {
return
}
C.panelShow(p.webview)
}
func (p *darwinPanelImpl) hide() {
if p.webview == nil {
return
}
C.panelHide(p.webview)
}
func (p *darwinPanelImpl) isVisible() bool {
if p.webview == nil {
return false
}
return bool(C.panelIsVisible(p.webview))
}
func (p *darwinPanelImpl) setZoom(zoom float64) {
if p.webview == nil {
return
}
C.panelSetZoom(p.webview, C.double(zoom))
}
func (p *darwinPanelImpl) getZoom() float64 {
if p.webview == nil {
return 1.0
}
return float64(C.panelGetZoom(p.webview))
}
func (p *darwinPanelImpl) openDevTools() {
if p.webview == nil {
return
}
C.panelOpenDevTools(p.webview)
}
func (p *darwinPanelImpl) focus() {
if p.webview == nil {
return
}
C.panelFocus(p.webview)
}
func (p *darwinPanelImpl) isFocused() bool {
if p.webview == nil {
return false
}
return bool(C.panelIsFocused(p.webview))
}

View file

@ -0,0 +1,79 @@
//go:build ios
package application
// iOS stub implementation for WebviewPanel
// Panels are not yet supported on iOS.
// All methods are no-ops until iOS platform support is implemented.
type iosPanelImpl struct {
panel *WebviewPanel
}
func newPanelImpl(panel *WebviewPanel) webviewPanelImpl {
return &iosPanelImpl{panel: panel}
}
func (p *iosPanelImpl) create() {
// Not implemented on iOS
}
func (p *iosPanelImpl) destroy() {
// Not implemented on iOS
}
func (p *iosPanelImpl) setBounds(_ Rect) {
// Not implemented on iOS
}
func (p *iosPanelImpl) bounds() Rect {
return Rect{}
}
func (p *iosPanelImpl) setZIndex(_ int) {
// Not implemented on iOS
}
func (p *iosPanelImpl) setURL(_ string) {
// Not implemented on iOS
}
func (p *iosPanelImpl) reload() {
// Not implemented on iOS
}
func (p *iosPanelImpl) forceReload() {
// Not implemented on iOS
}
func (p *iosPanelImpl) show() {
// Not implemented on iOS
}
func (p *iosPanelImpl) hide() {
// Not implemented on iOS
}
func (p *iosPanelImpl) isVisible() bool {
return false
}
func (p *iosPanelImpl) setZoom(_ float64) {
// Not implemented on iOS
}
func (p *iosPanelImpl) getZoom() float64 {
return 1.0
}
func (p *iosPanelImpl) openDevTools() {
// Not implemented on iOS
}
func (p *iosPanelImpl) focus() {
// Not implemented on iOS
}
func (p *iosPanelImpl) isFocused() bool {
return false
}

View file

@ -0,0 +1,352 @@
//go:build linux && cgo && !android
package application
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 gdk-3.0
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <stdlib.h>
// Create a new WebKitWebView for a panel
static GtkWidget* panel_new_webview() {
WebKitUserContentManager *manager = webkit_user_content_manager_new();
GtkWidget *webView = webkit_web_view_new_with_user_content_manager(manager);
return webView;
}
// Create a fixed container to hold the panel webview at specific position
static GtkWidget* panel_new_fixed() {
return gtk_fixed_new();
}
// Add webview to fixed container at position
static void panel_fixed_put(GtkWidget *fixed, GtkWidget *webview, int x, int y) {
gtk_fixed_put(GTK_FIXED(fixed), webview, x, y);
}
// Move webview in fixed container
static void panel_fixed_move(GtkWidget *fixed, GtkWidget *webview, int x, int y) {
gtk_fixed_move(GTK_FIXED(fixed), webview, x, y);
}
// Set webview size
static void panel_set_size(GtkWidget *webview, int width, int height) {
gtk_widget_set_size_request(webview, width, height);
}
// Load URL in webview
static void panel_load_url(GtkWidget *webview, const char *url) {
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url);
}
// Reload webview
static void panel_reload(GtkWidget *webview) {
webkit_web_view_reload(WEBKIT_WEB_VIEW(webview));
}
// Force reload webview (bypass cache)
static void panel_force_reload(GtkWidget *webview) {
webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(webview));
}
// Show webview
static void panel_show(GtkWidget *webview) {
gtk_widget_show(webview);
}
// Hide webview
static void panel_hide(GtkWidget *webview) {
gtk_widget_hide(webview);
}
// Check if visible
static gboolean panel_is_visible(GtkWidget *webview) {
return gtk_widget_get_visible(webview);
}
// Set zoom level
static void panel_set_zoom(GtkWidget *webview, double zoom) {
webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(webview), zoom);
}
// Get zoom level
static double panel_get_zoom(GtkWidget *webview) {
return webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(webview));
}
// Open inspector
static void panel_open_devtools(GtkWidget *webview) {
WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(webview));
webkit_web_inspector_show(inspector);
}
// Focus webview
static void panel_focus(GtkWidget *webview) {
gtk_widget_grab_focus(webview);
}
// Check if focused
static gboolean panel_is_focused(GtkWidget *webview) {
return gtk_widget_has_focus(webview);
}
// Set background color
static void panel_set_background_color(GtkWidget *webview, int r, int g, int b, int a) {
GdkRGBA color;
color.red = r / 255.0;
color.green = g / 255.0;
color.blue = b / 255.0;
color.alpha = a / 255.0;
webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(webview), &color);
}
// Enable/disable devtools
static void panel_enable_devtools(GtkWidget *webview, gboolean enable) {
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
webkit_settings_set_enable_developer_extras(settings, enable);
}
// Destroy the panel webview
static void panel_destroy(GtkWidget *webview) {
gtk_widget_destroy(webview);
}
// Get position allocation
static void panel_get_allocation(GtkWidget *webview, int *x, int *y, int *width, int *height) {
GtkAllocation alloc;
gtk_widget_get_allocation(webview, &alloc);
*x = alloc.x;
*y = alloc.y;
*width = alloc.width;
*height = alloc.height;
}
*/
import "C"
import (
"unsafe"
)
type linuxPanelImpl struct {
panel *WebviewPanel
webview *C.GtkWidget
fixed *C.GtkWidget // Fixed container to position the webview
parent *linuxWebviewWindow
}
func newPanelImpl(panel *WebviewPanel) webviewPanelImpl {
parentWindow := panel.parent
if parentWindow == nil || parentWindow.impl == nil {
return nil
}
linuxParent, ok := parentWindow.impl.(*linuxWebviewWindow)
if !ok {
return nil
}
return &linuxPanelImpl{
panel: panel,
parent: linuxParent,
}
}
func (p *linuxPanelImpl) create() {
options := p.panel.options
// Create the webview
p.webview = C.panel_new_webview()
// Set size
C.panel_set_size(p.webview, C.int(options.Width), C.int(options.Height))
// Create a fixed container if the parent's vbox doesn't have one for panels
// For simplicity, we'll use an overlay approach - add the webview directly to the vbox
// and use CSS/GTK positioning
// Actually, we need to use GtkFixed or GtkOverlay for absolute positioning
// For now, let's use the overlay approach with GtkFixed
p.fixed = C.panel_new_fixed()
// Add the webview to the fixed container at the specified position
C.panel_fixed_put(p.fixed, p.webview, C.int(options.X), C.int(options.Y))
// Add the fixed container to the parent's vbox (above the main webview)
vbox := (*C.GtkBox)(p.parent.vbox)
C.gtk_box_pack_start(vbox, p.fixed, 0, 0, 0) // Don't expand
// Enable devtools if in debug mode
debugMode := globalApplication.isDebugMode
devToolsEnabled := debugMode
if options.DevToolsEnabled != nil {
devToolsEnabled = *options.DevToolsEnabled
}
C.panel_enable_devtools(p.webview, C.gboolean(boolToInt(devToolsEnabled)))
// Set background color
if options.Transparent {
C.panel_set_background_color(p.webview, 0, 0, 0, 0)
} else {
C.panel_set_background_color(p.webview,
C.int(options.BackgroundColour.Red),
C.int(options.BackgroundColour.Green),
C.int(options.BackgroundColour.Blue),
C.int(options.BackgroundColour.Alpha),
)
}
// Set zoom if specified
if options.Zoom > 0 && options.Zoom != 1.0 {
C.panel_set_zoom(p.webview, C.double(options.Zoom))
}
// Set initial visibility
if options.Visible == nil || *options.Visible {
C.gtk_widget_show_all(p.fixed)
}
// Navigate to initial URL
if options.URL != "" {
// TODO: Add support for custom headers when WebKitWebView supports it
if len(options.Headers) > 0 {
globalApplication.debug("[Panel-Linux] Custom headers specified (not yet supported)",
"panelID", p.panel.id,
"headers", options.Headers)
}
url := C.CString(options.URL)
defer C.free(unsafe.Pointer(url))
C.panel_load_url(p.webview, url)
}
// Open inspector if requested
if debugMode && options.OpenInspectorOnStartup {
C.panel_open_devtools(p.webview)
}
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func (p *linuxPanelImpl) destroy() {
if p.fixed != nil {
C.panel_destroy(p.fixed)
p.fixed = nil
p.webview = nil
}
}
func (p *linuxPanelImpl) setBounds(bounds Rect) {
if p.webview == nil || p.fixed == nil {
return
}
C.panel_fixed_move(p.fixed, p.webview, C.int(bounds.X), C.int(bounds.Y))
C.panel_set_size(p.webview, C.int(bounds.Width), C.int(bounds.Height))
}
func (p *linuxPanelImpl) bounds() Rect {
if p.webview == nil {
return Rect{}
}
var x, y, width, height C.int
C.panel_get_allocation(p.webview, &x, &y, &width, &height)
return Rect{
X: int(x),
Y: int(y),
Width: int(width),
Height: int(height),
}
}
func (p *linuxPanelImpl) setZIndex(_ int) {
// GTK doesn't have a direct z-index concept
// We could use gtk_box_reorder_child to change ordering
// For now, this is a no-op
}
func (p *linuxPanelImpl) setURL(url string) {
if p.webview == nil {
return
}
urlStr := C.CString(url)
defer C.free(unsafe.Pointer(urlStr))
C.panel_load_url(p.webview, urlStr)
}
func (p *linuxPanelImpl) reload() {
if p.webview == nil {
return
}
C.panel_reload(p.webview)
}
func (p *linuxPanelImpl) forceReload() {
if p.webview == nil {
return
}
C.panel_force_reload(p.webview)
}
func (p *linuxPanelImpl) show() {
if p.fixed == nil {
return
}
C.gtk_widget_show_all(p.fixed)
}
func (p *linuxPanelImpl) hide() {
if p.fixed == nil {
return
}
C.gtk_widget_hide(p.fixed)
}
func (p *linuxPanelImpl) isVisible() bool {
if p.fixed == nil {
return false
}
return C.gtk_widget_get_visible(p.fixed) != 0
}
func (p *linuxPanelImpl) setZoom(zoom float64) {
if p.webview == nil {
return
}
C.panel_set_zoom(p.webview, C.double(zoom))
}
func (p *linuxPanelImpl) getZoom() float64 {
if p.webview == nil {
return 1.0
}
return float64(C.panel_get_zoom(p.webview))
}
func (p *linuxPanelImpl) openDevTools() {
if p.webview == nil {
return
}
C.panel_open_devtools(p.webview)
}
func (p *linuxPanelImpl) focus() {
if p.webview == nil {
return
}
C.panel_focus(p.webview)
}
func (p *linuxPanelImpl) isFocused() bool {
if p.webview == nil {
return false
}
return C.panel_is_focused(p.webview) != 0
}

View file

@ -0,0 +1,157 @@
package application
// WebviewPanelOptions contains options for creating a WebviewPanel.
// Panels are absolutely positioned webview containers within a window,
// similar to Electron's BrowserView or iframes in web development.
//
// Example - Simple panel:
//
// panel := window.NewPanel(application.WebviewPanelOptions{
// Name: "browser",
// URL: "https://example.com",
// X: 0, Y: 50, Width: 800, Height: 600,
// })
//
// Example - Panel with custom headers and anchoring:
//
// sidebar := window.NewPanel(application.WebviewPanelOptions{
// Name: "api-panel",
// URL: "https://api.example.com/dashboard",
// Headers: map[string]string{"Authorization": "Bearer token123"},
// X: 0, Y: 0, Width: 200, Height: 600,
// Anchor: application.AnchorTop | application.AnchorBottom | application.AnchorLeft,
// })
type WebviewPanelOptions struct {
// Name is a unique identifier for the panel within its parent window.
// Used for retrieving panels via window.GetPanel(name).
// If empty, a name will be auto-generated (e.g., "panel-1").
Name string
// ==================== Content ====================
// URL is the URL to load in the panel.
// Can be:
// - An external URL (e.g., "https://example.com")
// - A local path served by the asset server (e.g., "/panel.html")
URL string
// Headers are custom HTTP headers to send with the initial request.
// These headers are only applied to the initial navigation.
// Example: {"Authorization": "Bearer token", "X-Custom-Header": "value"}
Headers map[string]string
// UserAgent overrides the default user agent string for this panel.
// If empty, uses the default WebView2/WebKit user agent.
UserAgent string
// ==================== Position & Size ====================
// X is the horizontal position of the panel relative to the parent window's content area.
// Uses CSS pixels (device-independent).
X int
// Y is the vertical position of the panel relative to the parent window's content area.
// Uses CSS pixels (device-independent).
Y int
// Width is the width of the panel in CSS pixels.
// If 0, defaults to 400.
Width int
// Height is the height of the panel in CSS pixels.
// If 0, defaults to 300.
Height int
// ZIndex controls the stacking order of panels within the window.
// Higher values appear on top of lower values.
// The main webview has an effective ZIndex of 0.
// Default: 1
ZIndex int
// Anchor specifies how the panel should respond to window resizing.
// When anchored to an edge, the panel maintains its distance from that edge.
//
// Examples:
// - AnchorLeft | AnchorTop: Panel stays in top-left corner
// - AnchorLeft | AnchorTop | AnchorBottom: Left sidebar that stretches vertically
// - AnchorFill: Panel fills the entire window
//
// See also: DockLeft(), DockRight(), DockTop(), DockBottom(), FillWindow()
Anchor AnchorType
// ==================== Appearance ====================
// Visible controls whether the panel is initially visible.
// Default: true
Visible *bool
// BackgroundColour is the background color of the panel.
// Only used when Transparent is false.
BackgroundColour RGBA
// Transparent makes the panel background transparent.
// Useful for overlays or panels with rounded corners.
// Default: false
Transparent bool
// Frameless removes the default styling/border around the panel.
// Default: false
Frameless bool
// Zoom is the initial zoom level of the panel.
// 1.0 = 100%, 1.5 = 150%, etc.
// Default: 1.0
Zoom float64
// ==================== Developer Options ====================
// DevToolsEnabled enables the developer tools for this panel.
// Default: follows the application's debug mode setting
DevToolsEnabled *bool
// OpenInspectorOnStartup will open the inspector when the panel is first shown.
// Only works when DevToolsEnabled is true or app is in debug mode.
OpenInspectorOnStartup bool
}
// AnchorType defines how a panel is anchored within its parent window.
// Multiple anchors can be combined using bitwise OR.
//
// When a window is resized:
// - Anchored edges maintain their distance from the window edge
// - Non-anchored edges allow the panel to stretch/shrink
//
// Example combinations:
// - AnchorLeft: Panel stays on left, doesn't resize
// - AnchorLeft | AnchorRight: Panel stretches horizontally with window
// - AnchorTop | AnchorLeft | AnchorBottom: Left sidebar that stretches vertically
type AnchorType uint8
const (
// AnchorNone - panel uses absolute positioning only (default)
// Panel position and size remain fixed regardless of window size changes.
AnchorNone AnchorType = 0
// AnchorTop - panel maintains distance from top edge
AnchorTop AnchorType = 1 << iota
// AnchorBottom - panel maintains distance from bottom edge
AnchorBottom
// AnchorLeft - panel maintains distance from left edge
AnchorLeft
// AnchorRight - panel maintains distance from right edge
AnchorRight
// AnchorFill - panel fills the entire window (anchored to all edges)
// Equivalent to: AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
AnchorFill AnchorType = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
)
// HasAnchor checks if the anchor type includes a specific anchor.
func (a AnchorType) HasAnchor(anchor AnchorType) bool {
return a&anchor == anchor
}
// Note: Rect is defined in screenmanager.go

View file

@ -0,0 +1,425 @@
//go:build windows
package application
import (
"fmt"
"os"
"path/filepath"
"github.com/wailsapp/go-webview2/pkg/edge"
"github.com/wailsapp/wails/v3/internal/assetserver"
"github.com/wailsapp/wails/v3/pkg/w32"
)
type windowsPanelImpl struct {
panel *WebviewPanel
parent *windowsWebviewWindow
chromium *edge.Chromium
hwnd w32.HWND // Child window handle to host the WebView2
// Track navigation state
navigationCompleted bool
}
func newPanelImpl(panel *WebviewPanel) webviewPanelImpl {
parentWindow := panel.parent
if parentWindow == nil || parentWindow.impl == nil {
globalApplication.error("[Panel-Windows] newPanelImpl: parent or parent.impl is nil",
"parentNil", parentWindow == nil,
"parentImplNil", parentWindow != nil && parentWindow.impl == nil)
return nil
}
windowsParent, ok := parentWindow.impl.(*windowsWebviewWindow)
if !ok {
globalApplication.error("[Panel-Windows] newPanelImpl: parent.impl is not *windowsWebviewWindow")
return nil
}
globalApplication.debug("[Panel-Windows] newPanelImpl: created impl",
"panelID", panel.id,
"parentHWND", windowsParent.hwnd)
return &windowsPanelImpl{
panel: panel,
parent: windowsParent,
}
}
func (p *windowsPanelImpl) create() {
options := p.panel.options
globalApplication.debug("[Panel-Windows] create() starting",
"panelID", p.panel.id,
"panelName", p.panel.name,
"bounds", fmt.Sprintf("x=%d,y=%d,w=%d,h=%d", options.X, options.Y, options.Width, options.Height),
"parentHWND", p.parent.hwnd)
// Create a child window to host the WebView2
// We use WS_CHILD style to make it a child of the parent window
style := uint(w32.WS_CHILD | w32.WS_CLIPSIBLINGS)
if options.Visible == nil || *options.Visible {
style |= w32.WS_VISIBLE
}
// Convert DIP coordinates to physical pixels
bounds := DipToPhysicalRect(Rect{
X: options.X,
Y: options.Y,
Width: options.Width,
Height: options.Height,
})
globalApplication.debug("[Panel-Windows] Physical bounds after DIP conversion",
"panelID", p.panel.id,
"physicalBounds", fmt.Sprintf("x=%d,y=%d,w=%d,h=%d", bounds.X, bounds.Y, bounds.Width, bounds.Height))
// Create the child window
p.hwnd = w32.CreateWindowEx(
0,
w32.MustStringToUTF16Ptr("STATIC"), // Using STATIC class for the container
nil,
style,
bounds.X,
bounds.Y,
bounds.Width,
bounds.Height,
p.parent.hwnd,
0,
w32.GetModuleHandle(""),
nil,
)
if p.hwnd == 0 {
lastErr := w32.GetLastError()
globalApplication.error("[Panel-Windows] failed to create panel child window",
"panelID", p.panel.id,
"lastError", lastErr)
return
}
globalApplication.debug("[Panel-Windows] Child window created",
"panelID", p.panel.id,
"panelHWND", p.hwnd)
// Setup WebView2 (Chromium)
p.setupChromium()
}
func (p *windowsPanelImpl) setupChromium() {
globalApplication.debug("[Panel-Windows] setupChromium() starting", "panelID", p.panel.id)
p.chromium = edge.NewChromium()
if globalApplication.options.ErrorHandler != nil {
p.chromium.SetErrorCallback(globalApplication.options.ErrorHandler)
}
// Configure chromium with a unique data path for this panel
// WebView2 requires separate user data folders when multiple WebView2 instances
// are created in the same process with different environments
baseDataPath := globalApplication.options.Windows.WebviewUserDataPath
if baseDataPath == "" {
// Use default path based on app name
baseDataPath = filepath.Join(os.Getenv("AppData"), "wails-panels")
}
// Create a unique subdirectory for this panel
p.chromium.DataPath = filepath.Join(baseDataPath, fmt.Sprintf("panel-%d", p.panel.id))
p.chromium.BrowserPath = globalApplication.options.Windows.WebviewBrowserPath
globalApplication.debug("[Panel-Windows] Using DataPath", "panelID", p.panel.id, "dataPath", p.chromium.DataPath)
// Set up callbacks
p.chromium.MessageCallback = p.processMessage
p.chromium.NavigationCompletedCallback = p.navigationCompletedCallback
globalApplication.debug("[Panel-Windows] Calling Embed()", "panelID", p.panel.id, "hwnd", p.hwnd)
// Embed the WebView2 into our child window
p.chromium.Embed(p.hwnd)
globalApplication.debug("[Panel-Windows] Embed() completed, calling Resize()", "panelID", p.panel.id)
p.chromium.Resize()
// Configure settings
settings, err := p.chromium.GetSettings()
if err != nil {
globalApplication.error("[Panel-Windows] failed to get chromium settings", "panelID", p.panel.id, "error", err)
return
}
globalApplication.debug("[Panel-Windows] Settings obtained successfully", "panelID", p.panel.id)
debugMode := globalApplication.isDebugMode
// Disable context menus unless in debug mode or explicitly enabled
devToolsEnabled := debugMode
if p.panel.options.DevToolsEnabled != nil {
devToolsEnabled = *p.panel.options.DevToolsEnabled
}
err = settings.PutAreDefaultContextMenusEnabled(devToolsEnabled)
if err != nil {
globalApplication.error("[Panel-Windows] failed to configure context menus", "panelID", p.panel.id, "error", err)
}
err = settings.PutAreDevToolsEnabled(devToolsEnabled)
if err != nil {
globalApplication.error("[Panel-Windows] failed to configure devtools", "panelID", p.panel.id, "error", err)
}
// Set zoom if specified
if p.panel.options.Zoom > 0 && p.panel.options.Zoom != 1.0 {
p.chromium.PutZoomFactor(p.panel.options.Zoom)
}
// Set background colour
if p.panel.options.Transparent {
p.chromium.SetBackgroundColour(0, 0, 0, 0)
} else {
p.chromium.SetBackgroundColour(
p.panel.options.BackgroundColour.Red,
p.panel.options.BackgroundColour.Green,
p.panel.options.BackgroundColour.Blue,
p.panel.options.BackgroundColour.Alpha,
)
}
// Navigate to initial URL
if p.panel.options.URL != "" {
globalApplication.debug("[Panel-Windows] Navigating to URL", "panelID", p.panel.id, "url", p.panel.options.URL)
startURL, err := assetserver.GetStartURL(p.panel.options.URL)
if err != nil {
globalApplication.error("[Panel-Windows] failed to get start URL", "panelID", p.panel.id, "error", err)
return
}
globalApplication.debug("[Panel-Windows] Resolved start URL", "panelID", p.panel.id, "startURL", startURL)
// TODO: Add support for custom headers when WebView2 supports it
// For now, headers are logged but not applied
if len(p.panel.options.Headers) > 0 {
globalApplication.debug("[Panel-Windows] Custom headers specified (not yet supported)",
"panelID", p.panel.id,
"headers", p.panel.options.Headers)
}
p.chromium.Navigate(startURL)
} else {
globalApplication.debug("[Panel-Windows] No URL specified", "panelID", p.panel.id)
}
globalApplication.debug("[Panel-Windows] setupChromium() completed", "panelID", p.panel.id)
// Force show the panel window and bring it to the top
w32.ShowWindow(p.hwnd, w32.SW_SHOW)
w32.SetWindowPos(
p.hwnd,
w32.HWND_TOP,
0, 0, 0, 0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOACTIVATE|w32.SWP_SHOWWINDOW,
)
globalApplication.debug("[Panel-Windows] Panel window shown and brought to top",
"panelID", p.panel.id,
"hwnd", p.hwnd,
"isVisible", w32.IsWindowVisible(p.hwnd))
// Open inspector if requested
if debugMode && p.panel.options.OpenInspectorOnStartup {
p.chromium.OpenDevToolsWindow()
}
}
func (p *windowsPanelImpl) processMessage(message string, _ *edge.ICoreWebView2, _ *edge.ICoreWebView2WebMessageReceivedEventArgs) {
// For now, just log panel messages
// In future, we could route these to the parent window or handle panel-specific messages
globalApplication.debug("Panel message received", "panel", p.panel.name, "message", message)
}
func (p *windowsPanelImpl) navigationCompletedCallback(_ *edge.ICoreWebView2, _ *edge.ICoreWebView2NavigationCompletedEventArgs) {
p.navigationCompleted = true
// Execute any pending JS
// Navigation completed - no additional action needed
}
func (p *windowsPanelImpl) destroy() {
if p.chromium != nil {
p.chromium.ShuttingDown()
}
if p.hwnd != 0 {
w32.DestroyWindow(p.hwnd)
p.hwnd = 0
}
p.chromium = nil
}
func (p *windowsPanelImpl) setBounds(bounds Rect) {
if p.hwnd == 0 {
return
}
// Convert DIP to physical pixels
physicalBounds := DipToPhysicalRect(bounds)
// Move and resize the child window
w32.SetWindowPos(
p.hwnd,
0,
physicalBounds.X,
physicalBounds.Y,
physicalBounds.Width,
physicalBounds.Height,
w32.SWP_NOZORDER|w32.SWP_NOACTIVATE,
)
// Resize the WebView2 to fill the child window
if p.chromium != nil {
p.chromium.Resize()
}
}
func (p *windowsPanelImpl) bounds() Rect {
if p.hwnd == 0 {
return Rect{}
}
rect := w32.GetWindowRect(p.hwnd)
if rect == nil {
return Rect{}
}
// Get parent window position to calculate relative position
parentRect := w32.GetWindowRect(p.parent.hwnd)
if parentRect == nil {
return Rect{}
}
// Calculate position relative to parent's client area
parentClientX, parentClientY := w32.ClientToScreen(p.parent.hwnd, 0, 0)
physicalBounds := Rect{
X: int(rect.Left) - parentClientX,
Y: int(rect.Top) - parentClientY,
Width: int(rect.Right - rect.Left),
Height: int(rect.Bottom - rect.Top),
}
return PhysicalToDipRect(physicalBounds)
}
func (p *windowsPanelImpl) setZIndex(zIndex int) {
if p.hwnd == 0 {
return
}
// Use SetWindowPos to change z-order.
// Note: This is a binary implementation - panels are either on top (zIndex > 0)
// or at the bottom (zIndex <= 0). Granular z-index ordering is not supported
// on Windows because child windows share a z-order space and precise positioning
// would require tracking all panels and re-ordering them relative to each other.
var insertAfter uintptr
if zIndex > 0 {
insertAfter = w32.HWND_TOP
} else {
insertAfter = w32.HWND_BOTTOM
}
w32.SetWindowPos(
p.hwnd,
insertAfter,
0, 0, 0, 0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOACTIVATE,
)
}
func (p *windowsPanelImpl) setURL(url string) {
if p.chromium == nil {
return
}
startURL, err := assetserver.GetStartURL(url)
if err != nil {
globalApplication.error("failed to get start URL: %v", err)
return
}
p.navigationCompleted = false
p.chromium.Navigate(startURL)
}
func (p *windowsPanelImpl) reload() {
if p.chromium == nil {
return
}
globalApplication.dispatchOnMainThread(func() {
p.chromium.Eval("window.location.reload();")
})
}
func (p *windowsPanelImpl) forceReload() {
// WebView2 doesn't have a cache-bypass reload, so just reload normally
p.reload()
}
func (p *windowsPanelImpl) show() {
if p.hwnd == 0 {
return
}
w32.ShowWindow(p.hwnd, w32.SW_SHOW)
}
func (p *windowsPanelImpl) hide() {
if p.hwnd == 0 {
return
}
w32.ShowWindow(p.hwnd, w32.SW_HIDE)
}
func (p *windowsPanelImpl) isVisible() bool {
if p.hwnd == 0 {
return false
}
style := uint32(w32.GetWindowLong(p.hwnd, w32.GWL_STYLE))
return style&w32.WS_VISIBLE != 0
}
func (p *windowsPanelImpl) setZoom(zoom float64) {
if p.chromium == nil {
return
}
p.chromium.PutZoomFactor(zoom)
}
func (p *windowsPanelImpl) getZoom() float64 {
if p.chromium == nil {
return 1.0
}
controller := p.chromium.GetController()
if controller == nil {
return 1.0
}
factor, err := controller.GetZoomFactor()
if err != nil {
return 1.0
}
return factor
}
func (p *windowsPanelImpl) openDevTools() {
if p.chromium == nil {
return
}
p.chromium.OpenDevToolsWindow()
}
func (p *windowsPanelImpl) focus() {
if p.hwnd == 0 {
return
}
w32.SetFocus(p.hwnd)
if p.chromium != nil {
p.chromium.Focus()
}
}
func (p *windowsPanelImpl) isFocused() bool {
if p.hwnd == 0 {
return false
}
return w32.GetFocus() == p.hwnd
}

View file

@ -174,6 +174,11 @@ type WebviewWindow struct {
// unconditionallyClose marks the window to be unconditionally closed (atomic)
unconditionallyClose uint32
// Embedded panels management
panels map[uint]*WebviewPanel
panelsLock sync.RWMutex
panelResizeHandlerRegistered bool // Tracks if resize handler is registered
}
func (w *WebviewWindow) SetMenu(menu *Menu) {
@ -290,13 +295,15 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow {
eventListeners: make(map[uint][]*WindowEventListener),
eventHooks: make(map[uint][]*WindowEventListener),
menuBindings: make(map[string]*MenuItem),
panels: make(map[uint]*WebviewPanel),
}
result.setupEventMapping()
// Listen for window closing events and de
// Listen for window closing events and cleanup panels
result.OnWindowEvent(events.Common.WindowClosing, func(event *WindowEvent) {
atomic.StoreUint32(&result.unconditionallyClose, 1)
InvokeSync(result.destroyAllPanels)
InvokeSync(result.markAsDestroyed)
InvokeSync(result.impl.close)
globalApplication.Window.Remove(result.id)
@ -429,6 +436,9 @@ func (w *WebviewWindow) Run() {
}
InvokeSync(w.impl.run)
// Start any panels that were added before the window was run
w.runPanels()
}
// SetAlwaysOnTop sets the window to be always on top.
@ -1602,3 +1612,228 @@ func (w *WebviewWindow) SnapAssist() {
}
InvokeSync(w.impl.snapAssist)
}
// ============================================================================
// Panel Management Methods
// ============================================================================
// NewPanel creates a new WebviewPanel with the given options and adds it to this window.
// The panel is a secondary webview that can be positioned anywhere within the window.
// This is similar to Electron's BrowserView or the deprecated webview tag.
//
// Example:
//
// panel := window.NewPanel(application.WebviewPanelOptions{
// X: 0,
// Y: 0,
// Width: 300,
// Height: 400,
// URL: "https://example.com",
// })
func (w *WebviewWindow) NewPanel(options WebviewPanelOptions) *WebviewPanel {
globalApplication.debug("[Window] NewPanel() called",
"windowID", w.id,
"panelName", options.Name,
"windowImplNil", w.impl == nil,
"windowDestroyed", w.isDestroyed())
panel := NewPanel(options)
panel.parent = w
w.panelsLock.Lock()
w.panels[panel.id] = panel
w.panelsLock.Unlock()
globalApplication.debug("[Window] Panel registered",
"windowID", w.id,
"panelID", panel.id,
"panelName", panel.name)
// If window is already running, start the panel immediately
if w.impl != nil && !w.isDestroyed() {
globalApplication.debug("[Window] Window impl exists, starting panel immediately",
"windowID", w.id,
"panelID", panel.id)
// Initialize anchor with current window size
panel.initializeAnchor()
InvokeSync(panel.run)
// Ensure resize handler is registered if this panel has anchoring
if options.Anchor != AnchorNone {
w.ensurePanelResizeHandler()
}
} else {
globalApplication.debug("[Window] Window impl not ready, panel will be started later",
"windowID", w.id,
"panelID", panel.id,
"implNil", w.impl == nil,
"destroyed", w.isDestroyed())
}
return panel
}
// GetPanel returns a panel by its name, or nil if not found.
func (w *WebviewWindow) GetPanel(name string) *WebviewPanel {
w.panelsLock.RLock()
defer w.panelsLock.RUnlock()
for _, panel := range w.panels {
if panel.name == name {
return panel
}
}
return nil
}
// GetPanelByID returns a panel by its ID, or nil if not found.
func (w *WebviewWindow) GetPanelByID(id uint) *WebviewPanel {
w.panelsLock.RLock()
defer w.panelsLock.RUnlock()
return w.panels[id]
}
// GetPanels returns all panels attached to this window.
func (w *WebviewWindow) GetPanels() []*WebviewPanel {
w.panelsLock.RLock()
defer w.panelsLock.RUnlock()
panels := make([]*WebviewPanel, 0, len(w.panels))
for _, panel := range w.panels {
panels = append(panels, panel)
}
return panels
}
// RemovePanel removes a panel from this window by its name.
// Returns true if the panel was found and removed.
func (w *WebviewWindow) RemovePanel(name string) bool {
panel := w.GetPanel(name)
if panel == nil {
return false
}
panel.Destroy()
return true
}
// RemovePanelByID removes a panel from this window by its ID.
// Returns true if the panel was found and removed.
func (w *WebviewWindow) RemovePanelByID(id uint) bool {
panel := w.GetPanelByID(id)
if panel == nil {
return false
}
panel.Destroy()
return true
}
// removePanel is called by WebviewPanel.Destroy() to remove itself from the parent
func (w *WebviewWindow) removePanel(id uint) {
w.panelsLock.Lock()
defer w.panelsLock.Unlock()
delete(w.panels, id)
}
// runPanels starts all panels that haven't been started yet.
// This is called after the window's impl is created.
func (w *WebviewWindow) runPanels() {
globalApplication.debug("[Window] runPanels() called", "windowID", w.id)
// Collect panels under lock, then run them outside the lock
w.panelsLock.RLock()
panels := make([]*WebviewPanel, 0, len(w.panels))
for _, panel := range w.panels {
if panel.impl == nil {
panels = append(panels, panel)
}
}
w.panelsLock.RUnlock()
globalApplication.debug("[Window] runPanels() found panels to start",
"windowID", w.id,
"panelCount", len(panels))
for _, panel := range panels {
globalApplication.debug("[Window] runPanels() starting panel",
"windowID", w.id,
"panelID", panel.id,
"panelName", panel.name)
panel.initializeAnchor()
panel.run()
}
// Register resize event handler for panel responsive layout if needed
w.ensurePanelResizeHandler()
globalApplication.debug("[Window] runPanels() completed", "windowID", w.id)
}
// ensurePanelResizeHandler registers the resize event handler if there are anchored panels
// and the handler hasn't been registered yet.
func (w *WebviewWindow) ensurePanelResizeHandler() {
w.panelsLock.Lock()
if w.panelResizeHandlerRegistered {
w.panelsLock.Unlock()
return
}
// Check if we have any anchored panels
hasAnchoredPanels := false
for _, panel := range w.panels {
if panel.options.Anchor != AnchorNone {
hasAnchoredPanels = true
break
}
}
if !hasAnchoredPanels {
w.panelsLock.Unlock()
return
}
w.panelResizeHandlerRegistered = true
w.panelsLock.Unlock()
w.OnWindowEvent(events.Common.WindowDidResize, func(event *WindowEvent) {
w.handlePanelResize()
})
}
// handlePanelResize updates all anchored panels when the window is resized.
// This should be called when the window size changes.
func (w *WebviewWindow) handlePanelResize() {
w.panelsLock.RLock()
panels := make([]*WebviewPanel, 0, len(w.panels))
for _, panel := range w.panels {
if panel.options.Anchor != AnchorNone {
panels = append(panels, panel)
}
}
w.panelsLock.RUnlock()
if len(panels) == 0 {
return
}
// Get new window size
newWidth, newHeight := w.Size()
for _, panel := range panels {
panel.handleWindowResize(newWidth, newHeight)
}
}
// destroyAllPanels destroys all panels in this window.
// This is called when the window is closing.
func (w *WebviewWindow) destroyAllPanels() {
w.panelsLock.Lock()
panels := make([]*WebviewPanel, 0, len(w.panels))
for _, panel := range w.panels {
panels = append(panels, panel)
}
w.panelsLock.Unlock()
for _, panel := range panels {
panel.Destroy()
}
}