diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ccd8140c86..f9bb548843 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -49,6 +49,7 @@ body:
- PowerAccent
- PowerRename
- PowerToys Run
+ - Screen ruler
- Shortcut Guide
- STL Thumbnail
- SVG Preview
diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml
index 8b78313bb4..911f36344b 100644
--- a/.github/ISSUE_TEMPLATE/translation_issue.yml
+++ b/.github/ISSUE_TEMPLATE/translation_issue.yml
@@ -40,6 +40,7 @@ body:
- PowerAccent
- PowerRename
- PowerToys Run
+ - Screen Ruler
- Shortcut Guide
- SVG Preview
- SVG Thumbnail
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index a1c204c873..637c8da9d6 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -66,7 +66,6 @@ appdata
APPICON
appid
appium
-APPLASTZONE
Applets
Applicationcan
applicationframehost
@@ -121,7 +120,6 @@ AUTHN
AUTHZ
Autofill
autogenerate
-autogenerated
AUTOHIDE
AUTOMATIONPROPERTIES
Autorun
@@ -154,10 +152,13 @@ Bicubic
bigbar
bigobj
binlog
+BITMAPFILEHEADER
bitmapimage
BITMAPINFO
BITMAPINFOHEADER
+Bitmaps
bitmask
+BITSPIXEL
bla
Blockquotes
blog
@@ -266,6 +267,7 @@ CMINVOKECOMMANDINFOEX
CMock
CMONITORS
cmp
+cmpgt
cmyk
cnt
Cocklebiddy
@@ -309,6 +311,7 @@ CONFLICTINGMODIFIERSHORTCUT
CONOUT
Consolas
constexpr
+consts
contentdialog
contentfiles
CONTEXTHELP
@@ -368,13 +371,14 @@ customaction
CUSTOMACTIONTEST
cvd
CVirtual
+cvtepu
+cvtsi
cwchar
cwd
cxfksword
CXSMICON
CXVIRTUALSCREEN
cxxopts
-Cxxx
cyberrex
Cyrl
CYSMICON
@@ -383,6 +387,8 @@ cziplib
Dac
dacl
damienleroy
+DAffine
+DAFFINETRANSFORM
Danmarkshavn
DARKPURPLE
DARKTEAL
@@ -396,11 +402,15 @@ Dbg
Dbghelp
DBLCLKS
DBLEPSILON
+DCapture
+DCBA
DCOM
dcomp
dcompi
DComposition
+DDevice
ddf
+DDxgi
Deact
debian
debugbreak
@@ -463,6 +473,7 @@ dllexport
dllhost
dllmain
dlls
+Dmap
DNLEN
Dns
doctype
@@ -483,6 +494,7 @@ dreamsofameaningfullife
drivedetectionwarning
dshow
dst
+DTo
dutil
DVASPECT
DVASPECTINFO
@@ -528,6 +540,7 @@ ekus
elif
elseif
eltociear
+emmintrin
Emoji
emptyrecyclebin
ENABLEDPOPUP
@@ -544,6 +557,7 @@ enum
EOAC
EOL
epicgames
+epu
Eqn
ERASEBKGND
EREOF
@@ -618,8 +632,6 @@ finalizer
findfast
Firefox
FIXEDFILEINFO
-FLASHZONES
-FLASHZONESONQUICKSWITCH
flt
flyout
fmtlib
@@ -641,7 +653,6 @@ Functiondiscoverykeys
Futuna
fwlink
fwrite
-fxcop
FZE
gabime
GAC
@@ -666,12 +677,15 @@ GETMINMAXINFO
GETSTATE
GETTEXT
GETTEXTLENGTH
+GHND
github
githubusercontent
globals
+GMEM
GNumber
google
GPTR
+gpu
GSM
gtm
gui
@@ -710,6 +724,7 @@ helptext
Heure
HEVC
hfile
+HGFE
hglobal
hhk
HHmmss
@@ -756,6 +771,7 @@ hotlight
hotspot
Hovd
HPAINTBUFFER
+HPALETTE
HRAWINPUT
hread
HREDRAW
@@ -770,6 +786,7 @@ hsl
hstring
hsv
htcfreek
+HTCLIENT
HTHUMBNAIL
HTTRANSPARENT
HValue
@@ -811,6 +828,7 @@ IDD
IDelayed
IDesktop
IDictionary
+IDirect
IDirectory
IDispatch
IDispatcher
@@ -819,6 +837,7 @@ idl
IDLIST
IDOn
IDR
+IDrawing
IDrive
idx
IDXGI
@@ -847,6 +866,7 @@ IFormat
IFormatter
ifstream
IGraph
+IGraphics
iid
IImage
Iindex
@@ -1100,7 +1120,6 @@ LMEM
LMENU
lnk
LOADLIBRARYASDATAFILE
-LOADSTRING
LOBYTE
LOCALAPPDATA
LOCALDISPLAY
@@ -1123,6 +1142,7 @@ lookbehind
lowlevel
LOWORD
lparam
+LPBITMAPINFOHEADER
LPBYTE
LPCITEMIDLIST
LPCMINVOKECOMMANDINFO
@@ -1237,6 +1257,7 @@ MINIMIZEEND
MINIMIZESTART
miniz
minlevel
+minmax
MINORVERSION
Miracast
Mishkeegogamang
@@ -1265,11 +1286,9 @@ MOUSEACTIVATE
MOUSEHWHEEL
MOUSEINPUT
MOUSEMOVE
-MOUSESWITCH
MOUSEWHEEL
MOVESIZEEND
MOVESIZESTART
-MOVEWINDOWS
mozilla
mpmc
MRM
@@ -1366,6 +1385,7 @@ NOASYNC
NOCLOSEPROCESS
NOCOPYBITS
nodeca
+nodiscard
nodoc
noexcept
NOINHERITLAYOUT
@@ -1638,7 +1658,6 @@ QUERYENDSESSION
QUERYOPEN
QUEUESYNC
Quickime
-QUICKLAYOUTSWITCH
QUNS
qwertyuiopasdfghjklzxcvbnm
qword
@@ -1671,7 +1690,7 @@ rects
recyclebin
redirectedfrom
Redist
-Redistributable
+redistributable
reencode
reencoded
refactor
@@ -1711,7 +1730,6 @@ Resizable
resizers
resmimetype
RESOURCEID
-RESTORESIZE
RESTORETOMAXIMIZED
restrictedcapabilities
restrictederrorinfo
@@ -1739,6 +1757,7 @@ roadmap
robmensching
Roboto
rohanrdy
+Roolr
roslyn
Rothera
roundf
@@ -1812,6 +1831,7 @@ SETTINGCHANGE
settingsheader
settingshotkeycontrol
SETWORKAREA
+setzero
sfgao
SFGAOF
SFP
@@ -1827,7 +1847,6 @@ SHELLEXECUTEINFOW
shellscalingapi
SHFILEINFO
SHGFI
-SHIFTDRAG
Shl
shldisp
shlobj
@@ -1916,6 +1935,7 @@ srme
srre
srw
srwlock
+sse
ssf
ssh
sstream
@@ -1959,6 +1979,7 @@ stoull
strcmp
streampos
strftime
+strikethrough
Stringified
Stringify
STRINGIZE
@@ -1979,6 +2000,7 @@ subkey
SUBLANG
submenu
subquery
+subresource
substr
Sul
Superbar
@@ -2031,6 +2053,7 @@ taskkill
tasklist
taskschd
tchar
+Tcollab
tcscpy
TCustom
tdbuild
@@ -2043,7 +2066,6 @@ Tenggara
testcase
testhost
testprocess
-testzones
TEXCOORD
textblock
TEXTINCLUDE
@@ -2112,6 +2134,7 @@ uefi
UHash
UIA
uid
+UIEx
uint
uintptr
UIPI
@@ -2129,6 +2152,7 @@ undef
UNDNAME
unescape
Unicast
+UNICODETEXT
Unindent
Uninitialize
uninstall
@@ -2177,16 +2201,20 @@ Uvs
uwp
uxtheme
UYVY
+vabdq
validmodulename
Vanara
vcamp
vcdl
+vcgtq
VCINSTALLDIR
vcm
+Vcpkg
VCRT
vcvars
VDesktop
vdi
+vdupq
vec
VERBSONLY
VERBW
@@ -2196,6 +2224,8 @@ VERSIONINFO
Versioning
vformat
VFT
+vget
+vgetq
vid
VIDCAP
videoconference
@@ -2214,15 +2244,21 @@ visualstudio
viter
VKey
VKTAB
+vmovl
+vorrq
VOS
+vpaddlq
Vpn
+vqsubq
VREDRAW
+vreinterpretq
VSC
VSCBD
vscdb
vscode
vsconfig
VSCROLL
+vsetq
vsonline
vstemplate
VSTHRD
@@ -2241,11 +2277,13 @@ wchar
WClass
wcout
wcscat
+wcschr
wcscmp
wcscpy
wcslen
wcsncmp
wcsnicmp
+wcsstr
wdp
wdupenv
weakme
@@ -2307,6 +2345,7 @@ winsdkver
winspool
WINTHRESHOLD
winui
+winuiex
winxamlmanager
wistd
withinrafael
@@ -2383,6 +2422,7 @@ XLoc
XNamespace
XOffset
xpath
+XPixel
XResource
xsi
XStr
@@ -2403,10 +2443,7 @@ ZEROINIT
ZIndex
zipfile
zonable
-ZONECOLOR
-ZONEHIGHLIGHTCOLOR
zoneset
-ZONESETCHANGE
Zoneszonabletester
Zonev
zzz
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index cd26688338..58c63a041d 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -102,6 +102,11 @@
"modules\\launcher\\Plugins\\WebSearch\\Community.PowerToys.Run.Plugin.WebSearch.dll",
"modules\\launcher\\Plugins\\WindowsTerminal\\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.dll",
+ "modules\\MeasureTool\\PowerToys.MeasureToolModuleInterface.dll",
+ "modules\\MeasureTool\\PowerToys.MeasureToolCore.dll",
+ "modules\\MeasureTool\\PowerToys.MeasureToolUI.dll",
+ "modules\\MeasureTool\\PowerToys.MeasureToolUI.exe",
+
"modules\\MouseUtils\\PowerToys.FindMyMouse.dll",
"modules\\MouseUtils\\PowerToys.MouseHighlighter.dll",
"modules\\MouseUtils\\PowerToys.MousePointerCrosshairs.dll",
diff --git a/.pipelines/release.yml b/.pipelines/release.yml
index a2a1b08887..316f2fcaed 100644
--- a/.pipelines/release.yml
+++ b/.pipelines/release.yml
@@ -279,6 +279,22 @@ jobs:
configuration: $(BuildConfiguration)
maximumCpuCount: true
+ - task: VSBuild@1
+ displayName: Publish Measure Tool UI for Packaging
+ inputs:
+ solution: 'src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj'
+ vsVersion: 17.0
+ # The arguments should be the same as the ones for Settings; make sure they are.
+ msbuildArgs: >-
+ /target:Publish
+ /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never
+ /p:VCRTForwarders-IncludeDebugCRT=false
+ /p:PowerToysRoot=$(Build.SourcesDirectory)
+ /p:PublishProfile=InstallationPublishProfile.pubxml
+ platform: $(BuildPlatform)
+ configuration: $(BuildConfiguration)
+ maximumCpuCount: true
+
- task: VSBuild@1
displayName: Build PowerToysSetupCustomActions DLL # This dll needs to be build and signed before building the MSI.
inputs:
diff --git a/.vsconfig b/.vsconfig
index d4cd533958..cc03dca426 100644
--- a/.vsconfig
+++ b/.vsconfig
@@ -6,7 +6,8 @@
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.ManagedDesktop",
"Microsoft.VisualStudio.Workload.Universal",
- "Microsoft.VisualStudio.Component.Windows10SDK.18362",
+ "Microsoft.VisualStudio.Component.Windows10SDK.19041",
+ "Microsoft.VisualStudio.Component.Windows10SDK.20348",
"Microsoft.VisualStudio.ComponentGroup.UWP.VC",
"Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre",
"Microsoft.VisualStudio.Component.VC.ATL.Spectre"
diff --git a/Cpp.Build.props b/Cpp.Build.props
index b58ed24173..da85f4fb56 100644
--- a/Cpp.Build.props
+++ b/Cpp.Build.props
@@ -12,14 +12,14 @@
Release
x64
-
- Debug
- ARM64
-
-
- Release
- ARM64
-
+
+ Debug
+ ARM64
+
+
+ Release
+ ARM64
+
@@ -32,11 +32,12 @@
+ false
$(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath)
- pch.h
+ pch.h
Level3
true
TurnOffAllWarnings
@@ -83,8 +84,10 @@
-
+
10.0.19041.0
+ 10.0.19041.0
@@ -92,14 +95,17 @@
v143
$(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\
Unicode
+ true
-
+
true
true
-
+
false
true
false
diff --git a/NOTICE.md b/NOTICE.md
index cd51c99678..600a7261dc 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -6,6 +6,7 @@ This software incorporates material from third parties.
- ImageResizer
- PowerToys Run
- Installer/Runner
+- Measure tool
## Utility: Color Picker
@@ -274,3 +275,26 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to
+## Utility: Measure tool
+
+### sse2neon
+We adopted some functions from it.
+
+**Source**: https://github.com/DLTcollab/sse2neon
+
+sse2neon is freely redistributable under the MIT License.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PowerToys.sln b/PowerToys.sln
index 362efea911..e45815efc7 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -284,6 +284,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\process_path.h = src\common\utils\process_path.h
src\common\utils\registry.h = src\common\utils\registry.h
src\common\utils\resources.h = src\common\utils\resources.h
+ src\common\utils\serialized.h = src\common\utils\serialized.h
src\common\utils\string_utils.h = src\common\utils\string_utils.h
src\common\utils\timeutil.h = src\common\utils\timeutil.h
src\common\utils\UnhandledExceptionHandler.h = src\common\utils\UnhandledExceptionHandler.h
@@ -433,6 +434,19 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerOCRModuleInterface", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.History", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.History\Microsoft.PowerToys.Run.Plugin.History.csproj", "{212AD910-8488-4036-BE20-326931B75FB2}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MeasureTool", "MeasureTool", "{7AC943C9-52E8-44CF-9083-744D8049667B}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.MeasureToolCore", "src\modules\MeasureTool\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj", "{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}"
+ ProjectSection(ProjectDependencies) = postProject
+ {6955446D-23F7-4023-9BB3-8657F904AF99} = {6955446D-23F7-4023-9BB3-8657F904AF99}
+ {CABA8DFB-823B-4BF2-93AC-3F31984150D9} = {CABA8DFB-823B-4BF2-93AC-3F31984150D9}
+ {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} = {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}
+ EndProjectSection
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MeasureToolModuleInterface", "src\modules\MeasureTool\MeasureToolModuleInterface\MeasureToolModuleInterface.vcxproj", "{92C39820-9F84-4529-BC7D-22AAE514D63B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeasureToolUI", "src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj", "{515554D1-D004-4F7F-A107-2211FC0F6B2C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -1734,6 +1748,48 @@ Global
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.Build.0 = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.ActiveCfg = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.Build.0 = Release|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.ActiveCfg = Debug|arm64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.Build.0 = Debug|arm64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.ActiveCfg = Debug|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.Build.0 = Debug|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x86.ActiveCfg = Debug|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x86.Build.0 = Debug|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.ActiveCfg = Release|arm64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.Build.0 = Release|arm64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.ActiveCfg = Release|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.Build.0 = Release|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x86.ActiveCfg = Release|x64
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x86.Build.0 = Release|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.Build.0 = Debug|ARM64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.ActiveCfg = Debug|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.Build.0 = Debug|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x86.ActiveCfg = Debug|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x86.Build.0 = Debug|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.ActiveCfg = Release|ARM64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.Build.0 = Release|ARM64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.ActiveCfg = Release|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.Build.0 = Release|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x86.ActiveCfg = Release|x64
+ {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x86.Build.0 = Release|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.ActiveCfg = Debug|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.Build.0 = Debug|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.Deploy.0 = Debug|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.ActiveCfg = Debug|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.Build.0 = Debug|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.Deploy.0 = Debug|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.ActiveCfg = Debug|x86
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.Build.0 = Debug|x86
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.Deploy.0 = Debug|x86
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.ActiveCfg = Release|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.Build.0 = Release|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.Deploy.0 = Release|arm64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.ActiveCfg = Release|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.Build.0 = Release|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.Deploy.0 = Release|x64
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.ActiveCfg = Release|x86
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.Build.0 = Release|x86
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1881,6 +1937,10 @@ Global
{25C91A4E-BA4E-467A-85CD-8B62545BF674} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{6AB6A2D6-F859-4A82-9184-0BD29C9F07D1} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{212AD910-8488-4036-BE20-326931B75FB2} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
+ {7AC943C9-52E8-44CF-9083-744D8049667B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A} = {7AC943C9-52E8-44CF-9083-744D8049667B}
+ {92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B}
+ {515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/README.md b/README.md
index 00a5bc41d1..e9170ecda2 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@ This is a lighter release, with a shorter development cycle and focused on stabi
- There are reports of users who are [unable to open the Settings window](https://github.com/microsoft/PowerToys/issues/18015). This is being caused by incompatibilities with some applications (RTSS RivaTuner Statistics Server and MSI AfterBurner are known examples of this). If you're affected by this, please check the linked issue to verify if any of the presented solutions works for you.
### General
-- Upgraded the Windows App SDK runtimes to 1.1.2.
+- Upgraded the Windows App SDK runtimes to 1.1.4.
- The new Windows 11 context menu entries are now correctly added to Windows 11 dev channel insider builds. (This was a hotfix for 0.60)
- The old context menu entries are shown alongside the new Windows 11 context menu entries to be compatible with software that overrides the Windows 11 context menu behavior. (This was a hotfix for 0.60)
- Consolidated C# language version across the solution. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index f98c636c51..95716467db 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -14,6 +14,7 @@
+
@@ -50,11 +51,11 @@
-
+
-
+
-
+
@@ -118,6 +119,9 @@
+
+
+
@@ -461,6 +465,10 @@
+
+
+
+
@@ -961,6 +969,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1103,6 +1125,11 @@
+
+
+
+
+
diff --git a/installer/PowerToysSetup/publish.cmd b/installer/PowerToysSetup/publish.cmd
index 7e30f605c2..daebe026d4 100644
--- a/installer/PowerToysSetup/publish.cmd
+++ b/installer/PowerToysSetup/publish.cmd
@@ -18,3 +18,5 @@ msbuild !PTRoot!\src\modules\previewpane\MarkdownPreviewHandler\MarkdownPreviewH
msbuild !PTRoot!\src\modules\previewpane\SvgPreviewHandler\SvgPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml
msbuild !PTRoot!\src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvider.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml
+
+msbuild !PTRoot!\src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml
diff --git a/src/common/Display/monitors.cpp b/src/common/Display/monitors.cpp
index 39ce11462c..0dae6adbaa 100644
--- a/src/common/Display/monitors.cpp
+++ b/src/common/Display/monitors.cpp
@@ -2,72 +2,53 @@
#include
-namespace
+Box MonitorInfo::GetScreenSize(const bool includeNonWorkingArea) const
{
- // TODO: use compare
- bool operator<(const RECT& lhs, const RECT& rhs)
- {
- auto lhs_tuple = std::make_tuple(lhs.left, lhs.right, lhs.top, lhs.bottom);
- auto rhs_tuple = std::make_tuple(rhs.left, rhs.right, rhs.top, rhs.bottom);
- return lhs_tuple < rhs_tuple;
- }
+ return includeNonWorkingArea ? Box{ info.rcMonitor } : Box{ info.rcWork };
}
-bool operator==(const ScreenSize& lhs, const ScreenSize& rhs)
+bool MonitorInfo::IsPrimary() const
{
- auto lhs_tuple = std::make_tuple(lhs.rect.left, lhs.rect.right, lhs.rect.top, lhs.rect.bottom);
- auto rhs_tuple = std::make_tuple(rhs.rect.left, rhs.rect.right, rhs.rect.top, rhs.rect.bottom);
- return lhs_tuple == rhs_tuple;
+ return static_cast(info.dwFlags & MONITORINFOF_PRIMARY);
+}
+
+MonitorInfo::MonitorInfo(HMONITOR h) :
+ handle{ h }
+{
+ info.cbSize = sizeof(MONITORINFOEX);
+ GetMonitorInfoW(handle, &info);
}
static BOOL CALLBACK GetDisplaysEnumCb(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
{
- MONITORINFOEX monitorInfo;
- monitorInfo.cbSize = sizeof(MONITORINFOEX);
- if (GetMonitorInfo(monitor, &monitorInfo))
- {
- reinterpret_cast*>(data)->emplace_back(monitor, monitorInfo.rcWork);
- }
- return true;
-};
-
-static BOOL CALLBACK GetDisplaysEnumCbWithNonWorkingArea(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
-{
- MONITORINFOEX monitorInfo;
- monitorInfo.cbSize = sizeof(MONITORINFOEX);
- if (GetMonitorInfo(monitor, &monitorInfo))
- {
- reinterpret_cast*>(data)->emplace_back(monitor, monitorInfo.rcMonitor);
- }
+ auto* monitors = reinterpret_cast*>(data);
+ monitors->emplace_back(monitor);
return true;
};
std::vector MonitorInfo::GetMonitors(bool includeNonWorkingArea)
{
std::vector monitors;
- EnumDisplayMonitors(NULL, NULL, includeNonWorkingArea ? GetDisplaysEnumCbWithNonWorkingArea : GetDisplaysEnumCb, reinterpret_cast(&monitors));
- std::sort(begin(monitors), end(monitors), [](const MonitorInfo& lhs, const MonitorInfo& rhs) {
- return lhs.rect < rhs.rect;
+ EnumDisplayMonitors(nullptr, nullptr, GetDisplaysEnumCb, reinterpret_cast(&monitors));
+ std::sort(begin(monitors), end(monitors), [=](const MonitorInfo& lhs, const MonitorInfo& rhs) {
+ const auto lhsSize = lhs.GetScreenSize(includeNonWorkingArea);
+ const auto rhsSize = rhs.GetScreenSize(includeNonWorkingArea);
+
+ return lhsSize < rhsSize;
});
return monitors;
}
-static BOOL CALLBACK GetPrimaryDisplayEnumCb(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
-{
- MONITORINFOEX monitorInfo;
- monitorInfo.cbSize = sizeof(MONITORINFOEX);
-
- if (GetMonitorInfo(monitor, &monitorInfo) && (monitorInfo.dwFlags & MONITORINFOF_PRIMARY))
- {
- reinterpret_cast(data)->handle = monitor;
- reinterpret_cast(data)->rect = monitorInfo.rcWork;
- }
- return true;
-};
-
MonitorInfo MonitorInfo::GetPrimaryMonitor()
{
- MonitorInfo primary({}, {});
- EnumDisplayMonitors(NULL, NULL, GetPrimaryDisplayEnumCb, reinterpret_cast(&primary));
- return primary;
+ auto monitors = MonitorInfo::GetMonitors(false);
+ if (monitors.size() > 1)
+ {
+ for (auto monitor : monitors)
+ {
+ if (monitor.IsPrimary())
+ return monitor;
+ }
+ }
+ return monitors[0];
}
diff --git a/src/common/Display/monitors.h b/src/common/Display/monitors.h
index 7771e843d5..021f2b9efb 100644
--- a/src/common/Display/monitors.h
+++ b/src/common/Display/monitors.h
@@ -1,12 +1,20 @@
#pragma once
#include
+
+#include
+#include
#include
-struct ScreenSize
+// TODO: merge with FZ::Rect
+struct Box
{
- explicit ScreenSize(RECT rect) :
- rect(rect) {}
RECT rect;
+
+ explicit Box(RECT rect = {}) :
+ rect(rect) {}
+ Box(const Box&) = default;
+ Box& operator=(const Box&) = default;
+
int left() const { return rect.left; }
int right() const { return rect.right; }
int top() const { return rect.top; }
@@ -22,17 +30,31 @@ struct ScreenSize
POINT bottom_left() const { return { rect.left, rect.bottom }; };
POINT bottom_middle() const { return { rect.left + width() / 2, rect.bottom }; };
POINT bottom_right() const { return { rect.right, rect.bottom }; };
+ inline bool inside(const POINT point) const { return PtInRect(&rect, point); }
+
+ inline friend auto operator<=>(const Box& lhs, const Box& rhs)
+ {
+ auto lhs_tuple = std::make_tuple(lhs.rect.left, lhs.rect.right, lhs.rect.top, lhs.rect.bottom);
+ auto rhs_tuple = std::make_tuple(rhs.rect.left, rhs.rect.right, rhs.rect.top, rhs.rect.bottom);
+ return lhs_tuple <=> rhs_tuple;
+ }
};
-struct MonitorInfo : ScreenSize
+class MonitorInfo
{
- explicit MonitorInfo(HMONITOR monitor, RECT rect) :
- handle(monitor), ScreenSize(rect) {}
HMONITOR handle;
+ MONITORINFOEX info = {};
+
+public:
+ explicit MonitorInfo(HMONITOR h);
+ inline HMONITOR GetHandle() const
+ {
+ return handle;
+ }
+ Box GetScreenSize(const bool includeNonWorkingArea) const;
+ bool IsPrimary() const;
// Returns monitor rects ordered from left to right
static std::vector GetMonitors(bool includeNonWorkingArea);
static MonitorInfo GetPrimaryMonitor();
};
-
-bool operator==(const ScreenSize& lhs, const ScreenSize& rhs);
diff --git a/src/common/utils/serialized.h b/src/common/utils/serialized.h
new file mode 100644
index 0000000000..4611c08358
--- /dev/null
+++ b/src/common/utils/serialized.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include
+#include
+
+template
+class Serialized
+{
+ mutable std::shared_mutex m;
+ StateT s;
+
+public:
+ void Read(std::function fn) const
+ {
+ std::shared_lock lock{ m };
+ fn(s);
+ }
+
+ void Access(std::function fn)
+ {
+ std::unique_lock lock{ m };
+ fn(s);
+ }
+
+ void Reset()
+ {
+ std::unique_lock lock{ m };
+ s = {};
+ }
+};
diff --git a/src/common/utils/winapi_error.h b/src/common/utils/winapi_error.h
index b97d5c6450..b4cc01aa83 100644
--- a/src/common/utils/winapi_error.h
+++ b/src/common/utils/winapi_error.h
@@ -25,7 +25,7 @@ inline std::optional get_last_error_message(const DWORD dw)
inline std::wstring get_last_error_or_default(const DWORD dw)
{
auto message = get_last_error_message(dw);
- return message.has_value() ? message.value() : L"";
+ return message.has_value() ? *message : L"";
}
inline void show_last_error_message(const wchar_t* functionName, DWORD dw, const wchar_t* errorTitle)
diff --git a/src/common/utils/window.h b/src/common/utils/window.h
index 1f33109048..36e4b671c3 100644
--- a/src/common/utils/window.h
+++ b/src/common/utils/window.h
@@ -8,24 +8,24 @@
#include
// Initializes and runs windows message loop
-inline int run_message_loop(const bool until_idle = false, const std::optional timeout_seconds = {})
+inline int run_message_loop(const bool until_idle = false, const std::optional timeout_ms = {})
{
MSG msg{};
bool stop = false;
UINT_PTR timerId = 0;
- if (timeout_seconds.has_value())
+ if (timeout_ms.has_value())
{
- timerId = SetTimer(nullptr, 0, *timeout_seconds * 1000, nullptr);
+ timerId = SetTimer(nullptr, 0, *timeout_ms, nullptr);
}
- while (!stop && GetMessageW(&msg, nullptr, 0, 0))
+ while (!stop && (until_idle ? PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE) : GetMessageW(&msg, nullptr, 0, 0)))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
stop = until_idle && !PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE);
stop = stop || (msg.message == WM_TIMER && msg.wParam == timerId);
}
- if (timeout_seconds.has_value())
+ if (timeout_ms.has_value())
{
KillTimer(nullptr, timerId);
}
@@ -55,3 +55,24 @@ inline bool is_system_window(HWND hwnd, const char* class_name)
}
return false;
}
+
+template
+inline T GetWindowCreateParam(LPARAM lparam)
+{
+ static_assert(sizeof(T) <= sizeof(void*));
+ T data{ (T)(reinterpret_cast(lparam)->lpCreateParams) };
+ return data;
+}
+
+template
+inline void StoreWindowParam(HWND window, T data)
+{
+ static_assert(sizeof(T) <= sizeof(void*));
+ SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(data));
+}
+
+template
+inline T GetWindowParam(HWND window)
+{
+ return (T)GetWindowLongPtrW(window, GWLP_USERDATA);
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.cpp b/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.cpp
new file mode 100644
index 0000000000..c73faeeb00
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.cpp
@@ -0,0 +1,73 @@
+#include "pch.h"
+
+#include "BGRATextureView.h"
+
+#if defined(DEBUG_TEXTURE)
+void BGRATextureView::SaveAsBitmap(const char* filename) const
+{
+ wil::unique_hbitmap bitmap{ CreateBitmap(static_cast(pitch), static_cast(height), 1, 32, pixels) };
+ const HBITMAP hBitmap = bitmap.get();
+ DWORD dwPaletteSize = 0, dwBmBitsSize = 0, dwDIBSize = 0, dwWritten = 0;
+ LPBITMAPINFOHEADER lpBitmapInfo;
+ HANDLE hDib, hPal, hOldPal2 = NULL;
+ HDC hDC = CreateDC(TEXT("DISPLAY"), NULL, NULL, NULL);
+ const int iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
+ DeleteDC(hDC);
+ WORD wBitCount = 24;
+ if (iBits <= 1)
+ wBitCount = 1;
+ else if (iBits <= 4)
+ wBitCount = 4;
+ else if (iBits <= 8)
+ wBitCount = 8;
+
+ BITMAP Bitmap0;
+ GetObject(hBitmap, sizeof(Bitmap0), (LPSTR)&Bitmap0);
+ BITMAPINFOHEADER bi = {};
+ bi.biSize = sizeof(BITMAPINFOHEADER);
+ bi.biWidth = Bitmap0.bmWidth;
+ bi.biHeight = -Bitmap0.bmHeight;
+ bi.biPlanes = 1;
+ bi.biBitCount = wBitCount;
+ bi.biCompression = BI_RGB;
+ bi.biClrUsed = 256;
+ dwBmBitsSize = ((Bitmap0.bmWidth * wBitCount + 31) & ~31) / 8 * Bitmap0.bmHeight;
+ hDib = GlobalAlloc(GHND, dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
+ lpBitmapInfo = (LPBITMAPINFOHEADER)GlobalLock(hDib);
+ *lpBitmapInfo = bi;
+
+ hPal = GetStockObject(DEFAULT_PALETTE);
+ if (hPal)
+ {
+ hDC = GetDC(NULL);
+ hOldPal2 = SelectPalette(hDC, (HPALETTE)hPal, FALSE);
+ RealizePalette(hDC);
+ }
+
+ GetDIBits(hDC, hBitmap, 0, (UINT)Bitmap0.bmHeight, (LPSTR)lpBitmapInfo + sizeof(BITMAPINFOHEADER) + dwPaletteSize, (BITMAPINFO*)lpBitmapInfo, DIB_RGB_COLORS);
+
+ if (hOldPal2)
+ {
+ SelectPalette(hDC, (HPALETTE)hOldPal2, TRUE);
+ RealizePalette(hDC);
+ ReleaseDC(NULL, hDC);
+ }
+
+ wil::unique_handle fh{ CreateFileA(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL) };
+
+ if (!fh)
+ return;
+
+ BITMAPFILEHEADER bitmapFileHeader = {};
+ bitmapFileHeader.bfType = 0x4D42; // "BM"
+ dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
+ bitmapFileHeader.bfSize = dwDIBSize;
+ bitmapFileHeader.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize;
+
+ WriteFile(fh.get(), (LPSTR)&bitmapFileHeader, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
+
+ WriteFile(fh.get(), (LPSTR)lpBitmapInfo, dwDIBSize, &dwWritten, NULL);
+ GlobalUnlock(hDib);
+ GlobalFree(hDib);
+}
+#endif
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.h b/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.h
new file mode 100644
index 0000000000..18ceaeab45
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/BGRATextureView.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include
+#include
+#ifdef _M_ARM64
+#include
+#else
+#include
+#endif
+#include
+#include
+
+//#define DEBUG_TEXTURE
+
+#if defined(_M_ARM64)
+
+// Adopted from https://github.com/DLTcollab/sse2neon/blob/master/sse2neon.h
+
+using __m128i = int64x2_t;
+
+inline __m128i _mm_cvtsi32_si128(int a)
+{
+ return vreinterpretq_s64_s32(vsetq_lane_s32(a, vdupq_n_s32(0), 0));
+}
+
+inline __m128i _mm_or_si128(__m128i a, __m128i b)
+{
+ return vreinterpretq_s64_s32(
+ vorrq_s32(vreinterpretq_s32_s64(a), vreinterpretq_s32_s64(b)));
+}
+
+inline __m128i _mm_subs_epu8(__m128i a, __m128i b)
+{
+ return vreinterpretq_s64_u8(
+ vqsubq_u8(vreinterpretq_u8_s64(a), vreinterpretq_u8_s64(b)));
+}
+
+inline __m128i _mm_sad_epu8(__m128i a, __m128i b)
+{
+ uint16x8_t t = vpaddlq_u8(vabdq_u8((uint8x16_t)a, (uint8x16_t)b));
+ return vreinterpretq_s64_u64(vpaddlq_u32(vpaddlq_u16(t)));
+}
+
+inline __m128i _mm_setzero_si128(void)
+{
+ return vreinterpretq_s64_s32(vdupq_n_s32(0));
+}
+
+inline int _mm_cvtsi128_si32(__m128i a)
+{
+ return vgetq_lane_s32(vreinterpretq_s32_s64(a), 0);
+}
+
+inline __m128i _mm_set1_epi16(short w)
+{
+ return vreinterpretq_s64_s16(vdupq_n_s16(w));
+}
+
+inline __m128i _mm_cmpgt_epi16(__m128i a, __m128i b)
+{
+ return vreinterpretq_s64_u16(
+ vcgtq_s16(vreinterpretq_s16_s64(a), vreinterpretq_s16_s64(b)));
+}
+
+inline __m128i _mm_cvtepu8_epi16(__m128i a)
+{
+ uint8x16_t u8x16 = vreinterpretq_u8_s64(a); /* xxxx xxxx HGFE DCBA */
+ uint16x8_t u16x8 = vmovl_u8(vget_low_u8(u8x16)); /* 0H0G 0F0E 0D0C 0B0A */
+ return vreinterpretq_s64_u16(u16x8);
+}
+
+inline int64_t _mm_cvtsi128_si64(__m128i a)
+{
+ return vgetq_lane_s64(a, 0);
+}
+#endif
+
+inline __m128i distance_epu8(const __m128i a, __m128i b)
+{
+ return _mm_or_si128(_mm_subs_epu8(a, b),
+ _mm_subs_epu8(b, a));
+}
+
+struct BGRATextureView
+{
+ const uint32_t* pixels = nullptr;
+ size_t pitch = {};
+ size_t width = {};
+ size_t height = {};
+
+ BGRATextureView() = default;
+
+ BGRATextureView(BGRATextureView&& rhs) = default;
+
+ inline uint32_t GetPixel(const size_t x, const size_t y) const
+ {
+ assert(x < width && x >= 0);
+ assert(y < height && y >= 0);
+ return pixels[x + pitch * y];
+ }
+
+ template
+ static inline bool PixelsClose(const uint32_t pixel1, const uint32_t pixel2, uint8_t tolerance)
+ {
+ const __m128i rgba1 = _mm_cvtsi32_si128(pixel1);
+ const __m128i rgba2 = _mm_cvtsi32_si128(pixel2);
+ const __m128i distances = distance_epu8(rgba1, rgba2);
+
+ // Method 1: Test whether each channel distance is not greater than tolerance
+ if constexpr (perChannel)
+ {
+ const __m128i tolerances = _mm_set1_epi16(tolerance);
+ const auto gtResults128 = _mm_cmpgt_epi16(_mm_cvtepu8_epi16(distances), tolerances);
+ return _mm_cvtsi128_si64(gtResults128) == 0;
+ }
+ else
+ {
+ // Method 2: Test whether sum of all channel differences is smaller than tolerance
+ const int32_t score = _mm_cvtsi128_si32(_mm_sad_epu8(distances, _mm_setzero_si128())) & std::numeric_limits::max();
+ return score <= tolerance;
+ }
+ }
+
+#if defined(DEBUG_TEXTURE)
+ void SaveAsBitmap(const char* filename) const;
+#endif
+};
diff --git a/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp
new file mode 100644
index 0000000000..02b5872b73
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp
@@ -0,0 +1,161 @@
+#include "pch.h"
+#include "BoundsToolOverlayUI.h"
+#include "CoordinateSystemConversion.h"
+#include "Clipboard.h"
+
+#include
+
+LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
+{
+ switch (message)
+ {
+ case WM_CREATE:
+ {
+ auto toolState = GetWindowCreateParam(lparam);
+ StoreWindowParam(window, toolState);
+ break;
+ }
+ case WM_ERASEBKGND:
+ return 1;
+ case WM_KEYUP:
+ if (wparam == VK_ESCAPE)
+ {
+ PostMessageW(window, WM_CLOSE, {}, {});
+ }
+ break;
+ case WM_LBUTTONDOWN:
+ {
+ auto toolState = GetWindowParam(window);
+ if (!toolState)
+ break;
+ const POINT cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
+
+ D2D_POINT_2F newRegionStart = { .x = static_cast(cursorPos.x), .y = static_cast(cursorPos.y) };
+ toolState->perScreen[window].currentRegionStart = newRegionStart;
+ break;
+ }
+ case WM_CURSOR_LEFT_MONITOR:
+ {
+ auto toolState = GetWindowParam(window);
+ if (!toolState)
+ break;
+ toolState->perScreen[window].currentRegionStart = std::nullopt;
+ break;
+ }
+ case WM_LBUTTONUP:
+ {
+ auto toolState = GetWindowParam(window);
+ if (!toolState || !toolState->perScreen[window].currentRegionStart)
+ break;
+
+ toolState->commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
+ SetClipBoardToText(text.buffer);
+ });
+
+ if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress)
+ {
+ const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
+
+ D2D1_RECT_F rect;
+ std::tie(rect.left, rect.right) = std::minmax(static_cast(cursorPos.x), toolState->perScreen[window].currentRegionStart->x);
+ std::tie(rect.top, rect.bottom) = std::minmax(static_cast(cursorPos.y), toolState->perScreen[window].currentRegionStart->y);
+ toolState->perScreen[window].measurements.push_back(rect);
+ }
+
+ toolState->perScreen[window].currentRegionStart = std::nullopt;
+ break;
+ }
+ case WM_RBUTTONUP:
+ {
+ auto toolState = GetWindowParam(window);
+ if (!toolState)
+ break;
+
+ if (toolState->perScreen[window].currentRegionStart)
+ toolState->perScreen[window].currentRegionStart = std::nullopt;
+ else
+ {
+ if (toolState->perScreen[window].measurements.empty())
+ PostMessageW(window, WM_CLOSE, {}, {});
+ else
+ toolState->perScreen[window].measurements.clear();
+ }
+ break;
+ }
+ }
+
+ return DefWindowProcW(window, message, wparam, lparam);
+}
+
+namespace
+{
+ void DrawMeasurement(const D2D1_RECT_F rect,
+ const bool alignTextBoxToCenter,
+ const CommonState& commonState,
+ HWND window,
+ const D2DState& d2dState)
+ {
+ const bool screenQuadrantAware = !alignTextBoxToCenter;
+ const auto prevMode = d2dState.rt->GetAntialiasMode();
+ d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
+ d2dState.rt->DrawRectangle(rect, d2dState.solidBrushes[Brush::line].get());
+ d2dState.rt->SetAntialiasMode(prevMode);
+
+ OverlayBoxText text;
+ const auto width = std::abs(rect.right - rect.left + 1);
+ const auto height = std::abs(rect.top - rect.bottom + 1);
+ const uint32_t textLen = swprintf_s(text.buffer.data(),
+ text.buffer.size(),
+ L"%.0f × %.0f",
+ width,
+ height);
+ std::optional crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
+
+ commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
+ v = text;
+ });
+
+ float cornerX = rect.right;
+ float cornerY = rect.bottom;
+ if (alignTextBoxToCenter)
+ {
+ cornerX = rect.left + width / 2;
+ cornerY = rect.top + height / 2;
+ }
+
+ d2dState.DrawTextBox(text.buffer.data(),
+ textLen,
+ crossSymbolPos,
+ cornerX,
+ cornerY,
+ screenQuadrantAware,
+ window);
+ }
+}
+
+void DrawBoundsToolTick(const CommonState& commonState,
+ const BoundsToolState& toolState,
+ const HWND window,
+ const D2DState& d2dState)
+{
+ const auto it = toolState.perScreen.find(window);
+ if (it == end(toolState.perScreen))
+ return;
+
+ d2dState.rt->Clear();
+
+ const auto& perScreen = it->second;
+ for (const auto& measure : perScreen.measurements)
+ DrawMeasurement(measure, true, commonState, window, d2dState);
+
+ if (!perScreen.currentRegionStart.has_value())
+ return;
+
+ const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
+
+ const D2D1_RECT_F rect{ .left = perScreen.currentRegionStart->x,
+ .top = perScreen.currentRegionStart->y,
+ .right = static_cast(cursorPos.x),
+ .bottom = static_cast(cursorPos.y) };
+ DrawMeasurement(rect, false, commonState, window, d2dState);
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.h b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.h
new file mode 100644
index 0000000000..32e5505044
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.h
@@ -0,0 +1,10 @@
+#pragma once
+#include "D2DState.h"
+#include "ToolState.h"
+
+void DrawBoundsToolTick(const CommonState& commonState,
+ const BoundsToolState& toolState,
+ const HWND overlayWindow,
+ const D2DState& d2dState);
+
+LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/Clipboard.cpp b/src/modules/MeasureTool/MeasureToolCore/Clipboard.cpp
new file mode 100644
index 0000000000..eef57fd79d
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/Clipboard.cpp
@@ -0,0 +1,28 @@
+#include "pch.h"
+
+#include "Clipboard.h"
+
+void SetClipBoardToText(const std::wstring_view text)
+{
+ if (!OpenClipboard(nullptr))
+ {
+ return;
+ }
+
+ const wil::unique_hglobal handle{ GlobalAlloc(GMEM_MOVEABLE, static_cast((text.length() + 1) * sizeof(wchar_t))) };
+ if (!handle)
+ {
+ CloseClipboard();
+ return;
+ }
+
+ if (auto* bufPtr = static_cast(GlobalLock(handle.get())); bufPtr != nullptr)
+ {
+ text.copy(bufPtr, text.length());
+ GlobalUnlock(handle.get());
+ }
+
+ EmptyClipboard();
+ SetClipboardData(CF_UNICODETEXT, handle.get());
+ CloseClipboard();
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/Clipboard.h b/src/modules/MeasureTool/MeasureToolCore/Clipboard.h
new file mode 100644
index 0000000000..e53f5b61e8
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/Clipboard.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include
+
+void SetClipBoardToText(const std::wstring_view text);
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h b/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h
new file mode 100644
index 0000000000..a140809bf7
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN
+#include
+
+namespace convert
+{
+ // Converts a given point from multi-monitor coordinate system to the one relative to HWND
+ inline POINT FromSystemToRelative(HWND window, POINT p)
+ {
+ ScreenToClient(window, &p);
+ return p;
+ }
+
+ // Converts a given point from multi-monitor coordinate system to the one relative to HWND and also ready
+ // to be used in Direct2D calls with AA mode set to aliased
+ inline POINT FromSystemToRelativeForDirect2D(HWND window, POINT p)
+ {
+ ScreenToClient(window, &p);
+ // Submitting DrawLine calls to Direct2D with thickness == 1.f and AA mode set to aliased causes
+ // them to be drawn offset by [1,1] toward upper-left corner, so we must to compensate for that.
+ ++p.x;
+ ++p.y;
+ return p;
+ }
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp b/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp
new file mode 100644
index 0000000000..472e82c0a2
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp
@@ -0,0 +1,170 @@
+#include "pch.h"
+
+#include "constants.h"
+#include "D2DState.h"
+
+#include
+#include
+
+namespace
+{
+ void DetermineScreenQuadrant(const HWND window, long x, long y, bool& inLeftHalf, bool& inTopHalf)
+ {
+ RECT windowRect{};
+ GetWindowRect(window, &windowRect);
+ const long w = windowRect.right - windowRect.left;
+ const long h = windowRect.bottom - windowRect.top;
+ inLeftHalf = x < w / 2;
+ inTopHalf = y < h / 2;
+ }
+}
+
+D2DState::D2DState(const HWND overlayWindow, std::vector solidBrushesColors)
+{
+ std::lock_guard guard{ gpuAccessLock };
+
+ RECT clientRect = {};
+
+ winrt::check_bool(GetClientRect(overlayWindow, &clientRect));
+ winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &d2dFactory));
+
+ winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), writeFactory.put_unknown()));
+
+ // We should always use DPIAware::DEFAULT_DPI, since it's the correct thing to do in DPI-Aware mode
+ auto renderTargetProperties = D2D1::RenderTargetProperties(
+ D2D1_RENDER_TARGET_TYPE_DEFAULT,
+ D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
+ DPIAware::DEFAULT_DPI,
+ DPIAware::DEFAULT_DPI,
+ D2D1_RENDER_TARGET_USAGE_NONE,
+ D2D1_FEATURE_LEVEL_DEFAULT);
+
+ auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
+ auto hwndRenderTargetProperties = D2D1::HwndRenderTargetProperties(overlayWindow, renderTargetSize);
+
+ winrt::check_hresult(d2dFactory->CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties, &rt));
+ winrt::check_hresult(rt->CreateCompatibleRenderTarget(&bitmapRt));
+
+ unsigned dpi = DPIAware::DEFAULT_DPI;
+ DPIAware::GetScreenDPIForWindow(overlayWindow, dpi);
+ dpiScale = dpi / static_cast(DPIAware::DEFAULT_DPI);
+
+ winrt::check_hresult(writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
+ nullptr,
+ DWRITE_FONT_WEIGHT_NORMAL,
+ DWRITE_FONT_STYLE_NORMAL,
+ DWRITE_FONT_STRETCH_NORMAL,
+ consts::FONT_SIZE * dpiScale,
+ L"en-US",
+ &textFormat));
+ winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER));
+ winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));
+ winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
+
+ solidBrushes.resize(solidBrushesColors.size());
+ for (size_t i = 0; i < solidBrushes.size(); ++i)
+ {
+ winrt::check_hresult(rt->CreateSolidColorBrush(solidBrushesColors[i], &solidBrushes[i]));
+ }
+
+ const auto deviceContext = rt.query();
+ winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, &shadowEffect));
+ winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, consts::SHADOW_RADIUS));
+ winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_COLOR, D2D1::ColorF(0.f, 0.f, 0.f, consts::SHADOW_OPACITY)));
+
+ winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D12DAffineTransform, &affineTransformEffect));
+ affineTransformEffect->SetInputEffect(0, shadowEffect.get());
+
+ textRenderer = winrt::make_self(d2dFactory, rt, solidBrushes[Brush::foreground]);
+}
+
+void D2DState::DrawTextBox(const wchar_t* text,
+ const uint32_t textLen,
+ const std::optional halfOpaqueSymbolPos,
+ const float centerX,
+ const float centerY,
+ const bool screenQuadrantAware,
+ const HWND window) const
+{
+ wil::com_ptr textLayout;
+ winrt::check_hresult(writeFactory->CreateTextLayout(text,
+ textLen,
+ textFormat.get(),
+ std::numeric_limits::max(),
+ std::numeric_limits::max(),
+ &textLayout));
+ DWRITE_TEXT_METRICS textMetrics = {};
+ winrt::check_hresult(textLayout->GetMetrics(&textMetrics));
+ textMetrics.width *= consts::TEXT_BOX_MARGIN_COEFF;
+ textMetrics.height *= consts::TEXT_BOX_MARGIN_COEFF;
+ winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width));
+ winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height));
+
+ D2D1_RECT_F textRect{ .left = centerX - textMetrics.width / 2.f,
+ .top = centerY - textMetrics.height / 2.f,
+ .right = centerX + textMetrics.width / 2.f,
+ .bottom = centerY + textMetrics.height / 2.f };
+ if (screenQuadrantAware)
+ {
+ bool cursorInLeftScreenHalf = false;
+ bool cursorInTopScreenHalf = false;
+ DetermineScreenQuadrant(window,
+ static_cast(centerX),
+ static_cast(centerY),
+ cursorInLeftScreenHalf,
+ cursorInTopScreenHalf);
+ float textQuadrantOffsetX = textMetrics.width * dpiScale;
+ float textQuadrantOffsetY = textMetrics.height * dpiScale;
+ if (!cursorInLeftScreenHalf)
+ textQuadrantOffsetX *= -1.f;
+ if (!cursorInTopScreenHalf)
+ textQuadrantOffsetY *= -1.f;
+ textRect.left += textQuadrantOffsetX;
+ textRect.right += textQuadrantOffsetX;
+ textRect.top += textQuadrantOffsetY;
+ textRect.bottom += textQuadrantOffsetY;
+ }
+
+ // Draw shadow
+ bitmapRt->BeginDraw();
+ bitmapRt->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f));
+ D2D1_ROUNDED_RECT textBoxRect;
+ textBoxRect.radiusX = textBoxRect.radiusY = consts::TEXT_BOX_CORNER_RADIUS * dpiScale;
+ textBoxRect.rect.bottom = textRect.bottom;
+ textBoxRect.rect.top = textRect.top;
+ textBoxRect.rect.left = textRect.left;
+ textBoxRect.rect.right = textRect.right;
+ bitmapRt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
+ bitmapRt->EndDraw();
+
+ wil::com_ptr rtBitmap;
+ bitmapRt->GetBitmap(&rtBitmap);
+
+ shadowEffect->SetInput(0, rtBitmap.get());
+ const auto shadowMatrix = D2D1::Matrix3x2F::Translation(consts::SHADOW_OFFSET * dpiScale,
+ consts::SHADOW_OFFSET * dpiScale);
+ winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX,
+ shadowMatrix));
+ auto deviceContext = rt.query();
+ deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR);
+
+ // Draw text box border rectangle
+ rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
+ const float TEXT_BOX_PADDING = 1.f * dpiScale;
+ textBoxRect.rect.bottom -= TEXT_BOX_PADDING;
+ textBoxRect.rect.top += TEXT_BOX_PADDING;
+ textBoxRect.rect.left += TEXT_BOX_PADDING;
+ textBoxRect.rect.right -= TEXT_BOX_PADDING;
+
+ // Draw text & its box
+ rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get());
+
+ if (halfOpaqueSymbolPos.has_value())
+ {
+ DWRITE_TEXT_RANGE textRange = { static_cast(*halfOpaqueSymbolPos), 2 };
+ auto opacityEffect = winrt::make_self();
+ opacityEffect->alpha = consts::CROSS_OPACITY;
+ winrt::check_hresult(textLayout->SetDrawingEffect(opacityEffect.get(), textRange));
+ }
+ winrt::check_hresult(textLayout->Draw(nullptr, textRenderer.get(), textRect.left, textRect.top));
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/D2DState.h b/src/modules/MeasureTool/MeasureToolCore/D2DState.h
new file mode 100644
index 0000000000..385eb0957b
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/D2DState.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include "PerGlyphOpacityTextRender.h"
+
+enum Brush : size_t
+{
+ line,
+ foreground,
+ background,
+ border
+};
+
+struct D2DState
+{
+ wil::com_ptr d2dFactory;
+ wil::com_ptr writeFactory;
+ wil::com_ptr rt;
+ wil::com_ptr bitmapRt;
+ wil::com_ptr textFormat;
+ winrt::com_ptr textRenderer;
+ std::vector> solidBrushes;
+ wil::com_ptr shadowEffect;
+ wil::com_ptr affineTransformEffect;
+
+ float dpiScale = 1.f;
+
+ D2DState(const HWND window, std::vector solidBrushesColors);
+ void DrawTextBox(const wchar_t* text,
+ const uint32_t textLen,
+ const std::optional halfOpaqueSymbolPos,
+ const float centerX,
+ const float centerY,
+ const bool screenQuadrantAware,
+ const HWND window) const;
+};
diff --git a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp
new file mode 100644
index 0000000000..05da68d6a5
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp
@@ -0,0 +1,112 @@
+#include "pch.h"
+
+#include "constants.h"
+#include "EdgeDetection.h"
+template
+inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance)
+{
+ using namespace consts;
+
+ long xOffset = 0;
+ long yOffset = 0;
+
+ // For continuous capture, we'll be a bit off center from the cursor so the cross we draw won't interfere with the measurement.
+ if constexpr (ContinuousCapture)
+ {
+ if constexpr (IsX)
+ {
+ xOffset = Increment ? CURSOR_OFFSET_AMOUNT_X : -CURSOR_OFFSET_AMOUNT_X;
+ yOffset = 1;
+ }
+ else
+ {
+ xOffset = 1;
+ yOffset = Increment ? CURSOR_OFFSET_AMOUNT_Y : -CURSOR_OFFSET_AMOUNT_Y;
+ }
+ }
+
+ const size_t maxDim = IsX ? texture.width : texture.height;
+
+ long x = std::clamp(centerPoint.x + xOffset, 1, static_cast(texture.width - 2));
+ long y = std::clamp(centerPoint.y + yOffset, 1, static_cast(texture.height - 2));
+
+ const uint32_t startPixel = texture.GetPixel(x, y);
+ while (true)
+ {
+ long oldX = x;
+ long oldY = y;
+ if constexpr (IsX)
+ {
+ if constexpr (Increment)
+ {
+ if (++x == maxDim)
+ break;
+ }
+ else
+ {
+ if (--x == 0)
+ break;
+ }
+ }
+ else
+ {
+ if constexpr (Increment)
+ {
+ if (++y == maxDim)
+ break;
+ }
+ else
+ {
+ if (--y == 0)
+ break;
+ }
+ }
+
+ const uint32_t nextPixel = texture.GetPixel(x, y);
+ if (!texture.PixelsClose(startPixel, nextPixel, tolerance))
+ {
+ return IsX ? oldX : oldY;
+ }
+ }
+
+ return Increment ? static_cast(IsX ? texture.width : texture.height) - 1 : 0;
+}
+
+template
+inline RECT DetectEdgesInternal(const BGRATextureView& texture,
+ const POINT centerPoint,
+ const uint8_t tolerance)
+{
+ return RECT{ .left = FindEdge(texture, centerPoint, tolerance),
+ .top = FindEdge(texture, centerPoint, tolerance),
+ .right = FindEdge(texture, centerPoint, tolerance),
+ .bottom = FindEdge(texture, centerPoint, tolerance) };
+}
+
+RECT DetectEdges(const BGRATextureView& texture,
+ const POINT centerPoint,
+ const bool perChannel,
+ const uint8_t tolerance,
+ const bool continuousCapture)
+{
+ auto function = perChannel ? &DetectEdgesInternal : DetectEdgesInternal;
+ if (continuousCapture)
+ function = perChannel ? &DetectEdgesInternal : &DetectEdgesInternal;
+
+ return function(texture, centerPoint, tolerance);
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h
new file mode 100644
index 0000000000..175c820540
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "BGRATextureView.h"
+
+RECT DetectEdges(const BGRATextureView& texture,
+ const POINT centerPoint,
+ const bool perChannel,
+ const uint8_t tolerance,
+ const bool continuousCapture);
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/MeasureTool.def b/src/modules/MeasureTool/MeasureToolCore/MeasureTool.def
new file mode 100644
index 0000000000..24e7c1235c
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/MeasureTool.def
@@ -0,0 +1,3 @@
+EXPORTS
+DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
+DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE
diff --git a/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp
new file mode 100644
index 0000000000..216ee46cb6
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp
@@ -0,0 +1,300 @@
+#include "pch.h"
+
+#include "BGRATextureView.h"
+#include "Clipboard.h"
+#include "CoordinateSystemConversion.h"
+#include "constants.h"
+#include "MeasureToolOverlayUI.h"
+
+#include
+
+namespace
+{
+ inline std::pair ComputeCrossFeetLine(D2D_POINT_2F center, const bool horizontal)
+ {
+ D2D_POINT_2F start = center, end = center;
+ // Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines
+ if (horizontal)
+ {
+ start.x -= consts::FEET_HALF_LENGTH + 1.f;
+ end.x += consts::FEET_HALF_LENGTH;
+
+ start.y += 1.f;
+ end.y += 1.f;
+ }
+ else
+ {
+ start.y -= consts::FEET_HALF_LENGTH + 1.f;
+ end.y += consts::FEET_HALF_LENGTH;
+
+ start.x += 1.f;
+ end.x += 1.f;
+ }
+
+ return { start, end };
+ }
+}
+
+winrt::com_ptr ConvertID3D11Texture2DToD2D1Bitmap(wil::com_ptr rt,
+ winrt::com_ptr texture)
+{
+ std::lock_guard guard{ gpuAccessLock };
+
+ auto dxgiSurface = texture.try_as();
+ if (!dxgiSurface)
+ return nullptr;
+
+ DXGI_MAPPED_RECT bitmap2Dmap = {};
+ HRESULT hr = dxgiSurface->Map(&bitmap2Dmap, DXGI_MAP_READ);
+ if (FAILED(hr))
+ {
+ return nullptr;
+ }
+
+ D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() };
+ rt->GetDpi(&props.dpiX, &props.dpiY);
+ const auto sizeF = rt->GetSize();
+ winrt::com_ptr bitmap;
+ if (FAILED(rt->CreateBitmap(D2D1::SizeU(static_cast(sizeF.width),
+ static_cast(sizeF.height)),
+ bitmap2Dmap.pBits,
+ bitmap2Dmap.Pitch,
+ props,
+ bitmap.put())))
+ return nullptr;
+ if (FAILED(dxgiSurface->Unmap()))
+ return nullptr;
+
+ return bitmap;
+}
+
+LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
+{
+ switch (message)
+ {
+ case WM_CURSOR_LEFT_MONITOR:
+ {
+ if (auto state = GetWindowParam*>(window))
+ {
+ state->Access([&](MeasureToolState& s) {
+ s.perScreen[window].measuredEdges = {};
+ });
+ }
+ break;
+ }
+ case WM_NCHITTEST:
+ return HTCLIENT;
+ case WM_CREATE:
+ {
+ auto state = GetWindowCreateParam*>(lparam);
+ StoreWindowParam(window, state);
+
+#if !defined(DEBUG_OVERLAY)
+ for (; ShowCursor(false) > 0;)
+ ;
+#endif
+ break;
+ }
+ case WM_ERASEBKGND:
+ return 1;
+ case WM_KEYUP:
+ if (wparam == VK_ESCAPE)
+ {
+ PostMessageW(window, WM_CLOSE, {}, {});
+ }
+ break;
+ case WM_RBUTTONUP:
+ PostMessageW(window, WM_CLOSE, {}, {});
+ break;
+ case WM_LBUTTONUP:
+ if (auto state = GetWindowParam*>(window))
+ {
+ state->Read([](const MeasureToolState& s) { s.commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
+ SetClipBoardToText(text.buffer);
+ }); });
+ }
+ break;
+ case WM_MOUSEWHEEL:
+ if (auto state = GetWindowParam*>(window))
+ {
+ const int8_t step = static_cast(HIWORD(wparam)) < 0 ? -consts::MOUSE_WHEEL_TOLERANCE_STEP : consts::MOUSE_WHEEL_TOLERANCE_STEP;
+ state->Access([step](MeasureToolState& s) {
+ int wideVal = s.global.pixelTolerance;
+ wideVal += step;
+ s.global.pixelTolerance = static_cast(std::clamp(wideVal, 0, 255));
+ });
+ }
+ break;
+ }
+
+ return DefWindowProcW(window, message, wparam, lparam);
+}
+
+void DrawMeasureToolTick(const CommonState& commonState,
+ Serialized& toolState,
+ HWND window,
+ D2DState& d2dState)
+{
+ bool continuousCapture = {};
+ bool drawFeetOnCross = {};
+ bool drawHorizontalCrossLine = true;
+ bool drawVerticalCrossLine = true;
+ RECT measuredEdges{};
+ MeasureToolState::Mode mode = {};
+ winrt::com_ptr backgroundBitmap;
+ winrt::com_ptr backgroundTextureToConvert;
+
+ toolState.Read([&](const MeasureToolState& state) {
+ continuousCapture = state.global.continuousCapture;
+ drawFeetOnCross = state.global.drawFeetOnCross;
+ mode = state.global.mode;
+
+ if (auto it = state.perScreen.find(window); it != end(state.perScreen))
+ {
+ const auto& perScreen = it->second;
+ measuredEdges = perScreen.measuredEdges;
+
+ if (continuousCapture)
+ return;
+
+ if (perScreen.capturedScreenBitmap)
+ {
+ backgroundBitmap = perScreen.capturedScreenBitmap;
+ }
+ else if (perScreen.capturedScreenTexture)
+ {
+ backgroundTextureToConvert = perScreen.capturedScreenTexture;
+ }
+ }
+ });
+ switch (mode)
+ {
+ case MeasureToolState::Mode::Cross:
+ drawHorizontalCrossLine = true;
+ drawVerticalCrossLine = true;
+ break;
+ case MeasureToolState::Mode::Vertical:
+ drawHorizontalCrossLine = false;
+ drawVerticalCrossLine = true;
+ break;
+ case MeasureToolState::Mode::Horizontal:
+ drawHorizontalCrossLine = true;
+ drawVerticalCrossLine = false;
+ break;
+ }
+
+ if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
+ {
+ backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.rt, backgroundTextureToConvert);
+ if (backgroundBitmap)
+ {
+ toolState.Access([&](MeasureToolState& state) {
+ state.perScreen[window].capturedScreenTexture = {};
+ state.perScreen[window].capturedScreenBitmap = backgroundBitmap;
+ });
+ }
+ }
+
+ if (continuousCapture || !backgroundBitmap)
+ d2dState.rt->Clear();
+
+ // Add 1px to each dim, since the range we obtain from measuredEdges is inclusive.
+ const float hMeasure = static_cast(measuredEdges.right - measuredEdges.left + 1);
+ const float vMeasure = static_cast(measuredEdges.bottom - measuredEdges.top + 1);
+
+ // Prevent drawing until we get the first capture
+ const bool hasMeasure = (measuredEdges.right != measuredEdges.left) && (measuredEdges.bottom != measuredEdges.top);
+ if (!hasMeasure)
+ {
+ return;
+ }
+
+ if (!continuousCapture && backgroundBitmap)
+ {
+ d2dState.rt->DrawBitmap(backgroundBitmap.get());
+ }
+
+ const auto previousAliasingMode = d2dState.rt->GetAntialiasMode();
+ // Anti-aliasing is creating artifacts. Aliasing is for drawing straight lines.
+ d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
+
+ const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
+
+ if (drawHorizontalCrossLine)
+ {
+ const D2D_POINT_2F hLineStart{ .x = static_cast(measuredEdges.left), .y = static_cast(cursorPos.y) };
+ D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
+ d2dState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
+
+ if (drawFeetOnCross && !continuousCapture)
+ {
+ // To fill all pixels which are close, we call DrawLine with end point one pixel too far, since
+ // it doesn't get filled, i.e. end point of the range is excluded. However, we want to draw cross
+ // feet *on* the last pixel row, so we must subtract 1px from the corresponding axis.
+ hLineEnd.x -= 1.f;
+ auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
+ auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
+ d2dState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
+ d2dState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
+ }
+ }
+
+ if (drawVerticalCrossLine)
+ {
+ const D2D_POINT_2F vLineStart{ .x = static_cast(cursorPos.x), .y = static_cast(measuredEdges.top) };
+ D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
+ d2dState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
+
+ if (drawFeetOnCross && !continuousCapture)
+ {
+ vLineEnd.y -= 1.f;
+ auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
+ auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
+ d2dState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
+ d2dState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
+ }
+ }
+
+ // After drawing the lines, restore anti aliasing to draw the measurement tooltip.
+ d2dState.rt->SetAntialiasMode(previousAliasingMode);
+
+ uint32_t measureStringBufLen = 0;
+
+ OverlayBoxText text;
+ std::optional crossSymbolPos;
+ switch (mode)
+ {
+ case MeasureToolState::Mode::Cross:
+ measureStringBufLen = swprintf_s(text.buffer.data(),
+ text.buffer.size(),
+ L"%.0f × %.0f",
+ hMeasure,
+ vMeasure);
+ crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
+ break;
+ case MeasureToolState::Mode::Vertical:
+ measureStringBufLen = swprintf_s(text.buffer.data(),
+ text.buffer.size(),
+ L"%.0f",
+ vMeasure);
+ break;
+ case MeasureToolState::Mode::Horizontal:
+ measureStringBufLen = swprintf_s(text.buffer.data(),
+ text.buffer.size(),
+ L"%.0f",
+ hMeasure);
+ break;
+ }
+
+ commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
+ v = text;
+ });
+
+ d2dState.DrawTextBox(text.buffer.data(),
+ measureStringBufLen,
+ crossSymbolPos,
+ static_cast(cursorPos.x),
+ static_cast(cursorPos.y),
+ true,
+ window);
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.h b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.h
new file mode 100644
index 0000000000..2c64692d4b
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.h
@@ -0,0 +1,12 @@
+#pragma once
+#include "D2DState.h"
+#include "ToolState.h"
+
+#include
+
+void DrawMeasureToolTick(const CommonState& commonState,
+ Serialized& toolState,
+ HWND overlayWindow,
+ D2DState& d2dState);
+
+LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp
new file mode 100644
index 0000000000..c798e91193
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp
@@ -0,0 +1,231 @@
+#include "pch.h"
+
+#include "BoundsToolOverlayUI.h"
+#include "MeasureToolOverlayUI.h"
+#include "OverlayUI.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace NonLocalizable
+{
+ const wchar_t MeasureToolOverlayWindowName[] = L"PowerToys.MeasureToolOverlayWindow";
+ const wchar_t BoundsToolOverlayWindowName[] = L"PowerToys.BoundsToolOverlayWindow";
+}
+
+void CreateOverlayWindowClasses()
+{
+ WNDCLASSEXW wcex{ .cbSize = sizeof(WNDCLASSEX), .hInstance = GetModuleHandleW(nullptr) };
+
+ wcex.lpfnWndProc = MeasureToolWndProc;
+ wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName;
+ RegisterClassExW(&wcex);
+
+ wcex.lpfnWndProc = BoundsToolWndProc;
+ wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName;
+ wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
+ RegisterClassExW(&wcex);
+}
+
+HWND CreateOverlayUIWindow(const CommonState& commonState,
+ const MonitorInfo& monitor,
+ const wchar_t* windowClass,
+ void* extraParam)
+{
+ static std::once_flag windowClassesCreatedFlag;
+ std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses);
+
+ const auto screenArea = monitor.GetScreenSize(true);
+ HWND window{ CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
+ windowClass,
+ L"PowerToys.MeasureToolOverlay",
+ WS_POPUP,
+ screenArea.left(),
+ screenArea.top(),
+ screenArea.width(),
+ screenArea.height(),
+ HWND_DESKTOP,
+ nullptr,
+ GetModuleHandleW(nullptr),
+ extraParam) };
+ winrt::check_bool(window);
+ ShowWindow(window, SW_SHOWNORMAL);
+#if !defined(DEBUG_OVERLAY)
+ SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE);
+#else
+ (void)window;
+#endif
+
+ const int pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
+ if (wil::unique_hrgn hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) })
+ {
+ DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn.get(), FALSE };
+ DwmEnableBlurBehindWindow(window, &bh);
+ }
+
+ RECT windowRect = {};
+ // Exclude toolbar from the window's region to be able to use toolbar during tool usage.
+ if (monitor.IsPrimary() && GetWindowRect(window, &windowRect))
+ {
+ // will be freed during SetWindowRgn call
+ const HRGN windowRegion{ CreateRectRgn(windowRect.left, windowRect.top, windowRect.right, windowRect.bottom) };
+ wil::unique_hrgn toolbarRegion{ CreateRectRgn(commonState.toolbarBoundingBox.left(),
+ commonState.toolbarBoundingBox.top(),
+ commonState.toolbarBoundingBox.right(),
+ commonState.toolbarBoundingBox.bottom()) };
+ const auto res = CombineRgn(windowRegion, windowRegion, toolbarRegion.get(), RGN_DIFF);
+ if (res != ERROR)
+ SetWindowRgn(window, windowRegion, true);
+ }
+
+ return window;
+}
+
+std::vector AppendCommonOverlayUIColors(const D2D1::ColorF& lineColor)
+{
+ D2D1::ColorF foreground = D2D1::ColorF::Black;
+ D2D1::ColorF background = D2D1::ColorF(0.96f, 0.96f, 0.96f, 1.0f);
+ D2D1::ColorF border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f);
+
+ if (WindowsColors::is_dark_mode())
+ {
+ foreground = D2D1::ColorF::White;
+ background = D2D1::ColorF(0.17f, 0.17f, 0.17f, 1.0f);
+ border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f);
+ }
+
+ return { lineColor, foreground, background, border };
+}
+
+void OverlayUIState::RunUILoop()
+{
+ while (IsWindow(_window) && !_commonState.closeOnOtherMonitors)
+ {
+ const auto cursor = _commonState.cursorPosSystemSpace;
+ const bool cursorOnScreen = _monitorArea.inside(cursor);
+ const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor);
+
+ if (cursorOnScreen != _cursorOnScreen)
+ {
+ _cursorOnScreen = cursorOnScreen;
+ if (!cursorOnScreen)
+ {
+ if (_clearOnCursorLeavingScreen)
+ {
+ _d2dState.rt->BeginDraw();
+ _d2dState.rt->Clear();
+ _d2dState.rt->EndDraw();
+ }
+ PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {});
+ }
+ }
+ if (cursorOnScreen)
+ {
+ _d2dState.rt->BeginDraw();
+ if (!cursorOverToolbar)
+ _tickFunc();
+ else
+ _d2dState.rt->Clear();
+
+ {
+ // TODO: use latch to wait until all threads are created their corresponding d2d textures
+ // in the non-continuous mode
+ // std::lock_guard guard{ gpuAccessLock };
+ _d2dState.rt->EndDraw();
+ }
+ }
+
+ run_message_loop(true, 1);
+ }
+
+ DestroyWindow(_window);
+}
+
+template
+OverlayUIState::OverlayUIState(StateT& toolState,
+ TickFuncT tickFunc,
+ const CommonState& commonState,
+ HWND window) :
+ _window{ window },
+ _commonState{ commonState },
+ _d2dState{ window, AppendCommonOverlayUIColors(commonState.lineColor) },
+ _tickFunc{ [this, tickFunc, &toolState] {
+ tickFunc(_commonState, toolState, _window, _d2dState);
+ } }
+{
+}
+
+OverlayUIState::~OverlayUIState()
+{
+ PostMessageW(_window, WM_CLOSE, {}, {});
+ try
+ {
+ if (_uiThread.joinable())
+ _uiThread.join();
+ }
+ catch (...)
+ {
+ }
+}
+
+// Returning unique_ptr, since we need to pin ui state in memory
+template
+inline std::unique_ptr OverlayUIState::CreateInternal(ToolT& toolState,
+ TickFuncT tickFunc,
+ CommonState& commonState,
+ const wchar_t* toolWindowClassName,
+ void* windowParam,
+ const MonitorInfo& monitor,
+ const bool clearOnCursorLeavingScreen)
+{
+ wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset);
+ std::unique_ptr uiState;
+ auto threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] {
+ const HWND window = CreateOverlayUIWindow(commonState, monitor, toolWindowClassName, windowParam);
+ uiState = std::unique_ptr{ new OverlayUIState{ toolState, tickFunc, commonState, window } };
+ uiState->_monitorArea = monitor.GetScreenSize(true);
+ uiState->_clearOnCursorLeavingScreen = clearOnCursorLeavingScreen;
+ // we must create window + d2d state in the same thread, then store thread handle in uiState, thus
+ // lifetime is ok here, since we join the thread in destructor
+ auto* state = uiState.get();
+ uiCreatedEvent.SetEvent();
+
+ state->RunUILoop();
+
+ commonState.closeOnOtherMonitors = true;
+ commonState.sessionCompletedCallback();
+ });
+
+ uiCreatedEvent.wait();
+ uiState->_uiThread = std::move(threadHandle);
+ return uiState;
+}
+
+std::unique_ptr OverlayUIState::Create(Serialized& toolState,
+ CommonState& commonState,
+ const MonitorInfo& monitor)
+{
+ return OverlayUIState::CreateInternal(toolState,
+ DrawMeasureToolTick,
+ commonState,
+ NonLocalizable::MeasureToolOverlayWindowName,
+ &toolState,
+ monitor,
+ true);
+}
+
+std::unique_ptr OverlayUIState::Create(BoundsToolState& toolState,
+ CommonState& commonState,
+ const MonitorInfo& monitor)
+{
+ return OverlayUIState::CreateInternal(toolState,
+ DrawBoundsToolTick,
+ commonState,
+ NonLocalizable::BoundsToolOverlayWindowName,
+ &toolState,
+ monitor,
+ false);
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h
new file mode 100644
index 0000000000..e4d2b1ddfe
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "D2DState.h"
+#include "ToolState.h"
+
+#include
+#include
+
+class OverlayUIState final
+{
+ template
+ OverlayUIState(StateT& toolState,
+ TickFuncT tickFunc,
+ const CommonState& commonState,
+ HWND window);
+
+ Box _monitorArea;
+ HWND _window = {};
+ const CommonState& _commonState;
+ D2DState _d2dState;
+ std::function _tickFunc;
+ std::thread _uiThread;
+ bool _cursorOnScreen = true;
+ bool _clearOnCursorLeavingScreen = false;
+
+ template
+ static std::unique_ptr CreateInternal(ToolT& toolState,
+ TickFuncT tickFunc,
+ CommonState& commonState,
+ const wchar_t* toolWindowClassName,
+ void* windowParam,
+ const MonitorInfo& monitor,
+ const bool clearOnCursorLeavingScreen);
+
+public:
+ OverlayUIState(OverlayUIState&&) noexcept = default;
+ ~OverlayUIState();
+
+ static std::unique_ptr Create(BoundsToolState& toolState,
+ CommonState& commonState,
+ const MonitorInfo& monitor);
+ static std::unique_ptr Create(Serialized& toolState,
+ CommonState& commonState,
+ const MonitorInfo& monitor);
+ inline HWND overlayWindowHandle() const
+ {
+ return _window;
+ }
+
+ void RunUILoop();
+};
diff --git a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp
new file mode 100644
index 0000000000..7d02785951
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp
@@ -0,0 +1,141 @@
+#include "pch.h"
+
+#include "PerGlyphOpacityTextRender.h"
+
+PerGlyphOpacityTextRender::PerGlyphOpacityTextRender(
+ wil::com_ptr pD2DFactory,
+ wil::com_ptr rt,
+ wil::com_ptr baseBrush) :
+ _pD2DFactory{ pD2DFactory },
+ _rt{ rt },
+ _baseBrush{ baseBrush }
+{
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::DrawGlyphRun(void* /*clientDrawingContext*/,
+ FLOAT baselineOriginX,
+ FLOAT baselineOriginY,
+ DWRITE_MEASURING_MODE measuringMode,
+ _In_ const DWRITE_GLYPH_RUN* glyphRun,
+ _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/,
+ IUnknown* clientDrawingEffect) noexcept
+{
+ HRESULT hr = S_OK;
+ if (!clientDrawingEffect)
+ {
+ _rt->DrawGlyphRun(D2D1_POINT_2F{ .x = baselineOriginX, .y = baselineOriginY }, glyphRun, _baseBrush.get(), measuringMode);
+ return hr;
+ }
+ // Create the path geometry.
+ wil::com_ptr pathGeometry;
+ hr = _pD2DFactory->CreatePathGeometry(&pathGeometry);
+
+ // Write to the path geometry using the geometry sink.
+ ID2D1GeometrySink* pSink = nullptr;
+ if (SUCCEEDED(hr))
+ {
+ hr = pathGeometry->Open(&pSink);
+ }
+
+ // Get the glyph run outline geometries back from DirectWrite and place them within the
+ // geometry sink.
+ if (SUCCEEDED(hr))
+ {
+ hr = glyphRun->fontFace->GetGlyphRunOutline(
+ glyphRun->fontEmSize,
+ glyphRun->glyphIndices,
+ glyphRun->glyphAdvances,
+ glyphRun->glyphOffsets,
+ glyphRun->glyphCount,
+ glyphRun->isSideways,
+ glyphRun->bidiLevel % 2,
+ pSink);
+ }
+
+ // Close the geometry sink
+ if (SUCCEEDED(hr))
+ {
+ hr = pSink->Close();
+ }
+
+ // Initialize a matrix to translate the origin of the glyph run.
+ D2D1::Matrix3x2F const matrix = D2D1::Matrix3x2F(
+ 1.0f, 0.0f, 0.0f, 1.0f, baselineOriginX, baselineOriginY);
+
+ // Create the transformed geometry
+ wil::com_ptr pTransformedGeometry;
+ if (SUCCEEDED(hr))
+ {
+ hr = _pD2DFactory->CreateTransformedGeometry(pathGeometry.get(), &matrix, &pTransformedGeometry);
+ }
+
+ float prevOpacity = _baseBrush->GetOpacity();
+ OpacityEffect* opacityEffect = nullptr;
+ if (SUCCEEDED(hr))
+ {
+ hr = clientDrawingEffect->QueryInterface(__uuidof(IDrawingEffect), reinterpret_cast(&opacityEffect));
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ _baseBrush->SetOpacity(opacityEffect->alpha);
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ _rt->DrawGeometry(pTransformedGeometry.get(), _baseBrush.get());
+ _rt->FillGeometry(pTransformedGeometry.get(), _baseBrush.get());
+ _baseBrush->SetOpacity(prevOpacity);
+ }
+
+ return hr;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::DrawUnderline(void* /*clientDrawingContext*/,
+ FLOAT /*baselineOriginX*/,
+ FLOAT /*baselineOriginY*/,
+ _In_ const DWRITE_UNDERLINE* /*underline*/,
+ IUnknown* /*clientDrawingEffect*/) noexcept
+{
+ return E_NOTIMPL;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::DrawStrikethrough(void* /*clientDrawingContext*/,
+ FLOAT /*baselineOriginX*/,
+ FLOAT /*baselineOriginY*/,
+ _In_ const DWRITE_STRIKETHROUGH* /*strikethrough*/,
+ IUnknown* /*clientDrawingEffect*/) noexcept
+{
+ return E_NOTIMPL;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::DrawInlineObject(void* /*clientDrawingContext*/,
+ FLOAT /*originX*/,
+ FLOAT /*originY*/,
+ IDWriteInlineObject* /*inlineObject*/,
+ BOOL /*isSideways*/,
+ BOOL /*isRightToLeft*/,
+ IUnknown* /*clientDrawingEffect*/) noexcept
+{
+ return E_NOTIMPL;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::IsPixelSnappingDisabled(void* /*clientDrawingContext*/, BOOL* isDisabled) noexcept
+{
+ RETURN_HR_IF_NULL(E_INVALIDARG, isDisabled);
+
+ *isDisabled = false;
+ return S_OK;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::GetCurrentTransform(void* /*clientDrawingContext*/, DWRITE_MATRIX* transform) noexcept
+{
+ _rt->GetTransform(reinterpret_cast(transform));
+ return S_OK;
+}
+
+HRESULT __stdcall PerGlyphOpacityTextRender::GetPixelsPerDip(void* /*clientDrawingContext*/, FLOAT* pixelsPerDip) noexcept
+{
+ _rt->GetDpi(pixelsPerDip, pixelsPerDip);
+ return S_OK;
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h
new file mode 100644
index 0000000000..21cbbae661
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+struct __declspec(uuid("{01557C9F-E3DD-4C28-AE64-E731EAB479CC}")) IDrawingEffect : IUnknown
+{
+};
+
+struct OpacityEffect : winrt::implements
+{
+ float alpha = 1.f;
+};
+
+struct PerGlyphOpacityTextRender : winrt::implements
+{
+ wil::com_ptr _pD2DFactory;
+ wil::com_ptr _rt;
+ wil::com_ptr _baseBrush;
+
+ PerGlyphOpacityTextRender(
+ wil::com_ptr pD2DFactory,
+ wil::com_ptr rt,
+ wil::com_ptr baseBrush);
+
+ HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext,
+ FLOAT baselineOriginX,
+ FLOAT baselineOriginY,
+ DWRITE_MEASURING_MODE measuringMode,
+ _In_ const DWRITE_GLYPH_RUN* glyphRun,
+ _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
+ IUnknown* clientDrawingEffect) noexcept override;
+ HRESULT __stdcall DrawUnderline(void* clientDrawingContext,
+ FLOAT baselineOriginX,
+ FLOAT baselineOriginY,
+ _In_ const DWRITE_UNDERLINE* underline,
+ IUnknown* clientDrawingEffect) noexcept override;
+ HRESULT __stdcall DrawStrikethrough(void* clientDrawingContext,
+ FLOAT baselineOriginX,
+ FLOAT baselineOriginY,
+ _In_ const DWRITE_STRIKETHROUGH* strikethrough,
+ IUnknown* clientDrawingEffect) noexcept override;
+ HRESULT __stdcall DrawInlineObject(void* clientDrawingContext,
+ FLOAT originX,
+ FLOAT originY,
+ IDWriteInlineObject* inlineObject,
+ BOOL isSideways,
+ BOOL isRightToLeft,
+ IUnknown* clientDrawingEffect) noexcept override;
+ HRESULT __stdcall IsPixelSnappingDisabled(void* clientDrawingContext, BOOL* isDisabled) noexcept override;
+ HRESULT __stdcall GetCurrentTransform(void* clientDrawingContext, DWRITE_MATRIX* transform) noexcept override;
+ HRESULT __stdcall GetPixelsPerDip(void* clientDrawingContext, FLOAT* pixelsPerDip) noexcept override;
+};
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp
new file mode 100644
index 0000000000..7dcf664df3
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp
@@ -0,0 +1,156 @@
+#include "pch.h"
+
+#include
+#include
+#include
+#include
+
+#include "../MeasureToolModuleInterface/trace.h"
+#include "constants.h"
+#include "PowerToys.MeasureToolCore.h"
+#include "Core.g.cpp"
+#include "OverlayUI.h"
+#include "ScreenCapturing.h"
+
+//#define DEBUG_PRIMARY_MONITOR_ONLY
+
+std::recursive_mutex gpuAccessLock;
+
+namespace winrt::PowerToys::MeasureToolCore::implementation
+{
+ void Core::MouseCaptureThread()
+ {
+ while (!_stopMouseCaptureThreadSignal.is_signaled())
+ {
+ static_assert(sizeof(_commonState.cursorPosSystemSpace) == sizeof(LONG64));
+ POINT cursorPos = {};
+ GetCursorPos(&cursorPos);
+ InterlockedExchange64(reinterpret_cast(&_commonState.cursorPosSystemSpace), std::bit_cast(cursorPos));
+ std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
+ }
+ }
+
+ Core::Core() :
+ _mouseCaptureThread{ [this] { MouseCaptureThread(); } },
+ _stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset }
+ {
+ Trace::RegisterProvider();
+ LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
+ }
+
+ Core::~Core()
+ {
+ _stopMouseCaptureThreadSignal.SetEvent();
+ _mouseCaptureThread.join();
+
+ ResetState();
+ Trace::UnregisterProvider();
+ }
+
+ void Core::ResetState()
+ {
+ _commonState.closeOnOtherMonitors = true;
+ _overlayUIStates.clear();
+ _boundsToolState = { .commonState = &_commonState };
+ for (auto& thread : _screenCaptureThreads)
+ {
+ if (thread.joinable())
+ {
+ thread.join();
+ }
+ }
+ _screenCaptureThreads.clear();
+ _measureToolState.Reset();
+ _measureToolState.Access([&](MeasureToolState& s) {
+ s.commonState = &_commonState;
+ });
+
+ _settings = Settings::LoadFromFile();
+
+ _commonState.lineColor.r = _settings.lineColor[0] / 255.f;
+ _commonState.lineColor.g = _settings.lineColor[1] / 255.f;
+ _commonState.lineColor.b = _settings.lineColor[2] / 255.f;
+ _commonState.closeOnOtherMonitors = false;
+ }
+
+ void Core::StartBoundsTool()
+ {
+ ResetState();
+
+#if defined(DEBUG_PRIMARY_MONITOR_ONLY)
+ const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor();
+#else
+ for (const auto& monitorInfo : MonitorInfo::GetMonitors(true))
+#endif
+ {
+ auto overlayUI = OverlayUIState::Create(_boundsToolState, _commonState, monitorInfo);
+#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
+ if (!overlayUI)
+ continue;
+#endif
+ _overlayUIStates.push_back(std::move(overlayUI));
+ }
+ Trace::BoundsToolActivated();
+ }
+
+ void Core::StartMeasureTool(const bool horizontal, const bool vertical)
+ {
+ ResetState();
+
+ _measureToolState.Access([horizontal, vertical, this](MeasureToolState& state) {
+ if (horizontal)
+ state.global.mode = vertical ? MeasureToolState::Mode::Cross : MeasureToolState::Mode::Horizontal;
+ else
+ state.global.mode = MeasureToolState::Mode::Vertical;
+
+ state.global.continuousCapture = _settings.continuousCapture;
+ state.global.drawFeetOnCross = _settings.drawFeetOnCross;
+ state.global.pixelTolerance = _settings.pixelTolerance;
+ state.global.perColorChannelEdgeDetection = _settings.perColorChannelEdgeDetection;
+ });
+
+#if defined(DEBUG_PRIMARY_MONITOR_ONLY)
+ const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor();
+#else
+ for (const auto& monitorInfo : MonitorInfo::GetMonitors(true))
+#endif
+ {
+ auto overlayUI = OverlayUIState::Create(_measureToolState, _commonState, monitorInfo);
+#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
+ if (!overlayUI)
+ continue;
+#endif
+ _screenCaptureThreads.emplace_back(StartCapturingThread(_commonState,
+ _measureToolState,
+ overlayUI->overlayWindowHandle(),
+ monitorInfo));
+ _overlayUIStates.push_back(std::move(overlayUI));
+ }
+ Trace::MeasureToolActivated();
+ }
+
+ void MeasureToolCore::implementation::Core::SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger)
+ {
+ _commonState.sessionCompletedCallback = [trigger = std::move(sessionCompletedTrigger)] {
+ trigger();
+ };
+ }
+
+ void MeasureToolCore::implementation::Core::SetToolbarBoundingBox(const uint32_t fromX,
+ const uint32_t fromY,
+ const uint32_t toX,
+ const uint32_t toY)
+ {
+ _commonState.toolbarBoundingBox = Box{ RECT{ .left = static_cast(fromX),
+ .top = static_cast(fromY),
+ .right = static_cast(toX),
+ .bottom = static_cast(toY) } };
+ }
+
+ float MeasureToolCore::implementation::Core::GetDPIScaleForWindow(uint64_t windowHandle)
+ {
+ UINT dpi = DPIAware::DEFAULT_DPI;
+ DPIAware::GetScreenDPIForWindow(std::bit_cast(windowHandle), dpi);
+ return static_cast(dpi) / DPIAware::DEFAULT_DPI;
+ }
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h
new file mode 100644
index 0000000000..5177cc31ed
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "Core.g.h"
+#include "ToolState.h"
+#include "OverlayUI.h"
+#include "Settings.h"
+
+#include
+
+namespace winrt::PowerToys::MeasureToolCore::implementation
+{
+ struct Core : CoreT
+ {
+ Core();
+ ~Core();
+ void StartBoundsTool();
+ void StartMeasureTool(const bool horizontal, const bool vertical);
+ void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger);
+ void SetToolbarBoundingBox(const uint32_t fromX, const uint32_t fromY, const uint32_t toX, const uint32_t toY);
+ void ResetState();
+ float GetDPIScaleForWindow(uint64_t windowHandle);
+ void MouseCaptureThread();
+
+ std::thread _mouseCaptureThread;
+ std::vector _screenCaptureThreads;
+ wil::shared_event _stopMouseCaptureThreadSignal;
+
+ std::vector> _overlayUIStates;
+ Serialized _measureToolState;
+ BoundsToolState _boundsToolState;
+ CommonState _commonState;
+ Settings _settings;
+ };
+}
+
+namespace winrt::PowerToys::MeasureToolCore::factory_implementation
+{
+ struct Core : CoreT
+ {
+ };
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl
new file mode 100644
index 0000000000..95e68910a1
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl
@@ -0,0 +1,26 @@
+namespace PowerToys
+{
+ namespace MeasureToolCore
+ {
+ struct Point
+ {
+ Int32 X;
+ Int32 Y;
+ };
+
+ delegate void ToolSessionCompleted();
+
+ [default_interface]
+ runtimeclass Core
+ {
+ Core();
+ void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger);
+ void StartMeasureTool(Boolean horizontal, Boolean vertical);
+ void StartBoundsTool();
+ void ResetState();
+
+ void SetToolbarBoundingBox(Int32 fromX, Int32 fromY, Int32 toX, Int32 toY);
+ Single GetDPIScaleForWindow(Int64 windowHandle);
+ }
+ }
+}
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.rc b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.rc
new file mode 100644
index 0000000000..5fa3c8b90d
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.rc
@@ -0,0 +1,40 @@
+#include
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+#include "winres.h"
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
new file mode 100644
index 0000000000..9780d0d09a
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+ true
+ true
+ true
+ {54a93af7-60c7-4f6c-99d2-fbb1f75f853a}
+ PowerToys.MeasureToolCore
+ PowerToys.MeasureToolCore
+
+ PowerToys.MeasureToolCore
+ en-US
+ 16.0
+ false
+ false
+ Windows Store
+ 10.0
+ true
+ 10.0.20348.0
+ 10.0.19041.0
+ true
+ true
+ true
+
+
+
+ DynamicLibrary
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\MeasureTool\
+
+
+
+ Level4
+ %(AdditionalOptions) /bigobj
+ $(SolutionDir)src\;..\..\..\common\Telemetry;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Windows
+ false
+ MeasureTool.def
+ Shell32.lib;Shcore.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)
+
+
+
+
+ Use
+ pch.h
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PowerToys.MeasureToolCore.idl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PowerToys.MeasureToolCore.idl
+
+
+ Create
+
+
+
+
+
+
+
+ 2
+ true
+
+
+
+
+
+
+
+ {caba8dfb-823b-4bf2-93ac-3f31984150d9}
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+ {98537082-0fdb-40de-abd8-0dc5a4269bab}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters
new file mode 100644
index 0000000000..316a5b7aa6
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {e48dc53e-40b1-40cb-970a-f89935452892}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp
new file mode 100644
index 0000000000..00b60a3897
--- /dev/null
+++ b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp
@@ -0,0 +1,443 @@
+#include "pch.h"
+
+#include "BGRATextureView.h"
+#include "constants.h"
+#include "CoordinateSystemConversion.h"
+#include "EdgeDetection.h"
+#include "ScreenCapturing.h"
+
+#include
+
+//#define DEBUG_EDGES
+
+class MappedTextureView
+{
+ winrt::com_ptr context;
+ winrt::com_ptr texture;
+
+public:
+ BGRATextureView view;
+ MappedTextureView(winrt::com_ptr _texture,
+ winrt::com_ptr _context,
+ const size_t textureWidth,
+ const size_t textureHeight) :
+ texture{ std::move(_texture) }, context{ std::move(_context) }
+ {
+ D3D11_TEXTURE2D_DESC desc;
+ texture->GetDesc(&desc);
+
+ D3D11_MAPPED_SUBRESOURCE resource = {};
+ winrt::check_hresult(context->Map(texture.get(), D3D11CalcSubresource(0, 0, 0), D3D11_MAP_READ, 0, &resource));
+
+ view.pixels = static_cast(resource.pData);
+ view.pitch = resource.RowPitch / 4;
+ view.width = textureWidth;
+ view.height = textureHeight;
+ }
+
+ MappedTextureView(MappedTextureView&&) = default;
+ MappedTextureView& operator=(MappedTextureView&&) = default;
+
+ inline winrt::com_ptr GetTexture() const
+ {
+ return texture;
+ }
+
+ ~MappedTextureView()
+ {
+ if (context && texture)
+ context->Unmap(texture.get(), D3D11CalcSubresource(0, 0, 0));
+ }
+};
+
+class D3DCaptureState final
+{
+ winrt::com_ptr d3dDevice;
+ winrt::IDirect3DDevice device;
+ winrt::com_ptr swapChain;
+ winrt::com_ptr context;
+ winrt::SizeInt32 frameSize;
+
+ winrt::DirectXPixelFormat pixelFormat;
+ winrt::Direct3D11CaptureFramePool framePool;
+ winrt::GraphicsCaptureSession session;
+
+ std::function frameCallback;
+ Box monitorArea;
+ bool captureOutsideOfMonitor = false;
+
+ D3DCaptureState(winrt::com_ptr d3dDevice,
+ winrt::IDirect3DDevice _device,
+ winrt::com_ptr _swapChain,
+ winrt::com_ptr _context,
+ const winrt::GraphicsCaptureItem& item,
+ winrt::DirectXPixelFormat _pixelFormat,
+ Box monitorArea,
+ const bool captureOutsideOfMonitor);
+
+ winrt::com_ptr CopyFrameToCPU(const winrt::com_ptr& texture);
+
+ void OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&);
+
+ void StartSessionInPreferredMode();
+
+ std::mutex destructorMutex;
+
+public:
+ static std::unique_ptr Create(winrt::GraphicsCaptureItem item,
+ const winrt::DirectXPixelFormat pixelFormat,
+ Box monitorSize,
+ const bool captureOutsideOfMonitor);
+
+ ~D3DCaptureState();
+
+ void StartCapture(std::function _frameCallback);
+ MappedTextureView CaptureSingleFrame();
+
+ void StopCapture();
+};
+
+D3DCaptureState::D3DCaptureState(winrt::com_ptr _d3dDevice,
+ winrt::IDirect3DDevice _device,
+ winrt::com_ptr _swapChain,
+ winrt::com_ptr _context,
+ const winrt::GraphicsCaptureItem& item,
+ winrt::DirectXPixelFormat _pixelFormat,
+ Box _monitorArea,
+ const bool _captureOutsideOfMonitor) :
+ d3dDevice{ std::move(_d3dDevice) },
+ device{ std::move(_device) },
+ swapChain{ std::move(_swapChain) },
+ context{ std::move(_context) },
+ frameSize{ item.Size() },
+ pixelFormat{ std::move(_pixelFormat) },
+ framePool{ winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 2, item.Size()) },
+ session{ framePool.CreateCaptureSession(item) },
+ monitorArea{ _monitorArea },
+ captureOutsideOfMonitor{ _captureOutsideOfMonitor }
+{
+ framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
+}
+
+winrt::com_ptr D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr& frameTexture)
+{
+ D3D11_TEXTURE2D_DESC desc = {};
+ frameTexture->GetDesc(&desc);
+ desc.Usage = D3D11_USAGE_STAGING;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc.MiscFlags = 0;
+ desc.BindFlags = 0;
+
+ winrt::com_ptr cpuTexture;
+ winrt::check_hresult(d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put()));
+ context->CopyResource(cpuTexture.get(), frameTexture.get());
+
+ return cpuTexture;
+}
+
+template
+auto GetDXGIInterfaceFromObject(winrt::IInspectable const& object)
+{
+ auto access = object.as();
+ winrt::com_ptr result;
+ winrt::check_hresult(access->GetInterface(winrt::guid_of(), result.put_void()));
+ return result;
+}
+
+void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&)
+{
+ // Prevent calling a callback on a partially destroyed state
+ std::unique_lock callbackLock{ destructorMutex };
+
+ bool resized = false;
+ POINT cursorPos = {};
+ GetCursorPos(&cursorPos);
+
+ auto frame = sender.TryGetNextFrame();
+ winrt::check_bool(frame);
+ if (monitorArea.inside(cursorPos) || captureOutsideOfMonitor)
+ {
+ winrt::com_ptr texture;
+ {
+ if (auto newFrameSize = frame.ContentSize(); newFrameSize != frameSize)
+ {
+ winrt::check_hresult(swapChain->ResizeBuffers(2,
+ static_cast(newFrameSize.Height),
+ static_cast(newFrameSize.Width),
+ static_cast(pixelFormat),
+ 0));
+ frameSize = newFrameSize;
+ resized = true;
+ }
+
+ winrt::check_hresult(swapChain->GetBuffer(0, winrt::guid_of(), texture.put_void()));
+ auto gpuTexture = GetDXGIInterfaceFromObject(frame.Surface());
+ texture = CopyFrameToCPU(gpuTexture);
+ MappedTextureView textureView{ texture, context, static_cast(frameSize.Width), static_cast(frameSize.Height) };
+
+ frameCallback(std::move(textureView));
+ }
+ }
+
+ frame.Close();
+
+ DXGI_PRESENT_PARAMETERS presentParameters = {};
+ swapChain->Present1(1, 0, &presentParameters);
+
+ if (resized)
+ {
+ framePool.Recreate(device, pixelFormat, 2, frameSize);
+ }
+}
+
+std::unique_ptr D3DCaptureState::Create(winrt::GraphicsCaptureItem item,
+ const winrt::DirectXPixelFormat pixelFormat,
+ Box monitorArea,
+ const bool captureOutsideOfMonitor)
+{
+ std::lock_guard guard{ gpuAccessLock };
+
+ winrt::com_ptr d3dDevice;
+ UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
+#ifndef NDEBUG
+ flags |= D3D11_CREATE_DEVICE_DEBUG;
+#endif
+ HRESULT hr =
+ D3D11CreateDevice(nullptr,
+ D3D_DRIVER_TYPE_HARDWARE,
+ nullptr,
+ flags,
+ nullptr,
+ 0,
+ D3D11_SDK_VERSION,
+ d3dDevice.put(),
+ nullptr,
+ nullptr);
+ if (hr == DXGI_ERROR_UNSUPPORTED)
+ {
+ hr = D3D11CreateDevice(nullptr,
+ D3D_DRIVER_TYPE_WARP,
+ nullptr,
+ flags,
+ nullptr,
+ 0,
+ D3D11_SDK_VERSION,
+ d3dDevice.put(),
+ nullptr,
+ nullptr);
+ }
+ winrt::check_hresult(hr);
+
+ auto dxgiDevice = d3dDevice.as();
+ winrt::com_ptr d3dDeviceInspectable;
+ winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), d3dDeviceInspectable.put()));
+
+ const DXGI_SWAP_CHAIN_DESC1 desc = {
+ .Width = static_cast(item.Size().Width),
+ .Height = static_cast(item.Size().Height),
+ .Format = static_cast(pixelFormat),
+ .SampleDesc = { .Count = 1, .Quality = 0 },
+ .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
+ .BufferCount = 2,
+ .Scaling = DXGI_SCALING_STRETCH,
+ .SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
+ .AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
+ };
+ winrt::com_ptr adapter;
+ winrt::check_hresult(dxgiDevice->GetParent(winrt::guid_of(), adapter.put_void()));
+ winrt::com_ptr factory;
+ winrt::check_hresult(adapter->GetParent(winrt::guid_of(), factory.put_void()));
+
+ winrt::com_ptr swapChain;
+ winrt::check_hresult(factory->CreateSwapChainForComposition(d3dDevice.get(), &desc, nullptr, swapChain.put()));
+
+ winrt::com_ptr context;
+ d3dDevice->GetImmediateContext(context.put());
+ winrt::check_bool(context);
+
+ // We must create the object in a heap, since we need to pin it in memory to receive callbacks
+ auto statePtr = new D3DCaptureState{ d3dDevice,
+ d3dDeviceInspectable.as(),
+ std::move(swapChain),
+ std::move(context),
+ item,
+ pixelFormat,
+ monitorArea,
+ captureOutsideOfMonitor };
+
+ return std::unique_ptr{ statePtr };
+}
+
+D3DCaptureState::~D3DCaptureState()
+{
+ std::unique_lock callbackLock{ destructorMutex };
+ StopCapture();
+ framePool.Close();
+ device.Close();
+}
+
+void D3DCaptureState::StartSessionInPreferredMode()
+{
+ // Try disable border if possible (available on Windows ver >= 20348)
+ if (auto session3 = session.try_as())
+ {
+ session3.IsBorderRequired(false);
+ }
+
+ session.IsCursorCaptureEnabled(false);
+ session.StartCapture();
+}
+
+void D3DCaptureState::StartCapture(std::function _frameCallback)
+{
+ frameCallback = std::move(_frameCallback);
+ StartSessionInPreferredMode();
+}
+
+MappedTextureView D3DCaptureState::CaptureSingleFrame()
+{
+ std::optional