[Screen Ruler] Better error handling and reuse D3D device (#20223)

* [Screen Ruler] simplify lines calculation

* [Screen Ruler] Add inches and centimeters support

* [Chore] prefer x64 toolset to avoid hitting C1076

* [Screen Ruler] Allow making screenshots in non-continuous mode

* [Screen Ruler] Use single d3d device for all ops

* [Screen Ruler] remove gpu mutex and clean up screen capturing

* [Screen Ruler] handle and log DXGI initialization failure

* [Screen Ruler] Add unhandled exception handler

* [Screen Ruler] comment out Units of Measure setting

* [Screen Ruler] introduce a separate device dedicated for capturing
This commit is contained in:
Andrey Nekrasov
2022-09-05 15:39:56 +03:00
committed by GitHub
parent feead9c68b
commit 9d7c9c1746
40 changed files with 958 additions and 429 deletions

View File

@@ -346,7 +346,6 @@ CSIDL
csignal csignal
cso cso
CSRW CSRW
cstddef
cstdint cstdint
cstdlib cstdlib
cstring cstring
@@ -402,6 +401,7 @@ DBLEPSILON
DCapture DCapture
DCBA DCBA
DCOM DCOM
dcommon
dcomp dcomp
dcompi dcompi
DComposition DComposition
@@ -490,7 +490,6 @@ dreamsofameaningfullife
drivedetectionwarning drivedetectionwarning
dshow dshow
dst dst
DState
DTo DTo
dutil dutil
DVASPECT DVASPECT
@@ -522,6 +521,7 @@ DWORDLONG
dworigin dworigin
dwrite dwrite
dxgi dxgi
dxgidebug
dxgiformat dxgiformat
dxguid dxguid
ecount ecount
@@ -811,6 +811,7 @@ ICapture
icase icase
ICEBLUE ICEBLUE
IClass IClass
IClosable
ICollection ICollection
IColor IColor
ICommand ICommand
@@ -881,7 +882,6 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT IMAGERESIZEREXT
imageresizerinput imageresizerinput
imageresizersettings imageresizersettings
TEXTEXTRACTOR
imagingdevices imagingdevices
IMain IMain
IMarkdown IMarkdown
@@ -1278,6 +1278,7 @@ Moq
MOUSEACTIVATE MOUSEACTIVATE
MOUSEHWHEEL MOUSEHWHEEL
MOUSEINPUT MOUSEINPUT
MOUSELEAVE
MOUSEMOVE MOUSEMOVE
MOUSEWHEEL MOUSEWHEEL
MOVESIZEEND MOVESIZEEND
@@ -1632,7 +1633,6 @@ ptd
PTOKEN PTOKEN
PToy PToy
ptr ptr
ptrdiff
ptstr ptstr
PVOID PVOID
pwa pwa
@@ -1746,6 +1746,7 @@ RIGHTSCROLLBAR
riid riid
riverar riverar
RKey RKey
RLO
RMENU RMENU
RNumber RNumber
roadmap roadmap
@@ -2063,6 +2064,7 @@ testhost
testprocess testprocess
TEXCOORD TEXCOORD
textblock textblock
TEXTEXTRACTOR
TEXTINCLUDE TEXTINCLUDE
THH THH
THICKFRAME THICKFRAME
@@ -2272,7 +2274,6 @@ wchar
WClass WClass
wcout wcout
wcscat wcscat
wcschr
wcscmp wcscmp
wcscpy wcscpy
wcslen wcslen

View File

@@ -32,6 +32,7 @@
<!-- C++ source compile-specific things for all configurations --> <!-- C++ source compile-specific things for all configurations -->
<PropertyGroup> <PropertyGroup>
<PreferredToolArchitecture>x64</PreferredToolArchitecture>
<VcpkgEnabled>false</VcpkgEnabled> <VcpkgEnabled>false</VcpkgEnabled>
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath)</ExternalIncludePath> <ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath)</ExternalIncludePath>
</PropertyGroup> </PropertyGroup>

View File

@@ -119,7 +119,7 @@
<?define ImageResizerSparsePackageAssets=LargeTile.png;SmallTile.png;SplashScreen.png;Square150x150Logo.png;Square44x44Logo.png;storelogo.png;Wide310x150Logo.png?> <?define ImageResizerSparsePackageAssets=LargeTile.png;SmallTile.png;SplashScreen.png;Square150x150Logo.png;Square44x44Logo.png;storelogo.png;Wide310x150Logo.png?>
<?define MeasureToolFiles=CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WinUI.dll;MRM.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;PushNotificationsLongRunningTask.ProxyStub.dll;resources.pri;System.CodeDom.dll;System.Management.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinRT.Runtime.dll;WinUIEdit.dll;WinUIEx.dll;wuceffectsi.dll?> <?define MeasureToolFiles=CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WinUI.dll;MRM.dll;PowerToys.ManagedCommon.dll;PowerToys.Interop.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;PushNotificationsLongRunningTask.ProxyStub.dll;resources.pri;System.CodeDom.dll;System.Management.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinRT.Runtime.dll;WinUIEdit.dll;WinUIEx.dll;wuceffectsi.dll?>
<?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?> <?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>

View File

@@ -238,7 +238,7 @@ inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
} }
/* Handler to trap abort() calls */ /* Handler to trap abort() calls */
inline void AbortHandler(int signal_number) inline void AbortHandler(int /*signal_number*/)
{ {
Logger::error("--- ABORT"); Logger::error("--- ABORT");
try try

View File

@@ -30,7 +30,7 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
auto toolState = GetWindowParam<BoundsToolState*>(window); auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState) if (!toolState)
break; break;
const POINT cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace); const POINT cursorPos = convert::FromSystemToWindow(window, toolState->commonState->cursorPosSystemSpace);
D2D_POINT_2F newRegionStart = { .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(cursorPos.y) }; D2D_POINT_2F newRegionStart = { .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(cursorPos.y) };
toolState->perScreen[window].currentRegionStart = newRegionStart; toolState->perScreen[window].currentRegionStart = newRegionStart;
@@ -38,6 +38,8 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
} }
case WM_CURSOR_LEFT_MONITOR: case WM_CURSOR_LEFT_MONITOR:
{ {
for (; ShowCursor(true) < 0;)
;
auto toolState = GetWindowParam<BoundsToolState*>(window); auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState) if (!toolState)
break; break;
@@ -59,12 +61,12 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress) if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress)
{ {
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace); const auto cursorPos = convert::FromSystemToWindow(window, toolState->commonState->cursorPosSystemSpace);
D2D1_RECT_F rect; D2D1_RECT_F rect;
std::tie(rect.left, rect.right) = std::minmax(static_cast<float>(cursorPos.x), toolState->perScreen[window].currentRegionStart->x); std::tie(rect.left, rect.right) = std::minmax(static_cast<float>(cursorPos.x), toolState->perScreen[window].currentRegionStart->x);
std::tie(rect.top, rect.bottom) = std::minmax(static_cast<float>(cursorPos.y), toolState->perScreen[window].currentRegionStart->y); std::tie(rect.top, rect.bottom) = std::minmax(static_cast<float>(cursorPos.y), toolState->perScreen[window].currentRegionStart->y);
toolState->perScreen[window].measurements.push_back(rect); toolState->perScreen[window].measurements.push_back(Measurement{ rect });
} }
toolState->perScreen[window].currentRegionStart = std::nullopt; toolState->perScreen[window].currentRegionStart = std::nullopt;
@@ -97,45 +99,42 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
namespace namespace
{ {
void DrawMeasurement(const D2D1_RECT_F rect, void DrawMeasurement(const Measurement& measurement,
const bool alignTextBoxToCenter, const bool alignTextBoxToCenter,
const CommonState& commonState, const CommonState& commonState,
HWND window, HWND window,
const D2DState& d2dState) const D2DState& d2dState,
float mouseX,
float mouseY)
{ {
const bool screenQuadrantAware = !alignTextBoxToCenter; const bool screenQuadrantAware = !alignTextBoxToCenter;
const auto prevMode = d2dState.rt->GetAntialiasMode(); d2dState.ToggleAliasedLinesMode(true);
d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); d2dState.dxgiWindowState.rt->DrawRectangle(measurement.rect, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawRectangle(rect, d2dState.solidBrushes[Brush::line].get()); d2dState.ToggleAliasedLinesMode(false);
d2dState.rt->SetAntialiasMode(prevMode);
OverlayBoxText text; OverlayBoxText text;
const auto width = std::abs(rect.right - rect.left + 1); const auto [crossSymbolPos, measureStringBufLen] =
const auto height = std::abs(rect.top - rect.bottom + 1); measurement.Print(text.buffer.data(),
const uint32_t textLen = swprintf_s(text.buffer.data(),
text.buffer.size(), text.buffer.size(),
L"%.0f × %.0f", true,
width, true,
height); commonState.units);
std::optional<size_t> crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
commonState.overlayBoxText.Access([&](OverlayBoxText& v) { commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text; v = text;
}); });
float cornerX = rect.right;
float cornerY = rect.bottom;
if (alignTextBoxToCenter) if (alignTextBoxToCenter)
{ {
cornerX = rect.left + width / 2; mouseX = measurement.rect.left + measurement.Width(Measurement::Unit::Pixel) / 2;
cornerY = rect.top + height / 2; mouseY = measurement.rect.top + measurement.Height(Measurement::Unit::Pixel) / 2;
} }
d2dState.DrawTextBox(text.buffer.data(), d2dState.DrawTextBox(text.buffer.data(),
textLen, measureStringBufLen,
crossSymbolPos, crossSymbolPos,
cornerX, mouseX,
cornerY, mouseY,
screenQuadrantAware, screenQuadrantAware,
window); window);
} }
@@ -150,20 +149,21 @@ void DrawBoundsToolTick(const CommonState& commonState,
if (it == end(toolState.perScreen)) if (it == end(toolState.perScreen))
return; return;
d2dState.rt->Clear(); d2dState.dxgiWindowState.rt->Clear();
const auto& perScreen = it->second; const auto& perScreen = it->second;
for (const auto& measure : perScreen.measurements) for (const auto& measure : perScreen.measurements)
DrawMeasurement(measure, true, commonState, window, d2dState); DrawMeasurement(measure, true, commonState, window, d2dState, measure.rect.right, measure.rect.bottom);
if (!perScreen.currentRegionStart.has_value()) if (!perScreen.currentRegionStart.has_value())
return; return;
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace); const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
const D2D1_RECT_F rect{ .left = perScreen.currentRegionStart->x, D2D1_RECT_F rect;
.top = perScreen.currentRegionStart->y, const float cursorX = static_cast<float>(cursorPos.x);
.right = static_cast<float>(cursorPos.x), const float cursorY = static_cast<float>(cursorPos.y);
.bottom = static_cast<float>(cursorPos.y) }; std::tie(rect.left, rect.right) = std::minmax(cursorX, perScreen.currentRegionStart->x);
DrawMeasurement(rect, false, commonState, window, d2dState); std::tie(rect.top, rect.bottom) = std::minmax(cursorY, perScreen.currentRegionStart->y);
DrawMeasurement(Measurement{ rect }, false, commonState, window, d2dState, cursorX, cursorY);
} }

View File

@@ -6,21 +6,9 @@
namespace convert namespace convert
{ {
// Converts a given point from multi-monitor coordinate system to the one relative to HWND // Converts a given point from multi-monitor coordinate system to the one relative to HWND
inline POINT FromSystemToRelative(HWND window, POINT p) inline POINT FromSystemToWindow(HWND window, POINT p)
{ {
ScreenToClient(window, &p); ScreenToClient(window, &p);
return 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;
}
} }

View File

@@ -2,6 +2,7 @@
#include "constants.h" #include "constants.h"
#include "D2DState.h" #include "D2DState.h"
#include "DxgiAPI.h"
#include <common/Display/dpi_aware.h> #include <common/Display/dpi_aware.h>
#include <ToolState.h> #include <ToolState.h>
@@ -19,43 +20,28 @@ namespace
} }
} }
D2DState::D2DState(const HWND overlayWindow, std::vector<D2D1::ColorF> solidBrushesColors) D2DState::D2DState(const DxgiAPI* dxgi,
HWND window,
std::vector<D2D1::ColorF> solidBrushesColors)
{ {
std::lock_guard guard{ gpuAccessLock }; dxgiAPI = dxgi;
RECT clientRect = {};
winrt::check_bool(GetClientRect(overlayWindow, &clientRect));
winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &d2dFactory));
// 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; unsigned dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForWindow(overlayWindow, dpi); DPIAware::GetScreenDPIForWindow(window, dpi);
dpiScale = dpi / static_cast<float>(DPIAware::DEFAULT_DPI); dpiScale = dpi / static_cast<float>(DPIAware::DEFAULT_DPI);
winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), writeFactory.put_unknown())); dxgiWindowState = dxgiAPI->CreateD2D1RenderTarget(window);
winrt::check_hresult(writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
winrt::check_hresult(dxgiWindowState.rt->CreateCompatibleRenderTarget(bitmapRt.put()));
winrt::check_hresult(dxgiAPI->writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
nullptr, nullptr,
DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
consts::FONT_SIZE * dpiScale, consts::FONT_SIZE * dpiScale,
L"en-US", L"en-US",
&textFormat)); textFormat.put()));
winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)); winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER)); winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
@@ -63,22 +49,22 @@ D2DState::D2DState(const HWND overlayWindow, std::vector<D2D1::ColorF> solidBrus
solidBrushes.resize(solidBrushesColors.size()); solidBrushes.resize(solidBrushesColors.size());
for (size_t i = 0; i < solidBrushes.size(); ++i) for (size_t i = 0; i < solidBrushes.size(); ++i)
{ {
winrt::check_hresult(rt->CreateSolidColorBrush(solidBrushesColors[i], &solidBrushes[i])); winrt::check_hresult(dxgiWindowState.rt->CreateSolidColorBrush(solidBrushesColors[i], solidBrushes[i].put()));
} }
const auto deviceContext = rt.query<ID2D1DeviceContext>(); const auto deviceContext = dxgiWindowState.rt.as<ID2D1DeviceContext>();
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, &shadowEffect)); winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, shadowEffect.put()));
winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, consts::SHADOW_RADIUS)); 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(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)); winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D12DAffineTransform, affineTransformEffect.put()));
affineTransformEffect->SetInputEffect(0, shadowEffect.get()); affineTransformEffect->SetInputEffect(0, shadowEffect.get());
textRenderer = winrt::make_self<PerGlyphOpacityTextRender>(d2dFactory, rt, solidBrushes[Brush::foreground]); textRenderer = winrt::make_self<PerGlyphOpacityTextRender>(dxgi->d2dFactory2, dxgiWindowState.rt, solidBrushes[Brush::foreground]);
} }
void D2DState::DrawTextBox(const wchar_t* text, void D2DState::DrawTextBox(const wchar_t* text,
const uint32_t textLen, const size_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos, const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX, const float centerX,
const float centerY, const float centerY,
@@ -86,16 +72,19 @@ void D2DState::DrawTextBox(const wchar_t* text,
const HWND window) const const HWND window) const
{ {
wil::com_ptr<IDWriteTextLayout> textLayout; wil::com_ptr<IDWriteTextLayout> textLayout;
winrt::check_hresult(writeFactory->CreateTextLayout(text, winrt::check_hresult(
textLen, dxgiAPI->writeFactory->CreateTextLayout(text,
static_cast<uint32_t>(textLen),
textFormat.get(), textFormat.get(),
std::numeric_limits<float>::max(), std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(), std::numeric_limits<float>::max(),
&textLayout)); &textLayout));
DWRITE_TEXT_METRICS textMetrics = {}; DWRITE_TEXT_METRICS textMetrics = {};
winrt::check_hresult(textLayout->GetMetrics(&textMetrics)); winrt::check_hresult(textLayout->GetMetrics(&textMetrics));
textMetrics.width *= consts::TEXT_BOX_MARGIN_COEFF; // Assumes text doesn't contain new lines
textMetrics.height *= consts::TEXT_BOX_MARGIN_COEFF; const float lineHeight = textMetrics.height;
textMetrics.width += lineHeight;
textMetrics.height += lineHeight * .5f;
winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width)); winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width));
winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height)); winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height));
@@ -103,6 +92,8 @@ void D2DState::DrawTextBox(const wchar_t* text,
.top = centerY - textMetrics.height / 2.f, .top = centerY - textMetrics.height / 2.f,
.right = centerX + textMetrics.width / 2.f, .right = centerX + textMetrics.width / 2.f,
.bottom = centerY + textMetrics.height / 2.f }; .bottom = centerY + textMetrics.height / 2.f };
const float SHADOW_OFFSET = consts::SHADOW_OFFSET * dpiScale;
if (screenQuadrantAware) if (screenQuadrantAware)
{ {
bool cursorInLeftScreenHalf = false; bool cursorInLeftScreenHalf = false;
@@ -112,8 +103,8 @@ void D2DState::DrawTextBox(const wchar_t* text,
static_cast<long>(centerY), static_cast<long>(centerY),
cursorInLeftScreenHalf, cursorInLeftScreenHalf,
cursorInTopScreenHalf); cursorInTopScreenHalf);
float textQuadrantOffsetX = textMetrics.width * dpiScale; float textQuadrantOffsetX = textMetrics.width / 2.f + SHADOW_OFFSET;
float textQuadrantOffsetY = textMetrics.height * dpiScale; float textQuadrantOffsetY = textMetrics.height / 2.f + SHADOW_OFFSET;
if (!cursorInLeftScreenHalf) if (!cursorInLeftScreenHalf)
textQuadrantOffsetX *= -1.f; textQuadrantOffsetX *= -1.f;
if (!cursorInTopScreenHalf) if (!cursorInTopScreenHalf)
@@ -140,15 +131,14 @@ void D2DState::DrawTextBox(const wchar_t* text,
bitmapRt->GetBitmap(&rtBitmap); bitmapRt->GetBitmap(&rtBitmap);
shadowEffect->SetInput(0, rtBitmap.get()); shadowEffect->SetInput(0, rtBitmap.get());
const auto shadowMatrix = D2D1::Matrix3x2F::Translation(consts::SHADOW_OFFSET * dpiScale, const auto shadowMatrix = D2D1::Matrix3x2F::Translation(SHADOW_OFFSET, SHADOW_OFFSET);
consts::SHADOW_OFFSET * dpiScale);
winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX, winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX,
shadowMatrix)); shadowMatrix));
auto deviceContext = rt.query<ID2D1DeviceContext>(); auto deviceContext = dxgiWindowState.rt.as<ID2D1DeviceContext>();
deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR); deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR);
// Draw text box border rectangle // Draw text box border rectangle
rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get()); dxgiWindowState.rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
const float TEXT_BOX_PADDING = 1.f * dpiScale; const float TEXT_BOX_PADDING = 1.f * dpiScale;
textBoxRect.rect.bottom -= TEXT_BOX_PADDING; textBoxRect.rect.bottom -= TEXT_BOX_PADDING;
textBoxRect.rect.top += TEXT_BOX_PADDING; textBoxRect.rect.top += TEXT_BOX_PADDING;
@@ -156,7 +146,7 @@ void D2DState::DrawTextBox(const wchar_t* text,
textBoxRect.rect.right -= TEXT_BOX_PADDING; textBoxRect.rect.right -= TEXT_BOX_PADDING;
// Draw text & its box // Draw text & its box
rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get()); dxgiWindowState.rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get());
if (halfOpaqueSymbolPos.has_value()) if (halfOpaqueSymbolPos.has_value())
{ {
@@ -167,3 +157,19 @@ void D2DState::DrawTextBox(const wchar_t* text,
} }
winrt::check_hresult(textLayout->Draw(nullptr, textRenderer.get(), textRect.left, textRect.top)); winrt::check_hresult(textLayout->Draw(nullptr, textRenderer.get(), textRect.left, textRect.top));
} }
void D2DState::ToggleAliasedLinesMode(const bool enabled) const
{
if (enabled)
{
// Draw lines in the middle of a pixel to avoid bleeding, since [0,0] pixel is
// a rectangle filled from (0,0) to (1,1) and the lines use thickness = 1.
dxgiWindowState.rt->SetTransform(D2D1::Matrix3x2F::Translation(.5f, .5f));
dxgiWindowState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
}
else
{
dxgiWindowState.rt->SetTransform(D2D1::Matrix3x2F::Identity());
dxgiWindowState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
}
}

View File

@@ -3,10 +3,9 @@
#include <optional> #include <optional>
#include <vector> #include <vector>
#include <d2d1_3.h>
#include <wil/com.h>
#include <windef.h> #include <windef.h>
#include "DxgiAPI.h"
#include "PerGlyphOpacityTextRender.h" #include "PerGlyphOpacityTextRender.h"
enum Brush : size_t enum Brush : size_t
@@ -19,24 +18,27 @@ enum Brush : size_t
struct D2DState struct D2DState
{ {
wil::com_ptr<ID2D1Factory> d2dFactory; const DxgiAPI* dxgiAPI = nullptr;
wil::com_ptr<IDWriteFactory> writeFactory;
wil::com_ptr<ID2D1HwndRenderTarget> rt; DxgiWindowState dxgiWindowState;
wil::com_ptr<ID2D1BitmapRenderTarget> bitmapRt; winrt::com_ptr<ID2D1BitmapRenderTarget> bitmapRt;
wil::com_ptr<IDWriteTextFormat> textFormat; winrt::com_ptr<IDWriteTextFormat> textFormat;
winrt::com_ptr<PerGlyphOpacityTextRender> textRenderer; winrt::com_ptr<PerGlyphOpacityTextRender> textRenderer;
std::vector<wil::com_ptr<ID2D1SolidColorBrush>> solidBrushes; std::vector<winrt::com_ptr<ID2D1SolidColorBrush>> solidBrushes;
wil::com_ptr<ID2D1Effect> shadowEffect; winrt::com_ptr<ID2D1Effect> shadowEffect;
wil::com_ptr<ID2D1Effect> affineTransformEffect; winrt::com_ptr<ID2D1Effect> affineTransformEffect;
float dpiScale = 1.f; float dpiScale = 1.f;
D2DState(const HWND window, std::vector<D2D1::ColorF> solidBrushesColors); D2DState(const DxgiAPI*,
HWND window,
std::vector<D2D1::ColorF> solidBrushesColors);
void DrawTextBox(const wchar_t* text, void DrawTextBox(const wchar_t* text,
const uint32_t textLen, const size_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos, const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX, const float centerX,
const float centerY, const float centerY,
const bool screenQuadrantAware, const bool screenQuadrantAware,
const HWND window) const; const HWND window) const;
void ToggleAliasedLinesMode(const bool enabled) const;
}; };

View File

@@ -0,0 +1,145 @@
#include "pch.h"
#include "DxgiAPI.h"
#include <common/Display/dpi_aware.h>
//#define DEBUG_DEVICES
#define SEPARATE_D3D_FOR_CAPTURE
namespace
{
DxgiAPI::D3D CreateD3D()
{
DxgiAPI::D3D d3d;
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(DEBUG_DEVICES)
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
HRESULT hr =
D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3d.d3dDevice.put(),
nullptr,
nullptr);
if (hr == DXGI_ERROR_UNSUPPORTED)
{
hr = D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3d.d3dDevice.put(),
nullptr,
nullptr);
}
winrt::check_hresult(hr);
d3d.dxgiDevice = d3d.d3dDevice.as<IDXGIDevice>();
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(d3d.dxgiDevice.get(), d3d.d3dDeviceInspectable.put()));
winrt::com_ptr<IDXGIAdapter> adapter;
winrt::check_hresult(d3d.dxgiDevice->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), d3d.dxgiFactory2.put_void()));
d3d.d3dDevice->GetImmediateContext(d3d.d3dContext.put());
winrt::check_bool(d3d.d3dContext);
auto contextMultithread = d3d.d3dContext.as<ID3D11Multithread>();
contextMultithread->SetMultithreadProtected(true);
return d3d;
}
}
DxgiAPI::DxgiAPI()
{
const D2D1_FACTORY_OPTIONS d2dFactoryOptions = {
#if defined(DEBUG_DEVICES)
D2D1_DEBUG_LEVEL_INFORMATION
#else
D2D1_DEBUG_LEVEL_NONE
#endif
};
winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, d2dFactoryOptions, d2dFactory2.put()));
winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
winrt::guid_of<IDWriteFactory>(),
reinterpret_cast<IUnknown**>(writeFactory.put())));
auto d3d = CreateD3D();
d3dDevice = d3d.d3dDevice;
dxgiDevice = d3d.dxgiDevice;
d3dDeviceInspectable = d3d.d3dDeviceInspectable;
dxgiFactory2 = d3d.dxgiFactory2;
d3dContext = d3d.d3dContext;
#if defined(SEPARATE_D3D_FOR_CAPTURE)
auto d3dFC = CreateD3D();
d3dForCapture = d3dFC;
#else
d3dForCapture = d3d;
#endif
winrt::check_hresult(d2dFactory2->CreateDevice(dxgiDevice.get(), d2dDevice1.put()));
winrt::check_hresult(DCompositionCreateDevice(
dxgiDevice.get(),
winrt::guid_of<IDCompositionDevice>(),
compositionDevice.put_void()));
}
DxgiWindowState DxgiAPI::CreateD2D1RenderTarget(HWND window) const
{
RECT rect = {};
winrt::check_bool(GetClientRect(window, &rect));
const DXGI_SWAP_CHAIN_DESC1 desc = {
.Width = static_cast<UINT>(rect.right - rect.left),
.Height = static_cast<UINT>(rect.bottom - rect.top),
.Format = static_cast<DXGI_FORMAT>(winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized),
.SampleDesc = { .Count = 1, .Quality = 0 },
.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
.BufferCount = 2,
.Scaling = DXGI_SCALING_STRETCH,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
};
DxgiWindowState state;
winrt::com_ptr<ID2D1DeviceContext> rt;
d2dDevice1->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, rt.put());
state.rt = rt;
winrt::check_hresult(dxgiFactory2->CreateSwapChainForComposition(d3dDevice.get(),
&desc,
nullptr,
state.swapChain.put()));
winrt::com_ptr<IDXGISurface> surface;
winrt::check_hresult(state.swapChain->GetBuffer(0, winrt::guid_of<IDXGISurface>(), surface.put_void()));
const D2D1_BITMAP_PROPERTIES1 properties = {
.pixelFormat = { .format = DXGI_FORMAT_B8G8R8A8_UNORM, .alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED },
.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW
};
winrt::com_ptr<ID2D1Bitmap1> bitmap;
winrt::check_hresult(rt->CreateBitmapFromDxgiSurface(surface.get(),
properties,
bitmap.put()));
rt->SetTarget(bitmap.get());
winrt::check_hresult(compositionDevice->CreateTargetForHwnd(window,
true,
state.compositionTarget.put()));
winrt::com_ptr<IDCompositionVisual> visual;
winrt::check_hresult(compositionDevice->CreateVisual(visual.put()));
winrt::check_hresult(visual->SetContent(state.swapChain.get()));
winrt::check_hresult(state.compositionTarget->SetRoot(visual.get()));
winrt::check_hresult(compositionDevice->Commit());
return state;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <d2d1_3.h>
#include <d3d11_4.h>
#include <dcomp.h>
#include <dxgi1_3.h>
#include <inspectable.h>
#include <winrt/base.h>
struct DxgiWindowState
{
winrt::com_ptr<ID2D1RenderTarget> rt;
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::com_ptr<IDCompositionTarget> compositionTarget;
};
struct DxgiAPI final
{
struct D3D
{
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
winrt::com_ptr<IDXGIFactory2> dxgiFactory2;
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
};
winrt::com_ptr<ID2D1Factory2> d2dFactory2;
winrt::com_ptr<IDWriteFactory> writeFactory;
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
winrt::com_ptr<IDXGIFactory2> dxgiFactory2;
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
D3D d3dForCapture;
winrt::com_ptr<ID2D1Device1> d2dDevice1;
winrt::com_ptr<IDCompositionDevice> compositionDevice;
DxgiAPI();
enum class Uninitialized
{
};
explicit inline DxgiAPI(Uninitialized) {}
DxgiWindowState CreateD2D1RenderTarget(HWND window) const;
};

View File

@@ -2,8 +2,8 @@
#include "constants.h" #include "constants.h"
#include "EdgeDetection.h" #include "EdgeDetection.h"
template<bool PerChannel, template<bool PerChannel,
bool ContinuousCapture,
bool IsX, bool IsX,
bool Increment> bool Increment>
inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance) inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance)
@@ -57,25 +57,21 @@ inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, co
return Increment ? static_cast<long>(IsX ? texture.width : texture.height) - 1 : 0; return Increment ? static_cast<long>(IsX ? texture.width : texture.height) - 1 : 0;
} }
template<bool PerChannel, bool ContinuousCapture> template<bool PerChannel>
inline RECT DetectEdgesInternal(const BGRATextureView& texture, inline RECT DetectEdgesInternal(const BGRATextureView& texture,
const POINT centerPoint, const POINT centerPoint,
const uint8_t tolerance) const uint8_t tolerance)
{ {
return RECT{ .left = FindEdge<PerChannel, return RECT{ .left = FindEdge<PerChannel,
ContinuousCapture,
true, true,
false>(texture, centerPoint, tolerance), false>(texture, centerPoint, tolerance),
.top = FindEdge<PerChannel, .top = FindEdge<PerChannel,
ContinuousCapture,
false, false,
false>(texture, centerPoint, tolerance), false>(texture, centerPoint, tolerance),
.right = FindEdge<PerChannel, .right = FindEdge<PerChannel,
ContinuousCapture,
true, true,
true>(texture, centerPoint, tolerance), true>(texture, centerPoint, tolerance),
.bottom = FindEdge<PerChannel, .bottom = FindEdge<PerChannel,
ContinuousCapture,
false, false,
true>(texture, centerPoint, tolerance) }; true>(texture, centerPoint, tolerance) };
} }
@@ -83,12 +79,9 @@ inline RECT DetectEdgesInternal(const BGRATextureView& texture,
RECT DetectEdges(const BGRATextureView& texture, RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint, const POINT centerPoint,
const bool perChannel, const bool perChannel,
const uint8_t tolerance, const uint8_t tolerance)
const bool continuousCapture)
{ {
auto function = perChannel ? &DetectEdgesInternal<true, false> : DetectEdgesInternal<false, false>; auto function = perChannel ? &DetectEdgesInternal<true> : DetectEdgesInternal<false>;
if (continuousCapture)
function = perChannel ? &DetectEdgesInternal<true, true> : &DetectEdgesInternal<false, true>;
return function(texture, centerPoint, tolerance); return function(texture, centerPoint, tolerance);
} }

View File

@@ -5,5 +5,4 @@
RECT DetectEdges(const BGRATextureView& texture, RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint, const POINT centerPoint,
const bool perChannel, const bool perChannel,
const uint8_t tolerance, const uint8_t tolerance);
const bool continuousCapture);

View File

@@ -16,30 +16,22 @@ namespace
// Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines // Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines
if (horizontal) if (horizontal)
{ {
start.x -= consts::FEET_HALF_LENGTH + 1.f; start.x -= consts::FEET_HALF_LENGTH;
end.x += consts::FEET_HALF_LENGTH; end.x += consts::FEET_HALF_LENGTH + 1.f;
start.y += 1.f;
end.y += 1.f;
} }
else else
{ {
start.y -= consts::FEET_HALF_LENGTH + 1.f; start.y -= consts::FEET_HALF_LENGTH;
end.y += consts::FEET_HALF_LENGTH; end.y += consts::FEET_HALF_LENGTH + 1.f;
start.x += 1.f;
end.x += 1.f;
} }
return { start, end }; return { start, end };
} }
} }
winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(wil::com_ptr<ID2D1HwndRenderTarget> rt, winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(winrt::com_ptr<ID2D1RenderTarget> rt,
const MappedTextureView* capturedScreenTexture) const MappedTextureView* capturedScreenTexture)
{ {
std::lock_guard guard{ gpuAccessLock };
capturedScreenTexture->view.pixels; capturedScreenTexture->view.pixels;
D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() }; D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() };
@@ -62,6 +54,7 @@ LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LP
{ {
switch (message) switch (message)
{ {
case WM_MOUSELEAVE:
case WM_CURSOR_LEFT_MONITOR: case WM_CURSOR_LEFT_MONITOR:
{ {
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window)) if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
@@ -130,20 +123,27 @@ void DrawMeasureToolTick(const CommonState& commonState,
bool drawFeetOnCross = {}; bool drawFeetOnCross = {};
bool drawHorizontalCrossLine = true; bool drawHorizontalCrossLine = true;
bool drawVerticalCrossLine = true; bool drawVerticalCrossLine = true;
RECT measuredEdges{};
Measurement measuredEdges{};
MeasureToolState::Mode mode = {}; MeasureToolState::Mode mode = {};
winrt::com_ptr<ID2D1Bitmap> backgroundBitmap; winrt::com_ptr<ID2D1Bitmap> backgroundBitmap;
const MappedTextureView* backgroundTextureToConvert = nullptr; const MappedTextureView* backgroundTextureToConvert = nullptr;
bool gotMeasurement = false;
toolState.Read([&](const MeasureToolState& state) { toolState.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture; continuousCapture = state.global.continuousCapture;
drawFeetOnCross = state.global.drawFeetOnCross; drawFeetOnCross = state.global.drawFeetOnCross;
mode = state.global.mode; mode = state.global.mode;
if (auto it = state.perScreen.find(window); it != end(state.perScreen)) if (auto it = state.perScreen.find(window); it != end(state.perScreen))
{ {
const auto& perScreen = it->second; const auto& perScreen = it->second;
measuredEdges = perScreen.measuredEdges; if (!perScreen.measuredEdges)
{
return;
}
gotMeasurement = true;
measuredEdges = *perScreen.measuredEdges;
if (continuousCapture) if (continuousCapture)
return; return;
@@ -158,6 +158,10 @@ void DrawMeasureToolTick(const CommonState& commonState,
} }
} }
}); });
if (!gotMeasurement)
return;
switch (mode) switch (mode)
{ {
case MeasureToolState::Mode::Cross: case MeasureToolState::Mode::Cross:
@@ -176,7 +180,7 @@ void DrawMeasureToolTick(const CommonState& commonState,
if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert) if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
{ {
backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.rt, backgroundTextureToConvert); backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.dxgiWindowState.rt, backgroundTextureToConvert);
if (backgroundBitmap) if (backgroundBitmap)
{ {
toolState.Access([&](MeasureToolState& state) { toolState.Access([&](MeasureToolState& state) {
@@ -187,28 +191,24 @@ void DrawMeasureToolTick(const CommonState& commonState,
} }
if (continuousCapture || !backgroundBitmap) if (continuousCapture || !backgroundBitmap)
d2dState.rt->Clear(); d2dState.dxgiWindowState.rt->Clear();
// Add 1px to each dim, since the range we obtain from measuredEdges is inclusive. const float hMeasure = measuredEdges.Width(Measurement::Unit::Pixel);
const float hMeasure = static_cast<float>(measuredEdges.right - measuredEdges.left + 1); const float vMeasure = measuredEdges.Height(Measurement::Unit::Pixel);
const float vMeasure = static_cast<float>(measuredEdges.bottom - measuredEdges.top + 1);
if (!continuousCapture && backgroundBitmap) if (!continuousCapture && backgroundBitmap)
{ {
d2dState.rt->DrawBitmap(backgroundBitmap.get()); d2dState.dxgiWindowState.rt->DrawBitmap(backgroundBitmap.get());
} }
const auto previousAliasingMode = d2dState.rt->GetAntialiasMode(); const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
// 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);
d2dState.ToggleAliasedLinesMode(true);
if (drawHorizontalCrossLine) if (drawHorizontalCrossLine)
{ {
const D2D_POINT_2F hLineStart{ .x = static_cast<float>(measuredEdges.left), .y = static_cast<float>(cursorPos.y) }; const D2D_POINT_2F hLineStart{ .x = measuredEdges.rect.left, .y = static_cast<float>(cursorPos.y) };
D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y }; D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
d2dState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross) if (drawFeetOnCross)
{ {
@@ -218,57 +218,37 @@ void DrawMeasureToolTick(const CommonState& commonState,
hLineEnd.x -= 1.f; hLineEnd.x -= 1.f;
auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false); auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false); auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
d2dState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
} }
} }
if (drawVerticalCrossLine) if (drawVerticalCrossLine)
{ {
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(measuredEdges.top) }; const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = measuredEdges.rect.top };
D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure }; D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
d2dState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross) if (drawFeetOnCross)
{ {
vLineEnd.y -= 1.f; vLineEnd.y -= 1.f;
auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true); auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true); auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
d2dState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get()); d2dState.dxgiWindowState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
} }
} }
// After drawing the lines, restore anti aliasing to draw the measurement tooltip. d2dState.ToggleAliasedLinesMode(false);
d2dState.rt->SetAntialiasMode(previousAliasingMode);
uint32_t measureStringBufLen = 0;
OverlayBoxText text; OverlayBoxText text;
std::optional<size_t> crossSymbolPos;
switch (mode) const auto [crossSymbolPos, measureStringBufLen] =
{ measuredEdges.Print(text.buffer.data(),
case MeasureToolState::Mode::Cross:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(), text.buffer.size(),
L"%.0f × %.0f", drawHorizontalCrossLine,
hMeasure, drawVerticalCrossLine,
vMeasure); commonState.units);
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) { commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text; v = text;

View File

@@ -0,0 +1,91 @@
#include "pch.h"
#include "Measurement.h"
Measurement::Measurement(RECT winRect)
{
rect.left = static_cast<float>(winRect.left);
rect.right = static_cast<float>(winRect.right);
rect.top = static_cast<float>(winRect.top);
rect.bottom = static_cast<float>(winRect.bottom);
}
Measurement::Measurement(D2D1_RECT_F d2dRect) :
rect{ d2dRect }
{
}
namespace
{
inline float Convert(const float pixels, const Measurement::Unit units)
{
switch (units)
{
case Measurement::Unit::Pixel:
return pixels;
case Measurement::Unit::Inch:
return pixels / 96.f;
case Measurement::Unit::Centimetre:
return pixels / 96.f * 2.54f;
default:
return pixels;
}
}
}
inline float Measurement::Width(const Unit units) const
{
return Convert(rect.right - rect.left + 1.f, units);
}
inline float Measurement::Height(const Unit units) const
{
return Convert(rect.bottom - rect.top + 1.f, units);
}
Measurement::PrintResult Measurement::Print(wchar_t* buf,
const size_t bufSize,
const bool printWidth,
const bool printHeight,
const Unit units) const
{
PrintResult result;
if (printWidth)
{
result.strLen += swprintf_s(buf,
bufSize,
L"%g",
Width(units));
if (printHeight)
{
result.crossSymbolPos = result.strLen + 1;
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" \x00D7 ");
}
}
if (printHeight)
{
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L"%g",
Height(units));
}
switch (units)
{
case Measurement::Unit::Inch:
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" in");
break;
case Measurement::Unit::Centimetre:
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" cm");
break;
}
return result;
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <dcommon.h>
#include <windef.h>
struct Measurement
{
enum Unit
{
Pixel,
Inch,
Centimetre
};
D2D1_RECT_F rect = {}; // corners are inclusive
Measurement() = default;
Measurement(const Measurement&) = default;
Measurement& operator=(const Measurement&) = default;
explicit Measurement(D2D1_RECT_F d2dRect);
explicit Measurement(RECT winRect);
float Width(const Unit units) const;
float Height(const Unit units) const;
struct PrintResult
{
std::optional<size_t> crossSymbolPos;
size_t strLen = {};
};
PrintResult Print(wchar_t* buf,
const size_t bufSize,
const bool printWidth,
const bool printHeight,
const Unit units) const;
};

View File

@@ -1,6 +1,7 @@
#include "pch.h" #include "pch.h"
#include "BoundsToolOverlayUI.h" #include "BoundsToolOverlayUI.h"
#include "constants.h"
#include "MeasureToolOverlayUI.h" #include "MeasureToolOverlayUI.h"
#include "OverlayUI.h" #include "OverlayUI.h"
@@ -22,16 +23,17 @@ void CreateOverlayWindowClasses()
wcex.lpfnWndProc = MeasureToolWndProc; wcex.lpfnWndProc = MeasureToolWndProc;
wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName; wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName;
wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
RegisterClassExW(&wcex); RegisterClassExW(&wcex);
wcex.lpfnWndProc = BoundsToolWndProc; wcex.lpfnWndProc = BoundsToolWndProc;
wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName; wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName;
wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
RegisterClassExW(&wcex); RegisterClassExW(&wcex);
} }
HWND CreateOverlayUIWindow(const CommonState& commonState, HWND CreateOverlayUIWindow(const CommonState& commonState,
const MonitorInfo& monitor, const MonitorInfo& monitor,
const bool excludeFromCapture,
const wchar_t* windowClass, const wchar_t* windowClass,
void* extraParam) void* extraParam)
{ {
@@ -39,7 +41,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses); std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses);
const auto screenArea = monitor.GetScreenSize(true); const auto screenArea = monitor.GetScreenSize(true);
DWORD windowStyle = WS_EX_TOOLWINDOW; DWORD windowStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW;
#if !defined(DEBUG_OVERLAY) #if !defined(DEBUG_OVERLAY)
windowStyle |= WS_EX_TOPMOST; windowStyle |= WS_EX_TOPMOST;
#endif #endif
@@ -47,7 +49,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
CreateWindowExW(windowStyle, CreateWindowExW(windowStyle,
windowClass, windowClass,
L"PowerToys.MeasureToolOverlay", L"PowerToys.MeasureToolOverlay",
WS_POPUP, WS_POPUP | CS_HREDRAW | CS_VREDRAW,
screenArea.left(), screenArea.left(),
screenArea.top(), screenArea.top(),
screenArea.width(), screenArea.width(),
@@ -59,7 +61,11 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
}; };
winrt::check_bool(window); winrt::check_bool(window);
ShowWindow(window, SW_SHOWNORMAL); ShowWindow(window, SW_SHOWNORMAL);
UpdateWindow(window);
if (excludeFromCapture)
{
SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE); SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE);
}
#if !defined(DEBUG_OVERLAY) #if !defined(DEBUG_OVERLAY)
SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE); SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE);
#else #else
@@ -109,51 +115,60 @@ std::vector<D2D1::ColorF> AppendCommonOverlayUIColors(const D2D1::ColorF& lineCo
void OverlayUIState::RunUILoop() void OverlayUIState::RunUILoop()
{ {
bool cursorOnScreen = true;
while (IsWindow(_window) && !_commonState.closeOnOtherMonitors) while (IsWindow(_window) && !_commonState.closeOnOtherMonitors)
{ {
const auto now = std::chrono::high_resolution_clock::now();
const auto cursor = _commonState.cursorPosSystemSpace; const auto cursor = _commonState.cursorPosSystemSpace;
const bool cursorOnScreen = _monitorArea.inside(cursor);
const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor); const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor);
auto& dxgi = _d2dState.dxgiWindowState;
if (cursorOnScreen != _cursorOnScreen) if (_monitorArea.inside(cursor) != cursorOnScreen)
{ {
_cursorOnScreen = cursorOnScreen; cursorOnScreen = !cursorOnScreen;
if (!cursorOnScreen) if (!cursorOnScreen)
{ {
if (_clearOnCursorLeavingScreen)
{
_d2dState.rt->BeginDraw();
_d2dState.rt->Clear();
_d2dState.rt->EndDraw();
}
PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {}); PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {});
} }
} }
if (cursorOnScreen) run_message_loop(true, 1);
{
_d2dState.rt->BeginDraw(); dxgi.rt->BeginDraw();
dxgi.rt->Clear();
if (!cursorOverToolbar) if (!cursorOverToolbar)
_tickFunc(); _tickFunc();
else
_d2dState.rt->Clear();
_d2dState.rt->EndDraw(); dxgi.rt->EndDraw();
dxgi.swapChain->Present(0, 0);
if (cursorOnScreen)
{
const auto frameTime = std::chrono::high_resolution_clock::now() - now;
if (frameTime < consts::TARGET_FRAME_DURATION)
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION - frameTime);
}
}
else
{
// Don't consume resources while nothing could be updated
std::this_thread::sleep_for(std::chrono::milliseconds{ 200 });
} }
run_message_loop(true, 1);
} }
DestroyWindow(_window); DestroyWindow(_window);
} }
template<typename StateT, typename TickFuncT> template<typename StateT, typename TickFuncT>
OverlayUIState::OverlayUIState(StateT& toolState, OverlayUIState::OverlayUIState(const DxgiAPI* dxgiAPI,
StateT& toolState,
TickFuncT tickFunc, TickFuncT tickFunc,
const CommonState& commonState, const CommonState& commonState,
HWND window) : HWND window) :
_window{ window }, _window{ window },
_commonState{ commonState }, _commonState{ commonState },
_d2dState{ window, AppendCommonOverlayUIColors(commonState.lineColor) }, _d2dState{ dxgiAPI, window, AppendCommonOverlayUIColors(commonState.lineColor) },
_tickFunc{ [this, tickFunc, &toolState] { _tickFunc{ [this, tickFunc, &toolState] {
tickFunc(_commonState, toolState, _window, _d2dState); tickFunc(_commonState, toolState, _window, _d2dState);
} } } }
@@ -175,25 +190,30 @@ OverlayUIState::~OverlayUIState()
// Returning unique_ptr, since we need to pin ui state in memory // Returning unique_ptr, since we need to pin ui state in memory
template<typename ToolT, typename TickFuncT> template<typename ToolT, typename TickFuncT>
inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(ToolT& toolState, inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(const DxgiAPI* dxgi,
ToolT& toolState,
TickFuncT tickFunc, TickFuncT tickFunc,
CommonState& commonState, CommonState& commonState,
const wchar_t* toolWindowClassName, const wchar_t* toolWindowClassName,
void* windowParam, void* windowParam,
const MonitorInfo& monitor, const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen) const bool excludeFromCapture)
{ {
wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset); wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset);
std::unique_ptr<OverlayUIState> uiState; std::unique_ptr<OverlayUIState> uiState;
auto threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] { std::thread threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] {
const HWND window = CreateOverlayUIWindow(commonState, monitor, toolWindowClassName, windowParam); OverlayUIState* state = nullptr;
uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ toolState, tickFunc, commonState, window } }; {
auto sinalUICreatedEvent = wil::scope_exit([&] { uiCreatedEvent.SetEvent(); });
const HWND window = CreateOverlayUIWindow(commonState, monitor, excludeFromCapture, toolWindowClassName, windowParam);
uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ dxgi, toolState, tickFunc, commonState, window } };
uiState->_monitorArea = monitor.GetScreenSize(true); 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 // 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 // lifetime is ok here, since we join the thread in destructor
auto* state = uiState.get(); state = uiState.get();
uiCreatedEvent.SetEvent(); }
state->RunUILoop(); state->RunUILoop();
@@ -202,28 +222,40 @@ inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(ToolT& too
}); });
uiCreatedEvent.wait(); uiCreatedEvent.wait();
if (uiState)
uiState->_uiThread = std::move(threadHandle); uiState->_uiThread = std::move(threadHandle);
else if (threadHandle.joinable())
threadHandle.join();
return uiState; return uiState;
} }
std::unique_ptr<OverlayUIState> OverlayUIState::Create(Serialized<MeasureToolState>& toolState, std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi,
Serialized<MeasureToolState>& toolState,
CommonState& commonState, CommonState& commonState,
const MonitorInfo& monitor) const MonitorInfo& monitor)
{ {
return OverlayUIState::CreateInternal(toolState, bool excludeFromCapture = false;
toolState.Read([&](const MeasureToolState& s) {
excludeFromCapture = s.global.continuousCapture;
});
return OverlayUIState::CreateInternal(dxgi,
toolState,
DrawMeasureToolTick, DrawMeasureToolTick,
commonState, commonState,
NonLocalizable::MeasureToolOverlayWindowName, NonLocalizable::MeasureToolOverlayWindowName,
&toolState, &toolState,
monitor, monitor,
true); excludeFromCapture);
} }
std::unique_ptr<OverlayUIState> OverlayUIState::Create(BoundsToolState& toolState, std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi,
BoundsToolState& toolState,
CommonState& commonState, CommonState& commonState,
const MonitorInfo& monitor) const MonitorInfo& monitor)
{ {
return OverlayUIState::CreateInternal(toolState, return OverlayUIState::CreateInternal(dxgi,
toolState,
DrawBoundsToolTick, DrawBoundsToolTick,
commonState, commonState,
NonLocalizable::BoundsToolOverlayWindowName, NonLocalizable::BoundsToolOverlayWindowName,

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "DxgiAPI.h"
#include "D2DState.h" #include "D2DState.h"
#include "ToolState.h" #include "ToolState.h"
#include <common/display/monitors.h> #include <common/display/monitors.h>
@@ -9,7 +11,8 @@
class OverlayUIState final class OverlayUIState final
{ {
template<typename StateT, typename TickFuncT> template<typename StateT, typename TickFuncT>
OverlayUIState(StateT& toolState, OverlayUIState(const DxgiAPI* dxgiAPI,
StateT& toolState,
TickFuncT tickFunc, TickFuncT tickFunc,
const CommonState& commonState, const CommonState& commonState,
HWND window); HWND window);
@@ -20,26 +23,27 @@ class OverlayUIState final
D2DState _d2dState; D2DState _d2dState;
std::function<void()> _tickFunc; std::function<void()> _tickFunc;
std::thread _uiThread; std::thread _uiThread;
bool _cursorOnScreen = true;
bool _clearOnCursorLeavingScreen = false;
template<typename ToolT, typename TickFuncT> template<typename ToolT, typename TickFuncT>
static std::unique_ptr<OverlayUIState> CreateInternal(ToolT& toolState, static std::unique_ptr<OverlayUIState> CreateInternal(const DxgiAPI* dxgi,
ToolT& toolState,
TickFuncT tickFunc, TickFuncT tickFunc,
CommonState& commonState, CommonState& commonState,
const wchar_t* toolWindowClassName, const wchar_t* toolWindowClassName,
void* windowParam, void* windowParam,
const MonitorInfo& monitor, const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen); const bool excludeFromCapture);
public: public:
OverlayUIState(OverlayUIState&&) noexcept = default; OverlayUIState(OverlayUIState&&) noexcept = default;
~OverlayUIState(); ~OverlayUIState();
static std::unique_ptr<OverlayUIState> Create(BoundsToolState& toolState, static std::unique_ptr<OverlayUIState> Create(const DxgiAPI* dxgi,
BoundsToolState& toolState,
CommonState& commonState, CommonState& commonState,
const MonitorInfo& monitor); const MonitorInfo& monitor);
static std::unique_ptr<OverlayUIState> Create(Serialized<MeasureToolState>& toolState, static std::unique_ptr<OverlayUIState> Create(const DxgiAPI* dxgi,
Serialized<MeasureToolState>& toolState,
CommonState& commonState, CommonState& commonState,
const MonitorInfo& monitor); const MonitorInfo& monitor);
inline HWND overlayWindowHandle() const inline HWND overlayWindowHandle() const

View File

@@ -3,9 +3,9 @@
#include "PerGlyphOpacityTextRender.h" #include "PerGlyphOpacityTextRender.h"
PerGlyphOpacityTextRender::PerGlyphOpacityTextRender( PerGlyphOpacityTextRender::PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory, winrt::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt, winrt::com_ptr<ID2D1RenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush) : winrt::com_ptr<ID2D1SolidColorBrush> baseBrush) :
_pD2DFactory{ pD2DFactory.get() }, _pD2DFactory{ pD2DFactory.get() },
_rt{ rt.get() }, _rt{ rt.get() },
_baseBrush{ baseBrush.get() } _baseBrush{ baseBrush.get() }

View File

@@ -17,13 +17,13 @@ struct OpacityEffect : winrt::implements<OpacityEffect, IDrawingEffect>
struct PerGlyphOpacityTextRender : winrt::implements<PerGlyphOpacityTextRender, IDWriteTextRenderer> struct PerGlyphOpacityTextRender : winrt::implements<PerGlyphOpacityTextRender, IDWriteTextRenderer>
{ {
ID2D1Factory* _pD2DFactory = nullptr; ID2D1Factory* _pD2DFactory = nullptr;
ID2D1HwndRenderTarget* _rt = nullptr; ID2D1RenderTarget* _rt = nullptr;
ID2D1SolidColorBrush* _baseBrush = nullptr; ID2D1SolidColorBrush* _baseBrush = nullptr;
PerGlyphOpacityTextRender( PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory, winrt::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt, winrt::com_ptr<ID2D1RenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush); winrt::com_ptr<ID2D1SolidColorBrush> baseBrush);
HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext, HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext,
FLOAT baselineOriginX, FLOAT baselineOriginX,

View File

@@ -3,6 +3,7 @@
#include <common/display/dpi_aware.h> #include <common/display/dpi_aware.h>
#include <common/display/monitors.h> #include <common/display/monitors.h>
#include <common/utils/logger_helper.h> #include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
#include <common/logger/logger.h> #include <common/logger/logger.h>
#include "../MeasureToolModuleInterface/trace.h" #include "../MeasureToolModuleInterface/trace.h"
@@ -14,8 +15,6 @@
//#define DEBUG_PRIMARY_MONITOR_ONLY //#define DEBUG_PRIMARY_MONITOR_ONLY
std::recursive_mutex gpuAccessLock;
namespace winrt::PowerToys::MeasureToolCore::implementation namespace winrt::PowerToys::MeasureToolCore::implementation
{ {
void Core::MouseCaptureThread() void Core::MouseCaptureThread()
@@ -34,17 +33,33 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
_stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset }, _stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset },
_mouseCaptureThread{ [this] { MouseCaptureThread(); } } _mouseCaptureThread{ [this] { MouseCaptureThread(); } }
{ {
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
} }
Core::~Core() Core::~Core()
{ {
_stopMouseCaptureThreadSignal.SetEvent(); Close();
_mouseCaptureThread.join(); }
void Core::Close()
{
ResetState(); ResetState();
Trace::UnregisterProvider();
// avoid triggering d2d debug layer leak on shutdown
dxgiAPI = DxgiAPI{ DxgiAPI::Uninitialized{} };
#if 0
winrt::com_ptr<IDXGIDebug> dxgiDebug;
winrt::check_hresult(DXGIGetDebugInterface1({},
winrt::guid_of<IDXGIDebug>(),
dxgiDebug.put_void()));
dxgiDebug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_ALL);
#endif
if (!_stopMouseCaptureThreadSignal.is_signaled())
_stopMouseCaptureThreadSignal.SetEvent();
if (_mouseCaptureThread.joinable())
_mouseCaptureThread.join();
} }
void Core::ResetState() void Core::ResetState()
@@ -67,6 +82,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
_settings = Settings::LoadFromFile(); _settings = Settings::LoadFromFile();
_commonState.units = _settings.units;
_commonState.lineColor.r = _settings.lineColor[0] / 255.f; _commonState.lineColor.r = _settings.lineColor[0] / 255.f;
_commonState.lineColor.g = _settings.lineColor[1] / 255.f; _commonState.lineColor.g = _settings.lineColor[1] / 255.f;
_commonState.lineColor.b = _settings.lineColor[2] / 255.f; _commonState.lineColor.b = _settings.lineColor[2] / 255.f;
@@ -78,13 +94,15 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
ResetState(); ResetState();
#if defined(DEBUG_PRIMARY_MONITOR_ONLY) #if defined(DEBUG_PRIMARY_MONITOR_ONLY)
const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor(); std::vector<MonitorInfo> monitors = { MonitorInfo::GetPrimaryMonitor() };
const auto& monitorInfo = monitors[0];
#else #else
const auto monitors = MonitorInfo::GetMonitors(true); const auto monitors = MonitorInfo::GetMonitors(true);
for (const auto& monitorInfo : monitors) for (const auto& monitorInfo : monitors)
#endif #endif
{ {
auto overlayUI = OverlayUIState::Create(_boundsToolState, auto overlayUI = OverlayUIState::Create(&dxgiAPI,
_boundsToolState,
_commonState, _commonState,
monitorInfo); monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY) #if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
@@ -120,7 +138,8 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
for (const auto& monitorInfo : monitors) for (const auto& monitorInfo : monitors)
#endif #endif
{ {
auto overlayUI = OverlayUIState::Create(_measureToolState, auto overlayUI = OverlayUIState::Create(&dxgiAPI,
_measureToolState,
_commonState, _commonState,
monitorInfo); monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY) #if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
@@ -133,7 +152,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
for (size_t i = 0; i < monitors.size(); ++i) for (size_t i = 0; i < monitors.size(); ++i)
{ {
auto thread = StartCapturingThread( auto thread = StartCapturingThread(
&_d3dState, &dxgiAPI,
_commonState, _commonState,
_measureToolState, _measureToolState,
_overlayUIStates[i]->overlayWindowHandle(), _overlayUIStates[i]->overlayWindowHandle(),

View File

@@ -8,12 +8,29 @@
#include <common/utils/serialized.h> #include <common/utils/serialized.h>
#include "ScreenCapturing.h" #include "ScreenCapturing.h"
struct PowerToysMisc
{
PowerToysMisc()
{
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
InitUnhandledExceptionHandler();
}
~PowerToysMisc()
{
Trace::UnregisterProvider();
}
};
namespace winrt::PowerToys::MeasureToolCore::implementation namespace winrt::PowerToys::MeasureToolCore::implementation
{ {
struct Core : CoreT<Core> struct Core : PowerToysMisc, CoreT<Core>
{ {
Core(); Core();
~Core(); ~Core();
void Close();
void StartBoundsTool(); void StartBoundsTool();
void StartMeasureTool(const bool horizontal, const bool vertical); void StartMeasureTool(const bool horizontal, const bool vertical);
void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger); void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger);
@@ -22,7 +39,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
float GetDPIScaleForWindow(uint64_t windowHandle); float GetDPIScaleForWindow(uint64_t windowHandle);
void MouseCaptureThread(); void MouseCaptureThread();
D3DState _d3dState; DxgiAPI dxgiAPI;
wil::shared_event _stopMouseCaptureThreadSignal; wil::shared_event _stopMouseCaptureThreadSignal;
std::thread _mouseCaptureThread; std::thread _mouseCaptureThread;

View File

@@ -11,7 +11,7 @@ namespace PowerToys
delegate void ToolSessionCompleted(); delegate void ToolSessionCompleted();
[default_interface] [default_interface]
runtimeclass Core runtimeclass Core : Windows.Foundation.IClosable
{ {
Core(); Core();
void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger); void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger);

View File

@@ -55,7 +55,7 @@
<SubSystem>Windows</SubSystem> <SubSystem>Windows</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata> <GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>MeasureTool.def</ModuleDefinitionFile> <ModuleDefinitionFile>MeasureTool.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;Shcore.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>Dbghelp.lib;Shell32.lib;Shcore.lib;dcomp.lib;DXGI.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
@@ -74,8 +74,11 @@
<ClInclude Include="..\MeasureToolModuleInterface\trace.h" /> <ClInclude Include="..\MeasureToolModuleInterface\trace.h" />
<ClInclude Include="BoundsToolOverlayUI.h" /> <ClInclude Include="BoundsToolOverlayUI.h" />
<ClInclude Include="Clipboard.h" /> <ClInclude Include="Clipboard.h" />
<ClInclude Include="CoordinateSystemConversion.h" />
<ClInclude Include="constants.h" /> <ClInclude Include="constants.h" />
<ClInclude Include="D2DState.h" /> <ClInclude Include="D2DState.h" />
<ClInclude Include="DxgiAPI.h" />
<ClInclude Include="Measurement.h" />
<ClInclude Include="MeasureToolOverlayUI.h" /> <ClInclude Include="MeasureToolOverlayUI.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" /> <ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
@@ -96,7 +99,9 @@
<ClCompile Include="BoundsToolOverlayUI.cpp" /> <ClCompile Include="BoundsToolOverlayUI.cpp" />
<ClCompile Include="Clipboard.cpp" /> <ClCompile Include="Clipboard.cpp" />
<ClCompile Include="D2DState.cpp" /> <ClCompile Include="D2DState.cpp" />
<ClCompile Include="DxgiAPI.cpp" />
<ClCompile Include="EdgeDetection.cpp" /> <ClCompile Include="EdgeDetection.cpp" />
<ClCompile Include="Measurement.cpp" />
<ClCompile Include="MeasureToolOverlayUI.cpp" /> <ClCompile Include="MeasureToolOverlayUI.cpp" />
<ClCompile Include="OverlayUI.cpp" /> <ClCompile Include="OverlayUI.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" /> <ClCompile Include="PerGlyphOpacityTextRender.cpp" />

View File

@@ -17,6 +17,8 @@
<ClCompile Include="..\MeasureToolModuleInterface\trace.cpp" /> <ClCompile Include="..\MeasureToolModuleInterface\trace.cpp" />
<ClCompile Include="Clipboard.cpp" /> <ClCompile Include="Clipboard.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" /> <ClCompile Include="PerGlyphOpacityTextRender.cpp" />
<ClCompile Include="Measurement.cpp" />
<ClCompile Include="DxgiAPI.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@@ -34,6 +36,9 @@
<ClInclude Include="Clipboard.h" /> <ClInclude Include="Clipboard.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" /> <ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="CoordinateSystemConversion.h" />
<ClInclude Include="Measurement.h" />
<ClInclude Include="DxgiAPI.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="Assets"> <Filter Include="Assets">

View File

@@ -10,65 +10,47 @@
//#define DEBUG_EDGES //#define DEBUG_EDGES
D3DState::D3DState() namespace
{ {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; winrt::GraphicsCaptureItem CreateCaptureItemForMonitor(HMONITOR monitor)
#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, auto captureInterop = winrt::get_activation_factory<
D3D_DRIVER_TYPE_WARP, winrt::GraphicsCaptureItem,
nullptr, IGraphicsCaptureItemInterop>();
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3dDevice.put(),
nullptr,
nullptr);
}
winrt::check_hresult(hr);
dxgiDevice = d3dDevice.as<IDXGIDevice>(); winrt::GraphicsCaptureItem item = nullptr;
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), d3dDeviceInspectable.put()));
winrt::check_hresult(captureInterop->CreateForMonitor(
monitor,
winrt::guid_of<winrt::GraphicsCaptureItem>(),
winrt::put_abi(item)));
return item;
}
} }
class D3DCaptureState final class D3DCaptureState final
{ {
D3DState* d3dState = nullptr; DxgiAPI* dxgiAPI = nullptr;
winrt::IDirect3DDevice device; winrt::IDirect3DDevice device;
winrt::com_ptr<IDXGISwapChain1> swapChain; winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::com_ptr<ID3D11DeviceContext> context;
winrt::SizeInt32 frameSize;
winrt::SizeInt32 frameSize;
HMONITOR monitor = {};
winrt::DirectXPixelFormat pixelFormat; winrt::DirectXPixelFormat pixelFormat;
winrt::Direct3D11CaptureFramePool framePool;
winrt::GraphicsCaptureSession session; winrt::Direct3D11CaptureFramePool framePool = nullptr;
winrt::GraphicsCaptureSession session = nullptr;
std::function<void(MappedTextureView)> frameCallback; std::function<void(MappedTextureView)> frameCallback;
Box monitorArea; Box monitorArea;
bool captureOutsideOfMonitor = false; bool continuousCapture = false;
D3DCaptureState(D3DState* d3dState, D3DCaptureState(DxgiAPI* dxgiAPI,
winrt::com_ptr<IDXGISwapChain1> _swapChain, winrt::com_ptr<IDXGISwapChain1> swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context, winrt::DirectXPixelFormat pixelFormat,
const winrt::GraphicsCaptureItem& item, MonitorInfo monitorInfo,
winrt::DirectXPixelFormat _pixelFormat, const bool continuousCapture);
Box monitorArea,
const bool captureOutsideOfMonitor);
winrt::com_ptr<ID3D11Texture2D> CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& texture); winrt::com_ptr<ID3D11Texture2D> CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& texture);
@@ -76,14 +58,13 @@ class D3DCaptureState final
void StartSessionInPreferredMode(); void StartSessionInPreferredMode();
std::mutex destructorMutex; std::mutex frameArrivedMutex;
public: public:
static std::unique_ptr<D3DCaptureState> Create(D3DState* d3dState, static std::unique_ptr<D3DCaptureState> Create(DxgiAPI* dxgiAPI,
winrt::GraphicsCaptureItem item, MonitorInfo monitorInfo,
const winrt::DirectXPixelFormat pixelFormat, const winrt::DirectXPixelFormat pixelFormat,
Box monitorSize, const bool continuousCapture);
const bool captureOutsideOfMonitor);
~D3DCaptureState(); ~D3DCaptureState();
@@ -93,25 +74,19 @@ public:
void StopCapture(); void StopCapture();
}; };
D3DCaptureState::D3DCaptureState(D3DState* _d3dState, D3DCaptureState::D3DCaptureState(DxgiAPI* dxgiAPI,
winrt::com_ptr<IDXGISwapChain1> _swapChain, winrt::com_ptr<IDXGISwapChain1> _swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context, winrt::DirectXPixelFormat pixelFormat_,
const winrt::GraphicsCaptureItem& item, MonitorInfo monitorInfo,
winrt::DirectXPixelFormat _pixelFormat, const bool continuousCapture_) :
Box _monitorArea, dxgiAPI{ dxgiAPI },
const bool _captureOutsideOfMonitor) : device{ dxgiAPI->d3dForCapture.d3dDeviceInspectable.as<winrt::IDirect3DDevice>() },
d3dState{ _d3dState },
device{ _d3dState->d3dDeviceInspectable.as<winrt::IDirect3DDevice>() },
swapChain{ std::move(_swapChain) }, swapChain{ std::move(_swapChain) },
context{ std::move(_context) }, pixelFormat{ std::move(pixelFormat_) },
frameSize{ item.Size() }, monitor{ monitorInfo.GetHandle() },
pixelFormat{ std::move(_pixelFormat) }, monitorArea{ monitorInfo.GetScreenSize(true) },
framePool{ winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 1, item.Size()) }, continuousCapture{ continuousCapture_ }
session{ framePool.CreateCaptureSession(item) },
monitorArea{ _monitorArea },
captureOutsideOfMonitor{ _captureOutsideOfMonitor }
{ {
framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
} }
winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& frameTexture) winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& frameTexture)
@@ -124,8 +99,8 @@ winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com
desc.BindFlags = 0; desc.BindFlags = 0;
winrt::com_ptr<ID3D11Texture2D> cpuTexture; winrt::com_ptr<ID3D11Texture2D> cpuTexture;
winrt::check_hresult(d3dState->d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put())); winrt::check_hresult(dxgiAPI->d3dForCapture.d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put()));
context->CopyResource(cpuTexture.get(), frameTexture.get()); dxgiAPI->d3dForCapture.d3dContext->CopyResource(cpuTexture.get(), frameTexture.get());
return cpuTexture; return cpuTexture;
} }
@@ -142,15 +117,25 @@ auto GetDXGIInterfaceFromObject(winrt::IInspectable const& object)
void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&) void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&)
{ {
// Prevent calling a callback on a partially destroyed state // Prevent calling a callback on a partially destroyed state
std::unique_lock callbackLock{ destructorMutex }; std::lock_guard callbackLock{ frameArrivedMutex };
bool resized = false; bool resized = false;
POINT cursorPos = {}; POINT cursorPos = {};
GetCursorPos(&cursorPos); GetCursorPos(&cursorPos);
auto frame = sender.TryGetNextFrame(); winrt::Direct3D11CaptureFrame frame = nullptr;
winrt::check_bool(frame); try
if (monitorArea.inside(cursorPos) || captureOutsideOfMonitor) {
frame = sender.TryGetNextFrame();
}
catch (...)
{
}
if (!frame)
return;
if (monitorArea.inside(cursorPos) || !continuousCapture)
{ {
winrt::com_ptr<ID3D11Texture2D> texture; winrt::com_ptr<ID3D11Texture2D> texture;
{ {
@@ -170,7 +155,10 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se
auto gpuTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(surface); auto gpuTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(surface);
texture = CopyFrameToCPU(gpuTexture); texture = CopyFrameToCPU(gpuTexture);
surface.Close(); surface.Close();
MappedTextureView textureView{ texture, context, static_cast<size_t>(frameSize.Width), static_cast<size_t>(frameSize.Height) }; MappedTextureView textureView{ texture,
dxgiAPI->d3dForCapture.d3dContext,
static_cast<size_t>(frameSize.Width),
static_cast<size_t>(frameSize.Height) };
frameCallback(std::move(textureView)); frameCallback(std::move(textureView));
} }
@@ -178,69 +166,60 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se
frame.Close(); frame.Close();
DXGI_PRESENT_PARAMETERS presentParameters = {};
swapChain->Present1(1, 0, &presentParameters);
if (resized) if (resized)
{ {
framePool.Recreate(device, pixelFormat, 2, frameSize); framePool.Recreate(device, pixelFormat, 2, frameSize);
} }
} }
std::unique_ptr<D3DCaptureState> D3DCaptureState::Create(D3DState* d3dState, std::unique_ptr<D3DCaptureState> D3DCaptureState::Create(DxgiAPI* dxgiAPI,
winrt::GraphicsCaptureItem item, MonitorInfo monitorInfo,
const winrt::DirectXPixelFormat pixelFormat, const winrt::DirectXPixelFormat pixelFormat,
Box monitorArea, const bool continuousCapture)
const bool captureOutsideOfMonitor)
{ {
std::lock_guard guard{ gpuAccessLock }; const auto dims = monitorInfo.GetScreenSize(true);
const DXGI_SWAP_CHAIN_DESC1 desc = { const DXGI_SWAP_CHAIN_DESC1 desc = {
.Width = static_cast<uint32_t>(item.Size().Width), .Width = static_cast<uint32_t>(dims.width()),
.Height = static_cast<uint32_t>(item.Size().Height), .Height = static_cast<uint32_t>(dims.height()),
.Format = static_cast<DXGI_FORMAT>(pixelFormat), .Format = static_cast<DXGI_FORMAT>(pixelFormat),
.SampleDesc = { .Count = 1, .Quality = 0 }, .SampleDesc = { .Count = 1, .Quality = 0 },
.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT, .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
.BufferCount = 2, .BufferCount = 2,
.Scaling = DXGI_SCALING_STRETCH, .Scaling = DXGI_SCALING_STRETCH,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, .SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED, .AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
}; };
winrt::com_ptr<IDXGIAdapter> adapter;
winrt::check_hresult(d3dState->dxgiDevice->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
winrt::com_ptr<IDXGIFactory2> factory;
winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), factory.put_void()));
winrt::com_ptr<IDXGISwapChain1> swapChain; winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::check_hresult(factory->CreateSwapChainForComposition(d3dState->d3dDevice.get(), &desc, nullptr, swapChain.put())); winrt::check_hresult(dxgiAPI->d3dForCapture.dxgiFactory2->CreateSwapChainForComposition(dxgiAPI->d3dForCapture.d3dDevice.get(),
&desc,
winrt::com_ptr<ID3D11DeviceContext> context; nullptr,
d3dState->d3dDevice->GetImmediateContext(context.put()); swapChain.put()));
winrt::check_bool(context);
auto contextMultithread = context.as<ID3D11Multithread>();
contextMultithread->SetMultithreadProtected(true);
// We must create the object in a heap, since we need to pin it in memory to receive callbacks // We must create the object in a heap, since we need to pin it in memory to receive callbacks
auto statePtr = new D3DCaptureState{ d3dState, auto statePtr = new D3DCaptureState{ dxgiAPI,
std::move(swapChain), std::move(swapChain),
std::move(context),
item,
pixelFormat, pixelFormat,
monitorArea, std::move(monitorInfo),
captureOutsideOfMonitor }; continuousCapture };
return std::unique_ptr<D3DCaptureState>{ statePtr }; return std::unique_ptr<D3DCaptureState>{ statePtr };
} }
D3DCaptureState::~D3DCaptureState() D3DCaptureState::~D3DCaptureState()
{ {
std::unique_lock callbackLock{ destructorMutex }; std::unique_lock callbackLock{ frameArrivedMutex };
StopCapture(); StopCapture();
framePool.Close();
} }
void D3DCaptureState::StartSessionInPreferredMode() void D3DCaptureState::StartSessionInPreferredMode()
{ {
auto item = CreateCaptureItemForMonitor(monitor);
frameSize = item.Size();
framePool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 2, item.Size());
session = framePool.CreateCaptureSession(item);
framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
// Try disable border if possible (available on Windows ver >= 20348) // Try disable border if possible (available on Windows ver >= 20348)
if (auto session3 = session.try_as<winrt::IGraphicsCaptureSession3>()) if (auto session3 = session.try_as<winrt::IGraphicsCaptureSession3>())
{ {
@@ -270,27 +249,35 @@ MappedTextureView D3DCaptureState::CaptureSingleFrame()
result.emplace(std::move(tex)); result.emplace(std::move(tex));
frameArrivedEvent.SetEvent(); frameArrivedEvent.SetEvent();
}; };
std::lock_guard guard{ gpuAccessLock };
StartSessionInPreferredMode(); StartSessionInPreferredMode();
frameArrivedEvent.wait(); frameArrivedEvent.wait();
assert(result.has_value());
return std::move(*result); return std::move(*result);
} }
void D3DCaptureState::StopCapture() void D3DCaptureState::StopCapture()
{ {
try
{
if (session)
session.Close(); session.Close();
if (framePool)
framePool.Close();
}
catch (...)
{
// RPC call might fail here
}
} }
void UpdateCaptureState(const CommonState& commonState, void UpdateCaptureState(const CommonState& commonState,
Serialized<MeasureToolState>& state, Serialized<MeasureToolState>& state,
HWND window, HWND window,
const MappedTextureView& textureView, const MappedTextureView& textureView)
const bool continuousCapture)
{ {
const auto cursorPos = convert::FromSystemToRelative(window, commonState.cursorPosSystemSpace); const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
const bool cursorInLeftScreenHalf = cursorPos.x < textureView.view.width / 2; const bool cursorInLeftScreenHalf = cursorPos.x < textureView.view.width / 2;
const bool cursorInTopScreenHalf = cursorPos.y < textureView.view.height / 2; const bool cursorInTopScreenHalf = cursorPos.y < textureView.view.height / 2;
uint8_t pixelTolerance = {}; uint8_t pixelTolerance = {};
@@ -310,8 +297,7 @@ void UpdateCaptureState(const CommonState& commonState,
const RECT bounds = DetectEdges(textureView.view, const RECT bounds = DetectEdges(textureView.view,
cursorPos, cursorPos,
perColorChannelEdgeDetection, perColorChannelEdgeDetection,
pixelTolerance, pixelTolerance);
continuousCapture);
#if defined(DEBUG_EDGES) #if defined(DEBUG_EDGES)
char buffer[256]; char buffer[256];
@@ -328,51 +314,55 @@ void UpdateCaptureState(const CommonState& commonState,
OutputDebugStringA(buffer); OutputDebugStringA(buffer);
#endif #endif
state.Access([&](MeasureToolState& state) { state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = bounds; state.perScreen[window].measuredEdges = Measurement{ bounds };
}); });
} }
std::thread StartCapturingThread(D3DState* d3dState, std::thread StartCapturingThread(DxgiAPI* dxgiAPI,
const CommonState& commonState, const CommonState& commonState,
Serialized<MeasureToolState>& state, Serialized<MeasureToolState>& state,
HWND window, HWND window,
MonitorInfo monitor) MonitorInfo monitor)
{ {
return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, d3dState] { return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, dxgiAPI] {
auto captureInterop = winrt::get_activation_factory<
winrt::GraphicsCaptureItem,
IGraphicsCaptureItemInterop>();
winrt::GraphicsCaptureItem item = nullptr;
winrt::check_hresult(captureInterop->CreateForMonitor(
monitor.GetHandle(),
winrt::guid_of<winrt::GraphicsCaptureItem>(),
winrt::put_abi(item)));
bool continuousCapture = {}; bool continuousCapture = {};
state.Read([&](const MeasureToolState& state) { state.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture; continuousCapture = state.global.continuousCapture;
}); });
const auto monitorArea = monitor.GetScreenSize(true); auto captureState = D3DCaptureState::Create(dxgiAPI,
auto captureState = D3DCaptureState::Create(d3dState, monitor,
item,
winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized, winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized,
monitorArea, continuousCapture);
!continuousCapture); const auto monitorArea = monitor.GetScreenSize(true);
bool mouseOnMonitor = false;
if (continuousCapture) if (continuousCapture)
{ {
captureState->StartCapture([&, window](MappedTextureView textureView) {
UpdateCaptureState(commonState, state, window, textureView, continuousCapture);
});
while (IsWindow(window) && !commonState.closeOnOtherMonitors) while (IsWindow(window) && !commonState.closeOnOtherMonitors)
{
if (mouseOnMonitor == monitorArea.inside(commonState.cursorPosSystemSpace))
{ {
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION); std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
continue;
} }
mouseOnMonitor = !mouseOnMonitor;
if (mouseOnMonitor)
{
captureState->StartCapture([&, window](MappedTextureView textureView) {
UpdateCaptureState(commonState, state, window, textureView);
});
}
else
{
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = {};
});
captureState->StopCapture(); captureState->StopCapture();
} }
}
}
else else
{ {
const auto textureView = captureState->CaptureSingleFrame(); const auto textureView = captureState->CaptureSingleFrame();
@@ -394,7 +384,15 @@ std::thread StartCapturingThread(D3DState* d3dState,
auto path = std::filesystem::temp_directory_path() / buf; auto path = std::filesystem::temp_directory_path() / buf;
textureView.view.SaveAsBitmap(path.string().c_str()); textureView.view.SaveAsBitmap(path.string().c_str());
#endif #endif
UpdateCaptureState(commonState, state, window, textureView, continuousCapture); UpdateCaptureState(commonState, state, window, textureView);
mouseOnMonitor = true;
}
else if (mouseOnMonitor)
{
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = {};
});
mouseOnMonitor = false;
} }
const auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - now); const auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - now);
@@ -404,5 +402,7 @@ std::thread StartCapturingThread(D3DState* d3dState,
} }
} }
} }
captureState->StopCapture();
}); });
} }

View File

@@ -1,19 +1,11 @@
#pragma once #pragma once
#include "DxgiAPI.h"
#include "ToolState.h" #include "ToolState.h"
#include <common/utils/serialized.h> #include <common/utils/serialized.h>
struct D3DState std::thread StartCapturingThread(DxgiAPI* dxgiAPI,
{
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
D3DState();
};
std::thread StartCapturingThread(D3DState* d3dState,
const CommonState& commonState, const CommonState& commonState,
Serialized<MeasureToolState>& state, Serialized<MeasureToolState>& state,
HWND targetWindow, HWND targetWindow,

View File

@@ -15,6 +15,7 @@ namespace
const wchar_t JSON_KEY_PIXEL_TOLERANCE[] = L"PixelTolerance"; const wchar_t JSON_KEY_PIXEL_TOLERANCE[] = L"PixelTolerance";
const wchar_t JSON_KEY_PER_COLOR_CHANNEL_EDGE_DETECTION[] = L"PerColorChannelEdgeDetection"; const wchar_t JSON_KEY_PER_COLOR_CHANNEL_EDGE_DETECTION[] = L"PerColorChannelEdgeDetection";
const wchar_t JSON_KEY_MEASURE_CROSS_COLOR[] = L"MeasureCrossColor"; const wchar_t JSON_KEY_MEASURE_CROSS_COLOR[] = L"MeasureCrossColor";
const wchar_t JSON_KEY_UNITS_OF_MEASURE[] = L"UnitsOfMeasure";
} }
Settings Settings::LoadFromFile() Settings Settings::LoadFromFile()
@@ -65,6 +66,14 @@ Settings Settings::LoadFromFile()
catch (...) catch (...)
{ {
} }
try
{
result.units = static_cast<Measurement::Unit>(props.GetNamedObject(JSON_KEY_UNITS_OF_MEASURE).GetNamedNumber(JSON_KEY_VALUE));
}
catch (...)
{
}
} }
catch (...) catch (...)
{ {

View File

@@ -3,6 +3,8 @@
#include <array> #include <array>
#include <cinttypes> #include <cinttypes>
#include "Measurement.h"
struct Settings struct Settings
{ {
uint8_t pixelTolerance = 30; uint8_t pixelTolerance = 30;
@@ -10,6 +12,7 @@ struct Settings
bool drawFeetOnCross = true; bool drawFeetOnCross = true;
bool perColorChannelEdgeDetection = false; bool perColorChannelEdgeDetection = false;
std::array<uint8_t, 3> lineColor = {255, 69, 0}; std::array<uint8_t, 3> lineColor = {255, 69, 0};
Measurement::Unit units = Measurement::Unit::Pixel;
static Settings LoadFromFile(); static Settings LoadFromFile();
}; };

View File

@@ -16,10 +16,11 @@
//#define DEBUG_OVERLAY //#define DEBUG_OVERLAY
#include "BGRATextureView.h" #include "BGRATextureView.h"
#include "Measurement.h"
struct OverlayBoxText struct OverlayBoxText
{ {
std::array<wchar_t, 32> buffer = {}; std::array<wchar_t, 128> buffer = {};
}; };
struct CommonState struct CommonState
@@ -28,6 +29,8 @@ struct CommonState
D2D1::ColorF lineColor = D2D1::ColorF::OrangeRed; D2D1::ColorF lineColor = D2D1::ColorF::OrangeRed;
Box toolbarBoundingBox; Box toolbarBoundingBox;
Measurement::Unit units = Measurement::Unit::Pixel;
mutable Serialized<OverlayBoxText> overlayBoxText; mutable Serialized<OverlayBoxText> overlayBoxText;
POINT cursorPosSystemSpace = {}; // updated atomically POINT cursorPosSystemSpace = {}; // updated atomically
std::atomic_bool closeOnOtherMonitors = false; std::atomic_bool closeOnOtherMonitors = false;
@@ -38,7 +41,7 @@ struct BoundsToolState
struct PerScreen struct PerScreen
{ {
std::optional<D2D_POINT_2F> currentRegionStart; std::optional<D2D_POINT_2F> currentRegionStart;
std::vector<D2D1_RECT_F> measurements; std::vector<Measurement> measurements;
}; };
std::unordered_map<HWND, PerScreen> perScreen; std::unordered_map<HWND, PerScreen> perScreen;
@@ -67,7 +70,7 @@ struct MeasureToolState
{ {
bool cursorInLeftScreenHalf = false; bool cursorInLeftScreenHalf = false;
bool cursorInTopScreenHalf = false; bool cursorInTopScreenHalf = false;
RECT measuredEdges = {}; std::optional<Measurement> measuredEdges;
// While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed // While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed
// directly from a capturing thread. // directly from a capturing thread.
const MappedTextureView* capturedScreenTexture = nullptr; const MappedTextureView* capturedScreenTexture = nullptr;
@@ -79,6 +82,3 @@ struct MeasureToolState
CommonState* commonState = nullptr; // required for WndProc CommonState* commonState = nullptr; // required for WndProc
}; };
// Concurrently accessing Direct2D and Direct3D APIs make the driver go boom
extern std::recursive_mutex gpuAccessLock;

View File

@@ -4,12 +4,11 @@
namespace consts namespace consts
{ {
constexpr inline size_t TARGET_FRAME_RATE = 120; constexpr inline size_t TARGET_FRAME_RATE = 90;
constexpr inline auto TARGET_FRAME_DURATION = std::chrono::milliseconds{ 1000 } / TARGET_FRAME_RATE; constexpr inline auto TARGET_FRAME_DURATION = std::chrono::microseconds{ 1000000 } / TARGET_FRAME_RATE;
constexpr inline float FONT_SIZE = 14.f; constexpr inline float FONT_SIZE = 14.f;
constexpr inline float TEXT_BOX_CORNER_RADIUS = 4.f; constexpr inline float TEXT_BOX_CORNER_RADIUS = 4.f;
constexpr inline float TEXT_BOX_MARGIN_COEFF = 1.25f;
constexpr inline float FEET_HALF_LENGTH = 2.f; constexpr inline float FEET_HALF_LENGTH = 2.f;
constexpr inline float SHADOW_OPACITY = .4f; constexpr inline float SHADOW_OPACITY = .4f;
constexpr inline float SHADOW_RADIUS = 6.f; constexpr inline float SHADOW_RADIUS = 6.f;

View File

@@ -11,6 +11,7 @@
#include <d3d11.h> #include <d3d11.h>
#include <d3d11_4.h> #include <d3d11_4.h>
#include <dxgi1_6.h> #include <dxgi1_6.h>
#include <dxgidebug.h>
#include <d2d1_3.h> #include <d2d1_3.h>
#include <dwrite.h> #include <dwrite.h>
#include <dwmapi.h> #include <dwmapi.h>

View File

@@ -4,6 +4,7 @@
using System; using System;
using ManagedCommon; using ManagedCommon;
using MeasureToolUI.Helpers;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace MeasureToolUI namespace MeasureToolUI
@@ -42,7 +43,18 @@ namespace MeasureToolUI
} }
} }
_window = new MainWindow(); PowerToys.MeasureToolCore.Core core = null;
try
{
core = new PowerToys.MeasureToolCore.Core();
}
catch (Exception ex)
{
Logger.LogError($"MeasureToolCore failed to initialize: {ex}");
Environment.Exit(1);
}
_window = new MainWindow(core);
_window.Activate(); _window.Activate();
} }

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using interop;
using Microsoft.VisualBasic;
using Windows.Storage;
namespace MeasureToolUI.Helpers
{
public static class Logger
{
private static readonly string ApplicationLogPath = Path.Combine(interop.Constants.AppDataPath(), "Measure Tool\\MeasureToolUI\\Logs");
static Logger()
{
if (!Directory.Exists(ApplicationLogPath))
{
Directory.CreateDirectory(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogError(string message)
{
Log(message, "ERROR");
}
public static void LogError(string message, Exception ex)
{
Log(
message + Environment.NewLine +
ex?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
ex?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
ex?.StackTrace,
"ERROR");
}
public static void LogWarning(string message)
{
Log(message, "WARNING");
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType.Name;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@@ -18,12 +18,12 @@ namespace MeasureToolUI
/// <summary> /// <summary>
/// An empty window that can be used on its own or navigated to within a Frame. /// An empty window that can be used on its own or navigated to within a Frame.
/// </summary> /// </summary>
public sealed partial class MainWindow : WindowEx public sealed partial class MainWindow : WindowEx, IDisposable
{ {
private const int WindowWidth = 216; private const int WindowWidth = 216;
private const int WindowHeight = 50; private const int WindowHeight = 50;
private PowerToys.MeasureToolCore.Core _coreLogic = new PowerToys.MeasureToolCore.Core(); private PowerToys.MeasureToolCore.Core _coreLogic;
private AppWindow _appWindow; private AppWindow _appWindow;
private PointInt32 _initialPosition; private PointInt32 _initialPosition;
@@ -34,14 +34,14 @@ namespace MeasureToolUI
this.SetWindowSize(WindowWidth, WindowHeight); this.SetWindowSize(WindowWidth, WindowHeight);
} }
public MainWindow() public MainWindow(PowerToys.MeasureToolCore.Core core)
{ {
InitializeComponent(); InitializeComponent();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
_appWindow = AppWindow.GetFromWindowId(windowId); _appWindow = AppWindow.GetFromWindowId(windowId);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
var presenter = _appWindow.Presenter as OverlappedPresenter; var presenter = _appWindow.Presenter as OverlappedPresenter;
presenter.IsAlwaysOnTop = true; presenter.IsAlwaysOnTop = true;
this.SetIsAlwaysOnTop(true); this.SetIsAlwaysOnTop(true);
@@ -51,6 +51,8 @@ namespace MeasureToolUI
this.SetIsMaximizable(false); this.SetIsMaximizable(false);
IsTitleBarVisible = false; IsTitleBarVisible = false;
_coreLogic = core;
Closed += MainWindow_Closed;
DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest); DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
float dpiScale = _coreLogic.GetDPIScaleForWindow((int)hwnd); float dpiScale = _coreLogic.GetDPIScaleForWindow((int)hwnd);
@@ -64,6 +66,12 @@ namespace MeasureToolUI
OnPositionChanged(_initialPosition); OnPositionChanged(_initialPosition);
} }
private void MainWindow_Closed(object sender, WindowEventArgs args)
{
_coreLogic?.Dispose();
_coreLogic = null;
}
private void UpdateToolUsageCompletionEvent(object sender) private void UpdateToolUsageCompletionEvent(object sender)
{ {
_coreLogic.SetToolCompletionEvent(new PowerToys.MeasureToolCore.ToolSessionCompleted(() => _coreLogic.SetToolCompletionEvent(new PowerToys.MeasureToolCore.ToolSessionCompleted(() =>
@@ -136,5 +144,10 @@ namespace MeasureToolUI
_coreLogic.ResetState(); _coreLogic.ResetState();
this.Close(); this.Close();
} }
public void Dispose()
{
_coreLogic?.Dispose();
}
} }
} }

View File

@@ -79,6 +79,7 @@
<Folder Include="Assets\Icons\" /> <Folder Include="Assets\Icons\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj" /> <ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj" />
</ItemGroup> </ItemGroup>

View File

@@ -13,5 +13,6 @@ internal static class NativeMethods
internal static readonly IntPtr HWND_TOPMOST = new System.IntPtr(-1); internal static readonly IntPtr HWND_TOPMOST = new System.IntPtr(-1);
internal const uint SWP_NOSIZE = 0x0001; internal const uint SWP_NOSIZE = 0x0001;
internal const uint SWP_NOMOVE = 0x0002; internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_NOACTIVATE = 0x0010;
internal const uint SWP_SHOWWINDOW = 0x0040; internal const uint SWP_SHOWWINDOW = 0x0040;
} }

View File

@@ -14,6 +14,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public MeasureToolProperties() public MeasureToolProperties()
{ {
ActivationShortcut = new HotkeySettings(true, false, false, true, 0x4D); ActivationShortcut = new HotkeySettings(true, false, false, true, 0x4D);
UnitsOfMeasure = new IntProperty(0);
PixelTolerance = new IntProperty(30); PixelTolerance = new IntProperty(30);
ContinuousCapture = true; ContinuousCapture = true;
DrawFeetOnCross = true; DrawFeetOnCross = true;
@@ -32,6 +33,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonConverter(typeof(BoolPropertyJsonConverter))] [JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool PerColorChannelEdgeDetection { get; set; } public bool PerColorChannelEdgeDetection { get; set; }
public IntProperty UnitsOfMeasure { get; set; }
public IntProperty PixelTolerance { get; set; } public IntProperty PixelTolerance { get; set; }
public StringProperty MeasureCrossColor { get; set; } public StringProperty MeasureCrossColor { get; set; }

View File

@@ -129,6 +129,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
} }
} }
public int UnitsOfMeasure
{
get
{
return Settings.Properties.UnitsOfMeasure.Value;
}
set
{
if (Settings.Properties.UnitsOfMeasure.Value != value)
{
Settings.Properties.UnitsOfMeasure.Value = value;
NotifyPropertyChanged();
}
}
}
public int PixelTolerance public int PixelTolerance
{ {
get get

View File

@@ -155,6 +155,18 @@
<value>Customize the shortcut to bring up the command bar</value> <value>Customize the shortcut to bring up the command bar</value>
<comment>"Screen Ruler" is the name of the utility</comment> <comment>"Screen Ruler" is the name of the utility</comment>
</data> </data>
<data name="MeasureTool_UnitsOfMeasure.Header" xml:space="preserve">
<value>Units of measurement</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Pixels.Content" xml:space="preserve">
<value>Pixels</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Inches.Content" xml:space="preserve">
<value>Inches</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Centimeters.Content" xml:space="preserve">
<value>Centimeters</value>
</data>
<data name="MeasureTool_PixelTolerance.Header" xml:space="preserve"> <data name="MeasureTool_PixelTolerance.Header" xml:space="preserve">
<value>Pixel tolerance for edge detection</value> <value>Pixel tolerance for edge detection</value>
</data> </data>

View File

@@ -49,6 +49,7 @@
IsOpen="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}" IsOpen="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}"
IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}" IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}"
IsClosable="False" /> IsClosable="False" />
<controls:Setting x:Uid="MeasureTool_PerColorChannelEdgeDetection" Icon="&#xE7FB;"> <controls:Setting x:Uid="MeasureTool_PerColorChannelEdgeDetection" Icon="&#xE7FB;">
<controls:Setting.ActionContent> <controls:Setting.ActionContent>
<ToggleSwitch <ToggleSwitch
@@ -56,6 +57,7 @@
x:Uid="MeasureTool_PerColorChannelEdgeDetection_ToggleSwitch" /> x:Uid="MeasureTool_PerColorChannelEdgeDetection_ToggleSwitch" />
</controls:Setting.ActionContent> </controls:Setting.ActionContent>
</controls:Setting> </controls:Setting>
<controls:Setting x:Uid="MeasureTool_PixelTolerance"> <controls:Setting x:Uid="MeasureTool_PixelTolerance">
<controls:Setting.ActionContent> <controls:Setting.ActionContent>
<Slider Minimum="0" <Slider Minimum="0"
@@ -65,6 +67,16 @@
</controls:Setting.ActionContent> </controls:Setting.ActionContent>
</controls:Setting> </controls:Setting>
<!--<controls:Setting x:Uid="MeasureTool_UnitsOfMeasure" Icon="&#xECC6;">
<controls:Setting.ActionContent>
<ComboBox SelectedIndex="{x:Bind Path=ViewModel.UnitsOfMeasure, Mode=TwoWay}" MinWidth="{StaticResource SettingActionControlMinWidth}">
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Pixels" />
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Inches" />
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Centimeters" />
</ComboBox>
</controls:Setting.ActionContent>
</controls:Setting>-->
<controls:Setting x:Uid="MeasureTool_DrawFeetOnCross"> <controls:Setting x:Uid="MeasureTool_DrawFeetOnCross">
<controls:Setting.ActionContent> <controls:Setting.ActionContent>
<ToggleSwitch IsOn="{x:Bind ViewModel.DrawFeetOnCross, Mode=TwoWay}" x:Uid="MeasureTool_DrawFeetOnCross_ToggleSwitch" /> <ToggleSwitch IsOn="{x:Bind ViewModel.DrawFeetOnCross, Mode=TwoWay}" x:Uid="MeasureTool_DrawFeetOnCross_ToggleSwitch" />