/ src / modules / MeasureTool / MeasureToolCore / MeasureToolOverlayUI.cpp
MeasureToolOverlayUI.cpp
  1  #include "pch.h"
  2  
  3  #include "BGRATextureView.h"
  4  #include "Clipboard.h"
  5  #include "CoordinateSystemConversion.h"
  6  #include "constants.h"
  7  #include "MeasureToolOverlayUI.h"
  8  
  9  #include <common/utils/window.h>
 10  
 11  #include <exception>
 12  #include <iostream>
 13  #include <utility>
 14  #include <vector>
 15  
 16  namespace
 17  {
 18      constexpr std::pair<bool, bool> GetHorizontalVerticalLines(MeasureToolState::Mode mode)
 19      {
 20          switch (mode)
 21          {
 22          case MeasureToolState::Mode::Cross:
 23              return { true, true };
 24  
 25          case MeasureToolState::Mode::Vertical:
 26              return { false, true };
 27  
 28          case MeasureToolState::Mode::Horizontal:
 29              return { true, false };
 30  
 31          default:
 32              throw std::runtime_error("Unknown MeasureToolState Mode");
 33          }
 34      }
 35  
 36      void CopyToClipboard(HWND window, const MeasureToolState& toolState)
 37      {
 38          std::vector<Measurement> allMeasurements;
 39          for (const auto& [handle, perScreen] : toolState.perScreen)
 40          {
 41              for (const auto& [_, measurement] : perScreen.prevMeasurements)
 42              {
 43                  allMeasurements.push_back(measurement);
 44              }
 45  
 46              if (handle == window && perScreen.measuredEdges)
 47              {
 48                  allMeasurements.push_back(*perScreen.measuredEdges);
 49              }
 50          }
 51  
 52          const auto [printWidth, printHeight] = GetHorizontalVerticalLines(toolState.global.mode);
 53          SetClipboardToMeasurements(allMeasurements, printWidth, printHeight, toolState.commonState->units);
 54      }
 55  
 56      inline std::pair<D2D_POINT_2F, D2D_POINT_2F> ComputeCrossFeetLine(D2D_POINT_2F center, const bool horizontal)
 57      {
 58          D2D_POINT_2F start = center, end = center;
 59          // Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines
 60          if (horizontal)
 61          {
 62              start.x -= consts::FEET_HALF_LENGTH;
 63              end.x += consts::FEET_HALF_LENGTH + 1.f;
 64          }
 65          else
 66          {
 67              start.y -= consts::FEET_HALF_LENGTH;
 68              end.y += consts::FEET_HALF_LENGTH + 1.f;
 69          }
 70  
 71          return { start, end };
 72      }
 73  
 74      bool HandleCursorUp(HWND window, MeasureToolState* toolState, const POINT cursorPos)
 75      {
 76          ClipCursor(nullptr);
 77          CopyToClipboard(window, *toolState);
 78  
 79          auto& perScreen = toolState->perScreen[window];
 80  
 81          const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000;
 82          if (shiftPress && perScreen.measuredEdges)
 83          {
 84              perScreen.prevMeasurements.push_back(MeasureToolState::PerScreen::PrevMeasurement(cursorPos, perScreen.measuredEdges.value()));
 85          }
 86  
 87          perScreen.measuredEdges = std::nullopt;
 88  
 89          return !shiftPress;
 90      }
 91  
 92      void DrawMeasurement(const Measurement& measurement,
 93                           D2DState& d2dState,
 94                           bool drawFeetOnCross,
 95                           MeasureToolState::Mode mode,
 96                           POINT cursorPos,
 97                           const CommonState& commonState,
 98                           HWND window)
 99      {
100          const auto [drawHorizontalCrossLine, drawVerticalCrossLine] = GetHorizontalVerticalLines(mode);
101  
102          const float hMeasure = measurement.Width(Measurement::Unit::Pixel);
103          const float vMeasure = measurement.Height(Measurement::Unit::Pixel);
104  
105          d2dState.ToggleAliasedLinesMode(true);
106          if (drawHorizontalCrossLine)
107          {
108              const D2D_POINT_2F hLineStart{ .x = measurement.rect.left, .y = static_cast<float>(cursorPos.y) };
109              D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
110              d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
111  
112              if (drawFeetOnCross)
113              {
114                  // To fill all pixels which are close, we call DrawLine with end point one pixel too far, since
115                  // it doesn't get filled, i.e. end point of the range is excluded. However, we want to draw cross
116                  // feet *on* the last pixel row, so we must subtract 1px from the corresponding axis.
117                  hLineEnd.x -= 1.f;
118                  const auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
119                  const auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
120                  d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
121                  d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
122              }
123          }
124  
125          if (drawVerticalCrossLine)
126          {
127              const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = measurement.rect.top };
128              D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
129              d2dState.dxgiWindowState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
130  
131              if (drawFeetOnCross)
132              {
133                  vLineEnd.y -= 1.f;
134                  const auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
135                  const auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
136                  d2dState.dxgiWindowState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
137                  d2dState.dxgiWindowState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
138              }
139          }
140  
141          d2dState.ToggleAliasedLinesMode(false);
142  
143          OverlayBoxText text;
144  
145          const auto [crossSymbolPos, measureStringBufLen] =
146              measurement.Print(text.buffer.data(),
147                                text.buffer.size(),
148                                drawHorizontalCrossLine,
149                                drawVerticalCrossLine,
150                                commonState.units | Measurement::Unit::Pixel); // Always show pixels.
151  
152          d2dState.DrawTextBox(text.buffer.data(),
153                               measureStringBufLen,
154                               crossSymbolPos,
155                               D2D_POINT_2F{ static_cast<float>(cursorPos.x), static_cast<float>(cursorPos.y) },
156                               true,
157                               window);
158      }
159  }
160  
161  winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(winrt::com_ptr<ID2D1RenderTarget> rt,
162                                                                 const MappedTextureView* capturedScreenTexture)
163  {
164      D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() };
165      rt->GetDpi(&props.dpiX, &props.dpiY);
166      const auto sizeF = rt->GetSize();
167      winrt::com_ptr<ID2D1Bitmap> bitmap;
168      auto hr = rt->CreateBitmap(D2D1::SizeU(static_cast<uint32_t>(capturedScreenTexture->view.width),
169                                             static_cast<uint32_t>(capturedScreenTexture->view.height)),
170                                 capturedScreenTexture->view.pixels,
171                                 static_cast<uint32_t>(capturedScreenTexture->view.pitch * 4),
172                                 props,
173                                 bitmap.put());
174      if (FAILED(hr))
175          return nullptr;
176  
177      return bitmap;
178  }
179  
180  LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
181  {
182      switch (message)
183      {
184      case WM_MOUSELEAVE:
185      case WM_CURSOR_LEFT_MONITOR:
186      {
187          if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
188          {
189              state->Access([&](MeasureToolState& s) {
190                  s.perScreen[window].measuredEdges = {};
191              });
192          }
193          break;
194      }
195      case WM_NCHITTEST:
196          return HTCLIENT;
197      case WM_CREATE:
198      {
199          auto state = GetWindowCreateParam<Serialized<MeasureToolState>*>(lparam);
200          StoreWindowParam(window, state);
201  
202  #if !defined(DEBUG_OVERLAY)
203          for (; ShowCursor(false) >= 0;)
204              ;
205  #endif
206          break;
207      }
208      case WM_ERASEBKGND:
209          return 1;
210      case WM_KEYUP:
211          if (wparam == VK_ESCAPE)
212          {
213              PostMessageW(window, WM_CLOSE, {}, {});
214          }
215          break;
216      case WM_RBUTTONUP:
217      {
218          PostMessageW(window, WM_CLOSE, {}, {});
219          break;
220      }
221      case WM_LBUTTONUP:
222      {
223          bool shouldClose = true;
224  
225          if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
226          {
227              state->Access([&](MeasureToolState& s) {
228                  shouldClose = HandleCursorUp(window,
229                                               &s,
230                                               convert::FromSystemToWindow(window, s.commonState->cursorPosSystemSpace));
231              });
232          }
233  
234          if (shouldClose)
235          {
236              PostMessageW(window, WM_CLOSE, {}, {});
237          }
238          break;
239      }
240      case WM_MOUSEWHEEL:
241          if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
242          {
243              const int8_t step = static_cast<short>(HIWORD(wparam)) < 0 ? -consts::MOUSE_WHEEL_TOLERANCE_STEP : consts::MOUSE_WHEEL_TOLERANCE_STEP;
244              state->Access([step](MeasureToolState& s) {
245                  int wideVal = s.global.pixelTolerance;
246                  wideVal += step;
247                  s.global.pixelTolerance = static_cast<uint8_t>(std::clamp(wideVal, 0, 255));
248              });
249          }
250          break;
251      }
252  
253      return DefWindowProcW(window, message, wparam, lparam);
254  }
255  
256  void DrawMeasureToolTick(const CommonState& commonState,
257                           Serialized<MeasureToolState>& toolState,
258                           HWND window,
259                           D2DState& d2dState)
260  {
261      bool continuousCapture = {};
262      bool drawFeetOnCross = {};
263  
264      std::optional<Measurement> measuredEdges{};
265      MeasureToolState::Mode mode = {};
266      winrt::com_ptr<ID2D1Bitmap> backgroundBitmap;
267      const MappedTextureView* backgroundTextureToConvert = nullptr;
268      std::vector<MeasureToolState::PerScreen::PrevMeasurement> prevMeasurements;
269      toolState.Read([&](const MeasureToolState& state) {
270          continuousCapture = state.global.continuousCapture;
271          drawFeetOnCross = state.global.drawFeetOnCross;
272          mode = state.global.mode;
273  
274          if (const auto it = state.perScreen.find(window); it != end(state.perScreen))
275          {
276              const auto& perScreen = it->second;
277  
278              prevMeasurements = perScreen.prevMeasurements;
279  
280              if (!perScreen.measuredEdges)
281              {
282                  return;
283              }
284  
285              measuredEdges = perScreen.measuredEdges;
286  
287              if (continuousCapture)
288                  return;
289  
290              if (perScreen.capturedScreenBitmap)
291              {
292                  backgroundBitmap = perScreen.capturedScreenBitmap;
293              }
294              else if (perScreen.capturedScreenTexture)
295              {
296                  backgroundTextureToConvert = perScreen.capturedScreenTexture;
297              }
298          }
299      });
300  
301      if (!measuredEdges && prevMeasurements.empty())
302      {
303          return;
304      }
305  
306      if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
307      {
308          backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.dxgiWindowState.rt, backgroundTextureToConvert);
309          if (backgroundBitmap)
310          {
311              toolState.Access([&](MeasureToolState& state) {
312                  state.perScreen[window].capturedScreenTexture = {};
313                  state.perScreen[window].capturedScreenBitmap = backgroundBitmap;
314              });
315          }
316      }
317  
318      if (continuousCapture || !backgroundBitmap)
319      {
320          d2dState.dxgiWindowState.rt->Clear();
321      }
322  
323      if (!continuousCapture && backgroundBitmap)
324      {
325          d2dState.dxgiWindowState.rt->DrawBitmap(backgroundBitmap.get());
326      }
327  
328      for (const auto& [prevCursorPos, prevMeasurement] : prevMeasurements)
329      {
330          DrawMeasurement(prevMeasurement, d2dState, drawFeetOnCross, mode, prevCursorPos, commonState, window);
331      }
332  
333      if (measuredEdges)
334      {
335          const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
336          DrawMeasurement(*measuredEdges, d2dState, drawFeetOnCross, mode, cursorPos, commonState, window);
337      }
338  }