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