-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(windows): add support for custom hit-test logic for non-client regions #5462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
savely-krasovsky
wants to merge
19
commits into
wailsapp:master
Choose a base branch
from
savely-krasovsky:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
89e8d89
feat(windows): add support for custom hit-test logic for non-client r…
savely-krasovsky bc4951e
fix(windows): address review comments
savely-krasovsky 2519890
refactor: simplify non-client hit test handling logic
savely-krasovsky 819da92
fix: remove handling for non-client right mouse button events
savely-krasovsky 8182aa4
feat(windows): add cursor handling
savely-krasovsky a6b8b4c
feat(windows): handle right-click on window caption to display the sy…
savely-krasovsky c05ddde
fix(windows): enable non-client region tracking only is the feature i…
savely-krasovsky 2348d47
fix(windows): preserve native hover state for non-client buttons
savely-krasovsky 29e50ea
fix(windows): address review comments
savely-krasovsky 51ccf06
fix(runtime): rename app region css tracking internals
savely-krasovsky 202b03d
chore(windows): document non-client hit test ordering
savely-krasovsky 5266af5
feat(runtime): add runtime-config-ready event to improve initializati…
savely-krasovsky 38a4ca2
Merge branch 'master' into master
savely-krasovsky a1691fc
Merge branch 'master' into master
leaanthony 7038b1a
feat(windows): document `NonClientRegionSupport` and `WebView2Composi…
savely-krasovsky da3466b
docs: correct video asset path in frameless windows documentation
savely-krasovsky c655fe5
docs: clarify `WebView2CompositionHosting` experimental status
savely-krasovsky a87bf7a
docs: fix alignment issue in `WebView2CompositionHosting` example
savely-krasovsky b4d7717
docs: clarify Snap Layouts behavior and add reference to native non-c…
savely-krasovsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
222 changes: 222 additions & 0 deletions
222
v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| /* | ||
| _ __ _ __ | ||
| | | / /___(_) /____ | ||
| | | /| / / __ `/ / / ___/ | ||
| | |/ |/ / /_/ / / (__ ) | ||
| |__/|__/\__,_/_/_/____/ | ||
| The electron alternative for Go | ||
| (c) Lea Anthony 2019-present | ||
| */ | ||
|
|
||
| import { invoke } from "./system.js"; | ||
| import { whenReady } from "./utils.js"; | ||
|
|
||
| type AppRegionKind = "caption" | "minimize" | "maximize" | "close"; | ||
|
|
||
| interface AppRegion { | ||
| kind: AppRegionKind; | ||
| 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 validRegions = new Set<AppRegionKind>(["caption", "minimize", "maximize", "close"]); | ||
|
|
||
| // Setup | ||
| window._wails = window._wails || {}; | ||
|
|
||
| let updatePending = false; | ||
| let lastPayload = ""; | ||
| let observedElements = new Set<Element>(); | ||
| let resizeObserver: ResizeObserver | undefined; | ||
| let trackingStarted = false; | ||
|
|
||
| function normaliseRegionKind(value: string): AppRegionKind | undefined { | ||
| const region = value.trim().toLowerCase(); | ||
| if (validRegions.has(region as AppRegionKind)) { | ||
| return region as AppRegionKind; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| function appRegionForElement(element: Element): AppRegionKind | 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): AppRegion | undefined { | ||
| if (!(element instanceof HTMLElement)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const kind = appRegionForElement(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 startAppRegionTracking(). | ||
| 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 updateAppRegions(): void { | ||
| updatePending = false; | ||
|
|
||
| const elements = regionElements(); | ||
| const regions: AppRegion[] = []; | ||
| 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(updateAppRegions); | ||
| } | ||
|
|
||
| function startAppRegionTracking(): 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); | ||
| } | ||
|
|
||
| let environmentPolls = 0; | ||
| function tryStartAppRegionTracking(): void { | ||
| const os = window._wails.environment?.OS; | ||
| if (os === "windows") { | ||
| whenReady(startAppRegionTracking); | ||
| return; | ||
| } | ||
|
|
||
| if (os === undefined && environmentPolls++ < 100) { | ||
| // The runtime environment can arrive after this side-effect module loads. | ||
| window.setTimeout(tryStartAppRegionTracking, 50); | ||
| } | ||
| } | ||
|
|
||
| tryStartAppRegionTracking(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.