mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge 5378cb39bc into bb4fbf9574
This commit is contained in:
commit
e06fbf016f
16 changed files with 3374 additions and 1 deletions
189
v3/examples/webview-panel/README.md
Normal file
189
v3/examples/webview-panel/README.md
Normal 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 | ❌ | ✅ |
|
||||
225
v3/examples/webview-panel/assets/index.html
Normal file
225
v3/examples/webview-panel/assets/index.html
Normal 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>
|
||||
105
v3/examples/webview-panel/main.go
Normal file
105
v3/examples/webview-panel/main.go
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
282
v3/internal/runtime/desktop/@wailsio/runtime/src/panel.ts
Normal file
282
v3/internal/runtime/desktop/@wailsio/runtime/src/panel.ts
Normal 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]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ export const objectNames = Object.freeze({
|
|||
Browser: 9,
|
||||
CancelCall: 10,
|
||||
IOS: 11,
|
||||
Android: 12,
|
||||
Panel: 13,
|
||||
});
|
||||
export let clientId = nanoid();
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
207
v3/pkg/application/messageprocessor_panel.go
Normal file
207
v3/pkg/application/messageprocessor_panel.go
Normal 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
|
||||
}
|
||||
603
v3/pkg/application/webview_panel.go
Normal file
603
v3/pkg/application/webview_panel.go
Normal 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,
|
||||
}
|
||||
}
|
||||
79
v3/pkg/application/webview_panel_android.go
Normal file
79
v3/pkg/application/webview_panel_android.go
Normal 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
|
||||
}
|
||||
427
v3/pkg/application/webview_panel_darwin.go
Normal file
427
v3/pkg/application/webview_panel_darwin.go
Normal 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))
|
||||
}
|
||||
79
v3/pkg/application/webview_panel_ios.go
Normal file
79
v3/pkg/application/webview_panel_ios.go
Normal 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
|
||||
}
|
||||
352
v3/pkg/application/webview_panel_linux.go
Normal file
352
v3/pkg/application/webview_panel_linux.go
Normal 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
|
||||
}
|
||||
157
v3/pkg/application/webview_panel_options.go
Normal file
157
v3/pkg/application/webview_panel_options.go
Normal 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
|
||||
425
v3/pkg/application/webview_panel_windows.go
Normal file
425
v3/pkg/application/webview_panel_windows.go
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue