Pārlūkot izejas kodu

Make sidebar navigation clickable with active selection styling.

Switch main content between Home, Scan, and Premium tabs so each sidebar item is functional while Premium content remains a placeholder.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 14 stundas atpakaļ
vecāks
revīzija
3d8b6f4dc5

+ 30 - 8
smart_printer/SidebarView.swift

@@ -1,6 +1,16 @@
1
 import Cocoa
1
 import Cocoa
2
 
2
 
3
+enum SidebarDestination: Int, CaseIterable {
4
+    case home
5
+    case scan
6
+    case scanAndHome
7
+}
8
+
3
 final class SidebarView: NSView {
9
 final class SidebarView: NSView {
10
+    var onDestinationSelected: ((SidebarDestination) -> Void)?
11
+
12
+    private var navItems: [SidebarNavItem] = []
13
+
4
     init() {
14
     init() {
5
         super.init(frame: .zero)
15
         super.init(frame: .zero)
6
         wantsLayer = true
16
         wantsLayer = true
@@ -12,6 +22,12 @@ final class SidebarView: NSView {
12
     @available(*, unavailable)
22
     @available(*, unavailable)
13
     required init?(coder: NSCoder) { nil }
23
     required init?(coder: NSCoder) { nil }
14
 
24
 
25
+    func select(_ destination: SidebarDestination) {
26
+        for (index, item) in navItems.enumerated() {
27
+            item.isSelected = index == destination.rawValue
28
+        }
29
+    }
30
+
15
     private func setup() {
31
     private func setup() {
16
         let logoContainer = NSView()
32
         let logoContainer = NSView()
17
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
33
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -36,13 +52,19 @@ final class SidebarView: NSView {
36
         navStack.alignment = .leading
52
         navStack.alignment = .leading
37
         navStack.translatesAutoresizingMaskIntoConstraints = false
53
         navStack.translatesAutoresizingMaskIntoConstraints = false
38
 
54
 
39
-        let homeItem = SidebarNavItem(title: "Home", symbolName: "house.fill", style: .active)
40
-        let scanItem = SidebarNavItem(title: "Scan", symbolName: "viewfinder", style: .normal)
41
-        let premiumItem = SidebarNavItem(title: "Premium", symbolName: "diamond.fill", style: .premium)
42
-
43
-        navStack.addArrangedSubview(homeItem)
44
-        navStack.addArrangedSubview(scanItem)
45
-        navStack.addArrangedSubview(premiumItem)
55
+        let homeItem = SidebarNavItem(title: "Home", symbolName: "house.fill", isSelected: true)
56
+        let scanItem = SidebarNavItem(title: "Scan", symbolName: "viewfinder")
57
+        let scanAndHomeItem = SidebarNavItem(title: "Premium", symbolName: "diamond.fill")
58
+
59
+        navItems = [homeItem, scanItem, scanAndHomeItem]
60
+        for (index, item) in navItems.enumerated() {
61
+            item.onClick = { [weak self] in
62
+                guard let self, let destination = SidebarDestination(rawValue: index) else { return }
63
+                self.select(destination)
64
+                self.onDestinationSelected?(destination)
65
+            }
66
+            navStack.addArrangedSubview(item)
67
+        }
46
 
68
 
47
         addSubview(logoContainer)
69
         addSubview(logoContainer)
48
         logoContainer.addSubview(logoIcon)
70
         logoContainer.addSubview(logoIcon)
@@ -76,7 +98,7 @@ final class SidebarView: NSView {
76
 
98
 
77
             homeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
99
             homeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
78
             scanItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
100
             scanItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
79
-            premiumItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
101
+            scanAndHomeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
80
 
102
 
81
             divider.trailingAnchor.constraint(equalTo: trailingAnchor),
103
             divider.trailingAnchor.constraint(equalTo: trailingAnchor),
82
             divider.topAnchor.constraint(equalTo: topAnchor),
104
             divider.topAnchor.constraint(equalTo: topAnchor),

+ 14 - 9
smart_printer/UIComponents.swift

@@ -61,17 +61,21 @@ final class SidebarNavItem: NSControl {
61
     enum Style {
61
     enum Style {
62
         case normal
62
         case normal
63
         case active
63
         case active
64
-        case premium
65
     }
64
     }
66
 
65
 
66
+    var onClick: (() -> Void)?
67
+
67
     private let iconView = NSImageView()
68
     private let iconView = NSImageView()
68
     private let titleLabel = NSTextField(labelWithString: "")
69
     private let titleLabel = NSTextField(labelWithString: "")
69
     private let container = NSView()
70
     private let container = NSView()
70
-    private var style: Style = .normal
71
 
71
 
72
-    init(title: String, symbolName: String, style: Style = .normal) {
72
+    var isSelected: Bool = false {
73
+        didSet { applyStyle(isSelected ? .active : .normal) }
74
+    }
75
+
76
+    init(title: String, symbolName: String, isSelected: Bool = false) {
73
         super.init(frame: .zero)
77
         super.init(frame: .zero)
74
-        self.style = style
78
+        self.isSelected = isSelected
75
 
79
 
76
         titleLabel.stringValue = title
80
         titleLabel.stringValue = title
77
         titleLabel.font = AppTheme.mediumFont(size: 14)
81
         titleLabel.font = AppTheme.mediumFont(size: 14)
@@ -110,7 +114,7 @@ final class SidebarNavItem: NSControl {
110
             titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
114
             titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
111
         ])
115
         ])
112
 
116
 
113
-        applyStyle(style)
117
+        applyStyle(isSelected ? .active : .normal)
114
     }
118
     }
115
 
119
 
116
     @available(*, unavailable)
120
     @available(*, unavailable)
@@ -129,13 +133,14 @@ final class SidebarNavItem: NSControl {
129
             container.layer?.backgroundColor = AppTheme.homeActiveBackground.cgColor
133
             container.layer?.backgroundColor = AppTheme.homeActiveBackground.cgColor
130
             titleLabel.textColor = AppTheme.homeActiveForeground
134
             titleLabel.textColor = AppTheme.homeActiveForeground
131
             iconView.contentTintColor = AppTheme.homeActiveForeground
135
             iconView.contentTintColor = AppTheme.homeActiveForeground
132
-        case .premium:
133
-            container.layer?.backgroundColor = AppTheme.premiumBackground.cgColor
134
-            titleLabel.textColor = AppTheme.premiumForeground
135
-            iconView.contentTintColor = AppTheme.premiumForeground
136
         }
136
         }
137
     }
137
     }
138
 
138
 
139
+    override func mouseUp(with event: NSEvent) {
140
+        guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
141
+        onClick?()
142
+    }
143
+
139
     override func resetCursorRects() {
144
     override func resetCursorRects() {
140
         addCursorRect(bounds, cursor: .pointingHand)
145
         addCursorRect(bounds, cursor: .pointingHand)
141
     }
146
     }

+ 136 - 7
smart_printer/ViewController.swift

@@ -7,6 +7,12 @@ import Cocoa
7
 
7
 
8
 class ViewController: NSViewController {
8
 class ViewController: NSViewController {
9
 
9
 
10
+    private var sidebar: SidebarView!
11
+    private var homeContentView: NSView!
12
+    private var scanContentView: NSView!
13
+    private var scanAndHomeContentView: NSView!
14
+    private var contentContainer: NSView!
15
+
10
     override func loadView() {
16
     override func loadView() {
11
         let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
17
         let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
12
         container.autoresizingMask = [.width, .height]
18
         container.autoresizingMask = [.width, .height]
@@ -21,7 +27,7 @@ class ViewController: NSViewController {
21
     }
27
     }
22
 
28
 
23
     private func setupLayout() {
29
     private func setupLayout() {
24
-        let sidebar = SidebarView()
30
+        sidebar = SidebarView()
25
 
31
 
26
         let mainContent = NSView()
32
         let mainContent = NSView()
27
         mainContent.translatesAutoresizingMaskIntoConstraints = false
33
         mainContent.translatesAutoresizingMaskIntoConstraints = false
@@ -29,7 +35,17 @@ class ViewController: NSViewController {
29
         mainContent.layer?.backgroundColor = AppTheme.background.cgColor
35
         mainContent.layer?.backgroundColor = AppTheme.background.cgColor
30
 
36
 
31
         let header = makeHeader()
37
         let header = makeHeader()
32
-        let scrollView = makeScrollView()
38
+
39
+        contentContainer = NSView()
40
+        contentContainer.translatesAutoresizingMaskIntoConstraints = false
41
+
42
+        homeContentView = makeHomeContentView()
43
+        scanContentView = makeScanContentView()
44
+        scanAndHomeContentView = makeScanAndHomeContentView()
45
+
46
+        contentContainer.addSubview(homeContentView)
47
+        contentContainer.addSubview(scanContentView)
48
+        contentContainer.addSubview(scanAndHomeContentView)
33
 
49
 
34
         let wavePattern = WavePatternView()
50
         let wavePattern = WavePatternView()
35
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
51
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
@@ -37,9 +53,13 @@ class ViewController: NSViewController {
37
         view.addSubview(sidebar)
53
         view.addSubview(sidebar)
38
         view.addSubview(mainContent)
54
         view.addSubview(mainContent)
39
         mainContent.addSubview(header)
55
         mainContent.addSubview(header)
40
-        mainContent.addSubview(scrollView)
56
+        mainContent.addSubview(contentContainer)
41
         mainContent.addSubview(wavePattern)
57
         mainContent.addSubview(wavePattern)
42
 
58
 
59
+        sidebar.onDestinationSelected = { [weak self] destination in
60
+            self?.showDestination(destination)
61
+        }
62
+
43
         NSLayoutConstraint.activate([
63
         NSLayoutConstraint.activate([
44
             sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
64
             sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
45
             sidebar.topAnchor.constraint(equalTo: view.topAnchor),
65
             sidebar.topAnchor.constraint(equalTo: view.topAnchor),
@@ -55,16 +75,125 @@ class ViewController: NSViewController {
55
             header.topAnchor.constraint(equalTo: mainContent.topAnchor, constant: 16),
75
             header.topAnchor.constraint(equalTo: mainContent.topAnchor, constant: 16),
56
             header.heightAnchor.constraint(equalToConstant: 44),
76
             header.heightAnchor.constraint(equalToConstant: 44),
57
 
77
 
58
-            scrollView.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
59
-            scrollView.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
60
-            scrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
61
-            scrollView.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
78
+            contentContainer.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
79
+            contentContainer.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
80
+            contentContainer.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
81
+            contentContainer.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
62
 
82
 
63
             wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
83
             wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
64
             wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
84
             wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
65
             wavePattern.widthAnchor.constraint(equalToConstant: 200),
85
             wavePattern.widthAnchor.constraint(equalToConstant: 200),
66
             wavePattern.heightAnchor.constraint(equalToConstant: 140),
86
             wavePattern.heightAnchor.constraint(equalToConstant: 140),
67
         ])
87
         ])
88
+
89
+        pinContentView(homeContentView)
90
+        pinContentView(scanContentView)
91
+        pinContentView(scanAndHomeContentView)
92
+        showDestination(.home)
93
+    }
94
+
95
+    private func pinContentView(_ contentView: NSView) {
96
+        NSLayoutConstraint.activate([
97
+            contentView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
98
+            contentView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
99
+            contentView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
100
+            contentView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
101
+        ])
102
+    }
103
+
104
+    private func showDestination(_ destination: SidebarDestination) {
105
+        homeContentView.isHidden = destination != .home
106
+        scanContentView.isHidden = destination != .scan
107
+        scanAndHomeContentView.isHidden = destination != .scanAndHome
108
+    }
109
+
110
+    private func makeHomeContentView() -> NSView {
111
+        let container = NSView()
112
+        container.translatesAutoresizingMaskIntoConstraints = false
113
+
114
+        let scrollView = makeScrollView()
115
+        container.addSubview(scrollView)
116
+
117
+        NSLayoutConstraint.activate([
118
+            scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
119
+            scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
120
+            scrollView.topAnchor.constraint(equalTo: container.topAnchor),
121
+            scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
122
+        ])
123
+
124
+        return container
125
+    }
126
+
127
+    private func makeScanContentView() -> NSView {
128
+        makeDestinationScrollView(
129
+            title: "Scan",
130
+            features: [
131
+                FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
132
+                FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
133
+            ]
134
+        )
135
+    }
136
+
137
+    private func makeScanAndHomeContentView() -> NSView {
138
+        makeDestinationScrollView(
139
+            title: "Premium",
140
+            features: [
141
+                FeatureCardData(title: "From Photos", subtitle: "Take a photo from gallery", iconKind: .scanFile),
142
+                FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
143
+                FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
144
+                FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
145
+            ]
146
+        )
147
+    }
148
+
149
+    private func makeDestinationScrollView(title: String, features: [FeatureCardData]) -> NSView {
150
+        let container = NSView()
151
+        container.translatesAutoresizingMaskIntoConstraints = false
152
+        container.isHidden = true
153
+
154
+        let scrollView = NSScrollView()
155
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
156
+        scrollView.hasVerticalScroller = true
157
+        scrollView.hasHorizontalScroller = false
158
+        scrollView.autohidesScrollers = true
159
+        scrollView.drawsBackground = false
160
+        scrollView.borderType = .noBorder
161
+
162
+        let documentView = FlippedDocumentView()
163
+        documentView.translatesAutoresizingMaskIntoConstraints = false
164
+
165
+        let sectionTitle = makeSectionTitle(title, showGridIcon: true)
166
+        let grid = makeFeatureRow(features: features)
167
+
168
+        documentView.addSubview(sectionTitle)
169
+        documentView.addSubview(grid)
170
+        scrollView.documentView = documentView
171
+        container.addSubview(scrollView)
172
+
173
+        let contentGuide = scrollView.contentView
174
+
175
+        NSLayoutConstraint.activate([
176
+            scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
177
+            scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
178
+            scrollView.topAnchor.constraint(equalTo: container.topAnchor),
179
+            scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
180
+
181
+            documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
182
+            documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
183
+            documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
184
+            documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
185
+
186
+            sectionTitle.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
187
+            sectionTitle.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
188
+            sectionTitle.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
189
+
190
+            grid.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
191
+            grid.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
192
+            grid.topAnchor.constraint(equalTo: sectionTitle.bottomAnchor, constant: 16),
193
+            grid.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
194
+        ])
195
+
196
+        return container
68
     }
197
     }
69
 
198
 
70
     private func makeHeader() -> NSView {
199
     private func makeHeader() -> NSView {