Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
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.
 /// 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? {
     override func hitTest(_ point: NSPoint) -> NSView? {
         guard let superview else { return nil }
         guard let superview else { return nil }
         let local = convert(point, from: superview)
         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 final class SettingsMenuViewController: NSViewController {
     private let palette: Palette
     private let palette: Palette
     private let typography: Typography
     private let typography: Typography
@@ -727,7 +771,7 @@ private final class SettingsMenuViewController: NSViewController {
     }
     }
 
 
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
-        let row = RowHitTestView()
+        let row = HoverTrackingView()
         row.translatesAutoresizingMaskIntoConstraints = false
         row.translatesAutoresizingMaskIntoConstraints = false
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
 
 
@@ -751,6 +795,12 @@ private final class SettingsMenuViewController: NSViewController {
         row.addSubview(icon)
         row.addSubview(icon)
         row.addSubview(title)
         row.addSubview(title)
         row.addSubview(toggle)
         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([
         NSLayoutConstraint.activate([
             icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
             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 {
     private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
-        let row = RowHitTestView()
+        let row = HoverTrackingView()
         row.translatesAutoresizingMaskIntoConstraints = false
         row.translatesAutoresizingMaskIntoConstraints = false
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
 
 
@@ -794,6 +844,12 @@ private final class SettingsMenuViewController: NSViewController {
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
         row.addGestureRecognizer(click)
         row.addGestureRecognizer(click)
         row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
         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
         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 {
     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.wantsLayer = true
         item.layer?.cornerRadius = 10
         item.layer?.cornerRadius = 10
         item.layer?.backgroundColor = NSColor.clear.cgColor
         item.layer?.backgroundColor = NSColor.clear.cgColor
@@ -910,6 +966,10 @@ private extension ViewController {
         NSLayoutConstraint.activate(constraints)
         NSLayoutConstraint.activate(constraints)
 
 
         applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
         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(_:)))
         let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
         item.addGestureRecognizer(click)
         item.addGestureRecognizer(click)
@@ -917,9 +977,10 @@ private extension ViewController {
         return item
         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)
         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
         let tint = selected ? NSColor.white : palette.textSecondary
         guard item.subviews.count >= 2 else { return }
         guard item.subviews.count >= 2 else { return }
         let leading = item.subviews[0]
         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 {
     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.wantsLayer = true
         tab.layer?.cornerRadius = 19
         tab.layer?.cornerRadius = 19
         tab.layer?.backgroundColor = NSColor.clear.cgColor
         tab.layer?.backgroundColor = NSColor.clear.cgColor
@@ -981,6 +1042,10 @@ private extension ViewController {
         NSLayoutConstraint.activate(constraints)
         NSLayoutConstraint.activate(constraints)
 
 
         applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
         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(_:)))
         let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
         tab.addGestureRecognizer(click)
         tab.addGestureRecognizer(click)
@@ -988,9 +1053,10 @@ private extension ViewController {
         return tab
         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)
         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 }
         guard tab.subviews.count >= 2 else { return }
         let leading = tab.subviews[0]
         let leading = tab.subviews[0]
         let title = tab.subviews[1] as? NSTextField
         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 {
     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.translatesAutoresizingMaskIntoConstraints = false
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
@@ -1022,11 +1091,21 @@ private extension ViewController {
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
             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
         return button
     }
     }
 
 
     func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
     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.translatesAutoresizingMaskIntoConstraints = false
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
         button.heightAnchor.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)
             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
         return button
     }
     }
 }
 }