|
|
@@ -66,6 +66,7 @@ final class WavePatternView: NSView, AppearanceRefreshable {
|
|
|
final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
|
enum Style {
|
|
|
case normal
|
|
|
+ case hovered
|
|
|
case active
|
|
|
}
|
|
|
|
|
|
@@ -80,9 +81,11 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
|
private let iconView = NSImageView()
|
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
|
private let container = NSView()
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+ private var isHovered = false
|
|
|
|
|
|
var isSelected: Bool = false {
|
|
|
- didSet { applyStyle(isSelected ? .active : .normal) }
|
|
|
+ didSet { updateAppearance() }
|
|
|
}
|
|
|
|
|
|
init(title: String, symbolName: String, isSelected: Bool = false, accent: Accent = .standard) {
|
|
|
@@ -128,14 +131,29 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
|
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
|
|
|
])
|
|
|
|
|
|
- applyStyle(isSelected ? .active : .normal)
|
|
|
+ updateAppearance()
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.isHovered = hovering
|
|
|
+ self?.updateAppearance()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
|
|
func refreshAppearance() {
|
|
|
- applyStyle(isSelected ? .active : .normal)
|
|
|
+ updateAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppearance() {
|
|
|
+ if isSelected {
|
|
|
+ applyStyle(.active)
|
|
|
+ } else if isHovered {
|
|
|
+ applyStyle(.hovered)
|
|
|
+ } else {
|
|
|
+ applyStyle(.normal)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func applyStyle(_ style: Style) {
|
|
|
@@ -144,18 +162,24 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
|
|
|
|
let activeBackground = accent == .premium ? AppTheme.premiumBackground : AppTheme.homeActiveBackground
|
|
|
let activeForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.homeActiveForeground
|
|
|
-
|
|
|
+ let hoverBackground = accent == .premium ? AppTheme.premiumHoverBackground : AppTheme.sidebarHoverBackground
|
|
|
let normalForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.textPrimary
|
|
|
|
|
|
- switch style {
|
|
|
- case .normal:
|
|
|
- container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
- titleLabel.textColor = normalForeground
|
|
|
- iconView.contentTintColor = normalForeground
|
|
|
- case .active:
|
|
|
- container.layer?.backgroundColor = activeBackground.cgColor
|
|
|
- titleLabel.textColor = activeForeground
|
|
|
- iconView.contentTintColor = activeForeground
|
|
|
+ animateHover {
|
|
|
+ switch style {
|
|
|
+ case .normal:
|
|
|
+ container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
+ titleLabel.textColor = normalForeground
|
|
|
+ iconView.contentTintColor = normalForeground
|
|
|
+ case .hovered:
|
|
|
+ container.layer?.backgroundColor = hoverBackground.cgColor
|
|
|
+ titleLabel.textColor = normalForeground
|
|
|
+ iconView.contentTintColor = normalForeground
|
|
|
+ case .active:
|
|
|
+ container.layer?.backgroundColor = activeBackground.cgColor
|
|
|
+ titleLabel.textColor = activeForeground
|
|
|
+ iconView.contentTintColor = activeForeground
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -173,8 +197,11 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
|
|
|
|
final class PillButton: NSButton {
|
|
|
private let horizontalPadding: CGFloat = 16
|
|
|
+ private let baseColor: NSColor
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
|
|
|
init(title: String, color: NSColor) {
|
|
|
+ baseColor = color
|
|
|
super.init(frame: .zero)
|
|
|
self.title = title
|
|
|
isBordered = false
|
|
|
@@ -192,6 +219,20 @@ final class PillButton: NSButton {
|
|
|
}
|
|
|
|
|
|
heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ let color = hovering ? baseColor.blended(withFraction: 0.15, of: .black) ?? baseColor : baseColor
|
|
|
+ animateHover {
|
|
|
+ layer?.backgroundColor = color.cgColor
|
|
|
+ layer?.transform = hovering
|
|
|
+ ? CATransform3DMakeScale(1.03, 1.03, 1)
|
|
|
+ : CATransform3DIdentity
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
@@ -285,19 +326,22 @@ struct QuickStartCardData {
|
|
|
|
|
|
final class QuickStartCardView: NSView {
|
|
|
private let iconView: QuickStartIconView
|
|
|
+ private let gradientView: GradientCardView
|
|
|
private var iconWidthConstraint: NSLayoutConstraint!
|
|
|
private var iconHeightConstraint: NSLayoutConstraint!
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
|
|
|
init(data: QuickStartCardData) {
|
|
|
iconView = QuickStartIconView(kind: data.iconKind)
|
|
|
- super.init(frame: .zero)
|
|
|
- translatesAutoresizingMaskIntoConstraints = false
|
|
|
-
|
|
|
- let gradient = GradientCardView(
|
|
|
+ gradientView = GradientCardView(
|
|
|
colors: data.gradientColors,
|
|
|
startPoint: CGPoint(x: 0, y: 0.5),
|
|
|
endPoint: CGPoint(x: 1, y: 0.5)
|
|
|
)
|
|
|
+ super.init(frame: .zero)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let gradient = gradientView
|
|
|
gradient.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
let titleLabel = NSTextField(labelWithString: data.title)
|
|
|
@@ -344,11 +388,19 @@ final class QuickStartCardView: NSView {
|
|
|
iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
|
|
|
iconWidthConstraint.isActive = true
|
|
|
iconHeightConstraint.isActive = true
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ applyHoverLift(hovering, on: gradientView.layer)
|
|
|
+ }
|
|
|
+
|
|
|
override func layout() {
|
|
|
super.layout()
|
|
|
let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
|
|
|
@@ -357,6 +409,10 @@ final class QuickStartCardView: NSView {
|
|
|
iconHeightConstraint.constant = size
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ override func resetCursorRects() {
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// MARK: - Feature Card
|
|
|
@@ -374,6 +430,8 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
|
private let arrowButton: NSButton
|
|
|
private var iconWidthConstraint: NSLayoutConstraint!
|
|
|
private var iconHeightConstraint: NSLayoutConstraint!
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+ private var isHovered = false
|
|
|
|
|
|
init(data: FeatureCardData) {
|
|
|
iconView = FeatureIconView(kind: data.iconKind)
|
|
|
@@ -432,11 +490,20 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
|
iconWidthConstraint.isActive = true
|
|
|
iconHeightConstraint.isActive = true
|
|
|
refreshAppearance()
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ isHovered = hovering
|
|
|
+ applyHoverLift(hovering)
|
|
|
+ }
|
|
|
+
|
|
|
func refreshAppearance() {
|
|
|
layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
titleLabel.refreshThemeLabelColor()
|
|
|
@@ -444,6 +511,9 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
|
arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
|
|
|
arrowButton.layer?.borderColor = AppTheme.border.cgColor
|
|
|
arrowButton.contentTintColor = AppTheme.textSecondary
|
|
|
+ if isHovered {
|
|
|
+ applyHoverLift(true)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
override func layout() {
|