From 6e92a4f71e57f9221f99feb8f12e5b0048a51c60 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 1 May 2023 10:52:46 +1000 Subject: [PATCH] [v3 Windows] Support setMin/MaxSize, setPosition --- v3/STATUS.md | 97 +++++++------- v3/pkg/application/mainthread_windows.go | 5 +- v3/pkg/application/messageprocessor_window.go | 2 +- v3/pkg/application/webview_window_windows.go | 102 ++++++++++++--- v3/pkg/w32/screen.go | 118 ++++++++++++++++++ v3/pkg/w32/window.go | 24 ++-- 6 files changed, 263 insertions(+), 85 deletions(-) create mode 100644 v3/pkg/w32/screen.go diff --git a/v3/STATUS.md b/v3/STATUS.md index e84e0e58a..c7c6b40d1 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -26,55 +26,54 @@ Application interface methods Webview Window Interface Methods -| Method | Windows | Linux | Mac | Notes | -|----------------------------------------------------|---------|-------|-----|-------| -| setTitle(title string) | Y | | Y | | -| setSize(width, height int) | Y | | Y | | -| setAlwaysOnTop(alwaysOnTop bool) | Y | | Y | | -| setURL(url string) | | | Y | | -| setResizable(resizable bool) | Y | | Y | | -| setMinSize(width, height int) | | | Y | | -| setMaxSize(width, height int) | | | Y | | -| execJS(js string) | | | Y | | -| restore() | | | Y | | -| setBackgroundColour(color RGBA) | Y | | Y | | -| run() | Y | | Y | | -| center() | Y | | Y | | -| size() (int, int) | | | Y | | -| width() int | Y | | Y | | -| height() int | Y | | Y | | -| position() (int, int) | Y | | Y | | -| destroy() | | | Y | | -| reload() | | | Y | | -| forceReload() | | | Y | | -| toggleDevTools() | | | Y | | -| zoomReset() | | | Y | | -| zoomIn() | | | Y | | -| zoomOut() | | | Y | | -| getZoom() float64 | | | Y | | -| setZoom(zoom float64) | | | Y | | -| close() | | | Y | | -| zoom() | | | Y | | -| setHTML(html string) | | | Y | | -| setPosition(x int, y int) | | | Y | | -| on(eventID uint) | | | Y | | -| minimise() | Y | | Y | | -| unminimise() | Y | | Y | | -| maximise() | Y | | Y | | -| unmaximise() | Y | | Y | | -| fullscreen() | | | Y | | -| unfullscreen() | | | Y | | -| isMinimised() bool | Y | | Y | | -| isMaximised() bool | Y | | Y | | -| isFullscreen() bool | | | Y | | -| disableSizeConstraints() | | | Y | | -| setFullscreenButtonEnabled(enabled bool) | | | Y | | -| show() | Y | | Y | | -| hide() | Y | | Y | | -| getScreen() (*Screen, error) | | | Y | | -| setFrameless(bool) | | | Y | | -| openContextMenu(menu *Menu, data *ContextMenuData) | | | Y | | -| nativeWindowHandle() (uintptr, error) | Y | | | | +| Method | Windows | Linux | Mac | Notes | +|----------------------------------------------------|---------|-------|-----|------------------------------------------| +| center() | Y | | Y | | +| close() | | | Y | | +| destroy() | | | Y | | +| disableSizeConstraints() | | | Y | | +| execJS(js string) | | | Y | | +| forceReload() | | | Y | | +| fullscreen() | | | Y | | +| getScreen() (*Screen, error) | | | Y | | +| getZoom() float64 | | | Y | | +| height() int | Y | | Y | | +| hide() | Y | | Y | | +| isFullscreen() bool | | | Y | | +| isMaximised() bool | Y | | Y | | +| isMinimised() bool | Y | | Y | | +| maximise() | Y | | Y | | +| minimise() | Y | | Y | | +| nativeWindowHandle() (uintptr, error) | Y | | | | +| on(eventID uint) | | | Y | | +| openContextMenu(menu *Menu, data *ContextMenuData) | | | Y | | +| position() (int, int) | Y | | Y | | +| reload() | | | Y | | +| run() | Y | | Y | | +| setAlwaysOnTop(alwaysOnTop bool) | Y | | Y | | +| setBackgroundColour(color RGBA) | Y | | Y | | +| setFrameless(bool) | | | Y | | +| setFullscreenButtonEnabled(enabled bool) | X | | Y | There is no fullscreen button in Windows | +| setHTML(html string) | | | Y | | +| setMaxSize(width, height int) | Y | | Y | | +| setMinSize(width, height int) | Y | | Y | | +| setPosition(x int, y int) | Y | | Y | | +| setResizable(resizable bool) | Y | | Y | | +| setSize(width, height int) | Y | | Y | | +| setTitle(title string) | Y | | Y | | +| setURL(url string) | | | Y | | +| setZoom(zoom float64) | | | Y | | +| show() | Y | | Y | | +| size() (int, int) | | | Y | | +| toggleDevTools() | | | Y | | +| unfullscreen() | | | Y | | +| unmaximise() | Y | | Y | | +| unminimise() | Y | | Y | | +| width() int | Y | | Y | | +| zoom() | | | Y | | +| zoomIn() | | | Y | | +| zoomOut() | | | Y | | +| zoomReset() | | | Y | | ## Runtime diff --git a/v3/pkg/application/mainthread_windows.go b/v3/pkg/application/mainthread_windows.go index 80967ccab..f2f03454c 100644 --- a/v3/pkg/application/mainthread_windows.go +++ b/v3/pkg/application/mainthread_windows.go @@ -6,10 +6,7 @@ import ( "github.com/wailsapp/wails/v3/pkg/w32" "runtime" "sort" - "syscall" "unsafe" - - "github.com/samber/lo" ) var ( @@ -37,7 +34,7 @@ func (m *windowsApp) initMainLoop() { m.mainThreadWindowHWND = w32.CreateWindowEx( 0, windowClassName, - lo.Must(syscall.UTF16PtrFromString("__wails_hidden_mainthread")), + w32.MustStringToUTF16Ptr("__wails_hidden_mainthread"), w32.WS_DISABLED, w32.CW_USEDEFAULT, w32.CW_USEDEFAULT, diff --git a/v3/pkg/application/messageprocessor_window.go b/v3/pkg/application/messageprocessor_window.go index ab4507073..7789cad7c 100644 --- a/v3/pkg/application/messageprocessor_window.go +++ b/v3/pkg/application/messageprocessor_window.go @@ -46,7 +46,7 @@ func (m *MessageProcessor) processWindowMethod(method string, rw http.ResponseWr window.UnFullscreen() m.ok(rw) case "Minimise": - window.Minimize() + window.Minimise() m.ok(rw) case "UnMinimise": window.UnMinimise() diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index f86bbdd06..7e0bc76e6 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -7,10 +7,7 @@ import ( "fmt" "github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/w32" - "syscall" "unsafe" - - "github.com/samber/lo" ) var showDevTools = func(window unsafe.Pointer) {} @@ -19,6 +16,10 @@ type windowsWebviewWindow struct { windowImpl unsafe.Pointer parent *WebviewWindow hwnd w32.HWND + + // Size Restrictions + minWidth, minHeight int + maxWidth, maxHeight int } func (w *windowsWebviewWindow) nativeWindowHandle() uintptr { @@ -57,13 +58,13 @@ func (w *windowsWebviewWindow) setResizable(resizable bool) { } func (w *windowsWebviewWindow) setMinSize(width, height int) { - //TODO implement me - panic("implement me") + w.minWidth = width + w.minHeight = height } func (w *windowsWebviewWindow) setMaxSize(width, height int) { - //TODO implement me - panic("implement me") + w.maxWidth = width + w.maxHeight = height } func (w *windowsWebviewWindow) execJS(js string) { @@ -80,8 +81,15 @@ func (w *windowsWebviewWindow) run() { } func (w *windowsWebviewWindow) _run() { - var exStyle uint + + // Copy options options := w.parent.options + w.minWidth = options.MinWidth + w.minHeight = options.MinHeight + w.maxWidth = options.MaxWidth + w.maxHeight = options.MaxHeight + + var exStyle uint exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW if options.BackgroundType != BackgroundTypeSolid { exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP @@ -92,7 +100,7 @@ func (w *windowsWebviewWindow) _run() { w.hwnd = w32.CreateWindowEx( exStyle, windowClassName, - lo.Must(syscall.UTF16PtrFromString(options.Title)), + w32.MustStringToUTF16Ptr(options.Title), w32.WS_OVERLAPPEDWINDOW, w32.CW_USEDEFAULT, w32.CW_USEDEFAULT, @@ -139,7 +147,6 @@ func (w *windowsWebviewWindow) _run() { switch options.Windows.Theme { case SystemDefault: w.updateTheme(w32.IsCurrentlyDarkMode()) - // Setup a listener to respond to theme changes w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func() { w.updateTheme(w32.IsCurrentlyDarkMode()) }) @@ -248,8 +255,9 @@ func (w *windowsWebviewWindow) setHTML(html string) { } func (w *windowsWebviewWindow) setPosition(x int, y int) { - //TODO implement me - panic("implement me") + info := w32.GetMonitorInfoForWindow(w.hwnd) + workRect := info.RcWork + w32.SetWindowPos(w.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE) } func (w *windowsWebviewWindow) on(eventID uint) { @@ -476,9 +484,75 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp windowsApp := globalApplication.impl.(*windowsApp) windowsApp.unregisterWindow(w) return 0 - default: - return w32.DefWindowProc(w.hwnd, msg, wparam, lparam) + case w32.WM_GETMINMAXINFO: + mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam)) + hasConstraints := false + if w.minWidth > 0 || w.minHeight > 0 { + hasConstraints = true + + width, height := w.scaleWithWindowDPI(w.minWidth, w.minHeight) + if width > 0 { + mmi.PtMinTrackSize.X = int32(width) + } + if height > 0 { + mmi.PtMinTrackSize.Y = int32(height) + } + } + if w.maxWidth > 0 || w.maxHeight > 0 { + hasConstraints = true + + width, height := w.scaleWithWindowDPI(w.maxWidth, w.maxHeight) + if width > 0 { + mmi.PtMaxTrackSize.X = int32(width) + } + if height > 0 { + mmi.PtMaxTrackSize.Y = int32(height) + } + } + if hasConstraints { + 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 accureate + // 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 NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) { diff --git a/v3/pkg/w32/screen.go b/v3/pkg/w32/screen.go new file mode 100644 index 000000000..7f43beb2b --- /dev/null +++ b/v3/pkg/w32/screen.go @@ -0,0 +1,118 @@ +//go:build windows + +package w32 + +import ( + "fmt" + "syscall" + "unsafe" +) + +func MonitorsEqual(first MONITORINFO, second MONITORINFO) bool { + // Checks to make sure all the fields are the same. + // A cleaner way would be to check identity of devices. but I couldn't find a way of doing that using the win32 API + return first.DwFlags == second.DwFlags && + first.RcMonitor.Top == second.RcMonitor.Top && + first.RcMonitor.Bottom == second.RcMonitor.Bottom && + first.RcMonitor.Right == second.RcMonitor.Right && + first.RcMonitor.Left == second.RcMonitor.Left && + first.RcWork.Top == second.RcWork.Top && + first.RcWork.Bottom == second.RcWork.Bottom && + first.RcWork.Right == second.RcWork.Right && + first.RcWork.Left == second.RcWork.Left +} + +func GetMonitorInformation(hMonitor HMONITOR) (*MONITORINFO, error) { + // Adapted from winc.utils.getMonitorInfo + // See docs for + //https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa + + var info MONITORINFO + info.CbSize = uint32(unsafe.Sizeof(info)) + succeeded := GetMonitorInfo(hMonitor, &info) + if !succeeded { + return &info, fmt.Errorf("Windows call to getMonitorInfo failed") + } + return &info, nil +} + +type Screen struct { + IsCurrent bool + IsPrimary bool + Width int + Height int +} + +func EnumProc(hMonitor HMONITOR, hdcMonitor HDC, lprcMonitor *RECT, screenContainer *ScreenContainer) uintptr { + // adapted from https://stackoverflow.com/a/23492886/4188138 + + // see docs for the following pages to better understand this function + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow + + ourMonitorData := Screen{} + currentMonHndl := MonitorFromWindow(screenContainer.mainWinHandle, MONITOR_DEFAULTTONEAREST) + currentMonInfo, currErr := GetMonitorInformation(currentMonHndl) + + if currErr != nil { + screenContainer.errors = append(screenContainer.errors, currErr) + screenContainer.monitors = append(screenContainer.monitors, Screen{}) + // not sure what the consequences of returning false are, so let's just return true and handle it ourselves + return TRUE + } + + monInfo, err := GetMonitorInformation(hMonitor) + if err != nil { + screenContainer.errors = append(screenContainer.errors, err) + screenContainer.monitors = append(screenContainer.monitors, Screen{}) + return TRUE + } + + height := lprcMonitor.Right - lprcMonitor.Left + width := lprcMonitor.Bottom - lprcMonitor.Top + ourMonitorData.IsPrimary = monInfo.DwFlags&MONITORINFOF_PRIMARY == 1 + ourMonitorData.Height = int(width) + ourMonitorData.Width = int(height) + ourMonitorData.IsCurrent = MonitorsEqual(*currentMonInfo, *monInfo) + + // the reason we need a container is that we have don't know how many times this function will be called + // this "append" call could potentially do an allocation and rewrite the pointer to monitors. So we save the pointer in screenContainer.monitors + // and retrieve the values after all EnumProc calls + // If EnumProc is multi-threaded, this could be problematic. Although, I don't think it is. + screenContainer.monitors = append(screenContainer.monitors, ourMonitorData) + // let's keep screenContainer.errors the same size as screenContainer.monitors in case we want to match them up later if necessary + screenContainer.errors = append(screenContainer.errors, nil) + return TRUE +} + +type ScreenContainer struct { + monitors []Screen + errors []error + mainWinHandle HWND +} + +func GetAllScreens(mainWinHandle HWND) ([]Screen, error) { + // TODO fix hack of container sharing by having a proper data sharing mechanism between windows and the runtime + monitorContainer := ScreenContainer{mainWinHandle: mainWinHandle} + returnErr := error(nil) + var errorStrings []string + + dc := GetDC(0) + defer ReleaseDC(0, dc) + succeeded := EnumDisplayMonitors(dc, nil, syscall.NewCallback(EnumProc), unsafe.Pointer(&monitorContainer)) + if !succeeded { + return monitorContainer.monitors, fmt.Errorf("Windows call to EnumDisplayMonitors failed") + } + for idx, err := range monitorContainer.errors { + if err != nil { + errorStrings = append(errorStrings, fmt.Sprintf("Error from monitor #%v, %v", idx+1, err)) + } + } + + if len(errorStrings) > 0 { + returnErr = fmt.Errorf("%v errors encountered: %v", len(errorStrings), errorStrings) + } + return monitorContainer.monitors, returnErr +} diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go index 22cdb0411..3f626228b 100644 --- a/v3/pkg/w32/window.go +++ b/v3/pkg/w32/window.go @@ -2,6 +2,7 @@ package w32 import ( "fmt" + "github.com/samber/lo" "log" "strconv" "syscall" @@ -113,34 +114,23 @@ func showWindow(hwnd uintptr, cmdshow int) bool { } func MustStringToUTF16Ptr(input string) *uint16 { - ret, err := syscall.UTF16PtrFromString(input) - if err != nil { - panic(err) - } - return ret + return lo.Must(syscall.UTF16PtrFromString(input)) } func MustStringToUTF16uintptr(input string) uintptr { - ret, err := syscall.UTF16PtrFromString(input) - if err != nil { - panic(err) - } + ret := lo.Must(syscall.UTF16PtrFromString(input)) return uintptr(unsafe.Pointer(ret)) } -func MustUTF16FromString(input string) []uint16 { - ret, err := syscall.UTF16FromString(input) - if err != nil { - panic(err) - } - return ret +func MustStringToUTF16(input string) []uint16 { + return lo.Must(syscall.UTF16FromString(input)) } func CenterWindow(hwnd HWND) { windowInfo := getWindowInfo(hwnd) frameless := windowInfo.IsPopup() - info := getMonitorInfo(hwnd) + info := GetMonitorInfoForWindow(hwnd) workRect := info.RcWork screenMiddleW := workRect.Left + (workRect.Right-workRect.Left)/2 screenMiddleH := workRect.Top + (workRect.Bottom-workRect.Top)/2 @@ -164,7 +154,7 @@ func getWindowInfo(hwnd HWND) *WINDOWINFO { return &info } -func getMonitorInfo(hwnd HWND) *MONITORINFO { +func GetMonitorInfoForWindow(hwnd HWND) *MONITORINFO { currentMonitor := MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) var info MONITORINFO info.CbSize = uint32(unsafe.Sizeof(info))