Преглед на файлове

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
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
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
     override func hitTest(_ point: NSPoint) -> NSView? {
633
     override func hitTest(_ point: NSPoint) -> NSView? {
634
         guard let superview else { return nil }
634
         guard let superview else { return nil }
635
         let local = convert(point, from: superview)
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
 private final class SettingsMenuViewController: NSViewController {
684
 private final class SettingsMenuViewController: NSViewController {
641
     private let palette: Palette
685
     private let palette: Palette
642
     private let typography: Typography
686
     private let typography: Typography
@@ -727,7 +771,7 @@ private final class SettingsMenuViewController: NSViewController {
727
     }
771
     }
728
 
772
 
729
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
773
     private func settingsDarkModeRow(enabled: Bool) -> NSView {
730
-        let row = RowHitTestView()
774
+        let row = HoverTrackingView()
731
         row.translatesAutoresizingMaskIntoConstraints = false
775
         row.translatesAutoresizingMaskIntoConstraints = false
732
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
776
         row.heightAnchor.constraint(equalToConstant: 44).isActive = true
733
 
777
 
@@ -751,6 +795,12 @@ private final class SettingsMenuViewController: NSViewController {
751
         row.addSubview(icon)
795
         row.addSubview(icon)
752
         row.addSubview(title)
796
         row.addSubview(title)
753
         row.addSubview(toggle)
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
         NSLayoutConstraint.activate([
805
         NSLayoutConstraint.activate([
756
             icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
806
             icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
@@ -767,7 +817,7 @@ private final class SettingsMenuViewController: NSViewController {
767
     }
817
     }
768
 
818
 
769
     private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
819
     private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
770
-        let row = RowHitTestView()
820
+        let row = HoverTrackingView()
771
         row.translatesAutoresizingMaskIntoConstraints = false
821
         row.translatesAutoresizingMaskIntoConstraints = false
772
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
822
         row.heightAnchor.constraint(equalToConstant: 42).isActive = true
773
 
823
 
@@ -794,6 +844,12 @@ private final class SettingsMenuViewController: NSViewController {
794
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
844
         let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
795
         row.addGestureRecognizer(click)
845
         row.addGestureRecognizer(click)
796
         row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
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
         return row
854
         return row
799
     }
855
     }
@@ -852,7 +908,7 @@ private extension ViewController {
852
     }
908
     }
853
 
909
 
854
     func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
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
         item.wantsLayer = true
912
         item.wantsLayer = true
857
         item.layer?.cornerRadius = 10
913
         item.layer?.cornerRadius = 10
858
         item.layer?.backgroundColor = NSColor.clear.cgColor
914
         item.layer?.backgroundColor = NSColor.clear.cgColor
@@ -910,6 +966,10 @@ private extension ViewController {
910
         NSLayoutConstraint.activate(constraints)
966
         NSLayoutConstraint.activate(constraints)
911
 
967
 
912
         applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
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
         let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
974
         let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
915
         item.addGestureRecognizer(click)
975
         item.addGestureRecognizer(click)
@@ -917,9 +977,10 @@ private extension ViewController {
917
         return item
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
         let selected = (page == selectedSidebarPage)
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
         let tint = selected ? NSColor.white : palette.textSecondary
984
         let tint = selected ? NSColor.white : palette.textSecondary
924
         guard item.subviews.count >= 2 else { return }
985
         guard item.subviews.count >= 2 else { return }
925
         let leading = item.subviews[0]
986
         let leading = item.subviews[0]
@@ -939,7 +1000,7 @@ private extension ViewController {
939
     }
1000
     }
940
 
1001
 
941
     func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
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
         tab.wantsLayer = true
1004
         tab.wantsLayer = true
944
         tab.layer?.cornerRadius = 19
1005
         tab.layer?.cornerRadius = 19
945
         tab.layer?.backgroundColor = NSColor.clear.cgColor
1006
         tab.layer?.backgroundColor = NSColor.clear.cgColor
@@ -981,6 +1042,10 @@ private extension ViewController {
981
         NSLayoutConstraint.activate(constraints)
1042
         NSLayoutConstraint.activate(constraints)
982
 
1043
 
983
         applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
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
         let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
1050
         let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
986
         tab.addGestureRecognizer(click)
1051
         tab.addGestureRecognizer(click)
@@ -988,9 +1053,10 @@ private extension ViewController {
988
         return tab
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
         let selected = (provider == selectedMeetingProvider)
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
         guard tab.subviews.count >= 2 else { return }
1060
         guard tab.subviews.count >= 2 else { return }
995
         let leading = tab.subviews[0]
1061
         let leading = tab.subviews[0]
996
         let title = tab.subviews[1] as? NSTextField
1062
         let title = tab.subviews[1] as? NSTextField
@@ -1006,7 +1072,10 @@ private extension ViewController {
1006
     }
1072
     }
1007
 
1073
 
1008
     func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
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
         button.translatesAutoresizingMaskIntoConstraints = false
1079
         button.translatesAutoresizingMaskIntoConstraints = false
1011
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
1080
         button.widthAnchor.constraint(equalToConstant: width).isActive = true
1012
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
1081
         button.heightAnchor.constraint(equalToConstant: 36).isActive = true
@@ -1022,11 +1091,21 @@ private extension ViewController {
1022
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
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
         return button
1101
         return button
1026
     }
1102
     }
1027
 
1103
 
1028
     func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
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
         button.translatesAutoresizingMaskIntoConstraints = false
1109
         button.translatesAutoresizingMaskIntoConstraints = false
1031
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
1110
         button.widthAnchor.constraint(equalToConstant: size).isActive = true
1032
         button.heightAnchor.constraint(equalToConstant: size).isActive = true
1111
         button.heightAnchor.constraint(equalToConstant: size).isActive = true
@@ -1039,6 +1118,13 @@ private extension ViewController {
1039
             label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
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
         return button
1128
         return button
1043
     }
1129
     }
1044
 }
1130
 }