Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/clipboard.rs
1use anyhow::{anyhow, Result};
2use cocoa::appkit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString};
3use cocoa::foundation::{NSArray, NSData};
4use cocoa::{
5 base::{id, nil},
6 foundation::NSString,
7};
8use objc::{class, msg_send, sel, sel_impl};
9use std::ffi::CStr;
10use std::os::raw::{c_uchar, c_void};
11use std::slice;
12 
13use super::make_nsstring;
14use strato_ui_core::clipboard::{ClipboardContent, ImageData};
15 
16extern "C" {
17 fn getFilePathsFromPasteboard() -> id;
18}
19 
20pub struct Clipboard(id);
21 
22unsafe impl Send for Clipboard {}
23 
24impl Clipboard {
25 pub fn new() -> Result<Self> {
26 let pboard = unsafe { NSPasteboard::generalPasteboard(nil) };
27 if pboard.is_null() {
28 Err(anyhow!("NSPasteboard::generalPasteboard returned nil"))
29 } else {
30 Ok(Clipboard(pboard))
31 }
32 }
33}
34 
35unsafe fn pasteboard_type_for_image_mime_type(mime_type: &str) -> Option<id> {
36 let pasteboard_type = match mime_type {
37 "image/png" => "public.png",
38 "image/jpeg" => "public.jpeg",
39 "image/gif" => "public.gif",
40 "image/webp" => "public.webp",
41 "image/svg+xml" => "public.svg-image",
42 _ => return None,
43 };
44 Some(make_nsstring(pasteboard_type))
45}
46 
47impl crate::Clipboard for Clipboard {
48 fn write(&mut self, contents: ClipboardContent) {
49 unsafe {
50 let nsstr = make_nsstring(&contents.plain_text);
51 self.0
52 .declareTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeString), nil);
53 NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeString);
54 
55 if let Some(html) = contents.html {
56 let nsstr = make_nsstring(&html);
57 self.0
58 .addTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeHTML), nil);
59 NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeHTML);
60 }
61 
62 if let Some(images) = contents.images {
63 for image in images {
64 let Some(pasteboard_type) =
65 pasteboard_type_for_image_mime_type(&image.mime_type)
66 else {
67 continue;
68 };
69 let data: id = msg_send![class!(NSData), alloc];
70 let data: id = data.initWithBytes_length_(
71 image.data.as_ptr() as *const c_void,
72 image.data.len() as u64,
73 );
74 self.0
75 .addTypes_owner(NSArray::arrayWithObject(nil, pasteboard_type), nil);
76 let _: () = msg_send![self.0, setData: data forType: pasteboard_type];
77 // Balance the +1 retain from `[NSData alloc]`. The pasteboard retains
78 // `data` in `setData:forType:`, so the object stays alive as needed.
79 let _: () = msg_send![data, release];
80 }
81 }
82 }
83 }
84 
85 fn read(&mut self) -> ClipboardContent {
86 unsafe {
87 // Try getting file paths from the clipboard. If we end up with an empty
88 // array of file paths, fallback to getting the string from the pasteboard.
89 let file_paths = getFilePathsFromPasteboard();
90 let available_paths = file_paths.count();
91 
92 let text = NSPasteboard::stringForType(self.0, NSPasteboardTypeString);
93 let mut content = ClipboardContent::plain_text(if text != nil {
94 CStr::from_ptr(text.UTF8String())
95 .to_str()
96 .unwrap_or("")
97 .to_string()
98 } else {
99 String::from("")
100 });
101 
102 if available_paths > 0 {
103 content.paths = Some(
104 (0..available_paths)
105 .map(|i| {
106 let directory = file_paths.objectAtIndex(i);
107 let slice = slice::from_raw_parts(
108 directory.UTF8String() as *const c_uchar,
109 directory.len(),
110 );
111 std::str::from_utf8_unchecked(slice).to_string()
112 })
113 .collect::<Vec<String>>(),
114 );
115 }
116 
117 let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
118 if html != nil {
119 content.html = Some(
120 CStr::from_ptr(html.UTF8String())
121 .to_str()
122 .unwrap_or("")
123 .to_string(),
124 )
125 }
126 
127 // Try to read image data from clipboard
128 content.images = self.read_image_data_from_pasteboard();
129 
130 content
131 }
132 }
133}
134 
135impl Clipboard {
136 /// Reads image data from the macOS pasteboard.
137 ///
138 /// Checks for supported image formats and returns the first available image
139 /// data found, prioritizing common web-compatible formats.
140 fn read_image_data_from_pasteboard(&self) -> Option<Vec<ImageData>> {
141 unsafe {
142 // Check for common image types on macOS pasteboard
143 // macOS pasteboard type identifiers for supported image formats
144 // Ordered by preference for web compatibility
145 let supported_pasteboard_types = [
146 make_nsstring("public.png"),
147 make_nsstring("public.jpeg"),
148 make_nsstring("public.gif"),
149 make_nsstring("public.webp"),
150 make_nsstring("public.svg-image"),
151 make_nsstring("com.compuserve.gif"),
152 ];
153 
154 let mut images = Vec::new();
155 
156 for &pasteboard_type in &supported_pasteboard_types {
157 let data = NSPasteboard::dataForType(self.0, pasteboard_type);
158 if data != nil {
159 let length = NSData::length(data);
160 if length > 0 {
161 let bytes_ptr = NSData::bytes(data) as *const u8;
162 let bytes = slice::from_raw_parts(bytes_ptr, length as usize);
163 
164 let mime_type = match CStr::from_ptr(pasteboard_type.UTF8String())
165 .to_str()
166 .unwrap_or("")
167 {
168 "public.png" => "image/png",
169 "public.jpeg" => "image/jpeg",
170 "public.gif" | "com.compuserve.gif" => "image/gif",
171 "public.webp" => "image/webp",
172 "public.svg-image" => "image/svg+xml",
173 _ => "image/unknown",
174 };
175 
176 // Try to extract filename from HTML content if available
177 let filename = {
178 let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
179 if html != nil {
180 let html_str =
181 CStr::from_ptr(html.UTF8String()).to_str().unwrap_or("");
182 if !html_str.is_empty() {
183 crate::clipboard_utils::extract_filename_from_html(html_str)
184 } else {
185 None
186 }
187 } else {
188 None
189 }
190 };
191 
192 images.push(ImageData {
193 data: bytes.to_vec(),
194 mime_type: mime_type.to_string(),
195 filename,
196 });
197 }
198 }
199 }
200 
201 if images.is_empty() {
202 None
203 } else {
204 Some(images)
205 }
206 }
207 }
208}
209 
210#[cfg(test)]
211#[path = "clipboard_tests.rs"]
212mod tests;
213