|
|
@@ -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)
|
|
|
}
|
|
|
|