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 }