diff --git a/macos/Assets.xcassets/Custom Icon/Contents.json b/macos/Assets.xcassets/Custom Icon/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/Contents.json new file mode 100644 index 000000000..cc28dc42e --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "base.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/base.png b/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/base.png new file mode 100644 index 000000000..2c6f3a34b Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconBase.imageset/base.png differ diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/Contents.json new file mode 100644 index 000000000..ec32cf191 --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "crt-effect.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/crt-effect.png b/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/crt-effect.png new file mode 100644 index 000000000..c032c8400 Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/crt-effect.png differ diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/Contents.json new file mode 100644 index 000000000..286506fd9 --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ghosty.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/ghosty.png b/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/ghosty.png new file mode 100644 index 000000000..5df106fe6 Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/ghosty.png differ diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/Contents.json new file mode 100644 index 000000000..ed8e4328f --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "gloss.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/gloss.png b/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/gloss.png new file mode 100644 index 000000000..f57bc72a0 Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/gloss.png differ diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/Contents.json new file mode 100644 index 000000000..6d6a03eaf --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "screen-dark.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/screen-dark.png b/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/screen-dark.png new file mode 100644 index 000000000..2995fb9da Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/screen-dark.png differ diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/Contents.json b/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/Contents.json new file mode 100644 index 000000000..083891019 --- /dev/null +++ b/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "screen-mask.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/screen-mask.png b/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/screen-mask.png new file mode 100644 index 000000000..acc431862 Binary files /dev/null and b/macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/screen-mask.png differ diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index d07ebc12f..ea28a9ba4 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -39,6 +39,9 @@ A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; + A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */; }; + A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */; }; + A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; @@ -124,6 +127,9 @@ A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = ""; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; + A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconView.swift; sourceTree = ""; }; + A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = ""; }; + A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = ""; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -237,6 +243,7 @@ A57D79252C9C8782001D522E /* Secure Input */, A534263E2A7DCC5800EBB7A2 /* Settings */, A51BFC1C2B2FB5AB00E92F16 /* About */, + A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */, A51BFC292B30F69F00E92F16 /* Update */, ); path = Features; @@ -256,6 +263,7 @@ A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, + A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, @@ -307,6 +315,15 @@ path = macOS; sourceTree = ""; }; + A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */ = { + isa = PBXGroup; + children = ( + A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */, + A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */, + ); + path = "Colorized Ghostty Icon"; + sourceTree = ""; + }; A54CD6ED299BEB14008C95BB /* Sources */ = { isa = PBXGroup; children = ( @@ -580,8 +597,10 @@ files = ( A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, + A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, + A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, @@ -620,6 +639,7 @@ A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, + A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index ed257d9ec..895a53b67 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -519,6 +519,13 @@ class AppDelegate: NSObject, } else { GlobalEventTap.shared.disable() } + + if let colorizedIcon = ColorizedGhosttyIcon( + screenColors: [.purple, .blue], + ghostColor: .yellow + ).makeImage() { + NSApplication.shared.applicationIconImage = colorizedIcon + } } /// Sync the appearance of our app with the theme specified in the config. diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift new file mode 100644 index 000000000..0de33deea --- /dev/null +++ b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift @@ -0,0 +1,45 @@ +import Cocoa + +struct ColorizedGhosttyIcon { + /// The colors that make up the gradient of the screen. + let screenColors: [NSColor] + + /// The color of the ghost. + let ghostColor: NSColor + + /// Make a custom colorized ghostty icon. + func makeImage() -> NSImage? { + // All of our layers (in order) + guard let base = NSImage(named: "CustomIconBase") else { return nil } + guard let screen = NSImage(named: "CustomIconScreen") else { return nil } + guard let screenMask = NSImage(named: "CustomIconScreenMask") else { return nil } + guard let ghost = NSImage(named: "CustomIconGhost") else { return nil } + guard let crt = NSImage(named: "CustomIconCRT") else { return nil } + guard let gloss = NSImage(named: "CustomIconGloss") else { return nil } + + // Apply our color in various ways to our layers. + // NOTE: These functions are not built-in, they're implemented as an extension + // to NSImage in NSImage+Extension.swift. + guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil } + guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil } + + // Combine our layers using the proper blending modes + return.combine(images: [ + base, + screen, + screenGradient, + ghost, + tintedGhost, + crt, + gloss, + ], blendingModes: [ + .normal, + .normal, + .color, + .normal, + .color, + .overlay, + .normal, + ]) + } +} diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift new file mode 100644 index 000000000..3d37e1356 --- /dev/null +++ b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift @@ -0,0 +1,12 @@ +import SwiftUI +import Cocoa + +// For testing. +struct ColorizedGhosttyIconView: View { + var body: some View { + Image(nsImage: ColorizedGhosttyIcon( + screenColors: [.purple, .blue], + ghostColor: .yellow + ).makeImage()!) + } +} diff --git a/macos/Sources/Helpers/NSImage+Extension.swift b/macos/Sources/Helpers/NSImage+Extension.swift new file mode 100644 index 000000000..670148e27 --- /dev/null +++ b/macos/Sources/Helpers/NSImage+Extension.swift @@ -0,0 +1,90 @@ +import Cocoa + +extension NSImage { + /// Combine multiple images with the given blend modes. This is useful given a set + /// of layers to create a final rasterized image. + static func combine(images: [NSImage], blendingModes: [CGBlendMode]) -> NSImage? { + guard images.count == blendingModes.count else { return nil } + guard images.count > 0 else { return nil } + + // The final size will be the same size as our first image. + let size = images.first!.size + + // Create a bitmap context manually + guard let bitmapContext = CGContext( + data: nil, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: 0, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + + // Clear the context + bitmapContext.setFillColor(.clear) + bitmapContext.fill(.init(origin: .zero, size: size)) + + // Draw each image with its corresponding blend mode + for (index, image) in images.enumerated() { + guard let cgImage = image.cgImage( + forProposedRect: nil, + context: nil, + hints: nil + ) else { return nil } + + let blendMode = blendingModes[index] + bitmapContext.setBlendMode(blendMode) + bitmapContext.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + + // Create a CGImage from the context + guard let combinedCGImage = bitmapContext.makeImage() else { return nil } + + // Wrap the CGImage in an NSImage + return NSImage(cgImage: combinedCGImage, size: size) + } + + /// Apply a gradient onto this image, using this image as a mask. + func gradient(colors: [NSColor]) -> NSImage? { + let resultImage = NSImage(size: size) + resultImage.lockFocus() + defer { resultImage.unlockFocus() } + + // Draw the gradient + guard let gradient = NSGradient(colors: colors) else { return nil } + gradient.draw(in: .init(origin: .zero, size: size), angle: 90) + + // Apply the mask + draw(at: .zero, from: .zero, operation: .destinationIn, fraction: 1.0) + + return resultImage + } + + // Tint an NSImage with the given color by applying a basic fill on top of it. + func tint(color: NSColor) -> NSImage? { + // Create a new image with the same size as the base image + let newImage = NSImage(size: size) + + // Draw into the new image + newImage.lockFocus() + defer { newImage.unlockFocus() } + + // Set up the drawing context + guard let context = NSGraphicsContext.current?.cgContext else { return nil } + defer { context.restoreGState() } + + // Draw the base image + guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } + context.draw(cgImage, in: .init(origin: .zero, size: size)) + + // Set the tint color and blend mode + context.setFillColor(color.cgColor) + context.setBlendMode(.sourceAtop) + + // Apply the tint color over the entire image + context.fill(.init(origin: .zero, size: size)) + + return newImage + } +}