Files
ghostty-rs/crates/ghostty-macos/src/helpers/extensions/ns_image.rs
T
ddidderr 0137168c66 feat(macos): foundation of the macOS app ported from Swift to Rust
Phase 9, MA1: crates/ghostty-macos (AppKit via objc2; empty stub on
non-Apple targets) ports the foundation of Ghostty 1.3.1's Swift macOS
app (ghostty/macos): main, AppDelegate (+Ghostty) with lifecycle, dock
menu, open-file, notifications, shortcut sync and quit/reopen paths;
MainMenu.xib as generated menu tables built into NSMenus; the
Ghostty.App/Config/Surface wrappers driving libghostty in-process via
ghostty-lib; the generic helpers; scripts/macos-bundle.sh to assemble
Ghostty.app (dry-run on Linux). Feature areas are stubbed for MA2-MA5.
Design and type mapping: docs/rewrite/notes/macos-app-design.md
(App Intents is a Swift-only framework and cannot be ported).

Compile-checked only (no macOS here): just check-macos passes for
aarch64/x86_64-apple-darwin with -D warnings; 125 platform-independent
tests run on Linux.

Test Plan:
- just check-macos
- cargo test -p ghostty-macos
2026-10-12 19:25:44 +02:00

144 lines
4.8 KiB
Rust

//! `Helpers/Extensions/NSImage+Extension.swift`.
// SAFETY: every `unsafe` block in this module carries its own justification (FFI / objc2 calls).
#![allow(unsafe_code, deprecated)]
use objc2::{AnyThread, rc::Retained};
use objc2_app_kit::{NSColor, NSCompositingOperation, NSGradient, NSGraphicsContext, NSImage};
use objc2_core_graphics::{
CGBitmapContextCreate,
CGBitmapContextCreateImage,
CGBlendMode,
CGColorSpace,
CGContext,
CGImageAlphaInfo,
};
use objc2_foundation::{NSArray, NSPoint, NSRect, NSSize};
fn zero_rect(size: NSSize) -> NSRect {
NSRect::new(NSPoint::new(0.0, 0.0), size)
}
/// `NSImage.cgImage(forProposedRect: nil, context: nil, hints: nil)`.
#[allow(unsafe_code)]
fn cg_image(image: &NSImage) -> Option<Retained<objc2_core_graphics::CGImage>> {
// SAFETY: nil rect/context/hints are allowed.
unsafe { image.CGImageForProposedRect_context_hints(std::ptr::null_mut(), None, None) }
}
pub trait NSImageExt {
/// Apply a gradient onto this image, using this image as a mask.
fn gradient(&self, colors: &[Retained<NSColor>]) -> Option<Retained<NSImage>>;
/// Tint an NSImage with the given color by applying a basic fill on top of it.
fn tint(&self, color: &NSColor) -> Option<Retained<NSImage>>;
}
/// `NSImage.combine(images:blendingModes:)`: combine multiple images with the
/// given blend modes. This is useful given a set of layers to create a final
/// rasterized image.
#[allow(unsafe_code)]
#[must_use]
pub fn combine(
images: &[Retained<NSImage>],
blending_modes: &[CGBlendMode],
) -> Option<Retained<NSImage>> {
if images.len() != blending_modes.len() || images.is_empty() {
return None;
}
// The final size will be the same size as our first image.
let size = images[0].size();
// Create a bitmap context manually
let space = CGColorSpace::new_device_rgb();
// SAFETY: null data lets CoreGraphics allocate the backing store.
let ctx = unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
size.width as usize,
size.height as usize,
8,
0,
space.as_deref(),
CGImageAlphaInfo::PremultipliedLast.0,
)
}?;
// Clear the context
CGContext::set_fill_color_with_color(Some(&ctx), None);
CGContext::clear_rect(Some(&ctx), zero_rect(size));
// Draw each image with its corresponding blend mode
for (image, mode) in images.iter().zip(blending_modes) {
let cg = cg_image(image)?;
CGContext::set_blend_mode(Some(&ctx), *mode);
CGContext::draw_image(Some(&ctx), zero_rect(size), Some(&cg));
}
// Create a CGImage from the context
let combined = CGBitmapContextCreateImage(Some(&ctx))?;
// Wrap the CGImage in an NSImage
Some(NSImage::initWithCGImage_size(
NSImage::alloc(),
&combined,
size,
))
}
impl NSImageExt for NSImage {
fn gradient(&self, colors: &[Retained<NSColor>]) -> Option<Retained<NSImage>> {
let size = self.size();
let result = NSImage::initWithSize(NSImage::alloc(), size);
result.lockFocus();
let out = (|| {
// Draw the gradient
let array = NSArray::from_retained_slice(colors);
let gradient = NSGradient::initWithColors(NSGradient::alloc(), &array)?;
gradient.drawInRect_angle(zero_rect(size), 90.0);
// Apply the mask
self.drawAtPoint_fromRect_operation_fraction(
NSPoint::new(0.0, 0.0),
NSRect::ZERO,
NSCompositingOperation::DestinationIn,
1.0,
);
Some(())
})();
result.unlockFocus();
out.map(|()| result)
}
fn tint(&self, color: &NSColor) -> Option<Retained<NSImage>> {
let size = self.size();
// Create a new image with the same size as the base image
let new_image = NSImage::initWithSize(NSImage::alloc(), size);
// Draw into the new image
new_image.lockFocus();
let out = (|| {
// Set up the drawing context
let context = NSGraphicsContext::currentContext()?.CGContext();
// Draw the base image
let cg = cg_image(self)?;
CGContext::draw_image(Some(&context), zero_rect(size), Some(&cg));
// Set the tint color and blend mode
CGContext::set_fill_color_with_color(Some(&context), Some(&color.CGColor()));
CGContext::set_blend_mode(Some(&context), CGBlendMode::SourceAtop);
// Apply the tint color over the entire image
CGContext::fill_rect(Some(&context), zero_rect(size));
Some(context)
})();
if let Some(context) = &out {
CGContext::restore_g_state(Some(context));
}
new_image.unlockFocus();
out.map(|_| new_image)
}
}