OverlayUI.cpp
1 #include "pch.h" 2 3 #include "BoundsToolOverlayUI.h" 4 #include "constants.h" 5 #include "MeasureToolOverlayUI.h" 6 #include "OverlayUI.h" 7 8 #include <common/Display/dpi_aware.h> 9 #include <common/Display/monitors.h> 10 #include <common/logger/logger.h> 11 #include <common/Themes/windows_colors.h> 12 #include <common/utils/window.h> 13 14 namespace NonLocalizable 15 { 16 const wchar_t MeasureToolOverlayWindowName[] = L"PowerToys.MeasureToolOverlayWindow"; 17 const wchar_t BoundsToolOverlayWindowName[] = L"PowerToys.BoundsToolOverlayWindow"; 18 } 19 20 void CreateOverlayWindowClasses() 21 { 22 WNDCLASSEXW wcex{ .cbSize = sizeof(WNDCLASSEX), .hInstance = GetModuleHandleW(nullptr) }; 23 24 wcex.lpfnWndProc = MeasureToolWndProc; 25 wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName; 26 wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS); 27 RegisterClassExW(&wcex); 28 29 wcex.lpfnWndProc = BoundsToolWndProc; 30 wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName; 31 RegisterClassExW(&wcex); 32 } 33 34 HWND CreateOverlayUIWindow(const CommonState& commonState, 35 const MonitorInfo& monitor, 36 const bool excludeFromCapture, 37 const wchar_t* windowClass, 38 void* extraParam) 39 { 40 static std::once_flag windowClassesCreatedFlag; 41 std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses); 42 43 const auto screenArea = monitor.GetScreenSize(true); 44 DWORD windowStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW; 45 #if !defined(DEBUG_OVERLAY) 46 windowStyle |= WS_EX_TOPMOST; 47 #endif 48 HWND window{ 49 CreateWindowExW(windowStyle, 50 windowClass, 51 L"PowerToys.MeasureToolOverlay", 52 WS_POPUP | CS_HREDRAW | CS_VREDRAW, 53 screenArea.left(), 54 screenArea.top(), 55 screenArea.width(), 56 screenArea.height(), 57 HWND_DESKTOP, 58 nullptr, 59 GetModuleHandleW(nullptr), 60 extraParam) 61 }; 62 winrt::check_bool(window); 63 64 // Exclude overlay window from displaying in WIN+TAB preview, since WS_EX_TOOLWINDOW windows are displayed simultaneously on all virtual desktops. 65 // We can't remove WS_EX_TOOLWINDOW/WS_EX_NOACTIVATE flag, since we want to exclude the window from taskbar 66 BOOL val = TRUE; 67 DwmSetWindowAttribute(window, DWMWA_EXCLUDED_FROM_PEEK, &val, sizeof(val)); 68 69 // We want to receive input events as soon as possible to prevent issues with touch input 70 RegisterTouchWindow(window, TWF_WANTPALM); 71 72 ShowWindow(window, SW_SHOWNORMAL); 73 UpdateWindow(window); 74 if (excludeFromCapture) 75 { 76 SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE); 77 } 78 #if !defined(DEBUG_OVERLAY) 79 SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE); 80 #else 81 (void)window; 82 #endif 83 84 const int pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8; 85 if (wil::unique_hrgn hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) }) 86 { 87 DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn.get(), FALSE }; 88 DwmEnableBlurBehindWindow(window, &bh); 89 } 90 91 RECT windowRect = {}; 92 // Exclude toolbar from the window's region to be able to use toolbar during tool usage. 93 if (monitor.IsPrimary() && GetWindowRect(window, &windowRect)) 94 { 95 // will be freed during SetWindowRgn call 96 const HRGN windowRegion{ CreateRectRgn(windowRect.left, windowRect.top, windowRect.right, windowRect.bottom) }; 97 wil::unique_hrgn toolbarRegion{ CreateRectRgn(commonState.toolbarBoundingBox.left(), 98 commonState.toolbarBoundingBox.top(), 99 commonState.toolbarBoundingBox.right(), 100 commonState.toolbarBoundingBox.bottom()) }; 101 const auto res = CombineRgn(windowRegion, windowRegion, toolbarRegion.get(), RGN_DIFF); 102 if (res != ERROR) 103 SetWindowRgn(window, windowRegion, true); 104 } 105 106 return window; 107 } 108 109 std::vector<D2D1::ColorF> AppendCommonOverlayUIColors(const D2D1::ColorF& lineColor) 110 { 111 D2D1::ColorF foreground = D2D1::ColorF::Black; 112 D2D1::ColorF background = D2D1::ColorF(0.96f, 0.96f, 0.96f, .93f); 113 D2D1::ColorF border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f); 114 115 if (WindowsColors::is_dark_mode()) 116 { 117 foreground = D2D1::ColorF::White; 118 background = D2D1::ColorF(0.17f, 0.17f, 0.17f, .93f); 119 border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f); 120 } 121 122 return { lineColor, foreground, background, border }; 123 } 124 125 void OverlayUIState::RunUILoop() 126 { 127 bool cursorOnScreen = false; 128 129 while (IsWindow(_window) && !_commonState.closeOnOtherMonitors) 130 { 131 const auto now = std::chrono::high_resolution_clock::now(); 132 const auto cursor = _commonState.cursorPosSystemSpace; 133 const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor); 134 auto& dxgi = _d2dState.dxgiWindowState; 135 if (_monitorArea.inside(cursor) != cursorOnScreen) 136 { 137 cursorOnScreen = !cursorOnScreen; 138 if (!cursorOnScreen) 139 { 140 PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {}); 141 } 142 } 143 run_message_loop(true, 1); 144 145 dxgi.rt->BeginDraw(); 146 dxgi.rt->Clear(); 147 148 if (!cursorOverToolbar) 149 _tickFunc(); 150 151 dxgi.rt->EndDraw(); 152 dxgi.swapChain->Present(0, 0); 153 154 if (cursorOnScreen) 155 { 156 const auto frameTime = std::chrono::high_resolution_clock::now() - now; 157 if (frameTime < consts::TARGET_FRAME_DURATION) 158 { 159 std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION - frameTime); 160 } 161 } 162 else 163 { 164 // Don't consume resources while nothing could be updated 165 std::this_thread::sleep_for(std::chrono::milliseconds{ 200 }); 166 } 167 } 168 169 DestroyWindow(_window); 170 } 171 172 template<typename StateT, typename TickFuncT> 173 OverlayUIState::OverlayUIState(const DxgiAPI* dxgiAPI, 174 StateT& toolState, 175 TickFuncT tickFunc, 176 const CommonState& commonState, 177 HWND window) : 178 _window{ window }, 179 _commonState{ commonState }, 180 _d2dState{ dxgiAPI, window, AppendCommonOverlayUIColors(commonState.lineColor) }, 181 _tickFunc{ [this, tickFunc, &toolState] { 182 tickFunc(_commonState, toolState, _window, _d2dState); 183 } } 184 { 185 } 186 187 OverlayUIState::~OverlayUIState() 188 { 189 PostMessageW(_window, WM_CLOSE, {}, {}); 190 try 191 { 192 if (_uiThread.joinable()) 193 _uiThread.join(); 194 } 195 catch (...) 196 { 197 } 198 } 199 200 // Returning unique_ptr, since we need to pin ui state in memory 201 template<typename ToolT, typename TickFuncT> 202 inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(const DxgiAPI* dxgi, 203 ToolT& toolState, 204 TickFuncT tickFunc, 205 CommonState& commonState, 206 const wchar_t* toolWindowClassName, 207 void* windowParam, 208 const MonitorInfo& monitor, 209 const bool excludeFromCapture) 210 { 211 wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset); 212 std::unique_ptr<OverlayUIState> uiState; 213 std::thread threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] { 214 OverlayUIState* state = nullptr; 215 { 216 auto sinalUICreatedEvent = wil::scope_exit([&] { uiCreatedEvent.SetEvent(); }); 217 218 const HWND window = CreateOverlayUIWindow(commonState, monitor, excludeFromCapture, toolWindowClassName, windowParam); 219 220 uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ dxgi, toolState, tickFunc, commonState, window } }; 221 uiState->_monitorArea = monitor.GetScreenSize(true); 222 // we must create window + d2d state in the same thread, then store thread handle in uiState, thus 223 // lifetime is ok here, since we join the thread in destructor 224 state = uiState.get(); 225 } 226 227 state->RunUILoop(); 228 229 commonState.closeOnOtherMonitors = true; 230 commonState.sessionCompletedCallback(); 231 }); 232 233 uiCreatedEvent.wait(); 234 if (uiState) 235 uiState->_uiThread = std::move(threadHandle); 236 else if (threadHandle.joinable()) 237 threadHandle.join(); 238 239 return uiState; 240 } 241 242 std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi, 243 Serialized<MeasureToolState>& toolState, 244 CommonState& commonState, 245 const MonitorInfo& monitor) 246 { 247 bool excludeFromCapture = false; 248 toolState.Read([&](const MeasureToolState& s) { 249 excludeFromCapture = s.global.continuousCapture; 250 }); 251 return OverlayUIState::CreateInternal(dxgi, 252 toolState, 253 DrawMeasureToolTick, 254 commonState, 255 NonLocalizable::MeasureToolOverlayWindowName, 256 &toolState, 257 monitor, 258 excludeFromCapture); 259 } 260 261 std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi, 262 BoundsToolState& toolState, 263 CommonState& commonState, 264 const MonitorInfo& monitor) 265 { 266 return OverlayUIState::CreateInternal(dxgi, 267 toolState, 268 DrawBoundsToolTick, 269 commonState, 270 NonLocalizable::BoundsToolOverlayWindowName, 271 &toolState, 272 monitor, 273 false); 274 }