xM: generate and use a bundle icon
This commit is contained in:
parent
1c4343058d
commit
9e4692bb09
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue