Ver código fonte

Fix scan preview layout and show live results in the scanner overlay.

Normalize images for display so previews scale to their layout frame instead of expanding to bitmap size, and update the scanner UI with inline save/print actions and sidebar styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mês atrás
pai
commit
e7564fd2f5

+ 28 - 6
smart_printer/ScanFileView.swift

@@ -233,6 +233,29 @@ enum ScanFileService {
     }
     }
 }
 }
 
 
+// MARK: - Image Display
+
+/// Image view that scales to its layout frame instead of expanding to the bitmap's intrinsic size.
+final class ScanPreviewImageView: NSImageView {
+    override var intrinsicContentSize: NSSize {
+        NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric)
+    }
+}
+
+extension NSImage {
+    func normalizedForDisplay() -> NSImage {
+        guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return self }
+        return Self.normalizedForDisplay(cgImage: cgImage)
+    }
+
+    static func normalizedForDisplay(cgImage: CGImage) -> NSImage {
+        let size = NSSize(width: cgImage.width, height: cgImage.height)
+        let image = NSImage(size: size)
+        image.addRepresentation(NSBitmapImageRep(cgImage: cgImage))
+        return image
+    }
+}
+
 // MARK: - Document Processing
 // MARK: - Document Processing
 
 
 enum ScanDocumentProcessor {
 enum ScanDocumentProcessor {
@@ -262,10 +285,7 @@ enum ScanDocumentProcessor {
     }
     }
 
 
     private static func imageFromCGImage(_ cgImage: CGImage) -> NSImage {
     private static func imageFromCGImage(_ cgImage: CGImage) -> NSImage {
-        let rep = NSBitmapImageRep(cgImage: cgImage)
-        let image = NSImage()
-        image.addRepresentation(rep)
-        return image
+        NSImage.normalizedForDisplay(cgImage: cgImage)
     }
     }
 
 
     private static func perspectiveCorrectedImage(from cgImage: CGImage, observation: VNRectangleObservation) -> NSImage? {
     private static func perspectiveCorrectedImage(from cgImage: CGImage, observation: VNRectangleObservation) -> NSImage? {
@@ -454,7 +474,7 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
     private let titleLabel = NSTextField(labelWithString: "Scan File")
     private let titleLabel = NSTextField(labelWithString: "Scan File")
     private let counterLabel = NSTextField(labelWithString: "")
     private let counterLabel = NSTextField(labelWithString: "")
     private let previewContainer = NSView()
     private let previewContainer = NSView()
-    private let imageView = NSImageView()
+    private let imageView = ScanPreviewImageView()
     private let addPageButton = ScanFileSecondaryButton(title: "Add Page", symbolName: "plus")
     private let addPageButton = ScanFileSecondaryButton(title: "Add Page", symbolName: "plus")
     private let saveButton = ScanFileSecondaryButton(title: "Save", symbolName: "square.and.arrow.down")
     private let saveButton = ScanFileSecondaryButton(title: "Save", symbolName: "square.and.arrow.down")
     private let printButton = ScanFilePrintButton()
     private let printButton = ScanFilePrintButton()
@@ -626,7 +646,9 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
     private func showPage(at index: Int) {
     private func showPage(at index: Int) {
         guard pages.indices.contains(index) else { return }
         guard pages.indices.contains(index) else { return }
         currentIndex = index
         currentIndex = index
-        imageView.image = pages[index]
+        imageView.image = pages[index].normalizedForDisplay()
+        imageView.invalidateIntrinsicContentSize()
+        previewContainer.layoutSubtreeIfNeeded()
         if pages.count > 1 {
         if pages.count > 1 {
             counterLabel.stringValue = "Page \(index + 1) of \(pages.count)"
             counterLabel.stringValue = "Page \(index + 1) of \(pages.count)"
         } else {
         } else {

+ 139 - 43
smart_printer/ScannerOverlayView.swift

@@ -24,15 +24,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
     private let sharedHeader = ScannerSectionHeader(title: "SHARED")
     private let sharedHeader = ScannerSectionHeader(title: "SHARED")
     private let sharedList = NSStackView()
     private let sharedList = NSStackView()
     private let previewContainer = NSView()
     private let previewContainer = NSView()
-    private let previewImageView = NSImageView()
+    private let previewImageArea = NSView()
+    private let previewImageView = ScanPreviewImageView()
     private let placeholderView = ScannerPlaceholderView()
     private let placeholderView = ScannerPlaceholderView()
+    private let previewActionRow = NSStackView()
+    private var previewImageAreaBottomToContainer: NSLayoutConstraint!
+    private var previewImageAreaBottomToActions: NSLayoutConstraint!
+    private var previewActionRowHeight: NSLayoutConstraint!
     private let statusLabel = NSTextField(labelWithString: "Select a scanner to begin")
     private let statusLabel = NSTextField(labelWithString: "Select a scanner to begin")
     private let paperSizePopup = NSPopUpButton()
     private let paperSizePopup = NSPopUpButton()
     private let scanButton = ScannerPrimaryButton(title: "Scan")
     private let scanButton = ScannerPrimaryButton(title: "Scan")
     private let progressIndicator = NSProgressIndicator()
     private let progressIndicator = NSProgressIndicator()
 
 
     private let resultPreviewContainer = NSView()
     private let resultPreviewContainer = NSView()
-    private let resultImageView = NSImageView()
+    private let resultImageView = ScanPreviewImageView()
     private let savePDFButton = ScannerSecondaryButton(title: "Save as PDF", symbolName: "square.and.arrow.down")
     private let savePDFButton = ScannerSecondaryButton(title: "Save as PDF", symbolName: "square.and.arrow.down")
     private let printButton = ScannerSecondaryButton(title: "Print", symbolName: "printer.fill")
     private let printButton = ScannerSecondaryButton(title: "Print", symbolName: "printer.fill")
 
 
@@ -79,12 +84,15 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
 
 
     func showResult(_ image: NSImage) {
     func showResult(_ image: NSImage) {
         hasResult = true
         hasResult = true
-        scannedImage = image
-        resultImageView.image = image
+        displayScannedImage(image)
         scanPhaseView.isHidden = true
         scanPhaseView.isHidden = true
         resultPhaseView.isHidden = false
         resultPhaseView.isHidden = false
     }
     }
 
 
+    func updateScanPreview(_ image: NSImage) {
+        displayScannedImage(image)
+    }
+
     func present(in parent: NSView) {
     func present(in parent: NSView) {
         guard superview == nil else { return }
         guard superview == nil else { return }
         parent.addSubview(self)
         parent.addSubview(self)
@@ -120,7 +128,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
     func refreshAppearance() {
     func refreshAppearance() {
         layer?.backgroundColor = AppTheme.background.cgColor
         layer?.backgroundColor = AppTheme.background.cgColor
         titleLabel.textColor = AppTheme.textPrimary
         titleLabel.textColor = AppTheme.textPrimary
-        sidebarView.layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
+        sidebarView.layer?.backgroundColor = AppTheme.cardBackground.cgColor
+        sidebarView.layer?.borderColor = AppTheme.paywallBorder.cgColor
         previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
         previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
         previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
         previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
         resultPreviewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
         resultPreviewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
@@ -170,6 +179,9 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
         sidebarView.wantsLayer = true
         sidebarView.wantsLayer = true
+        sidebarView.layer?.cornerRadius = AppTheme.contentPanelCornerRadius
+        sidebarView.layer?.borderWidth = 1.5
+        sidebarView.layer?.masksToBounds = true
         sidebarView.translatesAutoresizingMaskIntoConstraints = false
         sidebarView.translatesAutoresizingMaskIntoConstraints = false
 
 
         localList.orientation = .vertical
         localList.orientation = .vertical
@@ -192,6 +204,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
         previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
 
 
+        previewImageArea.translatesAutoresizingMaskIntoConstraints = false
+
         previewImageView.imageScaling = .scaleProportionallyUpOrDown
         previewImageView.imageScaling = .scaleProportionallyUpOrDown
         previewImageView.imageAlignment = .alignCenter
         previewImageView.imageAlignment = .alignCenter
         previewImageView.translatesAutoresizingMaskIntoConstraints = false
         previewImageView.translatesAutoresizingMaskIntoConstraints = false
@@ -230,15 +244,29 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         progressIndicator.isDisplayedWhenStopped = false
         progressIndicator.isDisplayedWhenStopped = false
         progressIndicator.translatesAutoresizingMaskIntoConstraints = false
         progressIndicator.translatesAutoresizingMaskIntoConstraints = false
 
 
+        savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
+        printButton.onClick = { [weak self] in self?.printScan() }
+        savePDFButton.translatesAutoresizingMaskIntoConstraints = false
+        printButton.translatesAutoresizingMaskIntoConstraints = false
+
+        previewActionRow.orientation = .horizontal
+        previewActionRow.spacing = 12
+        previewActionRow.distribution = .fillEqually
+        previewActionRow.translatesAutoresizingMaskIntoConstraints = false
+        previewActionRow.isHidden = true
+        previewActionRow.addArrangedSubview(savePDFButton)
+        previewActionRow.addArrangedSubview(printButton)
+
         let sidebarStack = NSStackView()
         let sidebarStack = NSStackView()
         sidebarStack.orientation = .vertical
         sidebarStack.orientation = .vertical
         sidebarStack.alignment = .leading
         sidebarStack.alignment = .leading
-        sidebarStack.spacing = 6
+        sidebarStack.spacing = 8
         sidebarStack.translatesAutoresizingMaskIntoConstraints = false
         sidebarStack.translatesAutoresizingMaskIntoConstraints = false
         sidebarStack.addArrangedSubview(localHeader)
         sidebarStack.addArrangedSubview(localHeader)
         sidebarStack.addArrangedSubview(localList)
         sidebarStack.addArrangedSubview(localList)
         sidebarStack.addArrangedSubview(sharedHeader)
         sidebarStack.addArrangedSubview(sharedHeader)
         sidebarStack.addArrangedSubview(sharedList)
         sidebarStack.addArrangedSubview(sharedList)
+        sidebarStack.setCustomSpacing(18, after: localList)
 
 
         let sidebarScroll = NSScrollView()
         let sidebarScroll = NSScrollView()
         sidebarScroll.hasVerticalScroller = true
         sidebarScroll.hasVerticalScroller = true
@@ -254,8 +282,10 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         sidebarScroll.documentView = sidebarDocument
         sidebarScroll.documentView = sidebarDocument
         sidebarView.addSubview(sidebarScroll)
         sidebarView.addSubview(sidebarScroll)
 
 
-        previewContainer.addSubview(placeholderView)
-        previewContainer.addSubview(previewImageView)
+        previewImageArea.addSubview(previewImageView)
+        previewImageArea.addSubview(placeholderView)
+        previewContainer.addSubview(previewImageArea)
+        previewContainer.addSubview(previewActionRow)
 
 
         scanPhaseView.addSubview(backButton)
         scanPhaseView.addSubview(backButton)
         scanPhaseView.addSubview(titleLabel)
         scanPhaseView.addSubview(titleLabel)
@@ -275,20 +305,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
             titleLabel.centerXAnchor.constraint(equalTo: scanPhaseView.centerXAnchor),
             titleLabel.centerXAnchor.constraint(equalTo: scanPhaseView.centerXAnchor),
             titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
             titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
 
 
-            sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor),
+            sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
             sidebarView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 16),
             sidebarView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 16),
-            sidebarView.bottomAnchor.constraint(equalTo: scanPhaseView.bottomAnchor),
-            sidebarView.widthAnchor.constraint(equalToConstant: 168),
-
-            sidebarScroll.leadingAnchor.constraint(equalTo: sidebarView.leadingAnchor),
-            sidebarScroll.trailingAnchor.constraint(equalTo: sidebarView.trailingAnchor),
-            sidebarScroll.topAnchor.constraint(equalTo: sidebarView.topAnchor),
-            sidebarScroll.bottomAnchor.constraint(equalTo: sidebarView.bottomAnchor),
-
-            sidebarStack.leadingAnchor.constraint(equalTo: sidebarDocument.leadingAnchor, constant: 8),
-            sidebarStack.trailingAnchor.constraint(equalTo: sidebarDocument.trailingAnchor, constant: -8),
-            sidebarStack.topAnchor.constraint(equalTo: sidebarDocument.topAnchor, constant: 8),
-            sidebarStack.bottomAnchor.constraint(equalTo: sidebarDocument.bottomAnchor, constant: -8),
+            sidebarView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor),
+            sidebarView.widthAnchor.constraint(equalToConstant: 180),
+
+            sidebarScroll.leadingAnchor.constraint(equalTo: sidebarView.leadingAnchor, constant: 4),
+            sidebarScroll.trailingAnchor.constraint(equalTo: sidebarView.trailingAnchor, constant: -4),
+            sidebarScroll.topAnchor.constraint(equalTo: sidebarView.topAnchor, constant: 6),
+            sidebarScroll.bottomAnchor.constraint(equalTo: sidebarView.bottomAnchor, constant: -4),
+
+            sidebarStack.leadingAnchor.constraint(equalTo: sidebarDocument.leadingAnchor, constant: 14),
+            sidebarStack.trailingAnchor.constraint(equalTo: sidebarDocument.trailingAnchor, constant: -14),
+            sidebarStack.topAnchor.constraint(equalTo: sidebarDocument.topAnchor, constant: 22),
+            sidebarStack.bottomAnchor.constraint(equalTo: sidebarDocument.bottomAnchor, constant: -14),
             sidebarDocument.widthAnchor.constraint(equalTo: sidebarScroll.widthAnchor),
             sidebarDocument.widthAnchor.constraint(equalTo: sidebarScroll.widthAnchor),
             localHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
             localHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
             sharedHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
             sharedHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
@@ -300,15 +330,25 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
             previewContainer.topAnchor.constraint(equalTo: sidebarView.topAnchor),
             previewContainer.topAnchor.constraint(equalTo: sidebarView.topAnchor),
             previewContainer.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -12),
             previewContainer.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -12),
 
 
-            placeholderView.centerXAnchor.constraint(equalTo: previewContainer.centerXAnchor),
-            placeholderView.centerYAnchor.constraint(equalTo: previewContainer.centerYAnchor),
-            placeholderView.widthAnchor.constraint(equalTo: previewContainer.widthAnchor, multiplier: 0.55),
-            placeholderView.heightAnchor.constraint(equalTo: previewContainer.heightAnchor, multiplier: 0.7),
+            previewImageArea.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 16),
+            previewImageArea.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -16),
+            previewImageArea.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 16),
 
 
-            previewImageView.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 12),
-            previewImageView.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -12),
-            previewImageView.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 12),
-            previewImageView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -12),
+            previewActionRow.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 16),
+            previewActionRow.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -16),
+            previewActionRow.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -16),
+            savePDFButton.heightAnchor.constraint(equalToConstant: 40),
+            printButton.heightAnchor.constraint(equalToConstant: 40),
+
+            placeholderView.centerXAnchor.constraint(equalTo: previewImageArea.centerXAnchor),
+            placeholderView.centerYAnchor.constraint(equalTo: previewImageArea.centerYAnchor),
+            placeholderView.widthAnchor.constraint(equalTo: previewImageArea.widthAnchor, multiplier: 0.42),
+            placeholderView.heightAnchor.constraint(equalTo: placeholderView.widthAnchor),
+
+            previewImageView.leadingAnchor.constraint(equalTo: previewImageArea.leadingAnchor, constant: 20),
+            previewImageView.trailingAnchor.constraint(equalTo: previewImageArea.trailingAnchor, constant: -20),
+            previewImageView.topAnchor.constraint(equalTo: previewImageArea.topAnchor, constant: 20),
+            previewImageView.bottomAnchor.constraint(equalTo: previewImageArea.bottomAnchor, constant: -20),
 
 
             statusLabel.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor),
             statusLabel.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor),
             statusLabel.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor),
             statusLabel.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor),
@@ -326,6 +366,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
             scanButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
             scanButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
             scanButton.heightAnchor.constraint(equalToConstant: 32),
             scanButton.heightAnchor.constraint(equalToConstant: 32),
         ])
         ])
+
+        previewImageAreaBottomToContainer = previewImageArea.bottomAnchor.constraint(
+            equalTo: previewContainer.bottomAnchor,
+            constant: -16
+        )
+        previewImageAreaBottomToActions = previewImageArea.bottomAnchor.constraint(
+            equalTo: previewActionRow.topAnchor,
+            constant: -12
+        )
+        previewActionRowHeight = previewActionRow.heightAnchor.constraint(equalToConstant: 0)
+
+        previewImageAreaBottomToContainer.isActive = true
+        previewImageAreaBottomToActions.isActive = false
+        previewActionRowHeight.isActive = true
     }
     }
 
 
     private func setupResultPhase() {
     private func setupResultPhase() {
@@ -359,10 +413,12 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
         resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
 
 
-        savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
-        printButton.onClick = { [weak self] in self?.printScan() }
+        let resultSavePDFButton = ScannerSecondaryButton(title: "Save as PDF", symbolName: "square.and.arrow.down")
+        let resultPrintButton = ScannerSecondaryButton(title: "Print", symbolName: "printer.fill")
+        resultSavePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
+        resultPrintButton.onClick = { [weak self] in self?.printScan() }
 
 
-        let actionRow = NSStackView(views: [savePDFButton, printButton])
+        let actionRow = NSStackView(views: [resultSavePDFButton, resultPrintButton])
         actionRow.orientation = .horizontal
         actionRow.orientation = .horizontal
         actionRow.spacing = 12
         actionRow.spacing = 12
         actionRow.distribution = .fillEqually
         actionRow.distribution = .fillEqually
@@ -398,8 +454,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
             actionRow.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
             actionRow.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
             actionRow.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -AppTheme.contentPanelInset),
             actionRow.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -AppTheme.contentPanelInset),
             actionRow.bottomAnchor.constraint(equalTo: resultPhaseView.bottomAnchor, constant: -28),
             actionRow.bottomAnchor.constraint(equalTo: resultPhaseView.bottomAnchor, constant: -28),
-            savePDFButton.heightAnchor.constraint(equalToConstant: 40),
-            printButton.heightAnchor.constraint(equalToConstant: 40),
+            resultSavePDFButton.heightAnchor.constraint(equalToConstant: 40),
+            resultPrintButton.heightAnchor.constraint(equalToConstant: 40),
         ])
         ])
     }
     }
 
 
@@ -453,17 +509,19 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         let label = NSTextField(labelWithString: text)
         let label = NSTextField(labelWithString: text)
         label.font = AppTheme.regularFont(size: 12)
         label.font = AppTheme.regularFont(size: 12)
         label.textColor = AppTheme.textSecondary
         label.textColor = AppTheme.textSecondary
-        label.lineBreakMode = .byTruncatingTail
+        label.alignment = .center
+        label.lineBreakMode = .byWordWrapping
+        label.maximumNumberOfLines = 2
         label.translatesAutoresizingMaskIntoConstraints = false
         label.translatesAutoresizingMaskIntoConstraints = false
         let container = NSView()
         let container = NSView()
         container.translatesAutoresizingMaskIntoConstraints = false
         container.translatesAutoresizingMaskIntoConstraints = false
         container.addSubview(label)
         container.addSubview(label)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
-            label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 4),
-            label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4),
-            label.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
-            label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
-            container.heightAnchor.constraint(equalToConstant: 28),
+            label.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            label.topAnchor.constraint(equalTo: container.topAnchor, constant: 6),
+            label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -6),
+            container.heightAnchor.constraint(greaterThanOrEqualToConstant: 32),
         ])
         ])
         return container
         return container
     }
     }
@@ -507,6 +565,7 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         guard scanner.selectedDevice != nil else { return }
         guard scanner.selectedDevice != nil else { return }
         previewImageView.isHidden = true
         previewImageView.isHidden = true
         placeholderView.isHidden = false
         placeholderView.isHidden = false
+        setPreviewActionsVisible(false)
 
 
         if case .ready = scanner.state {
         if case .ready = scanner.state {
             scanner.scan()
             scanner.scan()
@@ -515,10 +574,34 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         }
         }
     }
     }
 
 
+    private func setPreviewActionsVisible(_ visible: Bool) {
+        previewActionRow.isHidden = !visible
+        previewActionRowHeight.constant = visible ? 40 : 0
+        previewImageAreaBottomToContainer.isActive = !visible
+        previewImageAreaBottomToActions.isActive = visible
+        previewContainer.layoutSubtreeIfNeeded()
+    }
+
+    private func displayScannedImage(_ image: NSImage) {
+        let displayImage = image.normalizedForDisplay()
+        hasResult = true
+        scannedImage = displayImage
+        previewImageView.image = displayImage
+        previewImageView.isHidden = false
+        placeholderView.isHidden = true
+        setPreviewActionsVisible(true)
+        resultImageView.image = displayImage
+        previewImageView.invalidateIntrinsicContentSize()
+        resultImageView.invalidateIntrinsicContentSize()
+        previewContainer.layoutSubtreeIfNeeded()
+        resultPreviewContainer.layoutSubtreeIfNeeded()
+    }
+
     private func handleScanResult(_ result: Result<NSImage, Error>) {
     private func handleScanResult(_ result: Result<NSImage, Error>) {
         progressIndicator.stopAnimation(nil)
         progressIndicator.stopAnimation(nil)
         switch result {
         switch result {
         case .success(let image):
         case .success(let image):
+            displayScannedImage(image)
             onScanComplete?(image)
             onScanComplete?(image)
         case .failure(let error):
         case .failure(let error):
             if case ScanError.cancelled = error { return }
             if case ScanError.cancelled = error { return }
@@ -708,8 +791,8 @@ private final class ScannerPlaceholderView: NSView, AppearanceRefreshable {
     }
     }
 
 
     override func draw(_ dirtyRect: NSRect) {
     override func draw(_ dirtyRect: NSRect) {
-        let rect = bounds.insetBy(dx: 20, dy: 20)
-        let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4)
+        let rect = bounds.insetBy(dx: 1, dy: 1)
+        let path = NSBezierPath(roundedRect: rect, xRadius: 16, yRadius: 16)
         path.setLineDash([6, 4], count: 2, phase: 0)
         path.setLineDash([6, 4], count: 2, phase: 0)
         AppTheme.border.setStroke()
         AppTheme.border.setStroke()
         path.lineWidth = 1.5
         path.lineWidth = 1.5
@@ -777,6 +860,8 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
 
 
     private let titleLabel = NSTextField(labelWithString: "")
     private let titleLabel = NSTextField(labelWithString: "")
     private let iconView = NSImageView()
     private let iconView = NSImageView()
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
 
 
     init(title: String, symbolName: String) {
     init(title: String, symbolName: String) {
         super.init(frame: .zero)
         super.init(frame: .zero)
@@ -804,6 +889,10 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
             titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
             titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
             titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),
             titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),
         ])
         ])
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.setHovered(hovering)
+        }
         refreshAppearance()
         refreshAppearance()
     }
     }
 
 
@@ -811,12 +900,19 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
     func refreshAppearance() {
     func refreshAppearance() {
-        layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
+        layer?.backgroundColor = (isHovered
+            ? AppTheme.elevatedBackground.highlight(withLevel: 0.05) ?? AppTheme.elevatedBackground
+            : AppTheme.elevatedBackground).cgColor
         layer?.borderColor = AppTheme.paywallBorder.cgColor
         layer?.borderColor = AppTheme.paywallBorder.cgColor
         titleLabel.textColor = AppTheme.textPrimary
         titleLabel.textColor = AppTheme.textPrimary
         iconView.contentTintColor = AppTheme.textPrimary
         iconView.contentTintColor = AppTheme.textPrimary
     }
     }
 
 
+    private func setHovered(_ hovering: Bool) {
+        isHovered = hovering
+        refreshAppearance()
+    }
+
     override func mouseUp(with event: NSEvent) {
     override func mouseUp(with event: NSEvent) {
         guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
         guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
         onClick?()
         onClick?()

+ 3 - 2
smart_printer/ViewController.swift

@@ -126,11 +126,12 @@ class ViewController: NSViewController {
                     processed = image
                     processed = image
                 }
                 }
 
 
+                let displayImage = processed.normalizedForDisplay()
                 if showScanPreview {
                 if showScanPreview {
-                    overlay.showResult(processed)
+                    overlay.updateScanPreview(displayImage)
                 } else {
                 } else {
                     self?.scannerDidComplete = true
                     self?.scannerDidComplete = true
-                    onComplete(.success(processed))
+                    onComplete(.success(displayImage))
                     overlay.dismiss()
                     overlay.dismiss()
                 }
                 }
             }
             }