Skip to content

Commit

Permalink
Revert "refactor: SpringMotionProtocol for NSWindow and NSPanel confo…
Browse files Browse the repository at this point in the history
…rmance"

This reverts commit f552afb.
  • Loading branch information
ostenemes committed Oct 18, 2024
1 parent 86e17fc commit 6626ac3
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 162 deletions.
2 changes: 1 addition & 1 deletion Sources/Model/SpringMotionPhysics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/// Implements the physics of spring motion.
///
/// The math is inspired by [this great post](https://www.ryanjuckett.com/damped-springs).
public final class SpringMotionPhysics {
final class SpringMotionPhysics {

private let posPosCoef: Double
private let posVelCoef: Double
Expand Down
2 changes: 1 addition & 1 deletion Sources/Model/SpringMotionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public final class SpringMotionState {
final class SpringMotionState {

struct Velocity {

Expand Down
112 changes: 95 additions & 17 deletions Sources/SpringMotionPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,124 @@
//

#if canImport(AppKit)
import AppKit

open class SpringMotionPanel: NSPanel, SpringMotionWindowProtocol {
/// Implement the shared protocol in the NSPanel subclass.


open class SpringMotionPanel: NSPanel {

/// Configuration that adjusts the spring physics behind the animation.
public var configuration: SpringConfiguration = .default {
didSet {
motionPhysics = .init(configuration: configuration, timeStep: 0.008)
}
}

public var destinationPoint: NSPoint? {

// MARK: Private
private var destinationPoint: NSPoint? {
didSet {
startMotion()
}
}
lazy public var motionPhysics = SpringMotionPhysics(configuration: configuration, timeStep: 0.008)
public var currentMotionState: SpringMotionState?
public var displayLink: CVDisplayLink?
public var anchorWindowFrameObservation: NSKeyValueObservation?

private lazy var motionPhysics = SpringMotionPhysics(configuration: configuration, timeStep: 0.008)
private var currentMotionState: SpringMotionState?
private var displayLink: CVDisplayLink?
private var anchorWindowFrameObservation: NSKeyValueObservation?
}

// MARK: - Mouse events
extension SprintMotionPanel {
// MARK: - API
public extension SpringMotionPanel {

override open func mouseDown(with event: NSEvent) {
/// Moves the window to the specified point with spring animation.
///
/// - Parameter point: a `CGPoint` that defines the window's destination and represents the window's center point.
func move(to point: NSPoint) {
destinationPoint = point
}

/// Starts following the given window with a certain offset from its center.
///
/// - Parameter anchorWindow: an `NSWindow` that this window will follow.
/// - Parameter offsetFromCenter: the difference between this window's center and the anchor window's center.
func pinToWindow(_ anchorWindow: NSWindow, offsetFromCenter: NSPoint) {
unpinFromWindow()
move(to: anchorWindow.frame.center + offsetFromCenter)
anchorWindowFrameObservation = anchorWindow.observe(\.frame, changeHandler: { [weak self] _, _ in
self?.move(to: anchorWindow.frame.center + offsetFromCenter)
})
}

/// Stops following a previously followed window, if any.
func unpinFromWindow() {
anchorWindowFrameObservation?.invalidate()
anchorWindowFrameObservation = nil
}
}

// MARK: - Mouse events
public extension SpringMotionPanel {

override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
/// Stop window from moving if the user grabbed it.
// Stop window from moving if the user grabbed it.
if isMovableByWindowBackground {
stopMotion()
}
}
override open func mouseUp(with event: NSEvent) {

override func mouseUp(with event: NSEvent) {
super.mouseUp(with: event)
/// Continue movement after the user lets go.
// Continue movement after the user lets go.
if isMovableByWindowBackground {
startMotion()
}
}
}

// MARK: - Start/stop motion
private extension SpringMotionPanel {

func startMotion() {
guard displayLink == nil,
let screenDescription = NSScreen.main?.deviceDescription,
let screenNumber = screenDescription[.init("NSScreenNumber")] as? NSNumber else { return }

CVDisplayLinkCreateWithCGDisplay(screenNumber.uint32Value, &displayLink)
guard let displayLink else { return }

CVDisplayLinkSetOutputHandler(displayLink) { [weak self] _, inNow, _, _, _ in
DispatchQueue.main.async { [weak self] in
guard let self, let destinationPoint else {
self?.stopMotion()
return
}

let currentState = currentMotionState ?? .init(position: frame.center, velocity: .zero)
let nextState = motionPhysics.calculateNextState(from: currentState, destinationPoint: destinationPoint)

if abs(nextState.velocity.horizontal) < 0.01 && abs(nextState.velocity.vertical) < 0.01 {
stopMotion()
return
}

self.setFrame(.init(x: nextState.position.x - frame.width / 2,
y: nextState.position.y - frame.height / 2,
width: frame.width,
height: frame.height), display: false)
currentMotionState = nextState
}

return kCVReturnSuccess
}

CVDisplayLinkStart(displayLink)
}

func stopMotion() {
guard let link = displayLink else { return }
CVDisplayLinkStop(link)
displayLink = nil
currentMotionState = nil
}
}

#endif
118 changes: 116 additions & 2 deletions Sources/SpringMotionWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,122 @@

import AppKit

open class SpringMotionWindow: SpringMotionBase {
/// This class inherits all functionality from SpringMotionBase.
open class SpringMotionWindow: NSWindow {

/// Configuration that adjusts the spring physics behind the animation.
public var configuration: SpringConfiguration = .default {
didSet {
motionPhysics = .init(configuration: configuration, timeStep: 0.008)
}
}

// MARK: Private
private var destinationPoint: NSPoint? {
didSet {
startMotion()
}
}

private lazy var motionPhysics = SpringMotionPhysics(configuration: configuration, timeStep: 0.008)
private var currentMotionState: SpringMotionState?
private var displayLink: CVDisplayLink?
private var anchorWindowFrameObservation: NSKeyValueObservation?
}

// MARK: - API
public extension SpringMotionWindow {

/// Moves the window to the specified point with spring animation.
///
/// - Parameter point: a `CGPoint` that defines the window's destination and represents the window's center point.
func move(to point: NSPoint) {
destinationPoint = point
}

/// Starts following the given window with a certain offset from its center.
///
/// - Parameter anchorWindow: an `NSWindow` that this window will follow.
/// - Parameter offsetFromCenter: the difference between this window's center and the anchor window's center.
func pinToWindow(_ anchorWindow: NSWindow, offsetFromCenter: NSPoint) {
unpinFromWindow()
move(to: anchorWindow.frame.center + offsetFromCenter)
anchorWindowFrameObservation = anchorWindow.observe(\.frame, changeHandler: { [weak self] _, _ in
self?.move(to: anchorWindow.frame.center + offsetFromCenter)
})
}

/// Stops following a previously followed window, if any.
func unpinFromWindow() {
anchorWindowFrameObservation?.invalidate()
anchorWindowFrameObservation = nil
}
}

// MARK: - Mouse events
extension SpringMotionWindow {

override open func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
// Stop window from moving if the user grabbed it.
if isMovableByWindowBackground {
stopMotion()
}
}

override open func mouseUp(with event: NSEvent) {
super.mouseUp(with: event)
// Continue movement after the user lets go.
if isMovableByWindowBackground {
startMotion()
}
}
}

// MARK: - Start/stop motion
private extension SpringMotionWindow {

func startMotion() {
guard displayLink == nil,
let screenDescription = NSScreen.main?.deviceDescription,
let screenNumber = screenDescription[.init("NSScreenNumber")] as? NSNumber else { return }

CVDisplayLinkCreateWithCGDisplay(screenNumber.uint32Value, &displayLink)
guard let displayLink else { return }

CVDisplayLinkSetOutputHandler(displayLink) { [weak self] _, inNow, _, _, _ in
DispatchQueue.main.async { [weak self] in
guard let self = self, let destinationPoint = self.destinationPoint else {
self?.stopMotion()
return
}

let currentState = self.currentMotionState ?? .init(position: self.frame.center, velocity: .zero)
let nextState = self.motionPhysics.calculateNextState(from: currentState, destinationPoint: destinationPoint)

if abs(nextState.velocity.horizontal) < 0.01 && abs(nextState.velocity.vertical) < 0.01 {
self.stopMotion()
return
}

self.setFrame(.init(x: nextState.position.x - self.frame.width / 2,
y: nextState.position.y - self.frame.height / 2,
width: self.frame.width,
height: self.frame.height), display: false)
self.currentMotionState = nextState
}

return kCVReturnSuccess
}

CVDisplayLinkStart(displayLink)
}

func stopMotion() {
guard let link = displayLink else { return }
CVDisplayLinkStop(link)
displayLink = nil
currentMotionState = nil
}
}

#endif
Loading

0 comments on commit 6626ac3

Please sign in to comment.