platform.rs

  1// todo(windows): remove
  2#![allow(unused_variables)]
  3
  4use std::{
  5    cell::{Cell, RefCell},
  6    ffi::{c_uint, c_void, OsString},
  7    iter::once,
  8    mem::transmute,
  9    os::windows::ffi::{OsStrExt, OsStringExt},
 10    path::{Path, PathBuf},
 11    rc::Rc,
 12    sync::{Arc, OnceLock},
 13};
 14
 15use ::util::ResultExt;
 16use anyhow::{anyhow, Context, Result};
 17use async_task::Runnable;
 18use copypasta::{ClipboardContext, ClipboardProvider};
 19use futures::channel::oneshot::{self, Receiver};
 20use itertools::Itertools;
 21use parking_lot::{Mutex, RwLock};
 22use semantic_version::SemanticVersion;
 23use smallvec::SmallVec;
 24use time::UtcOffset;
 25use windows::{
 26    core::*,
 27    Wdk::System::SystemServices::*,
 28    Win32::{
 29        Foundation::*,
 30        Graphics::Gdi::*,
 31        Media::*,
 32        Security::Credentials::*,
 33        Storage::FileSystem::*,
 34        System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
 35        UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
 36    },
 37};
 38
 39use crate::*;
 40
 41pub(crate) struct WindowsPlatform {
 42    inner: Rc<WindowsPlatformInner>,
 43}
 44
 45/// Windows settings pulled from SystemParametersInfo
 46/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
 47#[derive(Default, Debug)]
 48pub(crate) struct WindowsPlatformSystemSettings {
 49    /// SEE: SPI_GETWHEELSCROLLCHARS
 50    pub(crate) wheel_scroll_chars: u32,
 51
 52    /// SEE: SPI_GETWHEELSCROLLLINES
 53    pub(crate) wheel_scroll_lines: u32,
 54}
 55
 56pub(crate) struct WindowsPlatformInner {
 57    background_executor: BackgroundExecutor,
 58    pub(crate) foreground_executor: ForegroundExecutor,
 59    main_receiver: flume::Receiver<Runnable>,
 60    text_system: Arc<CosmicTextSystem>,
 61    callbacks: Mutex<Callbacks>,
 62    pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
 63    pub(crate) dispatch_event: OwnedHandle,
 64    pub(crate) settings: RefCell<WindowsPlatformSystemSettings>,
 65    pub icon: HICON,
 66    // NOTE: standard cursor handles don't need to close.
 67    pub(crate) current_cursor: Cell<HCURSOR>,
 68}
 69
 70impl WindowsPlatformInner {
 71    pub(crate) fn try_get_windows_inner_from_hwnd(
 72        &self,
 73        hwnd: HWND,
 74    ) -> Option<Rc<WindowsWindowInner>> {
 75        self.raw_window_handles
 76            .read()
 77            .iter()
 78            .find(|entry| *entry == &hwnd)
 79            .and_then(|hwnd| try_get_window_inner(*hwnd))
 80    }
 81}
 82
 83#[derive(Default)]
 84struct Callbacks {
 85    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 86    become_active: Option<Box<dyn FnMut()>>,
 87    resign_active: Option<Box<dyn FnMut()>>,
 88    quit: Option<Box<dyn FnMut()>>,
 89    reopen: Option<Box<dyn FnMut()>>,
 90    event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
 91    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 92    will_open_app_menu: Option<Box<dyn FnMut()>>,
 93    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 94}
 95
 96enum WindowsMessageWaitResult {
 97    ForegroundExecution,
 98    WindowsMessage(MSG),
 99    Error,
100}
101
102impl WindowsPlatformSystemSettings {
103    fn new() -> Self {
104        let mut settings = Self::default();
105        settings.update_all();
106        settings
107    }
108
109    pub(crate) fn update_all(&mut self) {
110        self.update_wheel_scroll_lines();
111        self.update_wheel_scroll_chars();
112    }
113
114    pub(crate) fn update_wheel_scroll_lines(&mut self) {
115        let mut value = c_uint::default();
116        let result = unsafe {
117            SystemParametersInfoW(
118                SPI_GETWHEELSCROLLLINES,
119                0,
120                Some((&mut value) as *mut c_uint as *mut c_void),
121                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
122            )
123        };
124
125        if result.log_err() != None {
126            self.wheel_scroll_lines = value;
127        }
128    }
129
130    pub(crate) fn update_wheel_scroll_chars(&mut self) {
131        let mut value = c_uint::default();
132        let result = unsafe {
133            SystemParametersInfoW(
134                SPI_GETWHEELSCROLLCHARS,
135                0,
136                Some((&mut value) as *mut c_uint as *mut c_void),
137                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
138            )
139        };
140
141        if result.log_err() != None {
142            self.wheel_scroll_chars = value;
143        }
144    }
145}
146
147impl WindowsPlatform {
148    pub(crate) fn new() -> Self {
149        unsafe {
150            OleInitialize(None).expect("unable to initialize Windows OLE");
151        }
152        let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
153        let dispatch_event =
154            OwnedHandle::new(unsafe { CreateEventW(None, false, false, None) }.unwrap());
155        let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event.to_raw()));
156        let background_executor = BackgroundExecutor::new(dispatcher.clone());
157        let foreground_executor = ForegroundExecutor::new(dispatcher);
158        let text_system = Arc::new(CosmicTextSystem::new());
159        let callbacks = Mutex::new(Callbacks::default());
160        let raw_window_handles = RwLock::new(SmallVec::new());
161        let settings = RefCell::new(WindowsPlatformSystemSettings::new());
162        let icon = load_icon().unwrap_or_default();
163        let current_cursor = Cell::new(load_cursor(CursorStyle::Arrow));
164        let inner = Rc::new(WindowsPlatformInner {
165            background_executor,
166            foreground_executor,
167            main_receiver,
168            text_system,
169            callbacks,
170            raw_window_handles,
171            dispatch_event,
172            settings,
173            icon,
174            current_cursor,
175        });
176        Self { inner }
177    }
178
179    fn run_foreground_tasks(&self) {
180        for runnable in self.inner.main_receiver.drain() {
181            runnable.run();
182        }
183    }
184
185    fn redraw_all(&self) {
186        for handle in self.inner.raw_window_handles.read().iter() {
187            unsafe {
188                RedrawWindow(
189                    *handle,
190                    None,
191                    HRGN::default(),
192                    RDW_INVALIDATE | RDW_UPDATENOW,
193                );
194            }
195        }
196    }
197}
198
199impl Platform for WindowsPlatform {
200    fn background_executor(&self) -> BackgroundExecutor {
201        self.inner.background_executor.clone()
202    }
203
204    fn foreground_executor(&self) -> ForegroundExecutor {
205        self.inner.foreground_executor.clone()
206    }
207
208    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
209        self.inner.text_system.clone()
210    }
211
212    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
213        on_finish_launching();
214        let dispatch_event = self.inner.dispatch_event.to_raw();
215        let vsync_event = create_event().unwrap();
216        let timer_stop_event = create_event().unwrap();
217        let raw_timer_stop_event = timer_stop_event.to_raw();
218        begin_vsync_timer(vsync_event.to_raw(), timer_stop_event);
219        'a: loop {
220            let wait_result = unsafe {
221                MsgWaitForMultipleObjects(
222                    Some(&[vsync_event.to_raw(), dispatch_event]),
223                    false,
224                    INFINITE,
225                    QS_ALLINPUT,
226                )
227            };
228
229            match wait_result {
230                // compositor clock ticked so we should draw a frame
231                WAIT_EVENT(0) => {
232                    self.redraw_all();
233                }
234                // foreground tasks are dispatched
235                WAIT_EVENT(1) => {
236                    self.run_foreground_tasks();
237                }
238                // Windows thread messages are posted
239                WAIT_EVENT(2) => {
240                    let mut msg = MSG::default();
241                    unsafe {
242                        while PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE).as_bool() {
243                            if msg.message == WM_QUIT {
244                                break 'a;
245                            }
246                            if msg.message == WM_SETTINGCHANGE {
247                                self.inner.settings.borrow_mut().update_all();
248                                continue;
249                            }
250                            TranslateMessage(&msg);
251                            DispatchMessageW(&msg);
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        end_vsync_timer(raw_timer_stop_event);
265
266        let mut callbacks = self.inner.callbacks.lock();
267        if let Some(callback) = callbacks.quit.as_mut() {
268            callback()
269        }
270    }
271
272    fn quit(&self) {
273        self.foreground_executor()
274            .spawn(async { unsafe { PostQuitMessage(0) } })
275            .detach();
276    }
277
278    fn restart(&self) {
279        let pid = std::process::id();
280        let Some(app_path) = self.app_path().log_err() else {
281            return;
282        };
283        let script = format!(
284            r#"
285            $pidToWaitFor = {}
286            $exePath = "{}"
287
288            while ($true) {{
289                $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue
290                if (-not $process) {{
291                    Start-Process -FilePath $exePath
292                    break
293                }}
294                Start-Sleep -Seconds 0.1
295            }}
296            "#,
297            pid,
298            app_path.display(),
299        );
300        let restart_process = std::process::Command::new("powershell.exe")
301            .arg("-command")
302            .arg(script)
303            .spawn();
304
305        match restart_process {
306            Ok(_) => self.quit(),
307            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
308        }
309    }
310
311    // todo(windows)
312    fn activate(&self, ignoring_other_apps: bool) {}
313
314    // todo(windows)
315    fn hide(&self) {
316        unimplemented!()
317    }
318
319    // todo(windows)
320    fn hide_other_apps(&self) {
321        unimplemented!()
322    }
323
324    // todo(windows)
325    fn unhide_other_apps(&self) {
326        unimplemented!()
327    }
328
329    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
330        WindowsDisplay::displays()
331    }
332
333    fn display(&self, id: crate::DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
334        if let Some(display) = WindowsDisplay::new(id) {
335            Some(Rc::new(display) as Rc<dyn PlatformDisplay>)
336        } else {
337            None
338        }
339    }
340
341    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
342        if let Some(display) = WindowsDisplay::primary_monitor() {
343            Some(Rc::new(display) as Rc<dyn PlatformDisplay>)
344        } else {
345            None
346        }
347    }
348
349    fn active_window(&self) -> Option<AnyWindowHandle> {
350        let active_window_hwnd = unsafe { GetActiveWindow() };
351        self.inner
352            .try_get_windows_inner_from_hwnd(active_window_hwnd)
353            .map(|inner| inner.handle)
354    }
355
356    fn open_window(
357        &self,
358        handle: AnyWindowHandle,
359        options: WindowParams,
360    ) -> Box<dyn PlatformWindow> {
361        Box::new(WindowsWindow::new(self.inner.clone(), handle, options))
362    }
363
364    // todo(windows)
365    fn window_appearance(&self) -> WindowAppearance {
366        WindowAppearance::Dark
367    }
368
369    fn open_url(&self, url: &str) {
370        let url_string = url.to_string();
371        self.background_executor()
372            .spawn(async move {
373                if url_string.is_empty() {
374                    return;
375                }
376                open_target(url_string.as_str());
377            })
378            .detach();
379    }
380
381    // todo(windows)
382    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
383        self.inner.callbacks.lock().open_urls = Some(callback);
384    }
385
386    fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
387        let (tx, rx) = oneshot::channel();
388
389        self.foreground_executor()
390            .spawn(async move {
391                let tx = Cell::new(Some(tx));
392
393                // create file open dialog
394                let folder_dialog: IFileOpenDialog = unsafe {
395                    CoCreateInstance::<std::option::Option<&IUnknown>, IFileOpenDialog>(
396                        &FileOpenDialog,
397                        None,
398                        CLSCTX_ALL,
399                    )
400                    .unwrap()
401                };
402
403                // dialog options
404                let mut dialog_options: FILEOPENDIALOGOPTIONS = FOS_FILEMUSTEXIST;
405                if options.multiple {
406                    dialog_options |= FOS_ALLOWMULTISELECT;
407                }
408                if options.directories {
409                    dialog_options |= FOS_PICKFOLDERS;
410                }
411
412                unsafe {
413                    folder_dialog.SetOptions(dialog_options).unwrap();
414                    folder_dialog
415                        .SetTitle(&HSTRING::from(OsString::from("Select a folder")))
416                        .unwrap();
417                }
418
419                let hr = unsafe { folder_dialog.Show(None) };
420
421                if hr.is_err() {
422                    if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
423                        // user canceled error
424                        if let Some(tx) = tx.take() {
425                            tx.send(None).unwrap();
426                        }
427                        return;
428                    }
429                }
430
431                let mut results = unsafe { folder_dialog.GetResults().unwrap() };
432
433                let mut paths: Vec<PathBuf> = Vec::new();
434                for i in 0..unsafe { results.GetCount().unwrap() } {
435                    let mut item: IShellItem = unsafe { results.GetItemAt(i).unwrap() };
436                    let mut path: PWSTR =
437                        unsafe { item.GetDisplayName(SIGDN_FILESYSPATH).unwrap() };
438                    let mut path_os_string = OsString::from_wide(unsafe { path.as_wide() });
439
440                    paths.push(PathBuf::from(path_os_string));
441                }
442
443                if let Some(tx) = tx.take() {
444                    if paths.len() == 0 {
445                        tx.send(None).unwrap();
446                    } else {
447                        tx.send(Some(paths)).unwrap();
448                    }
449                }
450            })
451            .detach();
452
453        rx
454    }
455
456    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
457        let directory = directory.to_owned();
458        let (tx, rx) = oneshot::channel();
459        self.foreground_executor()
460            .spawn(async move {
461                unsafe {
462                    let Ok(dialog) = show_savefile_dialog(directory) else {
463                        let _ = tx.send(None);
464                        return;
465                    };
466                    let Ok(_) = dialog.Show(None) else {
467                        let _ = tx.send(None); // user cancel
468                        return;
469                    };
470                    if let Ok(shell_item) = dialog.GetResult() {
471                        if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
472                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
473                            return;
474                        }
475                    }
476                    let _ = tx.send(None);
477                }
478            })
479            .detach();
480
481        rx
482    }
483
484    fn reveal_path(&self, path: &Path) {
485        let Ok(file_full_path) = path.canonicalize() else {
486            log::error!("unable to parse file path");
487            return;
488        };
489        self.background_executor()
490            .spawn(async move {
491                let Some(path) = file_full_path.to_str() else {
492                    return;
493                };
494                if path.is_empty() {
495                    return;
496                }
497                open_target(path);
498            })
499            .detach();
500    }
501
502    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
503        self.inner.callbacks.lock().become_active = Some(callback);
504    }
505
506    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
507        self.inner.callbacks.lock().resign_active = Some(callback);
508    }
509
510    fn on_quit(&self, callback: Box<dyn FnMut()>) {
511        self.inner.callbacks.lock().quit = Some(callback);
512    }
513
514    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
515        self.inner.callbacks.lock().reopen = Some(callback);
516    }
517
518    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
519        self.inner.callbacks.lock().event = Some(callback);
520    }
521
522    // todo(windows)
523    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
524
525    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
526        self.inner.callbacks.lock().app_menu_action = Some(callback);
527    }
528
529    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
530        self.inner.callbacks.lock().will_open_app_menu = Some(callback);
531    }
532
533    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
534        self.inner.callbacks.lock().validate_app_menu_command = Some(callback);
535    }
536
537    fn os_name(&self) -> &'static str {
538        "Windows"
539    }
540
541    fn os_version(&self) -> Result<SemanticVersion> {
542        let mut info = unsafe { std::mem::zeroed() };
543        let status = unsafe { RtlGetVersion(&mut info) };
544        if status.is_ok() {
545            Ok(SemanticVersion::new(
546                info.dwMajorVersion as _,
547                info.dwMinorVersion as _,
548                info.dwBuildNumber as _,
549            ))
550        } else {
551            Err(anyhow::anyhow!(
552                "unable to get Windows version: {}",
553                std::io::Error::last_os_error()
554            ))
555        }
556    }
557
558    fn app_version(&self) -> Result<SemanticVersion> {
559        let mut file_name_buffer = vec![0u16; MAX_PATH as usize];
560        let file_name = {
561            let mut file_name_buffer_capacity = MAX_PATH as usize;
562            let mut file_name_length;
563            loop {
564                file_name_length =
565                    unsafe { GetModuleFileNameW(None, &mut file_name_buffer) } as usize;
566                if file_name_length < file_name_buffer_capacity {
567                    break;
568                }
569                // buffer too small
570                file_name_buffer_capacity *= 2;
571                file_name_buffer = vec![0u16; file_name_buffer_capacity];
572            }
573            PCWSTR::from_raw(file_name_buffer[0..(file_name_length + 1)].as_ptr())
574        };
575
576        let version_info_block = {
577            let mut version_handle = 0;
578            let version_info_size =
579                unsafe { GetFileVersionInfoSizeW(file_name, Some(&mut version_handle)) } as usize;
580            if version_info_size == 0 {
581                log::error!(
582                    "unable to get version info size: {}",
583                    std::io::Error::last_os_error()
584                );
585                return Err(anyhow!("unable to get version info size"));
586            }
587            let mut version_data = vec![0u8; version_info_size + 2];
588            unsafe {
589                GetFileVersionInfoW(
590                    file_name,
591                    version_handle,
592                    version_info_size as u32,
593                    version_data.as_mut_ptr() as _,
594                )
595            }
596            .inspect_err(|_| {
597                log::error!(
598                    "unable to retrieve version info: {}",
599                    std::io::Error::last_os_error()
600                )
601            })?;
602            version_data
603        };
604
605        let version_info_raw = {
606            let mut buffer = unsafe { std::mem::zeroed() };
607            let mut size = 0;
608            let entry = "\\".encode_utf16().chain(Some(0)).collect_vec();
609            if !unsafe {
610                VerQueryValueW(
611                    version_info_block.as_ptr() as _,
612                    PCWSTR::from_raw(entry.as_ptr()),
613                    &mut buffer,
614                    &mut size,
615                )
616            }
617            .as_bool()
618            {
619                log::error!(
620                    "unable to query version info data: {}",
621                    std::io::Error::last_os_error()
622                );
623                return Err(anyhow!("the specified resource is not valid"));
624            }
625            if size == 0 {
626                log::error!(
627                    "unable to query version info data: {}",
628                    std::io::Error::last_os_error()
629                );
630                return Err(anyhow!("no value is available for the specified name"));
631            }
632            buffer
633        };
634
635        let version_info = unsafe { &*(version_info_raw as *mut VS_FIXEDFILEINFO) };
636        // https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
637        if version_info.dwSignature == 0xFEEF04BD {
638            return Ok(SemanticVersion::new(
639                ((version_info.dwProductVersionMS >> 16) & 0xFFFF) as usize,
640                (version_info.dwProductVersionMS & 0xFFFF) as usize,
641                ((version_info.dwProductVersionLS >> 16) & 0xFFFF) as usize,
642            ));
643        } else {
644            log::error!(
645                "no version info present: {}",
646                std::io::Error::last_os_error()
647            );
648            return Err(anyhow!("no version info present"));
649        }
650    }
651
652    fn app_path(&self) -> Result<PathBuf> {
653        Ok(std::env::current_exe()?)
654    }
655
656    fn local_timezone(&self) -> UtcOffset {
657        let mut info = unsafe { std::mem::zeroed() };
658        let ret = unsafe { GetTimeZoneInformation(&mut info) };
659        if ret == TIME_ZONE_ID_INVALID {
660            log::error!(
661                "Unable to get local timezone: {}",
662                std::io::Error::last_os_error()
663            );
664            return UtcOffset::UTC;
665        }
666        // Windows treat offset as:
667        // UTC = localtime + offset
668        // so we add a minus here
669        let hours = -info.Bias / 60;
670        let minutes = -info.Bias % 60;
671
672        UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap()
673    }
674
675    // todo(windows)
676    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
677        Err(anyhow!("not yet implemented"))
678    }
679
680    fn set_cursor_style(&self, style: CursorStyle) {
681        self.inner.current_cursor.set(load_cursor(style));
682    }
683
684    // todo(windows)
685    fn should_auto_hide_scrollbars(&self) -> bool {
686        false
687    }
688
689    fn write_to_clipboard(&self, item: ClipboardItem) {
690        if item.text.len() > 0 {
691            let mut ctx = ClipboardContext::new().unwrap();
692            ctx.set_contents(item.text().to_owned()).unwrap();
693        }
694    }
695
696    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
697        let mut ctx = ClipboardContext::new().unwrap();
698        let content = ctx.get_contents().unwrap();
699        Some(ClipboardItem {
700            text: content,
701            metadata: None,
702        })
703    }
704
705    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
706        let mut password = password.to_vec();
707        let mut username = username.encode_utf16().chain(once(0)).collect_vec();
708        let mut target_name = windows_credentials_target_name(url)
709            .encode_utf16()
710            .chain(once(0))
711            .collect_vec();
712        self.foreground_executor().spawn(async move {
713            let credentials = CREDENTIALW {
714                LastWritten: unsafe { GetSystemTimeAsFileTime() },
715                Flags: CRED_FLAGS(0),
716                Type: CRED_TYPE_GENERIC,
717                TargetName: PWSTR::from_raw(target_name.as_mut_ptr()),
718                CredentialBlobSize: password.len() as u32,
719                CredentialBlob: password.as_ptr() as *mut _,
720                Persist: CRED_PERSIST_LOCAL_MACHINE,
721                UserName: PWSTR::from_raw(username.as_mut_ptr()),
722                ..CREDENTIALW::default()
723            };
724            unsafe { CredWriteW(&credentials, 0) }?;
725            Ok(())
726        })
727    }
728
729    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
730        let mut target_name = windows_credentials_target_name(url)
731            .encode_utf16()
732            .chain(once(0))
733            .collect_vec();
734        self.foreground_executor().spawn(async move {
735            let mut credentials: *mut CREDENTIALW = std::ptr::null_mut();
736            unsafe {
737                CredReadW(
738                    PCWSTR::from_raw(target_name.as_ptr()),
739                    CRED_TYPE_GENERIC,
740                    0,
741                    &mut credentials,
742                )?
743            };
744
745            if credentials.is_null() {
746                Ok(None)
747            } else {
748                let username: String = unsafe { (*credentials).UserName.to_string()? };
749                let credential_blob = unsafe {
750                    std::slice::from_raw_parts(
751                        (*credentials).CredentialBlob,
752                        (*credentials).CredentialBlobSize as usize,
753                    )
754                };
755                let password = credential_blob.to_vec();
756                unsafe { CredFree(credentials as *const c_void) };
757                Ok(Some((username, password)))
758            }
759        })
760    }
761
762    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
763        let mut target_name = windows_credentials_target_name(url)
764            .encode_utf16()
765            .chain(once(0))
766            .collect_vec();
767        self.foreground_executor().spawn(async move {
768            unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };
769            Ok(())
770        })
771    }
772
773    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
774        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
775    }
776}
777
778impl Drop for WindowsPlatform {
779    fn drop(&mut self) {
780        unsafe {
781            OleUninitialize();
782        }
783    }
784}
785
786fn open_target(target: &str) {
787    unsafe {
788        let ret = ShellExecuteW(
789            None,
790            windows::core::w!("open"),
791            &HSTRING::from(target),
792            None,
793            None,
794            SW_SHOWDEFAULT,
795        );
796        if ret.0 <= 32 {
797            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
798        }
799    }
800}
801
802unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
803    let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?;
804    let bind_context = CreateBindCtx(0)?;
805    let Ok(full_path) = directory.canonicalize() else {
806        return Ok(dialog);
807    };
808    let dir_str = full_path.into_os_string();
809    if dir_str.is_empty() {
810        return Ok(dialog);
811    }
812    let dir_vec = dir_str.encode_wide().collect_vec();
813    let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context)
814        .inspect_err(|e| log::error!("unable to create IShellItem: {}", e));
815    if ret.is_ok() {
816        let dir_shell_item: IShellItem = ret.unwrap();
817        let _ = dialog
818            .SetFolder(&dir_shell_item)
819            .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e));
820    }
821
822    Ok(dialog)
823}
824
825fn begin_vsync_timer(vsync_event: HANDLE, timer_stop_event: OwnedHandle) {
826    let vsync_fn = select_vsync_fn();
827    std::thread::spawn(move || {
828        while vsync_fn(timer_stop_event.to_raw()) {
829            if unsafe { SetEvent(vsync_event) }.log_err().is_none() {
830                break;
831            }
832        }
833    });
834}
835
836fn end_vsync_timer(timer_stop_event: HANDLE) {
837    unsafe { SetEvent(timer_stop_event) }.log_err();
838}
839
840fn select_vsync_fn() -> Box<dyn Fn(HANDLE) -> bool + Send> {
841    if let Some(dcomp_fn) = load_dcomp_vsync_fn() {
842        log::info!("use DCompositionWaitForCompositorClock for vsync");
843        return Box::new(move |timer_stop_event| {
844            // will be 0 if woken up by timer_stop_event or 1 if the compositor clock ticked
845            // SEE: https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock
846            (unsafe { dcomp_fn(1, &timer_stop_event, INFINITE) }) == 1
847        });
848    }
849    log::info!("use fallback vsync function");
850    Box::new(fallback_vsync_fn())
851}
852
853fn load_dcomp_vsync_fn() -> Option<unsafe extern "system" fn(u32, *const HANDLE, u32) -> u32> {
854    static FN: OnceLock<Option<unsafe extern "system" fn(u32, *const HANDLE, u32) -> u32>> =
855        OnceLock::new();
856    *FN.get_or_init(|| {
857        let hmodule = unsafe { LoadLibraryW(windows::core::w!("dcomp.dll")) }.ok()?;
858        let address = unsafe {
859            GetProcAddress(
860                hmodule,
861                windows::core::s!("DCompositionWaitForCompositorClock"),
862            )
863        }?;
864        Some(unsafe { transmute(address) })
865    })
866}
867
868fn fallback_vsync_fn() -> impl Fn(HANDLE) -> bool + Send {
869    let freq = WindowsDisplay::primary_monitor()
870        .and_then(|monitor| monitor.frequency())
871        .unwrap_or(60);
872    log::info!("primaly refresh rate is {freq}Hz");
873
874    let interval = (1000 / freq).max(1);
875    log::info!("expected interval is {interval}ms");
876
877    unsafe { timeBeginPeriod(1) };
878
879    struct TimePeriod;
880    impl Drop for TimePeriod {
881        fn drop(&mut self) {
882            unsafe { timeEndPeriod(1) };
883        }
884    }
885    let period = TimePeriod;
886
887    move |timer_stop_event| {
888        let _ = (&period,);
889        (unsafe { WaitForSingleObject(timer_stop_event, interval) }) == WAIT_TIMEOUT
890    }
891}
892
893fn load_icon() -> Result<HICON> {
894    let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
895    let handle = unsafe {
896        LoadImageW(
897            module,
898            IDI_APPLICATION,
899            IMAGE_ICON,
900            0,
901            0,
902            LR_DEFAULTSIZE | LR_SHARED,
903        )
904        .context("unable to load icon file")?
905    };
906    Ok(HICON(handle.0))
907}