windows: Refactor clipboard implementation (#14347)

张小白 created

This PR provides a similar implementation to the macOS clipboard
implementation, adds support for metadata and includes tests.

Release Notes:

- N/A

Change summary

Cargo.lock                                          |  17 -
Cargo.toml                                          |   1 
crates/gpui/Cargo.toml                              |  10 
crates/gpui/src/platform.rs                         |   2 
crates/gpui/src/platform/cosmic_text/text_system.rs |   3 
crates/gpui/src/platform/windows/direct_write.rs    |  10 
crates/gpui/src/platform/windows/platform.rs        | 172 +++++++++++++-
7 files changed, 180 insertions(+), 35 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2406,16 +2406,6 @@ dependencies = [
  "worktree",
 ]
 
-[[package]]
-name = "clipboard-win"
-version = "3.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
-dependencies = [
- "lazy-bytes-cast",
- "winapi",
-]
-
 [[package]]
 name = "clock"
 version = "0.1.0"
@@ -4883,7 +4873,6 @@ dependencies = [
  "calloop",
  "calloop-wayland-source",
  "cbindgen",
- "clipboard-win",
  "cocoa",
  "collections",
  "core-foundation",
@@ -6068,12 +6057,6 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "lazy-bytes-cast"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
-
 [[package]]
 name = "lazy_static"
 version = "1.4.0"

Cargo.toml 🔗

@@ -452,6 +452,7 @@ features = [
     "Win32_System_Com_StructuredStorage",
     "Win32_System_DataExchange",
     "Win32_System_LibraryLoader",
+    "Win32_System_Memory",
     "Win32_System_Ole",
     "Win32_System_SystemInformation",
     "Win32_System_SystemServices",

crates/gpui/Cargo.toml 🔗

@@ -134,18 +134,22 @@ x11rb = { version = "0.13.0", features = [
     "resource_manager",
     "sync",
 ] }
-xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "2d4c4439160c7846ede0f0ece93bf73b1e613339", features = ["wayland", "x11"] }
+xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "2d4c4439160c7846ede0f0ece93bf73b1e613339", features = [
+    "wayland",
+    "x11",
+] }
 xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
     "x11rb-xcb",
     "x11rb-client",
 ] }
-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = [
+    "source-fontconfig-dlopen",
+] }
 x11-clipboard = "0.9.2"
 
 [target.'cfg(windows)'.dependencies]
 windows.workspace = true
 windows-core = "0.57"
-clipboard-win = "3.1.1"
 
 [[example]]
 name = "hello_world"

crates/gpui/src/platform.rs 🔗

@@ -406,6 +406,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
         raster_bounds: Bounds<DevicePixels>,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
+    #[cfg(target_os = "windows")]
+    fn destroy(&self);
 }
 
 #[derive(PartialEq, Eq, Hash, Clone)]

crates/gpui/src/platform/cosmic_text/text_system.rs 🔗

@@ -177,6 +177,9 @@ impl PlatformTextSystem for CosmicTextSystem {
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
         self.0.write().layout_line(text, font_size, runs)
     }
+
+    #[cfg(target_os = "windows")]
+    fn destroy(&self) {}
 }
 
 impl CosmicTextSystemState {

crates/gpui/src/platform/windows/direct_write.rs 🔗

@@ -1,4 +1,4 @@
-use std::{borrow::Cow, sync::Arc};
+use std::{borrow::Cow, mem::ManuallyDrop, sync::Arc};
 
 use ::util::ResultExt;
 use anyhow::{anyhow, Result};
@@ -39,7 +39,7 @@ pub(crate) struct DirectWriteTextSystem(RwLock<DirectWriteState>);
 struct DirectWriteComponent {
     locale: String,
     factory: IDWriteFactory5,
-    bitmap_factory: IWICImagingFactory2,
+    bitmap_factory: ManuallyDrop<IWICImagingFactory2>,
     d2d1_factory: ID2D1Factory,
     in_memory_loader: IDWriteInMemoryFontFileLoader,
     builder: IDWriteFontSetBuilder1,
@@ -79,6 +79,7 @@ impl DirectWriteComponent {
             let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
             let bitmap_factory: IWICImagingFactory2 =
                 CoCreateInstance(&CLSID_WICImagingFactory2, None, CLSCTX_INPROC_SERVER)?;
+            let bitmap_factory = ManuallyDrop::new(bitmap_factory);
             let d2d1_factory: ID2D1Factory =
                 D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?;
             // The `IDWriteInMemoryFontFileLoader` here is supported starting from
@@ -238,6 +239,11 @@ impl PlatformTextSystem for DirectWriteTextSystem {
                 ..Default::default()
             })
     }
+
+    fn destroy(&self) {
+        let mut lock = self.0.write();
+        unsafe { ManuallyDrop::drop(&mut lock.components.bitmap_factory) };
+    }
 }
 
 impl DirectWriteState {

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -12,7 +12,6 @@ use std::{
 
 use ::util::ResultExt;
 use anyhow::{anyhow, Context, Result};
-use clipboard_win::{get_clipboard_string, set_clipboard_string};
 use futures::channel::oneshot::{self, Receiver};
 use itertools::Itertools;
 use parking_lot::RwLock;
@@ -22,9 +21,22 @@ use windows::{
     core::*,
     Win32::{
         Foundation::*,
+        Globalization::u_memcpy,
         Graphics::Gdi::*,
         Security::Credentials::*,
-        System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
+        System::{
+            Com::*,
+            DataExchange::{
+                CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard,
+                RegisterClipboardFormatW, SetClipboardData,
+            },
+            LibraryLoader::*,
+            Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
+            Ole::*,
+            SystemInformation::*,
+            Threading::*,
+            Time::*,
+        },
         UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
     },
     UI::ViewManagement::UISettings,
@@ -40,6 +52,8 @@ pub(crate) struct WindowsPlatform {
     background_executor: BackgroundExecutor,
     foreground_executor: ForegroundExecutor,
     text_system: Arc<dyn PlatformTextSystem>,
+    clipboard_hash_format: u32,
+    clipboard_metadata_format: u32,
 }
 
 pub(crate) struct WindowsPlatformState {
@@ -88,6 +102,9 @@ impl WindowsPlatform {
         let icon = load_icon().unwrap_or_default();
         let state = RefCell::new(WindowsPlatformState::new());
         let raw_window_handles = RwLock::new(SmallVec::new());
+        let clipboard_hash_format = register_clipboard_format(CLIPBOARD_HASH_FORMAT).unwrap();
+        let clipboard_metadata_format =
+            register_clipboard_format(CLIPBOARD_METADATA_FORMAT).unwrap();
 
         Self {
             state,
@@ -96,6 +113,8 @@ impl WindowsPlatform {
             background_executor,
             foreground_executor,
             text_system,
+            clipboard_hash_format,
+            clipboard_metadata_format,
         }
     }
 
@@ -498,17 +517,15 @@ impl Platform for WindowsPlatform {
     }
 
     fn write_to_clipboard(&self, item: ClipboardItem) {
-        if item.text.len() > 0 {
-            set_clipboard_string(item.text()).unwrap();
-        }
+        write_to_clipboard(
+            item,
+            self.clipboard_hash_format,
+            self.clipboard_metadata_format,
+        );
     }
 
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
-        let text = get_clipboard_string().ok()?;
-        Some(ClipboardItem {
-            text,
-            metadata: None,
-        })
+        read_from_clipboard(self.clipboard_hash_format, self.clipboard_metadata_format)
     }
 
     fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
@@ -586,9 +603,8 @@ impl Platform for WindowsPlatform {
 
 impl Drop for WindowsPlatform {
     fn drop(&mut self) {
-        unsafe {
-            OleUninitialize();
-        }
+        self.text_system.destroy();
+        unsafe { OleUninitialize() };
     }
 }
 
@@ -680,3 +696,133 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
     let ui_settings = UISettings::new()?;
     Ok(ui_settings.AutoHideScrollBars()?)
 }
+
+fn register_clipboard_format(format: PCWSTR) -> Result<u32> {
+    let ret = unsafe { RegisterClipboardFormatW(format) };
+    if ret == 0 {
+        Err(anyhow::anyhow!(
+            "Error when registering clipboard format: {}",
+            std::io::Error::last_os_error()
+        ))
+    } else {
+        Ok(ret)
+    }
+}
+
+fn write_to_clipboard(item: ClipboardItem, hash_format: u32, metadata_format: u32) {
+    write_to_clipboard_inner(item, hash_format, metadata_format).log_err();
+    unsafe { CloseClipboard().log_err() };
+}
+
+fn write_to_clipboard_inner(
+    item: ClipboardItem,
+    hash_format: u32,
+    metadata_format: u32,
+) -> Result<()> {
+    unsafe {
+        OpenClipboard(None)?;
+        EmptyClipboard()?;
+        let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
+        set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
+
+        if let Some(ref metadata) = item.metadata {
+            let hash_result = {
+                let hash = ClipboardItem::text_hash(&item.text);
+                hash.to_ne_bytes()
+            };
+            let encode_wide = std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4);
+            set_data_to_clipboard(encode_wide, hash_format)?;
+
+            let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
+            set_data_to_clipboard(&metadata_wide, metadata_format)?;
+        }
+    }
+    Ok(())
+}
+
+fn set_data_to_clipboard(data: &[u16], format: u32) -> Result<()> {
+    unsafe {
+        let global = GlobalAlloc(GMEM_MOVEABLE, data.len() * 2)?;
+        let handle = GlobalLock(global);
+        u_memcpy(handle as _, data.as_ptr(), data.len() as _);
+        let _ = GlobalUnlock(global);
+        SetClipboardData(format, HANDLE(global.0 as isize))?;
+    }
+    Ok(())
+}
+
+fn read_from_clipboard(hash_format: u32, metadata_format: u32) -> Option<ClipboardItem> {
+    let result = read_from_clipboard_inner(hash_format, metadata_format).log_err();
+    unsafe { CloseClipboard().log_err() };
+    result
+}
+
+fn read_from_clipboard_inner(hash_format: u32, metadata_format: u32) -> Result<ClipboardItem> {
+    unsafe {
+        OpenClipboard(None)?;
+        let text = {
+            let handle = GetClipboardData(CF_UNICODETEXT.0 as u32)?;
+            let text = PCWSTR(handle.0 as *const u16);
+            String::from_utf16_lossy(text.as_wide())
+        };
+        let mut item = ClipboardItem {
+            text,
+            metadata: None,
+        };
+        let Some(hash) = read_hash_from_clipboard(hash_format) else {
+            return Ok(item);
+        };
+        let Some(metadata) = read_metadata_from_clipboard(metadata_format) else {
+            return Ok(item);
+        };
+        if hash == ClipboardItem::text_hash(&item.text) {
+            item.metadata = Some(metadata);
+        }
+        Ok(item)
+    }
+}
+
+fn read_hash_from_clipboard(hash_format: u32) -> Option<u64> {
+    unsafe {
+        let handle = GetClipboardData(hash_format).log_err()?;
+        let raw_ptr = handle.0 as *const u16;
+        let hash_bytes: [u8; 8] = std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
+            .to_vec()
+            .try_into()
+            .log_err()?;
+        Some(u64::from_ne_bytes(hash_bytes))
+    }
+}
+
+fn read_metadata_from_clipboard(metadata_format: u32) -> Option<String> {
+    unsafe {
+        let handle = GetClipboardData(metadata_format).log_err()?;
+        let text = PCWSTR(handle.0 as *const u16);
+        Some(String::from_utf16_lossy(text.as_wide()))
+    }
+}
+
+// clipboard
+pub const CLIPBOARD_HASH_FORMAT: PCWSTR = windows::core::w!("zed-text-hash");
+pub const CLIPBOARD_METADATA_FORMAT: PCWSTR = windows::core::w!("zed-metadata");
+
+#[cfg(test)]
+mod tests {
+    use crate::{ClipboardItem, Platform, WindowsPlatform};
+
+    #[test]
+    fn test_clipboard() {
+        let platform = WindowsPlatform::new();
+        let item = ClipboardItem::new("你好".to_string());
+        platform.write_to_clipboard(item.clone());
+        assert_eq!(platform.read_from_clipboard(), Some(item));
+
+        let item = ClipboardItem::new("12345".to_string());
+        platform.write_to_clipboard(item.clone());
+        assert_eq!(platform.read_from_clipboard(), Some(item));
+
+        let item = ClipboardItem::new("abcdef".to_string()).with_metadata(vec![3, 4]);
+        platform.write_to_clipboard(item.clone());
+        assert_eq!(platform.read_from_clipboard(), Some(item));
+    }
+}