From cb28de47f8e0b06e4778c379cdcd130fe334f1b3 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 6 May 2023 15:04:38 +1000 Subject: [PATCH] [v3 windows] Support irregular shaped windows --- v3/STATUS.md | 35 ++++++- v3/pkg/application/image.go | 20 ++++ v3/pkg/application/options_win.go | 3 + v3/pkg/application/webview_window_windows.go | 105 +++++++++++++------ v3/pkg/w32/constants.go | 8 ++ v3/pkg/w32/dwmapi.go | 4 +- v3/pkg/w32/image.go | 53 ++++++++++ v3/pkg/w32/typedef.go | 7 ++ v3/pkg/w32/user32.go | 16 +++ 9 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 v3/pkg/application/image.go create mode 100644 v3/pkg/w32/image.go diff --git a/v3/STATUS.md b/v3/STATUS.md index 0bdd58d3f..ea7331367 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -112,7 +112,7 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |------------|---------|-------|-----|-------| -| GetAll | | | Y | | +| GetAll | Y | | Y | | | GetPrimary | | | Y | | | GetCurrent | | | Y | | @@ -148,12 +148,10 @@ Webview Window Interface Methods | SetZoom | | | Y | Set view scale | | Screen | | | Y | Get screen for window | - ### Window Options -A 'Y' in the table below indicates that the option has been tested and is applied when the window is created. -An 'X' indicates that the option is not supported by the platform. - +A 'Y' in the table below indicates that the option has been tested and is applied when the window is created. +An 'X' indicates that the option is not supported by the platform. | Feature | Windows | Linux | Mac | Notes | |---------------------------------|---------|-------|-----|--------------------------------------------| @@ -288,4 +286,31 @@ Built-in plugin support: - [x] Translucency - [x] Custom Themes +### Windows Options + +| Feature | Default | Notes | +|-----------------------------------|---------|---------------------------------------------| +| BackdropType | | | +| DisableIcon | | | +| Theme | | | +| CustomTheme | | | +| DisableFramelessWindowDecorations | | | +| WindowMask | nil | Makes the window the contents of the bitmap | + + // Select the type of translucent backdrop. Requires Windows 11 22621 or later. + BackdropType BackdropType + // Disable the icon in the titlebar + DisableIcon bool + // Theme. Defaults to SystemDefault which will use whatever the system theme is. The application will follow system theme changes. + Theme Theme + // Custom colours for dark/light mode + CustomTheme *ThemeSettings + + // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. + // "Rounded Corners" are only available on Windows 11. + DisableFramelessWindowDecorations bool + + // WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape. + WindowMask []byte + ## Linux Specific diff --git a/v3/pkg/application/image.go b/v3/pkg/application/image.go new file mode 100644 index 000000000..fd77e06fd --- /dev/null +++ b/v3/pkg/application/image.go @@ -0,0 +1,20 @@ +package application + +import ( + "bytes" + "image" + "image/draw" + "image/png" +) + +func pngToImage(data []byte) (*image.RGBA, error) { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + bounds := img.Bounds() + rgba := image.NewRGBA(bounds) + draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) + return rgba, nil +} diff --git a/v3/pkg/application/options_win.go b/v3/pkg/application/options_win.go index 7e58cf9af..27cb3789b 100644 --- a/v3/pkg/application/options_win.go +++ b/v3/pkg/application/options_win.go @@ -31,6 +31,9 @@ type WindowsWindow struct { // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. // "Rounded Corners" are only available on Windows 11. DisableFramelessWindowDecorations bool + + // WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape. + WindowMask []byte } type Theme int diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 6d51a3f72..eb0b8d7ff 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -5,6 +5,7 @@ package application import ( "errors" "fmt" + "github.com/samber/lo" "strconv" "unicode/utf16" "unsafe" @@ -42,11 +43,13 @@ func (w *windowsWebviewWindow) setSize(width, height int) { } func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { - position := w32.HWND_NOTOPMOST - if alwaysOnTop { - position = w32.HWND_TOPMOST - } - w32.SetWindowPos(w.hwnd, position, 0, 0, 0, 0, uint(w32.SWP_NOMOVE|w32.SWP_NOSIZE)) + 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) { @@ -144,14 +147,6 @@ func (w *windowsWebviewWindow) run() { w.disableIcon() } - switch options.BackgroundType { - case BackgroundTypeSolid: - w.setBackgroundColour(options.BackgroundColour) - case BackgroundTypeTransparent: - case BackgroundTypeTranslucent: - w.setBackdropType(options.Windows.BackdropType) - } - // Process the theme switch options.Windows.Theme { case SystemDefault: @@ -165,6 +160,14 @@ func (w *windowsWebviewWindow) run() { w.updateTheme(true) } + switch options.BackgroundType { + case BackgroundTypeSolid: + w.setBackgroundColour(options.BackgroundColour) + case BackgroundTypeTransparent: + case BackgroundTypeTranslucent: + w.setBackdropType(options.Windows.BackdropType) + } + // Process StartState switch options.StartState { case WindowStateMaximised: @@ -177,6 +180,11 @@ func (w *windowsWebviewWindow) run() { w.fullscreen() } + // Process window mask + if options.Windows.WindowMask != nil { + w.setWindowMask(options.Windows.WindowMask) + } + w.setForeground() if !options.Hidden { @@ -497,22 +505,14 @@ func (w *windowsWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData func (w *windowsWebviewWindow) setStyle(b bool, style int) { currentStyle := int(w32.GetWindowLongPtr(w.hwnd, w32.GWL_STYLE)) if currentStyle != 0 { - if b { - currentStyle |= style - } else { - currentStyle &^= style - } + 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 { - if b { - currentStyle |= style - } else { - currentStyle &^= style - } + currentStyle = lo.Ternary(b, currentStyle|style, currentStyle&^style) w32.SetWindowLongPtr(w.hwnd, w32.GWL_EXSTYLE, uintptr(currentStyle)) } } @@ -529,13 +529,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) { w32.SetWindowCompositionAttribute(w.hwnd, &data) } else { - backdropValue := backdropType - // We default to None, but in w32 None = 1 and Auto = 0 - // So we check if the value given was Auto and set it to 0 - if backdropType == Auto { - backdropValue = None - } - w32.DwmSetWindowAttribute(w.hwnd, w32.DwmwaSystemBackdropType, w32.LPCVOID(&backdropValue), uint32(unsafe.Sizeof(backdropValue))) + w32.DwmSetWindowAttribute(w.hwnd, w32.DwmwaSystemBackdropType, w32.PVOID(&backdropType), unsafe.Sizeof(backdropType)) } } @@ -652,6 +646,13 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp } + if w.parent.options.Windows.WindowMask != nil { + switch msg { + case w32.WM_NCHITTEST: + return w32.HTCAPTION + } + } + if options := w.parent.options; options.Frameless { switch msg { case w32.WM_ACTIVATE: @@ -813,6 +814,50 @@ func (w *windowsWebviewWindow) scaleToDefaultDPI(width, height int) (int, int) { return scaledWidth, scaledHeight } +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 { + panic(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 +} + func ScaleWithDPI(pixels int, dpi uint) int { return (pixels * int(dpi)) / 96 } diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index a06db4fda..cb4cc930f 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -3549,3 +3549,11 @@ const ( SIF_TRACKPOS = 16 SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS ) + +const AC_SRC_OVER = 0 +const AC_SRC_ALPHA = 1 + +const ULW_COLORKEY = 1 +const ULW_ALPHA = 2 +const ULW_OPAQUE = 4 +const ULW_EX_NORESIZE = 8 diff --git a/v3/pkg/w32/dwmapi.go b/v3/pkg/w32/dwmapi.go index 4d3effeb4..b25310db2 100644 --- a/v3/pkg/w32/dwmapi.go +++ b/v3/pkg/w32/dwmapi.go @@ -14,12 +14,12 @@ var ( procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea") ) -func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute LPCVOID, cbAttribute uint32) HRESULT { +func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) HRESULT { ret, _, _ := procDwmSetWindowAttribute.Call( hwnd, uintptr(dwAttribute), uintptr(pvAttribute), - uintptr(cbAttribute)) + cbAttribute) return HRESULT(ret) } diff --git a/v3/pkg/w32/image.go b/v3/pkg/w32/image.go new file mode 100644 index 000000000..4a3036c10 --- /dev/null +++ b/v3/pkg/w32/image.go @@ -0,0 +1,53 @@ +package w32 + +import ( + "image" + "syscall" + "unsafe" +) + +func CreateHBITMAPFromImage(img *image.RGBA) (HBITMAP, error) { + bounds := img.Bounds() + width, height := bounds.Dx(), bounds.Dy() + + // Create a BITMAPINFO structure for the DIB + bmi := BITMAPINFO{ + BmiHeader: BITMAPINFOHEADER{ + BiSize: uint32(unsafe.Sizeof(BITMAPINFOHEADER{})), + BiWidth: int32(width), + BiHeight: int32(-height), // negative to indicate top-down bitmap + BiPlanes: 1, + BiBitCount: 32, + BiCompression: BI_RGB, + BiSizeImage: uint32(width * height * 4), // RGBA = 4 bytes + }, + } + + // Create the DIB section + var bits unsafe.Pointer + + hbmp := CreateDIBSection(0, &bmi, DIB_RGB_COLORS, &bits, 0, 0) + if hbmp == 0 { + return 0, syscall.GetLastError() + } + + // Copy the pixel data from the Go image to the DIB section + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + i := img.PixOffset(x, y) + r := img.Pix[i+0] + g := img.Pix[i+1] + b := img.Pix[i+2] + a := img.Pix[i+3] + + // Write the RGBA pixel data to the DIB section (BGR order) + offset := y*width*4 + x*4 + *((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 0))) = b + *((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 1))) = g + *((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 2))) = r + *((*uint8)(unsafe.Pointer(uintptr(bits) + uintptr(offset) + 3))) = a + } + } + + return hbmp, nil +} diff --git a/v3/pkg/w32/typedef.go b/v3/pkg/w32/typedef.go index 13735204c..bd50b71c5 100644 --- a/v3/pkg/w32/typedef.go +++ b/v3/pkg/w32/typedef.go @@ -459,6 +459,13 @@ type BITMAP struct { BmBits unsafe.Pointer } +type BLENDFUNCTION struct { + BlendOp byte + BlendFlags byte + SourceConstantAlpha byte + AlphaFormat byte +} + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd183567.aspx type DIBSECTION struct { DsBm BITMAP diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 002836c1f..b8f697288 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -139,6 +139,7 @@ var ( procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx") procCallNextHookEx = moduser32.NewProc("CallNextHookEx") procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow") + procUpdateLayeredWindow = moduser32.NewProc("UpdateLayeredWindow") procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") procSetClassLong = moduser32.NewProc("SetClassLongW") @@ -234,6 +235,21 @@ func UpdateWindow(hwnd HWND) bool { return ret != 0 } +func UpdateLayeredWindow(hwnd HWND, hdcDst HDC, pptDst *POINT, psize *SIZE, + hdcSrc HDC, pptSrc *POINT, crKey COLORREF, pblend *BLENDFUNCTION, dwFlags DWORD) bool { + ret, _, _ := procUpdateLayeredWindow.Call( + hwnd, + hdcDst, + uintptr(unsafe.Pointer(pptDst)), + uintptr(unsafe.Pointer(psize)), + hdcSrc, + uintptr(unsafe.Pointer(pptSrc)), + uintptr(crKey), + uintptr(unsafe.Pointer(pblend)), + uintptr(dwFlags)) + return ret != 0 +} + func PostThreadMessage(threadID HANDLE, msg int, wp, lp uintptr) { procPostThreadMessageW.Call(threadID, uintptr(msg), wp, lp) }