Fix GIF and Overlay issues; Release NightShot v0.19
This commit is contained in:
1 parent
dfcbacfb0c
commit
ceb0b5552e
10 files changed
+528
-208
No files matched your search
+140
-125
@@ -141,19 +141,34 @@ Tool toolFromAction(Action action) {
|
||||
}
|
||||
}
|
||||
|
||||
void drawArrow(Graphics& g, const Pen& pen, PointF start, PointF end) {
|
||||
g.DrawLine(&pen, start, end);
|
||||
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 len = std::sqrt(dx * dx + dy * dy);
|
||||
if (len < 1.0f) return;
|
||||
const float angle = std::atan2(dy, dx);
|
||||
const float head = std::clamp(len * 0.22f, 9.0f, 18.0f);
|
||||
const float spread = 0.55f;
|
||||
PointF a(end.X - head * std::cos(angle - spread), end.Y - head * std::sin(angle - spread));
|
||||
PointF b(end.X - head * std::cos(angle + spread), end.Y - head * std::sin(angle + spread));
|
||||
g.DrawLine(&pen, end, a);
|
||||
g.DrawLine(&pen, end, b);
|
||||
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) {
|
||||
@@ -184,7 +199,7 @@ void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offse
|
||||
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, shifted(a.points[0]), shifted(a.points[1]));
|
||||
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) {
|
||||
@@ -231,7 +246,7 @@ void drawAnnotationClipped(Graphics& g, const Annotation& annotation, const RECT
|
||||
maxY = std::max(maxY, point.Y + offsetY);
|
||||
}
|
||||
float padding = annotation.width * 0.5f + 5.0f;
|
||||
if (annotation.tool == Tool::Arrow) padding += 19.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<float>(annotation.text.size()) * size * 0.72f);
|
||||
@@ -562,8 +577,8 @@ struct OverlayWindow::Impl {
|
||||
markerWidth = static_cast<float>(stored.markerWidth);
|
||||
savedPalette = stored.paletteArgb;
|
||||
savedPaletteOccupied = stored.paletteOccupied;
|
||||
color = color_picker_math::colorFromArgb(savedPalette[0]);
|
||||
colorAlpha = color_picker_math::alphaFromArgb(savedPalette[0]);
|
||||
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{};
|
||||
@@ -600,8 +615,6 @@ struct OverlayWindow::Impl {
|
||||
Tool tool = Tool::Drag;
|
||||
COLORREF color = theme::kAccent;
|
||||
std::vector<Annotation> annotations;
|
||||
LONG appliedSelectionMoveDx = 0;
|
||||
LONG appliedSelectionMoveDy = 0;
|
||||
std::vector<Button> verticalButtons;
|
||||
std::vector<Button> horizontalButtons;
|
||||
std::array<RECT, 8> handleRects{};
|
||||
@@ -797,6 +810,7 @@ struct OverlayWindow::Impl {
|
||||
value.arrowWidth = static_cast<int>(std::lround(arrowWidth));
|
||||
value.rectangleWidth = static_cast<int>(std::lround(rectangleWidth));
|
||||
value.markerWidth = static_cast<int>(std::lround(markerWidth));
|
||||
value.selectedColorArgb = currentArgb();
|
||||
value.paletteArgb = savedPalette;
|
||||
value.paletteOccupied = savedPaletteOccupied;
|
||||
const bool saved = settings::save(value);
|
||||
@@ -837,6 +851,7 @@ struct OverlayWindow::Impl {
|
||||
color = nextColor;
|
||||
colorAlpha = nextAlpha;
|
||||
if (syncHsv) syncPickerFromRgb();
|
||||
persistEditorPreferences();
|
||||
}
|
||||
|
||||
std::uint32_t currentArgb() const {
|
||||
@@ -929,22 +944,22 @@ struct OverlayWindow::Impl {
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Alpha:
|
||||
colorAlpha = static_cast<BYTE>(std::clamp(value, 0, 255));
|
||||
setCurrentColor(color, static_cast<BYTE>(std::clamp(value, 0, 255)), false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Hue:
|
||||
pickerHsv.h = static_cast<float>((value % 360 + 360) % 360);
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Saturation:
|
||||
pickerHsv.s = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f;
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Value:
|
||||
pickerHsv.v = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f;
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
default: break;
|
||||
@@ -1051,19 +1066,19 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
bool sampleCapturedColor(POINT point) {
|
||||
if (!capture.pixels || point.x < 0 || point.y < 0 ||
|
||||
if (point.x < 0 || point.y < 0 ||
|
||||
point.x >= capture.width || point.y >= capture.height) return false;
|
||||
const auto* row = static_cast<const std::uint8_t*>(capture.pixels) +
|
||||
static_cast<std::size_t>(point.y) * static_cast<std::size_t>(capture.stride);
|
||||
const auto* pixel = row + static_cast<std::size_t>(point.x) * 4u;
|
||||
setCurrentColor(RGB(pixel[2], pixel[1], pixel[0]), 255);
|
||||
COLORREF sampled{};
|
||||
if (!capture.samplePixel(point.x, point.y, sampled)) return false;
|
||||
setCurrentColor(sampled, 255);
|
||||
return true;
|
||||
}
|
||||
|
||||
RECT brushPreviewBounds(POINT point) const {
|
||||
if (!hasSelection || !isDrawingTool(tool) || !pointInRect(selection, point)) return RECT{0, 0, 0, 0};
|
||||
const int diameter = std::max(3, static_cast<int>(std::ceil(currentStrokeWidth())));
|
||||
const int radius = diameter / 2 + 4;
|
||||
if (!hasSelection || !isDrawingTool(tool) || point.x < 0 || point.y < 0 ||
|
||||
point.x >= capture.width || point.y >= capture.height) return RECT{0, 0, 0, 0};
|
||||
const int diameter = std::max(1, static_cast<int>(std::ceil(currentStrokeWidth())));
|
||||
const int radius = (diameter + 1) / 2 + 2;
|
||||
return clampToClient(RECT{point.x - radius, point.y - radius,
|
||||
point.x + radius + 1, point.y + radius + 1});
|
||||
}
|
||||
@@ -1082,7 +1097,7 @@ struct OverlayWindow::Impl {
|
||||
const float step = tool == Tool::Marker ? 2.0f : 1.0f;
|
||||
const float maximum = tool == Tool::Marker ? 96.0f : 64.0f;
|
||||
const RECT previous = brushPreviewRect;
|
||||
*width = std::clamp(*width + (wheelDelta > 0 ? step : -step), 1.0f, maximum);
|
||||
*width = std::clamp(*width + (wheelDelta > 0 ? -step : step), 1.0f, maximum);
|
||||
persistEditorPreferences();
|
||||
brushPreviewRect = brushPreviewBounds(cursorPoint);
|
||||
invalidateRectPadded(previous, 3);
|
||||
@@ -1333,7 +1348,7 @@ struct OverlayWindow::Impl {
|
||||
maxY = std::max(maxY, point.Y);
|
||||
}
|
||||
int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 5;
|
||||
if (annotation.tool == Tool::Arrow) padding += 19;
|
||||
if (annotation.tool == Tool::Arrow) padding += 30;
|
||||
if (annotation.tool == Tool::Text) {
|
||||
const float size = std::max(14.0f, annotation.textSize);
|
||||
maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f);
|
||||
@@ -1726,6 +1741,7 @@ struct OverlayWindow::Impl {
|
||||
pickerDraggingSv = false;
|
||||
pickerDraggingHue = false;
|
||||
pickerDraggingAlpha = false;
|
||||
persistEditorPreferences();
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
@@ -1806,16 +1822,24 @@ struct OverlayWindow::Impl {
|
||||
|
||||
std::unique_ptr<Bitmap> renderSelectionBitmap() {
|
||||
if (!hasSelection || rectWidth(selection) <= 0 || rectHeight(selection) <= 0) return {};
|
||||
auto output = std::make_unique<Bitmap>(rectWidth(selection), rectHeight(selection), PixelFormat32bppARGB);
|
||||
// Screenshots are opaque. RGB avoids the zero-alpha semantics of GDI
|
||||
// writes into an ARGB GDI+ bitmap while the sparse monitor capture is
|
||||
// copied into this temporary crop.
|
||||
auto output = std::make_unique<Bitmap>(rectWidth(selection), rectHeight(selection), PixelFormat32bppRGB);
|
||||
Graphics g(output.get());
|
||||
g.Clear(Color(255, 255, 255, 255));
|
||||
g.Clear(Color(255, 0, 0, 0));
|
||||
HDC outputDc = g.GetHDC();
|
||||
if (!outputDc) return {};
|
||||
const bool copied = capture.blitTo(outputDc, 0, 0, selection);
|
||||
g.ReleaseHDC(outputDc);
|
||||
if (!copied) return {};
|
||||
|
||||
g.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
g.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit);
|
||||
Bitmap source(capture.bitmap, nullptr);
|
||||
Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
|
||||
g.DrawImage(&source, destination, selection.left, selection.top,
|
||||
rectWidth(selection), rectHeight(selection), UnitPixel);
|
||||
const Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
|
||||
g.SetClip(destination);
|
||||
// Annotations live in virtual-desktop coordinates. Selection is only a
|
||||
// crop rectangle, so moving/resizing it never mutates drawing data.
|
||||
for (const auto& annotation : annotations)
|
||||
drawAnnotation(g, annotation, static_cast<float>(-selection.left), static_cast<float>(-selection.top));
|
||||
return output;
|
||||
@@ -2418,44 +2442,35 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
void drawBrushPreview(Graphics& g, RECT dirty) {
|
||||
if (!hasSelection || selecting || drawing || draggingSelection || resizeHandle >= 0 ||
|
||||
if (!hasSelection || selecting || draggingSelection || resizeHandle >= 0 ||
|
||||
paletteOpen || eyedropperActive || !isDrawingTool(tool) ||
|
||||
!rectsIntersect(brushPreviewRect, dirty) ||
|
||||
!pointInRect(selection, cursorPoint)) return;
|
||||
const float width = std::max(1.0f, currentStrokeWidth());
|
||||
const float visualDiameter = std::max(4.0f, width);
|
||||
const float x = static_cast<float>(cursorPoint.x) - visualDiameter * 0.5f;
|
||||
const float y = static_cast<float>(cursorPoint.y) - visualDiameter * 0.5f;
|
||||
!rectsIntersect(brushPreviewRect, dirty)) return;
|
||||
|
||||
const float diameter = std::max(1.0f, currentStrokeWidth());
|
||||
const float x = static_cast<float>(cursorPoint.x) - diameter * 0.5f;
|
||||
const float y = static_cast<float>(cursorPoint.y) - diameter * 0.5f;
|
||||
const GraphicsState state = g.Save();
|
||||
g.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(PixelOffsetModeHalf);
|
||||
SolidBrush fill(Color(tool == Tool::Marker ? std::min<int>(colorAlpha, 96) : std::max<int>(70, colorAlpha),
|
||||
GetRValue(color), GetGValue(color), GetBValue(color)));
|
||||
g.FillEllipse(&fill, x, y, visualDiameter, visualDiameter);
|
||||
Pen shadow(Color(205, 0, 0, 0), 3.0f);
|
||||
Pen border(Color(245, 255, 255, 255), 1.0f);
|
||||
g.DrawEllipse(&shadow, x, y, visualDiameter, visualDiameter);
|
||||
g.DrawEllipse(&border, x, y, visualDiameter, visualDiameter);
|
||||
if (width < 4.0f) {
|
||||
SolidBrush center(toGdiColor(color, colorAlpha));
|
||||
const float centerSize = std::max(1.0f, width);
|
||||
g.FillEllipse(¢er,
|
||||
static_cast<float>(cursorPoint.x) - centerSize * 0.5f,
|
||||
static_cast<float>(cursorPoint.y) - centerSize * 0.5f,
|
||||
centerSize, centerSize);
|
||||
}
|
||||
|
||||
// Preview is deliberately hollow: it represents the exact physical
|
||||
// brush diameter without covering the pixels the user is trying to aim
|
||||
// at. Keep the border at one physical pixel on every monitor.
|
||||
Pen border(toGdiColor(color, static_cast<BYTE>(std::max<int>(colorAlpha, 160))), 1.0f);
|
||||
g.DrawEllipse(&border, x, y, diameter, diameter);
|
||||
g.Restore(state);
|
||||
}
|
||||
|
||||
void drawScene(Graphics& g, RECT dirty) {
|
||||
// Drawing is a virtual-desktop layer, independent from the crop. It is
|
||||
// intentionally visible even outside the selected rectangle and keeps
|
||||
// absolute coordinates while the selection moves or resizes.
|
||||
for (const auto& annotation : annotations) {
|
||||
drawAnnotationClipped(g, annotation, dirty);
|
||||
}
|
||||
|
||||
if (hasSelection || selecting) {
|
||||
const RECT r = selection;
|
||||
const GraphicsState annotationState = g.Save();
|
||||
g.SetClip(Rect(r.left, r.top, std::max(0, rectWidth(r)), std::max(0, rectHeight(r))), CombineModeIntersect);
|
||||
for (const auto& annotation : annotations) {
|
||||
drawAnnotationClipped(g, annotation, dirty);
|
||||
}
|
||||
g.Restore(annotationState);
|
||||
|
||||
const int borderPadding = std::max(10, ui.handle + 6);
|
||||
const std::array<RECT, 4> borderBands{{
|
||||
@@ -2473,7 +2488,7 @@ struct OverlayWindow::Impl {
|
||||
static_cast<REAL>(std::max(0, rectWidth(r) - 1)),
|
||||
static_cast<REAL>(std::max(0, rectHeight(r) - 1)));
|
||||
|
||||
if (hasSelection && !selecting) {
|
||||
if (hasSelection && !selecting && tool == Tool::Drag) {
|
||||
SolidBrush handleFill(toGdiColor(theme::kAccentHover));
|
||||
Pen handleBorder(Color(210, 255, 255, 255), 1.0f);
|
||||
for (const RECT& handle : handleRects) {
|
||||
@@ -2597,7 +2612,7 @@ struct OverlayWindow::Impl {
|
||||
const int height = rectHeight(tile);
|
||||
if (width <= 0 || height <= 0 || !ensureScratchBuffer(target, width, height)) return;
|
||||
|
||||
BitBlt(scratchDc, 0, 0, width, height, capture.memoryDc, tile.left, tile.top, SRCCOPY);
|
||||
static_cast<void>(capture.blitTo(scratchDc, 0, 0, tile));
|
||||
dimScratchRect(tile);
|
||||
|
||||
Graphics graphics(scratchDc);
|
||||
@@ -2666,11 +2681,11 @@ struct OverlayWindow::Impl {
|
||||
const int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 4;
|
||||
if ((annotation.tool == Tool::Line || annotation.tool == Tool::Arrow) && annotation.points.size() >= 2) {
|
||||
invalidateSegmentTiles(annotation.points[0], annotation.points[1],
|
||||
padding + (annotation.tool == Tool::Arrow ? 10 : 0));
|
||||
padding + (annotation.tool == Tool::Arrow ? 30 : 0));
|
||||
if (annotation.tool == Tool::Arrow) {
|
||||
const PointF end = annotation.points[1];
|
||||
invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 24, static_cast<LONG>(end.Y) - 24,
|
||||
static_cast<LONG>(end.X) + 25, static_cast<LONG>(end.Y) + 25}, 1);
|
||||
invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 36, static_cast<LONG>(end.Y) - 36,
|
||||
static_cast<LONG>(end.X) + 37, static_cast<LONG>(end.Y) + 37}, 1);
|
||||
}
|
||||
} else if (annotation.tool == Tool::Rectangle && annotation.points.size() >= 2) {
|
||||
invalidateRectangleOutline(annotation.points[0], annotation.points[1], padding);
|
||||
@@ -2680,7 +2695,9 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
void beginAnnotation(POINT p) {
|
||||
if (tool == Tool::Drag || !pointInRect(selection, p)) return;
|
||||
if (!hasSelection || tool == Tool::Drag) return;
|
||||
p.x = std::clamp(p.x, 0L, static_cast<LONG>(capture.width - 1));
|
||||
p.y = std::clamp(p.y, 0L, static_cast<LONG>(capture.height - 1));
|
||||
if (tool == Tool::Text) {
|
||||
beginTextEdit(p);
|
||||
return;
|
||||
@@ -2701,8 +2718,8 @@ struct OverlayWindow::Impl {
|
||||
|
||||
void updateAnnotation(POINT p) {
|
||||
if (!drawing || annotations.empty()) return;
|
||||
p.x = std::clamp(p.x, selection.left, selection.right);
|
||||
p.y = std::clamp(p.y, selection.top, selection.bottom);
|
||||
p.x = std::clamp(p.x, 0L, static_cast<LONG>(capture.width - 1));
|
||||
p.y = std::clamp(p.y, 0L, static_cast<LONG>(capture.height - 1));
|
||||
Annotation& annotation = annotations.back();
|
||||
|
||||
if (annotation.tool == Tool::Pen || annotation.tool == Tool::Marker) {
|
||||
@@ -2746,7 +2763,7 @@ struct OverlayWindow::Impl {
|
||||
if (!textEditing) return;
|
||||
const int monitorIndex = monitorIndexForPoint(textAnchor);
|
||||
const UiMetrics metrics = metricsForMonitor(monitorIndex);
|
||||
RECT safe = intersectRects(monitors[monitorIndex].workBounds, selection);
|
||||
RECT safe = monitors[monitorIndex].workBounds;
|
||||
RECT insetSafe = safe;
|
||||
InflateRect(&insetSafe, -metrics.margin, -metrics.margin);
|
||||
if (rectWidth(insetSafe) >= 40 && rectHeight(insetSafe) >= 24) safe = insetSafe;
|
||||
@@ -2779,7 +2796,7 @@ struct OverlayWindow::Impl {
|
||||
commitTextEdit();
|
||||
const int monitorIndex = monitorIndexForPoint(p);
|
||||
const UiMetrics metrics = metricsForMonitor(monitorIndex);
|
||||
RECT available = intersectRects(monitors[monitorIndex].workBounds, selection);
|
||||
RECT available = monitors[monitorIndex].workBounds;
|
||||
InflateRect(&available, -metrics.margin, -metrics.margin);
|
||||
if (rectWidth(available) < 50 || rectHeight(available) < 32) return;
|
||||
|
||||
@@ -2900,9 +2917,6 @@ struct OverlayWindow::Impl {
|
||||
const RECT previousUi = uiBounds();
|
||||
const RECT previousBadge = dimensionBadgeRect;
|
||||
const RECT previousPreview = brushPreviewRect;
|
||||
if (!annotations.empty()) {
|
||||
for (const auto& annotation : annotations) invalidateAnnotationShape(annotation);
|
||||
}
|
||||
|
||||
const LONG requestedDx = p.x - dragStart.x;
|
||||
const LONG requestedDy = p.y - dragStart.y;
|
||||
@@ -2910,29 +2924,11 @@ struct OverlayWindow::Impl {
|
||||
OffsetRect(&nextLogical, requestedDx, requestedDy);
|
||||
nextLogical = keepLogicalSelectionReachable(nextLogical);
|
||||
|
||||
const LONG actualDx = nextLogical.left - originalLogicalSelection.left;
|
||||
const LONG actualDy = nextLogical.top - originalLogicalSelection.top;
|
||||
logicalSelection = nextLogical;
|
||||
selection = visibleSelectionFromLogical(logicalSelection);
|
||||
|
||||
const LONG deltaDx = actualDx - appliedSelectionMoveDx;
|
||||
const LONG deltaDy = actualDy - appliedSelectionMoveDy;
|
||||
if (deltaDx != 0 || deltaDy != 0) {
|
||||
for (auto& annotation : annotations) {
|
||||
for (auto& point : annotation.points) {
|
||||
point.X += static_cast<REAL>(deltaDx);
|
||||
point.Y += static_cast<REAL>(deltaDy);
|
||||
}
|
||||
for (RECT& bounds : annotation.strokeChunkBounds) OffsetRect(&bounds, deltaDx, deltaDy);
|
||||
if (annotation.hasStrokeBounds) OffsetRect(&annotation.strokeBounds, deltaDx, deltaDy);
|
||||
}
|
||||
appliedSelectionMoveDx = actualDx;
|
||||
appliedSelectionMoveDy = actualDy;
|
||||
}
|
||||
|
||||
if (!annotations.empty()) {
|
||||
for (const auto& annotation : annotations) invalidateAnnotationShape(annotation);
|
||||
}
|
||||
// Annotations deliberately stay in absolute virtual-desktop
|
||||
// coordinates. Selection is only the crop rectangle.
|
||||
updateLayout();
|
||||
brushPreviewRect = brushPreviewBounds(cursorPoint);
|
||||
invalidateSelectionTransition(previousSelection, previousUi, previousBadge);
|
||||
@@ -2955,26 +2951,41 @@ struct OverlayWindow::Impl {
|
||||
if (pointInRect(pickerEyedropperRect, p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
|
||||
}
|
||||
if (!hasSelection) { SetCursor(LoadCursorW(nullptr, IDC_CROSS)); return; }
|
||||
const int handle = hitHandle(p);
|
||||
switch (handle) {
|
||||
case 0: case 4: SetCursor(LoadCursorW(nullptr, IDC_SIZENWSE)); return;
|
||||
case 2: case 6: SetCursor(LoadCursorW(nullptr, IDC_SIZENESW)); return;
|
||||
case 1: case 5: SetCursor(LoadCursorW(nullptr, IDC_SIZENS)); return;
|
||||
case 3: case 7: SetCursor(LoadCursorW(nullptr, IDC_SIZEWE)); return;
|
||||
default: break;
|
||||
}
|
||||
if (hitButton(p) >= 0 || hitPalette(p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
|
||||
if (hitToolbarPanel(p)) { SetCursor(LoadCursorW(nullptr, IDC_ARROW)); return; }
|
||||
if (hitSelectionBorder(p)) { SetCursor(LoadCursorW(nullptr, IDC_SIZEALL)); return; }
|
||||
if (pointInRect(selection, p)) {
|
||||
if (tool == Tool::Drag) SetCursor(LoadCursorW(nullptr, IDC_SIZEALL));
|
||||
else if (tool == Tool::Text) SetCursor(LoadCursorW(nullptr, IDC_IBEAM));
|
||||
else if (isDrawingTool(tool)) SetCursor(nullptr);
|
||||
else SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
|
||||
// The selection is interactive only in Drag mode. Drawing tools never
|
||||
// accidentally resize/move the crop when a stroke starts near its edge.
|
||||
if (tool == Tool::Drag) {
|
||||
const int handle = hitHandle(p);
|
||||
switch (handle) {
|
||||
case 0: case 4: SetCursor(LoadCursorW(nullptr, IDC_SIZENWSE)); return;
|
||||
case 2: case 6: SetCursor(LoadCursorW(nullptr, IDC_SIZENESW)); return;
|
||||
case 1: case 5: SetCursor(LoadCursorW(nullptr, IDC_SIZENS)); return;
|
||||
case 3: case 7: SetCursor(LoadCursorW(nullptr, IDC_SIZEWE)); return;
|
||||
default: break;
|
||||
}
|
||||
if (hitSelectionBorder(p) || pointInRect(selection, p)) {
|
||||
SetCursor(LoadCursorW(nullptr, IDC_SIZEALL));
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_ARROW));
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_ARROW));
|
||||
|
||||
if (tool == Tool::Text) {
|
||||
SetCursor(LoadCursorW(nullptr, IDC_IBEAM));
|
||||
return;
|
||||
}
|
||||
if (isDrawingTool(tool)) {
|
||||
// The hollow brush preview becomes the cursor over the entire
|
||||
// virtual desktop, not just over the crop.
|
||||
SetCursor(nullptr);
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
OverlayWindow::OverlayWindow(ScreenCapture&& capture, HWND notificationTarget, OverlayMode mode)
|
||||
@@ -3087,6 +3098,16 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
// Panel padding/gaps are UI, not transparent drawing holes.
|
||||
if (s.hitToolbarPanel(p)) return 0;
|
||||
|
||||
if (s.tool != Tool::Drag) {
|
||||
// Drawing/text tools own the entire virtual desktop. The
|
||||
// selection border and resize handles are intentionally
|
||||
// inert until the user explicitly switches back to Drag.
|
||||
s.beginAnnotation(p);
|
||||
s.updateBrushPreview(p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int handle = s.hitHandle(p);
|
||||
if (handle >= 0) {
|
||||
// A manual resize commits the currently visible rectangle as
|
||||
@@ -3099,26 +3120,21 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
SetCapture(s.hwnd);
|
||||
return 0;
|
||||
}
|
||||
if (s.hitSelectionBorder(p) ||
|
||||
(s.tool == Tool::Drag && pointInRect(s.selection, p))) {
|
||||
if (s.hitSelectionBorder(p) || pointInRect(s.selection, p)) {
|
||||
s.draggingSelection = true;
|
||||
s.originalSelection = s.selection;
|
||||
s.originalLogicalSelection = s.logicalSelection;
|
||||
s.appliedSelectionMoveDx = 0;
|
||||
s.appliedSelectionMoveDy = 0;
|
||||
s.dragStart = p;
|
||||
SetCapture(s.hwnd);
|
||||
return 0;
|
||||
}
|
||||
if (pointInRect(s.selection, p)) {
|
||||
s.beginAnnotation(p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Drag mode + click outside the crop starts a new crop. Global
|
||||
// annotations are deliberately preserved.
|
||||
const RECT abandonedSelection = s.selection;
|
||||
const RECT abandonedUi = s.uiBounds();
|
||||
const RECT abandonedBadge = s.dimensionBadgeRect;
|
||||
const RECT abandonedPreview = s.brushPreviewRect;
|
||||
s.annotations.clear();
|
||||
s.hasSelection = false;
|
||||
s.selecting = true;
|
||||
s.dragStart = p;
|
||||
@@ -3171,6 +3187,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
s.moveSelectionTo(p);
|
||||
} else if (s.drawing) {
|
||||
s.updateAnnotation(p);
|
||||
s.updateBrushPreview(p);
|
||||
} else {
|
||||
if (!s.hasSelection) {
|
||||
const int nextMonitor = s.monitorIndexForPoint(p);
|
||||
@@ -3220,8 +3237,6 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
s.invalidateUi();
|
||||
} else if (s.draggingSelection) {
|
||||
s.draggingSelection = false;
|
||||
s.appliedSelectionMoveDx = 0;
|
||||
s.appliedSelectionMoveDy = 0;
|
||||
ReleaseCapture();
|
||||
s.updateLayout();
|
||||
s.brushPreviewRect = s.brushPreviewBounds(p);
|
||||
@@ -3272,7 +3287,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
ScreenToClient(s.hwnd, &point);
|
||||
s.cursorPoint = point;
|
||||
if (!s.paletteOpen && !s.eyedropperActive && s.hasSelection &&
|
||||
pointInRect(s.selection, point) && Impl::isDrawingTool(s.tool)) {
|
||||
Impl::isDrawingTool(s.tool)) {
|
||||
s.adjustCurrentStrokeWidth(GET_WHEEL_DELTA_WPARAM(wParam));
|
||||
s.updateCursor(point);
|
||||
return 0;
|
||||
|
||||
Reference in new issue
Block a user