wails/v3/pkg/application/webview_window_windows.go
Lea Anthony a06d55804c
perf(v3): optimize JSON processing and reduce allocations in hot paths (#4843)
* perf(v3): optimize JSON processing and reduce allocations in hot paths

- Switch to goccy/go-json for method binding, events, and HTTP transport
  (21-63% faster, 40-60% less memory for method calls)
- Optimize BoundMethod struct layout to reduce padding (144 -> 136 bytes)
- Cache isVariadic flag at registration to avoid reflect call per invocation
- Use stack-allocated buffer for method arguments (<=8 args)
- Optimize result collection to avoid slice allocation for single return values
- Use sync.Map for MIME cache to improve concurrent read performance
- Use buffer pool for HTTP transport request body reading
- Lazily allocate CloseNotify channel in content type sniffer
- Remove debug CSS logging from asset server
- Add comprehensive benchmark tests (build tag: bench)

Performance improvements for BoundMethod.Call:
- SimpleCall:   1290ns -> 930ns (28% faster), 240B -> 80B (67% less memory)
- ComplexCall:  10500ns -> 3900ns (63% faster), 1192B -> 1020B (14% less)
- VariadicCall: 3460ns -> 1600ns (54% faster), 512B -> 289B (44% less)

* perf(v3): add max size limit to buffer pool to prevent memory bloat

Buffers larger than 512KB are not returned to the pool, allowing GC
to reclaim memory after large requests (e.g., base64 encoded images).

* perf(v3): remove mimetype library dependency, saving ~208KB binary size

- Replace github.com/wailsapp/mimetype with expanded extension map + stdlib
- Expand MIME type map from 16 to 50+ common web formats (fonts, audio, video, etc.)
- Add comprehensive test suite validating MIME detection for all web formats
- Use http.DetectContentType as fallback for unknown extensions
- Actual binary size reduction: 1.2MB (11MB -> 9.8MB in test app)

* perf(v3): migrate all runtime code to goccy/go-json

Migrate remaining encoding/json usages to goccy/go-json in:
- pkg/application (android, darwin, ios, single_instance, webview_window)
- pkg/services (kvstore, notifications on all platforms)
- internal/assetserver/webview (request/response handling)
- internal/runtime and internal/capabilities

Note: encoding/json (110KB) remains in binary because:
1. goccy/go-json imports it for interface compatibility (json.Marshaler, etc.)
2. log/slog (stdlib) uses it for JSON output

The performance benefit is in the hot paths which now use the faster library.

* perf(v3): replace gopkg.in/ini.v1 with minimal .desktop file parser

Replace the gopkg.in/ini.v1 dependency with a purpose-built minimal parser
for Linux .desktop files.

The new parser:
- Only extracts the Exec key from [Desktop Entry] section (all we need)
- Follows the Desktop Entry Specification
- Has comprehensive test coverage (40 tests) including:
  - All major file managers (Nautilus, Dolphin, Thunar, PCManFM, Caja, Nemo)
  - Edge cases (UTF-8, special chars, comments, empty files, etc.)
  - Buffer limit handling

Binary size reduction: 45KB (10.22MB -> 10.18MB)

* perf(v3): remove samber/lo from runtime code, saving ~310KB binary size

Replace samber/lo with Go 1.21+ stdlib slices package and minimal internal
helpers in all runtime code paths. This removes 80 transitive dependencies
from the production binary.

Changes:
- Create internal/sliceutil package with Unique and FindMapKey helpers
- Replace lo.Without with slices.DeleteFunc in event handling
- Replace lo.Ternary with inline if/else in Windows code
- Replace lo.Uniq with sliceutil.Unique for feature flags
- Replace lo.FindKey with sliceutil.FindMapKey for method aliases
- Replace lo.Filter with slices.DeleteFunc in event listeners
- Replace lo.Must with inline panic in w32 package

Binary size: 10.18MB -> 9.87MB (~310KB / 3% reduction)

Note: CLI tools still use samber/lo since they don't affect
production binary size. The application_debug.go file also
retains lo usage as it has //go:build !production tag.

* fix: address CodeRabbit review comments

- Use application/x-typescript MIME type (not IANA-registered text/typescript)
- Fix potential panic in mimetype_stdlib_test.go for short MIME strings
- Use cached isVariadic flag in bindings_optimized_bench_test.go

* fix: initialize goccy/go-json decoder early to fix Windows test failure

On Windows, goccy/go-json's type address calculation can fail if the
decoder is first invoked during test execution rather than at init time.
Force early initialization by unmarshaling a []int during package init.

See: https://github.com/goccy/go-json/issues/474

* 📝 Add docstrings to `v3/performance-improvements` (#4844)

* fix: initialize goccy/go-json decoder early to fix Windows test failure

On Windows, goccy/go-json's type address calculation can fail if the
decoder is first invoked during test execution rather than at init time.
Force early initialization by unmarshaling a []int during package init.

See: https://github.com/goccy/go-json/issues/474

* 📝 Add docstrings to `v3/performance-improvements`

Docstrings generation was requested by @leaanthony.

* https://github.com/wailsapp/wails/pull/4843#issuecomment-3703472562

The following files were modified:

* `v3/internal/assetserver/common.go`
* `v3/internal/assetserver/content_type_sniffer.go`
* `v3/internal/assetserver/mimecache.go`
* `v3/internal/fileexplorer/desktopfile.go`
* `v3/internal/fileexplorer/fileexplorer_linux.go`
* `v3/internal/sliceutil/sliceutil.go`
* `v3/pkg/application/application_ios.go`
* `v3/pkg/application/bindings.go`
* `v3/pkg/application/ios_runtime_ios.go`
* `v3/pkg/w32/window.go`

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-02 07:03:36 +11:00

2528 lines
75 KiB
Go

//go:build windows
package application
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
"github.com/bep/debounce"
"github.com/wailsapp/go-webview2/webviewloader"
"github.com/wailsapp/wails/v3/internal/assetserver"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
"github.com/wailsapp/wails/v3/internal/capabilities"
"github.com/wailsapp/wails/v3/internal/runtime"
"github.com/wailsapp/wails/v3/internal/sliceutil"
"github.com/wailsapp/go-webview2/pkg/edge"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/w32"
)
var edgeMap = map[string]uintptr{
"n-resize": w32.HTTOP,
"ne-resize": w32.HTTOPRIGHT,
"e-resize": w32.HTRIGHT,
"se-resize": w32.HTBOTTOMRIGHT,
"s-resize": w32.HTBOTTOM,
"sw-resize": w32.HTBOTTOMLEFT,
"w-resize": w32.HTLEFT,
"nw-resize": w32.HTTOPLEFT,
}
type windowsWebviewWindow struct {
windowImpl unsafe.Pointer
parent *WebviewWindow
hwnd w32.HWND
menu *Win32Menu
currentlyOpenContextMenu *Win32Menu
ignoreDPIChangeResizing bool
// Fullscreen flags
isCurrentlyFullscreen bool
previousWindowStyle uint32
previousWindowExStyle uint32
previousWindowPlacement w32.WINDOWPLACEMENT
// Webview
chromium *edge.Chromium
webviewNavigationCompleted bool
// Window visibility management - robust fallback for issue #2861
showRequested bool // Track if show() was called before navigation completed
visibilityTimeout *time.Timer // Timeout to show window if navigation is delayed
windowShown bool // Track if window container has been shown
// Track whether content protection has been applied to the native window yet
contentProtectionApplied bool
// resizeBorder* is the width/height of the resize border in pixels.
resizeBorderWidth int32
resizeBorderHeight int32
focusingChromium bool
dropTarget *w32.DropTarget
onceDo sync.Once
// Window move debouncer
moveDebouncer func(func())
resizeDebouncer func(func())
// isMinimizing indicates whether the window is currently being minimized
// Used to prevent unnecessary redraws during minimize/restore operations
isMinimizing bool
// menubarTheme is the theme for the menubar
menubarTheme *w32.MenuBarTheme
}
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
menu.Update()
w.menu = NewApplicationMenu(w, menu)
w.menu.parentWindow = w
w32.SetMenu(w.hwnd, w.menu.menu)
// Set menu background if theme is active
if w.menubarTheme != nil {
globalApplication.debug("Applying menubar theme in setMenu", "window", w.parent.id)
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
// Force a repaint of the menu area
w32.InvalidateRect(w.hwnd, nil, true)
} else {
globalApplication.debug("No menubar theme to apply in setMenu", "window", w.parent.id)
}
// Check if using translucent background with Mica - this makes menubars invisible
if w.parent.options.BackgroundType == BackgroundTypeTranslucent &&
(w.parent.options.Windows.BackdropType == Mica ||
w.parent.options.Windows.BackdropType == Acrylic ||
w.parent.options.Windows.BackdropType == Tabbed) {
// Log warning about menubar visibility issue
globalApplication.debug("Warning: Menubars may be invisible when using translucent backgrounds with Mica/Acrylic/Tabbed effects", "window", w.parent.id)
}
}
func (w *windowsWebviewWindow) cut() {
w.execJS("document.execCommand('cut')")
}
func (w *windowsWebviewWindow) paste() {
w.execJS(`
(async () => {
try {
// Try to read all available formats
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
// Check for image types
for (const type of clipboardItem.types) {
if (type.startsWith('image/')) {
const blob = await clipboardItem.getType(type);
const url = URL.createObjectURL(blob);
document.execCommand('insertHTML', false, '<img src="' + url + '">');
return;
}
}
// If no image found, try text
if (clipboardItem.types.includes('text/plain')) {
const text = await navigator.clipboard.readText();
document.execCommand('insertText', false, text);
return;
}
}
} catch(err) {
// Fallback to text-only paste if clipboard access fails
try {
const text = await navigator.clipboard.readText();
document.execCommand('insertText', false, text);
} catch(fallbackErr) {
console.error('Failed to paste:', err, fallbackErr);
}
}
})()
`)
}
func (w *windowsWebviewWindow) copy() {
w.execJS(`
(async () => {
try {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const container = document.createElement('div');
container.appendChild(range.cloneContents());
// Check if we have any images in the selection
const images = container.getElementsByTagName('img');
if (images.length > 0) {
// Handle image copy
const img = images[0]; // Take the first image
const response = await fetch(img.src);
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
} else {
// Handle text copy
const text = selection.toString();
if (text) {
await navigator.clipboard.writeText(text);
}
}
} catch(err) {
console.error('Failed to copy:', err);
}
})()
`)
}
func (w *windowsWebviewWindow) selectAll() {
w.execJS("document.execCommand('selectAll')")
}
func (w *windowsWebviewWindow) undo() {
w.execJS("document.execCommand('undo')")
}
func (w *windowsWebviewWindow) redo() {
w.execJS("document.execCommand('redo')")
}
func (w *windowsWebviewWindow) delete() {
w.execJS("document.execCommand('delete')")
}
func (w *windowsWebviewWindow) handleKeyEvent(_ string) {
// Unused on windows
}
func (w *windowsWebviewWindow) setEnabled(enabled bool) {
w32.EnableWindow(w.hwnd, enabled)
}
func (w *windowsWebviewWindow) print() error {
w.execJS("window.print();")
return nil
}
func (w *windowsWebviewWindow) startResize(border string) error {
if !w32.ReleaseCapture() {
return errors.New("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until resizing has been finished.
w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, edgeMap[border], 0)
return nil
}
func (w *windowsWebviewWindow) startDrag() error {
if !w32.ReleaseCapture() {
return errors.New("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until dragging has been finished.
w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0)
return nil
}
func (w *windowsWebviewWindow) nativeWindow() unsafe.Pointer {
return unsafe.Pointer(w.hwnd)
}
func (w *windowsWebviewWindow) setTitle(title string) {
w32.SetWindowText(w.hwnd, title)
}
func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
var hwndInsertAfter uintptr
if alwaysOnTop {
hwndInsertAfter = w32.HWND_TOPMOST
} else {
hwndInsertAfter = w32.HWND_NOTOPMOST
}
w32.SetWindowPos(w.hwnd,
hwndInsertAfter,
0,
0,
0,
0,
uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE))
}
func (w *windowsWebviewWindow) setURL(url string) {
// Navigate to the given URL in the webview
w.webviewNavigationCompleted = false
w.chromium.Navigate(url)
}
func (w *windowsWebviewWindow) setResizable(resizable bool) {
w.setStyle(resizable, w32.WS_THICKFRAME)
w.execJS(fmt.Sprintf("window._wails.setResizable(%v);", resizable))
}
func (w *windowsWebviewWindow) setMinSize(width, height int) {
w.parent.options.MinWidth = width
w.parent.options.MinHeight = height
}
func (w *windowsWebviewWindow) setMaxSize(width, height int) {
w.parent.options.MaxWidth = width
w.parent.options.MaxHeight = height
}
func (w *windowsWebviewWindow) execJS(js string) {
if w.chromium == nil {
return
}
globalApplication.dispatchOnMainThread(func() {
w.chromium.Eval(js)
})
}
func (w *windowsWebviewWindow) setBackgroundColour(color RGBA) {
switch w.parent.options.BackgroundType {
case BackgroundTypeSolid:
w32.SetBackgroundColour(w.hwnd, color.Red, color.Green, color.Blue)
w.chromium.SetBackgroundColour(color.Red, color.Green, color.Blue, color.Alpha)
case BackgroundTypeTransparent, BackgroundTypeTranslucent:
w.chromium.SetBackgroundColour(0, 0, 0, 0)
}
}
func (w *windowsWebviewWindow) framelessWithDecorations() bool {
return w.parent.options.Frameless && !w.parent.options.Windows.DisableFramelessWindowDecorations
}
func (w *windowsWebviewWindow) run() {
options := w.parent.options
// Initialize showRequested based on whether window should be hidden
// Non-hidden windows should be shown by default
w.showRequested = !options.Hidden
w.chromium = edge.NewChromium()
if globalApplication.options.ErrorHandler != nil {
w.chromium.SetErrorCallback(globalApplication.options.ErrorHandler)
}
exStyle := w32.WS_EX_CONTROLPARENT
if options.BackgroundType != BackgroundTypeSolid {
if (options.Frameless && options.BackgroundType == BackgroundTypeTransparent) ||
w.parent.options.IgnoreMouseEvents {
// Always if transparent and frameless
exStyle |= w32.WS_EX_TRANSPARENT | w32.WS_EX_LAYERED
} else {
// Only WS_EX_NOREDIRECTIONBITMAP if not (and not solid)
exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP
}
}
if options.AlwaysOnTop {
exStyle |= w32.WS_EX_TOPMOST
}
// If we're frameless, we need to add the WS_EX_TOOLWINDOW style to hide the window from the taskbar
if options.Windows.HiddenOnTaskbar {
//exStyle |= w32.WS_EX_TOOLWINDOW
exStyle |= w32.WS_EX_NOACTIVATE
} else {
exStyle |= w32.WS_EX_APPWINDOW
}
if options.Windows.ExStyle != 0 {
exStyle = options.Windows.ExStyle
}
bounds := Rect{
X: options.X,
Y: options.Y,
Width: options.Width,
Height: options.Height,
}
initialScreen := ScreenNearestDipRect(bounds)
physicalBounds := initialScreen.dipToPhysicalRect(bounds)
// Default window position applied by the system
// TODO: provide a way to set (0,0) as an initial position?
if options.X == 0 && options.Y == 0 {
physicalBounds.X = w32.CW_USEDEFAULT
physicalBounds.Y = w32.CW_USEDEFAULT
}
var appMenu w32.HMENU
// Process Menu
if !options.Frameless {
userMenu := w.parent.options.Windows.Menu
if userMenu != nil {
userMenu.Update()
w.menu = NewApplicationMenu(w, userMenu)
w.menu.parentWindow = w
appMenu = w.menu.menu
}
}
var parent w32.HWND
var style uint = w32.WS_OVERLAPPEDWINDOW
// If the window should be hidden initially, exclude WS_VISIBLE from the style
// This prevents the white window flash reported in issue #4611
if options.Hidden {
style = style &^ uint(w32.WS_VISIBLE)
}
w.hwnd = w32.CreateWindowEx(
uint(exStyle),
w32.MustStringToUTF16Ptr(globalApplication.options.Windows.WndClass),
w32.MustStringToUTF16Ptr(options.Title),
style,
physicalBounds.X,
physicalBounds.Y,
physicalBounds.Width,
physicalBounds.Height,
parent,
appMenu,
w32.GetModuleHandle(""),
nil)
if w.hwnd == 0 {
globalApplication.fatal("unable to create window")
}
// Ensure correct window size in case the scale factor of current screen is different from the initial one.
// This could happen when using the default window position and the window launches on a secondary monitor.
currentScreen, _ := w.getScreen()
if currentScreen.ScaleFactor != initialScreen.ScaleFactor {
w.setSize(options.Width, options.Height)
}
w.setupChromium()
if options.Windows.WindowDidMoveDebounceMS == 0 {
options.Windows.WindowDidMoveDebounceMS = 50
}
w.moveDebouncer = debounce.New(
time.Duration(options.Windows.WindowDidMoveDebounceMS) * time.Millisecond,
)
if options.Windows.ResizeDebounceMS > 0 {
w.resizeDebouncer = debounce.New(
time.Duration(options.Windows.ResizeDebounceMS) * time.Millisecond,
)
}
// Initialise the window buttons
w.setMinimiseButtonState(options.MinimiseButtonState)
w.setMaximiseButtonState(options.MaximiseButtonState)
w.setCloseButtonState(options.CloseButtonState)
// Register the window with the application
getNativeApplication().registerWindow(w)
w.setResizable(!options.DisableResize)
w.setIgnoreMouseEvents(options.IgnoreMouseEvents)
if options.Frameless {
// Inform the application of the frame change this is needed to trigger the WM_NCCALCSIZE event.
// => https://learn.microsoft.com/en-us/windows/win32/dwm/customframe#removing-the-standard-frame
// This is normally done in WM_CREATE but we can't handle that there because that is emitted during CreateWindowEx
// and at that time we can't yet register the window for calling our WndProc method.
// This must be called after setResizable above!
rcClient := w32.GetWindowRect(w.hwnd)
w32.SetWindowPos(w.hwnd,
0,
int(rcClient.Left),
int(rcClient.Top),
int(rcClient.Right-rcClient.Left),
int(rcClient.Bottom-rcClient.Top),
w32.SWP_FRAMECHANGED)
}
// Icon
if !options.Windows.DisableIcon {
// App icon ID is 3
icon, err := NewIconFromResource(w32.GetModuleHandle(""), uint16(3))
if err != nil {
// Try loading from the given icon
if globalApplication.options.Icon != nil {
icon, _ = w32.CreateLargeHIconFromImage(globalApplication.options.Icon)
}
}
if icon != 0 {
w.setIcon(icon)
}
} else {
w.disableIcon()
}
// Process the theme
switch options.Windows.Theme {
case SystemDefault:
isDark := w32.IsCurrentlyDarkMode()
if isDark {
w32.AllowDarkModeForWindow(w.hwnd, true)
}
w.updateTheme(isDark)
// Don't initialize default dark theme here if custom theme might be set
// The updateTheme call above will handle both default and custom themes
w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func(*ApplicationEvent) {
InvokeAsync(func() {
w.updateTheme(w32.IsCurrentlyDarkMode())
})
})
case Light:
w.updateTheme(false)
case Dark:
w32.AllowDarkModeForWindow(w.hwnd, true)
w.updateTheme(true)
// Don't initialize default dark theme here if custom theme might be set
// The updateTheme call above will handle custom themes
}
w.setBackgroundColour(options.BackgroundColour)
if options.BackgroundType == BackgroundTypeTranslucent {
w.setBackdropType(w.parent.options.Windows.BackdropType)
}
// Process StartState
switch options.StartState {
case WindowStateMaximised:
if w.parent.Resizable() {
w.maximise()
}
case WindowStateMinimised:
w.minimise()
case WindowStateFullscreen:
w.fullscreen()
case WindowStateNormal:
}
// Process window mask
if options.Windows.WindowMask != nil {
w.setWindowMask(options.Windows.WindowMask)
}
if options.InitialPosition == WindowCentered {
w.center()
} else {
w.setPosition(options.X, options.Y)
}
if options.Frameless {
// Trigger a resize to ensure the window is sized correctly
w.chromium.Resize()
}
}
func (w *windowsWebviewWindow) center() {
w32.CenterWindow(w.hwnd)
}
func (w *windowsWebviewWindow) disableSizeConstraints() {
w.setMaxSize(0, 0)
w.setMinSize(0, 0)
}
func (w *windowsWebviewWindow) enableSizeConstraints() {
options := w.parent.options
if options.MinWidth > 0 || options.MinHeight > 0 {
w.setMinSize(options.MinWidth, options.MinHeight)
}
if options.MaxWidth > 0 || options.MaxHeight > 0 {
w.setMaxSize(options.MaxWidth, options.MaxHeight)
}
}
func (w *windowsWebviewWindow) update() {
w32.UpdateWindow(w.hwnd)
}
// getBorderSizes returns the extended border size for the window
func (w *windowsWebviewWindow) getBorderSizes() *LRTB {
var result LRTB
var frame w32.RECT
w32.DwmGetWindowAttribute(
w.hwnd,
w32.DWMWA_EXTENDED_FRAME_BOUNDS,
unsafe.Pointer(&frame),
unsafe.Sizeof(frame),
)
rect := w32.GetWindowRect(w.hwnd)
result.Left = int(frame.Left - rect.Left)
result.Top = int(frame.Top - rect.Top)
result.Right = int(rect.Right - frame.Right)
result.Bottom = int(rect.Bottom - frame.Bottom)
return &result
}
// convertWindowToWebviewCoordinates converts window-relative coordinates to webview-relative coordinates
func (w *windowsWebviewWindow) convertWindowToWebviewCoordinates(windowX, windowY int) (int, int) {
// Get the client area of the window (this excludes borders, title bar, etc.)
clientRect := w32.GetClientRect(w.hwnd)
if clientRect == nil {
// Fallback: return coordinates as-is if we can't get client rect
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Failed to get client rect, returning original coordinates", "windowX", windowX, "windowY", windowY)
return windowX, windowY
}
// Get the window rect to calculate the offset
windowRect := w32.GetWindowRect(w.hwnd)
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Input window coordinates", "windowX", windowX, "windowY", windowY)
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Window rect",
"left", windowRect.Left, "top", windowRect.Top, "right", windowRect.Right, "bottom", windowRect.Bottom,
"width", windowRect.Right-windowRect.Left, "height", windowRect.Bottom-windowRect.Top)
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Client rect",
"left", clientRect.Left, "top", clientRect.Top, "right", clientRect.Right, "bottom", clientRect.Bottom,
"width", clientRect.Right-clientRect.Left, "height", clientRect.Bottom-clientRect.Top)
// Convert client (0,0) to screen coordinates to find where the client area starts
var point w32.POINT
point.X = 0
point.Y = 0
// Convert client (0,0) to screen coordinates
clientX, clientY := w32.ClientToScreen(w.hwnd, int(point.X), int(point.Y))
// The window coordinates from drag drop are relative to the window's top-left
// But we need them relative to the client area's top-left
// So we need to subtract the difference between window origin and client origin
windowOriginX := int(windowRect.Left)
windowOriginY := int(windowRect.Top)
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Client (0,0) in screen coordinates", "clientX", clientX, "clientY", clientY)
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Window origin in screen coordinates", "windowOriginX", windowOriginX, "windowOriginY", windowOriginY)
// Calculate the offset from window origin to client origin
offsetX := clientX - windowOriginX
offsetY := clientY - windowOriginY
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Calculated offset", "offsetX", offsetX, "offsetY", offsetY)
// Convert window-relative coordinates to webview-relative coordinates
webviewX := windowX - offsetX
webviewY := windowY - offsetY
globalApplication.debug("[DragDropDebug] convertWindowToWebviewCoordinates: Final webview coordinates", "webviewX", webviewX, "webviewY", webviewY)
return webviewX, webviewY
}
func (w *windowsWebviewWindow) physicalBounds() Rect {
// var rect w32.RECT
// // Get the extended frame bounds instead of the window rect to offset the invisible borders in Windows 10
// w32.DwmGetWindowAttribute(w.hwnd, w32.DWMWA_EXTENDED_FRAME_BOUNDS, unsafe.Pointer(&rect), unsafe.Sizeof(rect))
rect := w32.GetWindowRect(w.hwnd)
return Rect{
X: int(rect.Left),
Y: int(rect.Top),
Width: int(rect.Right - rect.Left),
Height: int(rect.Bottom - rect.Top),
}
}
func (w *windowsWebviewWindow) setPhysicalBounds(physicalBounds Rect) {
// // Offset invisible borders
// borderSize := w.getBorderSizes()
// physicalBounds.X -= borderSize.Left
// physicalBounds.Y -= borderSize.Top
// physicalBounds.Width += borderSize.Left + borderSize.Right
// physicalBounds.Height += borderSize.Top + borderSize.Bottom
// Set flag to ignore resizing the window with DPI change because we already calculated the correct size
// for the target position, this prevents double resizing issue when the window is moved between screens
previousFlag := w.ignoreDPIChangeResizing
w.ignoreDPIChangeResizing = true
w32.SetWindowPos(
w.hwnd,
0,
physicalBounds.X,
physicalBounds.Y,
physicalBounds.Width,
physicalBounds.Height,
w32.SWP_NOZORDER|w32.SWP_NOACTIVATE,
)
w.ignoreDPIChangeResizing = previousFlag
}
// Get window dip bounds
func (w *windowsWebviewWindow) bounds() Rect {
return PhysicalToDipRect(w.physicalBounds())
}
// Set window dip bounds
func (w *windowsWebviewWindow) setBounds(bounds Rect) {
w.setPhysicalBounds(DipToPhysicalRect(bounds))
}
func (w *windowsWebviewWindow) size() (int, int) {
bounds := w.bounds()
return bounds.Width, bounds.Height
}
func (w *windowsWebviewWindow) width() int {
return w.bounds().Width
}
func (w *windowsWebviewWindow) height() int {
return w.bounds().Height
}
func (w *windowsWebviewWindow) setSize(width, height int) {
bounds := w.bounds()
bounds.Width = width
bounds.Height = height
w.setBounds(bounds)
}
func (w *windowsWebviewWindow) position() (int, int) {
bounds := w.bounds()
return bounds.X, bounds.Y
}
func (w *windowsWebviewWindow) setPosition(x int, y int) {
bounds := w.bounds()
bounds.X = x
bounds.Y = y
w.setBounds(bounds)
}
// Get window position relative to the screen WorkArea on which it is
func (w *windowsWebviewWindow) relativePosition() (int, int) {
screen, _ := w.getScreen()
pos := screen.absoluteToRelativeDipPoint(w.bounds().Origin())
// Relative to WorkArea origin
pos.X -= (screen.WorkArea.X - screen.X)
pos.Y -= (screen.WorkArea.Y - screen.Y)
return pos.X, pos.Y
}
// Set window position relative to the screen WorkArea on which it is
func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
screen, _ := w.getScreen()
pos := screen.relativeToAbsoluteDipPoint(Point{X: x, Y: y})
// Relative to WorkArea origin
pos.X += (screen.WorkArea.X - screen.X)
pos.Y += (screen.WorkArea.Y - screen.Y)
w.setPosition(pos.X, pos.Y)
}
func (w *windowsWebviewWindow) destroy() {
w.parent.markAsDestroyed()
if w.dropTarget != nil {
w.dropTarget.Release()
}
// destroy the window
w32.DestroyWindow(w.hwnd)
}
func (w *windowsWebviewWindow) reload() {
w.execJS("window.location.reload();")
}
func (w *windowsWebviewWindow) forceReload() {
// noop
}
func (w *windowsWebviewWindow) zoomReset() {
w.setZoom(1.0)
}
func (w *windowsWebviewWindow) zoomIn() {
// Increase the zoom level by 0.05
currentZoom := w.getZoom()
if currentZoom == -1 {
return
}
w.setZoom(currentZoom + 0.05)
}
func (w *windowsWebviewWindow) zoomOut() {
// Decrease the zoom level by 0.05
currentZoom := w.getZoom()
if currentZoom == -1 {
return
}
if currentZoom > 1.05 {
// Decrease the zoom level by 0.05
w.setZoom(currentZoom - 0.05)
} else {
// Set the zoom level to 1.0
w.setZoom(1.0)
}
}
func (w *windowsWebviewWindow) getZoom() float64 {
controller := w.chromium.GetController()
factor, err := controller.GetZoomFactor()
if err != nil {
return -1
}
return factor
}
func (w *windowsWebviewWindow) setZoom(zoom float64) {
w.chromium.PutZoomFactor(zoom)
}
func (w *windowsWebviewWindow) close() {
// Send WM_CLOSE message to trigger the same flow as clicking the X button
w32.SendMessage(w.hwnd, w32.WM_CLOSE, 0, 0)
}
func (w *windowsWebviewWindow) zoom() {
// Noop
}
func (w *windowsWebviewWindow) setHTML(html string) {
// Render the given HTML in the webview window
w.execJS(fmt.Sprintf("document.documentElement.innerHTML = %q;", html))
}
// on is used to indicate that a particular event should be listened for
func (w *windowsWebviewWindow) on(_ uint) {
// We don't need to worry about this in Windows as we do not need
// to optimise cgo calls
}
func (w *windowsWebviewWindow) minimise() {
w32.ShowWindow(w.hwnd, w32.SW_MINIMIZE)
}
func (w *windowsWebviewWindow) unminimise() {
w.restore()
}
func (w *windowsWebviewWindow) maximise() {
w32.ShowWindow(w.hwnd, w32.SW_MAXIMIZE)
w.chromium.Focus()
}
func (w *windowsWebviewWindow) unmaximise() {
w.restore()
w.parent.emit(events.Windows.WindowUnMaximise)
}
func (w *windowsWebviewWindow) restore() {
w32.ShowWindow(w.hwnd, w32.SW_RESTORE)
w.chromium.Focus()
}
func (w *windowsWebviewWindow) fullscreen() {
if w.isFullscreen() {
return
}
if w.framelessWithDecorations() {
err := w32.ExtendFrameIntoClientArea(w.hwnd, false)
if err != nil {
globalApplication.handleFatalError(err)
}
}
w.disableSizeConstraints()
w.previousWindowStyle = uint32(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE))
w.previousWindowExStyle = uint32(w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE))
monitor := w32.MonitorFromWindow(w.hwnd, w32.MONITOR_DEFAULTTOPRIMARY)
var monitorInfo w32.MONITORINFO
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
if !w32.GetMonitorInfo(monitor, &monitorInfo) {
return
}
if !w32.GetWindowPlacement(w.hwnd, &w.previousWindowPlacement) {
return
}
// According to https://devblogs.microsoft.com/oldnewthing/20050505-04/?p=35703 one should use w32.WS_POPUP | w32.WS_VISIBLE
w32.SetWindowLong(
w.hwnd,
w32.GWL_STYLE,
w.previousWindowStyle & ^uint32(w32.WS_OVERLAPPEDWINDOW) | (w32.WS_POPUP|w32.WS_VISIBLE),
)
w32.SetWindowLong(
w.hwnd,
w32.GWL_EXSTYLE,
w.previousWindowExStyle & ^uint32(w32.WS_EX_DLGMODALFRAME),
)
w.isCurrentlyFullscreen = true
w32.SetWindowPos(w.hwnd, w32.HWND_TOP,
int(monitorInfo.RcMonitor.Left),
int(monitorInfo.RcMonitor.Top),
int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left),
int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top),
w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
// Hide the menubar in fullscreen mode
w32.SetMenu(w.hwnd, 0)
w.chromium.Focus()
w.parent.emit(events.Windows.WindowFullscreen)
}
func (w *windowsWebviewWindow) unfullscreen() {
if !w.isFullscreen() {
return
}
if w.framelessWithDecorations() {
err := w32.ExtendFrameIntoClientArea(w.hwnd, true)
if err != nil {
globalApplication.handleFatalError(err)
}
}
w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, w.previousWindowStyle)
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, w.previousWindowExStyle)
w32.SetWindowPlacement(w.hwnd, &w.previousWindowPlacement)
w.isCurrentlyFullscreen = false
// Restore the menubar when exiting fullscreen
if w.menu != nil {
w32.SetMenu(w.hwnd, w.menu.menu)
}
w32.SetWindowPos(w.hwnd, 0, 0, 0, 0, 0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
w.enableSizeConstraints()
w.parent.emit(events.Windows.WindowUnFullscreen)
}
func (w *windowsWebviewWindow) isMinimised() bool {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
return style&w32.WS_MINIMIZE != 0
}
func (w *windowsWebviewWindow) isMaximised() bool {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
return style&w32.WS_MAXIMIZE != 0
}
func (w *windowsWebviewWindow) isFocused() bool {
// Returns true if the window is currently focused
return w32.GetForegroundWindow() == w.hwnd
}
func (w *windowsWebviewWindow) isFullscreen() bool {
// TODO: Actually calculate this based on size of window against screen size
// => stffabi: This flag is essential since it indicates that we are in fullscreen mode even before the native properties
// reflect this, e.g. when needing to know if we are in fullscreen during a wndproc message.
// That's also why this flag is set before SetWindowPos in v2 in fullscreen/unfullscreen.
return w.isCurrentlyFullscreen
}
func (w *windowsWebviewWindow) isNormal() bool {
return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen()
}
func (w *windowsWebviewWindow) isVisible() bool {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
return style&w32.WS_VISIBLE != 0
}
func (w *windowsWebviewWindow) focus() {
w32.SetForegroundWindow(w.hwnd)
if w.isDisabled() {
return
}
if w.isMinimised() {
w.unminimise()
}
w.focusingChromium = true
w.chromium.Focus()
w.focusingChromium = false
}
// printStyle takes a windows style and prints it in a human-readable format
// This is for debugging window style issues
func (w *windowsWebviewWindow) printStyle() {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
fmt.Printf("Style: ")
if style&w32.WS_BORDER != 0 {
fmt.Printf("WS_BORDER ")
}
if style&w32.WS_CAPTION != 0 {
fmt.Printf("WS_CAPTION ")
}
if style&w32.WS_CHILD != 0 {
fmt.Printf("WS_CHILD ")
}
if style&w32.WS_CLIPCHILDREN != 0 {
fmt.Printf("WS_CLIPCHILDREN ")
}
if style&w32.WS_CLIPSIBLINGS != 0 {
fmt.Printf("WS_CLIPSIBLINGS ")
}
if style&w32.WS_DISABLED != 0 {
fmt.Printf("WS_DISABLED ")
}
if style&w32.WS_DLGFRAME != 0 {
fmt.Printf("WS_DLGFRAME ")
}
if style&w32.WS_GROUP != 0 {
fmt.Printf("WS_GROUP ")
}
if style&w32.WS_HSCROLL != 0 {
fmt.Printf("WS_HSCROLL ")
}
if style&w32.WS_MAXIMIZE != 0 {
fmt.Printf("WS_MAXIMIZE ")
}
if style&w32.WS_MAXIMIZEBOX != 0 {
fmt.Printf("WS_MAXIMIZEBOX ")
}
if style&w32.WS_MINIMIZE != 0 {
fmt.Printf("WS_MINIMIZE ")
}
if style&w32.WS_MINIMIZEBOX != 0 {
fmt.Printf("WS_MINIMIZEBOX ")
}
if style&w32.WS_OVERLAPPED != 0 {
fmt.Printf("WS_OVERLAPPED ")
}
if style&w32.WS_POPUP != 0 {
fmt.Printf("WS_POPUP ")
}
if style&w32.WS_SYSMENU != 0 {
fmt.Printf("WS_SYSMENU ")
}
if style&w32.WS_TABSTOP != 0 {
fmt.Printf("WS_TABSTOP ")
}
if style&w32.WS_THICKFRAME != 0 {
fmt.Printf("WS_THICKFRAME ")
}
if style&w32.WS_VISIBLE != 0 {
fmt.Printf("WS_VISIBLE ")
}
if style&w32.WS_VSCROLL != 0 {
fmt.Printf("WS_VSCROLL ")
}
fmt.Printf("\n")
// Do the same for the extended style
extendedStyle := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE))
fmt.Printf("Extended Style: ")
if extendedStyle&w32.WS_EX_ACCEPTFILES != 0 {
fmt.Printf("WS_EX_ACCEPTFILES ")
}
if extendedStyle&w32.WS_EX_APPWINDOW != 0 {
fmt.Printf("WS_EX_APPWINDOW ")
}
if extendedStyle&w32.WS_EX_CLIENTEDGE != 0 {
fmt.Printf("WS_EX_CLIENTEDGE ")
}
if extendedStyle&w32.WS_EX_COMPOSITED != 0 {
fmt.Printf("WS_EX_COMPOSITED ")
}
if extendedStyle&w32.WS_EX_CONTEXTHELP != 0 {
fmt.Printf("WS_EX_CONTEXTHELP ")
}
if extendedStyle&w32.WS_EX_CONTROLPARENT != 0 {
fmt.Printf("WS_EX_CONTROLPARENT ")
}
if extendedStyle&w32.WS_EX_DLGMODALFRAME != 0 {
fmt.Printf("WS_EX_DLGMODALFRAME ")
}
if extendedStyle&w32.WS_EX_LAYERED != 0 {
fmt.Printf("WS_EX_LAYERED ")
}
if extendedStyle&w32.WS_EX_LAYOUTRTL != 0 {
fmt.Printf("WS_EX_LAYOUTRTL ")
}
if extendedStyle&w32.WS_EX_LEFT != 0 {
fmt.Printf("WS_EX_LEFT ")
}
if extendedStyle&w32.WS_EX_LEFTSCROLLBAR != 0 {
fmt.Printf("WS_EX_LEFTSCROLLBAR ")
}
if extendedStyle&w32.WS_EX_LTRREADING != 0 {
fmt.Printf("WS_EX_LTRREADING ")
}
if extendedStyle&w32.WS_EX_MDICHILD != 0 {
fmt.Printf("WS_EX_MDICHILD ")
}
if extendedStyle&w32.WS_EX_NOACTIVATE != 0 {
fmt.Printf("WS_EX_NOACTIVATE ")
}
if extendedStyle&w32.WS_EX_NOINHERITLAYOUT != 0 {
fmt.Printf("WS_EX_NOINHERITLAYOUT ")
}
if extendedStyle&w32.WS_EX_NOPARENTNOTIFY != 0 {
fmt.Printf("WS_EX_NOPARENTNOTIFY ")
}
if extendedStyle&w32.WS_EX_NOREDIRECTIONBITMAP != 0 {
fmt.Printf("WS_EX_NOREDIRECTIONBITMAP ")
}
if extendedStyle&w32.WS_EX_OVERLAPPEDWINDOW != 0 {
fmt.Printf("WS_EX_OVERLAPPEDWINDOW ")
}
if extendedStyle&w32.WS_EX_PALETTEWINDOW != 0 {
fmt.Printf("WS_EX_PALETTEWINDOW ")
}
if extendedStyle&w32.WS_EX_RIGHT != 0 {
fmt.Printf("WS_EX_RIGHT ")
}
if extendedStyle&w32.WS_EX_RIGHTSCROLLBAR != 0 {
fmt.Printf("WS_EX_RIGHTSCROLLBAR ")
}
if extendedStyle&w32.WS_EX_RTLREADING != 0 {
fmt.Printf("WS_EX_RTLREADING ")
}
if extendedStyle&w32.WS_EX_STATICEDGE != 0 {
fmt.Printf("WS_EX_STATICEDGE ")
}
if extendedStyle&w32.WS_EX_TOOLWINDOW != 0 {
fmt.Printf("WS_EX_TOOLWINDOW ")
}
if extendedStyle&w32.WS_EX_TOPMOST != 0 {
fmt.Printf("WS_EX_TOPMOST ")
}
if extendedStyle&w32.WS_EX_TRANSPARENT != 0 {
fmt.Printf("WS_EX_TRANSPARENT ")
}
if extendedStyle&w32.WS_EX_WINDOWEDGE != 0 {
fmt.Printf("WS_EX_WINDOWEDGE ")
}
fmt.Printf("\n")
}
func (w *windowsWebviewWindow) show() {
// Always show the window container immediately (decouple from WebView state)
// This fixes issue #2861 where efficiency mode prevents window visibility
w32.ShowWindow(w.hwnd, w32.SW_SHOW)
w.windowShown = true
w.showRequested = true
w.updateContentProtection()
// Show WebView if navigation has completed
if w.webviewNavigationCompleted {
w.chromium.Show()
// Cancel timeout since we can show immediately
if w.visibilityTimeout != nil {
w.visibilityTimeout.Stop()
w.visibilityTimeout = nil
}
} else {
// Start timeout to show WebView if navigation is delayed (fallback for efficiency mode)
if w.visibilityTimeout == nil {
w.visibilityTimeout = time.AfterFunc(3*time.Second, func() {
// Show WebView even if navigation hasn't completed
// This prevents permanent invisibility in efficiency mode
if !w.webviewNavigationCompleted && w.chromium != nil {
w.chromium.Show()
}
w.visibilityTimeout = nil
})
}
}
}
func (w *windowsWebviewWindow) hide() {
w32.ShowWindow(w.hwnd, w32.SW_HIDE)
w.windowShown = false
w.showRequested = false
// Cancel any pending visibility timeout
if w.visibilityTimeout != nil {
w.visibilityTimeout.Stop()
w.visibilityTimeout = nil
}
}
// Get the screen for the current window
func (w *windowsWebviewWindow) getScreen() (*Screen, error) {
return getScreenForWindow(w)
}
func (w *windowsWebviewWindow) setFrameless(b bool) {
// Remove or add the frame
if b {
w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, w32.WS_VISIBLE|w32.WS_POPUP)
} else {
w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, w32.WS_VISIBLE|w32.WS_OVERLAPPEDWINDOW)
}
w32.SetWindowPos(
w.hwnd,
0,
0,
0,
0,
0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_FRAMECHANGED,
)
}
func newWindowImpl(parent *WebviewWindow) *windowsWebviewWindow {
result := &windowsWebviewWindow{
parent: parent,
resizeBorderWidth: int32(w32.GetSystemMetrics(w32.SM_CXSIZEFRAME)),
resizeBorderHeight: int32(w32.GetSystemMetrics(w32.SM_CYSIZEFRAME)),
// Initialize visibility tracking fields
showRequested: false,
visibilityTimeout: nil,
windowShown: false,
}
return result
}
func (w *windowsWebviewWindow) openContextMenu(menu *Menu, _ *ContextMenuData) {
// Destroy previous context menu if it exists to prevent memory leak
if w.currentlyOpenContextMenu != nil {
w.currentlyOpenContextMenu.Destroy()
}
// Create the menu from current Go-side menu state
thisMenu := NewPopupMenu(w.hwnd, menu)
thisMenu.Update()
w.currentlyOpenContextMenu = thisMenu
thisMenu.ShowAtCursor()
}
func (w *windowsWebviewWindow) setStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE))
if currentStyle != 0 {
if b {
currentStyle = currentStyle | style
} else {
currentStyle = currentStyle &^ style
}
w32.SetWindowLongPtr(w.hwnd, w32.GWL_STYLE, uintptr(currentStyle))
}
}
func (w *windowsWebviewWindow) setExStyle(b bool, style int) {
currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE))
if currentStyle != 0 {
if b {
currentStyle = currentStyle | style
} else {
currentStyle = currentStyle &^ style
}
w32.SetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE, uintptr(currentStyle))
}
}
func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) {
if !w32.SupportsBackdropTypes() {
var accent = w32.ACCENT_POLICY{
AccentState: w32.ACCENT_ENABLE_BLURBEHIND,
}
var data w32.WINDOWCOMPOSITIONATTRIBDATA
data.Attrib = w32.WCA_ACCENT_POLICY
data.PvData = w32.PVOID(&accent)
data.CbData = unsafe.Sizeof(accent)
w32.SetWindowCompositionAttribute(w.hwnd, &data)
} else {
w32.EnableTranslucency(w.hwnd, uint32(backdropType))
}
}
func (w *windowsWebviewWindow) setIcon(icon w32.HICON) {
w32.SendMessage(w.hwnd, w32.WM_SETICON, w32.ICON_BIG, icon)
}
func (w *windowsWebviewWindow) disableIcon() {
// TODO: If frameless, return
exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE)
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, uint32(exStyle|w32.WS_EX_DLGMODALFRAME))
w32.SetWindowPos(w.hwnd, 0, 0, 0, 0, 0,
uint(
w32.SWP_FRAMECHANGED|
w32.SWP_NOMOVE|
w32.SWP_NOSIZE|
w32.SWP_NOZORDER),
)
}
func (w *windowsWebviewWindow) processThemeColour(fn func(w32.HWND, uint32), value *uint32) {
if value == nil {
return
}
fn(w.hwnd, *value)
}
func (w *windowsWebviewWindow) isDisabled() bool {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
return style&w32.WS_DISABLED != 0
}
func (w *windowsWebviewWindow) updateTheme(isDarkMode bool) {
if w32.IsCurrentlyHighContrastMode() {
return
}
if !w32.SupportsThemes() {
return
}
w32.SetTheme(w.hwnd, isDarkMode)
// Clear any existing theme first
if w.menubarTheme != nil && !isDarkMode {
// Reset menu to default Windows theme when switching to light mode
w.menubarTheme = nil
if w.menu != nil {
// Clear the menu background by setting it to default
var mi w32.MENUINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS
mi.HbrBack = 0 // NULL brush resets to default
w32.SetMenuInfo(w.menu.menu, &mi)
}
}
// Custom theme processing
customTheme := w.parent.options.Windows.CustomTheme
// Custom theme
if w32.SupportsCustomThemes() {
var userTheme *MenuBarTheme
if isDarkMode {
userTheme = customTheme.DarkModeMenuBar
} else {
userTheme = customTheme.LightModeMenuBar
}
if userTheme != nil {
modeStr := "light"
if isDarkMode {
modeStr = "dark"
}
globalApplication.debug("Setting custom "+modeStr+" menubar theme", "window", w.parent.id)
w.menubarTheme = &w32.MenuBarTheme{
TitleBarBackground: userTheme.Default.Background,
TitleBarText: userTheme.Default.Text,
MenuBarBackground: userTheme.Default.Background, // Use default background for menubar
MenuHoverBackground: userTheme.Hover.Background,
MenuHoverText: userTheme.Hover.Text,
MenuSelectedBackground: userTheme.Selected.Background,
MenuSelectedText: userTheme.Selected.Text,
}
w.menubarTheme.Init()
// If menu is already set, update it
if w.menu != nil {
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
} else if userTheme == nil && isDarkMode {
// Use default dark theme if no custom theme provided
globalApplication.debug("Setting default dark menubar theme", "window", w.parent.id)
w.menubarTheme = &w32.MenuBarTheme{
TitleBarBackground: w32.RGBptr(45, 45, 45), // Dark titlebar
TitleBarText: w32.RGBptr(222, 222, 222), // Slightly muted white
MenuBarBackground: w32.RGBptr(33, 33, 33), // Standard dark mode (#212121)
MenuHoverBackground: w32.RGBptr(48, 48, 48), // Slightly lighter for hover (#303030)
MenuHoverText: w32.RGBptr(222, 222, 222), // Slightly muted white
MenuSelectedBackground: w32.RGBptr(48, 48, 48), // Same as hover
MenuSelectedText: w32.RGBptr(222, 222, 222), // Slightly muted white
}
w.menubarTheme.Init()
// If menu is already set, update it
if w.menu != nil {
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
} else if userTheme == nil && !isDarkMode && w.menu != nil {
// No custom theme for light mode - ensure menu is reset to default
globalApplication.debug("Resetting menu to default light theme", "window", w.parent.id)
var mi w32.MENUINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS
mi.HbrBack = 0 // NULL brush resets to default
w32.SetMenuInfo(w.menu.menu, &mi)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
// Define a map for theme selection
themeMap := map[bool]map[bool]*WindowTheme{
true: { // Window is active
true: customTheme.DarkModeActive, // Dark mode
false: customTheme.LightModeActive, // Light mode
},
false: { // Window is inactive
true: customTheme.DarkModeInactive, // Dark mode
false: customTheme.LightModeInactive, // Light mode
},
}
// Select the appropriate theme
theme := themeMap[w.isActive()][isDarkMode]
// Apply theme colors
if theme != nil {
w.processThemeColour(w32.SetTitleBarColour, theme.TitleBarColour)
w.processThemeColour(w32.SetTitleTextColour, theme.TitleTextColour)
w.processThemeColour(w32.SetBorderColour, theme.BorderColour)
}
}
}
func (w *windowsWebviewWindow) isActive() bool {
return w32.GetForegroundWindow() == w.hwnd
}
var resizePending int32
func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
// Use the original implementation that works perfectly for maximized
processed, code := w32.MenuBarWndProc(w.hwnd, msg, wparam, lparam, w.menubarTheme)
if processed {
return code
}
switch msg {
case w32.WM_ACTIVATE:
if int(wparam&0xffff) == w32.WA_INACTIVE {
w.parent.emit(events.Windows.WindowInactive)
}
if wparam == w32.WA_ACTIVE {
getNativeApplication().currentWindowID = w.parent.id
w.parent.emit(events.Windows.WindowActive)
}
if wparam == w32.WA_CLICKACTIVE {
getNativeApplication().currentWindowID = w.parent.id
w.parent.emit(events.Windows.WindowClickActive)
}
// If we want to have a frameless window but with the default frame decorations, extend the DWM client area.
// This Option is not affected by returning 0 in WM_NCCALCSIZE.
// As a result we have hidden the titlebar but still have the default window frame styling.
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks
if w.framelessWithDecorations() {
err := w32.ExtendFrameIntoClientArea(w.hwnd, true)
if err != nil {
globalApplication.handleFatalError(err)
}
}
case w32.WM_CLOSE:
if atomic.LoadUint32(&w.parent.unconditionallyClose) == 0 {
// We were called by `Close()` or pressing the close button on the window
w.parent.emit(events.Windows.WindowClosing)
return 0
}
defer func() {
windowsApp := globalApplication.impl.(*windowsApp)
windowsApp.unregisterWindow(w)
}()
// Now do the actual close
w.chromium.ShuttingDown()
return w32.DefWindowProc(w.hwnd, w32.WM_CLOSE, 0, 0)
case w32.WM_KILLFOCUS:
if w.focusingChromium {
return 0
}
w.parent.emit(events.Windows.WindowKillFocus)
case w32.WM_ENTERSIZEMOVE:
// This is needed to close open dropdowns when moving the window https://github.com/MicrosoftEdge/WebView2Feedback/issues/2290
w32.SetFocus(w.hwnd)
if int(w32.GetKeyState(w32.VK_LBUTTON))&(0x8000) != 0 {
// Left mouse button is down - window is being moved
w.parent.emit(events.Windows.WindowStartMove)
} else {
// Window is being resized
w.parent.emit(events.Windows.WindowStartResize)
}
case w32.WM_EXITSIZEMOVE:
if int(w32.GetKeyState(w32.VK_LBUTTON))&0x8000 != 0 {
w.parent.emit(events.Windows.WindowEndMove)
} else {
w.parent.emit(events.Windows.WindowEndResize)
}
case w32.WM_SETFOCUS:
w.focus()
w.parent.emit(events.Windows.WindowSetFocus)
case w32.WM_MOVE, w32.WM_MOVING:
_ = w.chromium.NotifyParentWindowPositionChanged()
w.moveDebouncer(func() {
w.parent.emit(events.Windows.WindowDidMove)
})
case w32.WM_SHOWWINDOW:
if wparam == 1 {
w.parent.emit(events.Windows.WindowShow)
w.updateContentProtection()
} else {
w.parent.emit(events.Windows.WindowHide)
}
case w32.WM_WINDOWPOSCHANGED:
windowPos := (*w32.WINDOWPOS)(unsafe.Pointer(lparam))
if windowPos.Flags&w32.SWP_NOZORDER == 0 {
w.parent.emit(events.Windows.WindowZOrderChanged)
}
case w32.WM_PAINT:
w.parent.emit(events.Windows.WindowPaint)
case w32.WM_ERASEBKGND:
w.parent.emit(events.Windows.WindowBackgroundErase)
return 1 // Let WebView2 handle background erasing
// WM_UAHDRAWMENUITEM is handled by MenuBarWndProc at the top of this function
// Check for keypress
case w32.WM_SYSCOMMAND:
switch wparam {
case w32.SC_KEYMENU:
if lparam == 0 {
// F10 or plain Alt key
if w.processKeyBinding(w32.VK_F10) {
return 0
}
} else {
// Alt + key combination
// The character code is in the low word of lparam
char := byte(lparam & 0xFF)
// Convert ASCII to virtual key code if needed
vkey := w32.VkKeyScan(uint16(char))
if w.processKeyBinding(uint(vkey)) {
return 0
}
}
}
case w32.WM_SYSKEYDOWN:
globalApplication.info("w32.WM_SYSKEYDOWN", "wparam", uint(wparam))
w.parent.emit(events.Windows.WindowKeyDown)
if w.processKeyBinding(uint(wparam)) {
return 0
}
case w32.WM_SYSKEYUP:
w.parent.emit(events.Windows.WindowKeyUp)
case w32.WM_KEYDOWN:
w.parent.emit(events.Windows.WindowKeyDown)
w.processKeyBinding(uint(wparam))
case w32.WM_KEYUP:
w.parent.emit(events.Windows.WindowKeyUp)
case w32.WM_SIZE:
switch wparam {
case w32.SIZE_MAXIMIZED:
if w.isMinimizing {
w.parent.emit(events.Windows.WindowUnMinimise)
}
w.isMinimizing = false
w.parent.emit(events.Windows.WindowMaximise)
// Force complete redraw when maximized
if w.menu != nil && w.menubarTheme != nil {
// Invalidate the entire window to force complete redraw
w32.RedrawWindow(w.hwnd, nil, 0, w32.RDW_FRAME|w32.RDW_INVALIDATE|w32.RDW_UPDATENOW)
}
case w32.SIZE_RESTORED:
if w.isMinimizing {
w.parent.emit(events.Windows.WindowUnMinimise)
}
w.isMinimizing = false
w.parent.emit(events.Windows.WindowRestore)
case w32.SIZE_MINIMIZED:
w.isMinimizing = true
w.parent.emit(events.Windows.WindowMinimise)
}
doResize := func() {
// Get the new size from lparam
width := int32(lparam & 0xFFFF)
height := int32((lparam >> 16) & 0xFFFF)
bounds := &edge.Rect{
Left: 0,
Top: 0,
Right: width,
Bottom: height,
}
InvokeSync(func() {
time.Sleep(1 * time.Nanosecond)
w.chromium.ResizeWithBounds(bounds)
atomic.StoreInt32(&resizePending, 0)
w.parent.emit(events.Windows.WindowDidResize)
})
}
if w.parent.options.Frameless && wparam == w32.SIZE_MINIMIZED {
// If the window is frameless, and we are minimizing, then we need to suppress the Resize on the
// WebView2. If we don't do this, restoring does not work as expected and first restores with some wrong
// size during the restore animation and only fully renders when the animation is done. This highly
// depends on the content in the WebView, see https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549
} else if w.resizeDebouncer != nil {
w.resizeDebouncer(doResize)
} else {
if atomic.CompareAndSwapInt32(&resizePending, 0, 1) {
doResize()
}
}
return 0
case w32.WM_GETMINMAXINFO:
mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam))
hasConstraints := false
options := w.parent.options
// Using ScreenManager to get the closest screen and scale according to its DPI is problematic
// here because in multi-monitor setup, when dragging the window between monitors with the mouse
// on the side with the higher DPI, the DPI change point is offset beyond the mid point, causing
// wrong scaling and unwanted resizing when using the monitor DPI. To avoid this issue, we use
// scaleWithWindowDPI() instead which retrieves the correct DPI with GetDpiForWindow().
if options.MinWidth > 0 || options.MinHeight > 0 {
hasConstraints = true
width, height := w.scaleWithWindowDPI(options.MinWidth, options.MinHeight)
if width > 0 {
mmi.PtMinTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMinTrackSize.Y = int32(height)
}
}
if options.MaxWidth > 0 || options.MaxHeight > 0 {
hasConstraints = true
width, height := w.scaleWithWindowDPI(options.MaxWidth, options.MaxHeight)
if width > 0 {
mmi.PtMaxTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMaxTrackSize.Y = int32(height)
}
}
if hasConstraints {
return 0
}
case w32.WM_DPICHANGED:
if !w.ignoreDPIChangeResizing {
newWindowRect := (*w32.RECT)(unsafe.Pointer(lparam))
w32.SetWindowPos(w.hwnd,
uintptr(0),
int(newWindowRect.Left),
int(newWindowRect.Top),
int(newWindowRect.Right-newWindowRect.Left),
int(newWindowRect.Bottom-newWindowRect.Top),
w32.SWP_NOZORDER|w32.SWP_NOACTIVATE)
}
w.parent.emit(events.Windows.WindowDPIChanged)
}
if w.parent.options.Windows.WindowMask != nil {
switch msg {
case w32.WM_NCHITTEST:
if w.parent.options.Windows.WindowMaskDraggable {
return w32.HTCAPTION
}
w.parent.emit(events.Windows.WindowNonClientHit)
return w32.HTCLIENT
case w32.WM_NCLBUTTONDOWN:
w.parent.emit(events.Windows.WindowNonClientMouseDown)
case w32.WM_NCLBUTTONUP:
w.parent.emit(events.Windows.WindowNonClientMouseUp)
case w32.WM_NCMOUSEMOVE:
w.parent.emit(events.Windows.WindowNonClientMouseMove)
case w32.WM_NCMOUSELEAVE:
w.parent.emit(events.Windows.WindowNonClientMouseLeave)
}
}
if w.menu != nil || w.currentlyOpenContextMenu != nil {
switch msg {
case w32.WM_COMMAND:
cmdMsgID := int(wparam & 0xffff)
switch cmdMsgID {
default:
var processed bool
if w.currentlyOpenContextMenu != nil {
processed = w.currentlyOpenContextMenu.ProcessCommand(cmdMsgID)
w.currentlyOpenContextMenu = nil
}
if !processed && w.menu != nil {
processed = w.menu.ProcessCommand(cmdMsgID)
}
}
}
}
if options := w.parent.options; options.Frameless {
switch msg {
case w32.WM_ACTIVATE:
// If we want to have a frameless window but with the default frame decorations, extend the DWM client area.
// This Option is not affected by returning 0 in WM_NCCALCSIZE.
// As a result we have hidden the titlebar but still have the default window frame styling.
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks
if w.framelessWithDecorations() {
err := w32.ExtendFrameIntoClientArea(w.hwnd, true)
if err != nil {
globalApplication.handleFatalError(err)
}
}
case w32.WM_NCCALCSIZE:
// Disable the standard frame by allowing the client area to take the full
// window size.
// See: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks
// This hides the titlebar and also disables the resizing from user interaction because the standard frame is not
// shown. We still need the WS_THICKFRAME style to enable resizing from the frontend.
if wparam != 0 {
rgrc := (*w32.RECT)(unsafe.Pointer(lparam))
if w.isCurrentlyFullscreen {
// In Full-Screen mode we don't need to adjust anything
// It essential we have the flag here, that is set before SetWindowPos in fullscreen/unfullscreen
// because the native size might not yet reflect we are in fullscreen during this event!
w.setPadding(edge.Rect{})
} else if w.isMaximised() {
// If the window is maximized we must adjust the client area to the work area of the monitor. Otherwise
// some content goes beyond the visible part of the monitor.
// Make sure to use the provided RECT to get the monitor, because during maximizig there might be
// a wrong monitor returned in multiscreen mode when using MonitorFromWindow.
// See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549
screen := ScreenNearestPhysicalRect(Rect{
X: int(rgrc.Left),
Y: int(rgrc.Top),
Width: int(rgrc.Right - rgrc.Left),
Height: int(rgrc.Bottom - rgrc.Top),
})
rect := screen.PhysicalWorkArea
maxWidth := options.MaxWidth
maxHeight := options.MaxHeight
if maxWidth > 0 {
maxWidth = screen.scale(maxWidth, false)
if rect.Width > maxWidth {
rect.Width = maxWidth
}
}
if maxHeight > 0 {
maxHeight = screen.scale(maxHeight, false)
if rect.Height > maxHeight {
rect.Height = maxHeight
}
}
*rgrc = w32.RECT{
Left: int32(rect.X),
Top: int32(rect.Y),
Right: int32(rect.X + rect.Width),
Bottom: int32(rect.Y + rect.Height),
}
w.setPadding(edge.Rect{})
} else {
// This is needed to work around the resize flickering in frameless mode with WindowDecorations
// See: https://stackoverflow.com/a/6558508
// The workaround from the SO answer suggests to reduce the bottom of the window by 1px.
// However, this would result in losing 1px of the WebView content.
// Increasing the bottom also worksaround the flickering, but we would lose 1px of the WebView content
// therefore let's pad the content with 1px at the bottom.
rgrc.Bottom += 1
w.setPadding(edge.Rect{Bottom: 1})
}
return 0
}
}
}
return w32.DefWindowProc(w.hwnd, msg, wparam, lparam)
}
func (w *windowsWebviewWindow) DPI() (w32.UINT, w32.UINT) {
if w32.HasGetDpiForWindowFunc() {
// GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accurate
// one, especially it is consistent with the WM_DPICHANGED event.
dpi := w32.GetDpiForWindow(w.hwnd)
return dpi, dpi
}
if w32.HasGetDPIForMonitorFunc() {
// GetDpiForWindow is supported beginning with Windows 8.1
monitor := w32.MonitorFromWindow(w.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 {
return 0, 0
}
var dpiX, dpiY w32.UINT
w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
return dpiX, dpiY
}
// If none of the above is supported fallback to the System DPI.
screen := w32.GetDC(0)
x := w32.GetDeviceCaps(screen, w32.LOGPIXELSX)
y := w32.GetDeviceCaps(screen, w32.LOGPIXELSY)
w32.ReleaseDC(0, screen)
return w32.UINT(x), w32.UINT(y)
}
func (w *windowsWebviewWindow) scaleWithWindowDPI(width, height int) (int, int) {
dpix, dpiy := w.DPI()
scaledWidth := ScaleWithDPI(width, dpix)
scaledHeight := ScaleWithDPI(height, dpiy)
return scaledWidth, scaledHeight
}
func ScaleWithDPI(pixels int, dpi uint) int {
return (pixels * int(dpi)) / 96
}
func (w *windowsWebviewWindow) setWindowMask(imageData []byte) {
// Set the window to a WS_EX_LAYERED window
newStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE) | w32.WS_EX_LAYERED
if w.isAlwaysOnTop() {
newStyle |= w32.WS_EX_TOPMOST
}
// Save the current window style
w.previousWindowExStyle = uint32(w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE))
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, uint32(newStyle))
data, err := pngToImage(imageData)
if err != nil {
globalApplication.fatal("fatal error in callback setWindowMask: %w", err)
}
bitmap, err := w32.CreateHBITMAPFromImage(data)
hdc := w32.CreateCompatibleDC(0)
defer w32.DeleteDC(hdc)
oldBitmap := w32.SelectObject(hdc, bitmap)
defer w32.SelectObject(hdc, oldBitmap)
screenDC := w32.GetDC(0)
defer w32.ReleaseDC(0, screenDC)
size := w32.SIZE{CX: int32(data.Bounds().Dx()), CY: int32(data.Bounds().Dy())}
ptSrc := w32.POINT{X: 0, Y: 0}
ptDst := w32.POINT{X: int32(w.width()), Y: int32(w.height())}
blend := w32.BLENDFUNCTION{
BlendOp: w32.AC_SRC_OVER,
BlendFlags: 0,
SourceConstantAlpha: 255,
AlphaFormat: w32.AC_SRC_ALPHA,
}
w32.UpdateLayeredWindow(w.hwnd, screenDC, &ptDst, &size, hdc, &ptSrc, 0, &blend, w32.ULW_ALPHA)
}
func (w *windowsWebviewWindow) isAlwaysOnTop() bool {
return w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE)&w32.WS_EX_TOPMOST != 0
}
// processMessage is given a message sent from JS via the postMessage API
// We put it on the global window message buffer to be processed centrally
func (w *windowsWebviewWindow) processMessage(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) {
topSource, err := sender.GetSource()
if err != nil {
globalApplication.error("Unable to get source from sender: %s", err.Error())
topSource = ""
}
senderSource, err := args.GetSource()
if err != nil {
globalApplication.error("Unable to get source from args: %s", err.Error())
senderSource = ""
}
// We send all messages to the centralised window message buffer
windowMessageBuffer <- &windowMessage{
windowId: w.parent.id,
message: message,
originInfo: &OriginInfo{
Origin: senderSource,
TopOrigin: topSource,
},
}
}
func (w *windowsWebviewWindow) processRequest(
req *edge.ICoreWebView2WebResourceRequest,
args *edge.ICoreWebView2WebResourceRequestedEventArgs,
) {
// Setting the UserAgent on the CoreWebView2Settings clears the whole default UserAgent of the Edge browser, but
// we want to just append our ApplicationIdentifier. So we adjust the UserAgent for every request.
if reqHeaders, err := req.GetHeaders(); err == nil {
useragent, _ := reqHeaders.GetHeader(assetserver.HeaderUserAgent)
useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ")
err = reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent)
if err != nil {
globalApplication.fatal("error setting UserAgent header: %w", err)
}
err = reqHeaders.SetHeader(
webViewRequestHeaderWindowId,
strconv.FormatUint(uint64(w.parent.id), 10),
)
if err != nil {
globalApplication.fatal("error setting WindowId header: %w", err)
}
err = reqHeaders.Release()
if err != nil {
globalApplication.fatal("error releasing headers: %w", err)
}
}
if globalApplication.assets == nil {
// We are using the devServer let the WebView2 handle the request with its default handler
return
}
//Get the request
uri, _ := req.GetUri()
reqUri, err := url.ParseRequestURI(uri)
if err != nil {
globalApplication.error("unable to parse request uri: uri='%s' error='%w'", uri, err)
return
}
if reqUri.Scheme != "http" {
// Let the WebView2 handle the request with its default handler
return
} else if !strings.HasPrefix(reqUri.Host, "wails.localhost") {
// Let the WebView2 handle the request with its default handler
return
}
webviewRequest, err := webview.NewRequest(
w.chromium.Environment(),
args,
func(fn func()) {
InvokeSync(fn)
})
if err != nil {
globalApplication.error("%s: NewRequest failed: %w", uri, err)
return
}
webviewRequests <- &webViewAssetRequest{
Request: webviewRequest,
windowId: w.parent.id,
windowName: w.parent.options.Name,
}
}
func (w *windowsWebviewWindow) setupChromium() {
chromium := w.chromium
debugMode := globalApplication.isDebugMode
opts := w.parent.options.Windows
webview2version, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(
globalApplication.options.Windows.WebviewBrowserPath,
)
if err != nil {
globalApplication.error("error getting WebView2 version: %w", err)
return
}
globalApplication.capabilities = capabilities.NewCapabilities(webview2version)
// We disable this by default. Can be overridden with the `EnableFraudulentWebsiteWarnings` option
opts.DisabledFeatures = append(opts.DisabledFeatures, "msSmartScreenProtection")
if len(opts.DisabledFeatures) > 0 {
opts.DisabledFeatures = sliceutil.Unique(opts.DisabledFeatures)
arg := fmt.Sprintf("--disable-features=%s", strings.Join(opts.DisabledFeatures, ","))
chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, arg)
}
if len(opts.EnabledFeatures) > 0 {
opts.EnabledFeatures = sliceutil.Unique(opts.EnabledFeatures)
arg := fmt.Sprintf("--enable-features=%s", strings.Join(opts.EnabledFeatures, ","))
chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, arg)
}
if len(opts.AdditionalLaunchArgs) > 0 {
chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, opts.AdditionalLaunchArgs...)
}
chromium.DataPath = globalApplication.options.Windows.WebviewUserDataPath
chromium.BrowserPath = globalApplication.options.Windows.WebviewBrowserPath
if opts.Permissions != nil {
for permission, state := range opts.Permissions {
chromium.SetPermission(edge.CoreWebView2PermissionKind(permission),
edge.CoreWebView2PermissionState(state))
}
}
chromium.MessageCallback = w.processMessage
chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects
chromium.WebResourceRequestedCallback = w.processRequest
chromium.ContainsFullScreenElementChangedCallback = w.fullscreenChanged
chromium.NavigationCompletedCallback = w.navigationCompleted
chromium.AcceleratorKeyCallback = w.processKeyBinding
chromium.Embed(w.hwnd)
// Prevent efficiency mode by keeping WebView2 visible (fixes issue #2861)
// Microsoft recommendation: keep IsVisible = true to avoid efficiency mode
// See: https://github.com/MicrosoftEdge/WebView2Feedback/discussions/4021
// TODO: Re-enable when PutIsVisible method is available in go-webview2 package
// err := chromium.PutIsVisible(true)
// if err != nil {
// globalApplication.error("Failed to set WebView2 visibility for efficiency mode prevention: %v", err)
// }
if chromium.HasCapability(edge.SwipeNavigation) {
err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures)
if err != nil {
globalApplication.handleFatalError(err)
}
}
if w.parent.options.EnableDragAndDrop {
if chromium.HasCapability(edge.AllowExternalDrop) {
err := chromium.AllowExternalDrag(false)
if err != nil {
globalApplication.handleFatalError(err)
}
}
// Initialize OLE for drag-and-drop operations
w32.OleInitialise()
w.dropTarget = w32.NewDropTarget()
w.dropTarget.OnDrop = func(files []string, x int, y int) {
w.parent.emit(events.Windows.WindowDragDrop)
globalApplication.debug("[DragDropDebug] Windows DropTarget OnDrop: Raw screen coordinates", "x", x, "y", y)
// Convert screen coordinates to window-relative coordinates first
// Windows DropTarget gives us screen coordinates, but we need window-relative coordinates
windowRect := w32.GetWindowRect(w.hwnd)
windowRelativeX := x - int(windowRect.Left)
windowRelativeY := y - int(windowRect.Top)
globalApplication.debug("[DragDropDebug] Windows DropTarget OnDrop: After screen-to-window conversion", "windowRelativeX", windowRelativeX, "windowRelativeY", windowRelativeY)
// Convert window-relative coordinates to webview-relative coordinates
webviewX, webviewY := w.convertWindowToWebviewCoordinates(windowRelativeX, windowRelativeY)
globalApplication.debug("[DragDropDebug] Windows DropTarget OnDrop: Final webview coordinates", "webviewX", webviewX, "webviewY", webviewY)
w.parent.InitiateFrontendDropProcessing(files, webviewX, webviewY)
}
if opts.OnEnterEffect != 0 {
w.dropTarget.OnEnterEffect = convertEffect(opts.OnEnterEffect)
}
if opts.OnOverEffect != 0 {
w.dropTarget.OnOverEffect = convertEffect(opts.OnOverEffect)
}
w.dropTarget.OnEnter = func() {
w.parent.emit(events.Windows.WindowDragEnter)
}
w.dropTarget.OnLeave = func() {
w.parent.emit(events.Windows.WindowDragLeave)
}
w.dropTarget.OnOver = func() {
w.parent.emit(events.Windows.WindowDragOver)
}
// Enumerate all the child windows for this window and register them as drop targets
w32.EnumChildWindows(w.hwnd, func(hwnd w32.HWND, lparam w32.LPARAM) w32.LRESULT {
// Check if the window class is "Chrome_RenderWidgetHostHWND"
// If it is, then we register it as a drop target
//windowName := w32.GetClassName(hwnd)
//println(windowName)
//if windowName == "Chrome_RenderWidgetHostHWND" {
err := w32.RegisterDragDrop(hwnd, w.dropTarget)
if err != nil && !errors.Is(err, syscall.Errno(w32.DRAGDROP_E_ALREADYREGISTERED)) {
globalApplication.error("error registering drag and drop: %w", err)
}
//}
return 1
})
}
err = chromium.PutIsGeneralAutofillEnabled(opts.GeneralAutofillEnabled)
if err != nil {
if errors.Is(err, edge.UnsupportedCapabilityError) {
globalApplication.warning("unsupported capability: GeneralAutofillEnabled")
} else {
globalApplication.handleFatalError(err)
}
}
err = chromium.PutIsPasswordAutosaveEnabled(opts.PasswordAutosaveEnabled)
if err != nil {
if errors.Is(err, edge.UnsupportedCapabilityError) {
globalApplication.warning("unsupported capability: PasswordAutosaveEnabled")
} else {
globalApplication.handleFatalError(err)
}
}
chromium.Resize()
settings, err := chromium.GetSettings()
if err != nil {
globalApplication.handleFatalError(err)
}
if settings == nil {
globalApplication.fatal("error getting settings")
}
err = settings.PutAreDefaultContextMenusEnabled(
debugMode || !w.parent.options.DefaultContextMenuDisabled,
)
if err != nil {
globalApplication.handleFatalError(err)
}
w.enableDevTools(settings)
if w.parent.options.Zoom > 0.0 {
chromium.PutZoomFactor(w.parent.options.Zoom)
}
err = settings.PutIsZoomControlEnabled(w.parent.options.ZoomControlEnabled)
if err != nil {
globalApplication.handleFatalError(err)
}
err = settings.PutIsStatusBarEnabled(false)
if err != nil {
globalApplication.handleFatalError(err)
}
err = settings.PutAreBrowserAcceleratorKeysEnabled(false)
if err != nil {
globalApplication.handleFatalError(err)
}
err = settings.PutIsSwipeNavigationEnabled(false)
if err != nil {
globalApplication.handleFatalError(err)
}
if debugMode && w.parent.options.OpenInspectorOnStartup {
chromium.OpenDevToolsWindow()
}
// Set background colour
w.setBackgroundColour(w.parent.options.BackgroundColour)
chromium.SetBackgroundColour(
w.parent.options.BackgroundColour.Red,
w.parent.options.BackgroundColour.Green,
w.parent.options.BackgroundColour.Blue,
w.parent.options.BackgroundColour.Alpha,
)
chromium.SetGlobalPermission(edge.CoreWebView2PermissionStateAllow)
chromium.AddWebResourceRequestedFilter("*", edge.COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL)
if w.parent.options.HTML != "" {
var script string
if w.parent.options.JS != "" {
script = w.parent.options.JS
}
if w.parent.options.CSS != "" {
script += fmt.Sprintf(
"; addEventListener(\"DOMContentLoaded\", (event) => { document.head.appendChild(document.createElement('style')).innerHTML=\"%s\"; });",
strings.ReplaceAll(w.parent.options.CSS, `"`, `\"`),
)
}
if script != "" {
chromium.Init(script)
}
chromium.NavigateToString(w.parent.options.HTML)
} else {
startURL, err := assetserver.GetStartURL(w.parent.options.URL)
if err != nil {
globalApplication.handleFatalError(err)
}
w.webviewNavigationCompleted = false
chromium.Navigate(startURL)
}
}
func (w *windowsWebviewWindow) fullscreenChanged(
sender *edge.ICoreWebView2,
_ *edge.ICoreWebView2ContainsFullScreenElementChangedEventArgs,
) {
isFullscreen, err := sender.GetContainsFullScreenElement()
if err != nil {
globalApplication.fatal("fatal error in callback fullscreenChanged: %w", err)
}
if isFullscreen {
w.fullscreen()
} else {
w.unfullscreen()
}
}
func convertEffect(effect DragEffect) w32.DWORD {
switch effect {
case DragEffectCopy:
return w32.DROPEFFECT_COPY
case DragEffectMove:
return w32.DROPEFFECT_MOVE
case DragEffectLink:
return w32.DROPEFFECT_LINK
default:
return w32.DROPEFFECT_NONE
}
}
func (w *windowsWebviewWindow) flash(enabled bool) {
w32.FlashWindow(w.hwnd, enabled)
}
func (w *windowsWebviewWindow) navigationCompleted(
sender *edge.ICoreWebView2,
args *edge.ICoreWebView2NavigationCompletedEventArgs,
) {
// Install the runtime core
w.execJS(runtime.Core(globalApplication.impl.GetFlags(globalApplication.options)))
// EmitEvent DomReady ApplicationEvent
windowEvents <- &windowEvent{EventID: uint(events.Windows.WebViewNavigationCompleted), WindowID: w.parent.id}
if w.webviewNavigationCompleted {
// NavigationCompleted is triggered for every Load. If an application uses reloads the Hide/Show will trigger
// a flickering of the window with every reload. So we only do this once for the first NavigationCompleted.
return
}
w.webviewNavigationCompleted = true
// Cancel any pending visibility timeout since navigation completed
if w.visibilityTimeout != nil {
w.visibilityTimeout.Stop()
w.visibilityTimeout = nil
}
wasFocused := w.isFocused()
// Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026
err := w.chromium.Hide()
if err != nil {
globalApplication.handleFatalError(err)
}
err = w.chromium.Show()
if err != nil {
globalApplication.handleFatalError(err)
}
if wasFocused {
w.focus()
}
// Only call parent.Show() if not hidden and show was requested but window wasn't shown yet
// The new robust show() method handles window visibility independently
if !w.parent.options.Hidden {
if w.showRequested && !w.windowShown {
w.parent.Show()
}
w.update()
}
}
func (w *windowsWebviewWindow) processKeyBinding(vkey uint) bool {
globalApplication.debug("Processing key binding", "vkey", vkey)
// Get the keyboard state and convert to an accelerator
var keyState [256]byte
if !w32.GetKeyboardState(keyState[:]) {
globalApplication.error("error getting keyboard state")
return false
}
var acc accelerator
// Check if CTRL is pressed
if keyState[w32.VK_CONTROL]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, ControlKey)
}
// Check if ALT is pressed
if keyState[w32.VK_MENU]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, OptionOrAltKey)
}
// Check if SHIFT is pressed
if keyState[w32.VK_SHIFT]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, ShiftKey)
}
// Check if WIN is pressed
if keyState[w32.VK_LWIN]&0x80 != 0 || keyState[w32.VK_RWIN]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, SuperKey)
}
if vkey != w32.VK_CONTROL && vkey != w32.VK_MENU && vkey != w32.VK_SHIFT &&
vkey != w32.VK_LWIN &&
vkey != w32.VK_RWIN {
// Convert the vkey to a string
accKey, ok := VirtualKeyCodes[vkey]
if !ok {
return false
}
acc.Key = accKey
}
accKey := acc.String()
globalApplication.debug("Processing key binding", "vkey", vkey, "acc", accKey)
// Process the key binding
if w.parent.processKeyBinding(accKey) {
return true
}
if accKey == "alt+f4" {
w32.PostMessage(w.hwnd, w32.WM_CLOSE, 0, 0)
return true
}
return false
}
func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(
message string,
sender *edge.ICoreWebView2,
args *edge.ICoreWebView2WebMessageReceivedEventArgs,
) {
if strings.HasPrefix(message, "FilesDropped") {
objs, err := args.GetAdditionalObjects()
if err != nil {
globalApplication.handleError(err)
return
}
defer func() {
err = objs.Release()
if err != nil {
globalApplication.error("error releasing objects: %w", err)
}
}()
count, err := objs.GetCount()
if err != nil {
globalApplication.error("cannot get count: %w", err)
return
}
var filenames []string
for i := uint32(0); i < count; i++ {
_file, err := objs.GetValueAtIndex(i)
if err != nil {
globalApplication.error("cannot get value at %d: %w", i, err)
return
}
file := (*edge.ICoreWebView2File)(unsafe.Pointer(_file))
// TODO: Fix this
defer file.Release()
filepath, err := file.GetPath()
if err != nil {
globalApplication.error("cannot get path for object at %d: %w", i, err)
return
}
filenames = append(filenames, filepath)
}
// Extract X/Y coordinates from message - format should be "FilesDropped:x:y"
var x, y int
parts := strings.Split(message, ":")
if len(parts) >= 3 {
if parsedX, err := strconv.Atoi(parts[1]); err == nil {
x = parsedX
}
if parsedY, err := strconv.Atoi(parts[2]); err == nil {
y = parsedY
}
}
globalApplication.debug("[DragDropDebug] processMessageWithAdditionalObjects: Raw WebView2 coordinates", "x", x, "y", y)
// Convert webview-relative coordinates to window-relative coordinates, then to webview-relative coordinates
// Note: The coordinates from WebView2 are already webview-relative, but let's log them for debugging
webviewX, webviewY := x, y
globalApplication.debug("[DragDropDebug] processMessageWithAdditionalObjects: Using coordinates as-is (already webview-relative)", "webviewX", webviewX, "webviewY", webviewY)
w.parent.InitiateFrontendDropProcessing(filenames, webviewX, webviewY)
return
}
}
func (w *windowsWebviewWindow) setMaximiseButtonEnabled(enabled bool) {
w.setStyle(enabled, w32.WS_MAXIMIZEBOX)
}
func (w *windowsWebviewWindow) setMinimiseButtonEnabled(enabled bool) {
w.setStyle(enabled, w32.WS_MINIMIZEBOX)
}
func (w *windowsWebviewWindow) toggleMenuBar() {
if w.menu != nil {
if w32.GetMenu(w.hwnd) == 0 {
w32.SetMenu(w.hwnd, w.menu.menu)
} else {
w32.SetMenu(w.hwnd, 0)
}
// Get the bounds of the client area
//bounds := w32.GetClientRect(w.hwnd)
// Resize the webview
w.chromium.Resize()
// Update size of webview
w.update()
// Restore focus to the webview after toggling menu
w.focus()
}
}
func (w *windowsWebviewWindow) enableRedraw() {
w32.SendMessage(w.hwnd, w32.WM_SETREDRAW, 1, 0)
w32.RedrawWindow(
w.hwnd,
nil,
0,
w32.RDW_ERASE|w32.RDW_FRAME|w32.RDW_INVALIDATE|w32.RDW_ALLCHILDREN,
)
}
func (w *windowsWebviewWindow) disableRedraw() {
w32.SendMessage(w.hwnd, w32.WM_SETREDRAW, 0, 0)
}
func (w *windowsWebviewWindow) disableRedrawWithCallback(callback func()) {
w.disableRedraw()
callback()
w.enableRedraw()
}
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) {
var err error
var result w32.HICON
if result = w32.LoadIconWithResourceID(instance, resId); result == 0 {
err = fmt.Errorf("cannot load icon from resource with id %v", resId)
}
return result, err
}
func (w *windowsWebviewWindow) setMinimiseButtonState(state ButtonState) {
switch state {
case ButtonDisabled, ButtonHidden:
w.setStyle(false, w32.WS_MINIMIZEBOX)
case ButtonEnabled:
w.setStyle(true, w32.WS_SYSMENU)
w.setStyle(true, w32.WS_MINIMIZEBOX)
}
}
func (w *windowsWebviewWindow) setMaximiseButtonState(state ButtonState) {
switch state {
case ButtonDisabled, ButtonHidden:
w.setStyle(false, w32.WS_MAXIMIZEBOX)
case ButtonEnabled:
w.setStyle(true, w32.WS_SYSMENU)
w.setStyle(true, w32.WS_MAXIMIZEBOX)
}
}
func (w *windowsWebviewWindow) setCloseButtonState(state ButtonState) {
switch state {
case ButtonEnabled:
w.setStyle(true, w32.WS_SYSMENU)
_ = w32.EnableCloseButton(w.hwnd)
case ButtonDisabled:
w.setStyle(true, w32.WS_SYSMENU)
_ = w32.DisableCloseButton(w.hwnd)
case ButtonHidden:
w.setStyle(false, w32.WS_SYSMENU)
}
}
func (w *windowsWebviewWindow) setGWLStyle(style int) {
w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, uint32(style))
}
func (w *windowsWebviewWindow) isIgnoreMouseEvents() bool {
exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE)
return exStyle&w32.WS_EX_TRANSPARENT != 0
}
func (w *windowsWebviewWindow) setIgnoreMouseEvents(ignore bool) {
exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE)
if ignore {
exStyle |= w32.WS_EX_LAYERED | w32.WS_EX_TRANSPARENT
} else {
exStyle &^= w32.WS_EX_TRANSPARENT
}
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, uint32(exStyle))
}
func (w *windowsWebviewWindow) setPadding(padding edge.Rect) {
// Skip SetPadding if window is being minimized to prevent flickering
if w.isMinimizing {
return
}
w.chromium.SetPadding(padding)
}
func (w *windowsWebviewWindow) showMenuBar() {
if w.menu != nil {
w32.SetMenu(w.hwnd, w.menu.menu)
}
}
func (w *windowsWebviewWindow) hideMenuBar() {
if w.menu != nil {
w32.SetMenu(w.hwnd, 0)
}
}
func (w *windowsWebviewWindow) snapAssist() {
// Simulate Win+Z key combination to trigger Snap Assist
// Press Windows key
w32.KeybdEvent(byte(w32.VK_LWIN), 0, 0, 0)
// Press Z key
w32.KeybdEvent(byte('Z'), 0, 0, 0)
// Release Z key
w32.KeybdEvent(byte('Z'), 0, w32.KEYEVENTF_KEYUP, 0)
// Release Windows key
w32.KeybdEvent(byte(w32.VK_LWIN), 0, w32.KEYEVENTF_KEYUP, 0)
}
func (w *windowsWebviewWindow) setContentProtection(enabled bool) {
// Ensure the option reflects the requested state for future show() calls
w.parent.options.ContentProtectionEnabled = enabled
w.updateContentProtection()
}
func (w *windowsWebviewWindow) updateContentProtection() {
if w.hwnd == 0 {
return
}
if !w.isVisible() {
// Defer updates until the window is visible to avoid affinity glitches.
return
}
desired := w.parent.options.ContentProtectionEnabled
if desired {
if w.applyDisplayAffinity(w32.WDA_EXCLUDEFROMCAPTURE) {
w.contentProtectionApplied = true
}
return
}
if w.applyDisplayAffinity(w32.WDA_NONE) {
w.contentProtectionApplied = false
}
}
func (w *windowsWebviewWindow) applyDisplayAffinity(affinity uint32) bool {
if ok := w32.SetWindowDisplayAffinity(w.hwnd, affinity); !ok {
// Note: wrapper already falls back to WDA_MONITOR on older Windows.
globalApplication.warning("SetWindowDisplayAffinity failed: window=%v, affinity=%v", w.parent.id, affinity)
return false
}
return true
}