use std::ffi::c_void;
use std::path::{Path, PathBuf};
use objc2::msg_send;
use objc2::runtime::{AnyObject, Bool};
use objc2_foundation::{NSData, NSDate, NSPoint, NSRect, NSRunLoop, NSSize, NSString};
use truce_rack_core::editor::{PluginEditor, WindowHandle};
use truce_rack_core::error::{Error, Result};
const NS_BITMAP_FILE_TYPE_PNG: usize = 4;
pub fn capture_editor(
editor: &mut dyn PluginEditor,
width: u32,
height: u32,
out_path: &Path,
) -> Result<()> {
unsafe {
let app_cls = objc2::class!(NSApplication);
let _: *mut AnyObject = msg_send![app_cls, sharedApplication];
}
let rect = NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: NSSize {
width: f64::from(width),
height: f64::from(height),
},
};
let parent: *mut AnyObject = unsafe {
let cls = objc2::class!(NSView);
let alloc: *mut AnyObject = msg_send![cls, alloc];
let view: *mut AnyObject = msg_send![alloc, initWithFrame: rect];
view
};
if parent.is_null() {
return Err(Error::Other("NSView alloc/init returned nil".into()));
}
let result = capture_inner(editor, parent, rect, out_path);
if editor.is_open() {
editor.close();
}
unsafe {
let _: () = msg_send![parent, release];
}
result
}
fn capture_inner(
editor: &mut dyn PluginEditor,
parent: *mut AnyObject,
rect: NSRect,
out_path: &Path,
) -> Result<()> {
editor.open(WindowHandle::NSView(parent.cast::<c_void>()), 1.0)?;
editor.show();
let capture_rect = if let Some((w, h)) = editor.size() {
let new_rect = NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: NSSize {
width: f64::from(w),
height: f64::from(h),
},
};
unsafe {
let _: () = msg_send![parent, setFrame: new_rect];
}
new_rect
} else {
rect
};
let run_loop = NSRunLoop::currentRunLoop();
let date = NSDate::dateWithTimeIntervalSinceNow(0.5);
run_loop.runUntilDate(&date);
let rep: *mut AnyObject =
unsafe { msg_send![parent, bitmapImageRepForCachingDisplayInRect: capture_rect] };
if rep.is_null() {
return Err(Error::Other(
"bitmapImageRepForCachingDisplayInRect: returned nil".into(),
));
}
unsafe {
let _: () = msg_send![parent, cacheDisplayInRect: capture_rect, toBitmapImageRep: rep];
}
let png_data: *mut NSData = unsafe {
msg_send![
rep,
representationUsingType: NS_BITMAP_FILE_TYPE_PNG,
properties: std::ptr::null::<AnyObject>(),
]
};
if png_data.is_null() {
return Err(Error::Other(
"representationUsingType:NSBitmapImageFileTypePNG returned nil".into(),
));
}
if let Some(parent_dir) = out_path.parent()
&& !parent_dir.as_os_str().is_empty()
{
std::fs::create_dir_all(parent_dir)
.map_err(|e| Error::Other(format!("create_dir_all {}: {e}", parent_dir.display())))?;
}
let path_str = out_path
.to_str()
.ok_or_else(|| Error::Other(format!("non-utf8 path: {}", out_path.display())))?;
let path_ns = NSString::from_str(path_str);
let wrote: Bool = unsafe {
let path_ref: &NSString = &path_ns;
msg_send![png_data, writeToFile: path_ref, atomically: Bool::YES]
};
if !wrote.as_bool() {
return Err(Error::Other(format!(
"writeToFile: returned NO for {}",
out_path.display()
)));
}
Ok(())
}
#[must_use]
pub fn sanitize(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut last_was_dash = false;
for c in name.chars() {
let mapped = if c.is_ascii_alphanumeric() {
Some(c.to_ascii_lowercase())
} else if matches!(c, ' ' | '/' | '\\' | '.' | '_') {
Some('-')
} else {
None
};
if let Some(ch) = mapped {
if ch == '-' && last_was_dash {
continue;
}
out.push(ch);
last_was_dash = ch == '-';
}
}
while out.ends_with('-') {
out.pop();
}
if out.is_empty() {
out.push_str("unnamed");
}
out
}
#[must_use]
pub fn default_output_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("truce-rack-screenshots")
}