Explorar el Código

Add corner resize handles for canvas text and images.

Lets users stretch or shrink placed content by dragging selection handles instead of relying only on toolbar font size controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hace 1 mes
padre
commit
3726628c39
Se han modificado 1 ficheros con 264 adiciones y 1 borrados
  1. 264 1
      smart_printer/DrawPrintView.swift

+ 264 - 1
smart_printer/DrawPrintView.swift

@@ -338,7 +338,24 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
 
 
 // MARK: - Canvas View
 // MARK: - Canvas View
 
 
+private enum CanvasResizeHandle {
+    case topLeft
+    case topRight
+    case bottomLeft
+    case bottomRight
+}
+
+private enum CanvasResizeTarget {
+    case text(UUID)
+    case image(UUID)
+}
+
 final class BlankCanvasView: NSView, AppearanceRefreshable {
 final class BlankCanvasView: NSView, AppearanceRefreshable {
+    private static let resizeHandleSize: CGFloat = 8
+    private static let resizeHandleHitPadding: CGFloat = 6
+    private static let minImageDimension: CGFloat = 24
+    private static let minTextFontSize: CGFloat = 8
+    private static let maxTextFontSize: CGFloat = 120
     override var isOpaque: Bool { true }
     override var isOpaque: Bool { true }
     override var mouseDownCanMoveWindow: Bool { false }
     override var mouseDownCanMoveWindow: Bool { false }
 
 
@@ -379,6 +396,13 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
     private var dragStartOrigin = NSPoint.zero
     private var dragStartOrigin = NSPoint.zero
     private var textDragSnapshotPushed = false
     private var textDragSnapshotPushed = false
     private var imageDragSnapshotPushed = false
     private var imageDragSnapshotPushed = false
+    private var resizingTextID: UUID?
+    private var resizingImageID: UUID?
+    private var activeResizeHandle: CanvasResizeHandle?
+    private var resizeStartPoint = NSPoint.zero
+    private var resizeStartRect = NSRect.zero
+    private var resizeStartFontSize: CGFloat = 18
+    private var resizeFixedCorner = NSPoint.zero
 
 
     private let undoButton = CanvasHistoryButton(symbolName: "arrow.uturn.backward", accessibilityLabel: "Undo")
     private let undoButton = CanvasHistoryButton(symbolName: "arrow.uturn.backward", accessibilityLabel: "Undo")
     private let redoButton = CanvasHistoryButton(symbolName: "arrow.uturn.forward", accessibilityLabel: "Redo")
     private let redoButton = CanvasHistoryButton(symbolName: "arrow.uturn.forward", accessibilityLabel: "Redo")
@@ -458,10 +482,16 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
                 return hit
                 return hit
             }
             }
         }
         }
+        if resizeHandle(at: point) != nil {
+            return self
+        }
         if (activeTool == .text || activeTool == .pointer),
         if (activeTool == .text || activeTool == .pointer),
            editingTextID == nil, textIndex(at: point) != nil {
            editingTextID == nil, textIndex(at: point) != nil {
             return self
             return self
         }
         }
+        if (activeTool == .image || activeTool == .pointer), imageIndex(at: point) != nil {
+            return self
+        }
         return super.hitTest(point)
         return super.hitTest(point)
     }
     }
 
 
@@ -506,6 +536,12 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
            item.id != editingTextID {
            item.id != editingTextID {
             drawTextSelection(for: item)
             drawTextSelection(for: item)
         }
         }
+
+        if (activeTool == .image || activeTool == .pointer),
+           let selectedID = selectedImageID,
+           let item = imageItems.first(where: { $0.id == selectedID }) {
+            drawImageSelection(for: item)
+        }
     }
     }
 
 
     private func drawTextItem(_ item: CanvasTextData) {
     private func drawTextItem(_ item: CanvasTextData) {
@@ -521,13 +557,38 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
     }
     }
 
 
     private func drawTextSelection(for item: CanvasTextData) {
     private func drawTextSelection(for item: CanvasTextData) {
-        let rect = textBounds(for: item).insetBy(dx: -4, dy: -4)
+        let rect = selectionRect(for: textBounds(for: item))
+        drawSelectionChrome(in: rect)
+    }
+
+    private func drawImageSelection(for item: CanvasImageData) {
+        let rect = selectionRect(for: NSRect(origin: item.origin, size: item.size))
+        drawSelectionChrome(in: rect)
+    }
+
+    private func selectionRect(for contentRect: NSRect) -> NSRect {
+        contentRect.insetBy(dx: -4, dy: -4)
+    }
+
+    private func drawSelectionChrome(in rect: NSRect) {
         NSColor.clear.setFill()
         NSColor.clear.setFill()
         let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4)
         let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4)
         path.setLineDash([4, 3], count: 2, phase: 0)
         path.setLineDash([4, 3], count: 2, phase: 0)
         path.lineWidth = 1.5
         path.lineWidth = 1.5
         NSColor(calibratedWhite: 0.55, alpha: 1).setStroke()
         NSColor(calibratedWhite: 0.55, alpha: 1).setStroke()
         path.stroke()
         path.stroke()
+        drawResizeHandles(in: rect)
+    }
+
+    private func drawResizeHandles(in rect: NSRect) {
+        for handleRect in resizeHandleRects(for: rect).values {
+            NSColor.white.setFill()
+            NSBezierPath(roundedRect: handleRect, xRadius: 2, yRadius: 2).fill()
+            NSColor(calibratedWhite: 0.55, alpha: 1).setStroke()
+            let border = NSBezierPath(roundedRect: handleRect, xRadius: 2, yRadius: 2)
+            border.lineWidth = 1
+            border.stroke()
+        }
     }
     }
 
 
     private func drawStroke(_ stroke: CanvasStroke, in context: CGContext?) {
     private func drawStroke(_ stroke: CanvasStroke, in context: CGContext?) {
@@ -552,6 +613,11 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
             return
             return
         }
         }
 
 
+        if let resize = resizeHandle(at: point) {
+            beginResize(resize, at: point)
+            return
+        }
+
         switch activeTool {
         switch activeTool {
         case .pointer:
         case .pointer:
             if let index = textIndex(at: point) {
             if let index = textIndex(at: point) {
@@ -618,6 +684,21 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
     override func mouseDragged(with event: NSEvent) {
     override func mouseDragged(with event: NSEvent) {
         let point = convert(event.locationInWindow, from: nil)
         let point = convert(event.locationInWindow, from: nil)
 
 
+        if let handle = activeResizeHandle {
+            if let id = resizingTextID,
+               let index = textItems.firstIndex(where: { $0.id == id }) {
+                applyTextResize(at: point, handle: handle, index: index)
+                needsDisplay = true
+                return
+            }
+            if let id = resizingImageID,
+               let index = imageItems.firstIndex(where: { $0.id == id }) {
+                applyImageResize(at: point, handle: handle, index: index)
+                needsDisplay = true
+                return
+            }
+        }
+
         if (activeTool == .text || activeTool == .pointer),
         if (activeTool == .text || activeTool == .pointer),
            let id = draggingTextID,
            let id = draggingTextID,
            let index = textItems.firstIndex(where: { $0.id == id }) {
            let index = textItems.firstIndex(where: { $0.id == id }) {
@@ -661,6 +742,14 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
     }
     }
 
 
     override func mouseUp(with event: NSEvent) {
     override func mouseUp(with event: NSEvent) {
+        if resizingTextID != nil || resizingImageID != nil {
+            resizingTextID = nil
+            resizingImageID = nil
+            activeResizeHandle = nil
+            notifyUndoState()
+            return
+        }
+
         if (activeTool == .text || activeTool == .pointer), draggingTextID != nil {
         if (activeTool == .text || activeTool == .pointer), draggingTextID != nil {
             draggingTextID = nil
             draggingTextID = nil
             textDragSnapshotPushed = false
             textDragSnapshotPushed = false
@@ -837,7 +926,12 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
         imageItems = snapshot.images
         imageItems = snapshot.images
         endEditingText()
         endEditingText()
         selectedTextID = nil
         selectedTextID = nil
+        selectedImageID = nil
         draggingTextID = nil
         draggingTextID = nil
+        draggingImageID = nil
+        resizingTextID = nil
+        resizingImageID = nil
+        activeResizeHandle = nil
         bringUndoButtonsToFront()
         bringUndoButtonsToFront()
         needsDisplay = true
         needsDisplay = true
         notifyUndoState()
         notifyUndoState()
@@ -928,6 +1022,175 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
         }
         }
         return nil
         return nil
     }
     }
+
+    private func resizeHandleRects(for rect: NSRect) -> [CanvasResizeHandle: NSRect] {
+        let half = Self.resizeHandleSize / 2
+        return [
+            .topLeft: NSRect(x: rect.minX - half, y: rect.maxY - half, width: Self.resizeHandleSize, height: Self.resizeHandleSize),
+            .topRight: NSRect(x: rect.maxX - half, y: rect.maxY - half, width: Self.resizeHandleSize, height: Self.resizeHandleSize),
+            .bottomLeft: NSRect(x: rect.minX - half, y: rect.minY - half, width: Self.resizeHandleSize, height: Self.resizeHandleSize),
+            .bottomRight: NSRect(x: rect.maxX - half, y: rect.minY - half, width: Self.resizeHandleSize, height: Self.resizeHandleSize),
+        ]
+    }
+
+    private func resizeHandle(at point: NSPoint) -> (CanvasResizeTarget, CanvasResizeHandle)? {
+        if (activeTool == .image || activeTool == .pointer),
+           let id = selectedImageID,
+           let item = imageItems.first(where: { $0.id == id }) {
+            let rect = selectionRect(for: NSRect(origin: item.origin, size: item.size))
+            if let handle = hitResizeHandle(at: point, in: rect) {
+                return (.image(id), handle)
+            }
+        }
+
+        if (activeTool == .text || activeTool == .pointer),
+           let id = selectedTextID,
+           id != editingTextID,
+           let item = textItems.first(where: { $0.id == id }) {
+            let rect = selectionRect(for: textBounds(for: item))
+            if let handle = hitResizeHandle(at: point, in: rect) {
+                return (.text(id), handle)
+            }
+        }
+
+        return nil
+    }
+
+    private func hitResizeHandle(at point: NSPoint, in rect: NSRect) -> CanvasResizeHandle? {
+        for (handle, handleRect) in resizeHandleRects(for: rect) {
+            let hitRect = handleRect.insetBy(dx: -Self.resizeHandleHitPadding, dy: -Self.resizeHandleHitPadding)
+            if hitRect.contains(point) {
+                return handle
+            }
+        }
+        return nil
+    }
+
+    private func fixedCorner(for rect: NSRect, handle: CanvasResizeHandle) -> NSPoint {
+        switch handle {
+        case .topLeft:
+            return NSPoint(x: rect.maxX, y: rect.minY)
+        case .topRight:
+            return NSPoint(x: rect.minX, y: rect.minY)
+        case .bottomLeft:
+            return NSPoint(x: rect.maxX, y: rect.maxY)
+        case .bottomRight:
+            return NSPoint(x: rect.minX, y: rect.maxY)
+        }
+    }
+
+    private func beginResize(_ target: (CanvasResizeTarget, CanvasResizeHandle), at point: NSPoint) {
+        let (resizeTarget, handle) = target
+        pushSnapshot()
+        activeResizeHandle = handle
+        resizeStartPoint = point
+
+        switch resizeTarget {
+        case .text(let id):
+            guard let item = textItems.first(where: { $0.id == id }) else { return }
+            resizingTextID = id
+            resizeStartRect = textBounds(for: item)
+            resizeStartFontSize = item.fontSize
+            resizeFixedCorner = fixedCorner(for: resizeStartRect, handle: handle)
+        case .image(let id):
+            guard let item = imageItems.first(where: { $0.id == id }) else { return }
+            resizingImageID = id
+            resizeStartRect = NSRect(origin: item.origin, size: item.size)
+            resizeFixedCorner = fixedCorner(for: resizeStartRect, handle: handle)
+        }
+    }
+
+    private func applyTextResize(at point: NSPoint, handle: CanvasResizeHandle, index: Int) {
+        let startDistance = hypot(resizeStartPoint.x - resizeFixedCorner.x, resizeStartPoint.y - resizeFixedCorner.y)
+        let newDistance = hypot(point.x - resizeFixedCorner.x, point.y - resizeFixedCorner.y)
+        guard startDistance > 1 else { return }
+
+        let scale = max(0.2, min(5, newDistance / startDistance))
+        let newFontSize = min(Self.maxTextFontSize, max(Self.minTextFontSize, resizeStartFontSize * scale))
+        textItems[index].fontSize = newFontSize
+
+        let bounds = textBounds(for: textItems[index])
+        var origin = NSPoint.zero
+        switch handle {
+        case .topLeft:
+            origin = NSPoint(x: resizeFixedCorner.x - bounds.width, y: resizeFixedCorner.y)
+        case .topRight:
+            origin = NSPoint(x: resizeFixedCorner.x, y: resizeFixedCorner.y)
+        case .bottomLeft:
+            origin = NSPoint(x: resizeFixedCorner.x - bounds.width, y: resizeFixedCorner.y - bounds.height)
+        case .bottomRight:
+            origin = NSPoint(x: resizeFixedCorner.x, y: resizeFixedCorner.y - bounds.height)
+        }
+        textItems[index].origin = clampedOrigin(origin, size: bounds.size)
+
+        if let field = textField(for: textItems[index].id) {
+            field.font = AppTheme.regularFont(size: newFontSize)
+            field.frame = textBounds(for: textItems[index])
+        }
+    }
+
+    private func applyImageResize(at point: NSPoint, handle: CanvasResizeHandle, index: Int) {
+        let dx = point.x - resizeStartPoint.x
+        let dy = point.y - resizeStartPoint.y
+        var rect = resizeStartRect
+
+        switch handle {
+        case .topLeft:
+            rect.origin.x += dx
+            rect.size.width -= dx
+            rect.size.height -= dy
+            rect.origin.y += dy
+        case .topRight:
+            rect.size.width += dx
+            rect.size.height -= dy
+            rect.origin.y += dy
+        case .bottomLeft:
+            rect.origin.x += dx
+            rect.size.width -= dx
+            rect.size.height += dy
+        case .bottomRight:
+            rect.size.width += dx
+            rect.size.height += dy
+        }
+
+        rect.size.width = max(Self.minImageDimension, rect.size.width)
+        rect.size.height = max(Self.minImageDimension, rect.size.height)
+
+        switch handle {
+        case .topLeft:
+            rect.origin.x = resizeFixedCorner.x - rect.size.width
+            rect.origin.y = resizeFixedCorner.y
+        case .topRight:
+            rect.origin.x = resizeFixedCorner.x
+            rect.origin.y = resizeFixedCorner.y
+        case .bottomLeft:
+            rect.origin.x = resizeFixedCorner.x - rect.size.width
+            rect.origin.y = resizeFixedCorner.y - rect.size.height
+        case .bottomRight:
+            rect.origin.x = resizeFixedCorner.x
+            rect.origin.y = resizeFixedCorner.y - rect.size.height
+        }
+
+        let clamped = clampedRect(rect)
+        imageItems[index].origin = clamped.origin
+        imageItems[index].size = clamped.size
+    }
+
+    private func clampedOrigin(_ origin: NSPoint, size: NSSize) -> NSPoint {
+        var clamped = origin
+        clamped.x = max(0, min(clamped.x, bounds.width - size.width))
+        clamped.y = max(0, min(clamped.y, bounds.height - size.height))
+        return clamped
+    }
+
+    private func clampedRect(_ rect: NSRect) -> NSRect {
+        var clamped = rect
+        clamped.size.width = max(Self.minImageDimension, min(clamped.size.width, bounds.width))
+        clamped.size.height = max(Self.minImageDimension, min(clamped.size.height, bounds.height))
+        clamped.origin.x = max(0, min(clamped.origin.x, bounds.width - clamped.size.width))
+        clamped.origin.y = max(0, min(clamped.origin.y, bounds.height - clamped.size.height))
+        return clamped
+    }
 }
 }
 
 
 // MARK: - Tools Panel
 // MARK: - Tools Panel