clipboard.rs

  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}