#include "overlay.hpp" #include "app_messages.hpp" #include "color_picker_math.hpp" #include "gif_options_window.hpp" #include "gif_recorder.hpp" #include "image_io.hpp" #include "settings.hpp" #include "theme.hpp" #include "uploader.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Gdiplus; namespace { constexpr wchar_t kOverlayClass[] = L"NightShotOverlayWindow"; enum class Tool { Drag, Pen, Line, Arrow, Rectangle, Marker, Text }; enum class Action { Drag, Pen, Line, Arrow, Rectangle, Marker, Text, Color, Undo, Upload, Share, Search, Gif, Print, Copy, Save, Close }; enum class ColorField { None, Hex, Red, Green, Blue, Alpha, Hue, Saturation, Value }; constexpr std::size_t kStrokeChunkSegments = 32; struct Annotation { Tool tool = Tool::Pen; std::vector points; // Pen/marker segments are grouped spatially so repainting a tiny dirty // rectangle does not scan a complete multi-thousand-point stroke. std::vector strokeChunkBounds; RECT strokeBounds{}; bool hasStrokeBounds = false; Color color = Color(255, 59, 130, 246); float width = 3.0f; float textSize = 22.0f; // physical pixels; captured when text editing begins std::wstring text; }; RECT unionRawRects(RECT a, RECT b) { if (a.right <= a.left || a.bottom <= a.top) return b; if (b.right <= b.left || b.bottom <= b.top) return a; return RECT{std::min(a.left, b.left), std::min(a.top, b.top), std::max(a.right, b.right), std::max(a.bottom, b.bottom)}; } void appendStrokeChunkBounds(Annotation& annotation, PointF first, PointF second) { if (annotation.tool != Tool::Pen && annotation.tool != Tool::Marker) return; if (annotation.points.size() < 2) return; const int padding = static_cast(std::ceil(annotation.width * 0.5f)) + 3; RECT segment{ static_cast(std::floor(std::min(first.X, second.X))) - padding, static_cast(std::floor(std::min(first.Y, second.Y))) - padding, static_cast(std::ceil(std::max(first.X, second.X))) + padding + 1, static_cast(std::ceil(std::max(first.Y, second.Y))) + padding + 1}; const std::size_t segmentIndex = annotation.points.size() - 2; const std::size_t chunkIndex = segmentIndex / kStrokeChunkSegments; if (annotation.strokeChunkBounds.size() <= chunkIndex) { annotation.strokeChunkBounds.push_back(segment); } else { annotation.strokeChunkBounds[chunkIndex] = unionRawRects(annotation.strokeChunkBounds[chunkIndex], segment); } annotation.strokeBounds = annotation.hasStrokeBounds ? unionRawRects(annotation.strokeBounds, segment) : segment; annotation.hasStrokeBounds = true; } struct Button { RECT rect{}; Action action = Action::Pen; bool enabled = true; }; RECT normalizeRect(POINT a, POINT b) { return RECT{std::min(a.x, b.x), std::min(a.y, b.y), std::max(a.x, b.x), std::max(a.y, b.y)}; } bool pointInRect(const RECT& r, POINT p) { return p.x >= r.left && p.x < r.right && p.y >= r.top && p.y < r.bottom; } int rectWidth(const RECT& r) { return r.right - r.left; } int rectHeight(const RECT& r) { return r.bottom - r.top; } Color toGdiColor(COLORREF c, BYTE alpha = 255) { return Color(alpha, GetRValue(c), GetGValue(c), GetBValue(c)); } std::wstring actionName(Action action) { switch (action) { case Action::Drag: return L"Move selection (D)"; case Action::Pen: return L"Pencil (P)"; case Action::Line: return L"Line (L)"; case Action::Arrow: return L"Arrow (A)"; case Action::Rectangle: return L"Rectangle (R)"; case Action::Marker: return L"Marker (M)"; case Action::Text: return L"Text (T)"; case Action::Color: return L"Color picker (C)"; case Action::Undo: return L"Undo (Ctrl+Z)"; case Action::Upload: return L"Upload"; case Action::Share: return L"Share"; case Action::Search: return L"Find similar"; case Action::Gif: return L"Record GIF (Ctrl+PrintScreen)"; case Action::Print: return L"Print"; case Action::Copy: return L"Copy (Ctrl+C)"; case Action::Save: return L"Save (Ctrl+S)"; case Action::Close: return L"Close (Esc)"; } return L""; } bool isToolAction(Action action) { return action >= Action::Drag && action <= Action::Text; } Tool toolFromAction(Action action) { switch (action) { case Action::Drag: return Tool::Drag; case Action::Line: return Tool::Line; case Action::Arrow: return Tool::Arrow; case Action::Rectangle: return Tool::Rectangle; case Action::Marker: return Tool::Marker; case Action::Text: return Tool::Text; default: return Tool::Pen; } } void drawArrow(Graphics& g, const Pen& pen, const Color& color, PointF start, PointF end) { const float dx = end.X - start.X; const float dy = end.Y - start.Y; const float length = std::sqrt(dx * dx + dy * dy); if (length < 1.0f) return; const float ux = dx / length; const float uy = dy / length; const float nx = -uy; const float ny = ux; const float stroke = std::max(1.0f, pen.GetWidth()); const float headLength = std::clamp(stroke * 4.6f + 7.0f, 11.0f, 27.0f); const float halfHeadWidth = std::clamp(headLength * 0.48f, 5.0f, 14.0f); // Lightshot-style arrow: one clean rounded shaft with a filled triangular // head. The shaft runs under the head slightly so antialiasing never leaves // a visible seam between the two shapes. const float shaftInset = std::min(headLength * 0.58f, length * 0.45f); PointF shaftEnd(end.X - ux * shaftInset, end.Y - uy * shaftInset); g.DrawLine(&pen, start, shaftEnd); PointF baseCenter(end.X - ux * headLength, end.Y - uy * headLength); PointF triangle[3]{ end, PointF(baseCenter.X + nx * halfHeadWidth, baseCenter.Y + ny * halfHeadWidth), PointF(baseCenter.X - nx * halfHeadWidth, baseCenter.Y - ny * halfHeadWidth)}; SolidBrush headBrush(color); g.FillPolygon(&headBrush, triangle, 3); } void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offsetY) { if (a.points.empty()) return; Pen pen(a.color, a.width); pen.SetLineJoin(LineJoinRound); pen.SetStartCap(LineCapRound); pen.SetEndCap(LineCapRound); auto shifted = [&](PointF p) { return PointF(p.X + offsetX, p.Y + offsetY); }; switch (a.tool) { case Tool::Drag: break; case Tool::Pen: case Tool::Marker: { if (a.points.size() == 1) { PointF p = shifted(a.points.front()); g.DrawEllipse(&pen, p.X, p.Y, 1.0f, 1.0f); break; } std::vector pts; pts.reserve(a.points.size()); for (auto p : a.points) pts.push_back(shifted(p)); g.DrawLines(&pen, pts.data(), static_cast(pts.size())); break; } case Tool::Line: if (a.points.size() >= 2) g.DrawLine(&pen, shifted(a.points[0]), shifted(a.points[1])); break; case Tool::Arrow: if (a.points.size() >= 2) drawArrow(g, pen, a.color, shifted(a.points[0]), shifted(a.points[1])); break; case Tool::Rectangle: if (a.points.size() >= 2) { const PointF p1 = shifted(a.points[0]); const PointF p2 = shifted(a.points[1]); const REAL x = std::min(p1.X, p2.X), y = std::min(p1.Y, p2.Y); g.DrawRectangle(&pen, x, y, std::abs(p2.X - p1.X), std::abs(p2.Y - p1.Y)); } break; case Tool::Text: { if (a.text.empty()) break; const GraphicsState state = g.Save(); g.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit); g.SetTextContrast(0); g.SetPixelOffsetMode(PixelOffsetModeDefault); FontFamily family(L"Segoe UI"); const REAL size = std::max(14.0f, std::round(a.textSize)); Font font(&family, size, FontStyleRegular, UnitPixel); SolidBrush brush(a.color); PointF p = shifted(a.points[0]); p.X = std::round(p.X); p.Y = std::round(p.Y); StringFormat format; format.SetFormatFlags(StringFormatFlagsNoWrap | StringFormatFlagsMeasureTrailingSpaces); g.DrawString(a.text.c_str(), -1, &font, p, &format, &brush); g.Restore(state); break; } } } void drawAnnotationClipped(Graphics& g, const Annotation& annotation, const RECT& clip, float offsetX = 0.0f, float offsetY = 0.0f) { if (annotation.points.empty()) return; if (annotation.tool != Tool::Pen && annotation.tool != Tool::Marker) { float minX = annotation.points.front().X + offsetX; float minY = annotation.points.front().Y + offsetY; float maxX = minX; float maxY = minY; for (const PointF& point : annotation.points) { minX = std::min(minX, point.X + offsetX); minY = std::min(minY, point.Y + offsetY); maxX = std::max(maxX, point.X + offsetX); maxY = std::max(maxY, point.Y + offsetY); } float padding = annotation.width * 0.5f + 5.0f; if (annotation.tool == Tool::Arrow) padding += 30.0f; if (annotation.tool == Tool::Text) { const float size = std::max(14.0f, annotation.textSize); maxX += std::max(size * 2.0f, static_cast(annotation.text.size()) * size * 0.72f); maxY += size * 1.45f; } if (minX - padding >= clip.right || maxX + padding <= clip.left || minY - padding >= clip.bottom || maxY + padding <= clip.top) return; drawAnnotation(g, annotation, offsetX, offsetY); return; } Pen pen(annotation.color, annotation.width); pen.SetLineJoin(LineJoinRound); pen.SetStartCap(LineCapRound); pen.SetEndCap(LineCapRound); const float padding = annotation.width * 0.5f + 2.0f; auto shifted = [&](PointF point) { return PointF(point.X + offsetX, point.Y + offsetY); }; if (annotation.points.size() == 1) { const PointF point = shifted(annotation.points.front()); if (point.X + padding >= clip.left && point.X - padding <= clip.right && point.Y + padding >= clip.top && point.Y - padding <= clip.bottom) { g.DrawEllipse(&pen, point.X, point.Y, 1.0f, 1.0f); } return; } auto drawSegmentRange = [&](std::size_t firstPointIndex, std::size_t endPointIndex) { endPointIndex = std::min(endPointIndex, annotation.points.size()); for (std::size_t i = firstPointIndex; i < endPointIndex; ++i) { const PointF first = shifted(annotation.points[i - 1]); const PointF second = shifted(annotation.points[i]); const float left = std::min(first.X, second.X) - padding; const float top = std::min(first.Y, second.Y) - padding; const float right = std::max(first.X, second.X) + padding; const float bottom = std::max(first.Y, second.Y) + padding; if (left < clip.right && right > clip.left && top < clip.bottom && bottom > clip.top) { g.DrawLine(&pen, first, second); } } }; if (annotation.hasStrokeBounds && offsetX == 0.0f && offsetY == 0.0f && (annotation.strokeBounds.left >= clip.right || annotation.strokeBounds.right <= clip.left || annotation.strokeBounds.top >= clip.bottom || annotation.strokeBounds.bottom <= clip.top)) return; if (!annotation.strokeChunkBounds.empty() && offsetX == 0.0f && offsetY == 0.0f) { for (std::size_t chunk = 0; chunk < annotation.strokeChunkBounds.size(); ++chunk) { const RECT& bounds = annotation.strokeChunkBounds[chunk]; if (bounds.left >= clip.right || bounds.right <= clip.left || bounds.top >= clip.bottom || bounds.bottom <= clip.top) continue; const std::size_t firstPointIndex = chunk * kStrokeChunkSegments + 1; drawSegmentRange(firstPointIndex, firstPointIndex + kStrokeChunkSegments); } } else { drawSegmentRange(1, annotation.points.size()); } } void addRoundedRect(GraphicsPath& path, const RectF& rect, float radius) { const float d = radius * 2.0f; path.AddArc(rect.X, rect.Y, d, d, 180, 90); path.AddArc(rect.GetRight() - d, rect.Y, d, d, 270, 90); path.AddArc(rect.GetRight() - d, rect.GetBottom() - d, d, d, 0, 90); path.AddArc(rect.X, rect.GetBottom() - d, d, d, 90, 90); path.CloseFigure(); } void fillRoundedRect(Graphics& g, const RectF& rect, float radius, const Brush& brush) { GraphicsPath path; addRoundedRect(path, rect, radius); g.FillPath(&brush, &path); } void strokeRoundedRect(Graphics& g, const RectF& rect, float radius, const Pen& pen) { GraphicsPath path; addRoundedRect(path, rect, radius); g.DrawPath(&pen, &path); } void fillRoundedGradient(Graphics& g, const RectF& rect, float radius, const Color& start, const Color& end, LinearGradientMode mode = LinearGradientModeVertical) { GraphicsPath path; addRoundedRect(path, rect, radius); LinearGradientBrush brush(rect, start, end, mode); g.FillPath(&brush, &path); } enum class UiIcon { Move, PenTool, Line, ArrowUpRight, Square, Marker, Type, Colors, Undo, Upload, Share, Search, Film, Printer, Copy, Save, Close }; class IconCanvas { public: IconCanvas(Graphics& graphics, const RectF& bounds, const Color& color) : g_(graphics), scale_(std::min(bounds.Width, bounds.Height) / 24.0f), ox_(std::floor(bounds.X + (bounds.Width - 24.0f * scale_) * 0.5f) + 0.5f), oy_(std::floor(bounds.Y + (bounds.Height - 24.0f * scale_) * 0.5f) + 0.5f), pen_(color, std::max(1.30f, 1.78f * scale_)), brush_(color) { pen_.SetStartCap(LineCapRound); pen_.SetEndCap(LineCapRound); pen_.SetLineJoin(LineJoinRound); } PointF point(float x, float y) const { return PointF(ox_ + x * scale_, oy_ + y * scale_); } RectF rect(float x, float y, float w, float h) const { return RectF(ox_ + x * scale_, oy_ + y * scale_, w * scale_, h * scale_); } void line(float x1, float y1, float x2, float y2) { g_.DrawLine(&pen_, point(x1, y1), point(x2, y2)); } void polyline(std::initializer_list> points, bool closed = false) { std::vector mapped; mapped.reserve(points.size()); for (const auto& [x, y] : points) mapped.push_back(point(x, y)); if (mapped.size() < 2) return; GraphicsPath path; path.AddLines(mapped.data(), static_cast(mapped.size())); if (closed) path.CloseFigure(); g_.DrawPath(&pen_, &path); } void ellipse(float x, float y, float w, float h) { const RectF bounds = rect(x, y, w, h); g_.DrawEllipse(&pen_, bounds); } void fillEllipse(float x, float y, float w, float h) { const RectF bounds = rect(x, y, w, h); g_.FillEllipse(&brush_, bounds); } void roundedRect(float x, float y, float w, float h, float radius) { const RectF bounds = rect(x, y, w, h); strokeRoundedRect(g_, bounds, radius * scale_, pen_); } void arc(float x, float y, float w, float h, float start, float sweep) { const RectF bounds = rect(x, y, w, h); g_.DrawArc(&pen_, bounds, start, sweep); } Graphics& graphics() { return g_; } Pen& pen() { return pen_; } private: Graphics& g_; float scale_; float ox_; float oy_; Pen pen_; SolidBrush brush_; }; void drawUiIcon(Graphics& g, UiIcon icon, const RectF& bounds, const Color& color, COLORREF selectedColor) { (void)selectedColor; const GraphicsState state = g.Save(); g.SetSmoothingMode(SmoothingModeAntiAlias); g.SetPixelOffsetMode(PixelOffsetModeHalf); g.SetCompositingQuality(CompositingQualityHighQuality); IconCanvas c(g, bounds, color); switch (icon) { case UiIcon::Move: c.line(12.0f, 3.0f, 12.0f, 21.0f); c.line(3.0f, 12.0f, 21.0f, 12.0f); c.polyline({{8.0f, 7.0f}, {12.0f, 3.0f}, {16.0f, 7.0f}}); c.polyline({{8.0f, 17.0f}, {12.0f, 21.0f}, {16.0f, 17.0f}}); c.polyline({{7.0f, 8.0f}, {3.0f, 12.0f}, {7.0f, 16.0f}}); c.polyline({{17.0f, 8.0f}, {21.0f, 12.0f}, {17.0f, 16.0f}}); break; case UiIcon::PenTool: c.polyline({{3.5f, 20.5f}, {5.0f, 15.5f}, {15.8f, 4.7f}, {19.3f, 8.2f}, {8.5f, 19.0f}, {3.5f, 20.5f}}, true); c.line(5.0f, 15.5f, 8.5f, 19.0f); c.line(14.4f, 6.1f, 17.9f, 9.6f); break; case UiIcon::Line: c.line(5.0f, 19.0f, 19.0f, 5.0f); break; case UiIcon::ArrowUpRight: c.line(5.0f, 19.0f, 19.0f, 5.0f); c.line(10.5f, 5.0f, 19.0f, 5.0f); c.line(19.0f, 5.0f, 19.0f, 13.5f); break; case UiIcon::Square: c.roundedRect(4.0f, 4.0f, 16.0f, 16.0f, 2.2f); break; case UiIcon::Marker: c.polyline({{5.0f, 16.0f}, {14.2f, 6.8f}, {17.2f, 9.8f}, {8.0f, 19.0f}, {5.0f, 19.0f}, {5.0f, 16.0f}}, true); c.line(14.2f, 6.8f, 16.4f, 4.6f); c.line(16.4f, 4.6f, 19.4f, 7.6f); c.line(19.4f, 7.6f, 17.2f, 9.8f); c.line(4.0f, 21.0f, 12.0f, 21.0f); break; case UiIcon::Type: c.line(4.0f, 6.0f, 4.0f, 4.0f); c.line(4.0f, 4.0f, 20.0f, 4.0f); c.line(20.0f, 4.0f, 20.0f, 6.0f); c.line(12.0f, 4.0f, 12.0f, 20.0f); c.line(8.5f, 20.0f, 15.5f, 20.0f); break; case UiIcon::Colors: { // Compact eyedropper + sampled-color well, matching the picker itself. c.polyline({{14.2f, 4.0f}, {20.0f, 9.8f}, {17.2f, 12.6f}, {11.4f, 6.8f}, {14.2f, 4.0f}}, true); c.line(13.0f, 8.4f, 6.2f, 15.2f); c.line(6.2f, 15.2f, 5.0f, 19.0f); c.line(5.0f, 19.0f, 8.8f, 17.8f); c.line(8.8f, 17.8f, 15.6f, 11.0f); c.ellipse(3.5f, 16.0f, 5.0f, 5.0f); break; } case UiIcon::Undo: c.arc(4.0f, 5.0f, 16.0f, 14.0f, 205.0f, 285.0f); c.line(5.3f, 9.0f, 5.3f, 4.8f); c.line(5.3f, 9.0f, 9.5f, 9.0f); break; case UiIcon::Upload: c.line(12.0f, 15.0f, 12.0f, 4.0f); c.line(7.8f, 8.2f, 12.0f, 4.0f); c.line(16.2f, 8.2f, 12.0f, 4.0f); c.polyline({{5.0f, 14.0f}, {5.0f, 18.0f}, {6.0f, 19.0f}, {18.0f, 19.0f}, {19.0f, 18.0f}, {19.0f, 14.0f}}); break; case UiIcon::Share: c.ellipse(3.5f, 9.5f, 4.0f, 4.0f); c.ellipse(16.5f, 3.5f, 4.0f, 4.0f); c.ellipse(16.5f, 16.5f, 4.0f, 4.0f); c.line(7.1f, 10.7f, 16.8f, 6.3f); c.line(7.1f, 12.3f, 16.8f, 17.7f); break; case UiIcon::Search: c.ellipse(4.0f, 4.0f, 13.0f, 13.0f); c.line(15.0f, 15.0f, 21.0f, 21.0f); break; case UiIcon::Film: c.roundedRect(3.0f, 5.0f, 18.0f, 14.0f, 2.0f); c.line(7.0f, 5.0f, 7.0f, 19.0f); c.line(17.0f, 5.0f, 17.0f, 19.0f); c.line(3.0f, 9.0f, 7.0f, 9.0f); c.line(3.0f, 15.0f, 7.0f, 15.0f); c.line(17.0f, 9.0f, 21.0f, 9.0f); c.line(17.0f, 15.0f, 21.0f, 15.0f); c.polyline({{10.0f, 9.0f}, {15.0f, 12.0f}, {10.0f, 15.0f}}, true); break; case UiIcon::Printer: c.roundedRect(3.0f, 8.0f, 18.0f, 10.0f, 2.0f); c.polyline({{7.0f, 8.0f}, {7.0f, 3.0f}, {17.0f, 3.0f}, {17.0f, 8.0f}}); c.roundedRect(7.0f, 14.0f, 10.0f, 7.0f, 1.2f); c.fillEllipse(17.2f, 10.7f, 1.8f, 1.8f); break; case UiIcon::Copy: c.roundedRect(7.0f, 7.0f, 13.0f, 13.0f, 2.0f); c.polyline({{16.0f, 7.0f}, {16.0f, 5.0f}, {15.0f, 4.0f}, {5.0f, 4.0f}, {4.0f, 5.0f}, {4.0f, 15.0f}, {5.0f, 16.0f}, {7.0f, 16.0f}}); break; case UiIcon::Save: c.polyline({{4.0f, 3.0f}, {17.0f, 3.0f}, {21.0f, 7.0f}, {21.0f, 21.0f}, {3.0f, 21.0f}, {3.0f, 4.0f}, {4.0f, 3.0f}}, true); c.roundedRect(7.0f, 13.0f, 10.0f, 8.0f, 1.2f); c.polyline({{7.0f, 3.0f}, {7.0f, 8.0f}, {16.0f, 8.0f}, {16.0f, 3.0f}}); break; case UiIcon::Close: c.line(5.0f, 5.0f, 19.0f, 19.0f); c.line(19.0f, 5.0f, 5.0f, 19.0f); break; } g.Restore(state); } UiIcon iconForAction(Action action) { switch (action) { case Action::Drag: return UiIcon::Move; case Action::Pen: return UiIcon::PenTool; case Action::Line: return UiIcon::Line; case Action::Arrow: return UiIcon::ArrowUpRight; case Action::Rectangle: return UiIcon::Square; case Action::Marker: return UiIcon::Marker; case Action::Text: return UiIcon::Type; case Action::Color: return UiIcon::Colors; case Action::Undo: return UiIcon::Undo; case Action::Upload: return UiIcon::Upload; case Action::Share: return UiIcon::Share; case Action::Search: return UiIcon::Search; case Action::Gif: return UiIcon::Film; case Action::Print: return UiIcon::Printer; case Action::Copy: return UiIcon::Copy; case Action::Save: return UiIcon::Save; case Action::Close: return UiIcon::Close; } return UiIcon::Close; } } // namespace struct OverlayWindow::Impl { struct MonitorArea { HMONITOR handle = nullptr; RECT bounds{}; RECT workBounds{}; UINT dpi = 96; }; struct UiMetrics { // Roughly 20% tighter than v0.11. The glyph itself is slightly larger, // so the panel loses empty air without making the icon harder to read. int button = 28; int gap = 1; int radius = 6; int panelPadding = 2; int icon = 19; int handle = 7; int margin = 6; int paletteCell = 25; int paletteSwatch = 18; int pickerPadding = 9; int pickerFieldHeight = 27; int tooltipDelayMs = 420; }; static constexpr UINT_PTR kHoverTimer = 1; static constexpr int kPaintTileWidth = 1024; static constexpr int kPaintTileHeight = 512; Impl(ScreenCapture&& screen, HWND target, OverlayMode initialMode) : notificationTarget(target), capture(std::move(screen)), mode(initialMode) { enumerateMonitors(); const settings::AppSettings stored = settings::load(); penWidth = static_cast(stored.penWidth); lineWidth = static_cast(stored.lineWidth); arrowWidth = static_cast(stored.arrowWidth); rectangleWidth = static_cast(stored.rectangleWidth); markerWidth = static_cast(stored.markerWidth); savedPalette = stored.paletteArgb; savedPaletteOccupied = stored.paletteOccupied; color = color_picker_math::colorFromArgb(stored.selectedColorArgb); colorAlpha = color_picker_math::alphaFromArgb(stored.selectedColorArgb); pickerHsv = color_picker_math::rgbToHsv(GetRValue(color), GetGValue(color), GetBValue(color)); POINT cursor{}; if (GetCursorPos(&cursor)) { cursor.x -= capture.x; cursor.y -= capture.y; hintMonitorIndex = monitorIndexForPoint(cursor); cursorPoint = cursor; } } ~Impl() { releaseScratchBuffer(); } HWND hwnd = nullptr; HWND notificationTarget = nullptr; ScreenCapture capture; OverlayMode mode = OverlayMode::Screenshot; // selection is the visible part inside the virtual desktop. // logicalSelection keeps the full rectangle while it is temporarily clipped // by a desktop edge, so dragging it back restores the original dimensions. RECT selection{}; RECT logicalSelection{}; RECT originalSelection{}; RECT originalLogicalSelection{}; POINT dragStart{}; POINT lastPoint{}; bool hasSelection = false; bool selecting = false; bool drawing = false; bool draggingSelection = false; int resizeHandle = -1; Tool tool = Tool::Drag; COLORREF color = theme::kAccent; std::vector annotations; std::vector