UIComponents.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. import Cocoa
  2. // MARK: - Content Panel
  3. final class ContentPanelView: NSView, AppearanceRefreshable {
  4. init(cornerRadius: CGFloat = AppTheme.contentPanelCornerRadius) {
  5. super.init(frame: .zero)
  6. wantsLayer = true
  7. layer?.cornerRadius = cornerRadius
  8. layer?.borderWidth = 1.5
  9. applyCardShadow()
  10. refreshAppearance()
  11. }
  12. @available(*, unavailable)
  13. required init?(coder: NSCoder) { nil }
  14. func refreshAppearance() {
  15. layer?.backgroundColor = AppTheme.cardBackground.cgColor
  16. layer?.borderColor = AppTheme.paywallBorder.cgColor
  17. }
  18. }
  19. // MARK: - Gradient Card View
  20. final class GradientCardView: NSView {
  21. private let gradientLayer = CAGradientLayer()
  22. init(colors: [NSColor], startPoint: CGPoint = CGPoint(x: 0, y: 0.5), endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
  23. super.init(frame: .zero)
  24. wantsLayer = true
  25. gradientLayer.colors = colors.map { $0.cgColor }
  26. gradientLayer.startPoint = startPoint
  27. gradientLayer.endPoint = endPoint
  28. gradientLayer.cornerRadius = AppTheme.cardCornerRadius
  29. layer?.addSublayer(gradientLayer)
  30. applyCardShadow()
  31. }
  32. @available(*, unavailable)
  33. required init?(coder: NSCoder) { nil }
  34. override func layout() {
  35. super.layout()
  36. gradientLayer.frame = bounds
  37. gradientLayer.cornerRadius = AppTheme.cardCornerRadius
  38. }
  39. }
  40. // MARK: - Wave Pattern
  41. final class WavePatternView: NSView, AppearanceRefreshable {
  42. var fixedDotColor: NSColor?
  43. override var isOpaque: Bool { false }
  44. func refreshAppearance() {
  45. guard fixedDotColor == nil else { return }
  46. needsDisplay = true
  47. }
  48. override func draw(_ dirtyRect: NSRect) {
  49. super.draw(dirtyRect)
  50. guard let context = NSGraphicsContext.current?.cgContext else { return }
  51. context.setFillColor((fixedDotColor ?? AppTheme.waveDotColor).cgColor)
  52. let spacing: CGFloat = 14
  53. let dotSize: CGFloat = 3
  54. let rows = Int(bounds.height / spacing) + 2
  55. let cols = Int(bounds.width / spacing) + 2
  56. for row in 0..<rows {
  57. for col in 0..<cols {
  58. let wave = sin(Double(col) * 0.35 + Double(row) * 0.25) * 8
  59. let x = CGFloat(col) * spacing + CGFloat(wave)
  60. let y = CGFloat(row) * spacing
  61. let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize)
  62. context.fillEllipse(in: rect)
  63. }
  64. }
  65. }
  66. }
  67. // MARK: - Sidebar Nav Item
  68. final class SidebarNavItem: NSControl, AppearanceRefreshable {
  69. enum Style {
  70. case normal
  71. case hovered
  72. case active
  73. }
  74. enum Accent {
  75. case standard
  76. case premium
  77. }
  78. var onClick: (() -> Void)?
  79. private let accent: Accent
  80. private let iconView = NSImageView()
  81. private let titleLabel = NSTextField(labelWithString: "")
  82. private let container = NSView()
  83. private var hoverTracker: HoverTracker?
  84. private var isHovered = false
  85. var isSelected: Bool = false {
  86. didSet { updateAppearance() }
  87. }
  88. init(title: String, symbolName: String, isSelected: Bool = false, accent: Accent = .standard) {
  89. self.accent = accent
  90. super.init(frame: .zero)
  91. self.isSelected = isSelected
  92. titleLabel.stringValue = title
  93. titleLabel.font = AppTheme.mediumFont(size: 14)
  94. titleLabel.themeLabelStyle = .primary
  95. titleLabel.textColor = AppTheme.textPrimary
  96. if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
  97. let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
  98. iconView.image = image.withSymbolConfiguration(config)
  99. }
  100. iconView.contentTintColor = AppTheme.textPrimary
  101. container.translatesAutoresizingMaskIntoConstraints = false
  102. iconView.translatesAutoresizingMaskIntoConstraints = false
  103. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  104. translatesAutoresizingMaskIntoConstraints = false
  105. addSubview(container)
  106. container.addSubview(iconView)
  107. container.addSubview(titleLabel)
  108. NSLayoutConstraint.activate([
  109. heightAnchor.constraint(equalToConstant: 44),
  110. container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
  111. container.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
  112. container.topAnchor.constraint(equalTo: topAnchor),
  113. container.bottomAnchor.constraint(equalTo: bottomAnchor),
  114. iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
  115. iconView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  116. iconView.widthAnchor.constraint(equalToConstant: 20),
  117. iconView.heightAnchor.constraint(equalToConstant: 20),
  118. titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
  119. titleLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  120. titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
  121. ])
  122. updateAppearance()
  123. hoverTracker = HoverTracker(view: self) { [weak self] hovering in
  124. self?.isHovered = hovering
  125. self?.updateAppearance()
  126. }
  127. }
  128. @available(*, unavailable)
  129. required init?(coder: NSCoder) { nil }
  130. func refreshAppearance() {
  131. updateAppearance()
  132. }
  133. private func updateAppearance() {
  134. if isSelected {
  135. applyStyle(.active)
  136. } else if isHovered {
  137. applyStyle(.hovered)
  138. } else {
  139. applyStyle(.normal)
  140. }
  141. }
  142. private func applyStyle(_ style: Style) {
  143. container.wantsLayer = true
  144. container.layer?.cornerRadius = AppTheme.cornerRadius
  145. let activeBackground = accent == .premium ? AppTheme.premiumBackground : AppTheme.homeActiveBackground
  146. let activeForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.homeActiveForeground
  147. let hoverBackground = accent == .premium ? AppTheme.premiumHoverBackground : AppTheme.sidebarHoverBackground
  148. let normalForeground = accent == .premium ? AppTheme.premiumForeground : AppTheme.textPrimary
  149. animateHover {
  150. switch style {
  151. case .normal:
  152. container.layer?.backgroundColor = NSColor.clear.cgColor
  153. titleLabel.textColor = normalForeground
  154. iconView.contentTintColor = normalForeground
  155. case .hovered:
  156. container.layer?.backgroundColor = hoverBackground.cgColor
  157. titleLabel.textColor = normalForeground
  158. iconView.contentTintColor = normalForeground
  159. case .active:
  160. container.layer?.backgroundColor = activeBackground.cgColor
  161. titleLabel.textColor = activeForeground
  162. iconView.contentTintColor = activeForeground
  163. }
  164. }
  165. }
  166. override func mouseUp(with event: NSEvent) {
  167. guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
  168. onClick?()
  169. }
  170. override func resetCursorRects() {
  171. addCursorRect(bounds, cursor: .pointingHand)
  172. }
  173. }
  174. // MARK: - Action Button
  175. final class PillButton: NSButton {
  176. private let horizontalPadding: CGFloat = 16
  177. private let baseColor: NSColor
  178. private var hoverTracker: HoverTracker?
  179. private let clickTarget: ButtonClickTarget
  180. var onClick: (() -> Void)? {
  181. get { clickTarget.handler }
  182. set { clickTarget.handler = newValue }
  183. }
  184. init(title: String, color: NSColor) {
  185. clickTarget = ButtonClickTarget()
  186. baseColor = color
  187. super.init(frame: .zero)
  188. self.title = title
  189. isBordered = false
  190. wantsLayer = true
  191. layer?.backgroundColor = color.cgColor
  192. layer?.cornerRadius = 11
  193. font = AppTheme.semiboldFont(size: 13)
  194. contentTintColor = .white
  195. target = clickTarget
  196. action = #selector(ButtonClickTarget.activate)
  197. if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
  198. let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
  199. image = arrow.withSymbolConfiguration(config)
  200. imagePosition = .imageTrailing
  201. imageHugsTitle = true
  202. }
  203. heightAnchor.constraint(equalToConstant: 40).isActive = true
  204. hoverTracker = HoverTracker(view: self) { [weak self] hovering in
  205. self?.setHovered(hovering)
  206. }
  207. }
  208. private func setHovered(_ hovering: Bool) {
  209. let color = hovering ? baseColor.blended(withFraction: 0.15, of: .black) ?? baseColor : baseColor
  210. animateHover {
  211. layer?.backgroundColor = color.cgColor
  212. layer?.transform = hovering
  213. ? CATransform3DMakeScale(1.03, 1.03, 1)
  214. : CATransform3DIdentity
  215. }
  216. }
  217. @available(*, unavailable)
  218. required init?(coder: NSCoder) { nil }
  219. override var intrinsicContentSize: NSSize {
  220. var size = super.intrinsicContentSize
  221. size.width += horizontalPadding * 2
  222. return size
  223. }
  224. override func draw(_ dirtyRect: NSRect) {
  225. let originalBounds = bounds
  226. defer { bounds = originalBounds }
  227. bounds = originalBounds.insetBy(dx: horizontalPadding, dy: 0)
  228. super.draw(dirtyRect)
  229. }
  230. override func resetCursorRects() {
  231. addCursorRect(bounds, cursor: .pointingHand)
  232. }
  233. }
  234. // MARK: - Sparkle Icon
  235. final class SparkleIconView: NSView {
  236. var color: NSColor = AppTheme.blue
  237. override var isFlipped: Bool { true }
  238. override var isOpaque: Bool { false }
  239. override func draw(_ dirtyRect: NSRect) {
  240. super.draw(dirtyRect)
  241. guard let context = NSGraphicsContext.current?.cgContext else { return }
  242. let s = min(bounds.width, bounds.height)
  243. let cx = bounds.midX
  244. let cy = bounds.midY
  245. let tipR = s * 0.34
  246. let valleyR = s * 0.09
  247. let tips = [
  248. CGPoint(x: cx, y: cy - tipR),
  249. CGPoint(x: cx + tipR, y: cy),
  250. CGPoint(x: cx, y: cy + tipR),
  251. CGPoint(x: cx - tipR, y: cy),
  252. ]
  253. let valleys = [
  254. CGPoint(x: cx + valleyR, y: cy - valleyR),
  255. CGPoint(x: cx + valleyR, y: cy + valleyR),
  256. CGPoint(x: cx - valleyR, y: cy + valleyR),
  257. CGPoint(x: cx - valleyR, y: cy - valleyR),
  258. ]
  259. let starPath = CGMutablePath()
  260. starPath.move(to: tips[0])
  261. for i in 0..<4 {
  262. starPath.addQuadCurve(to: tips[(i + 1) % 4], control: valleys[i])
  263. }
  264. starPath.closeSubpath()
  265. context.setFillColor(color.cgColor)
  266. context.addPath(starPath)
  267. context.fillPath()
  268. let dotR = s * 0.052
  269. let dotOffset = s * 0.30
  270. let dotPositions = [
  271. CGPoint(x: cx + dotOffset, y: cy - dotOffset),
  272. CGPoint(x: cx + dotOffset, y: cy + dotOffset),
  273. CGPoint(x: cx - dotOffset, y: cy + dotOffset),
  274. CGPoint(x: cx - dotOffset, y: cy - dotOffset),
  275. ]
  276. for pos in dotPositions {
  277. context.fillEllipse(in: CGRect(x: pos.x - dotR, y: pos.y - dotR, width: dotR * 2, height: dotR * 2))
  278. }
  279. }
  280. }
  281. // MARK: - Quick Start Card
  282. struct QuickStartCardData {
  283. let title: String
  284. let subtitle: String
  285. let buttonTitle: String
  286. let accentColor: NSColor
  287. let gradientColors: [NSColor]
  288. let iconKind: QuickStartIconKind
  289. }
  290. final class QuickStartCardView: NSView, AppearanceRefreshable {
  291. private let iconView: QuickStartIconView
  292. private let gradientView: GradientCardView
  293. private var iconWidthConstraint: NSLayoutConstraint!
  294. private var iconHeightConstraint: NSLayoutConstraint!
  295. private var hoverTracker: HoverTracker?
  296. private var isHovered = false
  297. private let onActivate: () -> Void
  298. init(data: QuickStartCardData, onActivate: @escaping () -> Void) {
  299. self.onActivate = onActivate
  300. iconView = QuickStartIconView(kind: data.iconKind)
  301. gradientView = GradientCardView(
  302. colors: data.gradientColors,
  303. startPoint: CGPoint(x: 0, y: 0.5),
  304. endPoint: CGPoint(x: 1, y: 0.5)
  305. )
  306. super.init(frame: .zero)
  307. translatesAutoresizingMaskIntoConstraints = false
  308. let gradient = gradientView
  309. gradient.translatesAutoresizingMaskIntoConstraints = false
  310. let titleLabel = NSTextField(labelWithString: data.title)
  311. titleLabel.font = AppTheme.semiboldFont(size: 22)
  312. titleLabel.textColor = data.accentColor
  313. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  314. let subtitleLabel = NSTextField(labelWithString: data.subtitle)
  315. subtitleLabel.font = AppTheme.regularFont(size: 13)
  316. subtitleLabel.textColor = AppTheme.quickStartCardSubtitle
  317. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  318. let button = PillButton(title: data.buttonTitle, color: data.accentColor)
  319. button.translatesAutoresizingMaskIntoConstraints = false
  320. button.onClick = onActivate
  321. addSubview(gradient)
  322. gradient.addSubview(titleLabel)
  323. gradient.addSubview(subtitleLabel)
  324. gradient.addSubview(button)
  325. gradient.addSubview(iconView)
  326. NSLayoutConstraint.activate([
  327. gradient.leadingAnchor.constraint(equalTo: leadingAnchor),
  328. gradient.trailingAnchor.constraint(equalTo: trailingAnchor),
  329. gradient.topAnchor.constraint(equalTo: topAnchor),
  330. gradient.bottomAnchor.constraint(equalTo: bottomAnchor),
  331. heightAnchor.constraint(equalToConstant: 148),
  332. titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 18),
  333. titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 24),
  334. subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  335. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
  336. subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -8),
  337. button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  338. button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -20),
  339. iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -8),
  340. iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor),
  341. ])
  342. iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
  343. iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
  344. iconWidthConstraint.isActive = true
  345. iconHeightConstraint.isActive = true
  346. hoverTracker = HoverTracker(view: self) { [weak self] hovering in
  347. self?.setHovered(hovering)
  348. }
  349. refreshAppearance()
  350. }
  351. @available(*, unavailable)
  352. required init?(coder: NSCoder) { nil }
  353. func refreshAppearance() {
  354. gradientView.layer?.cornerRadius = AppTheme.cardCornerRadius
  355. gradientView.layer?.borderWidth = isHovered ? 2 : 1.5
  356. gradientView.layer?.borderColor = AppTheme.paywallBorder.cgColor
  357. if isHovered {
  358. applyHoverLift(true, on: gradientView.layer)
  359. }
  360. }
  361. private func setHovered(_ hovering: Bool) {
  362. isHovered = hovering
  363. applyHoverLift(hovering, on: gradientView.layer)
  364. gradientView.layer?.borderWidth = hovering ? 2 : 1.5
  365. }
  366. override func layout() {
  367. super.layout()
  368. let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
  369. if iconWidthConstraint.constant != size {
  370. iconWidthConstraint.constant = size
  371. iconHeightConstraint.constant = size
  372. }
  373. }
  374. override func mouseUp(with event: NSEvent) {
  375. let location = convert(event.locationInWindow, from: nil)
  376. guard bounds.contains(location) else { return }
  377. onActivate()
  378. }
  379. override func resetCursorRects() {
  380. addCursorRect(bounds, cursor: .pointingHand)
  381. }
  382. }
  383. private final class ButtonClickTarget: NSObject {
  384. var handler: (() -> Void)?
  385. @objc func activate() {
  386. handler?()
  387. }
  388. }
  389. // MARK: - Feature Card
  390. struct FeatureCardData {
  391. let title: String
  392. let subtitle: String
  393. let iconKind: FeatureIconKind
  394. var onActivate: (() -> Void)? = nil
  395. }
  396. final class FeatureCardView: NSView, AppearanceRefreshable {
  397. private let iconView: FeatureIconView
  398. private let titleLabel: NSTextField
  399. private let subtitleLabel: NSTextField
  400. private let arrowButton: NSButton
  401. private var iconWidthConstraint: NSLayoutConstraint!
  402. private var iconHeightConstraint: NSLayoutConstraint!
  403. private var hoverTracker: HoverTracker?
  404. private var isHovered = false
  405. private let onActivate: (() -> Void)?
  406. init(data: FeatureCardData) {
  407. onActivate = data.onActivate
  408. iconView = FeatureIconView(kind: data.iconKind)
  409. titleLabel = NSTextField.themeLabel(data.title, style: .primary, font: AppTheme.semiboldFont(size: 14))
  410. subtitleLabel = NSTextField.themeLabel(data.subtitle, style: .secondary, font: AppTheme.regularFont(size: 11))
  411. arrowButton = NSButton()
  412. super.init(frame: .zero)
  413. translatesAutoresizingMaskIntoConstraints = false
  414. setContentHuggingPriority(.defaultLow, for: .horizontal)
  415. setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  416. wantsLayer = true
  417. layer?.cornerRadius = AppTheme.featureCardCornerRadius
  418. applyCardShadow()
  419. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  420. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  421. arrowButton.isBordered = false
  422. arrowButton.wantsLayer = true
  423. arrowButton.layer?.cornerRadius = 13
  424. arrowButton.layer?.borderWidth = 1.5
  425. arrowButton.translatesAutoresizingMaskIntoConstraints = false
  426. if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
  427. let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
  428. arrowButton.image = arrow.withSymbolConfiguration(config)
  429. }
  430. arrowButton.contentTintColor = AppTheme.textSecondary
  431. addSubview(iconView)
  432. addSubview(titleLabel)
  433. addSubview(subtitleLabel)
  434. addSubview(arrowButton)
  435. NSLayoutConstraint.activate([
  436. heightAnchor.constraint(equalToConstant: 96),
  437. iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
  438. iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
  439. titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
  440. titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
  441. titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
  442. subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  443. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
  444. subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
  445. arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
  446. arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
  447. arrowButton.widthAnchor.constraint(equalToConstant: 26),
  448. arrowButton.heightAnchor.constraint(equalToConstant: 26),
  449. ])
  450. iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
  451. iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
  452. iconWidthConstraint.isActive = true
  453. iconHeightConstraint.isActive = true
  454. refreshAppearance()
  455. hoverTracker = HoverTracker(view: self) { [weak self] hovering in
  456. self?.setHovered(hovering)
  457. }
  458. }
  459. @available(*, unavailable)
  460. required init?(coder: NSCoder) { nil }
  461. private func setHovered(_ hovering: Bool) {
  462. isHovered = hovering
  463. applyHoverLift(hovering)
  464. let borderWidth: CGFloat = hovering ? 2 : 1.5
  465. layer?.borderWidth = borderWidth
  466. arrowButton.layer?.borderWidth = borderWidth
  467. }
  468. func refreshAppearance() {
  469. layer?.backgroundColor = AppTheme.cardBackground.cgColor
  470. layer?.borderWidth = isHovered ? 2 : 1.5
  471. layer?.borderColor = AppTheme.paywallBorder.cgColor
  472. titleLabel.refreshThemeLabelColor()
  473. subtitleLabel.refreshThemeLabelColor()
  474. arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
  475. arrowButton.layer?.borderWidth = isHovered ? 2 : 1.5
  476. arrowButton.layer?.borderColor = AppTheme.paywallBorder.cgColor
  477. arrowButton.contentTintColor = AppTheme.textSecondary
  478. if isHovered {
  479. applyHoverLift(true)
  480. }
  481. }
  482. override func layout() {
  483. super.layout()
  484. let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
  485. if iconWidthConstraint.constant != size {
  486. iconWidthConstraint.constant = size
  487. iconHeightConstraint.constant = size
  488. }
  489. }
  490. override func mouseUp(with event: NSEvent) {
  491. let location = convert(event.locationInWindow, from: nil)
  492. guard bounds.contains(location) else { return }
  493. onActivate?()
  494. }
  495. override func resetCursorRects() {
  496. guard onActivate != nil else { return }
  497. addCursorRect(bounds, cursor: .pointingHand)
  498. }
  499. }