UIComponents.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import Cocoa
  2. // MARK: - Gradient Card View
  3. final class GradientCardView: NSView {
  4. private let gradientLayer = CAGradientLayer()
  5. init(colors: [NSColor], startPoint: CGPoint = CGPoint(x: 0, y: 0.5), endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
  6. super.init(frame: .zero)
  7. wantsLayer = true
  8. gradientLayer.colors = colors.map { $0.cgColor }
  9. gradientLayer.startPoint = startPoint
  10. gradientLayer.endPoint = endPoint
  11. gradientLayer.cornerRadius = AppTheme.cardCornerRadius
  12. layer?.addSublayer(gradientLayer)
  13. applyCardShadow()
  14. }
  15. @available(*, unavailable)
  16. required init?(coder: NSCoder) { nil }
  17. override func layout() {
  18. super.layout()
  19. gradientLayer.frame = bounds
  20. gradientLayer.cornerRadius = AppTheme.cardCornerRadius
  21. }
  22. }
  23. // MARK: - Wave Pattern
  24. final class WavePatternView: NSView {
  25. override var isOpaque: Bool { false }
  26. override func draw(_ dirtyRect: NSRect) {
  27. super.draw(dirtyRect)
  28. guard let context = NSGraphicsContext.current?.cgContext else { return }
  29. let dotColor = NSColor(calibratedWhite: 0.82, alpha: 0.55)
  30. context.setFillColor(dotColor.cgColor)
  31. let spacing: CGFloat = 14
  32. let dotSize: CGFloat = 3
  33. let rows = Int(bounds.height / spacing) + 2
  34. let cols = Int(bounds.width / spacing) + 2
  35. for row in 0..<rows {
  36. for col in 0..<cols {
  37. let wave = sin(Double(col) * 0.35 + Double(row) * 0.25) * 8
  38. let x = CGFloat(col) * spacing + CGFloat(wave)
  39. let y = CGFloat(row) * spacing
  40. let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize)
  41. context.fillEllipse(in: rect)
  42. }
  43. }
  44. }
  45. }
  46. // MARK: - Sidebar Nav Item
  47. final class SidebarNavItem: NSControl {
  48. enum Style {
  49. case normal
  50. case active
  51. }
  52. var onClick: (() -> Void)?
  53. private let iconView = NSImageView()
  54. private let titleLabel = NSTextField(labelWithString: "")
  55. private let container = NSView()
  56. var isSelected: Bool = false {
  57. didSet { applyStyle(isSelected ? .active : .normal) }
  58. }
  59. init(title: String, symbolName: String, isSelected: Bool = false) {
  60. super.init(frame: .zero)
  61. self.isSelected = isSelected
  62. titleLabel.stringValue = title
  63. titleLabel.font = AppTheme.mediumFont(size: 14)
  64. titleLabel.textColor = AppTheme.textPrimary
  65. if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
  66. let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
  67. iconView.image = image.withSymbolConfiguration(config)
  68. }
  69. iconView.contentTintColor = AppTheme.textPrimary
  70. container.translatesAutoresizingMaskIntoConstraints = false
  71. iconView.translatesAutoresizingMaskIntoConstraints = false
  72. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  73. translatesAutoresizingMaskIntoConstraints = false
  74. addSubview(container)
  75. container.addSubview(iconView)
  76. container.addSubview(titleLabel)
  77. NSLayoutConstraint.activate([
  78. heightAnchor.constraint(equalToConstant: 44),
  79. container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
  80. container.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
  81. container.topAnchor.constraint(equalTo: topAnchor),
  82. container.bottomAnchor.constraint(equalTo: bottomAnchor),
  83. iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
  84. iconView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  85. iconView.widthAnchor.constraint(equalToConstant: 20),
  86. iconView.heightAnchor.constraint(equalToConstant: 20),
  87. titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
  88. titleLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  89. titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
  90. ])
  91. applyStyle(isSelected ? .active : .normal)
  92. }
  93. @available(*, unavailable)
  94. required init?(coder: NSCoder) { nil }
  95. private func applyStyle(_ style: Style) {
  96. container.wantsLayer = true
  97. container.layer?.cornerRadius = AppTheme.cornerRadius
  98. switch style {
  99. case .normal:
  100. container.layer?.backgroundColor = NSColor.clear.cgColor
  101. titleLabel.textColor = AppTheme.textPrimary
  102. iconView.contentTintColor = AppTheme.textPrimary
  103. case .active:
  104. container.layer?.backgroundColor = AppTheme.homeActiveBackground.cgColor
  105. titleLabel.textColor = AppTheme.homeActiveForeground
  106. iconView.contentTintColor = AppTheme.homeActiveForeground
  107. }
  108. }
  109. override func mouseUp(with event: NSEvent) {
  110. guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
  111. onClick?()
  112. }
  113. override func resetCursorRects() {
  114. addCursorRect(bounds, cursor: .pointingHand)
  115. }
  116. }
  117. // MARK: - Action Button
  118. final class PillButton: NSButton {
  119. private let horizontalPadding: CGFloat = 16
  120. init(title: String, color: NSColor) {
  121. super.init(frame: .zero)
  122. self.title = title
  123. isBordered = false
  124. wantsLayer = true
  125. layer?.backgroundColor = color.cgColor
  126. layer?.cornerRadius = 11
  127. font = AppTheme.semiboldFont(size: 13)
  128. contentTintColor = .white
  129. if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
  130. let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
  131. image = arrow.withSymbolConfiguration(config)
  132. imagePosition = .imageTrailing
  133. imageHugsTitle = true
  134. }
  135. heightAnchor.constraint(equalToConstant: 40).isActive = true
  136. }
  137. @available(*, unavailable)
  138. required init?(coder: NSCoder) { nil }
  139. override var intrinsicContentSize: NSSize {
  140. var size = super.intrinsicContentSize
  141. size.width += horizontalPadding * 2
  142. return size
  143. }
  144. override func draw(_ dirtyRect: NSRect) {
  145. let originalBounds = bounds
  146. defer { bounds = originalBounds }
  147. bounds = originalBounds.insetBy(dx: horizontalPadding, dy: 0)
  148. super.draw(dirtyRect)
  149. }
  150. override func resetCursorRects() {
  151. addCursorRect(bounds, cursor: .pointingHand)
  152. }
  153. }
  154. // MARK: - Quick Start Card
  155. struct QuickStartCardData {
  156. let title: String
  157. let subtitle: String
  158. let buttonTitle: String
  159. let accentColor: NSColor
  160. let gradientColors: [NSColor]
  161. let iconKind: QuickStartIconKind
  162. }
  163. final class QuickStartCardView: NSView {
  164. private let iconView: QuickStartIconView
  165. private var iconWidthConstraint: NSLayoutConstraint!
  166. private var iconHeightConstraint: NSLayoutConstraint!
  167. init(data: QuickStartCardData) {
  168. iconView = QuickStartIconView(kind: data.iconKind)
  169. super.init(frame: .zero)
  170. translatesAutoresizingMaskIntoConstraints = false
  171. let gradient = GradientCardView(
  172. colors: data.gradientColors,
  173. startPoint: CGPoint(x: 0, y: 0.5),
  174. endPoint: CGPoint(x: 1, y: 0.5)
  175. )
  176. gradient.translatesAutoresizingMaskIntoConstraints = false
  177. let titleLabel = NSTextField(labelWithString: data.title)
  178. titleLabel.font = AppTheme.semiboldFont(size: 22)
  179. titleLabel.textColor = data.accentColor
  180. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  181. let subtitleLabel = NSTextField(labelWithString: data.subtitle)
  182. subtitleLabel.font = AppTheme.regularFont(size: 13)
  183. subtitleLabel.textColor = AppTheme.textSecondary
  184. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  185. let button = PillButton(title: data.buttonTitle, color: data.accentColor)
  186. button.translatesAutoresizingMaskIntoConstraints = false
  187. let wavePattern = WavePatternView()
  188. wavePattern.translatesAutoresizingMaskIntoConstraints = false
  189. addSubview(gradient)
  190. gradient.addSubview(wavePattern)
  191. gradient.addSubview(titleLabel)
  192. gradient.addSubview(subtitleLabel)
  193. gradient.addSubview(button)
  194. gradient.addSubview(iconView)
  195. NSLayoutConstraint.activate([
  196. gradient.leadingAnchor.constraint(equalTo: leadingAnchor),
  197. gradient.trailingAnchor.constraint(equalTo: trailingAnchor),
  198. gradient.topAnchor.constraint(equalTo: topAnchor),
  199. gradient.bottomAnchor.constraint(equalTo: bottomAnchor),
  200. heightAnchor.constraint(equalToConstant: 148),
  201. titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 18),
  202. titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 24),
  203. subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  204. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
  205. subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -8),
  206. button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  207. button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -20),
  208. wavePattern.trailingAnchor.constraint(equalTo: gradient.trailingAnchor),
  209. wavePattern.bottomAnchor.constraint(equalTo: gradient.bottomAnchor),
  210. wavePattern.widthAnchor.constraint(equalToConstant: 100),
  211. wavePattern.heightAnchor.constraint(equalToConstant: 70),
  212. iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -8),
  213. iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor),
  214. ])
  215. iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
  216. iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
  217. iconWidthConstraint.isActive = true
  218. iconHeightConstraint.isActive = true
  219. }
  220. @available(*, unavailable)
  221. required init?(coder: NSCoder) { nil }
  222. override func layout() {
  223. super.layout()
  224. let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
  225. if iconWidthConstraint.constant != size {
  226. iconWidthConstraint.constant = size
  227. iconHeightConstraint.constant = size
  228. }
  229. }
  230. }
  231. // MARK: - Feature Card
  232. struct FeatureCardData {
  233. let title: String
  234. let subtitle: String
  235. let iconKind: FeatureIconKind
  236. }
  237. final class FeatureCardView: NSView {
  238. private let iconView: FeatureIconView
  239. private var iconWidthConstraint: NSLayoutConstraint!
  240. private var iconHeightConstraint: NSLayoutConstraint!
  241. init(data: FeatureCardData) {
  242. iconView = FeatureIconView(kind: data.iconKind)
  243. super.init(frame: .zero)
  244. translatesAutoresizingMaskIntoConstraints = false
  245. setContentHuggingPriority(.defaultLow, for: .horizontal)
  246. setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  247. wantsLayer = true
  248. layer?.backgroundColor = AppTheme.cardBackground.cgColor
  249. layer?.cornerRadius = AppTheme.featureCardCornerRadius
  250. applyCardShadow()
  251. let titleLabel = NSTextField(labelWithString: data.title)
  252. titleLabel.font = AppTheme.semiboldFont(size: 14)
  253. titleLabel.textColor = AppTheme.textPrimary
  254. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  255. let subtitleLabel = NSTextField(labelWithString: data.subtitle)
  256. subtitleLabel.font = AppTheme.regularFont(size: 11)
  257. subtitleLabel.textColor = AppTheme.textSecondary
  258. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  259. let arrowButton = NSButton()
  260. arrowButton.isBordered = false
  261. arrowButton.wantsLayer = true
  262. arrowButton.layer?.backgroundColor = NSColor.white.cgColor
  263. arrowButton.layer?.cornerRadius = 13
  264. arrowButton.layer?.borderWidth = 1
  265. arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
  266. arrowButton.translatesAutoresizingMaskIntoConstraints = false
  267. if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
  268. let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
  269. arrowButton.image = arrow.withSymbolConfiguration(config)
  270. }
  271. arrowButton.contentTintColor = AppTheme.textSecondary
  272. addSubview(iconView)
  273. addSubview(titleLabel)
  274. addSubview(subtitleLabel)
  275. addSubview(arrowButton)
  276. NSLayoutConstraint.activate([
  277. heightAnchor.constraint(equalToConstant: 96),
  278. iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
  279. iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
  280. titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
  281. titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
  282. titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
  283. subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  284. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
  285. subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
  286. arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
  287. arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
  288. arrowButton.widthAnchor.constraint(equalToConstant: 26),
  289. arrowButton.heightAnchor.constraint(equalToConstant: 26),
  290. ])
  291. iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
  292. iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
  293. iconWidthConstraint.isActive = true
  294. iconHeightConstraint.isActive = true
  295. }
  296. @available(*, unavailable)
  297. required init?(coder: NSCoder) { nil }
  298. override func layout() {
  299. super.layout()
  300. let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
  301. if iconWidthConstraint.constant != size {
  302. iconWidthConstraint.constant = size
  303. iconHeightConstraint.constant = size
  304. }
  305. }
  306. override func resetCursorRects() {
  307. addCursorRect(bounds, cursor: .pointingHand)
  308. }
  309. }