aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2023-09-03 02:11:21 +0200
committerPřemysl Eric Janouch <p@janouch.name>2023-09-03 02:13:14 +0200
commit9e4692bb09742bb68a62ef79890279a9e967169a (patch)
tree4b4f19f2fb5851d301d85185ee15badad679c549
parent1c4343058da2a1e0c6d2fd87a9bde4ef4b378eae (diff)
downloadxK-9e4692bb09742bb68a62ef79890279a9e967169a.tar.gz
xK-9e4692bb09742bb68a62ef79890279a9e967169a.tar.xz
xK-9e4692bb09742bb68a62ef79890279a9e967169a.zip
xM: generate and use a bundle icon
-rw-r--r--xM/CMakeLists.txt15
-rw-r--r--xM/gen-icon.swift104
2 files changed, 118 insertions, 1 deletions
diff --git a/xM/CMakeLists.txt b/xM/CMakeLists.txt
index 3386926..b3484de 100644
--- a/xM/CMakeLists.txt
+++ b/xM/CMakeLists.txt
@@ -25,8 +25,21 @@ add_custom_command (OUTPUT xC-proto.swift
COMMENT "Generating xC relay protocol code" VERBATIM)
set (MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.${PROJECT_NAME})
+set (MACOSX_BUNDLE_ICON_FILE xM.icns)
+
+# Avoid including binary files in the repository by generating icons in code.
+# sips(1) + Javascript + iconutil(1) could probably also be used.
+find_program (SWIFT_EXECUTABLE swift REQUIRED)
+
+set (icon "${PROJECT_BINARY_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
+add_custom_command (OUTPUT "${icon}"
+ COMMAND ${SWIFT_EXECUTABLE} "${PROJECT_SOURCE_DIR}/gen-icon.swift" "${icon}"
+ DEPENDS gen-icon.swift
+ COMMENT "Generating xM application icon" VERBATIM)
+set_source_files_properties ("${icon}" PROPERTIES
+ MACOSX_PACKAGE_LOCATION Resources)
# Other requirements: macOS 10.14 for Network, and macOS 11 for Logger.
set (CMAKE_Swift_LANGUAGE_VERSION 5)
add_executable (xM MACOSX_BUNDLE
- main.swift ${PROJECT_BINARY_DIR}/xC-proto.swift)
+ main.swift "${icon}" "${PROJECT_BINARY_DIR}/xC-proto.swift")
diff --git a/xM/gen-icon.swift b/xM/gen-icon.swift
new file mode 100644
index 0000000..5e29016
--- /dev/null
+++ b/xM/gen-icon.swift
@@ -0,0 +1,104 @@
+// gen-icon.awk: generate a program icon for xM in the Apple icon format
+//
+// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
+// SPDX-License-Identifier: 0BSD
+//
+// NSGraphicsContext mostly just weirdly wraps over Quartz,
+// so we do it all in Quartz directly.
+import CoreGraphics
+import Foundation
+import ImageIO
+import UniformTypeIdentifiers
+
+// XXX: Not even AppKit provides real superelliptic squircles; screw it.
+func drawSquircle(context: CGContext, bounds: CGRect, radius: CGFloat) {
+ context.move(to: CGPointMake(bounds.minX, bounds.maxY - radius))
+ context.addArc(
+ center: CGPointMake(bounds.minX + radius, bounds.maxY - radius),
+ radius: radius, startAngle: .pi, endAngle: .pi / 2, clockwise: true)
+ context.addLine(to: CGPointMake(bounds.maxX - radius, bounds.maxY))
+ context.addArc(
+ center: CGPointMake(bounds.maxX - radius, bounds.maxY - radius),
+ radius: radius, startAngle: .pi / 2, endAngle: 0, clockwise: true)
+ context.addLine(to: CGPointMake(bounds.maxX, bounds.maxY - radius))
+ context.addArc(
+ center: CGPointMake(bounds.maxX - radius, bounds.minY + radius),
+ radius: radius, startAngle: 0, endAngle: .pi / -2, clockwise: true)
+ context.addLine(to: CGPointMake(bounds.minX + radius, bounds.minY))
+ context.addArc(
+ center: CGPointMake(bounds.minX + radius, bounds.minY + radius),
+ radius: radius, startAngle: .pi / -2, endAngle: .pi, clockwise: true)
+ context.closePath()
+}
+
+func drawIcon(scale: CGFloat) -> CGImage? {
+ let size = CGSizeMake(1024, 1024)
+
+ let colorspace = CGColorSpaceCreateDeviceRGB()
+ let context = CGContext(data: nil,
+ width: Int(size.width * scale), height: Int(size.height * scale),
+ bitsPerComponent: 8, bytesPerRow: 0, space: colorspace,
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
+ context.scaleBy(x: scale, y: scale)
+
+ let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200)
+ // The radius was something like size.{width,height}/6.4, at least for iOS.
+ drawSquircle(context: context, bounds: bounds, radius: 180)
+ let squircle = context.path!
+
+ // Gradients don't draw shadows, so draw it separately.
+ context.saveGState()
+ context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm),
+ blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.5))
+ context.setFillColor(CGColor(red: 1, green: 0x55p-8, blue: 0, alpha: 1))
+ context.fillPath()
+ context.restoreGState()
+
+ context.saveGState()
+ context.addPath(squircle)
+ context.clip()
+ context.drawLinearGradient(
+ CGGradient(colorsSpace: colorspace, colors: [
+ CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1),
+ CGColor(red: 1, green: 0xaap-8, blue: 0, alpha: 1)
+ ] as CFArray, locations: [0, 1])!,
+ start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100),
+ options: CGGradientDrawingOptions(rawValue: 0))
+ context.restoreGState()
+
+ context.move(to: CGPoint(x: size.width * 0.30, y: size.height * 0.30))
+ context.addLine(to: CGPoint(x: size.width * 0.30, y: size.height * 0.70))
+ context.addLine(to: CGPoint(x: size.width * 0.575, y: size.height * 0.425))
+ context.move(to: CGPoint(x: size.width * 0.70, y: size.height * 0.30))
+ context.addLine(to: CGPoint(x: size.width * 0.70, y: size.height * 0.70))
+ context.addLine(to: CGPoint(x: size.width * 0.425, y: size.height * 0.425))
+ context.setLineWidth(80)
+ context.setLineCap(.round)
+ context.setLineJoin(.round)
+ context.setStrokeColor(CGColor.white)
+ context.strokePath()
+ return context.makeImage()
+}
+
+if CommandLine.arguments.count != 2 {
+ print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns")
+ exit(EXIT_FAILURE)
+}
+
+let filename = CommandLine.arguments[1]
+
+let macOSSizes: Array<CGFloat> = [16, 32, 128, 256, 512]
+let icns = CGImageDestinationCreateWithURL(
+ URL(fileURLWithPath: filename) as CFURL,
+ UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)!
+for size in macOSSizes {
+ CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, nil)
+ CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [
+ kCGImagePropertyDPIWidth: 144,
+ kCGImagePropertyDPIHeight: 144,
+ ] as CFDictionary)
+}
+if !CGImageDestinationFinalize(icns) {
+ print("ICNS finalization failed.")
+ exit(EXIT_FAILURE)
+}