platform.rs

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