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