Procházet zdrojové kódy

Normalize feature card icons and make the Create & Print grid resize equally with the window.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 15 hodinami
rodič
revize
d9c60d4bb2

+ 2 - 2
smart_printer/AppTheme.swift

@@ -13,7 +13,7 @@ enum AppTheme {
13
 
13
 
14
     static let quickStartIconMax: CGFloat = 80
14
     static let quickStartIconMax: CGFloat = 80
15
     static let quickStartIconMin: CGFloat = 56
15
     static let quickStartIconMin: CGFloat = 56
16
-    static let featureIconMax: CGFloat = 56
16
+    static let featureIconMax: CGFloat = 48
17
     static let featureIconMin: CGFloat = 36
17
     static let featureIconMin: CGFloat = 36
18
 
18
 
19
     static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
19
     static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
@@ -21,7 +21,7 @@ enum AppTheme {
21
     }
21
     }
22
 
22
 
23
     static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
23
     static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
24
-        min(featureIconMax, max(featureIconMin, width * 0.38))
24
+        min(featureIconMax, max(featureIconMin, width * 0.24))
25
     }
25
     }
26
 
26
 
27
     static let cornerRadius: CGFloat = 14
27
     static let cornerRadius: CGFloat = 14

+ 90 - 68
smart_printer/FeatureIcons.swift

@@ -16,6 +16,10 @@ final class FeatureIconView: NSView {
16
         self.kind = kind
16
         self.kind = kind
17
         super.init(frame: .zero)
17
         super.init(frame: .zero)
18
         translatesAutoresizingMaskIntoConstraints = false
18
         translatesAutoresizingMaskIntoConstraints = false
19
+        setContentHuggingPriority(.required, for: .horizontal)
20
+        setContentHuggingPriority(.required, for: .vertical)
21
+        setContentCompressionResistancePriority(.required, for: .horizontal)
22
+        setContentCompressionResistancePriority(.required, for: .vertical)
19
     }
23
     }
20
 
24
 
21
     @available(*, unavailable)
25
     @available(*, unavailable)
@@ -38,42 +42,70 @@ final class FeatureIconView: NSView {
38
         }
42
         }
39
     }
43
     }
40
 
44
 
45
+    // MARK: - Layout
46
+
47
+    /// Shared artboard so every icon fills the same visual area.
48
+    private struct IconLayout {
49
+        let fill: CGRect
50
+        let d: CGFloat
51
+
52
+        init(bounds: NSRect) {
53
+            let side = min(bounds.width, bounds.height)
54
+            let originX = (bounds.width - side) / 2
55
+            let originY = (bounds.height - side) / 2
56
+            let inset = side * 0.12
57
+            fill = CGRect(x: originX + inset, y: originY + inset, width: side - inset * 2, height: side - inset * 2)
58
+            d = fill.width
59
+        }
60
+
61
+        func rect(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) -> CGRect {
62
+            CGRect(
63
+                x: fill.minX + fill.width * x,
64
+                y: fill.minY + fill.height * y,
65
+                width: fill.width * w,
66
+                height: fill.height * h
67
+            )
68
+        }
69
+
70
+        func dim(_ fraction: CGFloat) -> CGFloat {
71
+            d * fraction
72
+        }
73
+    }
74
+
41
     // MARK: - Scan File
75
     // MARK: - Scan File
42
 
76
 
43
     private func drawScanFile(in context: CGContext) {
77
     private func drawScanFile(in context: CGContext) {
44
-        let s = min(bounds.width, bounds.height)
45
-        let ox = (bounds.width - s) / 2
46
-        let oy = (bounds.height - s) / 2
78
+        let layout = IconLayout(bounds: bounds)
47
 
79
 
48
-        let bodyRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.84, height: s * 0.52)
49
-        drawSoftShadow(in: context, rect: CGRect(x: bodyRect.minX, y: bodyRect.maxY - s * 0.03, width: bodyRect.width, height: s * 0.05), radius: s * 0.025)
80
+        let bodyRect = layout.rect(0.08, 0.38, 0.84, 0.50)
81
+        drawSoftShadow(in: context, rect: CGRect(x: bodyRect.minX, y: bodyRect.maxY - layout.dim(0.03), width: bodyRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
50
 
82
 
51
         let blueTop = NSColor(red: 0.38, green: 0.66, blue: 1.0, alpha: 1)
83
         let blueTop = NSColor(red: 0.38, green: 0.66, blue: 1.0, alpha: 1)
52
         let blueBottom = NSColor(red: 0.18, green: 0.44, blue: 0.94, alpha: 1)
84
         let blueBottom = NSColor(red: 0.18, green: 0.44, blue: 0.94, alpha: 1)
53
-        fillGradient(in: context, path: roundedRect(bodyRect, radius: s * 0.06), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: bodyRect.midX, y: bodyRect.minY), end: CGPoint(x: bodyRect.midX, y: bodyRect.maxY))
85
+        fillGradient(in: context, path: roundedRect(bodyRect, radius: layout.dim(0.06)), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: bodyRect.midX, y: bodyRect.minY), end: CGPoint(x: bodyRect.midX, y: bodyRect.maxY))
54
 
86
 
55
-        let glassRect = CGRect(x: bodyRect.minX + s * 0.08, y: bodyRect.minY + s * 0.06, width: bodyRect.width * 0.84, height: bodyRect.height * 0.52)
87
+        let glassRect = CGRect(x: bodyRect.minX + layout.dim(0.06), y: bodyRect.minY + layout.dim(0.05), width: bodyRect.width * 0.84, height: bodyRect.height * 0.52)
56
         context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
88
         context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
57
-        context.addPath(roundedRect(glassRect, radius: s * 0.03))
89
+        context.addPath(roundedRect(glassRect, radius: layout.dim(0.03)))
58
         context.fillPath()
90
         context.fillPath()
59
 
91
 
60
-        let lidRect = CGRect(x: ox + s * 0.04, y: oy + s * 0.14, width: s * 0.92, height: s * 0.20)
61
-        fillGradient(in: context, path: roundedRect(lidRect, radius: s * 0.04), colors: [NSColor(red: 0.55, green: 0.76, blue: 1.0, alpha: 1).cgColor, blueTop.cgColor], start: CGPoint(x: lidRect.midX, y: lidRect.minY), end: CGPoint(x: lidRect.midX, y: lidRect.maxY))
92
+        let lidRect = layout.rect(0.06, 0.22, 0.88, 0.18)
93
+        fillGradient(in: context, path: roundedRect(lidRect, radius: layout.dim(0.04)), colors: [NSColor(red: 0.55, green: 0.76, blue: 1.0, alpha: 1).cgColor, blueTop.cgColor], start: CGPoint(x: lidRect.midX, y: lidRect.minY), end: CGPoint(x: lidRect.midX, y: lidRect.maxY))
62
 
94
 
63
-        let paperRect = CGRect(x: ox + s * 0.28, y: oy + s * 0.02, width: s * 0.44, height: s * 0.22)
95
+        let paperRect = layout.rect(0.28, 0.06, 0.44, 0.20)
64
         context.setFillColor(NSColor.white.cgColor)
96
         context.setFillColor(NSColor.white.cgColor)
65
-        context.addPath(roundedRect(paperRect, radius: s * 0.02))
97
+        context.addPath(roundedRect(paperRect, radius: layout.dim(0.02)))
66
         context.fillPath()
98
         context.fillPath()
67
 
99
 
68
         for i in 0..<3 {
100
         for i in 0..<3 {
69
-            let lineY = paperRect.minY + s * 0.06 + CGFloat(i) * s * 0.045
101
+            let lineY = paperRect.minY + layout.dim(0.06) + CGFloat(i) * layout.dim(0.045)
70
             let lineW = paperRect.width * (0.85 - CGFloat(i) * 0.1)
102
             let lineW = paperRect.width * (0.85 - CGFloat(i) * 0.1)
71
             context.setFillColor(NSColor(red: 0.72, green: 0.82, blue: 0.98, alpha: 1).cgColor)
103
             context.setFillColor(NSColor(red: 0.72, green: 0.82, blue: 0.98, alpha: 1).cgColor)
72
-            context.addPath(roundedRect(CGRect(x: paperRect.minX + s * 0.05, y: lineY, width: lineW, height: s * 0.025), radius: s * 0.01))
104
+            context.addPath(roundedRect(CGRect(x: paperRect.minX + layout.dim(0.05), y: lineY, width: lineW, height: layout.dim(0.025)), radius: layout.dim(0.01)))
73
             context.fillPath()
105
             context.fillPath()
74
         }
106
         }
75
 
107
 
76
-        let lightRect = CGRect(x: bodyRect.maxX - s * 0.14, y: bodyRect.minY + s * 0.08, width: s * 0.06, height: s * 0.06)
108
+        let lightRect = CGRect(x: bodyRect.maxX - layout.dim(0.14), y: bodyRect.minY + layout.dim(0.08), width: layout.dim(0.06), height: layout.dim(0.06))
77
         context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
109
         context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
78
         context.fillEllipse(in: lightRect)
110
         context.fillEllipse(in: lightRect)
79
     }
111
     }
@@ -81,22 +113,20 @@ final class FeatureIconView: NSView {
81
     // MARK: - Print Text
113
     // MARK: - Print Text
82
 
114
 
83
     private func drawPrintText(in context: CGContext) {
115
     private func drawPrintText(in context: CGContext) {
84
-        let s = min(bounds.width, bounds.height)
85
-        let ox = (bounds.width - s) / 2
86
-        let oy = (bounds.height - s) / 2
116
+        let layout = IconLayout(bounds: bounds)
87
 
117
 
88
-        let frameRect = CGRect(x: ox + s * 0.14, y: oy + s * 0.14, width: s * 0.72, height: s * 0.72)
89
-        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: s * 0.04, dy: -s * 0.02), radius: s * 0.03)
118
+        let frameRect = layout.rect(0.08, 0.08, 0.84, 0.84)
119
+        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: layout.dim(0.04), dy: -layout.dim(0.02)), radius: layout.dim(0.03))
90
 
120
 
91
         context.setStrokeColor(NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1).cgColor)
121
         context.setStrokeColor(NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1).cgColor)
92
-        context.setLineWidth(s * 0.025)
93
-        context.setLineDash(phase: 0, lengths: [s * 0.04, s * 0.04])
94
-        context.addPath(roundedRect(frameRect, radius: s * 0.04))
122
+        context.setLineWidth(layout.dim(0.03))
123
+        context.setLineDash(phase: 0, lengths: [layout.dim(0.04), layout.dim(0.04)])
124
+        context.addPath(roundedRect(frameRect, radius: layout.dim(0.04)))
95
         context.strokePath()
125
         context.strokePath()
96
         context.setLineDash(phase: 0, lengths: [])
126
         context.setLineDash(phase: 0, lengths: [])
97
 
127
 
98
         let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
128
         let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
99
-        let font = NSFont.systemFont(ofSize: s * 0.42, weight: .bold)
129
+        let font = NSFont.systemFont(ofSize: layout.dim(0.40), weight: .bold)
100
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
130
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
101
         let text = "T" as NSString
131
         let text = "T" as NSString
102
         let textSize = text.size(withAttributes: attrs)
132
         let textSize = text.size(withAttributes: attrs)
@@ -106,20 +136,18 @@ final class FeatureIconView: NSView {
106
     // MARK: - Print Contacts
136
     // MARK: - Print Contacts
107
 
137
 
108
     private func drawPrintContacts(in context: CGContext) {
138
     private func drawPrintContacts(in context: CGContext) {
109
-        let s = min(bounds.width, bounds.height)
110
-        let ox = (bounds.width - s) / 2
111
-        let oy = (bounds.height - s) / 2
139
+        let layout = IconLayout(bounds: bounds)
112
 
140
 
113
-        let bookRect = CGRect(x: ox + s * 0.12, y: oy + s * 0.18, width: s * 0.62, height: s * 0.68)
114
-        drawSoftShadow(in: context, rect: CGRect(x: bookRect.minX, y: bookRect.maxY - s * 0.03, width: bookRect.width, height: s * 0.05), radius: s * 0.025)
141
+        let bookRect = layout.rect(0.08, 0.08, 0.84, 0.84)
142
+        drawSoftShadow(in: context, rect: CGRect(x: bookRect.minX, y: bookRect.maxY - layout.dim(0.03), width: bookRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
115
 
143
 
116
         let greenTop = NSColor(red: 0.42, green: 0.86, blue: 0.52, alpha: 1)
144
         let greenTop = NSColor(red: 0.42, green: 0.86, blue: 0.52, alpha: 1)
117
         let greenBottom = NSColor(red: 0.18, green: 0.68, blue: 0.38, alpha: 1)
145
         let greenBottom = NSColor(red: 0.18, green: 0.68, blue: 0.38, alpha: 1)
118
-        fillGradient(in: context, path: roundedRect(bookRect, radius: s * 0.05), colors: [greenTop.cgColor, greenBottom.cgColor], start: CGPoint(x: bookRect.midX, y: bookRect.minY), end: CGPoint(x: bookRect.midX, y: bookRect.maxY))
146
+        fillGradient(in: context, path: roundedRect(bookRect, radius: layout.dim(0.05)), colors: [greenTop.cgColor, greenBottom.cgColor], start: CGPoint(x: bookRect.midX, y: bookRect.minY), end: CGPoint(x: bookRect.midX, y: bookRect.maxY))
119
 
147
 
120
-        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: s * 0.08, height: bookRect.height)
148
+        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: layout.dim(0.08), height: bookRect.height)
121
         context.setFillColor(NSColor(red: 0.12, green: 0.52, blue: 0.30, alpha: 1).cgColor)
149
         context.setFillColor(NSColor(red: 0.12, green: 0.52, blue: 0.30, alpha: 1).cgColor)
122
-        context.addPath(roundedRect(spineRect, radius: s * 0.02, topLeft: s * 0.05, topRight: 0, bottomLeft: s * 0.05, bottomRight: 0))
150
+        context.addPath(roundedRect(spineRect, radius: layout.dim(0.02), topLeft: layout.dim(0.05), topRight: 0, bottomLeft: layout.dim(0.05), bottomRight: 0))
123
         context.fillPath()
151
         context.fillPath()
124
 
152
 
125
         let tabColors: [NSColor] = [
153
         let tabColors: [NSColor] = [
@@ -128,42 +156,40 @@ final class FeatureIconView: NSView {
128
             NSColor(red: 0.96, green: 0.82, blue: 0.40, alpha: 1),
156
             NSColor(red: 0.96, green: 0.82, blue: 0.40, alpha: 1),
129
         ]
157
         ]
130
         for (i, color) in tabColors.enumerated() {
158
         for (i, color) in tabColors.enumerated() {
131
-            let tabRect = CGRect(x: bookRect.maxX - s * 0.02, y: bookRect.minY + s * 0.10 + CGFloat(i) * s * 0.14, width: s * 0.10, height: s * 0.10)
159
+            let tabRect = CGRect(x: bookRect.maxX - layout.dim(0.02), y: bookRect.minY + layout.dim(0.10) + CGFloat(i) * layout.dim(0.14), width: layout.dim(0.10), height: layout.dim(0.10))
132
             context.setFillColor(color.cgColor)
160
             context.setFillColor(color.cgColor)
133
-            context.addPath(roundedRect(tabRect, radius: s * 0.02, topLeft: 0, topRight: s * 0.02, bottomLeft: 0, bottomRight: s * 0.02))
161
+            context.addPath(roundedRect(tabRect, radius: layout.dim(0.02), topLeft: 0, topRight: layout.dim(0.02), bottomLeft: 0, bottomRight: layout.dim(0.02)))
134
             context.fillPath()
162
             context.fillPath()
135
         }
163
         }
136
 
164
 
137
-        let personCenter = CGPoint(x: bookRect.midX - s * 0.02, y: bookRect.midY + s * 0.04)
165
+        let personCenter = CGPoint(x: bookRect.midX - layout.dim(0.02), y: bookRect.midY + layout.dim(0.04))
138
         context.setFillColor(NSColor.white.cgColor)
166
         context.setFillColor(NSColor.white.cgColor)
139
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.10, y: personCenter.y - s * 0.16, width: s * 0.20, height: s * 0.20))
140
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.14, y: personCenter.y + s * 0.02, width: s * 0.28, height: s * 0.22))
167
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.10), y: personCenter.y - layout.dim(0.16), width: layout.dim(0.20), height: layout.dim(0.20)))
168
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.14), y: personCenter.y + layout.dim(0.02), width: layout.dim(0.28), height: layout.dim(0.22)))
141
     }
169
     }
142
 
170
 
143
     // MARK: - Print Website
171
     // MARK: - Print Website
144
 
172
 
145
     private func drawPrintWebsite(in context: CGContext) {
173
     private func drawPrintWebsite(in context: CGContext) {
146
-        let s = min(bounds.width, bounds.height)
147
-        let ox = (bounds.width - s) / 2
148
-        let oy = (bounds.height - s) / 2
174
+        let layout = IconLayout(bounds: bounds)
149
 
175
 
150
-        let globeRect = CGRect(x: ox + s * 0.10, y: oy + s * 0.14, width: s * 0.58, height: s * 0.58)
151
-        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -s * 0.03), radius: s * 0.03)
176
+        let globeRect = layout.rect(0.08, 0.08, 0.84, 0.84)
177
+        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -layout.dim(0.03)), radius: layout.dim(0.03))
152
 
178
 
153
         let blueTop = NSColor(red: 0.40, green: 0.70, blue: 1.0, alpha: 1)
179
         let blueTop = NSColor(red: 0.40, green: 0.70, blue: 1.0, alpha: 1)
154
         let blueBottom = NSColor(red: 0.16, green: 0.44, blue: 0.92, alpha: 1)
180
         let blueBottom = NSColor(red: 0.16, green: 0.44, blue: 0.92, alpha: 1)
155
         fillGradient(in: context, path: CGPath(ellipseIn: globeRect, transform: nil), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: globeRect.midX, y: globeRect.minY), end: CGPoint(x: globeRect.midX, y: globeRect.maxY))
181
         fillGradient(in: context, path: CGPath(ellipseIn: globeRect, transform: nil), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: globeRect.midX, y: globeRect.minY), end: CGPoint(x: globeRect.midX, y: globeRect.maxY))
156
 
182
 
157
         context.setStrokeColor(NSColor.white.withAlphaComponent(0.55).cgColor)
183
         context.setStrokeColor(NSColor.white.withAlphaComponent(0.55).cgColor)
158
-        context.setLineWidth(s * 0.018)
184
+        context.setLineWidth(layout.dim(0.018))
159
         context.strokeEllipse(in: globeRect.insetBy(dx: globeRect.width * 0.15, dy: 0))
185
         context.strokeEllipse(in: globeRect.insetBy(dx: globeRect.width * 0.15, dy: 0))
160
         context.strokeEllipse(in: globeRect.insetBy(dx: 0, dy: globeRect.height * 0.22))
186
         context.strokeEllipse(in: globeRect.insetBy(dx: 0, dy: globeRect.height * 0.22))
161
         context.move(to: CGPoint(x: globeRect.minX, y: globeRect.midY))
187
         context.move(to: CGPoint(x: globeRect.minX, y: globeRect.midY))
162
         context.addLine(to: CGPoint(x: globeRect.maxX, y: globeRect.midY))
188
         context.addLine(to: CGPoint(x: globeRect.maxX, y: globeRect.midY))
163
         context.strokePath()
189
         context.strokePath()
164
 
190
 
165
-        let cursorSize = s * 0.30
166
-        let cursorOrigin = CGPoint(x: ox + s * 0.52, y: oy + s * 0.38)
191
+        let cursorSize = layout.dim(0.22)
192
+        let cursorOrigin = CGPoint(x: layout.fill.maxX - layout.dim(0.12), y: layout.fill.minY + layout.fill.height * 0.42)
167
         context.setFillColor(NSColor.white.cgColor)
193
         context.setFillColor(NSColor.white.cgColor)
168
         let cursor = CGMutablePath()
194
         let cursor = CGMutablePath()
169
         cursor.move(to: cursorOrigin)
195
         cursor.move(to: cursorOrigin)
@@ -173,7 +199,7 @@ final class FeatureIconView: NSView {
173
         context.addPath(cursor)
199
         context.addPath(cursor)
174
         context.fillPath()
200
         context.fillPath()
175
         context.setStrokeColor(NSColor(red: 0.30, green: 0.50, blue: 0.90, alpha: 1).cgColor)
201
         context.setStrokeColor(NSColor(red: 0.30, green: 0.50, blue: 0.90, alpha: 1).cgColor)
176
-        context.setLineWidth(s * 0.02)
202
+        context.setLineWidth(layout.dim(0.02))
177
         context.addPath(cursor)
203
         context.addPath(cursor)
178
         context.strokePath()
204
         context.strokePath()
179
     }
205
     }
@@ -181,12 +207,10 @@ final class FeatureIconView: NSView {
181
     // MARK: - Draw & Print
207
     // MARK: - Draw & Print
182
 
208
 
183
     private func drawDrawPrint(in context: CGContext) {
209
     private func drawDrawPrint(in context: CGContext) {
184
-        let s = min(bounds.width, bounds.height)
185
-        let ox = (bounds.width - s) / 2
186
-        let oy = (bounds.height - s) / 2
210
+        let layout = IconLayout(bounds: bounds)
187
 
211
 
188
-        let paletteRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.72, height: s * 0.48)
189
-        drawSoftShadow(in: context, rect: CGRect(x: paletteRect.minX, y: paletteRect.maxY - s * 0.02, width: paletteRect.width, height: s * 0.04), radius: s * 0.025)
212
+        let paletteRect = layout.rect(0.06, 0.14, 0.80, 0.72)
213
+        drawSoftShadow(in: context, rect: CGRect(x: paletteRect.minX, y: paletteRect.maxY - layout.dim(0.02), width: paletteRect.width, height: layout.dim(0.04)), radius: layout.dim(0.025))
190
 
214
 
191
         let woodTop = NSColor(red: 0.96, green: 0.78, blue: 0.48, alpha: 1)
215
         let woodTop = NSColor(red: 0.96, green: 0.78, blue: 0.48, alpha: 1)
192
         let woodBottom = NSColor(red: 0.86, green: 0.58, blue: 0.28, alpha: 1)
216
         let woodBottom = NSColor(red: 0.86, green: 0.58, blue: 0.28, alpha: 1)
@@ -205,17 +229,17 @@ final class FeatureIconView: NSView {
205
             let angle = CGFloat(i) * .pi * 0.32 - .pi * 0.5
229
             let angle = CGFloat(i) * .pi * 0.32 - .pi * 0.5
206
             let radius = paletteRect.width * 0.28
230
             let radius = paletteRect.width * 0.28
207
             let dotCenter = CGPoint(x: thumbCenter.x + cos(angle) * radius, y: thumbCenter.y + sin(angle) * radius * 0.55)
231
             let dotCenter = CGPoint(x: thumbCenter.x + cos(angle) * radius, y: thumbCenter.y + sin(angle) * radius * 0.55)
208
-            let dotSize = s * 0.09
232
+            let dotSize = layout.dim(0.09)
209
             context.setFillColor(color.cgColor)
233
             context.setFillColor(color.cgColor)
210
             context.fillEllipse(in: CGRect(x: dotCenter.x - dotSize / 2, y: dotCenter.y - dotSize / 2, width: dotSize, height: dotSize))
234
             context.fillEllipse(in: CGRect(x: dotCenter.x - dotSize / 2, y: dotCenter.y - dotSize / 2, width: dotSize, height: dotSize))
211
         }
235
         }
212
 
236
 
213
         context.saveGState()
237
         context.saveGState()
214
-        context.translateBy(x: ox + s * 0.58, y: oy + s * 0.08)
238
+        context.translateBy(x: layout.fill.minX + layout.fill.width * 0.58, y: layout.fill.minY + layout.fill.height * 0.04)
215
         context.rotate(by: -.pi / 5)
239
         context.rotate(by: -.pi / 5)
216
-        let brushRect = CGRect(x: 0, y: 0, width: s * 0.10, height: s * 0.42)
217
-        fillGradient(in: context, path: roundedRect(brushRect, radius: s * 0.02), colors: [NSColor(red: 0.72, green: 0.48, blue: 0.28, alpha: 1).cgColor, NSColor(red: 0.52, green: 0.32, blue: 0.18, alpha: 1).cgColor], start: CGPoint(x: brushRect.midX, y: brushRect.minY), end: CGPoint(x: brushRect.midX, y: brushRect.maxY))
218
-        let bristleRect = CGRect(x: brushRect.midX - s * 0.05, y: brushRect.maxY - s * 0.02, width: s * 0.10, height: s * 0.10)
240
+        let brushRect = CGRect(x: 0, y: 0, width: layout.dim(0.10), height: layout.dim(0.44))
241
+        fillGradient(in: context, path: roundedRect(brushRect, radius: layout.dim(0.02)), colors: [NSColor(red: 0.72, green: 0.48, blue: 0.28, alpha: 1).cgColor, NSColor(red: 0.52, green: 0.32, blue: 0.18, alpha: 1).cgColor], start: CGPoint(x: brushRect.midX, y: brushRect.minY), end: CGPoint(x: brushRect.midX, y: brushRect.maxY))
242
+        let bristleRect = CGRect(x: brushRect.midX - layout.dim(0.05), y: brushRect.maxY - layout.dim(0.02), width: layout.dim(0.10), height: layout.dim(0.10))
219
         context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
243
         context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
220
         context.fillEllipse(in: bristleRect)
244
         context.fillEllipse(in: bristleRect)
221
         context.restoreGState()
245
         context.restoreGState()
@@ -232,34 +256,32 @@ final class FeatureIconView: NSView {
232
     // MARK: - OCR File
256
     // MARK: - OCR File
233
 
257
 
234
     private func drawOCRFile(in context: CGContext) {
258
     private func drawOCRFile(in context: CGContext) {
235
-        let s = min(bounds.width, bounds.height)
236
-        let ox = (bounds.width - s) / 2
237
-        let oy = (bounds.height - s) / 2
259
+        let layout = IconLayout(bounds: bounds)
238
 
260
 
239
-        let docRect = CGRect(x: ox + s * 0.18, y: oy + s * 0.10, width: s * 0.56, height: s * 0.72)
240
-        drawSoftShadow(in: context, rect: CGRect(x: docRect.minX, y: docRect.maxY - s * 0.03, width: docRect.width, height: s * 0.05), radius: s * 0.025)
261
+        let docRect = layout.rect(0.08, 0.06, 0.84, 0.88)
262
+        drawSoftShadow(in: context, rect: CGRect(x: docRect.minX, y: docRect.maxY - layout.dim(0.03), width: docRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
241
 
263
 
242
         context.setFillColor(NSColor.white.cgColor)
264
         context.setFillColor(NSColor.white.cgColor)
243
-        context.addPath(roundedRect(docRect, radius: s * 0.04))
265
+        context.addPath(roundedRect(docRect, radius: layout.dim(0.04)))
244
         context.fillPath()
266
         context.fillPath()
245
 
267
 
246
         let purpleTop = NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1)
268
         let purpleTop = NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1)
247
         let purpleBottom = NSColor(red: 0.45, green: 0.28, blue: 0.86, alpha: 1)
269
         let purpleBottom = NSColor(red: 0.45, green: 0.28, blue: 0.86, alpha: 1)
248
-        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: s * 0.18)
249
-        fillGradient(in: context, path: roundedRect(headerRect, radius: s * 0.04, topLeft: s * 0.04, topRight: s * 0.04, bottomLeft: 0, bottomRight: 0), colors: [purpleTop.cgColor, purpleBottom.cgColor], start: CGPoint(x: headerRect.midX, y: headerRect.minY), end: CGPoint(x: headerRect.midX, y: headerRect.maxY))
270
+        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: layout.dim(0.18))
271
+        fillGradient(in: context, path: roundedRect(headerRect, radius: layout.dim(0.04), topLeft: layout.dim(0.04), topRight: layout.dim(0.04), bottomLeft: 0, bottomRight: 0), colors: [purpleTop.cgColor, purpleBottom.cgColor], start: CGPoint(x: headerRect.midX, y: headerRect.minY), end: CGPoint(x: headerRect.midX, y: headerRect.maxY))
250
 
272
 
251
-        let font = NSFont.systemFont(ofSize: s * 0.14, weight: .bold)
273
+        let font = NSFont.systemFont(ofSize: layout.dim(0.14), weight: .bold)
252
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
274
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
253
         let text = "OCR" as NSString
275
         let text = "OCR" as NSString
254
         let textSize = text.size(withAttributes: attrs)
276
         let textSize = text.size(withAttributes: attrs)
255
-        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + s * 0.04), withAttributes: attrs)
277
+        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + layout.dim(0.04)), withAttributes: attrs)
256
 
278
 
257
         let lineColor = NSColor(red: 0.78, green: 0.72, blue: 0.96, alpha: 1)
279
         let lineColor = NSColor(red: 0.78, green: 0.72, blue: 0.96, alpha: 1)
258
         for i in 0..<4 {
280
         for i in 0..<4 {
259
-            let lineY = docRect.minY + s * 0.24 + CGFloat(i) * s * 0.10
281
+            let lineY = docRect.minY + layout.dim(0.24) + CGFloat(i) * layout.dim(0.10)
260
             let lineW = docRect.width * (0.80 - CGFloat(i) * 0.08)
282
             let lineW = docRect.width * (0.80 - CGFloat(i) * 0.08)
261
             context.setFillColor(lineColor.cgColor)
283
             context.setFillColor(lineColor.cgColor)
262
-            context.addPath(roundedRect(CGRect(x: docRect.minX + s * 0.08, y: lineY, width: lineW, height: s * 0.035), radius: s * 0.012))
284
+            context.addPath(roundedRect(CGRect(x: docRect.minX + layout.dim(0.08), y: lineY, width: lineW, height: layout.dim(0.035)), radius: layout.dim(0.012)))
263
             context.fillPath()
285
             context.fillPath()
264
         }
286
         }
265
     }
287
     }

+ 6 - 6
smart_printer/UIComponents.swift

@@ -308,12 +308,12 @@ final class FeatureCardView: NSView {
308
         applyCardShadow()
308
         applyCardShadow()
309
 
309
 
310
         let titleLabel = NSTextField(labelWithString: data.title)
310
         let titleLabel = NSTextField(labelWithString: data.title)
311
-        titleLabel.font = AppTheme.semiboldFont(size: 15)
311
+        titleLabel.font = AppTheme.semiboldFont(size: 14)
312
         titleLabel.textColor = AppTheme.textPrimary
312
         titleLabel.textColor = AppTheme.textPrimary
313
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
313
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
314
 
314
 
315
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
315
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
316
-        subtitleLabel.font = AppTheme.regularFont(size: 12)
316
+        subtitleLabel.font = AppTheme.regularFont(size: 11)
317
         subtitleLabel.textColor = AppTheme.textSecondary
317
         subtitleLabel.textColor = AppTheme.textSecondary
318
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
318
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
319
 
319
 
@@ -337,13 +337,13 @@ final class FeatureCardView: NSView {
337
         addSubview(arrowButton)
337
         addSubview(arrowButton)
338
 
338
 
339
         NSLayoutConstraint.activate([
339
         NSLayoutConstraint.activate([
340
-            heightAnchor.constraint(equalToConstant: 104),
340
+            heightAnchor.constraint(equalToConstant: 96),
341
 
341
 
342
-            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
342
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
343
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
343
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
344
 
344
 
345
-            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
346
-            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 22),
345
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
346
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
347
             titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
347
             titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
348
 
348
 
349
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
349
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),

+ 18 - 41
smart_printer/ViewController.swift

@@ -258,7 +258,15 @@ class ViewController: NSViewController {
258
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
258
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
259
         ]
259
         ]
260
 
260
 
261
-        let grid = makeFeatureGrid(features: features, columns: 4)
261
+        let topRow = makeFeatureRow(features: Array(features.prefix(4)))
262
+        let bottomRow = makeFeatureRow(features: Array(features.suffix(2)))
263
+
264
+        let grid = NSStackView(views: [topRow, bottomRow])
265
+        grid.orientation = .vertical
266
+        grid.spacing = AppTheme.featureGridSpacing
267
+        grid.distribution = .fillEqually
268
+        grid.translatesAutoresizingMaskIntoConstraints = false
269
+
262
         section.addSubview(grid)
270
         section.addSubview(grid)
263
 
271
 
264
         NSLayoutConstraint.activate([
272
         NSLayoutConstraint.activate([
@@ -274,49 +282,18 @@ class ViewController: NSViewController {
274
         return section
282
         return section
275
     }
283
     }
276
 
284
 
277
-    private func makeFeatureGrid(features: [FeatureCardData], columns: Int) -> NSView {
278
-        let grid = NSView()
279
-        grid.translatesAutoresizingMaskIntoConstraints = false
280
-
281
-        let spacing = AppTheme.featureGridSpacing
282
-        let cards = features.map { FeatureCardView(data: $0) }
283
-        let rowCount = (cards.count + columns - 1) / columns
284
-
285
-        for card in cards {
286
-            grid.addSubview(card)
287
-        }
288
-
289
-        for (index, card) in cards.enumerated() {
290
-            let row = index / columns
291
-            let col = index % columns
292
-            let columnAnchor = cards[col]
285
+    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
286
+        let row = NSStackView()
287
+        row.orientation = .horizontal
288
+        row.spacing = AppTheme.featureGridSpacing
289
+        row.distribution = .fillEqually
290
+        row.translatesAutoresizingMaskIntoConstraints = false
293
 
291
 
294
-            if col == 0 {
295
-                card.leadingAnchor.constraint(equalTo: grid.leadingAnchor).isActive = true
296
-            } else {
297
-                let leftCard = cards[index - 1]
298
-                card.leadingAnchor.constraint(equalTo: leftCard.trailingAnchor, constant: spacing).isActive = true
299
-            }
300
-
301
-            card.widthAnchor.constraint(equalTo: columnAnchor.widthAnchor).isActive = true
302
-
303
-            if row == 0 {
304
-                card.topAnchor.constraint(equalTo: grid.topAnchor).isActive = true
305
-            } else {
306
-                let aboveCard = cards[index - columns]
307
-                card.topAnchor.constraint(equalTo: aboveCard.bottomAnchor, constant: spacing).isActive = true
308
-            }
309
-
310
-            if col == columns - 1 {
311
-                card.trailingAnchor.constraint(equalTo: grid.trailingAnchor).isActive = true
312
-            }
313
-
314
-            if row == rowCount - 1 {
315
-                card.bottomAnchor.constraint(equalTo: grid.bottomAnchor).isActive = true
316
-            }
292
+        for data in features {
293
+            row.addArrangedSubview(FeatureCardView(data: data))
317
         }
294
         }
318
 
295
 
319
-        return grid
296
+        return row
320
     }
297
     }
321
 }
298
 }
322
 
299