소스 검색

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 주 전
부모
커밋
cbf2ef7756
1개의 변경된 파일97개의 추가작업 그리고 11개의 파일을 삭제
  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
 }