Calls Mac: a full-size icon, from an asset catalog on macOS 26 and later
Paul, macOS 27: "The Calls icon in macOS is tiny." Two causes. 1) The Dock. applicationDidFinishLaunching set NSApp.applicationIconImage to MacIcon.png, which the Dock draws as it is. Since 6b5a81f (0.4.5) MacIcon.png was generate-mac-icon.swift's 824-on-1024 tile drawn from Resources/AppIcon.png, which estilo's distributor had already rounded onto that grid, so the artwork was 664 of 1024 (65%) beside Apple's 80.5%. The generator now draws from the full-bleed square (the iPhone's master): 824 of 1024 again. On macOS 26 and later, when the bundle names an icon, nothing is set at launch, so the Dock shows the system's own rendering. 2) No modern icon. The bundle shipped only an ICNS, which macOS 26 and later treat as legacy (no masking, lighting, dark, tinted or clear styles). bundle-mac.py now wraps the full-bleed master in an Icon Composer AppIcon.icon, compiles it with xcrun actool (Xcode 26.6 here) into Assets.car, and sets CFBundleIconName = AppIcon. actool also flattens it onto the 824 grid for macOS 14 and 15. CFBundleIconFile and CallsMac.icns stay. Evidence in /tmp/mac-icon: NSWorkspace icons at 128/256/512 beside Notes, Maps, FaceTime: the new bundle's artwork is 412 of 512 (80.5%), as Apple's; the 0.4.8 Dock picture is 664 of 1024.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 files changed,
+57
−9
native/README.md+10 −3native/apple/Sources/CallsMac/CallsMac.swift+7 −0native/apple/Sources/CallsMac/Resources/MacIcon.pngbinarynative/scripts/bundle-mac.py+31 −3native/scripts/generate-mac-icon.swift+9 −3
diff --git a/native/README.md b/native/README.md| index b868347..06ca75b 100644 |
| --- a/native/README.md |
| +++ b/native/README.md |
| @@ -819,9 +819,16 @@ See [VERIFICATION.md](VERIFICATION.md) for the completed gate record and accepta |
| The iPhone asset catalog and Mac application resource use the approved Oficina |
| Calls icon. Both are generated copies of Estilo's `brand/png/calls-1024.png`; |
| update them with `python3 brand/distribute.py calls /path/to/calls` from Estilo. |
| -The Mac derives `MacIcon.png` with rounded corners and transparent padding using |
| -`native/scripts/generate-mac-icon.swift`. Its Finder ICNS and runtime Dock icon |
| -both use that Mac variant; the square master remains unchanged. The Lucide |
| +The Mac's icon comes from the iPhone's full-bleed square, the one master. |
| +`native/scripts/bundle-mac.py` wraps it in an Icon Composer `AppIcon.icon` and |
| +compiles it with `xcrun actool` into `Assets.car`, named by `CFBundleIconName`: |
| +macOS 26 and later mask, light and tint it themselves, and actool flattens it |
| +onto the 824-on-1024 grid for macOS 14 and 15. This needs Xcode 26 or later. |
| +`native/scripts/generate-mac-icon.swift` derives `MacIcon.png`, the master |
| +rounded onto that grid, for `CallsMac.icns` (`CFBundleIconFile`) and for the |
| +Dock icon set at launch, which happens only before macOS 26 or outside a |
| +bundle. Never feed it `Resources/AppIcon.png`, which is already on the grid: |
| +padding it twice shrank the Dock icon to 664 of 1024 from 0.4.5. The Lucide |
| notice is retained in `native/LICENSE.lucide`. |
| Third-party notices ship in the apps' Resources: |
diff --git a/native/apple/Sources/CallsMac/CallsMac.swift b/native/apple/Sources/CallsMac/CallsMac.swift| index 93e0348..45aacbf 100644 |
| --- a/native/apple/Sources/CallsMac/CallsMac.swift |
| +++ b/native/apple/Sources/CallsMac/CallsMac.swift |
| @@ -90,6 +90,13 @@ final class CallsAppDelegate: NSObject, NSApplicationDelegate { |
| } |
| func applicationDidFinishLaunching(_ notification: Notification) { |
| + // On macOS 26 and later a bundled app's Dock icon comes from Assets.car, |
| + // which the system masks and renders in the person's icon style. Setting |
| + // a picture here would replace that with a flat PNG, so only a run |
| + // without the bundle's icon (swift run) or an older macOS sets one. |
| + if #available(macOS 26, *), Bundle.main.object(forInfoDictionaryKey: "CFBundleIconName") != nil { |
| + return |
| + } |
| if let url = Bundle.main.url(forResource: "MacIcon", withExtension: "png") |
| ?? Bundle.module.url(forResource: "MacIcon", withExtension: "png") |
| { |
diff --git a/native/apple/Sources/CallsMac/Resources/MacIcon.png b/native/apple/Sources/CallsMac/Resources/MacIcon.png| index 1ba703e..e97c4fe 100644 |
| Binary files a/native/apple/Sources/CallsMac/Resources/MacIcon.png and b/native/apple/Sources/CallsMac/Resources/MacIcon.png differ |
diff --git a/native/scripts/bundle-mac.py b/native/scripts/bundle-mac.py| index f478491..18313a2 100644 |
| --- a/native/scripts/bundle-mac.py |
| +++ b/native/scripts/bundle-mac.py |
| @@ -1,4 +1,5 @@ |
| """Bundle the local development app. Distribution signing is a separate step.""" |
| +import json |
| from pathlib import Path |
| import plistlib |
| import shutil |
| @@ -29,8 +30,35 @@ shutil.copy2(root / 'PATENTS.webrtc', bundle / 'Resources/PATENTS.webrtc') |
| # The Mac's noise suppressor model (scripts/build-deepfilter.sh). Without |
| # it the app falls back to RNNoise. |
| shutil.copy2(root / 'build/DeepFilterNet3_onnx.tar.gz', bundle / 'Resources/DeepFilterNet3_onnx.tar.gz') |
| -# Finder reads the ICNS before the app launches; the PNG also supplies the |
| -# runtime Dock icon. Generate each required representation from the master. |
| +# macOS 26 and later draw an app's icon from an asset catalog: full-bleed |
| +# artwork that the system masks, lights and tints itself. An app with only an |
| +# ICNS is legacy there, its artwork shrunk onto a plain squircle. So compile |
| +# the square master (the iPhone's, which is opaque and unrounded) as an Icon |
| +# Composer .icon into Assets.car, named by CFBundleIconName. actool also |
| +# flattens it into renditions on the 824-point grid for macOS 14 and 15. |
| +icon_package = root / 'build/AppIcon.icon' |
| +if icon_package.exists(): shutil.rmtree(icon_package) |
| +(icon_package / 'Assets').mkdir(parents=True) |
| +shutil.copy2(root / 'ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png', icon_package / 'Assets/calls.png') |
| +(icon_package / 'icon.json').write_text(json.dumps({ |
| + 'fill': 'automatic', |
| + 'groups': [{'layers': [{'glass': False, 'image-name': 'calls.png', 'name': 'calls'}], |
| + 'shadow': {'kind': 'neutral', 'opacity': 0.5}, |
| + 'translucency': {'enabled': False, 'value': 0}}], |
| + 'supported-platforms': {'squares': ['macOS']}, |
| +}, indent=2)) |
| +catalog = root / 'build/AssetCatalog' |
| +if catalog.exists(): shutil.rmtree(catalog) |
| +catalog.mkdir(parents=True) |
| +subprocess.run(['xcrun', 'actool', str(icon_package), '--compile', str(catalog), |
| + '--platform', 'macosx', '--minimum-deployment-target', '14.0', |
| + '--app-icon', 'AppIcon', '--output-partial-info-plist', str(catalog / 'partial.plist'), |
| + '--errors', '--warnings'], check=True, stdout=subprocess.DEVNULL) |
| +if not (catalog / 'Assets.car').exists(): |
| + raise SystemExit('actool wrote no Assets.car: an Xcode that compiles .icon (26 or later) is required') |
| +shutil.copy2(catalog / 'Assets.car', bundle / 'Resources/Assets.car') |
| +# The ICNS stays as CFBundleIconFile for anything that reads only that key. |
| +# The PNG is the Dock icon for a run outside a bundle (CallsMac.swift). |
| iconset = root / 'build/AppIcon.iconset' |
| iconset.mkdir(parents=True, exist_ok=True) |
| for size in (16, 32, 128, 256, 512): |
| @@ -47,7 +75,7 @@ info = { |
| 'CFBundleName': 'Calls', 'CFBundleDisplayName': 'Calls', |
| 'CFBundleIdentifier': 'net.amadan.oficina.calls', |
| 'CFBundleExecutable': 'CallsMac', 'CFBundlePackageType': 'APPL', |
| - 'CFBundleIconFile': 'CallsMac.icns', |
| + 'CFBundleIconFile': 'CallsMac.icns', 'CFBundleIconName': 'AppIcon', |
| 'CFBundleShortVersionString': '0.1.0', 'CFBundleVersion': '1', |
| 'LSMinimumSystemVersion': '14.0', 'NSHighResolutionCapable': True, |
| 'NSCameraUseContinuityCameraDeviceType': True, |
diff --git a/native/scripts/generate-mac-icon.swift b/native/scripts/generate-mac-icon.swift| index ff8c7c4..1e96ac7 100644 |
| --- a/native/scripts/generate-mac-icon.swift |
| +++ b/native/scripts/generate-mac-icon.swift |
| @@ -1,11 +1,17 @@ |
| import AppKit |
| -// macOS displays the supplied silhouette; unlike iOS it does not mask an opaque master. |
| -// Keep the approved square master intact and derive a padded Mac icon from it. |
| +// Before macOS 26 the system draws the supplied silhouette as it is; unlike iOS |
| +// it does not mask an opaque master. Derive the Mac's rounded tile on Apple's |
| +// 824-on-1024 grid from the full-bleed square master, the iPhone's. Not from |
| +// Resources/AppIcon.png: that one is already rounded onto the grid, and |
| +// drawing it into the grid again left the artwork at 664 of 1024, small in the |
| +// Dock beside every other app. macOS 26 and later use Assets.car instead |
| +// (bundle-mac.py), where the system applies the mask. |
| let native = URL(fileURLWithPath: #filePath).standardizedFileURL |
| .deletingLastPathComponent().deletingLastPathComponent() |
| let resources = native.appendingPathComponent("apple/Sources/CallsMac/Resources") |
| -guard let source = NSImage(contentsOf: resources.appendingPathComponent("AppIcon.png")), |
| +let master = native.appendingPathComponent("ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png") |
| +guard let source = NSImage(contentsOf: master), |
| let bitmap = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 1024, pixelsHigh: 1024, |
| bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, |
| colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0), |