Selaa lähdekoodia

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 kuukautta sitten
vanhempi
sitoutus
cbf2ef7756
1 muutettua tiedostoa jossa 97 lisäystä ja 11 poistoa
  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
     }
 }