diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 4b08de51274..97a6ad2879a 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -346,7 +346,6 @@ CSIDL csignal cso CSRW -cstddef cstdint cstdlib cstring @@ -402,6 +401,7 @@ DBLEPSILON DCapture DCBA DCOM +dcommon dcomp dcompi DComposition @@ -490,7 +490,6 @@ dreamsofameaningfullife drivedetectionwarning dshow dst -DState DTo dutil DVASPECT @@ -522,6 +521,7 @@ DWORDLONG dworigin dwrite dxgi +dxgidebug dxgiformat dxguid ecount @@ -811,6 +811,7 @@ ICapture icase ICEBLUE IClass +IClosable ICollection IColor ICommand @@ -881,7 +882,6 @@ IMAGERESIZERCONTEXTMENU IMAGERESIZEREXT imageresizerinput imageresizersettings -TEXTEXTRACTOR imagingdevices IMain IMarkdown @@ -1278,6 +1278,7 @@ Moq MOUSEACTIVATE MOUSEHWHEEL MOUSEINPUT +MOUSELEAVE MOUSEMOVE MOUSEWHEEL MOVESIZEEND @@ -1632,7 +1633,6 @@ ptd PTOKEN PToy ptr -ptrdiff ptstr PVOID pwa @@ -1746,6 +1746,7 @@ RIGHTSCROLLBAR riid riverar RKey +RLO RMENU RNumber roadmap @@ -2063,6 +2064,7 @@ testhost testprocess TEXCOORD textblock +TEXTEXTRACTOR TEXTINCLUDE THH THICKFRAME @@ -2272,7 +2274,6 @@ wchar WClass wcout wcscat -wcschr wcscmp wcscpy wcslen diff --git a/Cpp.Build.props b/Cpp.Build.props index da85f4fb56a..9b56bed178e 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -32,6 +32,7 @@ + x64 false $(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath) diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 05c88b72ffc..3e7a208f7bb 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -119,7 +119,7 @@ - + diff --git a/src/common/utils/UnhandledExceptionHandler.h b/src/common/utils/UnhandledExceptionHandler.h index 037bcc8c92a..a3a69b29328 100644 --- a/src/common/utils/UnhandledExceptionHandler.h +++ b/src/common/utils/UnhandledExceptionHandler.h @@ -156,7 +156,7 @@ inline void LogStackTrace() Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError())); return; } - + STACKFRAME64 stack; memset(&stack, 0, sizeof(STACKFRAME64)); @@ -238,14 +238,14 @@ inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info) } /* Handler to trap abort() calls */ -inline void AbortHandler(int signal_number) +inline void AbortHandler(int /*signal_number*/) { Logger::error("--- ABORT"); try { LogStackTrace(); } - catch(...) + catch (...) { Logger::error("Failed to log stack trace on abort"); Logger::flush(); @@ -271,9 +271,9 @@ inline void InitUnhandledExceptionHandler(void) // Global handler for unhandled exceptions SetUnhandledExceptionFilter(UnhandledExceptionHandler); // Handler for abort() - signal(SIGABRT, &AbortHandler); + signal(SIGABRT, &AbortHandler); } - catch(...) + catch (...) { Logger::error("Failed to init global unhandled exception handler"); } diff --git a/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp index d061df5db3a..b5f21f40743 100644 --- a/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/BoundsToolOverlayUI.cpp @@ -30,7 +30,7 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA auto toolState = GetWindowParam(window); if (!toolState) 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(cursorPos.x), .y = static_cast(cursorPos.y) }; toolState->perScreen[window].currentRegionStart = newRegionStart; @@ -38,6 +38,8 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA } case WM_CURSOR_LEFT_MONITOR: { + for (; ShowCursor(true) < 0;) + ; auto toolState = GetWindowParam(window); if (!toolState) break; @@ -59,12 +61,12 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA 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; std::tie(rect.left, rect.right) = std::minmax(static_cast(cursorPos.x), toolState->perScreen[window].currentRegionStart->x); std::tie(rect.top, rect.bottom) = std::minmax(static_cast(cursorPos.y), toolState->perScreen[window].currentRegionStart->y); - toolState->perScreen[window].measurements.push_back(rect); + toolState->perScreen[window].measurements.push_back(Measurement{ rect }); } toolState->perScreen[window].currentRegionStart = std::nullopt; @@ -97,45 +99,42 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA namespace { - void DrawMeasurement(const D2D1_RECT_F rect, + void DrawMeasurement(const Measurement& measurement, const bool alignTextBoxToCenter, const CommonState& commonState, HWND window, - const D2DState& d2dState) + const D2DState& d2dState, + float mouseX, + float mouseY) { const bool screenQuadrantAware = !alignTextBoxToCenter; - const auto prevMode = d2dState.rt->GetAntialiasMode(); - d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); - d2dState.rt->DrawRectangle(rect, d2dState.solidBrushes[Brush::line].get()); - d2dState.rt->SetAntialiasMode(prevMode); + d2dState.ToggleAliasedLinesMode(true); + d2dState.dxgiWindowState.rt->DrawRectangle(measurement.rect, d2dState.solidBrushes[Brush::line].get()); + d2dState.ToggleAliasedLinesMode(false); OverlayBoxText text; - const auto width = std::abs(rect.right - rect.left + 1); - const auto height = std::abs(rect.top - rect.bottom + 1); - const uint32_t textLen = swprintf_s(text.buffer.data(), - text.buffer.size(), - L"%.0f × %.0f", - width, - height); - std::optional crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1; + const auto [crossSymbolPos, measureStringBufLen] = + measurement.Print(text.buffer.data(), + text.buffer.size(), + true, + true, + commonState.units); commonState.overlayBoxText.Access([&](OverlayBoxText& v) { v = text; }); - float cornerX = rect.right; - float cornerY = rect.bottom; if (alignTextBoxToCenter) { - cornerX = rect.left + width / 2; - cornerY = rect.top + height / 2; + mouseX = measurement.rect.left + measurement.Width(Measurement::Unit::Pixel) / 2; + mouseY = measurement.rect.top + measurement.Height(Measurement::Unit::Pixel) / 2; } d2dState.DrawTextBox(text.buffer.data(), - textLen, + measureStringBufLen, crossSymbolPos, - cornerX, - cornerY, + mouseX, + mouseY, screenQuadrantAware, window); } @@ -150,20 +149,21 @@ void DrawBoundsToolTick(const CommonState& commonState, if (it == end(toolState.perScreen)) return; - d2dState.rt->Clear(); + d2dState.dxgiWindowState.rt->Clear(); const auto& perScreen = it->second; 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()) 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, - .top = perScreen.currentRegionStart->y, - .right = static_cast(cursorPos.x), - .bottom = static_cast(cursorPos.y) }; - DrawMeasurement(rect, false, commonState, window, d2dState); + D2D1_RECT_F rect; + const float cursorX = static_cast(cursorPos.x); + const float cursorY = static_cast(cursorPos.y); + std::tie(rect.left, rect.right) = std::minmax(cursorX, perScreen.currentRegionStart->x); + std::tie(rect.top, rect.bottom) = std::minmax(cursorY, perScreen.currentRegionStart->y); + DrawMeasurement(Measurement{ rect }, false, commonState, window, d2dState, cursorX, cursorY); } diff --git a/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h b/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h index a140809bf79..152b627a817 100644 --- a/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h +++ b/src/modules/MeasureTool/MeasureToolCore/CoordinateSystemConversion.h @@ -6,21 +6,9 @@ namespace convert { // 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); return p; } - - // Converts a given point from multi-monitor coordinate system to the one relative to HWND and also ready - // to be used in Direct2D calls with AA mode set to aliased - inline POINT FromSystemToRelativeForDirect2D(HWND window, POINT p) - { - ScreenToClient(window, &p); - // Submitting DrawLine calls to Direct2D with thickness == 1.f and AA mode set to aliased causes - // them to be drawn offset by [1,1] toward upper-left corner, so we must to compensate for that. - ++p.x; - ++p.y; - return p; - } } diff --git a/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp b/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp index cd8bb24e358..62ffc430207 100644 --- a/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/D2DState.cpp @@ -2,6 +2,7 @@ #include "constants.h" #include "D2DState.h" +#include "DxgiAPI.h" #include #include @@ -19,43 +20,28 @@ namespace } } -D2DState::D2DState(const HWND overlayWindow, std::vector solidBrushesColors) +D2DState::D2DState(const DxgiAPI* dxgi, + HWND window, + std::vector solidBrushesColors) { - std::lock_guard guard{ gpuAccessLock }; - - RECT clientRect = {}; - - winrt::check_bool(GetClientRect(overlayWindow, &clientRect)); - winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &d2dFactory)); - - // 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)); + dxgiAPI = dxgi; unsigned dpi = DPIAware::DEFAULT_DPI; - DPIAware::GetScreenDPIForWindow(overlayWindow, dpi); + DPIAware::GetScreenDPIForWindow(window, dpi); dpiScale = dpi / static_cast(DPIAware::DEFAULT_DPI); - winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), writeFactory.put_unknown())); - winrt::check_hresult(writeFactory->CreateTextFormat(L"Segoe UI Variable Text", - nullptr, - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - consts::FONT_SIZE * dpiScale, - L"en-US", - &textFormat)); + dxgiWindowState = dxgiAPI->CreateD2D1RenderTarget(window); + + winrt::check_hresult(dxgiWindowState.rt->CreateCompatibleRenderTarget(bitmapRt.put())); + + winrt::check_hresult(dxgiAPI->writeFactory->CreateTextFormat(L"Segoe UI Variable Text", + nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + consts::FONT_SIZE * dpiScale, + L"en-US", + textFormat.put())); winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)); winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER)); winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); @@ -63,22 +49,22 @@ D2DState::D2DState(const HWND overlayWindow, std::vector solidBrus solidBrushes.resize(solidBrushesColors.size()); 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(); - winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, &shadowEffect)); + const auto deviceContext = dxgiWindowState.rt.as(); + 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_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()); - textRenderer = winrt::make_self(d2dFactory, rt, solidBrushes[Brush::foreground]); + textRenderer = winrt::make_self(dxgi->d2dFactory2, dxgiWindowState.rt, solidBrushes[Brush::foreground]); } void D2DState::DrawTextBox(const wchar_t* text, - const uint32_t textLen, + const size_t textLen, const std::optional halfOpaqueSymbolPos, const float centerX, const float centerY, @@ -86,16 +72,19 @@ void D2DState::DrawTextBox(const wchar_t* text, const HWND window) const { wil::com_ptr textLayout; - winrt::check_hresult(writeFactory->CreateTextLayout(text, - textLen, - textFormat.get(), - std::numeric_limits::max(), - std::numeric_limits::max(), - &textLayout)); + winrt::check_hresult( + dxgiAPI->writeFactory->CreateTextLayout(text, + static_cast(textLen), + textFormat.get(), + std::numeric_limits::max(), + std::numeric_limits::max(), + &textLayout)); DWRITE_TEXT_METRICS textMetrics = {}; winrt::check_hresult(textLayout->GetMetrics(&textMetrics)); - textMetrics.width *= consts::TEXT_BOX_MARGIN_COEFF; - textMetrics.height *= consts::TEXT_BOX_MARGIN_COEFF; + // Assumes text doesn't contain new lines + const float lineHeight = textMetrics.height; + textMetrics.width += lineHeight; + textMetrics.height += lineHeight * .5f; winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width)); winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height)); @@ -103,6 +92,8 @@ void D2DState::DrawTextBox(const wchar_t* text, .top = centerY - textMetrics.height / 2.f, .right = centerX + textMetrics.width / 2.f, .bottom = centerY + textMetrics.height / 2.f }; + + const float SHADOW_OFFSET = consts::SHADOW_OFFSET * dpiScale; if (screenQuadrantAware) { bool cursorInLeftScreenHalf = false; @@ -112,8 +103,8 @@ void D2DState::DrawTextBox(const wchar_t* text, static_cast(centerY), cursorInLeftScreenHalf, cursorInTopScreenHalf); - float textQuadrantOffsetX = textMetrics.width * dpiScale; - float textQuadrantOffsetY = textMetrics.height * dpiScale; + float textQuadrantOffsetX = textMetrics.width / 2.f + SHADOW_OFFSET; + float textQuadrantOffsetY = textMetrics.height / 2.f + SHADOW_OFFSET; if (!cursorInLeftScreenHalf) textQuadrantOffsetX *= -1.f; if (!cursorInTopScreenHalf) @@ -140,15 +131,14 @@ void D2DState::DrawTextBox(const wchar_t* text, bitmapRt->GetBitmap(&rtBitmap); shadowEffect->SetInput(0, rtBitmap.get()); - const auto shadowMatrix = D2D1::Matrix3x2F::Translation(consts::SHADOW_OFFSET * dpiScale, - consts::SHADOW_OFFSET * dpiScale); + const auto shadowMatrix = D2D1::Matrix3x2F::Translation(SHADOW_OFFSET, SHADOW_OFFSET); winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX, shadowMatrix)); - auto deviceContext = rt.query(); + auto deviceContext = dxgiWindowState.rt.as(); deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR); // 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; textBoxRect.rect.bottom -= 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; // Draw text & its box - rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get()); + dxgiWindowState.rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get()); 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)); } + +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); + } +} diff --git a/src/modules/MeasureTool/MeasureToolCore/D2DState.h b/src/modules/MeasureTool/MeasureToolCore/D2DState.h index 385eb0957be..18bad4536ae 100644 --- a/src/modules/MeasureTool/MeasureToolCore/D2DState.h +++ b/src/modules/MeasureTool/MeasureToolCore/D2DState.h @@ -3,10 +3,9 @@ #include #include -#include -#include #include +#include "DxgiAPI.h" #include "PerGlyphOpacityTextRender.h" enum Brush : size_t @@ -19,24 +18,27 @@ enum Brush : size_t struct D2DState { - wil::com_ptr d2dFactory; - wil::com_ptr writeFactory; - wil::com_ptr rt; - wil::com_ptr bitmapRt; - wil::com_ptr textFormat; - winrt::com_ptr textRenderer; - std::vector> solidBrushes; - wil::com_ptr shadowEffect; - wil::com_ptr affineTransformEffect; + const DxgiAPI* dxgiAPI = nullptr; + + DxgiWindowState dxgiWindowState; + winrt::com_ptr bitmapRt; + winrt::com_ptr textFormat; + winrt::com_ptr textRenderer; + std::vector> solidBrushes; + winrt::com_ptr shadowEffect; + winrt::com_ptr affineTransformEffect; float dpiScale = 1.f; - D2DState(const HWND window, std::vector solidBrushesColors); + D2DState(const DxgiAPI*, + HWND window, + std::vector solidBrushesColors); void DrawTextBox(const wchar_t* text, - const uint32_t textLen, + const size_t textLen, const std::optional halfOpaqueSymbolPos, const float centerX, const float centerY, const bool screenQuadrantAware, const HWND window) const; + void ToggleAliasedLinesMode(const bool enabled) const; }; diff --git a/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.cpp b/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.cpp new file mode 100644 index 00000000000..f7e724abb6e --- /dev/null +++ b/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.cpp @@ -0,0 +1,145 @@ +#include "pch.h" + +#include "DxgiAPI.h" + +#include + +//#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(); + winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(d3d.dxgiDevice.get(), d3d.d3dDeviceInspectable.put())); + + winrt::com_ptr adapter; + winrt::check_hresult(d3d.dxgiDevice->GetParent(winrt::guid_of(), adapter.put_void())); + winrt::check_hresult(adapter->GetParent(winrt::guid_of(), d3d.dxgiFactory2.put_void())); + + d3d.d3dDevice->GetImmediateContext(d3d.d3dContext.put()); + winrt::check_bool(d3d.d3dContext); + auto contextMultithread = d3d.d3dContext.as(); + 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(), + reinterpret_cast(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(), + 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(rect.right - rect.left), + .Height = static_cast(rect.bottom - rect.top), + .Format = static_cast(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 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 surface; + winrt::check_hresult(state.swapChain->GetBuffer(0, winrt::guid_of(), 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 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 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; +} diff --git a/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.h b/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.h new file mode 100644 index 00000000000..c18d3b0239d --- /dev/null +++ b/src/modules/MeasureTool/MeasureToolCore/DxgiAPI.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +struct DxgiWindowState +{ + winrt::com_ptr rt; + winrt::com_ptr swapChain; + winrt::com_ptr compositionTarget; +}; + +struct DxgiAPI final +{ + struct D3D + { + winrt::com_ptr d3dDevice; + winrt::com_ptr dxgiDevice; + winrt::com_ptr d3dDeviceInspectable; + winrt::com_ptr dxgiFactory2; + winrt::com_ptr d3dContext; + }; + + winrt::com_ptr d2dFactory2; + winrt::com_ptr writeFactory; + + winrt::com_ptr d3dDevice; + winrt::com_ptr dxgiDevice; + winrt::com_ptr d3dDeviceInspectable; + winrt::com_ptr dxgiFactory2; + winrt::com_ptr d3dContext; + + D3D d3dForCapture; + + winrt::com_ptr d2dDevice1; + winrt::com_ptr compositionDevice; + + DxgiAPI(); + + enum class Uninitialized + { + }; + explicit inline DxgiAPI(Uninitialized) {} + + DxgiWindowState CreateD2D1RenderTarget(HWND window) const; +}; \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp index 538698a018d..4445c6d1151 100644 --- a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.cpp @@ -2,8 +2,8 @@ #include "constants.h" #include "EdgeDetection.h" + template 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(IsX ? texture.width : texture.height) - 1 : 0; } -template +template inline RECT DetectEdgesInternal(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance) { return RECT{ .left = FindEdge(texture, centerPoint, tolerance), .top = FindEdge(texture, centerPoint, tolerance), .right = FindEdge(texture, centerPoint, tolerance), .bottom = FindEdge(texture, centerPoint, tolerance) }; } @@ -83,12 +79,9 @@ inline RECT DetectEdgesInternal(const BGRATextureView& texture, RECT DetectEdges(const BGRATextureView& texture, const POINT centerPoint, const bool perChannel, - const uint8_t tolerance, - const bool continuousCapture) + const uint8_t tolerance) { - auto function = perChannel ? &DetectEdgesInternal : DetectEdgesInternal; - if (continuousCapture) - function = perChannel ? &DetectEdgesInternal : &DetectEdgesInternal; + auto function = perChannel ? &DetectEdgesInternal : DetectEdgesInternal; return function(texture, centerPoint, tolerance); } diff --git a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h index 175c8205404..8942bf24429 100644 --- a/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h +++ b/src/modules/MeasureTool/MeasureToolCore/EdgeDetection.h @@ -5,5 +5,4 @@ RECT DetectEdges(const BGRATextureView& texture, const POINT centerPoint, const bool perChannel, - const uint8_t tolerance, - const bool continuousCapture); \ No newline at end of file + const uint8_t tolerance); \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp index 6090a84caee..6c276a0ada3 100644 --- a/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/MeasureToolOverlayUI.cpp @@ -16,30 +16,22 @@ namespace // Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines if (horizontal) { - start.x -= consts::FEET_HALF_LENGTH + 1.f; - end.x += consts::FEET_HALF_LENGTH; - - start.y += 1.f; - end.y += 1.f; + start.x -= consts::FEET_HALF_LENGTH; + end.x += consts::FEET_HALF_LENGTH + 1.f; } else { - start.y -= consts::FEET_HALF_LENGTH + 1.f; - end.y += consts::FEET_HALF_LENGTH; - - start.x += 1.f; - end.x += 1.f; + start.y -= consts::FEET_HALF_LENGTH; + end.y += consts::FEET_HALF_LENGTH + 1.f; } return { start, end }; } } -winrt::com_ptr ConvertID3D11Texture2DToD2D1Bitmap(wil::com_ptr rt, +winrt::com_ptr ConvertID3D11Texture2DToD2D1Bitmap(winrt::com_ptr rt, const MappedTextureView* capturedScreenTexture) { - std::lock_guard guard{ gpuAccessLock }; - capturedScreenTexture->view.pixels; D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() }; @@ -62,6 +54,7 @@ LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LP { switch (message) { + case WM_MOUSELEAVE: case WM_CURSOR_LEFT_MONITOR: { if (auto state = GetWindowParam*>(window)) @@ -130,20 +123,27 @@ void DrawMeasureToolTick(const CommonState& commonState, bool drawFeetOnCross = {}; bool drawHorizontalCrossLine = true; bool drawVerticalCrossLine = true; - RECT measuredEdges{}; + + Measurement measuredEdges{}; MeasureToolState::Mode mode = {}; winrt::com_ptr backgroundBitmap; const MappedTextureView* backgroundTextureToConvert = nullptr; + bool gotMeasurement = false; toolState.Read([&](const MeasureToolState& state) { continuousCapture = state.global.continuousCapture; drawFeetOnCross = state.global.drawFeetOnCross; mode = state.global.mode; - if (auto it = state.perScreen.find(window); it != end(state.perScreen)) { const auto& perScreen = it->second; - measuredEdges = perScreen.measuredEdges; + if (!perScreen.measuredEdges) + { + return; + } + + gotMeasurement = true; + measuredEdges = *perScreen.measuredEdges; if (continuousCapture) return; @@ -158,6 +158,10 @@ void DrawMeasureToolTick(const CommonState& commonState, } } }); + + if (!gotMeasurement) + return; + switch (mode) { case MeasureToolState::Mode::Cross: @@ -176,7 +180,7 @@ void DrawMeasureToolTick(const CommonState& commonState, if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert) { - backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.rt, backgroundTextureToConvert); + backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.dxgiWindowState.rt, backgroundTextureToConvert); if (backgroundBitmap) { toolState.Access([&](MeasureToolState& state) { @@ -187,28 +191,24 @@ void DrawMeasureToolTick(const CommonState& commonState, } 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 = static_cast(measuredEdges.right - measuredEdges.left + 1); - const float vMeasure = static_cast(measuredEdges.bottom - measuredEdges.top + 1); + const float hMeasure = measuredEdges.Width(Measurement::Unit::Pixel); + const float vMeasure = measuredEdges.Height(Measurement::Unit::Pixel); if (!continuousCapture && backgroundBitmap) { - d2dState.rt->DrawBitmap(backgroundBitmap.get()); + d2dState.dxgiWindowState.rt->DrawBitmap(backgroundBitmap.get()); } - const auto previousAliasingMode = d2dState.rt->GetAntialiasMode(); - // Anti-aliasing is creating artifacts. Aliasing is for drawing straight lines. - d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); - - const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace); + const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace); + d2dState.ToggleAliasedLinesMode(true); if (drawHorizontalCrossLine) { - const D2D_POINT_2F hLineStart{ .x = static_cast(measuredEdges.left), .y = static_cast(cursorPos.y) }; + const D2D_POINT_2F hLineStart{ .x = measuredEdges.rect.left, .y = static_cast(cursorPos.y) }; D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y }; - d2dState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get()); + d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get()); if (drawFeetOnCross) { @@ -218,57 +218,37 @@ void DrawMeasureToolTick(const CommonState& commonState, hLineEnd.x -= 1.f; auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false); auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false); - d2dState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get()); - d2dState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get()); + d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get()); + d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get()); } } if (drawVerticalCrossLine) { - const D2D_POINT_2F vLineStart{ .x = static_cast(cursorPos.x), .y = static_cast(measuredEdges.top) }; + const D2D_POINT_2F vLineStart{ .x = static_cast(cursorPos.x), .y = measuredEdges.rect.top }; 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) { vLineEnd.y -= 1.f; auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true); auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true); - d2dState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get()); - d2dState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get()); + d2dState.dxgiWindowState.rt->DrawLine(top_start, top_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.rt->SetAntialiasMode(previousAliasingMode); - - uint32_t measureStringBufLen = 0; + d2dState.ToggleAliasedLinesMode(false); OverlayBoxText text; - std::optional crossSymbolPos; - switch (mode) - { - case MeasureToolState::Mode::Cross: - measureStringBufLen = swprintf_s(text.buffer.data(), - text.buffer.size(), - L"%.0f × %.0f", - hMeasure, - vMeasure); - crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1; - break; - case MeasureToolState::Mode::Vertical: - measureStringBufLen = swprintf_s(text.buffer.data(), - text.buffer.size(), - L"%.0f", - vMeasure); - break; - case MeasureToolState::Mode::Horizontal: - measureStringBufLen = swprintf_s(text.buffer.data(), - text.buffer.size(), - L"%.0f", - hMeasure); - break; - } + + const auto [crossSymbolPos, measureStringBufLen] = + measuredEdges.Print(text.buffer.data(), + text.buffer.size(), + drawHorizontalCrossLine, + drawVerticalCrossLine, + commonState.units); commonState.overlayBoxText.Access([&](OverlayBoxText& v) { v = text; diff --git a/src/modules/MeasureTool/MeasureToolCore/Measurement.cpp b/src/modules/MeasureTool/MeasureToolCore/Measurement.cpp new file mode 100644 index 00000000000..614d9dd1c1f --- /dev/null +++ b/src/modules/MeasureTool/MeasureToolCore/Measurement.cpp @@ -0,0 +1,91 @@ +#include "pch.h" + +#include "Measurement.h" + +Measurement::Measurement(RECT winRect) +{ + rect.left = static_cast(winRect.left); + rect.right = static_cast(winRect.right); + rect.top = static_cast(winRect.top); + rect.bottom = static_cast(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; +} diff --git a/src/modules/MeasureTool/MeasureToolCore/Measurement.h b/src/modules/MeasureTool/MeasureToolCore/Measurement.h new file mode 100644 index 00000000000..fb8263566e3 --- /dev/null +++ b/src/modules/MeasureTool/MeasureToolCore/Measurement.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +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 crossSymbolPos; + size_t strLen = {}; + }; + + PrintResult Print(wchar_t* buf, + const size_t bufSize, + const bool printWidth, + const bool printHeight, + const Unit units) const; +}; diff --git a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp index 8ede412f24d..ac307ed836b 100644 --- a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "BoundsToolOverlayUI.h" +#include "constants.h" #include "MeasureToolOverlayUI.h" #include "OverlayUI.h" @@ -22,16 +23,17 @@ void CreateOverlayWindowClasses() wcex.lpfnWndProc = MeasureToolWndProc; wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName; + wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS); RegisterClassExW(&wcex); wcex.lpfnWndProc = BoundsToolWndProc; wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName; - wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS); RegisterClassExW(&wcex); } HWND CreateOverlayUIWindow(const CommonState& commonState, const MonitorInfo& monitor, + const bool excludeFromCapture, const wchar_t* windowClass, void* extraParam) { @@ -39,7 +41,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState, std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses); const auto screenArea = monitor.GetScreenSize(true); - DWORD windowStyle = WS_EX_TOOLWINDOW; + DWORD windowStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW; #if !defined(DEBUG_OVERLAY) windowStyle |= WS_EX_TOPMOST; #endif @@ -47,7 +49,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState, CreateWindowExW(windowStyle, windowClass, L"PowerToys.MeasureToolOverlay", - WS_POPUP, + WS_POPUP | CS_HREDRAW | CS_VREDRAW, screenArea.left(), screenArea.top(), screenArea.width(), @@ -59,7 +61,11 @@ HWND CreateOverlayUIWindow(const CommonState& commonState, }; winrt::check_bool(window); ShowWindow(window, SW_SHOWNORMAL); - SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE); + UpdateWindow(window); + if (excludeFromCapture) + { + SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE); + } #if !defined(DEBUG_OVERLAY) SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE); #else @@ -109,51 +115,60 @@ std::vector AppendCommonOverlayUIColors(const D2D1::ColorF& lineCo void OverlayUIState::RunUILoop() { + bool cursorOnScreen = true; + while (IsWindow(_window) && !_commonState.closeOnOtherMonitors) { + const auto now = std::chrono::high_resolution_clock::now(); const auto cursor = _commonState.cursorPosSystemSpace; - const bool cursorOnScreen = _monitorArea.inside(cursor); const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor); - - if (cursorOnScreen != _cursorOnScreen) + auto& dxgi = _d2dState.dxgiWindowState; + if (_monitorArea.inside(cursor) != cursorOnScreen) { - _cursorOnScreen = cursorOnScreen; + cursorOnScreen = !cursorOnScreen; if (!cursorOnScreen) { - if (_clearOnCursorLeavingScreen) - { - _d2dState.rt->BeginDraw(); - _d2dState.rt->Clear(); - _d2dState.rt->EndDraw(); - } PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {}); } } + run_message_loop(true, 1); + + dxgi.rt->BeginDraw(); + dxgi.rt->Clear(); + + if (!cursorOverToolbar) + _tickFunc(); + + dxgi.rt->EndDraw(); + dxgi.swapChain->Present(0, 0); + if (cursorOnScreen) { - _d2dState.rt->BeginDraw(); - if (!cursorOverToolbar) - _tickFunc(); - else - _d2dState.rt->Clear(); - - _d2dState.rt->EndDraw(); + 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); } template -OverlayUIState::OverlayUIState(StateT& toolState, +OverlayUIState::OverlayUIState(const DxgiAPI* dxgiAPI, + StateT& toolState, TickFuncT tickFunc, const CommonState& commonState, HWND window) : _window{ window }, _commonState{ commonState }, - _d2dState{ window, AppendCommonOverlayUIColors(commonState.lineColor) }, + _d2dState{ dxgiAPI, window, AppendCommonOverlayUIColors(commonState.lineColor) }, _tickFunc{ [this, tickFunc, &toolState] { tickFunc(_commonState, toolState, _window, _d2dState); } } @@ -175,25 +190,30 @@ OverlayUIState::~OverlayUIState() // Returning unique_ptr, since we need to pin ui state in memory template -inline std::unique_ptr OverlayUIState::CreateInternal(ToolT& toolState, +inline std::unique_ptr OverlayUIState::CreateInternal(const DxgiAPI* dxgi, + ToolT& toolState, TickFuncT tickFunc, CommonState& commonState, const wchar_t* toolWindowClassName, void* windowParam, const MonitorInfo& monitor, - const bool clearOnCursorLeavingScreen) + const bool excludeFromCapture) { wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset); std::unique_ptr uiState; - auto threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] { - const HWND window = CreateOverlayUIWindow(commonState, monitor, toolWindowClassName, windowParam); - uiState = std::unique_ptr{ new OverlayUIState{ toolState, tickFunc, commonState, window } }; - uiState->_monitorArea = monitor.GetScreenSize(true); - uiState->_clearOnCursorLeavingScreen = clearOnCursorLeavingScreen; - // we must create window + d2d state in the same thread, then store thread handle in uiState, thus - // lifetime is ok here, since we join the thread in destructor - auto* state = uiState.get(); - uiCreatedEvent.SetEvent(); + std::thread threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] { + OverlayUIState* state = nullptr; + { + auto sinalUICreatedEvent = wil::scope_exit([&] { uiCreatedEvent.SetEvent(); }); + + const HWND window = CreateOverlayUIWindow(commonState, monitor, excludeFromCapture, toolWindowClassName, windowParam); + + uiState = std::unique_ptr{ new OverlayUIState{ dxgi, toolState, tickFunc, commonState, window } }; + uiState->_monitorArea = monitor.GetScreenSize(true); + // 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 + state = uiState.get(); + } state->RunUILoop(); @@ -202,28 +222,40 @@ inline std::unique_ptr OverlayUIState::CreateInternal(ToolT& too }); uiCreatedEvent.wait(); - uiState->_uiThread = std::move(threadHandle); + if (uiState) + uiState->_uiThread = std::move(threadHandle); + else if (threadHandle.joinable()) + threadHandle.join(); + return uiState; } -std::unique_ptr OverlayUIState::Create(Serialized& toolState, +std::unique_ptr OverlayUIState::Create(const DxgiAPI* dxgi, + Serialized& toolState, CommonState& commonState, 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, commonState, NonLocalizable::MeasureToolOverlayWindowName, &toolState, monitor, - true); + excludeFromCapture); } -std::unique_ptr OverlayUIState::Create(BoundsToolState& toolState, +std::unique_ptr OverlayUIState::Create(const DxgiAPI* dxgi, + BoundsToolState& toolState, CommonState& commonState, const MonitorInfo& monitor) { - return OverlayUIState::CreateInternal(toolState, + return OverlayUIState::CreateInternal(dxgi, + toolState, DrawBoundsToolTick, commonState, NonLocalizable::BoundsToolOverlayWindowName, diff --git a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h index e4d2b1ddfeb..5f1b7be39d3 100644 --- a/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h +++ b/src/modules/MeasureTool/MeasureToolCore/OverlayUI.h @@ -1,6 +1,8 @@ #pragma once +#include "DxgiAPI.h" #include "D2DState.h" + #include "ToolState.h" #include @@ -9,7 +11,8 @@ class OverlayUIState final { template - OverlayUIState(StateT& toolState, + OverlayUIState(const DxgiAPI* dxgiAPI, + StateT& toolState, TickFuncT tickFunc, const CommonState& commonState, HWND window); @@ -20,26 +23,27 @@ class OverlayUIState final D2DState _d2dState; std::function _tickFunc; std::thread _uiThread; - bool _cursorOnScreen = true; - bool _clearOnCursorLeavingScreen = false; template - static std::unique_ptr CreateInternal(ToolT& toolState, + static std::unique_ptr CreateInternal(const DxgiAPI* dxgi, + ToolT& toolState, TickFuncT tickFunc, CommonState& commonState, const wchar_t* toolWindowClassName, void* windowParam, const MonitorInfo& monitor, - const bool clearOnCursorLeavingScreen); + const bool excludeFromCapture); public: OverlayUIState(OverlayUIState&&) noexcept = default; ~OverlayUIState(); - static std::unique_ptr Create(BoundsToolState& toolState, + static std::unique_ptr Create(const DxgiAPI* dxgi, + BoundsToolState& toolState, CommonState& commonState, const MonitorInfo& monitor); - static std::unique_ptr Create(Serialized& toolState, + static std::unique_ptr Create(const DxgiAPI* dxgi, + Serialized& toolState, CommonState& commonState, const MonitorInfo& monitor); inline HWND overlayWindowHandle() const diff --git a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp index 4ccd9d9f0e0..9325255d7e8 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.cpp @@ -3,9 +3,9 @@ #include "PerGlyphOpacityTextRender.h" PerGlyphOpacityTextRender::PerGlyphOpacityTextRender( - wil::com_ptr pD2DFactory, - wil::com_ptr rt, - wil::com_ptr baseBrush) : + winrt::com_ptr pD2DFactory, + winrt::com_ptr rt, + winrt::com_ptr baseBrush) : _pD2DFactory{ pD2DFactory.get() }, _rt{ rt.get() }, _baseBrush{ baseBrush.get() } diff --git a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h index d20b6dfce5c..3ce874f38c2 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h +++ b/src/modules/MeasureTool/MeasureToolCore/PerGlyphOpacityTextRender.h @@ -17,13 +17,13 @@ struct OpacityEffect : winrt::implements struct PerGlyphOpacityTextRender : winrt::implements { ID2D1Factory* _pD2DFactory = nullptr; - ID2D1HwndRenderTarget* _rt = nullptr; + ID2D1RenderTarget* _rt = nullptr; ID2D1SolidColorBrush* _baseBrush = nullptr; PerGlyphOpacityTextRender( - wil::com_ptr pD2DFactory, - wil::com_ptr rt, - wil::com_ptr baseBrush); + winrt::com_ptr pD2DFactory, + winrt::com_ptr rt, + winrt::com_ptr baseBrush); HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext, FLOAT baselineOriginX, diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp index f7e613a2b6e..d4f90ba678b 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "../MeasureToolModuleInterface/trace.h" @@ -14,8 +15,6 @@ //#define DEBUG_PRIMARY_MONITOR_ONLY -std::recursive_mutex gpuAccessLock; - namespace winrt::PowerToys::MeasureToolCore::implementation { void Core::MouseCaptureThread() @@ -34,17 +33,33 @@ namespace winrt::PowerToys::MeasureToolCore::implementation _stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset }, _mouseCaptureThread{ [this] { MouseCaptureThread(); } } { - Trace::RegisterProvider(); - LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool"); } Core::~Core() { - _stopMouseCaptureThreadSignal.SetEvent(); - _mouseCaptureThread.join(); + Close(); + } + void Core::Close() + { ResetState(); - Trace::UnregisterProvider(); + + // avoid triggering d2d debug layer leak on shutdown + dxgiAPI = DxgiAPI{ DxgiAPI::Uninitialized{} }; + +#if 0 + winrt::com_ptr dxgiDebug; + winrt::check_hresult(DXGIGetDebugInterface1({}, + winrt::guid_of(), + 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() @@ -67,6 +82,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation _settings = Settings::LoadFromFile(); + _commonState.units = _settings.units; _commonState.lineColor.r = _settings.lineColor[0] / 255.f; _commonState.lineColor.g = _settings.lineColor[1] / 255.f; _commonState.lineColor.b = _settings.lineColor[2] / 255.f; @@ -78,13 +94,15 @@ namespace winrt::PowerToys::MeasureToolCore::implementation ResetState(); #if defined(DEBUG_PRIMARY_MONITOR_ONLY) - const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor(); + std::vector monitors = { MonitorInfo::GetPrimaryMonitor() }; + const auto& monitorInfo = monitors[0]; #else const auto monitors = MonitorInfo::GetMonitors(true); for (const auto& monitorInfo : monitors) #endif { - auto overlayUI = OverlayUIState::Create(_boundsToolState, + auto overlayUI = OverlayUIState::Create(&dxgiAPI, + _boundsToolState, _commonState, monitorInfo); #if !defined(DEBUG_PRIMARY_MONITOR_ONLY) @@ -120,7 +138,8 @@ namespace winrt::PowerToys::MeasureToolCore::implementation for (const auto& monitorInfo : monitors) #endif { - auto overlayUI = OverlayUIState::Create(_measureToolState, + auto overlayUI = OverlayUIState::Create(&dxgiAPI, + _measureToolState, _commonState, monitorInfo); #if !defined(DEBUG_PRIMARY_MONITOR_ONLY) @@ -133,7 +152,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation for (size_t i = 0; i < monitors.size(); ++i) { auto thread = StartCapturingThread( - &_d3dState, + &dxgiAPI, _commonState, _measureToolState, _overlayUIStates[i]->overlayWindowHandle(), diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h index 4cb91d1069b..79728794a92 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.h @@ -8,12 +8,29 @@ #include #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 { - struct Core : CoreT + struct Core : PowerToysMisc, CoreT { Core(); ~Core(); + void Close(); + void StartBoundsTool(); void StartMeasureTool(const bool horizontal, const bool vertical); void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger); @@ -22,7 +39,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation float GetDPIScaleForWindow(uint64_t windowHandle); void MouseCaptureThread(); - D3DState _d3dState; + DxgiAPI dxgiAPI; wil::shared_event _stopMouseCaptureThreadSignal; std::thread _mouseCaptureThread; diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl index 95e68910a14..8704c0b6cd7 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.idl @@ -11,7 +11,7 @@ namespace PowerToys delegate void ToolSessionCompleted(); [default_interface] - runtimeclass Core + runtimeclass Core : Windows.Foundation.IClosable { Core(); void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger); diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj index 9780d0d09aa..fed339372db 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj @@ -55,7 +55,7 @@ Windows false MeasureTool.def - Shell32.lib;Shcore.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies) + Dbghelp.lib;Shell32.lib;Shcore.lib;dcomp.lib;DXGI.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies) @@ -74,8 +74,11 @@ + + + @@ -96,7 +99,9 @@ + + diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters index 316a5b7aa68..b2ce8ec14bf 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj.filters @@ -17,6 +17,8 @@ + + @@ -34,6 +36,9 @@ + + + diff --git a/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp index 2f95d175443..6b62b2db594 100644 --- a/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.cpp @@ -10,65 +10,47 @@ //#define DEBUG_EDGES -D3DState::D3DState() +namespace { - UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; -#ifndef NDEBUG - flags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - HRESULT hr = - D3D11CreateDevice(nullptr, - D3D_DRIVER_TYPE_HARDWARE, - nullptr, - flags, - nullptr, - 0, - D3D11_SDK_VERSION, - d3dDevice.put(), - nullptr, - nullptr); - if (hr == DXGI_ERROR_UNSUPPORTED) + winrt::GraphicsCaptureItem CreateCaptureItemForMonitor(HMONITOR monitor) { - hr = D3D11CreateDevice(nullptr, - D3D_DRIVER_TYPE_WARP, - nullptr, - flags, - nullptr, - 0, - D3D11_SDK_VERSION, - d3dDevice.put(), - nullptr, - nullptr); - } - winrt::check_hresult(hr); + auto captureInterop = winrt::get_activation_factory< + winrt::GraphicsCaptureItem, + IGraphicsCaptureItemInterop>(); + + winrt::GraphicsCaptureItem item = nullptr; - dxgiDevice = d3dDevice.as(); - winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), d3dDeviceInspectable.put())); + winrt::check_hresult(captureInterop->CreateForMonitor( + monitor, + winrt::guid_of(), + winrt::put_abi(item))); + + return item; + } } class D3DCaptureState final { - D3DState* d3dState = nullptr; + DxgiAPI* dxgiAPI = nullptr; winrt::IDirect3DDevice device; winrt::com_ptr swapChain; - winrt::com_ptr context; - winrt::SizeInt32 frameSize; + winrt::SizeInt32 frameSize; + HMONITOR monitor = {}; winrt::DirectXPixelFormat pixelFormat; - winrt::Direct3D11CaptureFramePool framePool; - winrt::GraphicsCaptureSession session; + + winrt::Direct3D11CaptureFramePool framePool = nullptr; + winrt::GraphicsCaptureSession session = nullptr; std::function frameCallback; Box monitorArea; - bool captureOutsideOfMonitor = false; + bool continuousCapture = false; - D3DCaptureState(D3DState* d3dState, - winrt::com_ptr _swapChain, - winrt::com_ptr _context, - const winrt::GraphicsCaptureItem& item, - winrt::DirectXPixelFormat _pixelFormat, - Box monitorArea, - const bool captureOutsideOfMonitor); + D3DCaptureState(DxgiAPI* dxgiAPI, + winrt::com_ptr swapChain, + winrt::DirectXPixelFormat pixelFormat, + MonitorInfo monitorInfo, + const bool continuousCapture); winrt::com_ptr CopyFrameToCPU(const winrt::com_ptr& texture); @@ -76,14 +58,13 @@ class D3DCaptureState final void StartSessionInPreferredMode(); - std::mutex destructorMutex; + std::mutex frameArrivedMutex; public: - static std::unique_ptr Create(D3DState* d3dState, - winrt::GraphicsCaptureItem item, + static std::unique_ptr Create(DxgiAPI* dxgiAPI, + MonitorInfo monitorInfo, const winrt::DirectXPixelFormat pixelFormat, - Box monitorSize, - const bool captureOutsideOfMonitor); + const bool continuousCapture); ~D3DCaptureState(); @@ -93,25 +74,19 @@ class D3DCaptureState final void StopCapture(); }; -D3DCaptureState::D3DCaptureState(D3DState* _d3dState, +D3DCaptureState::D3DCaptureState(DxgiAPI* dxgiAPI, winrt::com_ptr _swapChain, - winrt::com_ptr _context, - const winrt::GraphicsCaptureItem& item, - winrt::DirectXPixelFormat _pixelFormat, - Box _monitorArea, - const bool _captureOutsideOfMonitor) : - d3dState{ _d3dState }, - device{ _d3dState->d3dDeviceInspectable.as() }, + winrt::DirectXPixelFormat pixelFormat_, + MonitorInfo monitorInfo, + const bool continuousCapture_) : + dxgiAPI{ dxgiAPI }, + device{ dxgiAPI->d3dForCapture.d3dDeviceInspectable.as() }, swapChain{ std::move(_swapChain) }, - context{ std::move(_context) }, - frameSize{ item.Size() }, - pixelFormat{ std::move(_pixelFormat) }, - framePool{ winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 1, item.Size()) }, - session{ framePool.CreateCaptureSession(item) }, - monitorArea{ _monitorArea }, - captureOutsideOfMonitor{ _captureOutsideOfMonitor } + pixelFormat{ std::move(pixelFormat_) }, + monitor{ monitorInfo.GetHandle() }, + monitorArea{ monitorInfo.GetScreenSize(true) }, + continuousCapture{ continuousCapture_ } { - framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived }); } winrt::com_ptr D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr& frameTexture) @@ -124,8 +99,8 @@ winrt::com_ptr D3DCaptureState::CopyFrameToCPU(const winrt::com desc.BindFlags = 0; winrt::com_ptr cpuTexture; - winrt::check_hresult(d3dState->d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put())); - context->CopyResource(cpuTexture.get(), frameTexture.get()); + winrt::check_hresult(dxgiAPI->d3dForCapture.d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put())); + dxgiAPI->d3dForCapture.d3dContext->CopyResource(cpuTexture.get(), frameTexture.get()); return cpuTexture; } @@ -142,15 +117,25 @@ auto GetDXGIInterfaceFromObject(winrt::IInspectable const& object) void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&) { // Prevent calling a callback on a partially destroyed state - std::unique_lock callbackLock{ destructorMutex }; + std::lock_guard callbackLock{ frameArrivedMutex }; bool resized = false; POINT cursorPos = {}; GetCursorPos(&cursorPos); - auto frame = sender.TryGetNextFrame(); - winrt::check_bool(frame); - if (monitorArea.inside(cursorPos) || captureOutsideOfMonitor) + winrt::Direct3D11CaptureFrame frame = nullptr; + try + { + frame = sender.TryGetNextFrame(); + } + catch (...) + { + } + + if (!frame) + return; + + if (monitorArea.inside(cursorPos) || !continuousCapture) { winrt::com_ptr texture; { @@ -170,7 +155,10 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se auto gpuTexture = GetDXGIInterfaceFromObject(surface); texture = CopyFrameToCPU(gpuTexture); surface.Close(); - MappedTextureView textureView{ texture, context, static_cast(frameSize.Width), static_cast(frameSize.Height) }; + MappedTextureView textureView{ texture, + dxgiAPI->d3dForCapture.d3dContext, + static_cast(frameSize.Width), + static_cast(frameSize.Height) }; frameCallback(std::move(textureView)); } @@ -178,69 +166,60 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se frame.Close(); - DXGI_PRESENT_PARAMETERS presentParameters = {}; - swapChain->Present1(1, 0, &presentParameters); - if (resized) { framePool.Recreate(device, pixelFormat, 2, frameSize); } } -std::unique_ptr D3DCaptureState::Create(D3DState* d3dState, - winrt::GraphicsCaptureItem item, +std::unique_ptr D3DCaptureState::Create(DxgiAPI* dxgiAPI, + MonitorInfo monitorInfo, const winrt::DirectXPixelFormat pixelFormat, - Box monitorArea, - const bool captureOutsideOfMonitor) + const bool continuousCapture) { - std::lock_guard guard{ gpuAccessLock }; - + const auto dims = monitorInfo.GetScreenSize(true); const DXGI_SWAP_CHAIN_DESC1 desc = { - .Width = static_cast(item.Size().Width), - .Height = static_cast(item.Size().Height), + .Width = static_cast(dims.width()), + .Height = static_cast(dims.height()), .Format = static_cast(pixelFormat), .SampleDesc = { .Count = 1, .Quality = 0 }, .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT, .BufferCount = 2, .Scaling = DXGI_SCALING_STRETCH, - .SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + .SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD, .AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED, }; - winrt::com_ptr adapter; - winrt::check_hresult(d3dState->dxgiDevice->GetParent(winrt::guid_of(), adapter.put_void())); - winrt::com_ptr factory; - winrt::check_hresult(adapter->GetParent(winrt::guid_of(), factory.put_void())); winrt::com_ptr swapChain; - winrt::check_hresult(factory->CreateSwapChainForComposition(d3dState->d3dDevice.get(), &desc, nullptr, swapChain.put())); - - winrt::com_ptr context; - d3dState->d3dDevice->GetImmediateContext(context.put()); - winrt::check_bool(context); - auto contextMultithread = context.as(); - contextMultithread->SetMultithreadProtected(true); + winrt::check_hresult(dxgiAPI->d3dForCapture.dxgiFactory2->CreateSwapChainForComposition(dxgiAPI->d3dForCapture.d3dDevice.get(), + &desc, + nullptr, + swapChain.put())); // 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(context), - item, pixelFormat, - monitorArea, - captureOutsideOfMonitor }; + std::move(monitorInfo), + continuousCapture }; return std::unique_ptr{ statePtr }; } D3DCaptureState::~D3DCaptureState() { - std::unique_lock callbackLock{ destructorMutex }; + std::unique_lock callbackLock{ frameArrivedMutex }; StopCapture(); - framePool.Close(); } 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) if (auto session3 = session.try_as()) { @@ -270,27 +249,35 @@ MappedTextureView D3DCaptureState::CaptureSingleFrame() result.emplace(std::move(tex)); frameArrivedEvent.SetEvent(); }; - std::lock_guard guard{ gpuAccessLock }; StartSessionInPreferredMode(); frameArrivedEvent.wait(); - assert(result.has_value()); return std::move(*result); } void D3DCaptureState::StopCapture() { - session.Close(); + try + { + if (session) + session.Close(); + + if (framePool) + framePool.Close(); + } + catch (...) + { + // RPC call might fail here + } } void UpdateCaptureState(const CommonState& commonState, Serialized& state, HWND window, - const MappedTextureView& textureView, - const bool continuousCapture) + const MappedTextureView& textureView) { - 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 cursorInTopScreenHalf = cursorPos.y < textureView.view.height / 2; uint8_t pixelTolerance = {}; @@ -310,8 +297,7 @@ void UpdateCaptureState(const CommonState& commonState, const RECT bounds = DetectEdges(textureView.view, cursorPos, perColorChannelEdgeDetection, - pixelTolerance, - continuousCapture); + pixelTolerance); #if defined(DEBUG_EDGES) char buffer[256]; @@ -328,50 +314,54 @@ void UpdateCaptureState(const CommonState& commonState, OutputDebugStringA(buffer); #endif 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, Serialized& state, HWND window, MonitorInfo monitor) { - return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, d3dState] { - auto captureInterop = winrt::get_activation_factory< - winrt::GraphicsCaptureItem, - IGraphicsCaptureItemInterop>(); - - winrt::GraphicsCaptureItem item = nullptr; - - winrt::check_hresult(captureInterop->CreateForMonitor( - monitor.GetHandle(), - winrt::guid_of(), - winrt::put_abi(item))); - + return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, dxgiAPI] { bool continuousCapture = {}; state.Read([&](const MeasureToolState& state) { continuousCapture = state.global.continuousCapture; }); - const auto monitorArea = monitor.GetScreenSize(true); - auto captureState = D3DCaptureState::Create(d3dState, - item, + auto captureState = D3DCaptureState::Create(dxgiAPI, + monitor, winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized, - monitorArea, - !continuousCapture); + continuousCapture); + const auto monitorArea = monitor.GetScreenSize(true); + bool mouseOnMonitor = false; if (continuousCapture) { - captureState->StartCapture([&, window](MappedTextureView textureView) { - UpdateCaptureState(commonState, state, window, textureView, continuousCapture); - }); - while (IsWindow(window) && !commonState.closeOnOtherMonitors) { - std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION); + if (mouseOnMonitor == monitorArea.inside(commonState.cursorPosSystemSpace)) + { + 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 { @@ -394,7 +384,15 @@ std::thread StartCapturingThread(D3DState* d3dState, auto path = std::filesystem::temp_directory_path() / buf; textureView.view.SaveAsBitmap(path.string().c_str()); #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::high_resolution_clock::now() - now); @@ -404,5 +402,7 @@ std::thread StartCapturingThread(D3DState* d3dState, } } } + + captureState->StopCapture(); }); } diff --git a/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.h b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.h index 8cb7e912e63..4ae3554125a 100644 --- a/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.h +++ b/src/modules/MeasureTool/MeasureToolCore/ScreenCapturing.h @@ -1,19 +1,11 @@ #pragma once +#include "DxgiAPI.h" #include "ToolState.h" #include -struct D3DState -{ - winrt::com_ptr d3dDevice; - winrt::com_ptr dxgiDevice; - winrt::com_ptr d3dDeviceInspectable; - - D3DState(); -}; - -std::thread StartCapturingThread(D3DState* d3dState, +std::thread StartCapturingThread(DxgiAPI* dxgiAPI, const CommonState& commonState, Serialized& state, HWND targetWindow, diff --git a/src/modules/MeasureTool/MeasureToolCore/Settings.cpp b/src/modules/MeasureTool/MeasureToolCore/Settings.cpp index 22c809bec81..418b6d1a957 100644 --- a/src/modules/MeasureTool/MeasureToolCore/Settings.cpp +++ b/src/modules/MeasureTool/MeasureToolCore/Settings.cpp @@ -15,6 +15,7 @@ namespace 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_MEASURE_CROSS_COLOR[] = L"MeasureCrossColor"; + const wchar_t JSON_KEY_UNITS_OF_MEASURE[] = L"UnitsOfMeasure"; } Settings Settings::LoadFromFile() @@ -65,6 +66,14 @@ Settings Settings::LoadFromFile() catch (...) { } + + try + { + result.units = static_cast(props.GetNamedObject(JSON_KEY_UNITS_OF_MEASURE).GetNamedNumber(JSON_KEY_VALUE)); + } + catch (...) + { + } } catch (...) { diff --git a/src/modules/MeasureTool/MeasureToolCore/Settings.h b/src/modules/MeasureTool/MeasureToolCore/Settings.h index 75a397abc2e..76f3af7ea24 100644 --- a/src/modules/MeasureTool/MeasureToolCore/Settings.h +++ b/src/modules/MeasureTool/MeasureToolCore/Settings.h @@ -3,6 +3,8 @@ #include #include +#include "Measurement.h" + struct Settings { uint8_t pixelTolerance = 30; @@ -10,6 +12,7 @@ struct Settings bool drawFeetOnCross = true; bool perColorChannelEdgeDetection = false; std::array lineColor = {255, 69, 0}; + Measurement::Unit units = Measurement::Unit::Pixel; static Settings LoadFromFile(); }; \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/ToolState.h b/src/modules/MeasureTool/MeasureToolCore/ToolState.h index e5222892f07..b4e14a322b8 100644 --- a/src/modules/MeasureTool/MeasureToolCore/ToolState.h +++ b/src/modules/MeasureTool/MeasureToolCore/ToolState.h @@ -16,10 +16,11 @@ //#define DEBUG_OVERLAY #include "BGRATextureView.h" +#include "Measurement.h" struct OverlayBoxText { - std::array buffer = {}; + std::array buffer = {}; }; struct CommonState @@ -28,6 +29,8 @@ struct CommonState D2D1::ColorF lineColor = D2D1::ColorF::OrangeRed; Box toolbarBoundingBox; + Measurement::Unit units = Measurement::Unit::Pixel; + mutable Serialized overlayBoxText; POINT cursorPosSystemSpace = {}; // updated atomically std::atomic_bool closeOnOtherMonitors = false; @@ -38,7 +41,7 @@ struct BoundsToolState struct PerScreen { std::optional currentRegionStart; - std::vector measurements; + std::vector measurements; }; std::unordered_map perScreen; @@ -67,7 +70,7 @@ struct MeasureToolState { bool cursorInLeftScreenHalf = false; bool cursorInTopScreenHalf = false; - RECT measuredEdges = {}; + std::optional measuredEdges; // While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed // directly from a capturing thread. const MappedTextureView* capturedScreenTexture = nullptr; @@ -79,6 +82,3 @@ struct MeasureToolState CommonState* commonState = nullptr; // required for WndProc }; - -// Concurrently accessing Direct2D and Direct3D APIs make the driver go boom -extern std::recursive_mutex gpuAccessLock; \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/constants.h b/src/modules/MeasureTool/MeasureToolCore/constants.h index dbe7ab3dbde..36027140345 100644 --- a/src/modules/MeasureTool/MeasureToolCore/constants.h +++ b/src/modules/MeasureTool/MeasureToolCore/constants.h @@ -4,12 +4,11 @@ namespace consts { - constexpr inline size_t TARGET_FRAME_RATE = 120; - constexpr inline auto TARGET_FRAME_DURATION = std::chrono::milliseconds{ 1000 } / TARGET_FRAME_RATE; + constexpr inline size_t TARGET_FRAME_RATE = 90; + constexpr inline auto TARGET_FRAME_DURATION = std::chrono::microseconds{ 1000000 } / TARGET_FRAME_RATE; constexpr inline float FONT_SIZE = 14.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 SHADOW_OPACITY = .4f; constexpr inline float SHADOW_RADIUS = 6.f; diff --git a/src/modules/MeasureTool/MeasureToolCore/pch.h b/src/modules/MeasureTool/MeasureToolCore/pch.h index f2700373e2b..67ca0f9d8cd 100644 --- a/src/modules/MeasureTool/MeasureToolCore/pch.h +++ b/src/modules/MeasureTool/MeasureToolCore/pch.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/src/modules/MeasureTool/MeasureToolUI/App.xaml.cs b/src/modules/MeasureTool/MeasureToolUI/App.xaml.cs index d3fbed10fcb..348f1f9b940 100644 --- a/src/modules/MeasureTool/MeasureToolUI/App.xaml.cs +++ b/src/modules/MeasureTool/MeasureToolUI/App.xaml.cs @@ -4,6 +4,7 @@ using System; using ManagedCommon; +using MeasureToolUI.Helpers; using Microsoft.UI.Xaml; namespace MeasureToolUI @@ -42,7 +43,18 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) } } - _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(); } diff --git a/src/modules/MeasureTool/MeasureToolUI/Logger.cs b/src/modules/MeasureTool/MeasureToolUI/Logger.cs new file mode 100644 index 00000000000..0261433abbf --- /dev/null +++ b/src/modules/MeasureTool/MeasureToolUI/Logger.cs @@ -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; + } + } +} diff --git a/src/modules/MeasureTool/MeasureToolUI/MainWindow.xaml.cs b/src/modules/MeasureTool/MeasureToolUI/MainWindow.xaml.cs index 6881c3ffad3..f08a614aa45 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MainWindow.xaml.cs +++ b/src/modules/MeasureTool/MeasureToolUI/MainWindow.xaml.cs @@ -18,12 +18,12 @@ namespace MeasureToolUI /// /// An empty window that can be used on its own or navigated to within a Frame. /// - public sealed partial class MainWindow : WindowEx + public sealed partial class MainWindow : WindowEx, IDisposable { private const int WindowWidth = 216; private const int WindowHeight = 50; - private PowerToys.MeasureToolCore.Core _coreLogic = new PowerToys.MeasureToolCore.Core(); + private PowerToys.MeasureToolCore.Core _coreLogic; private AppWindow _appWindow; private PointInt32 _initialPosition; @@ -34,14 +34,14 @@ protected override void OnPositionChanged(PointInt32 position) this.SetWindowSize(WindowWidth, WindowHeight); } - public MainWindow() + public MainWindow(PowerToys.MeasureToolCore.Core core) { InitializeComponent(); var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); _appWindow = AppWindow.GetFromWindowId(windowId); - + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); var presenter = _appWindow.Presenter as OverlappedPresenter; presenter.IsAlwaysOnTop = true; this.SetIsAlwaysOnTop(true); @@ -51,6 +51,8 @@ public MainWindow() this.SetIsMaximizable(false); IsTitleBarVisible = false; + _coreLogic = core; + Closed += MainWindow_Closed; DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest); float dpiScale = _coreLogic.GetDPIScaleForWindow((int)hwnd); @@ -64,6 +66,12 @@ public MainWindow() OnPositionChanged(_initialPosition); } + private void MainWindow_Closed(object sender, WindowEventArgs args) + { + _coreLogic?.Dispose(); + _coreLogic = null; + } + private void UpdateToolUsageCompletionEvent(object sender) { _coreLogic.SetToolCompletionEvent(new PowerToys.MeasureToolCore.ToolSessionCompleted(() => @@ -136,5 +144,10 @@ private void ClosePanelTool_Click(object sender, RoutedEventArgs e) _coreLogic.ResetState(); this.Close(); } + + public void Dispose() + { + _coreLogic?.Dispose(); + } } } diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj index 0aa06d99db1..f158d6b81df 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj @@ -79,6 +79,7 @@ + diff --git a/src/modules/MeasureTool/MeasureToolUI/NativeMethods.cs b/src/modules/MeasureTool/MeasureToolUI/NativeMethods.cs index 5a0fef785c5..877adf54e1b 100644 --- a/src/modules/MeasureTool/MeasureToolUI/NativeMethods.cs +++ b/src/modules/MeasureTool/MeasureToolUI/NativeMethods.cs @@ -13,5 +13,6 @@ internal static class NativeMethods internal static readonly IntPtr HWND_TOPMOST = new System.IntPtr(-1); internal const uint SWP_NOSIZE = 0x0001; internal const uint SWP_NOMOVE = 0x0002; + internal const uint SWP_NOACTIVATE = 0x0010; internal const uint SWP_SHOWWINDOW = 0x0040; } diff --git a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs index 2becc0bc4b7..f83db3dfb5a 100644 --- a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs @@ -14,6 +14,7 @@ public class MeasureToolProperties public MeasureToolProperties() { ActivationShortcut = new HotkeySettings(true, false, false, true, 0x4D); + UnitsOfMeasure = new IntProperty(0); PixelTolerance = new IntProperty(30); ContinuousCapture = true; DrawFeetOnCross = true; @@ -32,6 +33,8 @@ public MeasureToolProperties() [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool PerColorChannelEdgeDetection { get; set; } + public IntProperty UnitsOfMeasure { get; set; } + public IntProperty PixelTolerance { get; set; } public StringProperty MeasureCrossColor { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/MeasureToolViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/MeasureToolViewModel.cs index 7dacf4c25fc..8b16ac7a73e 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/MeasureToolViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/MeasureToolViewModel.cs @@ -129,6 +129,23 @@ public bool PerColorChannelEdgeDetection } } + 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 { get diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 58203c4ca2d..79b2fae5e14 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -155,6 +155,18 @@ Customize the shortcut to bring up the command bar "Screen Ruler" is the name of the utility + + Units of measurement + + + Pixels + + + Inches + + + Centimeters + Pixel tolerance for edge detection diff --git a/src/settings-ui/Settings.UI/Views/MeasureToolPage.xaml b/src/settings-ui/Settings.UI/Views/MeasureToolPage.xaml index 49c5ad068cc..92b144e4958 100644 --- a/src/settings-ui/Settings.UI/Views/MeasureToolPage.xaml +++ b/src/settings-ui/Settings.UI/Views/MeasureToolPage.xaml @@ -49,6 +49,7 @@ IsOpen="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}" IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}" IsClosable="False" /> + + + +