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 2 viikkoa sitten
vanhempi
commit
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 {
629 629
 }
630 630
 
631 631
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
632
-private final class RowHitTestView: NSView {
632
+private class RowHitTestView: NSView {
633 633
     override func hitTest(_ point: NSPoint) -> NSView? {
634 634
         guard let superview else { return nil }
635 635
         let local = convert(point, from: superview)
@@ -637,6 +637,50 @@ private final class RowHitTestView: NSView {
637 637
     }
638 638
 }
639 639
 
640
+private final class HoverTrackingView: RowHitTestView {
641
+    var onHoverChanged: ((Bool) -> Void)?
642
+    var showsHandCursor = true
643
+
644
+    private var trackingAreaRef: NSTrackingArea?
645
+    private var isHovering = false {
646
+        didSet {
647
+            guard isHovering != oldValue else { return }
648
+            onHoverChanged?(isHovering)
649
+        }
650
+    }
651
+
652
+    override func updateTrackingAreas() {
653
+        super.updateTrackingAreas()
654
+        if let trackingAreaRef {
655
+            removeTrackingArea(trackingAreaRef)
656
+        }
657
+        let options: NSTrackingArea.Options = [
658
+            .activeInKeyWindow,
659
+            .inVisibleRect,
660
+            .mouseEnteredAndExited
661
+        ]
662
+        let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
663
+        addTrackingArea(area)
664
+        trackingAreaRef = area
665
+    }
666
+
667
+    override func mouseEntered(with event: NSEvent) {
668
+        super.mouseEntered(with: event)
669
+        isHovering = true
670
+    }
671
+
672
+    override func mouseExited(with event: NSEvent) {
673
+        super.mouseExited(with: event)
674
+        isHovering = false
675
+    }
676
+
677
+    override func resetCursorRects() {
678
+        super.resetCursorRects()
679
+        guard showsHandCursor else { return }
680
+        addCursorRect(bounds, cursor: .pointingHand)
681
+    }
682
+}
683
+
640 684
 private final class SettingsMenuViewController: NSViewController {
641 685
     private let palette: Palette
642 686
     private let typography: Typography
@@ -727,7 +771,7 @@ private final class SettingsMenuViewController: NSViewController {
727 771
     }
728 772
 
729 773
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
730
-        let row = RowHitTestView()
774
+        let row = HoverTrackingView()
731 775
         row.translatesAutoresizingMaskIntoConstraints = false
732 776
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
733 777
 
@@ -751,6 +795,12 @@ private final class SettingsMenuViewController: NSViewController {
751 795
         row.addSubview(icon)
752 796
         row.addSubview(title)
753 797
         row.addSubview(toggle)
798
+        row.onHoverChanged = { hovering in
799
+            row.wantsLayer = true
800
+            row.layer?.cornerRadius = 10
801
+            row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
802
+        }
803
+        row.onHoverChanged?(false)
754 804
 
755 805
         NSLayoutConstraint.activate([
756 806
             icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
@@ -767,7 +817,7 @@ private final class SettingsMenuViewController: NSViewController {
767 817
     }
768 818
 
769 819
     private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
770
-        let row = RowHitTestView()
820
+        let row = HoverTrackingView()
771 821
         row.translatesAutoresizingMaskIntoConstraints = false
772 822
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
773 823
 
@@ -794,6 +844,12 @@ private final class SettingsMenuViewController: NSViewController {
794 844
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
795 845
         row.addGestureRecognizer(click)
796 846
         row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
847
+        row.onHoverChanged = { hovering in
848
+            row.wantsLayer = true
849
+            row.layer?.cornerRadius = 10
850
+            row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
851
+        }
852
+        row.onHoverChanged?(false)
797 853
 
798 854
         return row
799 855
     }
@@ -852,7 +908,7 @@ private extension ViewController {
852 908
     }
853 909
 
854 910
     func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
855
-        let item = RowHitTestView()
911
+        let item = HoverTrackingView()
856 912
         item.wantsLayer = true
857 913
         item.layer?.cornerRadius = 10
858 914
         item.layer?.backgroundColor = NSColor.clear.cgColor
@@ -910,6 +966,10 @@ private extension ViewController {
910 966
         NSLayoutConstraint.activate(constraints)
911 967
 
912 968
         applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
969
+        item.onHoverChanged = { [weak self, weak item] hovering in
970
+            guard let self, let item else { return }
971
+            self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
972
+        }
913 973
 
914 974
         let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
915 975
         item.addGestureRecognizer(click)
@@ -917,9 +977,10 @@ private extension ViewController {
917 977
         return item
918 978
     }
919 979
 
920
-    func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool) {
980
+    func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
921 981
         let selected = (page == selectedSidebarPage)
922
-        item.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
982
+        let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
983
+        item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
923 984
         let tint = selected ? NSColor.white : palette.textSecondary
924 985
         guard item.subviews.count >= 2 else { return }
925 986
         let leading = item.subviews[0]
@@ -939,7 +1000,7 @@ private extension ViewController {
939 1000
     }
940 1001
 
941 1002
     func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
942
-        let tab = RowHitTestView()
1003
+        let tab = HoverTrackingView()
943 1004
         tab.wantsLayer = true
944 1005
         tab.layer?.cornerRadius = 19
945 1006
         tab.layer?.backgroundColor = NSColor.clear.cgColor
@@ -981,6 +1042,10 @@ private extension ViewController {
981 1042
         NSLayoutConstraint.activate(constraints)
982 1043
 
983 1044
         applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
1045
+        tab.onHoverChanged = { [weak self, weak tab] hovering in
1046
+            guard let self, let tab else { return }
1047
+            self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering)
1048
+        }
984 1049
 
985 1050
         let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
986 1051
         tab.addGestureRecognizer(click)
@@ -988,9 +1053,10 @@ private extension ViewController {
988 1053
         return tab
989 1054
     }
990 1055
 
991
-    func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool) {
1056
+    func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) {
992 1057
         let selected = (provider == selectedMeetingProvider)
993
-        tab.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
1058
+        let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
1059
+        tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
994 1060
         guard tab.subviews.count >= 2 else { return }
995 1061
         let leading = tab.subviews[0]
996 1062
         let title = tab.subviews[1] as? NSTextField
@@ -1006,7 +1072,10 @@ private extension ViewController {
1006 1072
     }
1007 1073
 
1008 1074
     func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
1009
-        let button = roundedContainer(cornerRadius: 9, color: color)
1075
+        let button = HoverTrackingView()
1076
+        button.wantsLayer = true
1077
+        button.layer?.cornerRadius = 9
1078
+        button.layer?.backgroundColor = color.cgColor
1010 1079
         button.translatesAutoresizingMaskIntoConstraints = false
1011 1080
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
1012 1081
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
@@ -1022,11 +1091,21 @@ private extension ViewController {
1022 1091
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
1023 1092
         ])
1024 1093
 
1094
+        let baseColor = (title == "Cancel") ? palette.cancelButton : color
1095
+        let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
1096
+        button.onHoverChanged = { hovering in
1097
+            button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1098
+        }
1099
+        button.onHoverChanged?(false)
1100
+
1025 1101
         return button
1026 1102
     }
1027 1103
 
1028 1104
     func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
1029
-        let button = roundedContainer(cornerRadius: size / 2, color: palette.inputBackground)
1105
+        let button = HoverTrackingView()
1106
+        button.wantsLayer = true
1107
+        button.layer?.cornerRadius = size / 2
1108
+        button.layer?.backgroundColor = palette.inputBackground.cgColor
1030 1109
         button.translatesAutoresizingMaskIntoConstraints = false
1031 1110
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
1032 1111
         button.heightAnchor.constraint(equalToConstant: size).isActive = true
@@ -1039,6 +1118,13 @@ private extension ViewController {
1039 1118
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
1040 1119
         ])
1041 1120
 
1121
+        let baseColor = palette.inputBackground
1122
+        let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
1123
+        button.onHoverChanged = { hovering in
1124
+            button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1125
+        }
1126
+        button.onHoverChanged?(false)
1127
+
1042 1128
         return button
1043 1129
     }
1044 1130
 }