A Swift package that applies macOS Genie-style minimize/restore effects to any NSWindow using the private CGSSetWindowWarp API.
- macOS 14.0+
- (Tested only on macOS 26.4)
- Swift 5.9+
Add the package as a local dependency or reference the repository URL:
dependencies: [
.package(url: "https://github.com/usagimaru/GenieWarpMesh.git", from: "1.0.0")
]Then add GenieWarpMesh to your target's dependencies:
.target(
name: "YourApp",
dependencies: ["GenieWarpMesh"]
)If your app directly uses CGS private APIs (e.g. CGSGetWindowBounds), also add CGSPrivate:
dependencies: ["GenieWarpMesh", "CGSPrivate"]Create a GenieEffect instance and call minimize(window:to:direction:completion:) / restore(window:from:direction:completion:). All rect parameters use the Cocoa coordinate system (bottom-left origin) — you can pass NSWindow.frame or NSScreen.frame values directly.
import GenieWarpMesh
let genieEffect = GenieEffect()
// Minimize: warp the window into the target rect and hide it
genieEffect.minimize(window: myWindow, to: dockIconFrame) {
print("Window is now hidden")
}
// Restore: reverse-warp from the target rect to re-show the window
genieEffect.restore(window: myWindow, from: dockIconFrame) {
print("Window is visible again")
myWindow.makeKey()
}After minimize completes, the window is ordered out (hidden) but not closed — its warp state is preserved. The next restore (or another minimize) automatically resets the warp before starting.
Note:
GenieEffectdoes not track whether the window is minimized. It only holds internal animation state (isAnimating/isReversed). Your app is responsible for managing a flag likeisMinimizedto decide whether to callminimizeorrestore.
class WindowController: NSWindowController {
private let genieEffect = GenieEffect()
private var isMinimized = false
func toggleGenie() {
guard let window = self.window else { return }
let targetRect = dockTileFrame() // Your target rect in Cocoa coordinates
if isMinimized {
genieEffect.restore(window: window, from: targetRect) { [weak self] in
self?.isMinimized = false
window.makeKey()
}
}
else {
genieEffect.minimize(window: window, to: targetRect) { [weak self] in
self?.isMinimized = true
}
}
}
}GenieDirection specifies which edge the window warps toward:
| Value | Description |
|---|---|
.auto (default) |
Determined from source/target geometry |
.bottom |
Warp toward the bottom edge |
.top |
Warp toward the top edge |
.left |
Warp toward the left edge |
.right |
Warp toward the right edge |
.auto compares the horizontal and vertical distances between the centers of the source window and target rect, then selects the dominant axis. You can omit the direction parameter to use auto-detection:
// Direction is automatically determined
genieEffect.minimize(window: myWindow, to: targetRect)GenieEffect properties can be customized before calling minimize / restore:
// Animation
genieEffect.duration = 0.5 // Animation duration (seconds)
genieEffect.easingType = .easeInOutQuart // Main easing curve
genieEffect.retreatEasingType = .easeInQuad // Easing for retreat movement
// Curve shape
genieEffect.curveP1Ratio = 0.45 // Bézier control point P1 position (0–1)
genieEffect.curveP2Ratio = 0.65 // Bézier control point P2 position (0–1)
// Deformation behavior
genieEffect.widthEnd = 0.4 // Progress at which width shrink completes
genieEffect.slideStart = 0.15 // Progress at which sliding begins
genieEffect.stretchPower = 2.0 // Trailing edge stretch intensity
// Retreat (auto-correction when source and target are close)
genieEffect.retreatEnd = 0.4 // Progress at which retreat completes
genieEffect.skipCutoffOnRetreat = true // Skip cutoff during retreat
// Phase cutoff (trim the animation timeline)
genieEffect.minimizeRawTStart = 0.0 // Forward: skip this portion from the start
genieEffect.minimizeRawTEnd = 1.0 // Forward: stop at this point
genieEffect.restoreRawTStart = 0.0 // Reverse: skip this portion from the start
genieEffect.restoreRawTEnd = 1.0 // Reverse: stop at this point
// Mesh resolution
genieEffect.gridWidth = 8 // Mesh grid columns
genieEffect.gridHeight = 20 // Mesh grid rows
genieEffect.adaptiveMesh = true // Auto-adjust resolution by directionThe EasingType enum provides standard polynomial easing functions:
linear
easeInQuad / easeOutQuad / easeInOutQuad (2nd order)
easeInCubic / easeOutCubic / easeInOutCubic (3rd order)
easeInQuart / easeOutQuart / easeInOutQuart (4th order)
easeInQuint / easeOutQuint / easeInOutQuint (5th order)
Monitor the animation progress (0.0 → 1.0) on every display frame:
genieEffect.progressHandler = { progress in
// Update a progress indicator, etc.
print("Progress: \(progress)")
}
genieEffect.minimize(window: myWindow, to: targetRect) {
genieEffect.progressHandler = nil // Clean up when done
}The library includes DebugOverlayWindow, a full-screen transparent overlay that visualizes Bézier curve paths, control points, mesh wireframes, and corrected frames. This is the easiest way to debug the effect:
let debugOverlay = DebugOverlayWindow()
debugOverlay.orderFront(nil)
genieEffect.debugOverlayReceiver = debugOverlayCall fitToScreen() when the screen geometry changes, and clearCurves() to reset:
debugOverlay.fitToScreen() // After screen resolution change
debugOverlay.clearCurves() // Clear all visualizationsImplement the GenieDebugOverlay protocol for custom visualization:
class MyOverlay: NSWindow, GenieDebugOverlay {
func receiveCurveGuideData(
leftCurve: (p0: CGPoint, p1: CGPoint, p2: CGPoint, p3: CGPoint),
rightCurve: (p0: CGPoint, p1: CGPoint, p2: CGPoint, p3: CGPoint),
sourceFrame: CGRect,
targetFrame: CGRect,
fitRect: CGRect?,
leftExtensionEnd: CGPoint?,
rightExtensionEnd: CGPoint?,
correctedData: CorrectedCurveData?
) { /* Draw curve guides */ }
func receiveMeshEdgePoints(
_ points: [CGPoint],
gridWidth: Int,
gridHeight: Int,
screenHeight: CGFloat
) { /* Draw mesh wireframe */ }
func clearMeshEdgePoints() { /* Clear mesh visualization */ }
}You can also call updateDebugOverlayForCurrentLayout(sourceFrame:targetFrame:direction:) to refresh the overlay without running an animation — useful when windows are being dragged:
genieEffect.updateDebugOverlayForCurrentLayout(
sourceFrame: window.frame,
targetFrame: targetPanel.frame,
direction: .auto
)When the source window and target rect are very close (within 20 pt on the warp axis), the Bézier curve becomes too short for a convincing effect. GenieEffect automatically computes a corrected frame that moves the source away from the target, then smoothly animates the retreat. You can preview this correction:
if let corrected = genieEffect.computeCorrectedFrame(
sourceFrame: window.frame,
targetFrame: targetRect,
direction: .bottom
) {
print("Window will retreat to: \(corrected)")
}
// Returns nil when no correction is needed- This library uses the private
CGSSetWindowWarpAPI which is not documented by Apple. Apps using this API may not be accepted on the Mac App Store. - The
CGSPrivatemodule exposes C declarations forCGSSetWindowWarp,CGSGetWindowBounds, and related functions.
See LICENSE for details.
