1use std::sync::LazyLock;
2
3use anyhow::Result;
4use collections::{FxHashMap, FxHashSet};
5use itertools::Itertools;
6use windows::Win32::{
7 Foundation::{HANDLE, HGLOBAL},
8 System::{
9 DataExchange::{
10 CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
11 GetClipboardData, GetClipboardFormatNameW, IsClipboardFormatAvailable, OpenClipboard,
12 RegisterClipboardFormatW, SetClipboardData,
13 },
14 Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock},
15 Ole::{CF_HDROP, CF_UNICODETEXT},
16 },
17 UI::Shell::{DragQueryFileW, HDROP},
18};
19use windows_core::PCWSTR;
20
21use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash};
22
23// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
24const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
25
26// Clipboard formats
27static CLIPBOARD_HASH_FORMAT: LazyLock<u32> =
28 LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash")));
29static CLIPBOARD_METADATA_FORMAT: LazyLock<u32> =
30 LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata")));
31static CLIPBOARD_SVG_FORMAT: LazyLock<u32> =
32 LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml")));
33static CLIPBOARD_GIF_FORMAT: LazyLock<u32> =
34 LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF")));
35static CLIPBOARD_PNG_FORMAT: LazyLock<u32> =
36 LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG")));
37static CLIPBOARD_JPG_FORMAT: LazyLock<u32> =
38 LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF")));
39
40// Helper maps and sets
41static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::new(|| {
42 let mut formats_map = FxHashMap::default();
43 formats_map.insert(CF_UNICODETEXT.0 as u32, ClipboardFormatType::Text);
44 formats_map.insert(*CLIPBOARD_PNG_FORMAT, ClipboardFormatType::Image);
45 formats_map.insert(*CLIPBOARD_GIF_FORMAT, ClipboardFormatType::Image);
46 formats_map.insert(*CLIPBOARD_JPG_FORMAT, ClipboardFormatType::Image);
47 formats_map.insert(*CLIPBOARD_SVG_FORMAT, ClipboardFormatType::Image);
48 formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
49 formats_map
50});
51static FORMATS_SET: LazyLock<FxHashSet<u32>> = LazyLock::new(|| {
52 let mut formats_map = FxHashSet::default();
53 formats_map.insert(CF_UNICODETEXT.0 as u32);
54 formats_map.insert(*CLIPBOARD_PNG_FORMAT);
55 formats_map.insert(*CLIPBOARD_GIF_FORMAT);
56 formats_map.insert(*CLIPBOARD_JPG_FORMAT);
57 formats_map.insert(*CLIPBOARD_SVG_FORMAT);
58 formats_map.insert(CF_HDROP.0 as u32);
59 formats_map
60});
61static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
62 let mut formats_map = FxHashMap::default();
63 formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
64 formats_map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
65 formats_map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
66 formats_map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
67 formats_map
68});
69
70#[derive(Debug, Clone, Copy)]
71enum ClipboardFormatType {
72 Text,
73 Image,
74 Files,
75}
76
77pub(crate) fn write_to_clipboard(item: ClipboardItem) {
78 with_clipboard(|| write_to_clipboard_inner(item));
79}
80
81pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
82 with_clipboard(|| {
83 with_best_match_format(|item_format| match format_to_type(item_format) {
84 ClipboardFormatType::Text => read_string_from_clipboard(),
85 ClipboardFormatType::Image => read_image_from_clipboard(item_format),
86 ClipboardFormatType::Files => read_files_from_clipboard(),
87 })
88 })
89 .flatten()
90}
91
92pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
93where
94 F: FnMut(String),
95{
96 let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
97 for file_index in 0..file_count {
98 let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
99 let mut buffer = vec![0u16; filename_length + 1];
100 let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
101 if ret == 0 {
102 log::error!("unable to read file name of dragged file");
103 continue;
104 }
105 match String::from_utf16(&buffer[0..filename_length]) {
106 Ok(file_name) => f(file_name),
107 Err(e) => {
108 log::error!("dragged file name is not UTF-16: {}", e)
109 }
110 }
111 }
112}
113
114fn with_clipboard<F, T>(f: F) -> Option<T>
115where
116 F: FnOnce() -> T,
117{
118 match unsafe { OpenClipboard(None) } {
119 Ok(()) => {
120 let result = f();
121 if let Err(e) = unsafe { CloseClipboard() } {
122 log::error!("Failed to close clipboard: {e}",);
123 }
124 Some(result)
125 }
126 Err(e) => {
127 log::error!("Failed to open clipboard: {e}",);
128 None
129 }
130 }
131}
132
133fn register_clipboard_format(format: PCWSTR) -> u32 {
134 let ret = unsafe { RegisterClipboardFormatW(format) };
135 if ret == 0 {
136 panic!(
137 "Error when registering clipboard format: {}",
138 std::io::Error::last_os_error()
139 );
140 }
141 ret
142}
143
144#[inline]
145fn format_to_type(item_format: u32) -> &'static ClipboardFormatType {
146 FORMATS_MAP.get(&item_format).unwrap()
147}
148
149// Currently, we only write the first item.
150fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
151 unsafe {
152 EmptyClipboard()?;
153 }
154 match item.entries().first() {
155 Some(entry) => match entry {
156 ClipboardEntry::String(string) => {
157 write_string_to_clipboard(string)?;
158 }
159 ClipboardEntry::Image(image) => {
160 write_image_to_clipboard(image)?;
161 }
162 },
163 None => {
164 // Writing an empty list of entries just clears the clipboard.
165 }
166 }
167 Ok(())
168}
169
170fn write_string_to_clipboard(item: &ClipboardString) -> Result<()> {
171 let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
172 set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
173
174 if let Some(metadata) = item.metadata.as_ref() {
175 let hash_result = {
176 let hash = ClipboardString::text_hash(&item.text);
177 hash.to_ne_bytes()
178 };
179 let encode_wide =
180 unsafe { std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4) };
181 set_data_to_clipboard(encode_wide, *CLIPBOARD_HASH_FORMAT)?;
182
183 let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
184 set_data_to_clipboard(&metadata_wide, *CLIPBOARD_METADATA_FORMAT)?;
185 }
186 Ok(())
187}
188
189fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
190 unsafe {
191 let global = GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?;
192 let handle = GlobalLock(global);
193 std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
194 let _ = GlobalUnlock(global);
195 SetClipboardData(format, Some(HANDLE(global.0)))?;
196 }
197 Ok(())
198}
199
200// Here writing PNG to the clipboard to better support other apps. For more info, please ref to
201// the PR.
202fn write_image_to_clipboard(item: &Image) -> Result<()> {
203 match item.format {
204 ImageFormat::Svg => set_data_to_clipboard(item.bytes(), *CLIPBOARD_SVG_FORMAT)?,
205 ImageFormat::Gif => {
206 set_data_to_clipboard(item.bytes(), *CLIPBOARD_GIF_FORMAT)?;
207 let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Gif)?;
208 set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
209 }
210 ImageFormat::Png => {
211 set_data_to_clipboard(item.bytes(), *CLIPBOARD_PNG_FORMAT)?;
212 let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Png)?;
213 set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
214 }
215 ImageFormat::Jpeg => {
216 set_data_to_clipboard(item.bytes(), *CLIPBOARD_JPG_FORMAT)?;
217 let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Jpeg)?;
218 set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
219 }
220 other => {
221 log::warn!(
222 "Clipboard unsupported image format: {:?}, convert to PNG instead.",
223 item.format
224 );
225 let png_bytes = convert_image_to_png_format(item.bytes(), other)?;
226 set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
227 }
228 }
229 Ok(())
230}
231
232fn convert_image_to_png_format(bytes: &[u8], image_format: ImageFormat) -> Result<Vec<u8>> {
233 let image = image::load_from_memory_with_format(bytes, image_format.into())?;
234 let mut output_buf = Vec::new();
235 image.write_to(
236 &mut std::io::Cursor::new(&mut output_buf),
237 image::ImageFormat::Png,
238 )?;
239 Ok(output_buf)
240}
241
242// Here, we enumerate all formats on the clipboard and find the first one that we can process.
243// The reason we don't use `GetPriorityClipboardFormat` is that it sometimes returns the
244// wrong format.
245// For instance, when copying a JPEG image from Microsoft Word, there may be several formats
246// on the clipboard: Jpeg, Png, Svg.
247// If we use `GetPriorityClipboardFormat`, it will return Svg, which is not what we want.
248fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
249where
250 F: Fn(u32) -> Option<ClipboardEntry>,
251{
252 let count = unsafe { CountClipboardFormats() };
253 let mut clipboard_format = 0;
254 for _ in 0..count {
255 clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
256 let Some(item_format) = FORMATS_SET.get(&clipboard_format) else {
257 continue;
258 };
259 if let Some(entry) = f(*item_format) {
260 return Some(ClipboardItem {
261 entries: vec![entry],
262 });
263 }
264 }
265 // log the formats that we don't support yet.
266 {
267 clipboard_format = 0;
268 for _ in 0..count {
269 clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
270 let mut buffer = [0u16; 64];
271 unsafe { GetClipboardFormatNameW(clipboard_format, &mut buffer) };
272 let format_name = String::from_utf16_lossy(&buffer);
273 log::warn!(
274 "Try to paste with unsupported clipboard format: {}, {}.",
275 clipboard_format,
276 format_name
277 );
278 }
279 }
280 None
281}
282
283fn read_string_from_clipboard() -> Option<ClipboardEntry> {
284 let text = with_clipboard_data(CF_UNICODETEXT.0 as u32, |data_ptr, _| {
285 let pcwstr = PCWSTR(data_ptr as *const u16);
286 String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
287 })?;
288 let Some(hash) = read_hash_from_clipboard() else {
289 return Some(ClipboardEntry::String(ClipboardString::new(text)));
290 };
291 let Some(metadata) = read_metadata_from_clipboard() else {
292 return Some(ClipboardEntry::String(ClipboardString::new(text)));
293 };
294 if hash == ClipboardString::text_hash(&text) {
295 Some(ClipboardEntry::String(ClipboardString {
296 text,
297 metadata: Some(metadata),
298 }))
299 } else {
300 Some(ClipboardEntry::String(ClipboardString::new(text)))
301 }
302}
303
304fn read_hash_from_clipboard() -> Option<u64> {
305 if unsafe { IsClipboardFormatAvailable(*CLIPBOARD_HASH_FORMAT).is_err() } {
306 return None;
307 }
308 with_clipboard_data(*CLIPBOARD_HASH_FORMAT, |data_ptr, size| {
309 if size < 8 {
310 return None;
311 }
312 let hash_bytes: [u8; 8] = unsafe {
313 std::slice::from_raw_parts(data_ptr.cast::<u8>(), 8)
314 .try_into()
315 .ok()
316 }?;
317 Some(u64::from_ne_bytes(hash_bytes))
318 })?
319}
320
321fn read_metadata_from_clipboard() -> Option<String> {
322 unsafe { IsClipboardFormatAvailable(*CLIPBOARD_METADATA_FORMAT).ok()? };
323 with_clipboard_data(*CLIPBOARD_METADATA_FORMAT, |data_ptr, _size| {
324 let pcwstr = PCWSTR(data_ptr as *const u16);
325 String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
326 })
327}
328
329fn read_image_from_clipboard(format: u32) -> Option<ClipboardEntry> {
330 let image_format = format_number_to_image_format(format)?;
331 read_image_for_type(format, *image_format)
332}
333
334#[inline]
335fn format_number_to_image_format(format_number: u32) -> Option<&'static ImageFormat> {
336 IMAGE_FORMATS_MAP.get(&format_number)
337}
338
339fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<ClipboardEntry> {
340 let (bytes, id) = with_clipboard_data(format_number, |data_ptr, size| {
341 let bytes = unsafe { std::slice::from_raw_parts(data_ptr as *mut u8 as _, size).to_vec() };
342 let id = hash(&bytes);
343 (bytes, id)
344 })?;
345 Some(ClipboardEntry::Image(Image { format, bytes, id }))
346}
347
348fn read_files_from_clipboard() -> Option<ClipboardEntry> {
349 let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
350 let hdrop = HDROP(data_ptr);
351 let mut filenames = String::new();
352 with_file_names(hdrop, |file_name| {
353 filenames.push_str(&file_name);
354 });
355 filenames
356 })?;
357 Some(ClipboardEntry::String(ClipboardString {
358 text,
359 metadata: None,
360 }))
361}
362
363fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>
364where
365 F: FnOnce(*mut std::ffi::c_void, usize) -> R,
366{
367 let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0);
368 let size = unsafe { GlobalSize(global) };
369 let data_ptr = unsafe { GlobalLock(global) };
370 let result = f(data_ptr, size);
371 unsafe { GlobalUnlock(global).ok() };
372 Some(result)
373}
374
375impl From<ImageFormat> for image::ImageFormat {
376 fn from(value: ImageFormat) -> Self {
377 match value {
378 ImageFormat::Png => image::ImageFormat::Png,
379 ImageFormat::Jpeg => image::ImageFormat::Jpeg,
380 ImageFormat::Webp => image::ImageFormat::WebP,
381 ImageFormat::Gif => image::ImageFormat::Gif,
382 // TODO: ImageFormat::Svg
383 ImageFormat::Bmp => image::ImageFormat::Bmp,
384 ImageFormat::Tiff => image::ImageFormat::Tiff,
385 _ => unreachable!(),
386 }
387 }
388}