소스 검색

Add hover effects to interactive controls

Add hover tracking with pointer cursor and subtle hover states for sidebar items, meeting tabs, action buttons, icon buttons, and settings popover rows.

Made-with: Cursor
huzaifahayat12 3 달 전
부모
커밋
cbf2ef7756
1개의 변경된 파일97개의 추가작업 그리고 11개의 파일을 삭제
  1. 97 11
      meetings_app/ViewController.swift

+ 97 - 11
meetings_app/ViewController.swift

@@ -629,7 +629,7 @@ private extension ViewController {
 }
 
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
-private final class RowHitTestView: NSView {
+private class RowHitTestView: NSView {
     override func hitTest(_ point: NSPoint) -> NSView? {
         guard let superview else { return nil }
         let local = convert(point, from: superview)
@@ -637,6 +637,50 @@ private final class RowHitTestView: NSView {
     }
 }
 
+private final class HoverTrackingView: RowHitTestView {
+    var onHoverChanged: ((Bool) -> Void)?
+    var showsHandCursor = true
+
+    private var trackingAreaRef: NSTrackingArea?
+    private var isHovering = false {
+        didSet {
+            guard isHovering != oldValue else { return }
+            onHoverChanged?(isHovering)
+        }
+    }
+
+    override func updateTrackingAreas() {
+        super.updateTrackingAreas()
+        if let trackingAreaRef {
+            removeTrackingArea(trackingAreaRef)
+        }
+        let options: NSTrackingArea.Options = [
+            .activeInKeyWindow,
+            .inVisibleRect,
+            .mouseEnteredAndExited
+        ]
+        let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
+        addTrackingArea(area)
+        trackingAreaRef = area
+    }
+
+    override func mouseEntered(with event: NSEvent) {
+        super.mouseEntered(with: event)
+        isHovering = true
+    }
+
+    override func mouseExited(with event: NSEvent) {
+        super.mouseExited(with: event)
+        isHovering = false
+    }
+
+    override func resetCursorRects() {
+        super.resetCursorRects()
+        guard showsHandCursor else { return }
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}
+
 private final class SettingsMenuViewController: NSViewController {
     private let palette: Palette
     private let typography: Typography
@@ -727,7 +771,7 @@ private final class SettingsMenuViewController: NSViewController {
     }
 
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
-        let row = RowHitTestView()
+        let row = HoverTrackingView()
         row.translatesAutoresizingMaskIntoConstraints = false
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
 
@@ -751,6 +795,12 @@ private final class SettingsMenuViewController: NSViewController {
         row.addSubview(icon)
         row.addSubview(title)
         row.addSubview(toggle)
+        row.onHoverChanged = { hovering in
+            row.wantsLayer = true
+            row.layer?.cornerRadius = 10
+            row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
+        }
+        row.onHoverChanged?(false)
 
         NSLayoutConstraint.activate([
             icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
@@ -767,7 +817,7 @@ private final class SettingsMenuViewController: NSViewController {
     }
 
     private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
-        let row = RowHitTestView()
+        let row = HoverTrackingView()
         row.translatesAutoresizingMaskIntoConstraints = false
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
 
@@ -794,6 +844,12 @@ private final class SettingsMenuViewController: NSViewController {
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
         row.addGestureRecognizer(click)
         row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
+        row.onHoverChanged = { hovering in
+            row.wantsLayer = true
+            row.layer?.cornerRadius = 10
+            row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
+        }
+        row.onHoverChanged?(false)
 
         return row
     }
@@ -852,7 +908,7 @@ private extension ViewController {
     }
 
     func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
-        let item = RowHitTestView()
+        let item = HoverTrackingView()
         item.wantsLayer = true
         item.layer?.cornerRadius = 10
         item.layer?.backgroundColor = NSColor.clear.cgColor
@@ -910,6 +966,10 @@ private extension ViewController {
         NSLayoutConstraint.activate(constraints)
 
         applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
+        item.onHoverChanged = { [weak self, weak item] hovering in
+            guard let self, let item else { return }
+            self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
+        }
 
         let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
         item.addGestureRecognizer(click)
@@ -917,9 +977,10 @@ private extension ViewController {
         return item
     }
 
-    func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool) {
+    func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
         let selected = (page == selectedSidebarPage)
-        item.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
+        let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
+        item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
         let tint = selected ? NSColor.white : palette.textSecondary
         guard item.subviews.count >= 2 else { return }
         let leading = item.subviews[0]
@@ -939,7 +1000,7 @@ private extension ViewController {
     }
 
     func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
-        let tab = RowHitTestView()
+        let tab = HoverTrackingView()
         tab.wantsLayer = true
         tab.layer?.cornerRadius = 19
         tab.layer?.backgroundColor = NSColor.clear.cgColor
@@ -981,6 +1042,10 @@ private extension ViewController {
         NSLayoutConstraint.activate(constraints)
 
         applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
+        tab.onHoverChanged = { [weak self, weak tab] hovering in
+            guard let self, let tab else { return }
+            self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering)
+        }
 
         let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
         tab.addGestureRecognizer(click)
@@ -988,9 +1053,10 @@ private extension ViewController {
         return tab
     }
 
-    func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool) {
+    func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) {
         let selected = (provider == selectedMeetingProvider)
-        tab.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
+        let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
+        tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
         guard tab.subviews.count >= 2 else { return }
         let leading = tab.subviews[0]
         let title = tab.subviews[1] as? NSTextField
@@ -1006,7 +1072,10 @@ private extension ViewController {
     }
 
     func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
-        let button = roundedContainer(cornerRadius: 9, color: color)
+        let button = HoverTrackingView()
+        button.wantsLayer = true
+        button.layer?.cornerRadius = 9
+        button.layer?.backgroundColor = color.cgColor
         button.translatesAutoresizingMaskIntoConstraints = false
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
@@ -1022,11 +1091,21 @@ private extension ViewController {
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
         ])
 
+        let baseColor = (title == "Cancel") ? palette.cancelButton : color
+        let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
+        button.onHoverChanged = { hovering in
+            button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+        }
+        button.onHoverChanged?(false)
+
         return button
     }
 
     func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
-        let button = roundedContainer(cornerRadius: size / 2, color: palette.inputBackground)
+        let button = HoverTrackingView()
+        button.wantsLayer = true
+        button.layer?.cornerRadius = size / 2
+        button.layer?.backgroundColor = palette.inputBackground.cgColor
         button.translatesAutoresizingMaskIntoConstraints = false
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
         button.heightAnchor.constraint(equalToConstant: size).isActive = true
@@ -1039,6 +1118,13 @@ private extension ViewController {
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
         ])
 
+        let baseColor = palette.inputBackground
+        let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
+        button.onHoverChanged = { hovering in
+            button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+        }
+        button.onHoverChanged?(false)
+
         return button
     }
 }