Sfoglia il codice sorgente

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 12 ore fa
parent
commit
3d8b6f4dc5

+ 30 - 8
smart_printer/SidebarView.swift

@@ -1,6 +1,16 @@
1 1
 import Cocoa
2 2
 
3
+enum SidebarDestination: Int, CaseIterable {
4
+    case home
5
+    case scan
6
+    case scanAndHome
7
+}
8
+
3 9
 final class SidebarView: NSView {
10
+    var onDestinationSelected: ((SidebarDestination) -> Void)?
11
+
12
+    private var navItems: [SidebarNavItem] = []
13
+
4 14
     init() {
5 15
         super.init(frame: .zero)
6 16
         wantsLayer = true
@@ -12,6 +22,12 @@ final class SidebarView: NSView {
12 22
     @available(*, unavailable)
13 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 31
     private func setup() {
16 32
         let logoContainer = NSView()
17 33
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -36,13 +52,19 @@ final class SidebarView: NSView {
36 52
         navStack.alignment = .leading
37 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 69
         addSubview(logoContainer)
48 70
         logoContainer.addSubview(logoIcon)
@@ -76,7 +98,7 @@ final class SidebarView: NSView {
76 98
 
77 99
             homeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
78 100
             scanItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
79
-            premiumItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
101
+            scanAndHomeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
80 102
 
81 103
             divider.trailingAnchor.constraint(equalTo: trailingAnchor),
82 104
             divider.topAnchor.constraint(equalTo: topAnchor),

+ 14 - 9
smart_printer/UIComponents.swift

@@ -61,17 +61,21 @@ final class SidebarNavItem: NSControl {
61 61
     enum Style {
62 62
         case normal
63 63
         case active
64
-        case premium
65 64
     }
66 65
 
66
+    var onClick: (() -> Void)?
67
+
67 68
     private let iconView = NSImageView()
68 69
     private let titleLabel = NSTextField(labelWithString: "")
69 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 77
         super.init(frame: .zero)
74
-        self.style = style
78
+        self.isSelected = isSelected
75 79
 
76 80
         titleLabel.stringValue = title
77 81
         titleLabel.font = AppTheme.mediumFont(size: 14)
@@ -110,7 +114,7 @@ final class SidebarNavItem: NSControl {
110 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 120
     @available(*, unavailable)
@@ -129,13 +133,14 @@ final class SidebarNavItem: NSControl {
129 133
             container.layer?.backgroundColor = AppTheme.homeActiveBackground.cgColor
130 134
             titleLabel.textColor = AppTheme.homeActiveForeground
131 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 144
     override func resetCursorRects() {
140 145
         addCursorRect(bounds, cursor: .pointingHand)
141 146
     }

+ 136 - 7
smart_printer/ViewController.swift

@@ -7,6 +7,12 @@ import Cocoa
7 7
 
8 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 16
     override func loadView() {
11 17
         let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
12 18
         container.autoresizingMask = [.width, .height]
@@ -21,7 +27,7 @@ class ViewController: NSViewController {
21 27
     }
22 28
 
23 29
     private func setupLayout() {
24
-        let sidebar = SidebarView()
30
+        sidebar = SidebarView()
25 31
 
26 32
         let mainContent = NSView()
27 33
         mainContent.translatesAutoresizingMaskIntoConstraints = false
@@ -29,7 +35,17 @@ class ViewController: NSViewController {
29 35
         mainContent.layer?.backgroundColor = AppTheme.background.cgColor
30 36
 
31 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 50
         let wavePattern = WavePatternView()
35 51
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
@@ -37,9 +53,13 @@ class ViewController: NSViewController {
37 53
         view.addSubview(sidebar)
38 54
         view.addSubview(mainContent)
39 55
         mainContent.addSubview(header)
40
-        mainContent.addSubview(scrollView)
56
+        mainContent.addSubview(contentContainer)
41 57
         mainContent.addSubview(wavePattern)
42 58
 
59
+        sidebar.onDestinationSelected = { [weak self] destination in
60
+            self?.showDestination(destination)
61
+        }
62
+
43 63
         NSLayoutConstraint.activate([
44 64
             sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
45 65
             sidebar.topAnchor.constraint(equalTo: view.topAnchor),
@@ -55,16 +75,125 @@ class ViewController: NSViewController {
55 75
             header.topAnchor.constraint(equalTo: mainContent.topAnchor, constant: 16),
56 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 83
             wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
64 84
             wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
65 85
             wavePattern.widthAnchor.constraint(equalToConstant: 200),
66 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 199
     private func makeHeader() -> NSView {