platform.rs

  1use std::{
  2    cell::RefCell,
  3    mem::ManuallyDrop,
  4    path::{Path, PathBuf},
  5    rc::Rc,
  6    sync::Arc,
  7};
  8
  9use ::util::ResultExt;
 10use anyhow::{anyhow, Context, Result};
 11use async_task::Runnable;
 12use futures::channel::oneshot::{self, Receiver};
 13use itertools::Itertools;
 14use parking_lot::RwLock;
 15use smallvec::SmallVec;
 16use windows::{
 17    core::*,
 18    Win32::{
 19        Foundation::*,
 20        Graphics::{
 21            Gdi::*,
 22            Imaging::{CLSID_WICImagingFactory, IWICImagingFactory},
 23        },
 24        Security::Credentials::*,
 25        System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*},
 26        UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
 27    },
 28    UI::ViewManagement::UISettings,
 29};
 30
 31use crate::*;
 32
 33pub(crate) struct WindowsPlatform {
 34    state: RefCell<WindowsPlatformState>,
 35    raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
 36    // The window handles that are hided by `hide` method.
 37    hidden_windows: RwLock<SmallVec<[HWND; 4]>>,
 38    // The below members will never change throughout the entire lifecycle of the app.
 39    icon: HICON,
 40    main_receiver: flume::Receiver<Runnable>,
 41    dispatch_event: HANDLE,
 42    background_executor: BackgroundExecutor,
 43    foreground_executor: ForegroundExecutor,
 44    text_system: Arc<DirectWriteTextSystem>,
 45    windows_version: WindowsVersion,
 46    bitmap_factory: ManuallyDrop<IWICImagingFactory>,
 47    validation_number: usize,
 48}
 49
 50pub(crate) struct WindowsPlatformState {
 51    callbacks: PlatformCallbacks,
 52    // NOTE: standard cursor handles don't need to close.
 53    pub(crate) current_cursor: HCURSOR,
 54}
 55
 56#[derive(Default)]
 57struct PlatformCallbacks {
 58    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 59    quit: Option<Box<dyn FnMut()>>,
 60    reopen: Option<Box<dyn FnMut()>>,
 61    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 62    will_open_app_menu: Option<Box<dyn FnMut()>>,
 63    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 64}
 65
 66impl WindowsPlatformState {
 67    fn new() -> Self {
 68        let callbacks = PlatformCallbacks::default();
 69        let current_cursor = load_cursor(CursorStyle::Arrow);
 70
 71        Self {
 72            callbacks,
 73            current_cursor,
 74        }
 75    }
 76}
 77
 78impl WindowsPlatform {
 79    pub(crate) fn new() -> Self {
 80        unsafe {
 81            OleInitialize(None).expect("unable to initialize Windows OLE");
 82        }
 83        let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
 84        let dispatch_event = unsafe { CreateEventW(None, false, false, None) }.unwrap();
 85        let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event));
 86        let background_executor = BackgroundExecutor::new(dispatcher.clone());
 87        let foreground_executor = ForegroundExecutor::new(dispatcher);
 88        let bitmap_factory = ManuallyDrop::new(unsafe {
 89            CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
 90                .expect("Error creating bitmap factory.")
 91        });
 92        let text_system = Arc::new(
 93            DirectWriteTextSystem::new(&bitmap_factory)
 94                .expect("Error creating DirectWriteTextSystem"),
 95        );
 96        let icon = load_icon().unwrap_or_default();
 97        let state = RefCell::new(WindowsPlatformState::new());
 98        let raw_window_handles = RwLock::new(SmallVec::new());
 99        let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
100        let validation_number = rand::random::<usize>();
101
102        Self {
103            state,
104            raw_window_handles,
105            hidden_windows: RwLock::new(SmallVec::new()),
106            icon,
107            main_receiver,
108            dispatch_event,
109            background_executor,
110            foreground_executor,
111            text_system,
112            windows_version,
113            bitmap_factory,
114            validation_number,
115        }
116    }
117
118    fn redraw_all(&self) {
119        for handle in self.raw_window_handles.read().iter() {
120            unsafe {
121                RedrawWindow(
122                    *handle,
123                    None,
124                    HRGN::default(),
125                    RDW_INVALIDATE | RDW_UPDATENOW,
126                )
127                .ok()
128                .log_err();
129            }
130        }
131    }
132
133    pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowStatePtr>> {
134        self.raw_window_handles
135            .read()
136            .iter()
137            .find(|entry| *entry == &hwnd)
138            .and_then(|hwnd| try_get_window_inner(*hwnd))
139    }
140
141    #[inline]
142    fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) {
143        self.raw_window_handles
144            .read()
145            .iter()
146            .for_each(|handle| unsafe {
147                PostMessageW(*handle, message, wparam, lparam).log_err();
148            });
149    }
150
151    fn close_one_window(
152        &self,
153        target_window: HWND,
154        validation_number: usize,
155        msg: *const MSG,
156    ) -> bool {
157        if validation_number != self.validation_number {
158            unsafe { DispatchMessageW(msg) };
159            return false;
160        }
161        let mut lock = self.raw_window_handles.write();
162        let index = lock
163            .iter()
164            .position(|handle| *handle == target_window)
165            .unwrap();
166        lock.remove(index);
167
168        lock.is_empty()
169    }
170
171    #[inline]
172    fn run_foreground_tasks(&self) {
173        for runnable in self.main_receiver.drain() {
174            runnable.run();
175        }
176    }
177
178    fn generate_creation_info(&self) -> WindowCreationInfo {
179        WindowCreationInfo {
180            icon: self.icon,
181            executor: self.foreground_executor.clone(),
182            current_cursor: self.state.borrow().current_cursor,
183            windows_version: self.windows_version,
184            validation_number: self.validation_number,
185            main_receiver: self.main_receiver.clone(),
186        }
187    }
188}
189
190impl Platform for WindowsPlatform {
191    fn background_executor(&self) -> BackgroundExecutor {
192        self.background_executor.clone()
193    }
194
195    fn foreground_executor(&self) -> ForegroundExecutor {
196        self.foreground_executor.clone()
197    }
198
199    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
200        self.text_system.clone()
201    }
202
203    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
204        on_finish_launching();
205        let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };
206        begin_vsync(*vsync_event);
207        'a: loop {
208            let wait_result = unsafe {
209                MsgWaitForMultipleObjects(
210                    Some(&[*vsync_event, self.dispatch_event]),
211                    false,
212                    INFINITE,
213                    QS_ALLINPUT,
214                )
215            };
216
217            match wait_result {
218                // compositor clock ticked so we should draw a frame
219                WAIT_EVENT(0) => self.redraw_all(),
220                // foreground tasks are dispatched
221                WAIT_EVENT(1) => self.run_foreground_tasks(),
222                // Windows thread messages are posted
223                WAIT_EVENT(2) => {
224                    let mut msg = MSG::default();
225                    unsafe {
226                        while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
227                            match msg.message {
228                                WM_QUIT => break 'a,
229                                CLOSE_ONE_WINDOW => {
230                                    if self.close_one_window(
231                                        HWND(msg.lParam.0 as _),
232                                        msg.wParam.0,
233                                        &msg,
234                                    ) {
235                                        break 'a;
236                                    }
237                                }
238                                _ => {
239                                    // todo(windows)
240                                    // crate `windows 0.56` reports true as Err
241                                    TranslateMessage(&msg).as_bool();
242                                    DispatchMessageW(&msg);
243                                }
244                            }
245                        }
246                    }
247                    // foreground tasks may have been queued in the message handlers
248                    self.run_foreground_tasks();
249                }
250                _ => {
251                    log::error!("Something went wrong while waiting {:?}", wait_result);
252                    break;
253                }
254            }
255        }
256
257        if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
258            callback();
259        }
260    }
261
262    fn quit(&self) {
263        self.foreground_executor()
264            .spawn(async { unsafe { PostQuitMessage(0) } })
265            .detach();
266    }
267
268    fn restart(&self, _: Option<PathBuf>) {
269        let pid = std::process::id();
270        let Some(app_path) = self.app_path().log_err() else {
271            return;
272        };
273        let script = format!(
274            r#"
275            $pidToWaitFor = {}
276            $exePath = "{}"
277
278            while ($true) {{
279                $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue
280                if (-not $process) {{
281                    Start-Process -FilePath $exePath
282                    break
283                }}
284                Start-Sleep -Seconds 0.1
285            }}
286            "#,
287            pid,
288            app_path.display(),
289        );
290        let restart_process = std::process::Command::new("powershell.exe")
291            .arg("-command")
292            .arg(script)
293            .spawn();
294
295        match restart_process {
296            Ok(_) => self.quit(),
297            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
298        }
299    }
300
301    fn activate(&self, _ignoring_other_apps: bool) {
302        let mut state = self.hidden_windows.write();
303        state.iter().for_each(|handle| unsafe {
304            ShowWindow(*handle, SW_SHOW).ok().log_err();
305        });
306        state.clear();
307    }
308
309    fn hide(&self) {
310        let mut state = self.hidden_windows.write();
311        self.raw_window_handles
312            .read()
313            .iter()
314            .for_each(|handle| unsafe {
315                if IsWindowVisible(*handle).as_bool() {
316                    state.push(*handle);
317                    ShowWindow(*handle, SW_HIDE).ok().log_err();
318                }
319            });
320    }
321
322    // todo(windows)
323    fn hide_other_apps(&self) {
324        unimplemented!()
325    }
326
327    // todo(windows)
328    fn unhide_other_apps(&self) {
329        unimplemented!()
330    }
331
332    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
333        WindowsDisplay::displays()
334    }
335
336    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
337        WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
338    }
339
340    fn active_window(&self) -> Option<AnyWindowHandle> {
341        let active_window_hwnd = unsafe { GetActiveWindow() };
342        self.try_get_windows_inner_from_hwnd(active_window_hwnd)
343            .map(|inner| inner.handle)
344    }
345
346    fn open_window(
347        &self,
348        handle: AnyWindowHandle,
349        options: WindowParams,
350    ) -> Result<Box<dyn PlatformWindow>> {
351        let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
352        let handle = window.get_raw_handle();
353        self.raw_window_handles.write().push(handle);
354
355        Ok(Box::new(window))
356    }
357
358    fn window_appearance(&self) -> WindowAppearance {
359        system_appearance().log_err().unwrap_or_default()
360    }
361
362    fn open_url(&self, url: &str) {
363        let url_string = url.to_string();
364        self.background_executor()
365            .spawn(async move {
366                if url_string.is_empty() {
367                    return;
368                }
369                open_target(url_string.as_str());
370            })
371            .detach();
372    }
373
374    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
375        self.state.borrow_mut().callbacks.open_urls = Some(callback);
376    }
377
378    fn prompt_for_paths(
379        &self,
380        options: PathPromptOptions,
381    ) -> Receiver<Result<Option<Vec<PathBuf>>>> {
382        let (tx, rx) = oneshot::channel();
383        self.foreground_executor()
384            .spawn(async move {
385                let _ = tx.send(file_open_dialog(options));
386            })
387            .detach();
388
389        rx
390    }
391
392    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
393        let directory = directory.to_owned();
394        let (tx, rx) = oneshot::channel();
395        self.foreground_executor()
396            .spawn(async move {
397                let _ = tx.send(file_save_dialog(directory));
398            })
399            .detach();
400
401        rx
402    }
403
404    fn reveal_path(&self, path: &Path) {
405        let Ok(file_full_path) = path.canonicalize() else {
406            log::error!("unable to parse file path");
407            return;
408        };
409        self.background_executor()
410            .spawn(async move {
411                let Some(path) = file_full_path.to_str() else {
412                    return;
413                };
414                if path.is_empty() {
415                    return;
416                }
417                open_target_in_explorer(path);
418            })
419            .detach();
420    }
421
422    fn open_with_system(&self, path: &Path) {
423        let Ok(full_path) = path.canonicalize() else {
424            log::error!("unable to parse file full path: {}", path.display());
425            return;
426        };
427        self.background_executor()
428            .spawn(async move {
429                let Some(full_path_str) = full_path.to_str() else {
430                    return;
431                };
432                if full_path_str.is_empty() {
433                    return;
434                };
435                open_target(full_path_str);
436            })
437            .detach();
438    }
439
440    fn on_quit(&self, callback: Box<dyn FnMut()>) {
441        self.state.borrow_mut().callbacks.quit = Some(callback);
442    }
443
444    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
445        self.state.borrow_mut().callbacks.reopen = Some(callback);
446    }
447
448    // todo(windows)
449    fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
450    fn set_dock_menu(&self, _menus: Vec<MenuItem>, _keymap: &Keymap) {}
451
452    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
453        self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
454    }
455
456    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
457        self.state.borrow_mut().callbacks.will_open_app_menu = Some(callback);
458    }
459
460    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
461        self.state.borrow_mut().callbacks.validate_app_menu_command = Some(callback);
462    }
463
464    fn app_path(&self) -> Result<PathBuf> {
465        Ok(std::env::current_exe()?)
466    }
467
468    // todo(windows)
469    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
470        Err(anyhow!("not yet implemented"))
471    }
472
473    fn set_cursor_style(&self, style: CursorStyle) {
474        let hcursor = load_cursor(style);
475        let mut lock = self.state.borrow_mut();
476        if lock.current_cursor.0 != hcursor.0 {
477            self.post_message(CURSOR_STYLE_CHANGED, WPARAM(0), LPARAM(hcursor.0 as isize));
478            lock.current_cursor = hcursor;
479        }
480    }
481
482    fn should_auto_hide_scrollbars(&self) -> bool {
483        should_auto_hide_scrollbars().log_err().unwrap_or(false)
484    }
485
486    fn write_to_clipboard(&self, item: ClipboardItem) {
487        write_to_clipboard(item);
488    }
489
490    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
491        read_from_clipboard()
492    }
493
494    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
495        let mut password = password.to_vec();
496        let mut username = username.encode_utf16().chain(Some(0)).collect_vec();
497        let mut target_name = windows_credentials_target_name(url)
498            .encode_utf16()
499            .chain(Some(0))
500            .collect_vec();
501        self.foreground_executor().spawn(async move {
502            let credentials = CREDENTIALW {
503                LastWritten: unsafe { GetSystemTimeAsFileTime() },
504                Flags: CRED_FLAGS(0),
505                Type: CRED_TYPE_GENERIC,
506                TargetName: PWSTR::from_raw(target_name.as_mut_ptr()),
507                CredentialBlobSize: password.len() as u32,
508                CredentialBlob: password.as_ptr() as *mut _,
509                Persist: CRED_PERSIST_LOCAL_MACHINE,
510                UserName: PWSTR::from_raw(username.as_mut_ptr()),
511                ..CREDENTIALW::default()
512            };
513            unsafe { CredWriteW(&credentials, 0) }?;
514            Ok(())
515        })
516    }
517
518    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
519        let mut target_name = windows_credentials_target_name(url)
520            .encode_utf16()
521            .chain(Some(0))
522            .collect_vec();
523        self.foreground_executor().spawn(async move {
524            let mut credentials: *mut CREDENTIALW = std::ptr::null_mut();
525            unsafe {
526                CredReadW(
527                    PCWSTR::from_raw(target_name.as_ptr()),
528                    CRED_TYPE_GENERIC,
529                    0,
530                    &mut credentials,
531                )?
532            };
533
534            if credentials.is_null() {
535                Ok(None)
536            } else {
537                let username: String = unsafe { (*credentials).UserName.to_string()? };
538                let credential_blob = unsafe {
539                    std::slice::from_raw_parts(
540                        (*credentials).CredentialBlob,
541                        (*credentials).CredentialBlobSize as usize,
542                    )
543                };
544                let password = credential_blob.to_vec();
545                unsafe { CredFree(credentials as *const _ as _) };
546                Ok(Some((username, password)))
547            }
548        })
549    }
550
551    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
552        let mut target_name = windows_credentials_target_name(url)
553            .encode_utf16()
554            .chain(Some(0))
555            .collect_vec();
556        self.foreground_executor().spawn(async move {
557            unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };
558            Ok(())
559        })
560    }
561
562    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
563        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
564    }
565}
566
567impl Drop for WindowsPlatform {
568    fn drop(&mut self) {
569        unsafe {
570            ManuallyDrop::drop(&mut self.bitmap_factory);
571            OleUninitialize();
572        }
573    }
574}
575
576pub(crate) struct WindowCreationInfo {
577    pub(crate) icon: HICON,
578    pub(crate) executor: ForegroundExecutor,
579    pub(crate) current_cursor: HCURSOR,
580    pub(crate) windows_version: WindowsVersion,
581    pub(crate) validation_number: usize,
582    pub(crate) main_receiver: flume::Receiver<Runnable>,
583}
584
585fn open_target(target: &str) {
586    unsafe {
587        let ret = ShellExecuteW(
588            None,
589            windows::core::w!("open"),
590            &HSTRING::from(target),
591            None,
592            None,
593            SW_SHOWDEFAULT,
594        );
595        if ret.0 as isize <= 32 {
596            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
597        }
598    }
599}
600
601fn open_target_in_explorer(target: &str) {
602    unsafe {
603        let ret = ShellExecuteW(
604            None,
605            windows::core::w!("open"),
606            windows::core::w!("explorer.exe"),
607            &HSTRING::from(format!("/select,{}", target).as_str()),
608            None,
609            SW_SHOWDEFAULT,
610        );
611        if ret.0 as isize <= 32 {
612            log::error!(
613                "Unable to open target in explorer: {}",
614                std::io::Error::last_os_error()
615            );
616        }
617    }
618}
619
620fn file_open_dialog(options: PathPromptOptions) -> Result<Option<Vec<PathBuf>>> {
621    let folder_dialog: IFileOpenDialog =
622        unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_ALL)? };
623
624    let mut dialog_options = FOS_FILEMUSTEXIST;
625    if options.multiple {
626        dialog_options |= FOS_ALLOWMULTISELECT;
627    }
628    if options.directories {
629        dialog_options |= FOS_PICKFOLDERS;
630    }
631
632    unsafe {
633        folder_dialog.SetOptions(dialog_options)?;
634        if folder_dialog.Show(None).is_err() {
635            // User cancelled
636            return Ok(None);
637        }
638    }
639
640    let results = unsafe { folder_dialog.GetResults()? };
641    let file_count = unsafe { results.GetCount()? };
642    if file_count == 0 {
643        return Ok(None);
644    }
645
646    let mut paths = Vec::new();
647    for i in 0..file_count {
648        let item = unsafe { results.GetItemAt(i)? };
649        let path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? };
650        paths.push(PathBuf::from(path));
651    }
652
653    Ok(Some(paths))
654}
655
656fn file_save_dialog(directory: PathBuf) -> Result<Option<PathBuf>> {
657    let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
658    if !directory.to_string_lossy().is_empty() {
659        if let Some(full_path) = directory.canonicalize().log_err() {
660            let full_path = full_path.to_string_lossy();
661            let full_path_str = full_path.trim_start_matches("\\\\?\\");
662            if !full_path_str.is_empty() {
663                let path_item: IShellItem =
664                    unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_str), None)? };
665                unsafe { dialog.SetFolder(&path_item).log_err() };
666            }
667        }
668    }
669    unsafe {
670        dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC {
671            pszName: windows::core::w!("All files"),
672            pszSpec: windows::core::w!("*.*"),
673        }])?;
674        if dialog.Show(None).is_err() {
675            // User cancelled
676            return Ok(None);
677        }
678    }
679    let shell_item = unsafe { dialog.GetResult()? };
680    let file_path_string = unsafe {
681        let pwstr = shell_item.GetDisplayName(SIGDN_FILESYSPATH)?;
682        let string = pwstr.to_string()?;
683        CoTaskMemFree(Some(pwstr.0 as _));
684        string
685    };
686    Ok(Some(PathBuf::from(file_path_string)))
687}
688
689fn begin_vsync(vsync_event: HANDLE) {
690    let event: SafeHandle = vsync_event.into();
691    std::thread::spawn(move || unsafe {
692        loop {
693            windows::Win32::Graphics::Dwm::DwmFlush().log_err();
694            SetEvent(*event).log_err();
695        }
696    });
697}
698
699fn load_icon() -> Result<HICON> {
700    let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
701    let handle = unsafe {
702        LoadImageW(
703            module,
704            IDI_APPLICATION,
705            IMAGE_ICON,
706            0,
707            0,
708            LR_DEFAULTSIZE | LR_SHARED,
709        )
710        .context("unable to load icon file")?
711    };
712    Ok(HICON(handle.0))
713}
714
715#[inline]
716fn should_auto_hide_scrollbars() -> Result<bool> {
717    let ui_settings = UISettings::new()?;
718    Ok(ui_settings.AutoHideScrollBars()?)
719}
720
721#[cfg(test)]
722mod tests {
723    use crate::{ClipboardItem, Platform, WindowsPlatform};
724
725    #[test]
726    fn test_clipboard() {
727        let platform = WindowsPlatform::new();
728        let item = ClipboardItem::new_string("你好".to_string());
729        platform.write_to_clipboard(item.clone());
730        assert_eq!(platform.read_from_clipboard(), Some(item));
731
732        let item = ClipboardItem::new_string("12345".to_string());
733        platform.write_to_clipboard(item.clone());
734        assert_eq!(platform.read_from_clipboard(), Some(item));
735
736        let item = ClipboardItem::new_string_with_json_metadata("abcdef".to_string(), vec![3, 4]);
737        platform.write_to_clipboard(item.clone());
738        assert_eq!(platform.read_from_clipboard(), Some(item));
739    }
740}