diff --git a/Free Ruler.xcodeproj/project.pbxproj b/Free Ruler.xcodeproj/project.pbxproj index 077354e..8debbed 100644 --- a/Free Ruler.xcodeproj/project.pbxproj +++ b/Free Ruler.xcodeproj/project.pbxproj @@ -32,13 +32,15 @@ 50B1D3532D05D00100B1D135 /* RulerCursorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3522D05D00100B1D135 /* RulerCursorController.swift */; }; 50B1D3552D05E00000B1D138 /* RulerTickLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */; }; 50B1D3592D06000100B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; }; - 50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; }; 50B1D35D2D06000400B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; }; + 50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; }; 50B1D3612D06000700B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; }; 50B1D3652D06010100B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; }; 50B1D3662D06010200B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; }; 50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; }; 50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; }; + 50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; }; + 50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; }; 50C6D891228BDBAD0091F19E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50C6D890228BDBAD0091F19E /* Images.xcassets */; }; 50D7BEE7227D42FD0008B95E /* RulerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE6227D42FD0008B95E /* RulerController.swift */; }; 50D7BEE9227D43270008B95E /* Ruler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE8227D43270008B95E /* Ruler.swift */; }; @@ -118,6 +120,7 @@ 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGenerator.swift; sourceTree = ""; }; 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshotPreview.swift; sourceTree = ""; }; 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeHandleView.swift; sourceTree = ""; }; + 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitLabelView.swift; sourceTree = ""; }; 50C6D890228BDBAD0091F19E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 50D7BEE6227D42FD0008B95E /* RulerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RulerController.swift; sourceTree = ""; }; 50D7BEE8227D43270008B95E /* Ruler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ruler.swift; sourceTree = ""; }; @@ -139,7 +142,7 @@ 8F629823243003EA004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = ""; }; 8F629825243003F6004F9099 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreferencesController.xib; sourceTree = ""; }; 8F629828243003FF004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/PreferencesController.strings; sourceTree = ""; }; - AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = "Free Ruler.app"; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; }; B894A5002BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = ""; }; B894A5012BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/PreferencesController.strings; sourceTree = ""; }; D9DBE8A12C791B1600A42589 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = ""; }; @@ -252,6 +255,7 @@ 50D7BEEA227D432E0008B95E /* RulerWindow.swift */, 50D7BEEC227D5C810008B95E /* RuleView.swift */, 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */, + 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */, 50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */, 6F41029E22607DC900F06A10 /* HorizontalRule.swift */, 5012CAAC226AB09000BD9565 /* VerticalRule.swift */, @@ -479,6 +483,7 @@ 6F4102892260712F00F06A10 /* AppDelegate.swift in Sources */, 50D7BEED227D5C810008B95E /* RuleView.swift in Sources */, 50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */, + 50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */, 50D7BEE9227D43270008B95E /* Ruler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -504,6 +509,7 @@ 23E2B80975F07733EB6CF5DB /* AppDelegate.swift in Sources */, 9A082BBC4A583513B0858C41 /* RuleView.swift in Sources */, 50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */, + 50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */, E6BC6663BAF0D11D7A9D2195 /* Ruler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Free Ruler/AppDelegate.swift b/Free Ruler/AppDelegate.swift index 41c91ee..bd7c5cc 100644 --- a/Free Ruler/AppDelegate.swift +++ b/Free Ruler/AppDelegate.swift @@ -34,6 +34,12 @@ private enum HotkeyBezelLocalizationKey: String { case rulersUngrouped = "HotkeyBezel.RulersUngrouped" case shadowEnabled = "HotkeyBezel.ShadowEnabled" case shadowDisabled = "HotkeyBezel.ShadowDisabled" + case horizontalOriginFormat = "HotkeyBezel.HorizontalOriginFormat" + case verticalOriginFormat = "HotkeyBezel.VerticalOriginFormat" + case originLeft = "HotkeyBezel.OriginLeft" + case originRight = "HotkeyBezel.OriginRight" + case originTop = "HotkeyBezel.OriginTop" + case originBottom = "HotkeyBezel.OriginBottom" case unitsFormat = "HotkeyBezel.UnitsFormat" case pixelsUnit = "Unit.Pixels.Abbreviation" case millimetersUnit = "Unit.Millimeters.Abbreviation" @@ -57,6 +63,18 @@ private enum HotkeyBezelLocalizationKey: String { return "Hotkey status bezel text indicating ruler shadow is enabled" case .shadowDisabled: return "Hotkey status bezel text indicating ruler shadow is disabled" + case .horizontalOriginFormat: + return "Hotkey status bezel format for the horizontal ruler origin side" + case .verticalOriginFormat: + return "Hotkey status bezel format for the vertical ruler origin side" + case .originLeft: + return "Hotkey status bezel value for a ruler origin on the left" + case .originRight: + return "Hotkey status bezel value for a ruler origin on the right" + case .originTop: + return "Hotkey status bezel value for a ruler origin at the top" + case .originBottom: + return "Hotkey status bezel value for a ruler origin at the bottom" case .unitsFormat: return "Hotkey status bezel format for the selected measurement unit" case .pixelsUnit: @@ -127,7 +145,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { #endif showRulers() - } #if DEBUG @@ -218,6 +235,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in self.redrawRulers() }, + prefs.observe(\Prefs.zeroCorner, options: .new) { prefs, changed in + self.redrawRulers() + }, ] } @@ -262,6 +282,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { "backgroundOpacity", "rulerColor", "unit", + "zeroCorner", "NSWindow Frame horizontal-ruler", "NSWindow Frame vertical-ruler", "NSWindow Frame preferencesWindow", @@ -274,6 +295,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { prefs.backgroundOpacity = 50 prefs.rulerColor = Prefs.defaultRulerFillColor prefs.unit = .pixels + prefs.zeroCorner = Prefs.defaultZeroCorner } func createRulersIfNeeded() { @@ -479,15 +501,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func resetRulerPositions(_ sender: Any) { createRulersIfNeeded() + prefs.zeroCorner = Prefs.defaultZeroCorner + // ungroup rulers during reset operation - let groupRulers = prefs.groupRulers prefs.groupRulers = false for ruler in rulers { ruler.resetPosition() showRuler(ruler) } - // reset groupRulers to previous value - prefs.groupRulers = groupRulers + + prefs.groupRulers = Prefs.defaultGroupRulers updateRulerGrouping() } @@ -503,7 +526,108 @@ class AppDelegate: NSObject, NSApplicationDelegate { toggleRuler(orientation: .vertical) } - func performRulerHotkey(keyCode: Int, sender: Any) -> Bool { + @IBAction func flipHorizontalRuler(_ sender: Any) { + flipRulers(along: .horizontal) + showHorizontalOriginHotkeyBezel(on: bezelScreen(for: sender)) + } + + @IBAction func flipVerticalRuler(_ sender: Any) { + flipRulers(along: .vertical) + showVerticalOriginHotkeyBezel(on: bezelScreen(for: sender)) + } + + func flipRulers(along orientation: Orientation) { + createRulersIfNeeded() + + let oldGeometry = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner) + let flippedCorner = prefs.zeroCorner.flipped(along: orientation) + let flippedRuler = existingRulerController(orientation: orientation) + let otherOrientation: Orientation = orientation == .horizontal ? .vertical : .horizontal + let otherRuler = existingRulerController(orientation: otherOrientation) + let zeroPointOffset = zeroPointOffset( + from: flippedRuler?.rulerWindow, + to: otherRuler?.rulerWindow, + geometry: oldGeometry + ) + + prefs.zeroCorner = flippedCorner + + guard prefs.groupRulers, + let flippedWindow = flippedRuler?.rulerWindow, + let otherWindow = otherRuler?.rulerWindow, + isRulerWindowShown(otherWindow), + let zeroPointOffset = zeroPointOffset else { return } + + let newGeometry = ZeroCornerGeometry(zeroCorner: flippedCorner) + let flippedZeroPoint = newGeometry.zeroPoint(in: flippedWindow.frame, for: orientation) + let targetOtherZeroPoint = NSPoint( + x: flippedZeroPoint.x + zeroPointOffset.width, + y: flippedZeroPoint.y + zeroPointOffset.height + ) + let otherFrame = newGeometry.frame( + for: otherOrientation, + zeroPoint: targetOtherZeroPoint, + size: otherWindow.frame.size + ) + + detachRulerWindows() + otherWindow.setFrame(otherFrame, display: true) + updateRulerGrouping() + } + + func isRulerWindowShown(_ window: RulerWindow) -> Bool { + return window.isVisible || window.parent != nil || rulers.contains { + $0.rulerWindow.childWindows?.contains(window) == true + } + } + + private func zeroPointOffset( + from sourceWindow: RulerWindow?, + to targetWindow: RulerWindow?, + geometry: ZeroCornerGeometry + ) -> NSSize? { + guard let sourceWindow = sourceWindow, + let targetWindow = targetWindow else { return nil } + + let sourceZeroPoint = geometry.zeroPoint( + in: sourceWindow.frame, + for: sourceWindow.ruler.orientation + ) + let targetZeroPoint = geometry.zeroPoint( + in: targetWindow.frame, + for: targetWindow.ruler.orientation + ) + + return NSSize( + width: targetZeroPoint.x - sourceZeroPoint.x, + height: targetZeroPoint.y - sourceZeroPoint.y + ) + } + + func performRulerHotkey( + keyCode: Int, + modifierFlags: NSEvent.ModifierFlags, + sender: Any + ) -> Bool { + let keyboardModifiers = modifierFlags + .intersection(.deviceIndependentFlagsMask) + .subtracting(.capsLock) + + if keyboardModifiers == .shift { + switch keyCode { + case kVK_ANSI_H: + flipHorizontalRuler(sender) + case kVK_ANSI_V: + flipVerticalRuler(sender) + default: + return false + } + + return true + } + + guard keyboardModifiers.isEmpty else { return false } + switch keyCode { case kVK_ANSI_H: toggleHorizontalRuler(sender) @@ -538,6 +662,40 @@ class AppDelegate: NSObject, NSApplicationDelegate { showHotkeyBezel(prefs.groupRulers ? .rulersGrouped : .rulersUngrouped, on: screen) } + private func showHorizontalOriginHotkeyBezel(on screen: NSScreen?) { + switch prefs.zeroCorner { + case .topLeft, .bottomLeft: + showHotkeyBezel( + format: .horizontalOriginFormat, + HotkeyBezelLocalizationKey.originLeft.localizedString, + on: screen + ) + case .topRight, .bottomRight: + showHotkeyBezel( + format: .horizontalOriginFormat, + HotkeyBezelLocalizationKey.originRight.localizedString, + on: screen + ) + } + } + + private func showVerticalOriginHotkeyBezel(on screen: NSScreen?) { + switch prefs.zeroCorner { + case .topLeft, .topRight: + showHotkeyBezel( + format: .verticalOriginFormat, + HotkeyBezelLocalizationKey.originTop.localizedString, + on: screen + ) + case .bottomLeft, .bottomRight: + showHotkeyBezel( + format: .verticalOriginFormat, + HotkeyBezelLocalizationKey.originBottom.localizedString, + on: screen + ) + } + } + private func bezelScreen(for sender: Any) -> NSScreen? { if let rulerController = sender as? RulerController { return rulerController.rulerWindow.screen diff --git a/Free Ruler/AppStoreScreenshotPreview.swift b/Free Ruler/AppStoreScreenshotPreview.swift index 0dae12d..5b19422 100644 --- a/Free Ruler/AppStoreScreenshotPreview.swift +++ b/Free Ruler/AppStoreScreenshotPreview.swift @@ -3,10 +3,6 @@ import Cocoa import SwiftUI private struct AppStoreScreenshotPalette { - let screen1Background = #colorLiteral(red: 0.385, green: 0.49, blue: 0.7, alpha: 1) - let screen2Background = #colorLiteral(red: 0.3084420562, green: 0.521068275, blue: 0.509829402, alpha: 1) - let screen3Background = AppStoreScreenshotLayout.screen3BackgroundColor - let screen4Background = #colorLiteral(red: 0.5181607008, green: 0.4312165375, blue: 0.6487324834, alpha: 1) let text = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) let secondaryText = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.78) let darkText = #colorLiteral(red: 0.13, green: 0.14, blue: 0.17, alpha: 1) @@ -29,92 +25,166 @@ private enum AppStoreScreenshotFontFamily { case helveticaNeue } -private enum AppStoreScreenshotScreen { - case screen1 - case screen2 - case screen3 - case screen4 +private enum AppStoreScreenshotScenario { + case measure + case units + case colors + case flipRulers + case preferences +} - func background(in palette: AppStoreScreenshotPalette) -> NSColor { - switch self { - case .screen1: - return palette.screen1Background - case .screen2: - return palette.screen2Background - case .screen3: - return palette.screen3Background - case .screen4: - return palette.screen4Background - } - } +private struct AppStoreScreenshotScreen { + let scenario: AppStoreScreenshotScenario + let title: String + let subtitle: String + let previewName: String + let outputFilename: String + let backgroundColor: NSColor + let usesDarkCopy: Bool + + static let measure = AppStoreScreenshotScreen( + scenario: .measure, + title: AppStoreMeasureScreenshotLayout.title, + subtitle: AppStoreMeasureScreenshotLayout.subtitle, + previewName: AppStoreMeasureScreenshotLayout.previewName, + outputFilename: AppStoreMeasureScreenshotLayout.outputFilename, + backgroundColor: AppStoreMeasureScreenshotLayout.backgroundColor, + usesDarkCopy: false + ) + + static let units = AppStoreScreenshotScreen( + scenario: .units, + title: AppStoreUnitsScreenshotLayout.title, + subtitle: AppStoreUnitsScreenshotLayout.subtitle, + previewName: AppStoreUnitsScreenshotLayout.previewName, + outputFilename: AppStoreUnitsScreenshotLayout.outputFilename, + backgroundColor: AppStoreUnitsScreenshotLayout.backgroundColor, + usesDarkCopy: false + ) + + static let colors = AppStoreScreenshotScreen( + scenario: .colors, + title: AppStoreColorsScreenshotLayout.title, + subtitle: AppStoreColorsScreenshotLayout.subtitle, + previewName: AppStoreColorsScreenshotLayout.previewName, + outputFilename: AppStoreColorsScreenshotLayout.outputFilename, + backgroundColor: AppStoreColorsScreenshotLayout.backgroundColor, + usesDarkCopy: true + ) + + static let flipRulers = AppStoreScreenshotScreen( + scenario: .flipRulers, + title: AppStoreFlipScreenshotLayout.title, + subtitle: AppStoreFlipScreenshotLayout.subtitle, + previewName: AppStoreFlipScreenshotLayout.previewName, + outputFilename: AppStoreFlipScreenshotLayout.outputFilename, + backgroundColor: AppStoreFlipScreenshotLayout.backgroundColor, + usesDarkCopy: false + ) + + static let preferences = AppStoreScreenshotScreen( + scenario: .preferences, + title: AppStorePreferencesScreenshotLayout.title, + subtitle: AppStorePreferencesScreenshotLayout.subtitle, + previewName: AppStorePreferencesScreenshotLayout.previewName, + outputFilename: AppStorePreferencesScreenshotLayout.outputFilename, + backgroundColor: AppStorePreferencesScreenshotLayout.backgroundColor, + usesDarkCopy: false + ) + + static let allInOutputOrder: [AppStoreScreenshotScreen] = [ + .measure, + .colors, + .flipRulers, + .units, + .preferences, + ] - var headline: String { - switch self { - case .screen1: - return "A ruler for your Mac" - case .screen2: - return "Switch units instantly" - case .screen3: - return "Color your world" - case .screen4: - return "Pick your preferences" - } + func titleColor(in palette: AppStoreScreenshotPalette) -> NSColor { + usesDarkCopy ? palette.darkText : palette.text } - var description: String { - switch self { - case .screen1: - return "Measure anything on your screen." - case .screen2: - return "Use pixels, millimeters, or inches." - case .screen3: - return "Follow your heart. Be hue you want to be." - case .screen4: - return "Change color, opacity, and more." - } + func subtitleColor(in palette: AppStoreScreenshotPalette) -> NSColor { + usesDarkCopy ? palette.darkSecondaryText : palette.secondaryText } +} - var previewName: String { - switch self { - case .screen1: - return "Screen 1 - Measure anything" - case .screen2: - return "Screen 2 - Units" - case .screen3: - return "Screen 3 - Colors" - case .screen4: - return "Screen 4 - Preferences" +private struct AppStoreFlipRulerSet { + let zeroCorner: ZeroCorner + let x: CGFloat + let y: CGFloat + let horizontalLength: CGFloat + let verticalLength: CGFloat + let fillColor: NSColor + let layoutSize: NSSize + + func horizontalFrame(rulerScale: CGFloat) -> NSRect { + let x = zeroCornerX + let y = zeroCornerY + let thickness = Ruler.thickness * rulerScale + let overlap = rulerScale + let frameX: CGFloat + let frameY: CGFloat + + switch zeroCorner { + case .topLeft: + frameX = x - overlap + frameY = y - thickness + case .topRight: + frameX = x - horizontalLength + frameY = y - thickness + case .bottomLeft: + frameX = x - overlap + frameY = y - overlap + case .bottomRight: + frameX = x - horizontalLength + frameY = y - overlap } - } - var outputFilename: String { - switch self { - case .screen1: - return "01-measure-anything.png" - case .screen2: - return "03-switch-units.png" - case .screen3: - return "02-custom-colors.png" - case .screen4: - return "04-customize-rulers.png" + return NSRect(x: frameX, y: frameY, width: horizontalLength, height: thickness) + } + + func verticalFrame(rulerScale: CGFloat) -> NSRect { + let x = zeroCornerX + let y = zeroCornerY + let thickness = Ruler.thickness * rulerScale + let overlap = rulerScale + let frameX: CGFloat + let frameY: CGFloat + + switch zeroCorner { + case .topLeft: + frameX = x - thickness + frameY = y - overlap + case .topRight: + frameX = x - overlap + frameY = y - overlap + case .bottomLeft: + frameX = x - thickness + frameY = y - verticalLength + case .bottomRight: + frameX = x - overlap + frameY = y - verticalLength } + + return NSRect(x: frameX, y: frameY, width: thickness, height: verticalLength) } - func headlineColor(in palette: AppStoreScreenshotPalette) -> NSColor { - switch self { - case .screen3: - return palette.darkText - case .screen1, .screen2, .screen4: - return palette.text + private var zeroCornerX: CGFloat { + switch zeroCorner { + case .topLeft, .bottomLeft: + return x + case .topRight, .bottomRight: + return layoutSize.width - x } } - func descriptionColor(in palette: AppStoreScreenshotPalette) -> NSColor { - switch self { - case .screen3: - return palette.darkSecondaryText - case .screen1, .screen2, .screen4: - return palette.secondaryText + private var zeroCornerY: CGFloat { + switch zeroCorner { + case .topLeft, .topRight: + return y + case .bottomLeft, .bottomRight: + return layoutSize.height - y } } } @@ -126,7 +196,7 @@ enum AppStoreScreenshotRenderer { withIntermediateDirectories: true ) - for screen in screens { + for screen in AppStoreScreenshotScreen.allInOutputOrder { let image = try render(screen: screen) let outputURL = outputDirectory.appendingPathComponent(screen.outputFilename) try writePNG(image, to: outputURL) @@ -134,13 +204,6 @@ enum AppStoreScreenshotRenderer { } } - private static let screens: [AppStoreScreenshotScreen] = [ - .screen1, - .screen3, - .screen2, - .screen4, - ] - private static func render(screen: AppStoreScreenshotScreen) throws -> NSImage { let view = AppStoreScreenshotScenarioNSView(screen: screen) view.frame = NSRect(origin: .zero, size: AppStoreScreenshotLayout.canvasSize) @@ -196,39 +259,77 @@ enum AppStoreScreenshotRendererError: LocalizedError { } } +private struct AppStoreScreenshotCopyViewLayout { + let viewX: CGFloat + let viewY: CGFloat + let iconX: CGFloat + let iconY: CGFloat + let iconSize: CGFloat + let titleX: CGFloat + let titleY: CGFloat + let titleWidth: CGFloat + let titleHeight: CGFloat + let subtitleX: CGFloat + let subtitleY: CGFloat + let subtitleWidth: CGFloat + let subtitleHeight: CGFloat + + var frame: NSRect { + NSRect(x: viewX, y: viewY, width: boundsSize.width, height: boundsSize.height) + } + + var boundsSize: NSSize { + NSSize( + width: max(iconX + iconSize, titleX + titleWidth, subtitleX + subtitleWidth), + height: max(iconY + iconSize, titleY + titleHeight, subtitleY + subtitleHeight) + ) + } + + var iconRect: NSRect { + rect(x: iconX, y: iconY, width: iconSize, height: iconSize) + } + + var titleRect: NSRect { + rect(x: titleX, y: titleY, width: titleWidth, height: titleHeight) + } + + var subtitleRect: NSRect { + rect(x: subtitleX, y: subtitleY, width: subtitleWidth, height: subtitleHeight) + } + + private func rect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> NSRect { + NSRect(x: x, y: y, width: width, height: height) + } +} + private enum AppStoreScreenshotLayout { static let canvasWidth: CGFloat = 2880 static let canvasHeight: CGFloat = 1800 static let previewWidth: CGFloat = 960 static let previewHeight: CGFloat = 600 - static let iconX: CGFloat = 640 - static let iconY: CGFloat = 80 - static let iconSize: CGFloat = 360 - - static let headlineX: CGFloat = 1050 - static let headlineY: CGFloat = 140 - static let headlineWidth: CGFloat = 1800 - static let headlineHeight: CGFloat = 120 - static let headlineFontSize: CGFloat = 100 - static let headlineFontWeight: NSFont.Weight = .semibold - - static let descriptionX: CGFloat = 1050 - static let descriptionY: CGFloat = 270 - static let descriptionWidth: CGFloat = 1600 - static let descriptionHeight: CGFloat = 100 - static let descriptionFontSize: CGFloat = 80 + static let copyViewX: CGFloat = 640 + static let copyViewY: CGFloat = 80 + static let copyIconX: CGFloat = 0 + static let copyIconY: CGFloat = 0 + static let copyIconSize: CGFloat = 360 + static let copyTitleX: CGFloat = 410 + static let copyTitleY: CGFloat = 60 + static let titleWidth: CGFloat = 1800 + static let titleHeight: CGFloat = 120 + static let titleFontSize: CGFloat = 100 + static let titleFontWeight: NSFont.Weight = .semibold + + static let copySubtitleX: CGFloat = 410 + static let copySubtitleY: CGFloat = 190 + static let subtitleWidth: CGFloat = 1600 + static let subtitleHeight: CGFloat = 100 + static let subtitleFontSize: CGFloat = 80 static let textFontFamily: AppStoreScreenshotFontFamily = .system static let textFontWeight: NSFont.Weight = .regular - static let rulerScale: CGFloat = 4.4 static let rulerBorderWidth: CGFloat = 1 - static let rulerCornerX: CGFloat = 420 - static let rulerCornerY: CGFloat = 500 - static let horizontalRulerLength: CGFloat = 1850 - static let verticalRulerLength: CGFloat = 1040 - static let sampleWindowCornerRadius: CGFloat = 34 static let sampleWindowBorderWidth: CGFloat = 1 static let sampleWindowShadowBlur: CGFloat = 26 @@ -236,13 +337,6 @@ private enum AppStoreScreenshotLayout { static let sampleWindowShadowYOffset: CGFloat = 12 static let sampleWindowShadowOpacity: CGFloat = 0.28 - static let screen1BoxGutter: CGFloat = 70 - static let screen1Box1Width: CGFloat = 800 - static let screen1Box2Height: CGFloat = 180 - static let screen1BoxBorderColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.24) - static let screen1BoxBorderWidth: CGFloat = 4 - static let screen1BoxBorderRadius: CGFloat = 16 - static let titlebarHorizontalOutset: CGFloat = 0 static let titlebarHeight: CGFloat = 150 static let trafficLightDiameter: CGFloat = 48 @@ -250,78 +344,52 @@ private enum AppStoreScreenshotLayout { static let trafficLightXPadding: CGFloat = 56 static let trafficLightYPadding: CGFloat = 52 - static let screen2RulerScale: CGFloat = 6 - static let screen2RulerX: CGFloat = 550 - static let screen2RulerXOffset: CGFloat = -200 - static let screen2FirstRulerY: CGFloat = 620 - static let screen2RulerVerticalSpacing: CGFloat = 350 - static let screen2RulerLength: CGFloat = 2200 - - static let screen3BackgroundColor = #colorLiteral(red: 0.875857736, green: 0.8972384907, blue: 0.94, alpha: 1) - static let screen3RulerOpacity: CGFloat = 1 - static let screen3RulerCount = 7 - static let screen3RulerGap: CGFloat = 30 - static let screen3FirstRulerStart: CGFloat = screen3RulerGap - static let screen3LastRulerEnd: CGFloat = canvasWidth - screen3RulerGap - static let screen3RulerArcTop: CGFloat = 450 - static let screen3RulerArcBottom: CGFloat = 1000 - static let screen3RulerCurvature: CGFloat = 1.5 - static let screen3RulerOffscreenBottom: CGFloat = 2230 - static let screen3RulerBorderWidth: CGFloat = 2 - static let screen3RulerShadowBlur: CGFloat = 0 - static let screen3RulerShadowXOffset: CGFloat = 0 - static let screen3RulerShadowYOffset: CGFloat = 0 - static let screen3RulerShadowOpacity: CGFloat = 0 - static let screen3RulerColors = [ - #colorLiteral(red: 0.961, green: 0.294, blue: 0.333, alpha: 1), - #colorLiteral(red: 0.984, green: 0.545, blue: 0.224, alpha: 1), - #colorLiteral(red: 0.957, green: 0.796, blue: 0.247, alpha: 1), - #colorLiteral(red: 0.322, green: 0.686, blue: 0.416, alpha: 1), - #colorLiteral(red: 0.227, green: 0.62, blue: 0.804, alpha: 1), - #colorLiteral(red: 0.357, green: 0.408, blue: 0.827, alpha: 1), - #colorLiteral(red: 0.667, green: 0.404, blue: 0.753, alpha: 1), - ] - - static let screen4RulerScale: CGFloat = 6 - static let screen4RulerOpacity: CGFloat = 0.75 - static let screen4VerticalRulerX: CGFloat = 250 - static let screen4VerticalRulerY: CGFloat = 170 - static let screen4VerticalRulerLength: CGFloat = 2050 - static let screen4HorizontalRulerX: CGFloat = 50 - static let screen4HorizontalRulerY: CGFloat = 1180 - static let screen4HorizontalRulerLength: CGFloat = 3000 - static let screen4PreferencesWindowX: CGFloat = 680 - static let screen4PreferencesWindowY: CGFloat = 540 - static let screen4PreferencesWindowScale: CGFloat = 4 - static let screen4PreferencesContentWidth: CGFloat = 350 - static let screen4PreferencesContentHeight: CGFloat = 333 - static let screen4PreferencesWindowShadowOpacity: CGFloat = 0.28 - static let screen4PreferencesWindowShadowYOffset: CGFloat = -5 - - static var screen4ForegroundOpacityPercent: Int { - Int((screen4RulerOpacity * 100).rounded()) - } - - static let screen4BackgroundOpacityPercent = 50 - static let screen4FloatRulers = true - static let screen4GroupRulers = true - static let screen4RulerShadow = false - static var canvasSize: NSSize { NSSize(width: canvasWidth, height: canvasHeight) } - static var iconRect: NSRect { - NSRect(x: iconX, y: iconY, width: iconSize, height: iconSize) + static var copyViewLayout: AppStoreScreenshotCopyViewLayout { + copyViewLayout(viewX: copyViewX, viewY: copyViewY) + } + + static func copyViewLayout(viewX: CGFloat, viewY: CGFloat) -> AppStoreScreenshotCopyViewLayout { + AppStoreScreenshotCopyViewLayout( + viewX: viewX, + viewY: viewY, + iconX: copyIconX, + iconY: copyIconY, + iconSize: copyIconSize, + titleX: copyTitleX, + titleY: copyTitleY, + titleWidth: titleWidth, + titleHeight: titleHeight, + subtitleX: copySubtitleX, + subtitleY: copySubtitleY, + subtitleWidth: subtitleWidth, + subtitleHeight: subtitleHeight + ) } +} - static var headlineRect: NSRect { - NSRect(x: headlineX, y: headlineY, width: headlineWidth, height: headlineHeight) - } +private enum AppStoreMeasureScreenshotLayout { + static let title = "A ruler for your Mac" + static let subtitle = "Measure anything on your screen." + static let previewName = "Measure anything" + static let outputFilename = "01-measure-anything.png" + static let backgroundColor = #colorLiteral(red: 0.385, green: 0.49, blue: 0.7, alpha: 1) - static var descriptionRect: NSRect { - NSRect(x: descriptionX, y: descriptionY, width: descriptionWidth, height: descriptionHeight) - } + static let rulerScale: CGFloat = 4.4 + static let rulerCornerX: CGFloat = 420 + static let rulerCornerY: CGFloat = 500 + static let horizontalRulerLength: CGFloat = 1850 + static let verticalRulerLength: CGFloat = 1040 + + static let boxGutter: CGFloat = 70 + static let box1Width: CGFloat = 800 + static let box2Height: CGFloat = 180 + static let boxBorderColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.24) + static let boxBorderWidth: CGFloat = 4 + static let boxBorderRadius: CGFloat = 16 static var scaledRulerThickness: CGFloat { Ruler.thickness * rulerScale @@ -354,161 +422,317 @@ private enum AppStoreScreenshotLayout { ) } - static var titlebarRect: NSRect { - NSRect( - x: sampleWindowRect.minX - titlebarHorizontalOutset, - y: sampleWindowRect.minY, - width: sampleWindowRect.width + (titlebarHorizontalOutset * 2), - height: titlebarHeight - ) - } - static var sampleWindowContentRect: NSRect { NSRect( x: sampleWindowRect.minX, - y: sampleWindowRect.minY + titlebarHeight, + y: sampleWindowRect.minY + AppStoreScreenshotLayout.titlebarHeight, width: sampleWindowRect.width, - height: sampleWindowRect.height - titlebarHeight + height: sampleWindowRect.height - AppStoreScreenshotLayout.titlebarHeight ) } - static var screen1Box1Rect: NSRect { + static var box1Rect: NSRect { NSRect( - x: sampleWindowContentRect.minX + screen1BoxGutter, - y: sampleWindowContentRect.minY + screen1BoxGutter, - width: screen1Box1Width, - height: sampleWindowContentRect.height - (screen1BoxGutter * 2) + x: sampleWindowContentRect.minX + boxGutter, + y: sampleWindowContentRect.minY + boxGutter, + width: box1Width, + height: sampleWindowContentRect.height - (boxGutter * 2) ) } - static var screen1Box2Rect: NSRect { - let x = screen1Box1Rect.maxX + screen1BoxGutter + static var box2Rect: NSRect { + let x = box1Rect.maxX + boxGutter return NSRect( x: x, - y: sampleWindowContentRect.minY + screen1BoxGutter, - width: sampleWindowContentRect.maxX - x - screen1BoxGutter, - height: screen1Box2Height + y: sampleWindowContentRect.minY + boxGutter, + width: sampleWindowContentRect.maxX - x - boxGutter, + height: box2Height ) } - static var screen1Box3Rect: NSRect { - let y = screen1Box2Rect.maxY + screen1BoxGutter + static var box3Rect: NSRect { + let y = box2Rect.maxY + boxGutter return NSRect( - x: screen1Box2Rect.minX, + x: box2Rect.minX, y: y, - width: screen1Box2Rect.width, - height: sampleWindowContentRect.maxY - y - screen1BoxGutter + width: box2Rect.width, + height: sampleWindowContentRect.maxY - y - boxGutter ) } +} - static var trafficLightOrigin: CGPoint { - CGPoint( - x: sampleWindowRect.minX + trafficLightXPadding, - y: sampleWindowRect.minY + trafficLightYPadding - ) - } +private enum AppStoreUnitsScreenshotLayout { + static let title = "Switch units instantly" + static let subtitle = "Use pixels, millimeters, or inches." + static let previewName = "Units" + static let outputFilename = "04-switch-units.png" + static let backgroundColor = #colorLiteral(red: 0.3084420562, green: 0.521068275, blue: 0.509829402, alpha: 1) + + static let rulerScale: CGFloat = 6 + static let rulerX: CGFloat = 550 + static let rulerXOffset: CGFloat = -200 + static let firstRulerY: CGFloat = 620 + static let rulerVerticalSpacing: CGFloat = 350 + static let rulerLength: CGFloat = 2200 - static var screen2ScaledRulerThickness: CGFloat { - Ruler.thickness * screen2RulerScale + static var scaledRulerThickness: CGFloat { + Ruler.thickness * rulerScale } - static func screen2RulerRect(index: Int) -> NSRect { + static func rulerRect(index: Int) -> NSRect { NSRect( - x: screen2RulerX + CGFloat(index) * screen2RulerXOffset, - y: screen2FirstRulerY + CGFloat(index) * screen2RulerVerticalSpacing, - width: screen2RulerLength, - height: screen2ScaledRulerThickness + x: rulerX + CGFloat(index) * rulerXOffset, + y: firstRulerY + CGFloat(index) * rulerVerticalSpacing, + width: rulerLength, + height: scaledRulerThickness ) } +} - static var screen3RulerScale: CGFloat { - screen3ScaledRulerThickness / Ruler.thickness +private enum AppStoreColorsScreenshotLayout { + static let title = "Color your world" + static let subtitle = "Follow your heart. Be hue you want to be." + static let previewName = "Colors" + static let outputFilename = "02-custom-colors.png" + static let backgroundColor = #colorLiteral(red: 0.875857736, green: 0.8972384907, blue: 0.94, alpha: 1) + + static let rulerOpacity: CGFloat = 1 + static let rulerCount = 7 + static let rulerGap: CGFloat = 30 + static let firstRulerStart: CGFloat = rulerGap + static let lastRulerEnd: CGFloat = AppStoreScreenshotLayout.canvasWidth - rulerGap + static let rulerArcTop: CGFloat = 450 + static let rulerArcBottom: CGFloat = 1000 + static let rulerCurvature: CGFloat = 1.5 + static let rulerOffscreenBottom: CGFloat = 2230 + static let rulerBorderWidth: CGFloat = 2 + static let rulerShadowBlur: CGFloat = 0 + static let rulerShadowXOffset: CGFloat = 0 + static let rulerShadowYOffset: CGFloat = 0 + static let rulerShadowOpacity: CGFloat = 0 + static let rulerColors = [ + #colorLiteral(red: 0.961, green: 0.294, blue: 0.333, alpha: 1), + #colorLiteral(red: 0.984, green: 0.545, blue: 0.224, alpha: 1), + #colorLiteral(red: 0.957, green: 0.796, blue: 0.247, alpha: 1), + #colorLiteral(red: 0.322, green: 0.686, blue: 0.416, alpha: 1), + #colorLiteral(red: 0.227, green: 0.62, blue: 0.804, alpha: 1), + #colorLiteral(red: 0.357, green: 0.408, blue: 0.827, alpha: 1), + #colorLiteral(red: 0.667, green: 0.404, blue: 0.753, alpha: 1), + ] + + static var rulerScale: CGFloat { + scaledRulerThickness / Ruler.thickness } - private static var screen3AvailableRulerWidth: CGFloat { - screen3LastRulerEnd - screen3FirstRulerStart - (CGFloat(screen3RulerCount - 1) * screen3RulerGap) + private static var availableRulerWidth: CGFloat { + lastRulerEnd - firstRulerStart - (CGFloat(rulerCount - 1) * rulerGap) } - static var screen3ScaledRulerThickness: CGFloat { - screen3AvailableRulerWidth / CGFloat(screen3RulerCount) + static var scaledRulerThickness: CGFloat { + availableRulerWidth / CGFloat(rulerCount) } - static func screen3RulerRect(index: Int) -> NSRect { - let top = screen3RulerTopY(index: index) + static func rulerRect(index: Int) -> NSRect { + let top = rulerTopY(index: index) return NSRect( - x: screen3FirstRulerStart + CGFloat(index) * (screen3ScaledRulerThickness + screen3RulerGap), + x: firstRulerStart + CGFloat(index) * (scaledRulerThickness + rulerGap), y: top, - width: screen3ScaledRulerThickness, - height: screen3RulerOffscreenBottom - top + width: scaledRulerThickness, + height: rulerOffscreenBottom - top ) } - static func screen3RulerTopY(index: Int) -> CGFloat { - guard screen3RulerCount > 1 else { return screen3RulerArcTop } + static func rulerTopY(index: Int) -> CGFloat { + guard rulerCount > 1 else { return rulerArcTop } - let midpoint = CGFloat(screen3RulerCount - 1) / 2 + let midpoint = CGFloat(rulerCount - 1) / 2 let distanceFromCenter = abs(CGFloat(index) - midpoint) / midpoint - let curvedDistance = pow(distanceFromCenter, screen3RulerCurvature) - return screen3RulerArcTop + (screen3RulerArcBottom - screen3RulerArcTop) * curvedDistance + let curvedDistance = pow(distanceFromCenter, rulerCurvature) + return rulerArcTop + (rulerArcBottom - rulerArcTop) * curvedDistance + } +} + +private enum AppStoreFlipScreenshotLayout { + static let previewName = "Flip Rulers" + static let title = "Don’t flip out. Or do." + static let subtitle = "Your rulers, in any orientation." + static let outputFilename = "03-flip-rulers.png" + static let backgroundColor = #colorLiteral(red: 0.7959441489, green: 0.3479741865, blue: 0.516690138, alpha: 1) + + static let rulerScale: CGFloat = 4.5 + static let copyViewX: CGFloat = 700 + static let copyViewY: CGFloat = 700 + + // X/Y inset each set's zero corner from its named canvas corner. + static let topLeftX: CGFloat = 280 + static let topLeftY: CGFloat = 280 + static let topLeftHorizontalLength: CGFloat = 2200 + static let topLeftVerticalLength: CGFloat = 1354 + static let topLeftRulerColor = #colorLiteral(red: 0.95, green: 0.7125, blue: 0.8019480475, alpha: 1) + + static let topRightX: CGFloat = 520 + static let topRightY: CGFloat = 520 + static let topRightHorizontalLength: CGFloat = 1783 + static let topRightVerticalLength: CGFloat = 900 + static let topRightRulerColor = #colorLiteral(red: 0.98, green: 0.735, blue: 0.8272727226, alpha: 1) + + static let bottomRightX: CGFloat = 280 + static let bottomRightY: CGFloat = 280 + static let bottomRightHorizontalLength: CGFloat = 2200 + static let bottomRightVerticalLength: CGFloat = 1354 + static let bottomRightRulerColor = #colorLiteral(red: 0.95, green: 0.7125, blue: 0.8019480475, alpha: 1) + + static let bottomLeftX: CGFloat = 530 + static let bottomLeftY: CGFloat = 515 + static let bottomLeftHorizontalLength: CGFloat = 1783 + static let bottomLeftVerticalLength: CGFloat = 900 + static let bottomLeftRulerColor = #colorLiteral(red: 0.98, green: 0.735, blue: 0.8272727227, alpha: 1) + + static var copyViewLayout: AppStoreScreenshotCopyViewLayout { + AppStoreScreenshotLayout.copyViewLayout(viewX: copyViewX, viewY: copyViewY) + } + + static var rulerSets: [AppStoreFlipRulerSet] { + let layoutSize = AppStoreScreenshotLayout.canvasSize + return [ + AppStoreFlipRulerSet( + zeroCorner: .topLeft, + x: topLeftX, + y: topLeftY, + horizontalLength: topLeftHorizontalLength, + verticalLength: topLeftVerticalLength, + fillColor: topLeftRulerColor, + layoutSize: layoutSize + ), + AppStoreFlipRulerSet( + zeroCorner: .topRight, + x: topRightX, + y: topRightY, + horizontalLength: topRightHorizontalLength, + verticalLength: topRightVerticalLength, + fillColor: topRightRulerColor, + layoutSize: layoutSize + ), + AppStoreFlipRulerSet( + zeroCorner: .bottomLeft, + x: bottomLeftX, + y: bottomLeftY, + horizontalLength: bottomLeftHorizontalLength, + verticalLength: bottomLeftVerticalLength, + fillColor: bottomLeftRulerColor, + layoutSize: layoutSize + ), + AppStoreFlipRulerSet( + zeroCorner: .bottomRight, + x: bottomRightX, + y: bottomRightY, + horizontalLength: bottomRightHorizontalLength, + verticalLength: bottomRightVerticalLength, + fillColor: bottomRightRulerColor, + layoutSize: layoutSize + ), + ] + } + + static func horizontalBoundsSize(for rulerSet: AppStoreFlipRulerSet) -> NSSize { + NSSize( + width: rulerSet.horizontalLength / rulerScale, + height: Ruler.thickness + ) } - static var screen4ScaledRulerThickness: CGFloat { - Ruler.thickness * screen4RulerScale + static func verticalBoundsSize(for rulerSet: AppStoreFlipRulerSet) -> NSSize { + NSSize( + width: Ruler.thickness, + height: rulerSet.verticalLength / rulerScale + ) } +} - static var screen4VerticalRulerRect: NSRect { +private enum AppStorePreferencesScreenshotLayout { + static let title = "Pick your preferences" + static let subtitle = "Change color, opacity, and more." + static let previewName = "Preferences" + static let outputFilename = "05-customize-rulers.png" + static let backgroundColor = #colorLiteral(red: 0.5181607008, green: 0.4312165375, blue: 0.6487324834, alpha: 1) + + static let rulerScale: CGFloat = 6 + static let rulerOpacity: CGFloat = 0.75 + static let verticalRulerX: CGFloat = 250 + static let verticalRulerY: CGFloat = 170 + static let verticalRulerLength: CGFloat = 2050 + static let horizontalRulerX: CGFloat = 50 + static let horizontalRulerY: CGFloat = 1180 + static let horizontalRulerLength: CGFloat = 3000 + static let preferencesWindowX: CGFloat = 680 + static let preferencesWindowY: CGFloat = 540 + static let preferencesWindowScale: CGFloat = 4 + static let preferencesContentWidth: CGFloat = 350 + static let preferencesContentHeight: CGFloat = 333 + static let preferencesWindowShadowOpacity: CGFloat = 0.28 + static let preferencesWindowShadowYOffset: CGFloat = -5 + + static var foregroundOpacityPercent: Int { + Int((rulerOpacity * 100).rounded()) + } + + static let backgroundOpacityPercent = 50 + static let floatRulers = true + static let groupRulers = true + static let rulerShadow = false + + static var scaledRulerThickness: CGFloat { + Ruler.thickness * rulerScale + } + + static var verticalRulerRect: NSRect { NSRect( - x: screen4VerticalRulerX, - y: screen4VerticalRulerY, - width: screen4ScaledRulerThickness, - height: screen4VerticalRulerLength + x: verticalRulerX, + y: verticalRulerY, + width: scaledRulerThickness, + height: verticalRulerLength ) } - static var screen4HorizontalRulerRect: NSRect { + static var horizontalRulerRect: NSRect { NSRect( - x: screen4HorizontalRulerX, - y: screen4HorizontalRulerY, - width: screen4HorizontalRulerLength, - height: screen4ScaledRulerThickness + x: horizontalRulerX, + y: horizontalRulerY, + width: horizontalRulerLength, + height: scaledRulerThickness ) } - static var screen4PreferencesContentSize: NSSize { - NSSize(width: screen4PreferencesContentWidth, height: screen4PreferencesContentHeight) + static var preferencesContentSize: NSSize { + NSSize(width: preferencesContentWidth, height: preferencesContentHeight) } - static var screen4PreferencesWindowRect: NSRect { + static var preferencesWindowRect: NSRect { NSRect( - x: screen4PreferencesWindowX, - y: screen4PreferencesWindowY, - width: screen4PreferencesContentWidth * screen4PreferencesWindowScale, - height: titlebarHeight + screen4PreferencesContentHeight * screen4PreferencesWindowScale + x: preferencesWindowX, + y: preferencesWindowY, + width: preferencesContentWidth * preferencesWindowScale, + height: AppStoreScreenshotLayout.titlebarHeight + preferencesContentHeight * preferencesWindowScale ) } - static var screen4PreferencesContentRect: NSRect { + static var preferencesContentRect: NSRect { NSRect( - x: screen4PreferencesWindowX, - y: screen4PreferencesWindowY + titlebarHeight, - width: screen4PreferencesContentWidth * screen4PreferencesWindowScale, - height: screen4PreferencesContentHeight * screen4PreferencesWindowScale + x: preferencesWindowX, + y: preferencesWindowY + AppStoreScreenshotLayout.titlebarHeight, + width: preferencesContentWidth * preferencesWindowScale, + height: preferencesContentHeight * preferencesWindowScale ) } - } struct AppStoreScreenshotPreview: PreviewProvider { static var previews: some View { Group { - AppStoreScreenshotScenarioView(screen: .screen1) - .previewDisplayName(AppStoreScreenshotScreen.screen1.previewName) - AppStoreScreenshotScenarioView(screen: .screen3) - .previewDisplayName(AppStoreScreenshotScreen.screen3.previewName) - AppStoreScreenshotScenarioView(screen: .screen2) - .previewDisplayName(AppStoreScreenshotScreen.screen2.previewName) - AppStoreScreenshotScenarioView(screen: .screen4) - .previewDisplayName(AppStoreScreenshotScreen.screen4.previewName) + ForEach(AppStoreScreenshotScreen.allInOutputOrder, id: \.outputFilename) { screen in + AppStoreScreenshotScenarioView(screen: screen) + .previewDisplayName(screen.previewName) + } } .aspectRatio(16.0 / 10.0, contentMode: .fit) .frame(width: AppStoreScreenshotLayout.previewWidth, height: AppStoreScreenshotLayout.previewHeight) @@ -594,6 +818,89 @@ private struct AppStoreViewPlacement { } } +private final class AppStoreScreenshotCopyView: NSView { + private let title: String + private let subtitle: String + private let titleColor: NSColor + private let subtitleColor: NSColor + private let layout: AppStoreScreenshotCopyViewLayout + + override var isFlipped: Bool { + true + } + + init( + title: String, + subtitle: String, + titleColor: NSColor, + subtitleColor: NSColor, + layout: AppStoreScreenshotCopyViewLayout + ) { + self.title = title + self.subtitle = subtitle + self.titleColor = titleColor + self.subtitleColor = subtitleColor + self.layout = layout + super.init(frame: NSRect(origin: .zero, size: layout.boundsSize)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let icon = AppIconRenderer.image(size: 128) + icon.draw(in: layout.iconRect) + + drawText( + title, + in: layout.titleRect, + size: AppStoreScreenshotLayout.titleFontSize, + color: titleColor, + weight: AppStoreScreenshotLayout.titleFontWeight + ) + drawText( + subtitle, + in: layout.subtitleRect, + size: AppStoreScreenshotLayout.subtitleFontSize, + color: subtitleColor + ) + } + + private func drawText( + _ text: String, + in rect: NSRect, + size: CGFloat, + color: NSColor, + weight: NSFont.Weight = AppStoreScreenshotLayout.textFontWeight + ) { + let style = NSMutableParagraphStyle() + style.alignment = .left + + let attributes: [NSAttributedString.Key: Any] = [ + .font: labelFont(size: size, weight: weight), + .foregroundColor: color, + .paragraphStyle: style, + ] + + text.draw(with: rect, options: [.usesLineFragmentOrigin], attributes: attributes) + } + + private func labelFont(size: CGFloat, weight: NSFont.Weight) -> NSFont { + switch AppStoreScreenshotLayout.textFontFamily { + case .helveticaNeue: + return NSFont( + name: "HelveticaNeue", + size: size + ) ?? NSFont.systemFont(ofSize: size, weight: weight) + case .system: + return NSFont.systemFont(ofSize: size, weight: weight) + } + } +} + private final class AppStoreActiveSnapshotWindow: NSWindow { override var canBecomeKey: Bool { true @@ -606,13 +913,19 @@ private final class AppStoreActiveSnapshotWindow: NSWindow { private final class AppStoreHorizontalRule: HorizontalRule { private let screenshotUnit: Unit + private let screenshotZeroCorner: ZeroCorner override var unit: Unit { screenshotUnit } - init(unit: Unit, frame: NSRect) { + override var zeroCorner: ZeroCorner { + screenshotZeroCorner + } + + init(unit: Unit, frame: NSRect, zeroCorner: ZeroCorner = .topLeft) { self.screenshotUnit = unit + self.screenshotZeroCorner = zeroCorner super.init(frame: frame) } @@ -628,13 +941,19 @@ private final class AppStoreHorizontalRule: HorizontalRule { private final class AppStoreVerticalRule: VerticalRule { private let screenshotUnit: Unit + private let screenshotZeroCorner: ZeroCorner override var unit: Unit { screenshotUnit } - init(unit: Unit, frame: NSRect) { + override var zeroCorner: ZeroCorner { + screenshotZeroCorner + } + + init(unit: Unit, frame: NSRect, zeroCorner: ZeroCorner = .topLeft) { self.screenshotUnit = unit + self.screenshotZeroCorner = zeroCorner super.init(frame: frame) } @@ -654,6 +973,7 @@ private final class AppStoreScreenshotScenarioNSView: NSView { private let rulerPlacements: [AppStoreRulerPlacement] private let preferencesController: PreferencesController? private let viewPlacements: [AppStoreViewPlacement] + private let copyViewPlacement: AppStoreViewPlacement override var isFlipped: Bool { true @@ -669,9 +989,10 @@ private final class AppStoreScreenshotScenarioNSView: NSView { init(screen: AppStoreScreenshotScreen) { self.screen = screen self.rulerPlacements = Self.makeRulerPlacements(for: screen) - let preferencesController = screen == .screen4 ? Self.makePreferencesController() : nil + let preferencesController = screen.scenario == .preferences ? Self.makePreferencesController() : nil self.preferencesController = preferencesController self.viewPlacements = Self.makeViewPlacements(for: screen, preferencesController: preferencesController) + self.copyViewPlacement = Self.makeCopyViewPlacement(for: screen) super.init(frame: NSRect(origin: .zero, size: AppStoreScreenshotLayout.canvasSize)) for viewPlacement in viewPlacements { addSubview(viewPlacement.container) @@ -680,6 +1001,7 @@ private final class AppStoreScreenshotScenarioNSView: NSView { configureRuler(rulerPlacement.view) addSubview(rulerPlacement.container) } + addSubview(copyViewPlacement.container) } required init?(coder: NSCoder) { @@ -727,64 +1049,65 @@ private final class AppStoreScreenshotScenarioNSView: NSView { for viewPlacement in viewPlacements { layoutView(viewPlacement, frame: viewPlacement.frame, transform: transform) } + layoutView(copyViewPlacement, frame: copyViewPlacement.frame, transform: transform) } private static func makeRulerPlacements(for screen: AppStoreScreenshotScreen) -> [AppStoreRulerPlacement] { - switch screen { - case .screen1: + switch screen.scenario { + case .measure: let horizontalBoundsSize = NSSize( - width: AppStoreScreenshotLayout.horizontalRulerLength / AppStoreScreenshotLayout.rulerScale, + width: AppStoreMeasureScreenshotLayout.horizontalRulerLength / AppStoreMeasureScreenshotLayout.rulerScale, height: Ruler.thickness ) let verticalBoundsSize = NSSize( width: Ruler.thickness, - height: AppStoreScreenshotLayout.verticalRulerLength / AppStoreScreenshotLayout.rulerScale + height: AppStoreMeasureScreenshotLayout.verticalRulerLength / AppStoreMeasureScreenshotLayout.rulerScale ) return [ AppStoreRulerPlacement( view: AppStoreHorizontalRule(unit: .pixels, frame: NSRect(origin: .zero, size: horizontalBoundsSize)), - frame: AppStoreScreenshotLayout.horizontalRulerRect, + frame: AppStoreMeasureScreenshotLayout.horizontalRulerRect, boundsSize: horizontalBoundsSize ), AppStoreRulerPlacement( view: AppStoreVerticalRule(unit: .pixels, frame: NSRect(origin: .zero, size: verticalBoundsSize)), - frame: AppStoreScreenshotLayout.verticalRulerRect, + frame: AppStoreMeasureScreenshotLayout.verticalRulerRect, boundsSize: verticalBoundsSize ), ] - case .screen2: + case .units: let rulerBoundsSize = NSSize( - width: AppStoreScreenshotLayout.screen2RulerLength / AppStoreScreenshotLayout.screen2RulerScale, + width: AppStoreUnitsScreenshotLayout.rulerLength / AppStoreUnitsScreenshotLayout.rulerScale, height: Ruler.thickness ) return [Unit.pixels, .millimeters, .inches].enumerated().map { index, unit in AppStoreRulerPlacement( view: AppStoreHorizontalRule(unit: unit, frame: NSRect(origin: .zero, size: rulerBoundsSize)), - frame: AppStoreScreenshotLayout.screen2RulerRect(index: index), + frame: AppStoreUnitsScreenshotLayout.rulerRect(index: index), boundsSize: rulerBoundsSize ) } - case .screen3: - return (0.. [AppStoreViewPlacement] { - guard screen == .screen4, + guard screen.scenario == .preferences, let preferencesView = preferencesController?.window?.contentView else { return [] } @@ -840,11 +1192,11 @@ private final class AppStoreScreenshotScenarioNSView: NSView { let originalFloatRulers = prefs.floatRulers let originalGroupRulers = prefs.groupRulers let originalRulerShadow = prefs.rulerShadow - prefs.foregroundOpacity = AppStoreScreenshotLayout.screen4ForegroundOpacityPercent - prefs.backgroundOpacity = AppStoreScreenshotLayout.screen4BackgroundOpacityPercent - prefs.floatRulers = AppStoreScreenshotLayout.screen4FloatRulers - prefs.groupRulers = AppStoreScreenshotLayout.screen4GroupRulers - prefs.rulerShadow = AppStoreScreenshotLayout.screen4RulerShadow + prefs.foregroundOpacity = AppStorePreferencesScreenshotLayout.foregroundOpacityPercent + prefs.backgroundOpacity = AppStorePreferencesScreenshotLayout.backgroundOpacityPercent + prefs.floatRulers = AppStorePreferencesScreenshotLayout.floatRulers + prefs.groupRulers = AppStorePreferencesScreenshotLayout.groupRulers + prefs.rulerShadow = AppStorePreferencesScreenshotLayout.rulerShadow preferencesController?.updateView() preferencesController?.window?.contentView?.layoutSubtreeIfNeeded() defer { @@ -855,22 +1207,46 @@ private final class AppStoreScreenshotScenarioNSView: NSView { prefs.rulerShadow = originalRulerShadow } - let imageView = NSImageView(frame: NSRect(origin: .zero, size: AppStoreScreenshotLayout.screen4PreferencesContentSize)) + let imageView = NSImageView(frame: NSRect(origin: .zero, size: AppStorePreferencesScreenshotLayout.preferencesContentSize)) imageView.image = snapshot(preferencesView, preferencesController: preferencesController) imageView.imageScaling = .scaleAxesIndependently - return [ - AppStoreViewPlacement( - view: imageView, - frame: AppStoreScreenshotLayout.screen4PreferencesContentRect, - boundsSize: AppStoreScreenshotLayout.screen4PreferencesContentSize - ), - ] + return [AppStoreViewPlacement( + view: imageView, + frame: AppStorePreferencesScreenshotLayout.preferencesContentRect, + boundsSize: AppStorePreferencesScreenshotLayout.preferencesContentSize + )] + } + + private static func makeCopyViewPlacement(for screen: AppStoreScreenshotScreen) -> AppStoreViewPlacement { + let palette = AppStoreScreenshotPalette() + let layout = copyViewLayout(for: screen) + let copyView = AppStoreScreenshotCopyView( + title: screen.title, + subtitle: screen.subtitle, + titleColor: screen.titleColor(in: palette), + subtitleColor: screen.subtitleColor(in: palette), + layout: layout + ) + return AppStoreViewPlacement( + view: copyView, + frame: layout.frame, + boundsSize: layout.boundsSize + ) + } + + private static func copyViewLayout(for screen: AppStoreScreenshotScreen) -> AppStoreScreenshotCopyViewLayout { + switch screen.scenario { + case .flipRulers: + return AppStoreFlipScreenshotLayout.copyViewLayout + case .measure, .units, .colors, .preferences: + return AppStoreScreenshotLayout.copyViewLayout + } } private static func snapshot(_ view: NSView, preferencesController: PreferencesController?) -> NSImage { let snapshotWindow = AppStoreActiveSnapshotWindow( - contentRect: NSRect(origin: .zero, size: AppStoreScreenshotLayout.screen4PreferencesContentSize), + contentRect: NSRect(origin: .zero, size: AppStorePreferencesScreenshotLayout.preferencesContentSize), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: true @@ -906,12 +1282,12 @@ private final class AppStoreScreenshotScenarioNSView: NSView { drawActiveSliderOverlay( for: preferencesController.foregroundOpacitySlider, in: image, - value: CGFloat(AppStoreScreenshotLayout.screen4ForegroundOpacityPercent) + value: CGFloat(AppStorePreferencesScreenshotLayout.foregroundOpacityPercent) ) drawActiveSliderOverlay( for: preferencesController.backgroundOpacitySlider, in: image, - value: CGFloat(AppStoreScreenshotLayout.screen4BackgroundOpacityPercent) + value: CGFloat(AppStorePreferencesScreenshotLayout.backgroundOpacityPercent) ) } return image @@ -1070,42 +1446,24 @@ private final class AppStoreScreenshotScenarioNSView: NSView { } private func drawScenario() { - screen.background(in: palette).setFill() + screen.backgroundColor.setFill() NSRect(origin: .zero, size: AppStoreScreenshotLayout.canvasSize).fill() - drawCopy() - switch screen { - case .screen1: - drawSampleWindow(AppStoreScreenshotLayout.sampleWindowRect) - drawScreen1Boxes() - case .screen2: + switch screen.scenario { + case .measure: + drawSampleWindow(AppStoreMeasureScreenshotLayout.sampleWindowRect) + drawMeasureBoxes() + case .units: break - case .screen3: + case .colors: break - case .screen4: - drawSampleWindow(AppStoreScreenshotLayout.screen4PreferencesWindowRect) + case .flipRulers: + break + case .preferences: + drawSampleWindow(AppStorePreferencesScreenshotLayout.preferencesWindowRect) } } - private func drawCopy() { - let icon = AppIconRenderer.image(size: 128) - icon.draw(in: AppStoreScreenshotLayout.iconRect) - - drawText( - screen.headline, - in: AppStoreScreenshotLayout.headlineRect, - size: AppStoreScreenshotLayout.headlineFontSize, - color: screen.headlineColor(in: palette), - weight: AppStoreScreenshotLayout.headlineFontWeight - ) - drawText( - screen.description, - in: AppStoreScreenshotLayout.descriptionRect, - size: AppStoreScreenshotLayout.descriptionFontSize, - color: screen.descriptionColor(in: palette) - ) - } - private func configureRuler(_ view: RuleView) { view.showMouseTick = false } @@ -1115,7 +1473,7 @@ private final class AppStoreScreenshotScenarioNSView: NSView { drawShadow( rect, radius: AppStoreScreenshotLayout.sampleWindowCornerRadius, - fill: screen.background(in: palette), + fill: screen.backgroundColor, blur: AppStoreScreenshotLayout.sampleWindowShadowBlur, offset: NSSize( width: AppStoreScreenshotLayout.sampleWindowShadowXOffset, @@ -1146,17 +1504,17 @@ private final class AppStoreScreenshotScenarioNSView: NSView { drawTrafficLights(at: sampleWindowTrafficLightOrigin(for: rect)) } - private func drawScreen1Boxes() { + private func drawMeasureBoxes() { for rect in [ - AppStoreScreenshotLayout.screen1Box1Rect, - AppStoreScreenshotLayout.screen1Box2Rect, - AppStoreScreenshotLayout.screen1Box3Rect, + AppStoreMeasureScreenshotLayout.box1Rect, + AppStoreMeasureScreenshotLayout.box2Rect, + AppStoreMeasureScreenshotLayout.box3Rect, ] { stroke( rect, - color: AppStoreScreenshotLayout.screen1BoxBorderColor, - width: AppStoreScreenshotLayout.screen1BoxBorderWidth, - radius: AppStoreScreenshotLayout.screen1BoxBorderRadius + color: AppStoreMeasureScreenshotLayout.boxBorderColor, + width: AppStoreMeasureScreenshotLayout.boxBorderWidth, + radius: AppStoreMeasureScreenshotLayout.boxBorderRadius ) } } @@ -1188,20 +1546,20 @@ private final class AppStoreScreenshotScenarioNSView: NSView { } private var sampleWindowShadowOpacity: CGFloat { - switch screen { - case .screen1, .screen2, .screen3: + switch screen.scenario { + case .measure, .units, .colors, .flipRulers: return AppStoreScreenshotLayout.sampleWindowShadowOpacity - case .screen4: - return AppStoreScreenshotLayout.screen4PreferencesWindowShadowOpacity + case .preferences: + return AppStorePreferencesScreenshotLayout.preferencesWindowShadowOpacity } } private var sampleWindowShadowYOffset: CGFloat { - switch screen { - case .screen1, .screen2, .screen3: + switch screen.scenario { + case .measure, .units, .colors, .flipRulers: return AppStoreScreenshotLayout.sampleWindowShadowYOffset - case .screen4: - return AppStoreScreenshotLayout.screen4PreferencesWindowShadowYOffset + case .preferences: + return AppStorePreferencesScreenshotLayout.preferencesWindowShadowYOffset } } @@ -1264,37 +1622,6 @@ private final class AppStoreScreenshotScenarioNSView: NSView { path.stroke() } - private func drawText( - _ text: String, - in rect: NSRect, - size: CGFloat, - color: NSColor, - weight: NSFont.Weight = AppStoreScreenshotLayout.textFontWeight, - alignment: NSTextAlignment = .left - ) { - let style = NSMutableParagraphStyle() - style.alignment = alignment - - let attributes: [NSAttributedString.Key: Any] = [ - .font: labelFont(size: size, weight: weight), - .foregroundColor: color, - .paragraphStyle: style, - ] - - text.draw(with: rect, options: [.usesLineFragmentOrigin], attributes: attributes) - } - - private func labelFont(size: CGFloat, weight: NSFont.Weight) -> NSFont { - switch AppStoreScreenshotLayout.textFontFamily { - case .helveticaNeue: - return NSFont( - name: "HelveticaNeue", - size: size - ) ?? .systemFont(ofSize: size, weight: weight) - case .system: - return .systemFont(ofSize: size, weight: weight) - } - } } #endif diff --git a/Free Ruler/Base.lproj/MainMenu.xib b/Free Ruler/Base.lproj/MainMenu.xib index 9a1d7b4..9813031 100644 --- a/Free Ruler/Base.lproj/MainMenu.xib +++ b/Free Ruler/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -187,6 +187,25 @@ + + + + + + + + + + + + + + + + + + + @@ -212,7 +231,7 @@ - + diff --git a/Free Ruler/Base.lproj/PreferencesController.xib b/Free Ruler/Base.lproj/PreferencesController.xib index 1fc1203..ae045ff 100644 --- a/Free Ruler/Base.lproj/PreferencesController.xib +++ b/Free Ruler/Base.lproj/PreferencesController.xib @@ -98,7 +98,7 @@