|
|
@@ -66,6 +66,7 @@ final class WavePatternView: NSView, AppearanceRefreshable {
|
|
66
|
66
|
final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
67
|
67
|
enum Style {
|
|
68
|
68
|
case normal
|
|
|
69
|
+ case hovered
|
|
69
|
70
|
case active
|
|
70
|
71
|
}
|
|
71
|
72
|
|
|
|
@@ -80,9 +81,11 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
80
|
81
|
private let iconView = NSImageView()
|
|
81
|
82
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
82
|
83
|
private let container = NSView()
|
|
|
84
|
+ private var hoverTracker: HoverTracker?
|
|
|
85
|
+ private var isHovered = false
|
|
83
|
86
|
|
|
84
|
87
|
var isSelected: Bool = false {
|
|
85
|
|
- didSet { applyStyle(isSelected ? .active : .normal) }
|
|
|
88
|
+ didSet { updateAppearance() }
|
|
86
|
89
|
}
|
|
87
|
90
|
|
|
88
|
91
|
init(title: String, symbolName: String, isSelected: Bool = false, accent: Accent = .standard) {
|
|
|
@@ -128,14 +131,29 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
128
|
131
|
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
|
|
129
|
132
|
])
|
|
130
|
133
|
|
|
131
|
|
- applyStyle(isSelected ? .active : .normal)
|
|
|
134
|
+ updateAppearance()
|
|
|
135
|
+
|
|
|
136
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
137
|
+ self?.isHovered = hovering
|
|
|
138
|
+ self?.updateAppearance()
|
|
|
139
|
+ }
|
|
132
|
140
|
}
|
|
133
|
141
|
|
|
134
|
142
|
@available(*, unavailable)
|
|
135
|
143
|
required init?(coder: NSCoder) { nil }
|
|
136
|
144
|
|
|
137
|
145
|
func refreshAppearance() {
|
|
138
|
|
- applyStyle(isSelected ? .active : .normal)
|
|
|
146
|
+ updateAppearance()
|
|
|
147
|
+ }
|
|
|
148
|
+
|
|
|
149
|
+ private func updateAppearance() {
|
|
|
150
|
+ if isSelected {
|
|
|
151
|
+ applyStyle(.active)
|
|
|
152
|
+ } else if isHovered {
|
|
|
153
|
+ applyStyle(.hovered)
|
|
|
154
|
+ } else {
|
|
|
155
|
+ applyStyle(.normal)
|
|
|
156
|
+ }
|
|
139
|
157
|
}
|
|
140
|
158
|
|
|
141
|
159
|
private func applyStyle(_ style: Style) {
|
|
|
@@ -144,18 +162,24 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
144
|
162
|
|
|
145
|
163
|
let activeBackground = accent == .premium ? AppTheme.premiumBackground : AppTheme.homeActiveBackground
|
|
146
|
164
|
let activeForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.homeActiveForeground
|
|
147
|
|
-
|
|
|
165
|
+ let hoverBackground = accent == .premium ? AppTheme.premiumHoverBackground : AppTheme.sidebarHoverBackground
|
|
148
|
166
|
let normalForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.textPrimary
|
|
149
|
167
|
|
|
150
|
|
- switch style {
|
|
151
|
|
- case .normal:
|
|
152
|
|
- container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
153
|
|
- titleLabel.textColor = normalForeground
|
|
154
|
|
- iconView.contentTintColor = normalForeground
|
|
155
|
|
- case .active:
|
|
156
|
|
- container.layer?.backgroundColor = activeBackground.cgColor
|
|
157
|
|
- titleLabel.textColor = activeForeground
|
|
158
|
|
- iconView.contentTintColor = activeForeground
|
|
|
168
|
+ animateHover {
|
|
|
169
|
+ switch style {
|
|
|
170
|
+ case .normal:
|
|
|
171
|
+ container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
172
|
+ titleLabel.textColor = normalForeground
|
|
|
173
|
+ iconView.contentTintColor = normalForeground
|
|
|
174
|
+ case .hovered:
|
|
|
175
|
+ container.layer?.backgroundColor = hoverBackground.cgColor
|
|
|
176
|
+ titleLabel.textColor = normalForeground
|
|
|
177
|
+ iconView.contentTintColor = normalForeground
|
|
|
178
|
+ case .active:
|
|
|
179
|
+ container.layer?.backgroundColor = activeBackground.cgColor
|
|
|
180
|
+ titleLabel.textColor = activeForeground
|
|
|
181
|
+ iconView.contentTintColor = activeForeground
|
|
|
182
|
+ }
|
|
159
|
183
|
}
|
|
160
|
184
|
}
|
|
161
|
185
|
|
|
|
@@ -173,8 +197,11 @@ final class SidebarNavItem: NSControl, AppearanceRefreshable {
|
|
173
|
197
|
|
|
174
|
198
|
final class PillButton: NSButton {
|
|
175
|
199
|
private let horizontalPadding: CGFloat = 16
|
|
|
200
|
+ private let baseColor: NSColor
|
|
|
201
|
+ private var hoverTracker: HoverTracker?
|
|
176
|
202
|
|
|
177
|
203
|
init(title: String, color: NSColor) {
|
|
|
204
|
+ baseColor = color
|
|
178
|
205
|
super.init(frame: .zero)
|
|
179
|
206
|
self.title = title
|
|
180
|
207
|
isBordered = false
|
|
|
@@ -192,6 +219,20 @@ final class PillButton: NSButton {
|
|
192
|
219
|
}
|
|
193
|
220
|
|
|
194
|
221
|
heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
222
|
+
|
|
|
223
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
224
|
+ self?.setHovered(hovering)
|
|
|
225
|
+ }
|
|
|
226
|
+ }
|
|
|
227
|
+
|
|
|
228
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
229
|
+ let color = hovering ? baseColor.blended(withFraction: 0.15, of: .black) ?? baseColor : baseColor
|
|
|
230
|
+ animateHover {
|
|
|
231
|
+ layer?.backgroundColor = color.cgColor
|
|
|
232
|
+ layer?.transform = hovering
|
|
|
233
|
+ ? CATransform3DMakeScale(1.03, 1.03, 1)
|
|
|
234
|
+ : CATransform3DIdentity
|
|
|
235
|
+ }
|
|
195
|
236
|
}
|
|
196
|
237
|
|
|
197
|
238
|
@available(*, unavailable)
|
|
|
@@ -285,19 +326,22 @@ struct QuickStartCardData {
|
|
285
|
326
|
|
|
286
|
327
|
final class QuickStartCardView: NSView {
|
|
287
|
328
|
private let iconView: QuickStartIconView
|
|
|
329
|
+ private let gradientView: GradientCardView
|
|
288
|
330
|
private var iconWidthConstraint: NSLayoutConstraint!
|
|
289
|
331
|
private var iconHeightConstraint: NSLayoutConstraint!
|
|
|
332
|
+ private var hoverTracker: HoverTracker?
|
|
290
|
333
|
|
|
291
|
334
|
init(data: QuickStartCardData) {
|
|
292
|
335
|
iconView = QuickStartIconView(kind: data.iconKind)
|
|
293
|
|
- super.init(frame: .zero)
|
|
294
|
|
- translatesAutoresizingMaskIntoConstraints = false
|
|
295
|
|
-
|
|
296
|
|
- let gradient = GradientCardView(
|
|
|
336
|
+ gradientView = GradientCardView(
|
|
297
|
337
|
colors: data.gradientColors,
|
|
298
|
338
|
startPoint: CGPoint(x: 0, y: 0.5),
|
|
299
|
339
|
endPoint: CGPoint(x: 1, y: 0.5)
|
|
300
|
340
|
)
|
|
|
341
|
+ super.init(frame: .zero)
|
|
|
342
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
343
|
+
|
|
|
344
|
+ let gradient = gradientView
|
|
301
|
345
|
gradient.translatesAutoresizingMaskIntoConstraints = false
|
|
302
|
346
|
|
|
303
|
347
|
let titleLabel = NSTextField(labelWithString: data.title)
|
|
|
@@ -344,11 +388,19 @@ final class QuickStartCardView: NSView {
|
|
344
|
388
|
iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
|
|
345
|
389
|
iconWidthConstraint.isActive = true
|
|
346
|
390
|
iconHeightConstraint.isActive = true
|
|
|
391
|
+
|
|
|
392
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
393
|
+ self?.setHovered(hovering)
|
|
|
394
|
+ }
|
|
347
|
395
|
}
|
|
348
|
396
|
|
|
349
|
397
|
@available(*, unavailable)
|
|
350
|
398
|
required init?(coder: NSCoder) { nil }
|
|
351
|
399
|
|
|
|
400
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
401
|
+ applyHoverLift(hovering, on: gradientView.layer)
|
|
|
402
|
+ }
|
|
|
403
|
+
|
|
352
|
404
|
override func layout() {
|
|
353
|
405
|
super.layout()
|
|
354
|
406
|
let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
|
|
|
@@ -357,6 +409,10 @@ final class QuickStartCardView: NSView {
|
|
357
|
409
|
iconHeightConstraint.constant = size
|
|
358
|
410
|
}
|
|
359
|
411
|
}
|
|
|
412
|
+
|
|
|
413
|
+ override func resetCursorRects() {
|
|
|
414
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
415
|
+ }
|
|
360
|
416
|
}
|
|
361
|
417
|
|
|
362
|
418
|
// MARK: - Feature Card
|
|
|
@@ -374,6 +430,8 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
374
|
430
|
private let arrowButton: NSButton
|
|
375
|
431
|
private var iconWidthConstraint: NSLayoutConstraint!
|
|
376
|
432
|
private var iconHeightConstraint: NSLayoutConstraint!
|
|
|
433
|
+ private var hoverTracker: HoverTracker?
|
|
|
434
|
+ private var isHovered = false
|
|
377
|
435
|
|
|
378
|
436
|
init(data: FeatureCardData) {
|
|
379
|
437
|
iconView = FeatureIconView(kind: data.iconKind)
|
|
|
@@ -432,11 +490,20 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
432
|
490
|
iconWidthConstraint.isActive = true
|
|
433
|
491
|
iconHeightConstraint.isActive = true
|
|
434
|
492
|
refreshAppearance()
|
|
|
493
|
+
|
|
|
494
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
495
|
+ self?.setHovered(hovering)
|
|
|
496
|
+ }
|
|
435
|
497
|
}
|
|
436
|
498
|
|
|
437
|
499
|
@available(*, unavailable)
|
|
438
|
500
|
required init?(coder: NSCoder) { nil }
|
|
439
|
501
|
|
|
|
502
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
503
|
+ isHovered = hovering
|
|
|
504
|
+ applyHoverLift(hovering)
|
|
|
505
|
+ }
|
|
|
506
|
+
|
|
440
|
507
|
func refreshAppearance() {
|
|
441
|
508
|
layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
442
|
509
|
titleLabel.refreshThemeLabelColor()
|
|
|
@@ -444,6 +511,9 @@ final class FeatureCardView: NSView, AppearanceRefreshable {
|
|
444
|
511
|
arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
|
|
445
|
512
|
arrowButton.layer?.borderColor = AppTheme.border.cgColor
|
|
446
|
513
|
arrowButton.contentTintColor = AppTheme.textSecondary
|
|
|
514
|
+ if isHovered {
|
|
|
515
|
+ applyHoverLift(true)
|
|
|
516
|
+ }
|
|
447
|
517
|
}
|
|
448
|
518
|
|
|
449
|
519
|
override func layout() {
|