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