Переглянути джерело

Add animated circular hover ring on signed-in Google profile

Wrap the auth button in GoogleProfileAuthHostView, expand layout inset when
showing the avatar, and pulse the accent ring line width on hover. Clear the
pill border/background in avatar mode so the ring reads as the hover affordance.

Made-with: Cursor
huzaifahayat12 3 місяців тому
батько
коміт
b3bf889096
1 змінених файлів з 134 додано та 1 видалено
  1. 134 1
      meetings_app/ViewController.swift

+ 134 - 1
meetings_app/ViewController.swift

@@ -6,6 +6,7 @@
 //
 
 import Cocoa
+import QuartzCore
 import WebKit
 import AuthenticationServices
 
@@ -85,6 +86,9 @@ final class ViewController: NSViewController {
     private weak var scheduleScrollRightButton: NSView?
     private weak var scheduleFilterDropdown: NSPopUpButton?
     private weak var scheduleGoogleAuthButton: NSButton?
+    private weak var scheduleGoogleAuthHostView: GoogleProfileAuthHostView?
+    private var scheduleGoogleAuthHostPadWidthConstraint: NSLayoutConstraint?
+    private var scheduleGoogleAuthHostPadHeightConstraint: NSLayoutConstraint?
     private var scheduleGoogleAuthButtonWidthConstraint: NSLayoutConstraint?
     private var scheduleGoogleAuthButtonHeightConstraint: NSLayoutConstraint?
     /// Circular avatar size when signed in (top-right, Google-style).
@@ -1847,10 +1851,25 @@ private extension ViewController {
         row.addArrangedSubview(spacer)
         spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
 
+        let host = GoogleProfileAuthHostView()
+        host.translatesAutoresizingMaskIntoConstraints = false
         let authButton = makeGoogleAuthButton()
+        host.authButton = authButton
+        scheduleGoogleAuthHostView = host
         scheduleGoogleAuthButton = authButton
+        host.addSubview(authButton)
+        NSLayoutConstraint.activate([
+            authButton.centerXAnchor.constraint(equalTo: host.centerXAnchor),
+            authButton.centerYAnchor.constraint(equalTo: host.centerYAnchor)
+        ])
+        let hostPadW = host.widthAnchor.constraint(equalTo: authButton.widthAnchor, constant: 0)
+        let hostPadH = host.heightAnchor.constraint(equalTo: authButton.heightAnchor, constant: 0)
+        hostPadW.isActive = true
+        hostPadH.isActive = true
+        scheduleGoogleAuthHostPadWidthConstraint = hostPadW
+        scheduleGoogleAuthHostPadHeightConstraint = hostPadH
         updateGoogleAuthButtonTitle()
-        row.addArrangedSubview(authButton)
+        row.addArrangedSubview(host)
 
         row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
         return row
@@ -1928,6 +1947,7 @@ private extension ViewController {
         scheduleGoogleAuthButtonWidthConstraint = widthConstraint
         button.onHoverChanged = { [weak self] hovering in
             self?.scheduleGoogleAuthHovering = hovering
+            self?.scheduleGoogleAuthHostView?.setProfileHoverActive(hovering)
             self?.applyGoogleAuthButtonSurface()
         }
         button.onHoverChanged?(false)
@@ -2164,6 +2184,103 @@ extension ViewController: NSWindowDelegate {
     }
 }
 
+/// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
+private final class GoogleProfileAuthHostView: NSView {
+    weak var authButton: NSButton? {
+        didSet { needsLayout = true }
+    }
+
+    private let ringLayer = CAShapeLayer()
+    private var avatarRingMode = false
+    private static let ringLineWidth: CGFloat = 2.25
+
+    override init(frame frameRect: NSRect) {
+        super.init(frame: frameRect)
+        wantsLayer = true
+        layer?.masksToBounds = false
+        ringLayer.fillColor = nil
+        ringLayer.strokeColor = NSColor.clear.cgColor
+        ringLayer.lineWidth = Self.ringLineWidth
+        ringLayer.lineCap = .round
+        ringLayer.opacity = 0
+        ringLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
+        layer?.insertSublayer(ringLayer, at: 0)
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        nil
+    }
+
+    func setAvatarRingMode(_ enabled: Bool) {
+        avatarRingMode = enabled
+        if enabled == false {
+            ringLayer.removeAllAnimations()
+            ringLayer.opacity = 0
+            ringLayer.lineWidth = Self.ringLineWidth
+        }
+        needsLayout = true
+    }
+
+    func updateRingAppearance(isDark: Bool, accent: NSColor) {
+        let stroke = isDark
+            ? accent.blended(withFraction: 0.22, of: NSColor.white) ?? accent
+            : accent
+        CATransaction.begin()
+        CATransaction.setDisableActions(true)
+        ringLayer.strokeColor = stroke.withAlphaComponent(0.95).cgColor
+        CATransaction.commit()
+    }
+
+    func setProfileHoverActive(_ active: Bool) {
+        guard avatarRingMode else { return }
+        ringLayer.removeAnimation(forKey: "pulse")
+        if active {
+            layoutRingPathIfNeeded()
+            CATransaction.begin()
+            CATransaction.setAnimationDuration(0.22)
+            ringLayer.opacity = 1
+            CATransaction.commit()
+            let pulse = CABasicAnimation(keyPath: "lineWidth")
+            pulse.fromValue = Self.ringLineWidth * 0.88
+            pulse.toValue = Self.ringLineWidth * 1.45
+            pulse.duration = 0.72
+            pulse.autoreverses = true
+            pulse.repeatCount = .infinity
+            pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+            ringLayer.add(pulse, forKey: "pulse")
+        } else {
+            CATransaction.begin()
+            CATransaction.setAnimationDuration(0.18)
+            ringLayer.opacity = 0
+            CATransaction.commit()
+            ringLayer.lineWidth = Self.ringLineWidth
+        }
+    }
+
+    private func layoutRingPathIfNeeded() {
+        guard avatarRingMode, let btn = authButton else { return }
+        let f = btn.frame
+        guard f.width > 1, f.height > 1 else { return }
+        let center = CGPoint(x: f.midX, y: f.midY)
+        let avatarR = min(f.width, f.height) / 2
+        let gap: CGFloat = 3.5
+        let ringRadius = avatarR + gap
+        let d = ringRadius * 2
+        CATransaction.begin()
+        CATransaction.setDisableActions(true)
+        ringLayer.bounds = CGRect(x: 0, y: 0, width: d, height: d)
+        ringLayer.position = center
+        ringLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: d, height: d)), transform: nil)
+        CATransaction.commit()
+    }
+
+    override func layout() {
+        super.layout()
+        layoutRingPathIfNeeded()
+    }
+}
+
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
 private class RowHitTestView: NSView {
     override func hitTest(_ point: NSPoint) -> NSView? {
@@ -3238,6 +3355,14 @@ private extension ViewController {
         guard let button = scheduleGoogleAuthButton else { return }
 
         let profileName = scheduleCurrentProfile?.name ?? "Google account"
+        let ringHostInset: CGFloat = signedIn ? 14 : 0
+        scheduleGoogleAuthHostPadWidthConstraint?.constant = ringHostInset
+        scheduleGoogleAuthHostPadHeightConstraint?.constant = ringHostInset
+        scheduleGoogleAuthHostView?.setAvatarRingMode(signedIn)
+        scheduleGoogleAuthHostView?.updateRingAppearance(isDark: darkModeEnabled, accent: palette.primaryBlue)
+        if signedIn == false {
+            scheduleGoogleAuthHostView?.setProfileHoverActive(false)
+        }
 
         if signedIn {
             button.setAccessibilityLabel("\(profileName), Google account")
@@ -3360,7 +3485,14 @@ private extension ViewController {
 
     private func applyGoogleAuthButtonSurface() {
         guard let button = scheduleGoogleAuthButton else { return }
+        let signedIn = (googleOAuth.loadTokens() != nil)
         let isDark = darkModeEnabled
+        if signedIn {
+            button.layer?.backgroundColor = NSColor.clear.cgColor
+            button.layer?.borderWidth = 0
+            scheduleGoogleAuthHostView?.updateRingAppearance(isDark: isDark, accent: palette.primaryBlue)
+            return
+        }
         let baseBackground = isDark
             ? NSColor(calibratedRed: 8.0 / 255.0, green: 14.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
             : NSColor.white
@@ -3372,6 +3504,7 @@ private extension ViewController {
         let hoverBorder = isDark
             ? NSColor(calibratedWhite: 0.62, alpha: 1)
             : NSColor(calibratedWhite: 0.56, alpha: 1)
+        button.layer?.borderWidth = 1
         button.layer?.backgroundColor = (scheduleGoogleAuthHovering ? hoverBackground : baseBackground).cgColor
         button.layer?.borderColor = (scheduleGoogleAuthHovering ? hoverBorder : baseBorder).cgColor
     }