diff --git a/v3/examples/window/main.go b/v3/examples/window/main.go index d40a71cfa03..7c47aef3fe3 100644 --- a/v3/examples/window/main.go +++ b/v3/examples/window/main.go @@ -701,6 +701,14 @@ func main() { w.OpenDevTools() }) }) + + if runtime.GOOS == "windows" { + stateMenu.Add("Show SnapAssist").OnClick(func(ctx *application.Context) { + currentWindow(func(w *application.WebviewWindow) { + w.ShowSnapAssist() + }) + }) + } if runtime.GOOS != "darwin" { stateMenu.Add("Flash for 5s").OnClick(func(ctx *application.Context) { diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 67d1250740c..3fad0e6ac6f 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -107,6 +107,7 @@ type ( selectAll() redo() showMenuBar() + showSnapAssist() hideMenuBar() toggleMenuBar() setMenu(menu *Menu) @@ -458,6 +459,7 @@ func (w *WebviewWindow) Show() Window { return w } + // Hide hides the window. func (w *WebviewWindow) Hide() Window { w.options.Hidden = true @@ -1413,6 +1415,15 @@ func (w *WebviewWindow) ShowMenuBar() { InvokeSync(w.impl.showMenuBar) } +// ShowSnapAssist shows the Windows SnapAssist overlay for this window. +// This is a Windows-only feature and does nothing on other platforms. +func (w *WebviewWindow) ShowSnapAssist() { + if w.impl == nil || w.isDestroyed() { + return + } + InvokeSync(w.impl.showSnapAssist) +} + // HideMenuBar hides the menu bar for the window. func (w *WebviewWindow) HideMenuBar() { if w.impl == nil || w.isDestroyed() { diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index eb3f8376969..bf966491d96 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -1435,6 +1435,7 @@ func (w *macosWebviewWindow) redo() { } func (w *macosWebviewWindow) showMenuBar() {} +func (w *macosWebviewWindow) showSnapAssist() {} func (w *macosWebviewWindow) hideMenuBar() {} func (w *macosWebviewWindow) toggleMenuBar() {} func (w *macosWebviewWindow) setMenu(_ *Menu) {} diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index acf8a47c9b8..6f89b582d79 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -425,5 +425,6 @@ func (w *linuxWebviewWindow) hide() { } func (w *linuxWebviewWindow) showMenuBar() {} +func (w *linuxWebviewWindow) showSnapAssist() {} func (w *linuxWebviewWindow) hideMenuBar() {} func (w *linuxWebviewWindow) toggleMenuBar() {} diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index a3969694323..3653632327d 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -2276,8 +2276,73 @@ func (w *windowsWebviewWindow) showMenuBar() { } } +func (w *windowsWebviewWindow) showSnapAssist() { + // Ensure this window is focused first + w32.SetForegroundWindow(w.hwnd) + + // Send Win+Z key combination to trigger SnapAssist + w.sendKeyCombo(0x5A, 0x5B) // Z key with Windows key +} + func (w *windowsWebviewWindow) hideMenuBar() { if w.menu != nil { w32.SetMenu(w.hwnd, 0) } } + +// sendKeyCombo sends a key combination (key + modifier) +func (w *windowsWebviewWindow) sendKeyCombo(key, modifier uint16) { + // Create input array for key combination + inputs := make([]w32.INPUT, 4) + + // Modifier key down (Windows key) + inputs[0] = w32.INPUT{ + Type: 1, // INPUT_KEYBOARD + Ki: w32.KEYBDINPUT{ + WVk: modifier, + WScan: 0, + DwFlags: 0, + Time: 0, + DwExtraInfo: 0, + }, + } + + // Main key down (Z key) + inputs[1] = w32.INPUT{ + Type: 1, // INPUT_KEYBOARD + Ki: w32.KEYBDINPUT{ + WVk: key, + WScan: 0, + DwFlags: 0, + Time: 0, + DwExtraInfo: 0, + }, + } + + // Main key up (Z key) + inputs[2] = w32.INPUT{ + Type: 1, // INPUT_KEYBOARD + Ki: w32.KEYBDINPUT{ + WVk: key, + WScan: 0, + DwFlags: 0x0002, // KEYEVENTF_KEYUP + Time: 0, + DwExtraInfo: 0, + }, + } + + // Modifier key up (Windows key) + inputs[3] = w32.INPUT{ + Type: 1, // INPUT_KEYBOARD + Ki: w32.KEYBDINPUT{ + WVk: modifier, + WScan: 0, + DwFlags: 0x0002, // KEYEVENTF_KEYUP + Time: 0, + DwExtraInfo: 0, + }, + } + + // Send the input events + w32.SendInput(len(inputs), unsafe.Pointer(&inputs[0]), int(unsafe.Sizeof(w32.INPUT{}))) +} diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index 58b4c4bf52b..03a8df80a06 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -71,6 +71,7 @@ type Window interface { SetZoom(magnification float64) Window Show() Window ShowMenuBar() + ShowSnapAssist() Size() (width int, height int) OpenDevTools() ToggleFullscreen() diff --git a/v3/pkg/application/window_manager.go b/v3/pkg/application/window_manager.go index ec6a3298a08..f972378bb9a 100644 --- a/v3/pkg/application/window_manager.go +++ b/v3/pkg/application/window_manager.go @@ -133,3 +133,4 @@ func (wm *WindowManager) GetAll() []Window { } return windows } + diff --git a/v3/pkg/application/window_manager_darwin.go b/v3/pkg/application/window_manager_darwin.go new file mode 100644 index 00000000000..785eb1163d8 --- /dev/null +++ b/v3/pkg/application/window_manager_darwin.go @@ -0,0 +1,8 @@ +//go:build darwin + +package application + +// showSnapAssist is a no-op on macOS as SnapAssist is a Windows-only feature +func showSnapAssist(window *WebviewWindow) { + // SnapAssist is not available on macOS +} \ No newline at end of file diff --git a/v3/pkg/application/window_manager_linux.go b/v3/pkg/application/window_manager_linux.go new file mode 100644 index 00000000000..c172d51a169 --- /dev/null +++ b/v3/pkg/application/window_manager_linux.go @@ -0,0 +1,8 @@ +//go:build linux + +package application + +// showSnapAssist is a no-op on Linux as SnapAssist is a Windows-only feature +func showSnapAssist(window *WebviewWindow) { + // SnapAssist is not available on Linux +} \ No newline at end of file diff --git a/v3/pkg/application/window_manager_windows.go b/v3/pkg/application/window_manager_windows.go new file mode 100644 index 00000000000..16898ab292c --- /dev/null +++ b/v3/pkg/application/window_manager_windows.go @@ -0,0 +1,95 @@ +//go:build windows + +package application + +import ( + "time" + "unsafe" + + "github.com/wailsapp/wails/v3/pkg/w32" +) + +// showSnapAssist sends Win+Z key combination to trigger SnapAssist for the specified window +func showSnapAssist(window *WebviewWindow) { + // First, ensure the window is visible and focused to target SnapAssist correctly + window.Show() // Ensure window is visible + window.Restore() // Restore if minimized + + // Get the native window handle and set it as foreground + if hwnd := w32.HWND(window.impl.nativeWindowHandle()); hwnd != 0 { + w32.SetForegroundWindow(hwnd) + // Small delay to ensure window focus is established + time.Sleep(50 * time.Millisecond) + } + + // Send Win+Z key combination using a robotgo-style approach + sendKeyCombo(w32.VK_Z, w32.VK_LWIN) +} + +// sendKeyCombo sends a key combination (key + modifiers) +func sendKeyCombo(key uint16, modifiers ...uint16) { + // Calculate total inputs needed (press and release for each key) + numKeys := len(modifiers) + 1 + inputs := make([]w32.INPUT, numKeys*2) + + inputIndex := 0 + + // Press all modifier keys first + for _, modifier := range modifiers { + inputs[inputIndex] = w32.INPUT{ + Type: w32.INPUT_KEYBOARD, + Ki: w32.KEYBDINPUT{ + WVk: modifier, + WScan: 0, + DwFlags: 0, + Time: 0, + DwExtraInfo: 0, + }, + } + inputIndex++ + } + + // Press the main key + inputs[inputIndex] = w32.INPUT{ + Type: w32.INPUT_KEYBOARD, + Ki: w32.KEYBDINPUT{ + WVk: key, + WScan: 0, + DwFlags: 0, + Time: 0, + DwExtraInfo: 0, + }, + } + inputIndex++ + + // Release the main key + inputs[inputIndex] = w32.INPUT{ + Type: w32.INPUT_KEYBOARD, + Ki: w32.KEYBDINPUT{ + WVk: key, + WScan: 0, + DwFlags: w32.KEYEVENTF_KEYUP, + Time: 0, + DwExtraInfo: 0, + }, + } + inputIndex++ + + // Release all modifier keys in reverse order + for i := len(modifiers) - 1; i >= 0; i-- { + inputs[inputIndex] = w32.INPUT{ + Type: w32.INPUT_KEYBOARD, + Ki: w32.KEYBDINPUT{ + WVk: modifiers[i], + WScan: 0, + DwFlags: w32.KEYEVENTF_KEYUP, + Time: 0, + DwExtraInfo: 0, + }, + } + inputIndex++ + } + + // Send all input events + w32.SendInput(len(inputs), unsafe.Pointer(&inputs[0]), int(unsafe.Sizeof(w32.INPUT{}))) +} \ No newline at end of file diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index 83ed4b9d1fc..b9c3a89e2cf 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -1994,6 +1994,7 @@ const ( VK_INSERT = 0x2D VK_DELETE = 0x2E VK_HELP = 0x2F + VK_Z = 0x5A VK_LWIN = 0x5B VK_RWIN = 0x5C VK_APPS = 0x5D @@ -2900,6 +2901,10 @@ const ( INPUT_HARDWARE = 2 ) +const ( + KEYEVENTF_KEYUP = 0x0002 +) + const ( MOUSEEVENTF_ABSOLUTE = 0x8000 MOUSEEVENTF_HWHEEL = 0x01000 @@ -3555,6 +3560,7 @@ const ( VK_INSERT = 45 VK_DELETE = 46 VK_HELP = 47 + VK_Z = 0x5A VK_LWIN = 0x5B VK_RWIN = 0x5C VK_APPS = 0x5D diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 8988b786e62..e35bd38027b 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -1333,34 +1333,14 @@ func ChangeDisplaySettingsEx(szDeviceName *uint16, devMode *DEVMODE, hwnd HWND, return int32(ret) } -/* -func SendInput(inputs []INPUT) uint32 { - var validInputs []C.INPUT - - for _, oneInput := range inputs { - input := C.INPUT{_type: C.DWORD(oneInput.Type)} - - switch oneInput.Type { - case INPUT_MOUSE: - (*MouseInput)(unsafe.Pointer(&input)).mi = oneInput.Mi - case INPUT_KEYBOARD: - (*KbdInput)(unsafe.Pointer(&input)).ki = oneInput.Ki - case INPUT_HARDWARE: - (*HardwareInput)(unsafe.Pointer(&input)).hi = oneInput.Hi - default: - panic("unkown type") - } - - validInputs = append(validInputs, input) - } - +func SendInput(nInputs int, pInputs unsafe.Pointer, cbSize int) uint32 { ret, _, _ := procSendInput.Call( - uintptr(len(validInputs)), - uintptr(unsafe.Pointer(&validInputs[0])), - uintptr(unsafe.Sizeof(C.INPUT{})), + uintptr(nInputs), + uintptr(pInputs), + uintptr(cbSize), ) return uint32(ret) -}*/ +} func SetWindowsHookEx(idHook int, lpfn HOOKPROC, hMod HINSTANCE, dwThreadId DWORD) HHOOK { ret, _, _ := procSetWindowsHookEx.Call(