mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 22:31:06 +08:00
2100 lines
61 KiB
Go
2100 lines
61 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/samber/lo"
|
|
|
|
"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
|
|
|
|
// 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
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
|
|
menu.Update()
|
|
w.menu = NewApplicationMenu(w, menu)
|
|
w.menu.parentWindow = w
|
|
w32.SetMenu(w.hwnd, w.menu.menu)
|
|
}
|
|
|
|
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) nativeWindowHandle() uintptr {
|
|
return w.hwnd
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) setTitle(title string) {
|
|
w32.SetWindowText(w.hwnd, title)
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
|
|
w32.SetWindowPos(w.hwnd,
|
|
lo.Ternary(alwaysOnTop, w32.HWND_TOPMOST, w32.HWND_NOTOPMOST),
|
|
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) {
|
|
w32.SetBackgroundColour(w.hwnd, color.Red, color.Green, color.Blue)
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) framelessWithDecorations() bool {
|
|
return w.parent.options.Frameless && !w.parent.options.Windows.DisableFramelessWindowDecorations
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) run() {
|
|
|
|
options := w.parent.options
|
|
|
|
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
|
|
|
|
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")
|
|
}
|
|
|
|
// Process ContentProtection
|
|
w.setContentProtection(w.parent.options.ContentProtectionEnabled)
|
|
|
|
// 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:
|
|
w.updateTheme(w32.IsCurrentlyDarkMode())
|
|
w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func(*ApplicationEvent) {
|
|
InvokeAsync(func() {
|
|
w.updateTheme(w32.IsCurrentlyDarkMode())
|
|
})
|
|
})
|
|
case Light:
|
|
w.updateTheme(false)
|
|
case Dark:
|
|
w.updateTheme(true)
|
|
}
|
|
|
|
switch options.BackgroundType {
|
|
case BackgroundTypeSolid:
|
|
var col = options.BackgroundColour
|
|
w.setBackgroundColour(col)
|
|
w.chromium.SetBackgroundColour(col.Red, col.Green, col.Blue, col.Alpha)
|
|
case BackgroundTypeTransparent:
|
|
w.chromium.SetBackgroundColour(0, 0, 0, 0)
|
|
case BackgroundTypeTranslucent:
|
|
w.chromium.SetBackgroundColour(0, 0, 0, 0)
|
|
w.setBackdropType(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
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) physicalBounds() Rect {
|
|
rect := w32.GetWindowRect(w.hwnd)
|
|
// Compensate for invisible borders
|
|
borderSize := w.getBorderSizes()
|
|
return Rect{
|
|
X: int(rect.Left) + borderSize.Left,
|
|
Y: int(rect.Top) + borderSize.Top,
|
|
Width: int(rect.Right-rect.Left) - borderSize.Left - borderSize.Right,
|
|
Height: int(rect.Bottom-rect.Top) - borderSize.Top - borderSize.Bottom,
|
|
}
|
|
}
|
|
|
|
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() {
|
|
if w.isFullscreen() {
|
|
w.unfullscreen()
|
|
}
|
|
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)
|
|
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
|
|
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) setFullscreenButtonEnabled(_ bool) {
|
|
// Unused in Windows
|
|
}
|
|
|
|
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() {
|
|
if w.webviewNavigationCompleted {
|
|
w.chromium.Show()
|
|
w32.ShowWindow(w.hwnd, w32.SW_SHOW)
|
|
}
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) hide() {
|
|
w32.ShowWindow(w.hwnd, w32.SW_HIDE)
|
|
}
|
|
|
|
// 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)),
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) openContextMenu(menu *Menu, _ *ContextMenuData) {
|
|
// Create the menu
|
|
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 {
|
|
currentStyle = lo.Ternary(b, currentStyle|style, 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 {
|
|
currentStyle = lo.Ternary(b, currentStyle|style, 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, int32(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) 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)
|
|
|
|
// Custom theme processing
|
|
customTheme := w.parent.options.Windows.CustomTheme
|
|
// Custom theme
|
|
if w32.SupportsCustomThemes() && customTheme != nil {
|
|
if w.isActive() {
|
|
if isDarkMode {
|
|
w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBar)
|
|
w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleText)
|
|
w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorder)
|
|
} else {
|
|
w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBar)
|
|
w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleText)
|
|
w32.SetBorderColour(w.hwnd, customTheme.LightModeBorder)
|
|
}
|
|
} else {
|
|
if isDarkMode {
|
|
w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBarInactive)
|
|
w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleTextInactive)
|
|
w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorderInactive)
|
|
} else {
|
|
w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBarInactive)
|
|
w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleTextInactive)
|
|
w32.SetBorderColour(w.hwnd, customTheme.LightModeBorderInactive)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *windowsWebviewWindow) isActive() bool {
|
|
return w32.GetForegroundWindow() == w.hwnd
|
|
}
|
|
|
|
var resizePending int32
|
|
|
|
func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
|
|
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 w.parent.unconditionallyClose == false {
|
|
// 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)
|
|
} 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
|
|
// 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: %v", 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:
|
|
w.parent.emit(events.Windows.WindowMaximise)
|
|
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 workaround 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 loosing 1px of the WebView content.
|
|
// Increasing the bottom also worksaround the flickering but we would loose 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) {
|
|
// We send all messages to the centralised window message buffer
|
|
windowMessageBuffer <- &windowMessage{
|
|
windowId: w.parent.id,
|
|
message: message,
|
|
}
|
|
}
|
|
|
|
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 = lo.Uniq(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 = lo.Uniq(opts.EnabledFeatures)
|
|
arg := fmt.Sprintf("--enable-features=%s", strings.Join(opts.EnabledFeatures, ","))
|
|
chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, arg)
|
|
}
|
|
|
|
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)
|
|
|
|
if chromium.HasCapability(edge.SwipeNavigation) {
|
|
err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures)
|
|
if err != nil {
|
|
globalApplication.handleFatalError(err)
|
|
}
|
|
}
|
|
|
|
if chromium.HasCapability(edge.AllowExternalDrop) {
|
|
err := chromium.AllowExternalDrag(false)
|
|
if err != nil {
|
|
globalApplication.handleFatalError(err)
|
|
}
|
|
}
|
|
if w.parent.options.EnableDragAndDrop {
|
|
w.dropTarget = w32.NewDropTarget()
|
|
w.dropTarget.OnDrop = func(files []string) {
|
|
w.parent.emit(events.Windows.WindowDragDrop)
|
|
windowDragAndDropBuffer <- &dragAndDropMessage{
|
|
windowId: windowID,
|
|
filenames: files,
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// We will get round to this
|
|
//if chromium.HasCapability(edge.AllowExternalDrop) {
|
|
// err := chromium.AllowExternalDrag(w.parent.options.EnableDragAndDrop)
|
|
// if err != nil {
|
|
// globalApplication.handleFatalError(err)
|
|
// }
|
|
// if w.parent.options.EnableDragAndDrop {
|
|
// chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects
|
|
// }
|
|
//}
|
|
|
|
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())
|
|
|
|
// 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
|
|
|
|
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()
|
|
}
|
|
if !w.parent.options.Hidden {
|
|
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)
|
|
}
|
|
|
|
addDragAndDropMessage(w.parent.id, filenames)
|
|
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) setContentProtection(enabled bool) {
|
|
var affinity uint32 = w32.WDA_EXCLUDEFROMCAPTURE
|
|
if !enabled {
|
|
affinity = w32.WDA_NONE
|
|
}
|
|
w32.SetWindowDisplayAffinity(w.hwnd, affinity)
|
|
}
|