Explorar o código

Improve desktop widget placement and avoid macOS system widget zone

Use live panel frames plus persisted origins for collision-aware grid
placement. Persist widget origin immediately after positioning.

Reserve the top-left desktop area where system widgets render above
third-party windows so new panels open beside them, not underneath.
Add fallbacks to the right of that zone and to screen center.

Made-with: Cursor
huzaifahayat12 hai 3 meses
pai
achega
808738d243
Modificáronse 1 ficheiros con 103 adicións e 17 borrados
  1. 103 17
      google_apps/Widgets/DesktopWidgetWindowManager.swift

+ 103 - 17
google_apps/Widgets/DesktopWidgetWindowManager.swift

@@ -24,11 +24,17 @@ final class DesktopWidgetWindowManager {
 
         if let origin = resolvedInstance.origin {
             panel.setFrameOrigin(NSPoint(x: origin.x, y: origin.y))
+            // Persist immediately so future auto-placement has accurate collision data.
+            persistOrigin(instanceID: instance.id, origin: panel.frame.origin)
         } else {
             panel.center()
+            // Persist immediately so future auto-placement has accurate collision data.
+            persistOrigin(instanceID: instance.id, origin: panel.frame.origin)
         }
 
+        // Ensure the newly added widget is visually on top.
         panel.orderFront(nil)
+        panel.orderFrontRegardless()
     }
 
     func restore(appProvider: (UUID) -> LauncherApp?) {
@@ -131,27 +137,107 @@ final class DesktopWidgetWindowManager {
         }
     }
 
+    /// Region where macOS system desktop widgets (Calendar, Weather, Photos, etc.) usually sit.
+    /// Those windows are composited above normal third-party desktop windows, so our panels cannot
+    /// appear “on top” of them—auto-placement must avoid this area so the user sees our widget beside them.
+    private func reservedSystemDesktopWidgetZone(in visibleFrame: NSRect) -> NSRect {
+        let margin: CGFloat = 18
+        let w = min(560, visibleFrame.width * 0.48)
+        let h = min(420, visibleFrame.height * 0.55)
+        return NSRect(
+            x: visibleFrame.minX + margin,
+            y: visibleFrame.maxY - margin - h,
+            width: w,
+            height: h
+        )
+    }
+
     private func nextAutoPlacement(for size: WidgetSize) -> WidgetInstance.Origin {
-        let panel = panelSize(for: size)
+        let panelSizeForNew = panelSize(for: size)
         let frame = NSScreen.main?.visibleFrame ?? NSRect(x: 100, y: 100, width: 1200, height: 800)
+        let systemWidgetZone = reservedSystemDesktopWidgetZone(in: frame)
+
         let margin: CGFloat = 18
-        let stepX: CGFloat = 28
-        let stepY: CGFloat = 24
-        let columns = max(1, Int((frame.width - margin * 2) / max(panel.width + margin, 1)))
-        let existing = loadInstances().count
-        let row = existing / columns
-        let col = existing % columns
-
-        let maxX = frame.maxX - panel.width - margin
-        let minX = frame.minX + margin
-        let baseX = minX + CGFloat(col) * (panel.width + stepX)
-        let x = min(max(baseX, minX), maxX)
-
-        let maxY = frame.maxY - panel.height - margin
-        let minY = frame.minY + margin
-        let baseY = maxY - CGFloat(row) * (panel.height + stepY)
-        let y = min(max(baseY, minY), maxY)
+        let gapX: CGFloat = 26
+        let gapY: CGFloat = 20
+        let cellW = panelSizeForNew.width + gapX
+        let cellH = panelSizeForNew.height + gapY
+
+        // Existing widgets:
+        // 1) Live panels already on screen (most reliable; avoids missing persisted origins).
+        // 2) Persisted widget origins (fallback for already-closed widgets).
+        let liveRects: [NSRect] = windowsByInstanceID.values.map { $0.frame }
+        let persistedRects: [NSRect] = loadInstances().compactMap { instance in
+            guard let origin = instance.origin else { return nil }
+            let s = panelSize(for: instance.size)
+            return NSRect(x: origin.x, y: origin.y, width: s.width, height: s.height)
+        }
+        let existingRects = liveRects + persistedRects
+
+        let keepOut: CGFloat = 8
+        func overlapsExisting(_ candidate: NSRect) -> Bool {
+            let inflated = candidate.insetBy(dx: -keepOut, dy: -keepOut)
+            return existingRects.contains(where: { $0.intersects(inflated) })
+        }
+
+        func overlapsSystemWidgetZone(_ candidate: NSRect) -> Bool {
+            candidate.intersects(systemWidgetZone)
+        }
+
+        let cols = max(1, Int((frame.width - margin * 2) / max(cellW, 1)))
+        let maxSlots = 200
+
+        // Scan row-major for the first slot that avoids our widgets and Apple's desktop widget zone.
+        for slot in 0..<maxSlots {
+            let row = slot / cols
+            let col = slot % cols
+
+            let x = frame.minX + margin + CGFloat(col) * cellW
+            let y = frame.maxY - margin - panelSizeForNew.height - CGFloat(row) * cellH
+
+            let candidate = NSRect(x: x, y: y, width: panelSizeForNew.width, height: panelSizeForNew.height)
+
+            // Keep inside visible bounds with margin.
+            guard candidate.minX >= frame.minX + margin,
+                  candidate.maxX <= frame.maxX - margin,
+                  candidate.minY >= frame.minY + margin,
+                  candidate.maxY <= frame.maxY - margin else {
+                continue
+            }
+
+            if !overlapsExisting(candidate), !overlapsSystemWidgetZone(candidate) {
+                return WidgetInstance.Origin(x: candidate.minX, y: candidate.minY)
+            }
+        }
+
+        // Fallback: first column to the right of the system widget zone, then continue the grid.
+        let safeMinX = max(systemWidgetZone.maxX + gapX, frame.minX + margin)
+        for slot in 0..<maxSlots {
+            let row = slot / cols
+            let col = slot % cols
+
+            let x = safeMinX + CGFloat(col) * cellW
+            let y = frame.maxY - margin - panelSizeForNew.height - CGFloat(row) * cellH
+
+            let candidate = NSRect(x: x, y: y, width: panelSizeForNew.width, height: panelSizeForNew.height)
+
+            guard candidate.minX >= frame.minX + margin,
+                  candidate.maxX <= frame.maxX - margin,
+                  candidate.minY >= frame.minY + margin,
+                  candidate.maxY <= frame.maxY - margin else {
+                continue
+            }
+
+            if !overlapsExisting(candidate) {
+                return WidgetInstance.Origin(x: candidate.minX, y: candidate.minY)
+            }
+        }
 
+        // Last resort: center of screen (usually clear of the top-left widget stack).
+        let cx = frame.midX - panelSizeForNew.width / 2
+        let cy = frame.midY - panelSizeForNew.height / 2
+        let x = min(max(cx, frame.minX + margin), frame.maxX - margin - panelSizeForNew.width)
+        let y = min(max(cy, frame.minY + margin), frame.maxY - margin - panelSizeForNew.height)
         return WidgetInstance.Origin(x: x, y: y)
     }