/ src / modules / MeasureTool / MeasureToolCore / OverlayUI.cpp
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  }