diff --git a/docs/src/assets/windows-native-non-client-regions/wails-app-region.mp4 b/docs/src/assets/windows-native-non-client-regions/wails-app-region.mp4 new file mode 100644 index 00000000000..c399b404ea4 Binary files /dev/null and b/docs/src/assets/windows-native-non-client-regions/wails-app-region.mp4 differ diff --git a/docs/src/content/docs/features/windows/basics.mdx b/docs/src/content/docs/features/windows/basics.mdx index 55a3f623096..0d5bd935f2d 100644 --- a/docs/src/content/docs/features/windows/basics.mdx +++ b/docs/src/content/docs/features/windows/basics.mdx @@ -403,7 +403,7 @@ mainWindow.AttachModal(childWindow) There is no per-window `SetIcon` — the application icon is set on the app via `app.SetIcon([]byte)` (or for a Linux-specific window icon, the `application.LinuxWindow.Icon` field at window creation). **Snap Assist:** - Shows Windows 11 snap layout options for the window. + Shows Windows 11 snap layout options through the system shortcut path. For a custom HTML maximize button with native hover Snap Layouts, use [Native Non-Client Regions on Windows](/features/windows/frameless#native-non-client-regions-on-windows) instead. **Taskbar flashing:** Useful for notifications when window is minimised. diff --git a/docs/src/content/docs/features/windows/frameless.mdx b/docs/src/content/docs/features/windows/frameless.mdx index 0a22a7b763a..1da696e330f 100644 --- a/docs/src/content/docs/features/windows/frameless.mdx +++ b/docs/src/content/docs/features/windows/frameless.mdx @@ -6,6 +6,7 @@ sidebar: --- import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components"; +import wailsAppRegionVideo from "../../../../assets/windows-native-non-client-regions/wails-app-region.mp4"; ## Frameless Windows @@ -173,6 +174,158 @@ document.querySelector('.maximize').addEventListener('click', () => Window.Maxim document.querySelector('.close').addEventListener('click', () => Window.Close()) ``` +## Native Non-Client Regions on Windows + +Windows can treat parts of a custom title bar as native non-client areas. This lets you draw the title bar and caption buttons with any HTML/CSS design while keeping native Windows behavior: the caption area drags the window, the maximize button can show Windows 11 Snap Assist / Snap Layouts, and the minimize, maximize, and close buttons receive native hit testing and mouse state. + +The video below shows a custom HTML/CSS title bar using native Windows hit testing, including Windows 11 Snap Assist / Snap Layouts on a custom maximize button. + + + +Wails supports two Windows-specific mechanisms: + +- `app-region` through WebView2's native non-client region support +- `--wails-app-region` through Wails runtime tracking for custom caption buttons + +### Choosing a Mode + +:::caution[Experimental] +`WebView2CompositionHosting` changes how the window hosts and interacts with WebView2 under the hood. Instead of the default HWND-hosted WebView2 controller, Wails uses composition controller hosting and forwards input explicitly. This mode may have rendering, input, focus, or WebView2 Runtime compatibility issues. Enable it only when you need native custom caption-button behavior, and test your app carefully on the Windows and WebView2 Runtime versions you support. +::: + +Choose based on what you need from Windows: + +- Use `NonClientRegionSupport` for simple native app dragging with WebView2's `app-region: drag` and `app-region: no-drag`. +- Use `WebView2CompositionHosting` when your custom minimize, maximize, and close buttons should behave like native Windows caption buttons. +- Enable both when the same window needs WebView2-native `app-region` support and Wails-managed custom caption-button regions. + +`NonClientRegionSupport` is the lightweight native alternative to Wails' `--wails-draggable` tracking. You mark draggable and non-draggable areas with CSS, WebView2 decides which pixels belong to the caption, and Wails asks WebView2 for the native region when hit testing. + +That is the full scope of this mode today. It does not make custom minimize, maximize, or close buttons behave like native Windows caption buttons, and it does not enable Windows 11 Snap Assist / Snap Layouts for a custom maximize button. Use it when you need simple native app dragging without the extra machinery of `--wails-draggable`. + +`WebView2CompositionHosting` is for custom caption buttons with native behavior. Wails tracks DOM rectangles marked with `--wails-app-region`, maps them to Windows hit-test values such as `HTMINBUTTON`, `HTMAXBUTTON`, and `HTCLOSE`, and forwards mouse input back into the composition-hosted WebView2 surface. That is what allows a custom maximize button to participate in Windows 11 Snap Assist / Snap Layouts while keeping any visual design you choose. + +Put another way: `NonClientRegionSupport` is WebView2-native CSS region support. `WebView2CompositionHosting` is Wails taking responsibility for host-owned composition and custom non-client hit testing. + +### WebView2 app-region + +Enable WebView2's native non-client region support for the window: + +```go +window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Frameless: true, + Windows: application.WindowsWindow{ + NonClientRegionSupport: true, + }, +}) +``` + +Then mark draggable areas with the CSS `app-region` property: + +```css +.titlebar { + app-region: drag; +} + +.titlebar button, +.titlebar input, +.titlebar select, +.titlebar textarea { + app-region: no-drag; +} +``` + +Use this when you only need native caption dragging and your title bar controls are handled with normal frontend clicks. + +The limitation of this mode is that it is constrained by WebView2's own non-client region support. In current WebView2 releases, that means drag and no-drag regions only. It is not intended to model fully custom frontend caption buttons with distinct native minimize, maximize, and close roles. + +### Custom Caption Buttons with Native Behavior + +For custom minimize, maximize, and close buttons that should behave like system caption buttons, enable composition hosting: + +:::caution[Experimental] +`WebView2CompositionHosting` uses WebView2 composition controller hosting with DirectComposition. See [Choosing a Mode](#choosing-a-mode) before enabling it. +::: + +```go +window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Frameless: true, + Windows: application.WindowsWindow{ + WebView2CompositionHosting: true, + }, +}) +``` + +Then mark each frontend region with `--wails-app-region`: + +```html +
+
My Application
+
+ + + +
+
+``` + +```css +.titlebar { + --wails-app-region: caption; + height: 40px; +} + +.window-controls { + display: flex; + height: 100%; +} + +.window-button { + width: 46px; + border: 0; + background: transparent; +} + +.window-button.minimize { + --wails-app-region: minimize; +} + +.window-button.maximize { + --wails-app-region: maximize; +} + +.window-button.close { + --wails-app-region: close; +} +``` + +Supported `--wails-app-region` values: + +- `caption` - draggable caption area +- `minimize` - native minimize button hit target +- `maximize` - native maximize button hit target, including Windows 11 Snap Assist / Snap Layouts hover behavior +- `close` - native close button hit target + +The Wails runtime observes DOM, style, size, scroll, and viewport changes, then sends region snapshots to the native window. Region geometry is measured in CSS pixels and converted to physical pixels for Windows hit testing. + +The visual design remains entirely yours. The regions only tell Windows what each rectangle means; the button shape, icon, color, spacing, hover styling, and layout still come from your frontend. + +For compatibility with earlier builds of this feature, Wails also accepts the legacy `--wails-non-client-region` property. New code should use `--wails-app-region`. + +### Combining Both + +You can enable both options when you want WebView2 `app-region` support and Wails-managed caption button regions in the same window: + +```go +window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Frameless: true, + Windows: application.WindowsWindow{ + NonClientRegionSupport: true, + WebView2CompositionHosting: true, + }, +}) +``` + ## System Buttons ### Implementing Close/Minimise/Maximise @@ -358,6 +511,7 @@ body { // Trigger Windows 11 Snap Assist window.SnapAssist() ``` + This triggers Snap Layouts through the Windows hotkey path. For a custom HTML maximize button with native hover Snap Layouts, use [Native Non-Client Regions on Windows](#native-non-client-regions-on-windows) instead. **Custom title bar height:** Windows automatically detects drag regions from CSS. diff --git a/docs/src/content/docs/features/windows/options.mdx b/docs/src/content/docs/features/windows/options.mdx index 9f169d20129..3866673e02a 100644 --- a/docs/src/content/docs/features/windows/options.mdx +++ b/docs/src/content/docs/features/windows/options.mdx @@ -859,6 +859,8 @@ Windows: application.WindowsWindow{ BackdropType: application.Auto, CustomTheme: application.ThemeSettings{}, DisableFramelessWindowDecorations: false, + NonClientRegionSupport: false, + WebView2CompositionHosting: false, }, ``` @@ -880,6 +882,15 @@ There are no `WindowsBackdropTypeMica`-style constants — use `application.Mica **DisableFramelessWindowDecorations** (`bool`) - Disable default frameless decorations (Aero shadow, rounded corners). +**NonClientRegionSupport** (`bool`) +- Enables WebView2-native `app-region: drag` / `app-region: no-drag` support for frameless custom title bars. +- This is for simple native app dragging only. It does not provide native custom caption-button behavior or Windows 11 Snap Assist / Snap Layouts for custom maximize buttons. + +**WebView2CompositionHosting** (`bool`) +- Enables Wails-managed `--wails-app-region` support for custom caption buttons with native Windows behavior, including Windows 11 Snap Assist / Snap Layouts on custom maximize buttons. +- Experimental. This hosts WebView2 through `ICoreWebView2CompositionController` and DirectComposition instead of the default HWND-hosted controller. +- Can be combined with `NonClientRegionSupport` when a window needs both WebView2-native `app-region` support and Wails-managed custom caption-button regions. + **Example:** ```go @@ -889,6 +900,17 @@ Windows: application.WindowsWindow{ }, ``` +**Example - Custom Windows title bar regions:** + +```go +Windows: application.WindowsWindow{ + NonClientRegionSupport: true, + WebView2CompositionHosting: true, +}, +``` + +See [Frameless Windows](/features/windows/frameless#native-non-client-regions-on-windows) for the detailed behavior, tradeoffs, and matching CSS. + ### Linux Options (per-window) The per-window struct is `application.LinuxWindow` — **not** `LinuxOptions`. diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package.json b/v3/internal/runtime/desktop/@wailsio/runtime/package.json index 593f726bdff..cd96ab1df22 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/package.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/package.json @@ -31,6 +31,7 @@ ], "sideEffects": [ "./dist/index.js", + "./dist/appregion.js", "./dist/contextmenu.js", "./dist/drag.js" ], diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts new file mode 100644 index 00000000000..dd3fd7ff87c --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts @@ -0,0 +1,228 @@ +/* + _ __ _ __ +| | / /___(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { invoke } from "./system.js"; +import { whenReady } from "./utils.js"; + +type NonClientRegionKind = "caption" | "minimize" | "maximize" | "close"; + +interface NonClientRegion { + kind: NonClientRegionKind; + left: number; + top: number; + right: number; + bottom: number; +} + +/* +--wails-non-client-region: caption; marks an area that can drag the window +--wails-non-client-region: minimize; marks a custom minimize button +--wails-non-client-region: maximize; marks a custom maximize button +--wails-non-client-region: close; marks a custom close button +*/ +const regionProperty = "--wails-non-client-region"; +const runtimeConfigReadyEvent = "wails:runtime-config-ready"; +const validRegions = new Set(["caption", "minimize", "maximize", "close"]); + +// Setup +window._wails = window._wails || {}; + +let updatePending = false; +let lastPayload = ""; +let observedElements = new Set(); +let resizeObserver: ResizeObserver | undefined; +let trackingStarted = false; + +function normaliseRegionKind(value: string): NonClientRegionKind | undefined { + const region = value.trim().toLowerCase(); + if (validRegions.has(region as NonClientRegionKind)) { + return region as NonClientRegionKind; + } + return undefined; +} + +function nonClientRegionForElement(element: Element): NonClientRegionKind | undefined { + if (!(element instanceof HTMLElement)) { + return undefined; + } + + const style = window.getComputedStyle(element); + const region = normaliseRegionKind(style.getPropertyValue(regionProperty)); + if (!region) { + return undefined; + } + + const parent = element.parentElement; + if (parent) { + const parentStyle = window.getComputedStyle(parent); + // The CSS property is inherited. Only report the outermost element for + // each contiguous region so native hit testing sees stable rectangles. + if (normaliseRegionKind(parentStyle.getPropertyValue(regionProperty)) === region) { + return undefined; + } + } + + return region; +} + +function isVisible(element: HTMLElement): boolean { + const style = window.getComputedStyle(element); + return style.display !== "none" && + style.visibility !== "hidden" && + style.contentVisibility !== "hidden"; +} + +function elementRegion(element: Element): NonClientRegion | undefined { + if (!(element instanceof HTMLElement)) { + return undefined; + } + + const kind = nonClientRegionForElement(element); + if (!kind || !isVisible(element)) { + return undefined; + } + + const rect = element.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) { + return undefined; + } + + // Native hit testing runs in physical pixels, while DOM geometry is in CSS pixels. + const scale = window.devicePixelRatio || 1; + const left = Math.floor(rect.left * scale); + const top = Math.floor(rect.top * scale); + const right = Math.ceil(rect.right * scale); + const bottom = Math.ceil(rect.bottom * scale); + + if (right <= left || bottom <= top) { + return undefined; + } + + return { kind, left, top, right, bottom }; +} + +function regionElements(): Element[] { + const elements: Element[] = []; + + if (document.documentElement) { + elements.push(document.documentElement); + } + if (document.body) { + elements.push(document.body); + elements.push(...document.body.querySelectorAll("*")); + } + + return elements; +} + +function observeRegionElements(elements: Element[]): void { + if (typeof ResizeObserver === "undefined") { + return; + } + + // Track size changes only for active region elements. DOM structure and style + // changes are covered by MutationObserver in startNonClientRegionTracking(). + resizeObserver ??= new ResizeObserver(scheduleUpdate); + const nextElements = new Set(elements); + + for (const element of observedElements) { + if (!nextElements.has(element)) { + resizeObserver.unobserve(element); + } + } + + for (const element of nextElements) { + if (!observedElements.has(element)) { + resizeObserver.observe(element); + } + } + + observedElements = nextElements; +} + +function updateNonClientRegions(): void { + updatePending = false; + + const elements = regionElements(); + const regions: NonClientRegion[] = []; + const activeElements: Element[] = []; + + for (const element of elements) { + const region = elementRegion(element); + if (region) { + regions.push(region); + activeElements.push(element); + } + } + + observeRegionElements(activeElements); + + const payload = JSON.stringify({ version: 1, regions }); + if (payload === lastPayload) { + // Avoid sending duplicate native messages during resize or style churn. + return; + } + + lastPayload = payload; + invoke("wails:non-client-region:" + payload); +} + +function scheduleUpdate(): void { + if (updatePending) { + return; + } + + // Batch region updates to animation frames so layout is measured once per frame. + updatePending = true; + window.requestAnimationFrame(updateNonClientRegions); +} + +function startNonClientRegionTracking(): void { + if (trackingStarted) { + return; + } + + trackingStarted = true; + // Send an initial empty or populated region list once the DOM is ready. + scheduleUpdate(); + + const mutationObserver = new MutationObserver(scheduleUpdate); + mutationObserver.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true, + }); + + window.addEventListener("resize", scheduleUpdate); + window.addEventListener("scroll", scheduleUpdate, true); + window.visualViewport?.addEventListener("resize", scheduleUpdate); + window.visualViewport?.addEventListener("scroll", scheduleUpdate); +} + +function tryStartNonClientRegionTracking(): boolean { + const os = window._wails.environment?.OS; + if (os === undefined) { + return false; + } + + const enabled = window._wails.flags?.nonClientRegionTracking; + if (os === "windows") { + if (enabled === true) { + whenReady(startNonClientRegionTracking); + } + return true; + } + + return true; +} + +if (!tryStartNonClientRegionTracking()) { + window.addEventListener(runtimeConfigReadyEvent, tryStartNonClientRegionTracking, { once: true }); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts index 54624f7fc65..542c6afb0be 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts @@ -152,6 +152,10 @@ function primaryDown(event: MouseEvent): void { } if (resizeEdge) { + if (event.type !== 'mousedown') { + return; + } + // Ready to resize if the primary button was pressed for the first time. canResize = true; // Do not start drag operations when on resize edges. diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts index 2a6aff439e6..7534d2a5fa1 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts @@ -13,6 +13,7 @@ window._wails = window._wails || {}; import "./contextmenu.js"; import "./drag.js"; +import "./appregion.js"; // Re-export public API import * as Application from "./application.js"; diff --git a/v3/internal/runtime/runtime.go b/v3/internal/runtime/runtime.go index d35ff05524c..bbd165fc539 100644 --- a/v3/internal/runtime/runtime.go +++ b/v3/internal/runtime/runtime.go @@ -7,6 +7,7 @@ import ( ) var runtimeInit = `window._wails=window._wails||{};window._wails.flags=window._wails.flags||{};window.wails=window.wails||{};` +var runtimeConfigReady = `Promise.resolve().then(function(){window.dispatchEvent(new Event("wails:runtime-config-ready"));});` func Core(flags map[string]any) string { flagsStr := "" @@ -17,5 +18,5 @@ func Core(flags map[string]any) string { } } - return runtimeInit + flagsStr + invoke + environment + return runtimeInit + flagsStr + invoke + environment + runtimeConfigReady } diff --git a/v3/pkg/application/application_server.go b/v3/pkg/application/application_server.go index c7ed9696ac2..353f2cabe94 100644 --- a/v3/pkg/application/application_server.go +++ b/v3/pkg/application/application_server.go @@ -533,37 +533,38 @@ func (w *serverWebviewWindow) startDrag() error { func (w *serverWebviewWindow) startResize(border string) error { return errors.New("resize not available in server mode") } -func (w *serverWebviewWindow) print() error { return errors.New("print not available in server mode") } -func (w *serverWebviewWindow) setEnabled(enabled bool) {} -func (w *serverWebviewWindow) physicalBounds() Rect { return Rect{} } -func (w *serverWebviewWindow) setPhysicalBounds(bounds Rect) {} -func (w *serverWebviewWindow) bounds() Rect { return Rect{} } -func (w *serverWebviewWindow) setBounds(bounds Rect) {} -func (w *serverWebviewWindow) position() (int, int) { return 0, 0 } -func (w *serverWebviewWindow) setPosition(x int, y int) {} -func (w *serverWebviewWindow) centerOnScreen(_ *Screen) {} -func (w *serverWebviewWindow) relativePosition() (int, int) { return 0, 0 } -func (w *serverWebviewWindow) setRelativePosition(x int, y int) {} -func (w *serverWebviewWindow) flash(enabled bool) {} -func (w *serverWebviewWindow) handleKeyEvent(acceleratorString string) {} -func (w *serverWebviewWindow) getBorderSizes() *LRTB { return &LRTB{} } -func (w *serverWebviewWindow) setMinimiseButtonState(state ButtonState) {} -func (w *serverWebviewWindow) setMaximiseButtonState(state ButtonState) {} -func (w *serverWebviewWindow) setCloseButtonState(state ButtonState) {} -func (w *serverWebviewWindow) setFullscreenButtonState(state ButtonState) {} -func (w *serverWebviewWindow) isIgnoreMouseEvents() bool { return false } -func (w *serverWebviewWindow) setIgnoreMouseEvents(ignore bool) {} -func (w *serverWebviewWindow) cut() {} -func (w *serverWebviewWindow) copy() {} -func (w *serverWebviewWindow) paste() {} -func (w *serverWebviewWindow) undo() {} -func (w *serverWebviewWindow) delete() {} -func (w *serverWebviewWindow) selectAll() {} -func (w *serverWebviewWindow) redo() {} -func (w *serverWebviewWindow) showMenuBar() {} -func (w *serverWebviewWindow) hideMenuBar() {} -func (w *serverWebviewWindow) toggleMenuBar() {} -func (w *serverWebviewWindow) setMenu(menu *Menu) {} -func (w *serverWebviewWindow) snapAssist() {} -func (w *serverWebviewWindow) attachModal(modalWindow *WebviewWindow) {} -func (w *serverWebviewWindow) setContentProtection(enabled bool) {} +func (w *serverWebviewWindow) print() error { return errors.New("print not available in server mode") } +func (w *serverWebviewWindow) setEnabled(enabled bool) {} +func (w *serverWebviewWindow) physicalBounds() Rect { return Rect{} } +func (w *serverWebviewWindow) setPhysicalBounds(bounds Rect) {} +func (w *serverWebviewWindow) bounds() Rect { return Rect{} } +func (w *serverWebviewWindow) setBounds(bounds Rect) {} +func (w *serverWebviewWindow) position() (int, int) { return 0, 0 } +func (w *serverWebviewWindow) setPosition(x int, y int) {} +func (w *serverWebviewWindow) centerOnScreen(_ *Screen) {} +func (w *serverWebviewWindow) relativePosition() (int, int) { return 0, 0 } +func (w *serverWebviewWindow) setRelativePosition(x int, y int) {} +func (w *serverWebviewWindow) flash(enabled bool) {} +func (w *serverWebviewWindow) handleKeyEvent(acceleratorString string) {} +func (w *serverWebviewWindow) getBorderSizes() *LRTB { return &LRTB{} } +func (w *serverWebviewWindow) setMinimiseButtonState(state ButtonState) {} +func (w *serverWebviewWindow) setMaximiseButtonState(state ButtonState) {} +func (w *serverWebviewWindow) setCloseButtonState(state ButtonState) {} +func (w *serverWebviewWindow) setFullscreenButtonState(state ButtonState) {} +func (w *serverWebviewWindow) isIgnoreMouseEvents() bool { return false } +func (w *serverWebviewWindow) setIgnoreMouseEvents(ignore bool) {} +func (w *serverWebviewWindow) cut() {} +func (w *serverWebviewWindow) copy() {} +func (w *serverWebviewWindow) paste() {} +func (w *serverWebviewWindow) undo() {} +func (w *serverWebviewWindow) delete() {} +func (w *serverWebviewWindow) selectAll() {} +func (w *serverWebviewWindow) redo() {} +func (w *serverWebviewWindow) showMenuBar() {} +func (w *serverWebviewWindow) hideMenuBar() {} +func (w *serverWebviewWindow) toggleMenuBar() {} +func (w *serverWebviewWindow) setMenu(menu *Menu) {} +func (w *serverWebviewWindow) snapAssist() {} +func (w *serverWebviewWindow) attachModal(modalWindow *WebviewWindow) {} +func (w *serverWebviewWindow) setContentProtection(enabled bool) {} +func (w *serverWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) {} diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 9003e3cc77f..c64847d6a75 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -1,6 +1,7 @@ package application import ( + "encoding/json" "fmt" "runtime" "slices" @@ -9,10 +10,8 @@ import ( "sync/atomic" "unsafe" - "encoding/json" - - "github.com/wailsapp/wails/v3/internal/optional" "github.com/wailsapp/wails/v3/internal/assetserver" + "github.com/wailsapp/wails/v3/internal/optional" "github.com/wailsapp/wails/v3/pkg/events" ) @@ -116,7 +115,31 @@ type ( snapAssist() setContentProtection(enabled bool) attachModal(modalWindow *WebviewWindow) + setNonClientHitTestRegions([]nonClientHitTestRegion) + } + + nonClientHitTestKind string + + // nonClientHitTestRegion is a frontend-owned client-area rectangle in physical pixels. + nonClientHitTestRegion struct { + Kind nonClientHitTestKind `json:"kind,omitempty"` + Left int `json:"left"` + Top int `json:"top"` + Right int `json:"right"` + Bottom int `json:"bottom"` } + + nonClientHitTestRegionsMessage struct { + Version int `json:"version,omitempty"` + Regions []nonClientHitTestRegion `json:"regions"` + } +) + +const ( + nonClientHitTestKindCaption nonClientHitTestKind = "caption" + nonClientHitTestKindMinimize nonClientHitTestKind = "minimize" + nonClientHitTestKindMaximize nonClientHitTestKind = "maximize" + nonClientHitTestKindClose nonClientHitTestKind = "close" ) type WindowEvent struct { @@ -776,6 +799,9 @@ func (w *WebviewWindow) HandleMessage(message string) { w.Error("%w", err) } } + case strings.HasPrefix(message, "wails:non-client-region:"): + message = strings.Replace(message, "wails:non-client-region:", "", 1) + w.handleNonClientRegionMessage(message) case message == "wails:runtime:ready": w.emit(events.Common.WindowRuntimeReady) w.pendingJSMutex.Lock() @@ -817,6 +843,41 @@ func (w *WebviewWindow) HandleMessage(message string) { } } +func (w *WebviewWindow) handleNonClientRegionMessage(payload string) { + var message nonClientHitTestRegionsMessage + if err := json.Unmarshal([]byte(payload), &message); err != nil { + w.Error("failed to parse non-client regions: %w", err) + return + } + + for i, r := range message.Regions { + if err := validateNonClientHitTestRegion(r); err != nil { + w.Error("region %d: %w", i, err) + return + } + } + + InvokeSync(func() { + w.impl.setNonClientHitTestRegions(message.Regions) + }) +} + +func validateNonClientHitTestRegion(region nonClientHitTestRegion) error { + if region.Right <= region.Left || region.Bottom <= region.Top { + return fmt.Errorf("invalid rectangle") + } + + switch nonClientHitTestKind(strings.ToLower(string(region.Kind))) { + case nonClientHitTestKindCaption, + nonClientHitTestKindMinimize, + nonClientHitTestKindMaximize, + nonClientHitTestKindClose: + return nil + default: + return fmt.Errorf("unknown region kind %q", region.Kind) + } +} + func (w *WebviewWindow) startResize(border string) error { if w.impl == nil || w.isDestroyed() { return nil diff --git a/v3/pkg/application/webview_window_android.go b/v3/pkg/application/webview_window_android.go index 2624acef556..1c07896429e 100644 --- a/v3/pkg/application/webview_window_android.go +++ b/v3/pkg/application/webview_window_android.go @@ -337,6 +337,9 @@ func (w *androidWebviewWindow) setContentProtection(_ bool) { // Android content protection - could be implemented with FLAG_SECURE } +func (w *androidWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) { +} + func (w *androidWebviewWindow) setHTML(html string) { // TODO: Implement via JNI androidLogf("debug", "setHTML called") diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 5e874867624..444d77be697 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -1658,6 +1658,9 @@ func (w *macosWebviewWindow) setContentProtection(enabled bool) { C.setContentProtection(w.nsWindow, C.bool(enabled)) } +func (w *macosWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) { +} + func (w *macosWebviewWindow) attachModal(modalWindow *WebviewWindow) { if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() { return diff --git a/v3/pkg/application/webview_window_ios.go b/v3/pkg/application/webview_window_ios.go index 6641e6c5f48..2a48e43e911 100644 --- a/v3/pkg/application/webview_window_ios.go +++ b/v3/pkg/application/webview_window_ios.go @@ -373,6 +373,9 @@ func (w *iosWebviewWindow) setContentProtection(_ bool) { // iOS content protection - could be implemented with UIScreen captured notifications } +func (w *iosWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) { +} + func (w *iosWebviewWindow) setHTML(html string) { if w.nativeHandle == nil || html == "" { return diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 0670bf99ef6..8671a4e748a 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -464,3 +464,5 @@ func (w *linuxWebviewWindow) hideMenuBar() {} func (w *linuxWebviewWindow) toggleMenuBar() {} func (w *linuxWebviewWindow) snapAssist() {} // No-op on Linux func (w *linuxWebviewWindow) setContentProtection(enabled bool) {} +func (w *linuxWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) { +} diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index 1592e5baac7..34181c4177a 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -312,6 +312,21 @@ type WindowsWindow struct { // Default: 0 ResizeDebounceMS uint16 + // NonClientRegionSupport enables WebView2's native non-client region support + // for this window when the installed WebView2 Runtime supports it. This is + // primarily intended to make app-region: drag style custom titlebars work + // with native non-client hit testing. + // Default: false + NonClientRegionSupport bool + + // WebView2CompositionHosting creates WebView2 with visual hosting using + // ICoreWebView2CompositionController and DirectComposition instead of the + // HWND-hosted controller. This is intended for custom host-owned non-client + // hit-testing, for example manual caption-button regions rendered in web + // content and resolved through GetNonClientRegionAtPoint / SendMouseInput. + // Default: false + WebView2CompositionHosting bool + // WindowDidMoveDebounceMS is the amount of time to debounce the WindowDidMove event // when moving the window // Default: 0 diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 79040ce6131..2c4c5671fbb 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -13,17 +13,17 @@ import ( "time" "unsafe" - "github.com/wailsapp/wails/v3/internal/debounce" - "github.com/wailsapp/wails/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/debounce" "github.com/wailsapp/wails/v3/internal/runtime" "github.com/wailsapp/wails/v3/internal/sliceutil" + "github.com/wailsapp/wails/webview2/webviewloader" - "github.com/wailsapp/wails/webview2/pkg/edge" "github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/w32" + "github.com/wailsapp/wails/webview2/pkg/edge" ) var edgeMap = map[string]uintptr{ @@ -82,6 +82,13 @@ type windowsWebviewWindow struct { // cannot be used for this because keyboard snap (Win+Left) bypasses those messages. lastSizeWParam uintptr + nonClientHitTest nonClientHitTestState + // Tracks the caption button currently pressed through forwarded non-client input. + // Once capture is active, Windows reports movement as normal client mouse input, + // so we need this state to keep WebView hover/pressed transitions native-like. + activeNonClientButton uintptr + activeNonClientButtonHovered bool + // menubarTheme is the theme for the menubar menubarTheme *w32.MenuBarTheme @@ -350,6 +357,9 @@ func (w *windowsWebviewWindow) run() { w.showRequested = !options.Hidden w.chromium = edge.NewChromium() + w.chromium.NonClientRegionSupportEnabled = options.Windows.NonClientRegionSupport + w.chromium.CompositionControllerEnabled = options.Windows.WebView2CompositionHosting + w.chromium.SetCursorChangedCallback(w.applyCompositionCursor) if globalApplication.options.ErrorHandler != nil { w.chromium.SetErrorCallback(globalApplication.options.ErrorHandler) } @@ -1498,6 +1508,16 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp return code } + if w.parent.options.Windows.WebView2CompositionHosting && w.chromium != nil && w.chromium.CompositionControllerReady() { + if result, handled := w.routeNonClientInput(msg, wparam, lparam); handled { + return result + } + + if w.routeCompositionMouseInput(msg, wparam, lparam) { + return 0 + } + } + if msg == w32.WM_NCHITTEST && w.isCurrentlyFullscreen { return w32.HTCLIENT } @@ -2251,12 +2271,15 @@ func (w *windowsWebviewWindow) navigationCompleted( args *edge.ICoreWebView2NavigationCompletedEventArgs, ) { - // Install the runtime core - w.execJS(runtime.Core(globalApplication.impl.GetFlags(globalApplication.options))) - - // Set the EnableFileDrop flag for this window (Windows-specific) - // The JS runtime checks this before processing file drops - w.execJS(fmt.Sprintf("window._wails.flags.enableFileDrop = %v;", w.parent.options.EnableFileDrop)) + // Inject runtime core and window-specific flags together so side-effect + // runtime modules see a consistent _wails configuration at startup. + js := runtime.Core(globalApplication.impl.GetFlags(globalApplication.options)) + js += fmt.Sprintf( + "window._wails.flags.enableFileDrop = %v; window._wails.flags.nonClientRegionTracking = %v;", + w.parent.options.EnableFileDrop, + w.parent.options.Windows.WebView2CompositionHosting, + ) + w.execJS(js) // EmitEvent DomReady ApplicationEvent windowEvents <- &windowEvent{EventID: uint(events.Windows.WebViewNavigationCompleted), WindowID: w.parent.id} diff --git a/v3/pkg/application/webview_window_windows_nonclient.go b/v3/pkg/application/webview_window_windows_nonclient.go new file mode 100644 index 00000000000..9fa4b9914fd --- /dev/null +++ b/v3/pkg/application/webview_window_windows_nonclient.go @@ -0,0 +1,507 @@ +//go:build windows + +package application + +import ( + "slices" + "sync" + + "github.com/wailsapp/wails/v3/pkg/w32" + "github.com/wailsapp/wails/webview2/pkg/edge" +) + +type nonClientHitTestState struct { + mu sync.RWMutex + regions []nonClientHitTestRegion +} + +func (s *nonClientHitTestState) set(regions []nonClientHitTestRegion) { + s.mu.Lock() + if len(regions) == 0 { + s.regions = nil + } else { + s.regions = slices.Clone(regions) + } + s.mu.Unlock() +} + +func (s *nonClientHitTestState) snapshot() []nonClientHitTestRegion { + s.mu.RLock() + defer s.mu.RUnlock() + if len(s.regions) == 0 { + return nil + } + return append([]nonClientHitTestRegion(nil), s.regions...) +} + +func (w *windowsWebviewWindow) setNonClientHitTestRegions(regions []nonClientHitTestRegion) { + w.nonClientHitTest.set(regions) +} + +func (w *windowsWebviewWindow) applyCompositionCursor(cursor edge.HCURSOR, systemCursorID uint32) { + hcursor := w32.HCURSOR(cursor) + if hcursor == 0 && systemCursorID != 0 { + hcursor = w32.LoadCursorWithResourceID(0, uint16(systemCursorID)) + } + if hcursor == 0 { + return + } + + w32.SetClassCursor(w.hwnd, hcursor) + w32.SetCursor(hcursor) +} + +func (w *windowsWebviewWindow) routeNonClientInput(msg uint32, wparam, lparam uintptr) (uintptr, bool) { + switch msg { + case w32.WM_NCHITTEST: + if hitTest, handled := w32.DwmDefWindowProc(w.hwnd, msg, wparam, lparam); handled { + if hitTest != w32.HTCLIENT && hitTest != w32.HTNOWHERE { + return hitTest, true + } + } + + screenX := int(w32.GET_X_LPARAM(lparam)) + screenY := int(w32.GET_Y_LPARAM(lparam)) + + if hitTest, ok := w.resizeBorderHitTest(screenX, screenY); ok { + return hitTest, true + } + + return w.nonClientHitTestFromScreen(screenX, screenY) + case w32.WM_NCMOUSEMOVE: + screenX := int(w32.GET_X_LPARAM(lparam)) + screenY := int(w32.GET_Y_LPARAM(lparam)) + + hitTest, ok := w.nonClientHitTestFromScreen(screenX, screenY) + if !ok || hitTest != wparam { + return 0, false + } + + clientX, clientY, ok := w32.ScreenToClient( + w.hwnd, + int(w32.GET_X_LPARAM(lparam)), + int(w32.GET_Y_LPARAM(lparam)), + ) + if !ok { + return 0, false + } + + _ = w.chromium.SendMouseInput( + edge.COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + 0, + clientX, + clientY, + ) + + return w32.DwmDefWindowProc(w.hwnd, msg, wparam, lparam) + case w32.WM_NCMOUSELEAVE: + // Windows can emit a spurious NCMOUSELEAVE right after a forwarded + // non-client button press. Suppress it until the captured mouse move + // path below can decide whether the pointer really left the active button. + if w32.GetCapture() == w.hwnd && w.activeNonClientButton != 0 { + return 0, true + } + + _ = w.chromium.SendMouseInput( + edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + 0, + 0, + 0, + ) + case w32.WM_NCLBUTTONDOWN, w32.WM_NCLBUTTONUP, w32.WM_NCLBUTTONDBLCLK: + switch wparam { + case w32.HTMINBUTTON, w32.HTMAXBUTTON, w32.HTCLOSE: + default: + return 0, false + } + + if w.forwardFrontendNonClientButtonInput(msg, wparam, lparam) { + return 0, true + } + case w32.WM_NCRBUTTONUP: + if wparam != w32.HTCAPTION && wparam != w32.HTSYSMENU { + return 0, false + } + + screenX := int(w32.GET_X_LPARAM(lparam)) + screenY := int(w32.GET_Y_LPARAM(lparam)) + + if hitTest, ok := w.nonClientHitTestFromScreen(screenX, screenY); !ok || hitTest != w32.HTCAPTION { + return 0, false + } + + if w.showCaptionSystemMenu(screenX, screenY) { + return 0, true + } + } + + return 0, false +} + +func (w *windowsWebviewWindow) nonClientHitTestFromScreen(screenX, screenY int) (uintptr, bool) { + regions := w.nonClientHitTest.snapshot() + // Later frontend regions should win when rectangles overlap, matching the + // DOM/CSS order used to collect them (for example caption buttons over a + // caption drag area). + for i := len(regions) - 1; i >= 0; i-- { + r := regions[i] + + left, top := w32.ClientToScreen(w.hwnd, r.Left, r.Top) + right, bottom := w32.ClientToScreen(w.hwnd, r.Right, r.Bottom) + + // outside app + if screenX < left || screenX >= right || screenY < top || screenY >= bottom { + continue + } + + switch r.Kind { + case nonClientHitTestKindMaximize: + return w32.HTMAXBUTTON, true + case nonClientHitTestKindCaption: + return w32.HTCAPTION, true + case nonClientHitTestKindMinimize: + return w32.HTMINBUTTON, true + case nonClientHitTestKindClose: + return w32.HTCLOSE, true + } + } + + // fallback to default non-client region hit test (app-region) + if !w.chromium.NonClientRegionSupportEnabled { + return 0, false + } + + clientX, clientY, ok := w32.ScreenToClient(w.hwnd, screenX, screenY) + if !ok { + return 0, false + } + + region, handled, err := w.chromium.GetNonClientRegionAtPoint(int32(clientX), int32(clientY)) + if err != nil || !handled { + return 0, false + } + + switch region { + case edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_CAPTION: + return w32.HTCAPTION, true + case edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_MINIMIZE: + return w32.HTMINBUTTON, true + case edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_MAXIMIZE: + return w32.HTMAXBUTTON, true + case edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_CLOSE: + return w32.HTCLOSE, true + case edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_CLIENT, + edge.COREWEBVIEW2_NON_CLIENT_REGION_KIND_NOWHERE: + return 0, false + default: + return 0, false + } +} + +func (w *windowsWebviewWindow) resizeBorderHitTest(screenX, screenY int) (uintptr, bool) { + if w.parent.options.DisableResize || w.isMaximised() || w.isFullscreen() { + return 0, false + } + + rect := w32.GetWindowRect(w.hwnd) + width := int(rect.Right - rect.Left) + height := int(rect.Bottom - rect.Top) + if width <= 0 || height <= 0 { + return 0, false + } + + x := screenX - int(rect.Left) + y := screenY - int(rect.Top) + if x < 0 || y < 0 || x >= width || y >= height { + return 0, false + } + + borderX := int(w.resizeBorderWidth) + w32.GetSystemMetrics(w32.SM_CXPADDEDBORDER) + if borderX < 1 { + borderX = 1 + } + borderY := int(w.resizeBorderHeight) + w32.GetSystemMetrics(w32.SM_CXPADDEDBORDER) + if borderY < 1 { + borderY = 1 + } + + left := x < borderX + right := x >= width-borderX + top := y < borderY + bottom := y >= height-borderY + + switch { + case top && left: + return w32.HTTOPLEFT, true + case top && right: + return w32.HTTOPRIGHT, true + case bottom && left: + return w32.HTBOTTOMLEFT, true + case bottom && right: + return w32.HTBOTTOMRIGHT, true + case top: + return w32.HTTOP, true + case bottom: + return w32.HTBOTTOM, true + case left: + return w32.HTLEFT, true + case right: + return w32.HTRIGHT, true + default: + return 0, false + } +} + +func (w *windowsWebviewWindow) forwardFrontendNonClientButtonInput(msg uint32, wparam, lparam uintptr) bool { + screenX := int(w32.GET_X_LPARAM(lparam)) + screenY := int(w32.GET_Y_LPARAM(lparam)) + + hitTest, ok := w.nonClientHitTestFromScreen(screenX, screenY) + if !ok || hitTest != wparam { + return false + } + + clientX, clientY, ok := w32.ScreenToClient(w.hwnd, screenX, screenY) + if !ok { + return false + } + + eventKind, virtualKeys, ok := nonClientLeftButtonMouseEvent(msg) + if !ok { + return false + } + + if err := w.chromium.SendMouseInput( + eventKind, + virtualKeys, + 0, + clientX, + clientY, + ); err != nil { + return false + } + + // Remember which caption button started the press. During capture, Windows + // sends movement as WM_MOUSEMOVE, so later moves must be compared against + // this original hit-test rather than any caption button under the cursor. + if msg == w32.WM_NCLBUTTONDOWN || msg == w32.WM_NCLBUTTONDBLCLK { + w.activeNonClientButton = wparam + w.activeNonClientButtonHovered = true + } + + w.updateCompositionMouseCapture(msg) + + return true +} + +func (w *windowsWebviewWindow) showCaptionSystemMenu(screenX, screenY int) bool { + menu := w32.GetSystemMenu(w.hwnd, false) + if menu == 0 { + return false + } + + w32.SetForegroundWindow(w.hwnd) + command := w32.TrackPopupMenuCommand( + menu, + w32.TPM_LEFTALIGN|w32.TPM_TOPALIGN|w32.TPM_RIGHTBUTTON, + int32(screenX), + int32(screenY), + w.hwnd, + nil, + ) + w32.PostMessage(w.hwnd, w32.WM_NULL, 0, 0) + if command == 0 { + return true + } + + w32.SendMessage(w.hwnd, w32.WM_SYSCOMMAND, command, 0) + return true +} + +func (w *windowsWebviewWindow) routeCompositionMouseInput(msg uint32, wparam, lparam uintptr) bool { + eventKind, ok := webviewCompositionMouseEventKind(msg) + if !ok { + return false + } + + clientX, clientY, ok := w.compositionMouseClientPoint(msg, lparam) + if !ok { + return false + } + + var mouseData uint32 + switch msg { + case w32.WM_MOUSEWHEEL, w32.WM_MOUSEHWHEEL: + mouseData = uint32(int16(wparam >> 16)) + case w32.WM_XBUTTONDOWN, w32.WM_XBUTTONUP, w32.WM_XBUTTONDBLCLK: + mouseData = uint32(wparam >> 16) + } + + // While a forwarded caption button owns capture, WebView still receives + // normal mouse moves. Only forward those moves while the cursor remains over + // the same button; otherwise synthesize leave so pressed styling is cleared. + if msg == w32.WM_MOUSEMOVE && w.activeNonClientButton != 0 && w32.GetCapture() == w.hwnd { + if !w.updateActiveNonClientButtonHover() { + return true + } + } + + if err := w.chromium.SendMouseInput( + eventKind, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS(uint32(wparam)&0xffff), + mouseData, + clientX, + clientY, + ); err != nil { + return false + } + + w.updateCompositionMouseCapture(msg) + + return true +} + +func (w *windowsWebviewWindow) compositionMouseClientPoint(msg uint32, lparam uintptr) (int, int, bool) { + switch msg { + case w32.WM_MOUSELEAVE: + return 0, 0, true + case w32.WM_MOUSEWHEEL, w32.WM_MOUSEHWHEEL: + return w32.ScreenToClient(w.hwnd, int(w32.GET_X_LPARAM(lparam)), int(w32.GET_Y_LPARAM(lparam))) + default: + return int(w32.GET_X_LPARAM(lparam)), int(w32.GET_Y_LPARAM(lparam)), true + } +} + +func (w *windowsWebviewWindow) updateCompositionMouseCapture(msg uint32) { + if isCompositionMouseButtonDown(msg) { + if w32.GetCapture() != w.hwnd { + w32.SetCapture(w.hwnd) + } + return + } + + if isCompositionMouseButtonUp(msg) && w32.GetCapture() == w.hwnd { + w32.ReleaseCapture() + + // Button release ends the synthetic caption-button interaction. + w.activeNonClientButton = 0 + w.activeNonClientButtonHovered = false + } +} + +// updateActiveNonClientButtonHover returns whether the current move should be +// forwarded to WebView. It sends a single LEAVE when capture moves off the +// originally pressed caption button, matching native caption-button behavior. +func (w *windowsWebviewWindow) updateActiveNonClientButtonHover() bool { + screenX, screenY, ok := w32.GetCursorPos() + if !ok { + return true + } + + hitTest, ok := w.nonClientHitTestFromScreen(screenX, screenY) + hovered := ok && hitTest == w.activeNonClientButton + if hovered { + w.activeNonClientButtonHovered = true + return true + } + + if w.activeNonClientButtonHovered { + _ = w.chromium.SendMouseInput( + edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + 0, + 0, + 0, + ) + w.activeNonClientButtonHovered = false + } + + return false +} + +func nonClientLeftButtonMouseEvent(msg uint32) (edge.COREWEBVIEW2_MOUSE_EVENT_KIND, edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS, bool) { + switch msg { + case w32.WM_NCLBUTTONDOWN: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON, + true + case w32.WM_NCLBUTTONUP: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + true + case w32.WM_NCLBUTTONDBLCLK: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOUBLE_CLICK, + edge.COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON, + true + default: + return 0, 0, false + } +} + +func isCompositionMouseButtonDown(msg uint32) bool { + switch msg { + case w32.WM_LBUTTONDOWN, + w32.WM_NCLBUTTONDOWN, + w32.WM_NCLBUTTONDBLCLK, + w32.WM_RBUTTONDOWN, + w32.WM_MBUTTONDOWN, + w32.WM_XBUTTONDOWN: + return true + default: + return false + } +} + +func isCompositionMouseButtonUp(msg uint32) bool { + switch msg { + case w32.WM_LBUTTONUP, + w32.WM_NCLBUTTONUP, + w32.WM_RBUTTONUP, + w32.WM_MBUTTONUP, + w32.WM_XBUTTONUP: + return true + default: + return false + } +} + +func webviewCompositionMouseEventKind(msg uint32) (edge.COREWEBVIEW2_MOUSE_EVENT_KIND, bool) { + switch msg { + case w32.WM_MOUSELEAVE: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE, true + case w32.WM_MOUSEMOVE: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, true + case w32.WM_LBUTTONDOWN: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN, true + case w32.WM_LBUTTONUP: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP, true + case w32.WM_LBUTTONDBLCLK: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOUBLE_CLICK, true + case w32.WM_RBUTTONDOWN: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN, true + case w32.WM_RBUTTONUP: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP, true + case w32.WM_RBUTTONDBLCLK: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOUBLE_CLICK, true + case w32.WM_MBUTTONDOWN: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN, true + case w32.WM_MBUTTONUP: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP, true + case w32.WM_MBUTTONDBLCLK: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOUBLE_CLICK, true + case w32.WM_XBUTTONDOWN: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOWN, true + case w32.WM_XBUTTONUP: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_UP, true + case w32.WM_XBUTTONDBLCLK: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOUBLE_CLICK, true + case w32.WM_MOUSEWHEEL: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL, true + case w32.WM_MOUSEHWHEEL: + return edge.COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, true + default: + return 0, false + } +} diff --git a/v3/pkg/w32/dwmapi.go b/v3/pkg/w32/dwmapi.go index 1b911efc316..d66d17c7013 100644 --- a/v3/pkg/w32/dwmapi.go +++ b/v3/pkg/w32/dwmapi.go @@ -13,6 +13,7 @@ var ( procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") procDwmGetWindowAttribute = moddwmapi.NewProc("DwmGetWindowAttribute") procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea") + procDwmDefWindowProc = moddwmapi.NewProc("DwmDefWindowProc") ) func DwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) HRESULT { @@ -33,6 +34,18 @@ func DwmGetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribut return HRESULT(ret) } +func DwmDefWindowProc(hwnd HWND, msg uint32, wparam, lparam uintptr) (uintptr, bool) { + var result uintptr + ret, _, _ := procDwmDefWindowProc.Call( + hwnd, + uintptr(msg), + wparam, + lparam, + uintptr(unsafe.Pointer(&result)), + ) + return result, ret != 0 +} + func dwmExtendFrameIntoClientArea(hwnd uintptr, margins *MARGINS) error { ret, _, _ := procDwmExtendFrameIntoClientArea.Call( hwnd, diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index a68c9b8e43b..a6b3e81de7e 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -52,9 +52,9 @@ var ( procScreenToClient = moduser32.NewProc("ScreenToClient") procCallWindowProc = moduser32.NewProc("CallWindowProcW") procSetWindowLong = moduser32.NewProc("SetWindowLongW") - procSetWindowLongPtr = moduser32.NewProc("SetWindowLongW") + procSetWindowLongPtr = moduser32.NewProc("SetWindowLongPtrW") procGetWindowLong = moduser32.NewProc("GetWindowLongW") - procGetWindowLongPtr = moduser32.NewProc("GetWindowLongW") + procGetWindowLongPtr = moduser32.NewProc("GetWindowLongPtrW") procEnableWindow = moduser32.NewProc("EnableWindow") procIsWindowEnabled = moduser32.NewProc("IsWindowEnabled") procIsWindowVisible = moduser32.NewProc("IsWindowVisible") @@ -67,6 +67,7 @@ var ( procGetClientRect = moduser32.NewProc("GetClientRect") procGetDC = moduser32.NewProc("GetDC") procReleaseDC = moduser32.NewProc("ReleaseDC") + procGetCapture = moduser32.NewProc("GetCapture") procSetCapture = moduser32.NewProc("SetCapture") procReleaseCapture = moduser32.NewProc("ReleaseCapture") procGetWindowThreadProcessId = moduser32.NewProc("GetWindowThreadProcessId") @@ -139,9 +140,9 @@ var ( procGetDpiForSystem = moduser32.NewProc("GetDpiForSystem") procGetDpiForWindow = moduser32.NewProc("GetDpiForWindow") procSetProcessDPIAware = moduser32.NewProc("SetProcessDPIAware") - procSetProcessDpiAwarenessContext = moduser32.NewProc("SetProcessDpiAwarenessContext") - procGetThreadDpiAwarenessContext = moduser32.NewProc("GetThreadDpiAwarenessContext") - procAreDpiAwarenessContextsEqual = moduser32.NewProc("AreDpiAwarenessContextsEqual") + procSetProcessDpiAwarenessContext = moduser32.NewProc("SetProcessDpiAwarenessContext") + procGetThreadDpiAwarenessContext = moduser32.NewProc("GetThreadDpiAwarenessContext") + procAreDpiAwarenessContextsEqual = moduser32.NewProc("AreDpiAwarenessContextsEqual") procEnumDisplayMonitors = moduser32.NewProc("EnumDisplayMonitors") procEnumDisplayDevices = moduser32.NewProc("EnumDisplayDevicesW") procEnumDisplaySettings = moduser32.NewProc("EnumDisplaySettingsW") @@ -760,6 +761,11 @@ func SetCapture(hwnd HWND) HWND { return HWND(ret) } +func GetCapture() HWND { + ret, _, _ := procGetCapture.Call() + return HWND(ret) +} + func ReleaseCapture() bool { ret, _, _ := procReleaseCapture.Call() @@ -1525,6 +1531,18 @@ func TrackPopupMenu(hmenu HMENU, flags uint32, x, y int32, reserved int32, hwnd return ret != 0 } +func TrackPopupMenuCommand(hmenu HMENU, flags uint32, x, y int32, hwnd HWND, prcRect *RECT) uintptr { + ret, _, _ := procTrackPopupMenu.Call( + uintptr(hmenu), + uintptr(flags|TPM_RETURNCMD), + uintptr(x), + uintptr(y), + 0, + uintptr(hwnd), + uintptr(unsafe.Pointer(prcRect))) + return ret +} + // KeybdEvent synthesizes a keystroke. The system can use such a synthesized keystroke to generate a WM_KEYUP or WM_KEYDOWN message. // bVk: Virtual-key code // bScan: Hardware scan code diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go index c26ec414d1e..602649ea4a0 100644 --- a/v3/pkg/w32/window.go +++ b/v3/pkg/w32/window.go @@ -43,6 +43,7 @@ var Fatal func(error) const ( GCLP_HBRBACKGROUND int32 = -10 + GCLP_HCURSOR int32 = -12 GCLP_HICON int32 = -14 ) @@ -63,7 +64,10 @@ func ExtendFrameIntoClientArea(hwnd uintptr, extend bool) error { // are shown if transparent ant translucent. var margins MARGINS if extend { - margins = MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons + // Leave the top edge unextended so the Windows 11 Snap Layout flyout does + // not appear over custom HTMAXBUTTON regions. Side and bottom margins + // preserve the default frame styling. + margins = MARGINS{1, 1, 0, 1} } if err := dwmExtendFrameIntoClientArea(hwnd, &margins); err != nil { return fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err) @@ -114,6 +118,10 @@ func SetApplicationIcon(hwnd uintptr, icon HICON) { setClassLongPtr(hwnd, GCLP_HICON, icon) } +func SetClassCursor(hwnd uintptr, cursor HCURSOR) { + setClassLongPtr(hwnd, GCLP_HCURSOR, cursor) +} + func SetBackgroundColour(hwnd uintptr, r, g, b uint8) { col := uint32(r) | uint32(g)<<8 | uint32(b)<<16 hbrush, _, _ := procCreateSolidBrush.Call(uintptr(col)) @@ -192,7 +200,7 @@ func MustStringToUTF16uintptr(input string) uintptr { } // MustStringToUTF16 converts s to UTF-16 encoding, stripping any embedded NULs and panicking on error. -// +// // The returned slice is suitable for Windows API calls that expect a UTF-16 encoded string. func MustStringToUTF16(input string) []uint16 { input = stripNulls(input) @@ -339,6 +347,11 @@ func EnableCloseButton(hwnd HWND) error { return nil } +func GetSystemMenu(hwnd HWND, revert bool) HMENU { + ret, _, _ := getSystemMenu.Call(hwnd, uintptr(BoolToBOOL(revert))) + return HMENU(ret) +} + func FindWindowW(className, windowName *uint16) HWND { ret, _, _ := findWindow.Call( uintptr(unsafe.Pointer(className)), @@ -374,4 +387,4 @@ func SendMessageToWindow(hwnd HWND, msg string) { func GetMenu(hwnd HWND) HMENU { ret, _, _ := getMenuProc.Call(hwnd) return ret -} \ No newline at end of file +} diff --git a/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.go b/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.go new file mode 100644 index 00000000000..e610931b2f9 --- /dev/null +++ b/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.go @@ -0,0 +1,26 @@ +//go:build windows + +package edge + +type COREWEBVIEW2_MOUSE_EVENT_KIND uint32 + +const ( + COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL = 526 + COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOUBLE_CLICK = 515 + COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN = 513 + COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP = 514 + COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE = 675 + COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOUBLE_CLICK = 521 + COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN = 519 + COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP = 520 + COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE = 512 + COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOUBLE_CLICK = 518 + COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN = 516 + COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP = 517 + COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL = 522 + COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOUBLE_CLICK = 525 + COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOWN = 523 + COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_UP = 524 + COREWEBVIEW2_MOUSE_EVENT_KIND_NON_CLIENT_RIGHT_BUTTON_DOWN = 164 + COREWEBVIEW2_MOUSE_EVENT_KIND_NON_CLIENT_RIGHT_BUTTON_UP = 165 +) diff --git a/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.go b/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.go new file mode 100644 index 00000000000..363edbae068 --- /dev/null +++ b/webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.go @@ -0,0 +1,16 @@ +//go:build windows + +package edge + +type COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS uint32 + +const ( + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 0 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 1 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_RIGHT_BUTTON COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 2 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_SHIFT COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 4 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_CONTROL COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 8 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_MIDDLE_BUTTON COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 16 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_X_BUTTON1 COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 32 + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_X_BUTTON2 COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS = 64 +) diff --git a/webview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.go b/webview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.go new file mode 100644 index 00000000000..2cda60b0253 --- /dev/null +++ b/webview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.go @@ -0,0 +1,14 @@ +//go:build windows + +package edge + +type COREWEBVIEW2_NON_CLIENT_REGION_KIND uint32 + +const ( + COREWEBVIEW2_NON_CLIENT_REGION_KIND_NOWHERE = 0 + COREWEBVIEW2_NON_CLIENT_REGION_KIND_CLIENT = 1 + COREWEBVIEW2_NON_CLIENT_REGION_KIND_CAPTION = 2 + COREWEBVIEW2_NON_CLIENT_REGION_KIND_MINIMIZE = 8 + COREWEBVIEW2_NON_CLIENT_REGION_KIND_MAXIMIZE = 9 + COREWEBVIEW2_NON_CLIENT_REGION_KIND_CLOSE = 20 +) diff --git a/webview2/pkg/edge/ICoreWebView2CompositionController.go b/webview2/pkg/edge/ICoreWebView2CompositionController.go new file mode 100644 index 00000000000..60295f90b3a --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2CompositionController.go @@ -0,0 +1,109 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" +) + +type ICoreWebView2CompositionControllerVtbl struct { + _IUnknownVtbl + GetRootVisualTarget ComProc + PutRootVisualTarget ComProc + SendMouseInput ComProc + SendPointerInput ComProc + GetCursor ComProc + GetSystemCursorId ComProc + AddCursorChanged ComProc + RemoveCursorChanged ComProc +} + +type ICoreWebView2CompositionController struct { + Vtbl *ICoreWebView2CompositionControllerVtbl +} + +func (i *ICoreWebView2CompositionController) AddRef() uintptr { + ret, _, _ := i.Vtbl.AddRef.Call(uintptr(unsafe.Pointer(i))) + + return ret +} + +func (i *ICoreWebView2CompositionController) Release() uintptr { + ret, _, _ := i.Vtbl.Release.Call(uintptr(unsafe.Pointer(i))) + + return ret +} + +func (i *ICoreWebView2CompositionController) GetICoreWebView2Controller() *ICoreWebView2Controller { + var result *ICoreWebView2Controller + + iidICoreWebView2Controller := NewGUID("{4D00C0D1-9434-4EB6-8078-8697A560334F}") + _, _, _ = i.Vtbl.QueryInterface.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(iidICoreWebView2Controller)), + uintptr(unsafe.Pointer(&result))) + + return result +} + +func (i *ICoreWebView2CompositionController) PutRootVisualTarget(target *IUnknown) error { + hr, _, _ := i.Vtbl.PutRootVisualTarget.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(target)), + ) + if int32(hr) < 0 { + return syscall.Errno(hr) + } + return nil +} + +func (i *ICoreWebView2CompositionController) SendMouseInput(eventKind COREWEBVIEW2_MOUSE_EVENT_KIND, virtualKeys COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS, mouseData uint32, point POINT) error { + hr, _, _ := i.Vtbl.SendMouseInput.Call( + uintptr(unsafe.Pointer(i)), + uintptr(eventKind), + uintptr(virtualKeys), + uintptr(mouseData), + point.uintptr(), + ) + if int32(hr) < 0 { + return syscall.Errno(hr) + } + return nil +} + +func (i *ICoreWebView2CompositionController) GetCursor() (HCURSOR, error) { + var cursor HCURSOR + hr, _, _ := i.Vtbl.GetCursor.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(&cursor)), + ) + if int32(hr) < 0 { + return 0, syscall.Errno(hr) + } + return cursor, nil +} + +func (i *ICoreWebView2CompositionController) GetSystemCursorId() (uint32, error) { + var cursorID uint32 + hr, _, _ := i.Vtbl.GetSystemCursorId.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(&cursorID)), + ) + if int32(hr) < 0 { + return 0, syscall.Errno(hr) + } + return cursorID, nil +} + +func (i *ICoreWebView2CompositionController) AddCursorChanged(eventHandler *iCoreWebView2CursorChangedEventHandler, token *_EventRegistrationToken) error { + hr, _, _ := i.Vtbl.AddCursorChanged.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(eventHandler)), + uintptr(unsafe.Pointer(token)), + ) + if int32(hr) < 0 { + return syscall.Errno(hr) + } + return nil +} diff --git a/webview2/pkg/edge/ICoreWebView2CompositionController4.go b/webview2/pkg/edge/ICoreWebView2CompositionController4.go new file mode 100644 index 00000000000..3b710281ede --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2CompositionController4.go @@ -0,0 +1,73 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type ICoreWebView2CompositionController4Vtbl struct { + _IUnknownVtbl + GetRootVisualTarget ComProc + PutRootVisualTarget ComProc + SendMouseInput ComProc + SendPointerInput ComProc + GetCursor ComProc + GetSystemCursorId ComProc + AddCursorChanged ComProc + RemoveCursorChanged ComProc + GetUIAProvider ComProc + DragEnter ComProc + DragLeave ComProc + DragOver ComProc + Drop ComProc + GetNonClientRegionAtPoint ComProc + QueryNonClientRegion ComProc + AddNonClientRegionChanged ComProc + RemoveNonClientRegionChanged ComProc +} + +type ICoreWebView2CompositionController4 struct { + Vtbl *ICoreWebView2CompositionController4Vtbl +} + +func (i *ICoreWebView2CompositionController4) AddRef() uintptr { + ret, _, _ := i.Vtbl.AddRef.Call(uintptr(unsafe.Pointer(i))) + + return ret +} + +func (i *ICoreWebView2CompositionController4) Release() uintptr { + ret, _, _ := i.Vtbl.Release.Call(uintptr(unsafe.Pointer(i))) + + return ret +} + +func (i *ICoreWebView2CompositionController) GetICoreWebView2CompositionController4() *ICoreWebView2CompositionController4 { + var result *ICoreWebView2CompositionController4 + + iidICoreWebView2CompositionController4 := NewGUID("{7C367B9B-3D2B-450F-9E58-D61A20F486AA}") + _, _, _ = i.Vtbl.QueryInterface.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(iidICoreWebView2CompositionController4)), + uintptr(unsafe.Pointer(&result))) + + return result +} + +func (i *ICoreWebView2CompositionController4) GetNonClientRegionAtPoint(point POINT) (COREWEBVIEW2_NON_CLIENT_REGION_KIND, error) { + var value COREWEBVIEW2_NON_CLIENT_REGION_KIND + + hr, _, _ := i.Vtbl.GetNonClientRegionAtPoint.Call( + uintptr(unsafe.Pointer(i)), + point.uintptr(), + uintptr(unsafe.Pointer(&value)), + ) + if windows.Handle(hr) != windows.S_OK { + return 0, syscall.Errno(hr) + } + return value, nil +} diff --git a/webview2/pkg/edge/ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler.go b/webview2/pkg/edge/ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler.go new file mode 100644 index 00000000000..b9ac1587d1a --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler.go @@ -0,0 +1,50 @@ +//go:build windows + +package edge + +type iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerVtbl struct { + _IUnknownVtbl + Invoke ComProc +} + +type iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler struct { + vtbl *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerVtbl + impl iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerImpl +} + +func iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownQueryInterface(this *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler, refiid, object uintptr) uintptr { + return this.impl.QueryInterface(refiid, object) +} + +func iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownAddRef(this *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler) uintptr { + return this.impl.AddRef() +} + +func iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownRelease(this *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler) uintptr { + return this.impl.Release() +} + +func iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerInvoke(this *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler, errorCode uintptr, result *ICoreWebView2CompositionController) uintptr { + return this.impl.CreateCoreWebView2CompositionControllerCompleted(errorCode, result) +} + +type iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerImpl interface { + _IUnknownImpl + CreateCoreWebView2CompositionControllerCompleted(errorCode uintptr, result *ICoreWebView2CompositionController) uintptr +} + +var iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerFn = iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerVtbl{ + _IUnknownVtbl{ + NewComProc(iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownQueryInterface), + NewComProc(iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownAddRef), + NewComProc(iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerIUnknownRelease), + }, + NewComProc(iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerInvoke), +} + +func newICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler(impl iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerImpl) *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler { + return &iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler{ + vtbl: &iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandlerFn, + impl: impl, + } +} diff --git a/webview2/pkg/edge/ICoreWebView2CursorChangedEventHandler.go b/webview2/pkg/edge/ICoreWebView2CursorChangedEventHandler.go new file mode 100644 index 00000000000..7c816d36639 --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2CursorChangedEventHandler.go @@ -0,0 +1,50 @@ +//go:build windows + +package edge + +type iCoreWebView2CursorChangedEventHandlerVtbl struct { + _IUnknownVtbl + Invoke ComProc +} + +type iCoreWebView2CursorChangedEventHandler struct { + vtbl *iCoreWebView2CursorChangedEventHandlerVtbl + impl iCoreWebView2CursorChangedEventHandlerImpl +} + +func iCoreWebView2CursorChangedEventHandlerIUnknownQueryInterface(this *iCoreWebView2CursorChangedEventHandler, refiid, object uintptr) uintptr { + return this.impl.QueryInterface(refiid, object) +} + +func iCoreWebView2CursorChangedEventHandlerIUnknownAddRef(this *iCoreWebView2CursorChangedEventHandler) uintptr { + return this.impl.AddRef() +} + +func iCoreWebView2CursorChangedEventHandlerIUnknownRelease(this *iCoreWebView2CursorChangedEventHandler) uintptr { + return this.impl.Release() +} + +func iCoreWebView2CursorChangedEventHandlerInvoke(this *iCoreWebView2CursorChangedEventHandler, sender *ICoreWebView2CompositionController, args *IUnknown) uintptr { + return this.impl.CursorChanged(sender, args) +} + +type iCoreWebView2CursorChangedEventHandlerImpl interface { + _IUnknownImpl + CursorChanged(sender *ICoreWebView2CompositionController, args *IUnknown) uintptr +} + +var iCoreWebView2CursorChangedEventHandlerFn = iCoreWebView2CursorChangedEventHandlerVtbl{ + _IUnknownVtbl{ + NewComProc(iCoreWebView2CursorChangedEventHandlerIUnknownQueryInterface), + NewComProc(iCoreWebView2CursorChangedEventHandlerIUnknownAddRef), + NewComProc(iCoreWebView2CursorChangedEventHandlerIUnknownRelease), + }, + NewComProc(iCoreWebView2CursorChangedEventHandlerInvoke), +} + +func newICoreWebView2CursorChangedEventHandler(impl iCoreWebView2CursorChangedEventHandlerImpl) *iCoreWebView2CursorChangedEventHandler { + return &iCoreWebView2CursorChangedEventHandler{ + vtbl: &iCoreWebView2CursorChangedEventHandlerFn, + impl: impl, + } +} diff --git a/webview2/pkg/edge/ICoreWebView2Environment3.go b/webview2/pkg/edge/ICoreWebView2Environment3.go new file mode 100644 index 00000000000..992278ee4c7 --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2Environment3.go @@ -0,0 +1,63 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type iCoreWebView2Environment3Vtbl struct { + _IUnknownVtbl + CreateCoreWebView2Controller ComProc + CreateWebResourceResponse ComProc + GetBrowserVersionString ComProc + AddNewBrowserVersionAvailable ComProc + RemoveNewBrowserVersionAvailable ComProc + CreateWebResourceRequest ComProc + CreateCoreWebView2CompositionController ComProc + CreateCoreWebView2PointerInfo ComProc +} + +type ICoreWebView2Environment3 struct { + vtbl *iCoreWebView2Environment3Vtbl +} + +func (e *ICoreWebView2Environment3) AddRef() uintptr { + ret, _, _ := e.vtbl.AddRef.Call(uintptr(unsafe.Pointer(e))) + + return ret +} + +func (e *ICoreWebView2Environment3) Release() uintptr { + ret, _, _ := e.vtbl.Release.Call(uintptr(unsafe.Pointer(e))) + + return ret +} + +func (e *ICoreWebView2Environment) GetICoreWebView2Environment3() *ICoreWebView2Environment3 { + var result *ICoreWebView2Environment3 + + iidICoreWebView2Environment3 := NewGUID("{80a22ae3-be7c-4ce2-afe1-5a50056cdeeb}") + _, _, _ = e.vtbl.QueryInterface.Call( + uintptr(unsafe.Pointer(e)), + uintptr(unsafe.Pointer(iidICoreWebView2Environment3)), + uintptr(unsafe.Pointer(&result))) + + return result +} + +func (e *ICoreWebView2Environment3) CreateCoreWebView2CompositionController(parentWindow uintptr, handler *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler) error { + hr, _, _ := e.vtbl.CreateCoreWebView2CompositionController.Call( + uintptr(unsafe.Pointer(e)), + parentWindow, + uintptr(unsafe.Pointer(handler)), + ) + if windows.Handle(hr) != windows.S_OK { + return syscall.Errno(hr) + } + + return nil +} diff --git a/webview2/pkg/edge/ICoreWebView2Settings9.go b/webview2/pkg/edge/ICoreWebView2Settings9.go new file mode 100644 index 00000000000..e80ba2e0950 --- /dev/null +++ b/webview2/pkg/edge/ICoreWebView2Settings9.go @@ -0,0 +1,95 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type ICoreWebView2Settings9Vtbl struct { + _IUnknownVtbl + GetIsScriptEnabled ComProc + PutIsScriptEnabled ComProc + GetIsWebMessageEnabled ComProc + PutIsWebMessageEnabled ComProc + GetAreDefaultScriptDialogsEnabled ComProc + PutAreDefaultScriptDialogsEnabled ComProc + GetIsStatusBarEnabled ComProc + PutIsStatusBarEnabled ComProc + GetAreDevToolsEnabled ComProc + PutAreDevToolsEnabled ComProc + GetAreDefaultContextMenusEnabled ComProc + PutAreDefaultContextMenusEnabled ComProc + GetAreHostObjectsAllowed ComProc + PutAreHostObjectsAllowed ComProc + GetIsZoomControlEnabled ComProc + PutIsZoomControlEnabled ComProc + GetIsBuiltInErrorPageEnabled ComProc + PutIsBuiltInErrorPageEnabled ComProc + GetUserAgent ComProc + PutUserAgent ComProc + GetAreBrowserAcceleratorKeysEnabled ComProc + PutAreBrowserAcceleratorKeysEnabled ComProc + GetIsPasswordAutosaveEnabled ComProc + PutIsPasswordAutosaveEnabled ComProc + GetIsGeneralAutofillEnabled ComProc + PutIsGeneralAutofillEnabled ComProc + GetIsPinchZoomEnabled ComProc + PutIsPinchZoomEnabled ComProc + GetIsSwipeNavigationEnabled ComProc + PutIsSwipeNavigationEnabled ComProc + GetHiddenPdfToolbarItems ComProc + PutHiddenPdfToolbarItems ComProc + GetIsReputationCheckingRequired ComProc + PutIsReputationCheckingRequired ComProc + GetIsNonClientRegionSupportEnabled ComProc + PutIsNonClientRegionSupportEnabled ComProc +} + +type ICoreWebView2Settings9 struct { + Vtbl *ICoreWebView2Settings9Vtbl +} + +func (i *ICoreWebView2Settings9) AddRef() uintptr { + refCounter, _, _ := i.Vtbl.AddRef.Call(uintptr(unsafe.Pointer(i))) + return refCounter +} + +func (i *ICoreWebViewSettings) GetICoreWebView2Settings9() *ICoreWebView2Settings9 { + var result *ICoreWebView2Settings9 + + iidICoreWebView2Settings9 := NewGUID("{0528a73b-e92d-49f4-927a-e547dddaa37d}") + _, _, _ = i.vtbl.QueryInterface.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(iidICoreWebView2Settings9)), + uintptr(unsafe.Pointer(&result))) + + return result +} + +func (i *ICoreWebView2Settings9) GetIsNonClientRegionSupportEnabled() (bool, error) { + var value int32 + + hr, _, _ := i.Vtbl.GetIsNonClientRegionSupportEnabled.Call( + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(&value)), + ) + if windows.Handle(hr) != windows.S_OK { + return false, syscall.Errno(hr) + } + return value != 0, nil +} + +func (i *ICoreWebView2Settings9) PutIsNonClientRegionSupportEnabled(value bool) error { + hr, _, _ := i.Vtbl.PutIsNonClientRegionSupportEnabled.Call( + uintptr(unsafe.Pointer(i)), + uintptr(boolToInt(value)), + ) + if windows.Handle(hr) != windows.S_OK { + return syscall.Errno(hr) + } + return nil +} diff --git a/webview2/pkg/edge/IDCompositionDevice.go b/webview2/pkg/edge/IDCompositionDevice.go new file mode 100644 index 00000000000..f6910df3098 --- /dev/null +++ b/webview2/pkg/edge/IDCompositionDevice.go @@ -0,0 +1,86 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var procDCompositionCreateDevice2 = windows.NewLazySystemDLL("dcomp.dll").NewProc("DCompositionCreateDevice2") + +type iDCompositionDeviceVtbl struct { + _IUnknownVtbl + Commit ComProc + WaitForCommitCompletion ComProc + GetFrameStatistics ComProc + CreateTargetForHwnd ComProc + CreateVisual ComProc +} + +type iDCompositionDevice struct { + vtbl *iDCompositionDeviceVtbl +} + +func (d *iDCompositionDevice) AddRef() uintptr { + ret, _, _ := d.vtbl.AddRef.Call(uintptr(unsafe.Pointer(d))) + + return ret +} + +func (d *iDCompositionDevice) Release() uintptr { + ret, _, _ := d.vtbl.Release.Call(uintptr(unsafe.Pointer(d))) + + return ret +} + +func dCompositionCreateDevice2() (*iDCompositionDevice, error) { + var device *iDCompositionDevice + iidIDCompositionDevice := NewGUID("{C37EA93A-E7AA-450D-B16F-9746CB0407F3}") + + hr, _, _ := procDCompositionCreateDevice2.Call( + 0, + uintptr(unsafe.Pointer(iidIDCompositionDevice)), + uintptr(unsafe.Pointer(&device)), + ) + if windows.Handle(hr) != windows.S_OK { + return nil, syscall.Errno(hr) + } + return device, nil +} + +func (d *iDCompositionDevice) CreateTargetForHwnd(hwnd uintptr, topmost bool) (*iDCompositionTarget, error) { + var target *iDCompositionTarget + hr, _, _ := d.vtbl.CreateTargetForHwnd.Call( + uintptr(unsafe.Pointer(d)), + hwnd, + uintptr(boolToInt(topmost)), + uintptr(unsafe.Pointer(&target)), + ) + if windows.Handle(hr) != windows.S_OK { + return nil, syscall.Errno(hr) + } + return target, nil +} + +func (d *iDCompositionDevice) CreateVisual() (*iDCompositionVisual, error) { + var visual *iDCompositionVisual + hr, _, _ := d.vtbl.CreateVisual.Call( + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&visual)), + ) + if windows.Handle(hr) != windows.S_OK { + return nil, syscall.Errno(hr) + } + return visual, nil +} + +func (d *iDCompositionDevice) Commit() error { + hr, _, _ := d.vtbl.Commit.Call(uintptr(unsafe.Pointer(d))) + if windows.Handle(hr) != windows.S_OK { + return syscall.Errno(hr) + } + return nil +} diff --git a/webview2/pkg/edge/IDCompositionTarget.go b/webview2/pkg/edge/IDCompositionTarget.go new file mode 100644 index 00000000000..b5b1c131408 --- /dev/null +++ b/webview2/pkg/edge/IDCompositionTarget.go @@ -0,0 +1,42 @@ +//go:build windows + +package edge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type iDCompositionTargetVtbl struct { + _IUnknownVtbl + SetRoot ComProc +} + +type iDCompositionTarget struct { + vtbl *iDCompositionTargetVtbl +} + +func (t *iDCompositionTarget) AddRef() uintptr { + ret, _, _ := t.vtbl.AddRef.Call(uintptr(unsafe.Pointer(t))) + + return ret +} + +func (t *iDCompositionTarget) Release() uintptr { + ret, _, _ := t.vtbl.Release.Call(uintptr(unsafe.Pointer(t))) + + return ret +} + +func (t *iDCompositionTarget) SetRoot(visual *iDCompositionVisual) error { + hr, _, _ := t.vtbl.SetRoot.Call( + uintptr(unsafe.Pointer(t)), + uintptr(unsafe.Pointer(visual)), + ) + if windows.Handle(hr) != windows.S_OK { + return syscall.Errno(hr) + } + return nil +} diff --git a/webview2/pkg/edge/IDCompositionVisual.go b/webview2/pkg/edge/IDCompositionVisual.go new file mode 100644 index 00000000000..9e7444ab18d --- /dev/null +++ b/webview2/pkg/edge/IDCompositionVisual.go @@ -0,0 +1,25 @@ +//go:build windows + +package edge + +import "unsafe" + +type iDCompositionVisualVtbl struct { + _IUnknownVtbl +} + +type iDCompositionVisual struct { + vtbl *iDCompositionVisualVtbl +} + +func (v *iDCompositionVisual) AddRef() uintptr { + ret, _, _ := v.vtbl.AddRef.Call(uintptr(unsafe.Pointer(v))) + + return ret +} + +func (v *iDCompositionVisual) Release() uintptr { + ret, _, _ := v.vtbl.Release.Call(uintptr(unsafe.Pointer(v))) + + return ret +} diff --git a/webview2/pkg/edge/chromium.go b/webview2/pkg/edge/chromium.go index 2d6a9ceb10b..861d3c63f7c 100644 --- a/webview2/pkg/edge/chromium.go +++ b/webview2/pkg/edge/chromium.go @@ -56,26 +56,33 @@ type Chromium struct { } controller *ICoreWebView2Controller + compositionController *ICoreWebView2CompositionController + compositionController4 *ICoreWebView2CompositionController4 webview *ICoreWebView2 inited uintptr envCompleted *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler controllerCompleted *iCoreWebView2CreateCoreWebView2ControllerCompletedHandler + compositionControllerCompleted *iCoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler webMessageReceived *iCoreWebView2WebMessageReceivedEventHandler containsFullScreenElementChanged *ICoreWebView2ContainsFullScreenElementChangedEventHandler permissionRequested *iCoreWebView2PermissionRequestedEventHandler webResourceRequested *iCoreWebView2WebResourceRequestedEventHandler acceleratorKeyPressed *ICoreWebView2AcceleratorKeyPressedEventHandler + cursorChanged *iCoreWebView2CursorChangedEventHandler navigationCompleted *ICoreWebView2NavigationCompletedEventHandler processFailed *ICoreWebView2ProcessFailedEventHandler environment *ICoreWebView2Environment webview2RuntimeVersion string + compositionHost *compositionHost // Settings - Debug bool - DataPath string - BrowserPath string - AdditionalBrowserArgs []string + Debug bool + DataPath string + BrowserPath string + AdditionalBrowserArgs []string + NonClientRegionSupportEnabled bool + CompositionControllerEnabled bool // permissions permissions map[CoreWebView2PermissionKind]CoreWebView2PermissionState @@ -89,6 +96,7 @@ type Chromium struct { ProcessFailedCallback func(sender *ICoreWebView2, args *ICoreWebView2ProcessFailedEventArgs) ContainsFullScreenElementChangedCallback func(sender *ICoreWebView2, args *ICoreWebView2ContainsFullScreenElementChangedEventArgs) AcceleratorKeyCallback func(uint) bool + CursorChangedCallback func(cursor HCURSOR, systemCursorID uint32) // Error handling globalErrorCallback func(error) @@ -115,10 +123,12 @@ func NewChromium() *Chromium { */ e.envCompleted = newICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler(e) e.controllerCompleted = newICoreWebView2CreateCoreWebView2ControllerCompletedHandler(e) + e.compositionControllerCompleted = newICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler(e) e.webMessageReceived = newICoreWebView2WebMessageReceivedEventHandler(e) e.permissionRequested = newICoreWebView2PermissionRequestedEventHandler(e) e.webResourceRequested = newICoreWebView2WebResourceRequestedEventHandler(e) e.acceleratorKeyPressed = newICoreWebView2AcceleratorKeyPressedEventHandler(e) + e.cursorChanged = newICoreWebView2CursorChangedEventHandler(e) e.navigationCompleted = newICoreWebView2NavigationCompletedEventHandler(e) e.processFailed = newICoreWebView2ProcessFailedEventHandler(e) e.containsFullScreenElementChanged = newICoreWebView2ContainsFullScreenElementChangedEventHandler(e) @@ -158,6 +168,12 @@ func (e *Chromium) SetErrorCallback(callback func(error)) { } } +func (e *Chromium) SetCursorChangedCallback(callback func(cursor HCURSOR, systemCursorID uint32)) { + if callback != nil { + e.CursorChangedCallback = callback + } +} + func (e *Chromium) Embed(hwnd uintptr) bool { var err error @@ -317,10 +333,19 @@ func (e *Chromium) EnvironmentCompleted(res uintptr, env *ICoreWebView2Environme log.Printf("[WebView2] Environment created successfully\n") - env.vtbl.AddRef.Call(uintptr(unsafe.Pointer(env))) + env.AddRef() e.environment = env - err := env.CreateCoreWebView2Controller(e.hwnd, e.controllerCompleted) + var err error + if !e.CompositionControllerEnabled { + err = env.CreateCoreWebView2Controller(e.hwnd, e.controllerCompleted) + } else { + err = e.createCoreWebView2CompositionController(env) + if errors.Is(err, UnsupportedCapabilityError) { + e.CompositionControllerEnabled = false + err = env.CreateCoreWebView2Controller(e.hwnd, e.controllerCompleted) + } + } if err != nil { e.errorCallback(err) } @@ -332,9 +357,50 @@ func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller e.errorCallback(fmt.Errorf("error creating controller with %08x: %s", res, syscall.Errno(res))) } + return e.initializeController(controller) +} + +func (e *Chromium) createCoreWebView2CompositionController(env *ICoreWebView2Environment) error { + env3 := env.GetICoreWebView2Environment3() + if env3 == nil { + return UnsupportedCapabilityError + } + defer env3.Release() + + host, err := newCompositionHost(e.hwnd) + if err != nil { + return err + } + e.compositionHost = host + + return env3.CreateCoreWebView2CompositionController(e.hwnd, e.compositionControllerCompleted) +} + +func (e *Chromium) CreateCoreWebView2CompositionControllerCompleted(res uintptr, compositionController *ICoreWebView2CompositionController) uintptr { + if int32(res) < 0 { + e.errorCallback(fmt.Errorf("error creating composition controller with %08x: %s", res, syscall.Errno(res))) + } + + compositionController.AddRef() + e.compositionController = compositionController + e.compositionController4 = compositionController.GetICoreWebView2CompositionController4() + + if err := e.compositionHost.attachController(e.compositionController); err != nil { + e.errorCallback(err) + } + + controller := compositionController.GetICoreWebView2Controller() + if controller == nil { + e.errorCallback(fmt.Errorf("error getting controller from composition controller")) + } + + return e.initializeController(controller) +} + +func (e *Chromium) initializeController(controller *ICoreWebView2Controller) uintptr { var err error - controller.vtbl.AddRef.Call(uintptr(unsafe.Pointer(controller))) + controller.AddRef() e.controller = controller // Try to get ICoreWebView2Controller3 interface for better performance @@ -355,7 +421,14 @@ func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller e.errorCallback(err) } - e.webview.vtbl.AddRef.Call(uintptr(unsafe.Pointer(e.webview))) + e.webview.AddRef() + if e.NonClientRegionSupportEnabled { + if err := e.PutIsNonClientRegionSupportEnabled(true); err != nil { + if !errors.Is(err, UnsupportedCapabilityError) { + e.errorCallback(err) + } + } + } err = e.webview.AddWebMessageReceived(e.webMessageReceived, &token) if err != nil { e.errorCallback(err) @@ -380,11 +453,16 @@ func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller if err != nil { e.errorCallback(err) } - err = e.controller.AddAcceleratorKeyPressed(e.acceleratorKeyPressed, &token) if err != nil { e.errorCallback(err) } + if e.compositionController != nil { + err = e.compositionController.AddCursorChanged(e.cursorChanged, &token) + if err != nil { + e.errorCallback(err) + } + } atomic.StoreUintptr(&e.inited, 1) @@ -398,6 +476,27 @@ func (e *Chromium) ContainsFullScreenElementChanged(sender *ICoreWebView2, args return 0 } +func (e *Chromium) CursorChanged(sender *ICoreWebView2CompositionController, _ *IUnknown) uintptr { + if e.CursorChangedCallback == nil { + return 0 + } + + cursor, err := sender.GetCursor() + if err != nil { + e.errorCallback(err) + return 0 + } + + systemCursorID, err := sender.GetSystemCursorId() + if err != nil { + e.errorCallback(err) + return 0 + } + + e.CursorChangedCallback(cursor, systemCursorID) + return 0 +} + func (e *Chromium) MessageReceived(sender *ICoreWebView2, args *ICoreWebView2WebMessageReceivedEventArgs) uintptr { message, err := args.TryGetWebMessageAsString() if err != nil { @@ -560,6 +659,19 @@ func (e *Chromium) ProcessFailed(sender *ICoreWebView2, args *ICoreWebView2Proce return 0 } +func (e *Chromium) Bounds() *Rect { + if e == nil || e.controller == nil { + return nil + } + + rect, err := e.controller.GetBounds() + if err != nil { + e.errorCallback(err) + return nil + } + return rect +} + func (e *Chromium) NotifyParentWindowPositionChanged() error { //It looks like the wndproc function is called before the controller initialization is complete. //Because of this the controller is nil @@ -594,6 +706,16 @@ func (e *Chromium) HasCapability(c Capability) bool { return HasCapability(e.webview2RuntimeVersion, c) } +func (e *Chromium) CompositionControllerReady() bool { + return e != nil && e.compositionController != nil +} + +func (e *Chromium) NonClientRegionHitTestReady() bool { + return e != nil && + e.compositionController4 != nil && + HasCapability(e.webview2RuntimeVersion, NonClientRegion) +} + func (e *Chromium) GetIsSwipeNavigationEnabled() (bool, error) { if !HasCapability(e.webview2RuntimeVersion, SwipeNavigation) { return false, UnsupportedCapabilityError @@ -662,6 +784,46 @@ func (e *Chromium) PutIsSwipeNavigationEnabled(enabled bool) error { return nil } +func (e *Chromium) PutIsNonClientRegionSupportEnabled(enabled bool) error { + if !HasCapability(e.webview2RuntimeVersion, NonClientRegion) { + return UnsupportedCapabilityError + } + webview2Settings, err := e.webview.GetSettings() + if err != nil { + return err + } + webview2Settings9 := webview2Settings.GetICoreWebView2Settings9() + if webview2Settings9 == nil { + return UnsupportedCapabilityError + } + return webview2Settings9.PutIsNonClientRegionSupportEnabled(enabled) +} + +func (e *Chromium) GetNonClientRegionAtPoint(x, y int32) (COREWEBVIEW2_NON_CLIENT_REGION_KIND, bool, error) { + if !e.NonClientRegionHitTestReady() { + return COREWEBVIEW2_NON_CLIENT_REGION_KIND_NOWHERE, false, nil + } + + region, err := e.compositionController4.GetNonClientRegionAtPoint(POINT{X: x, Y: y}) + if err != nil { + return COREWEBVIEW2_NON_CLIENT_REGION_KIND_NOWHERE, false, err + } + return region, true, nil +} + +func (e *Chromium) SendMouseInput( + eventKind COREWEBVIEW2_MOUSE_EVENT_KIND, + virtualKeys COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS, + mouseData uint32, + x, y int, +) error { + if !e.CompositionControllerReady() { + return errors.New("webview2 composition controller is not initialized") + } + + return e.compositionController.SendMouseInput(eventKind, virtualKeys, mouseData, POINT{X: int32(x), Y: int32(y)}) +} + func (e *Chromium) AllowExternalDrag(allow bool) error { if !HasCapability(e.webview2RuntimeVersion, AllowExternalDrop) { return UnsupportedCapabilityError diff --git a/webview2/pkg/edge/com.go b/webview2/pkg/edge/com.go index 72cb64d9d12..43b1807f9ba 100644 --- a/webview2/pkg/edge/com.go +++ b/webview2/pkg/edge/com.go @@ -39,6 +39,11 @@ type IUnknownImpl interface { type POINT struct { X, Y int32 } + +func (p POINT) uintptr() uintptr { + return uintptr(uint32(p.X)) | uintptr(uint64(uint32(p.Y))<<32) +} + type RECT struct { Left int32 Top int32 diff --git a/webview2/pkg/edge/composition_host.go b/webview2/pkg/edge/composition_host.go new file mode 100644 index 00000000000..4b00ca8677f --- /dev/null +++ b/webview2/pkg/edge/composition_host.go @@ -0,0 +1,47 @@ +//go:build windows + +package edge + +import ( + "errors" + "unsafe" +) + +type compositionHost struct { + device *iDCompositionDevice + target *iDCompositionTarget + visual *iDCompositionVisual +} + +func newCompositionHost(hwnd uintptr) (*compositionHost, error) { + device, err := dCompositionCreateDevice2() + if err != nil { + return nil, err + } + target, err := device.CreateTargetForHwnd(hwnd, true) + if err != nil { + return nil, err + } + visual, err := device.CreateVisual() + if err != nil { + return nil, err + } + return &compositionHost{ + device: device, + target: target, + visual: visual, + }, nil +} + +func (h *compositionHost) attachController(controller *ICoreWebView2CompositionController) error { + if h == nil || h.device == nil || h.target == nil || h.visual == nil || controller == nil { + return errors.New("direct composition host is not initialized") + } + if err := controller.PutRootVisualTarget((*IUnknown)(unsafe.Pointer(h.visual))); err != nil { + return err + } + if err := h.target.SetRoot(h.visual); err != nil { + return err + } + return h.device.Commit() +} diff --git a/webview2/pkg/edge/corewebview2.go b/webview2/pkg/edge/corewebview2.go index 289c4aaf76b..ace3c4cd7dc 100644 --- a/webview2/pkg/edge/corewebview2.go +++ b/webview2/pkg/edge/corewebview2.go @@ -432,6 +432,18 @@ type ICoreWebView2Environment struct { vtbl *iCoreWebView2EnvironmentVtbl } +func (e *ICoreWebView2Environment) AddRef() uintptr { + ret, _, _ := e.vtbl.AddRef.Call(uintptr(unsafe.Pointer(e))) + + return ret +} + +func (e *ICoreWebView2Environment) Release() uintptr { + ret, _, _ := e.vtbl.Release.Call(uintptr(unsafe.Pointer(e))) + + return ret +} + // CreateCoreWebView2Controller asynchronously creates a new WebView. func (e *ICoreWebView2Environment) CreateCoreWebView2Controller(parentWindow uintptr, handler *iCoreWebView2CreateCoreWebView2ControllerCompletedHandler) error { hr, _, _ := e.vtbl.CreateCoreWebView2Controller.Call(