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,
  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}