platform.rs

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