platform.rs

  1// todo(windows): remove
  2#![allow(unused_variables)]
  3
  4use std::{
  5    cell::{Cell, RefCell},
  6    ffi::{c_void, OsString},
  7    os::windows::ffi::{OsStrExt, OsStringExt},
  8    path::{Path, PathBuf},
  9    rc::Rc,
 10    sync::Arc,
 11};
 12
 13use ::util::ResultExt;
 14use anyhow::{anyhow, Context, Result};
 15use copypasta::{ClipboardContext, ClipboardProvider};
 16use futures::channel::oneshot::{self, Receiver};
 17use itertools::Itertools;
 18use parking_lot::RwLock;
 19use smallvec::SmallVec;
 20use time::UtcOffset;
 21use windows::{
 22    core::*,
 23    Win32::{
 24        Foundation::*,
 25        Graphics::Gdi::*,
 26        Security::Credentials::*,
 27        System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
 28        UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
 29    },
 30};
 31
 32use crate::*;
 33
 34pub(crate) struct WindowsPlatform {
 35    state: RefCell<WindowsPlatformState>,
 36    raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
 37    // The below members will never change throughout the entire lifecycle of the app.
 38    icon: HICON,
 39    background_executor: BackgroundExecutor,
 40    foreground_executor: ForegroundExecutor,
 41    text_system: Arc<dyn PlatformTextSystem>,
 42}
 43
 44pub(crate) struct WindowsPlatformState {
 45    callbacks: PlatformCallbacks,
 46    // NOTE: standard cursor handles don't need to close.
 47    pub(crate) current_cursor: HCURSOR,
 48}
 49
 50#[derive(Default)]
 51struct PlatformCallbacks {
 52    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 53    quit: Option<Box<dyn FnMut()>>,
 54    reopen: Option<Box<dyn FnMut()>>,
 55    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 56    will_open_app_menu: Option<Box<dyn FnMut()>>,
 57    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 58}
 59
 60impl WindowsPlatformState {
 61    fn new() -> Self {
 62        let callbacks = PlatformCallbacks::default();
 63        let current_cursor = load_cursor(CursorStyle::Arrow);
 64
 65        Self {
 66            callbacks,
 67            current_cursor,
 68        }
 69    }
 70}
 71
 72impl WindowsPlatform {
 73    pub(crate) fn new() -> Self {
 74        unsafe {
 75            OleInitialize(None).expect("unable to initialize Windows OLE");
 76        }
 77        let dispatcher = Arc::new(WindowsDispatcher::new());
 78        let background_executor = BackgroundExecutor::new(dispatcher.clone());
 79        let foreground_executor = ForegroundExecutor::new(dispatcher);
 80        let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() {
 81            log::info!("Using direct write text system.");
 82            Arc::new(direct_write) as Arc<dyn PlatformTextSystem>
 83        } else {
 84            log::info!("Using cosmic text system.");
 85            Arc::new(CosmicTextSystem::new()) as Arc<dyn PlatformTextSystem>
 86        };
 87        let icon = load_icon().unwrap_or_default();
 88        let state = RefCell::new(WindowsPlatformState::new());
 89        let raw_window_handles = RwLock::new(SmallVec::new());
 90
 91        Self {
 92            state,
 93            raw_window_handles,
 94            icon,
 95            background_executor,
 96            foreground_executor,
 97            text_system,
 98        }
 99    }
100
101    fn redraw_all(&self) {
102        for handle in self.raw_window_handles.read().iter() {
103            unsafe {
104                RedrawWindow(
105                    *handle,
106                    None,
107                    HRGN::default(),
108                    RDW_INVALIDATE | RDW_UPDATENOW,
109                )
110                .ok()
111                .log_err();
112            }
113        }
114    }
115
116    pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowStatePtr>> {
117        self.raw_window_handles
118            .read()
119            .iter()
120            .find(|entry| *entry == &hwnd)
121            .and_then(|hwnd| try_get_window_inner(*hwnd))
122    }
123
124    #[inline]
125    fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) {
126        self.raw_window_handles
127            .read()
128            .iter()
129            .for_each(|handle| unsafe {
130                PostMessageW(*handle, message, wparam, lparam).log_err();
131            });
132    }
133
134    fn close_one_window(&self, target_window: HWND) -> bool {
135        let mut lock = self.raw_window_handles.write();
136        let index = lock
137            .iter()
138            .position(|handle| *handle == target_window)
139            .unwrap();
140        lock.remove(index);
141
142        lock.is_empty()
143    }
144}
145
146impl Platform for WindowsPlatform {
147    fn background_executor(&self) -> BackgroundExecutor {
148        self.background_executor.clone()
149    }
150
151    fn foreground_executor(&self) -> ForegroundExecutor {
152        self.foreground_executor.clone()
153    }
154
155    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
156        self.text_system.clone()
157    }
158
159    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
160        on_finish_launching();
161        let vsync_event = create_event().unwrap();
162        begin_vsync(vsync_event.to_raw());
163        'a: loop {
164            let wait_result = unsafe {
165                MsgWaitForMultipleObjects(
166                    Some(&[vsync_event.to_raw()]),
167                    false,
168                    INFINITE,
169                    QS_ALLINPUT,
170                )
171            };
172
173            match wait_result {
174                // compositor clock ticked so we should draw a frame
175                WAIT_EVENT(0) => {
176                    self.redraw_all();
177                }
178                // Windows thread messages are posted
179                WAIT_EVENT(1) => {
180                    let mut msg = MSG::default();
181                    unsafe {
182                        while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
183                            match msg.message {
184                                WM_QUIT => break 'a,
185                                CLOSE_ONE_WINDOW => {
186                                    if self.close_one_window(HWND(msg.lParam.0)) {
187                                        break 'a;
188                                    }
189                                }
190                                _ => {
191                                    // todo(windows)
192                                    // crate `windows 0.56` reports true as Err
193                                    TranslateMessage(&msg).as_bool();
194                                    DispatchMessageW(&msg);
195                                }
196                            }
197                        }
198                    }
199                }
200                _ => {
201                    log::error!("Something went wrong while waiting {:?}", wait_result);
202                    break;
203                }
204            }
205        }
206
207        if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
208            callback();
209        }
210    }
211
212    fn quit(&self) {
213        self.foreground_executor()
214            .spawn(async { unsafe { PostQuitMessage(0) } })
215            .detach();
216    }
217
218    fn restart(&self, _: Option<PathBuf>) {
219        let pid = std::process::id();
220        let Some(app_path) = self.app_path().log_err() else {
221            return;
222        };
223        let script = format!(
224            r#"
225            $pidToWaitFor = {}
226            $exePath = "{}"
227
228            while ($true) {{
229                $process = Get-Process -Id $pidToWaitFor -ErrorAction SilentlyContinue
230                if (-not $process) {{
231                    Start-Process -FilePath $exePath
232                    break
233                }}
234                Start-Sleep -Seconds 0.1
235            }}
236            "#,
237            pid,
238            app_path.display(),
239        );
240        let restart_process = std::process::Command::new("powershell.exe")
241            .arg("-command")
242            .arg(script)
243            .spawn();
244
245        match restart_process {
246            Ok(_) => self.quit(),
247            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
248        }
249    }
250
251    // todo(windows)
252    fn activate(&self, ignoring_other_apps: bool) {}
253
254    // todo(windows)
255    fn hide(&self) {
256        unimplemented!()
257    }
258
259    // todo(windows)
260    fn hide_other_apps(&self) {
261        unimplemented!()
262    }
263
264    // todo(windows)
265    fn unhide_other_apps(&self) {
266        unimplemented!()
267    }
268
269    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
270        WindowsDisplay::displays()
271    }
272
273    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
274        WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
275    }
276
277    fn active_window(&self) -> Option<AnyWindowHandle> {
278        let active_window_hwnd = unsafe { GetActiveWindow() };
279        self.try_get_windows_inner_from_hwnd(active_window_hwnd)
280            .map(|inner| inner.handle)
281    }
282
283    fn open_window(
284        &self,
285        handle: AnyWindowHandle,
286        options: WindowParams,
287    ) -> Result<Box<dyn PlatformWindow>> {
288        let lock = self.state.borrow();
289        let window = WindowsWindow::new(
290            handle,
291            options,
292            self.icon,
293            self.foreground_executor.clone(),
294            lock.current_cursor,
295        );
296        drop(lock);
297        let handle = window.get_raw_handle();
298        self.raw_window_handles.write().push(handle);
299
300        Ok(Box::new(window))
301    }
302
303    // todo(windows)
304    fn window_appearance(&self) -> WindowAppearance {
305        WindowAppearance::Dark
306    }
307
308    fn open_url(&self, url: &str) {
309        let url_string = url.to_string();
310        self.background_executor()
311            .spawn(async move {
312                if url_string.is_empty() {
313                    return;
314                }
315                open_target(url_string.as_str());
316            })
317            .detach();
318    }
319
320    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
321        self.state.borrow_mut().callbacks.open_urls = Some(callback);
322    }
323
324    fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
325        let (tx, rx) = oneshot::channel();
326
327        self.foreground_executor()
328            .spawn(async move {
329                let tx = Cell::new(Some(tx));
330
331                // create file open dialog
332                let folder_dialog: IFileOpenDialog = unsafe {
333                    CoCreateInstance::<std::option::Option<&IUnknown>, IFileOpenDialog>(
334                        &FileOpenDialog,
335                        None,
336                        CLSCTX_ALL,
337                    )
338                    .unwrap()
339                };
340
341                // dialog options
342                let mut dialog_options: FILEOPENDIALOGOPTIONS = FOS_FILEMUSTEXIST;
343                if options.multiple {
344                    dialog_options |= FOS_ALLOWMULTISELECT;
345                }
346                if options.directories {
347                    dialog_options |= FOS_PICKFOLDERS;
348                }
349
350                unsafe {
351                    folder_dialog.SetOptions(dialog_options).unwrap();
352                    folder_dialog
353                        .SetTitle(&HSTRING::from(OsString::from("Select a folder")))
354                        .unwrap();
355                }
356
357                let hr = unsafe { folder_dialog.Show(None) };
358
359                if hr.is_err() {
360                    if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
361                        // user canceled error
362                        if let Some(tx) = tx.take() {
363                            tx.send(None).unwrap();
364                        }
365                        return;
366                    }
367                }
368
369                let mut results = unsafe { folder_dialog.GetResults().unwrap() };
370
371                let mut paths: Vec<PathBuf> = Vec::new();
372                for i in 0..unsafe { results.GetCount().unwrap() } {
373                    let mut item: IShellItem = unsafe { results.GetItemAt(i).unwrap() };
374                    let mut path: PWSTR =
375                        unsafe { item.GetDisplayName(SIGDN_FILESYSPATH).unwrap() };
376                    let mut path_os_string = OsString::from_wide(unsafe { path.as_wide() });
377
378                    paths.push(PathBuf::from(path_os_string));
379                }
380
381                if let Some(tx) = tx.take() {
382                    if paths.len() == 0 {
383                        tx.send(None).unwrap();
384                    } else {
385                        tx.send(Some(paths)).unwrap();
386                    }
387                }
388            })
389            .detach();
390
391        rx
392    }
393
394    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
395        let directory = directory.to_owned();
396        let (tx, rx) = oneshot::channel();
397        self.foreground_executor()
398            .spawn(async move {
399                unsafe {
400                    let Ok(dialog) = show_savefile_dialog(directory) else {
401                        let _ = tx.send(None);
402                        return;
403                    };
404                    let Ok(_) = dialog.Show(None) else {
405                        let _ = tx.send(None); // user cancel
406                        return;
407                    };
408                    if let Ok(shell_item) = dialog.GetResult() {
409                        if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
410                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
411                            return;
412                        }
413                    }
414                    let _ = tx.send(None);
415                }
416            })
417            .detach();
418
419        rx
420    }
421
422    fn reveal_path(&self, path: &Path) {
423        let Ok(file_full_path) = path.canonicalize() else {
424            log::error!("unable to parse file path");
425            return;
426        };
427        self.background_executor()
428            .spawn(async move {
429                let Some(path) = file_full_path.to_str() else {
430                    return;
431                };
432                if path.is_empty() {
433                    return;
434                }
435                open_target_in_explorer(path);
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    fn local_timezone(&self) -> UtcOffset {
469        let mut info = unsafe { std::mem::zeroed() };
470        let ret = unsafe { GetTimeZoneInformation(&mut info) };
471        if ret == TIME_ZONE_ID_INVALID {
472            log::error!(
473                "Unable to get local timezone: {}",
474                std::io::Error::last_os_error()
475            );
476            return UtcOffset::UTC;
477        }
478        // Windows treat offset as:
479        // UTC = localtime + offset
480        // so we add a minus here
481        let hours = -info.Bias / 60;
482        let minutes = -info.Bias % 60;
483
484        UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap()
485    }
486
487    // todo(windows)
488    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
489        Err(anyhow!("not yet implemented"))
490    }
491
492    fn set_cursor_style(&self, style: CursorStyle) {
493        let hcursor = load_cursor(style);
494        let mut lock = self.state.borrow_mut();
495        if lock.current_cursor.0 != hcursor.0 {
496            self.post_message(CURSOR_STYLE_CHANGED, WPARAM(0), LPARAM(hcursor.0));
497            lock.current_cursor = hcursor;
498        }
499    }
500
501    // todo(windows)
502    fn should_auto_hide_scrollbars(&self) -> bool {
503        false
504    }
505
506    fn write_to_clipboard(&self, item: ClipboardItem) {
507        if item.text.len() > 0 {
508            let mut ctx = ClipboardContext::new().unwrap();
509            ctx.set_contents(item.text().to_owned()).unwrap();
510        }
511    }
512
513    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
514        let mut ctx = ClipboardContext::new().unwrap();
515        let content = ctx.get_contents().ok()?;
516        Some(ClipboardItem {
517            text: content,
518            metadata: None,
519        })
520    }
521
522    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
523        let mut password = password.to_vec();
524        let mut username = username.encode_utf16().chain(Some(0)).collect_vec();
525        let mut target_name = windows_credentials_target_name(url)
526            .encode_utf16()
527            .chain(Some(0))
528            .collect_vec();
529        self.foreground_executor().spawn(async move {
530            let credentials = CREDENTIALW {
531                LastWritten: unsafe { GetSystemTimeAsFileTime() },
532                Flags: CRED_FLAGS(0),
533                Type: CRED_TYPE_GENERIC,
534                TargetName: PWSTR::from_raw(target_name.as_mut_ptr()),
535                CredentialBlobSize: password.len() as u32,
536                CredentialBlob: password.as_ptr() as *mut _,
537                Persist: CRED_PERSIST_LOCAL_MACHINE,
538                UserName: PWSTR::from_raw(username.as_mut_ptr()),
539                ..CREDENTIALW::default()
540            };
541            unsafe { CredWriteW(&credentials, 0) }?;
542            Ok(())
543        })
544    }
545
546    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
547        let mut target_name = windows_credentials_target_name(url)
548            .encode_utf16()
549            .chain(Some(0))
550            .collect_vec();
551        self.foreground_executor().spawn(async move {
552            let mut credentials: *mut CREDENTIALW = std::ptr::null_mut();
553            unsafe {
554                CredReadW(
555                    PCWSTR::from_raw(target_name.as_ptr()),
556                    CRED_TYPE_GENERIC,
557                    0,
558                    &mut credentials,
559                )?
560            };
561
562            if credentials.is_null() {
563                Ok(None)
564            } else {
565                let username: String = unsafe { (*credentials).UserName.to_string()? };
566                let credential_blob = unsafe {
567                    std::slice::from_raw_parts(
568                        (*credentials).CredentialBlob,
569                        (*credentials).CredentialBlobSize as usize,
570                    )
571                };
572                let password = credential_blob.to_vec();
573                unsafe { CredFree(credentials as *const c_void) };
574                Ok(Some((username, password)))
575            }
576        })
577    }
578
579    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
580        let mut target_name = windows_credentials_target_name(url)
581            .encode_utf16()
582            .chain(Some(0))
583            .collect_vec();
584        self.foreground_executor().spawn(async move {
585            unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };
586            Ok(())
587        })
588    }
589
590    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
591        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
592    }
593}
594
595impl Drop for WindowsPlatform {
596    fn drop(&mut self) {
597        unsafe {
598            OleUninitialize();
599        }
600    }
601}
602
603fn open_target(target: &str) {
604    unsafe {
605        let ret = ShellExecuteW(
606            None,
607            windows::core::w!("open"),
608            &HSTRING::from(target),
609            None,
610            None,
611            SW_SHOWDEFAULT,
612        );
613        if ret.0 <= 32 {
614            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
615        }
616    }
617}
618
619fn open_target_in_explorer(target: &str) {
620    unsafe {
621        let ret = ShellExecuteW(
622            None,
623            windows::core::w!("open"),
624            windows::core::w!("explorer.exe"),
625            &HSTRING::from(format!("/select,{}", target).as_str()),
626            None,
627            SW_SHOWDEFAULT,
628        );
629        if ret.0 <= 32 {
630            log::error!(
631                "Unable to open target in explorer: {}",
632                std::io::Error::last_os_error()
633            );
634        }
635    }
636}
637
638unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
639    let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?;
640    let bind_context = CreateBindCtx(0)?;
641    let Ok(full_path) = directory.canonicalize() else {
642        return Ok(dialog);
643    };
644    let dir_str = full_path.into_os_string();
645    if dir_str.is_empty() {
646        return Ok(dialog);
647    }
648    let dir_vec = dir_str.encode_wide().collect_vec();
649    let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context)
650        .inspect_err(|e| log::error!("unable to create IShellItem: {}", e));
651    if ret.is_ok() {
652        let dir_shell_item: IShellItem = ret.unwrap();
653        let _ = dialog
654            .SetFolder(&dir_shell_item)
655            .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e));
656    }
657
658    Ok(dialog)
659}
660
661fn begin_vsync(vsync_evnet: HANDLE) {
662    std::thread::spawn(move || unsafe {
663        loop {
664            windows::Win32::Graphics::Dwm::DwmFlush().log_err();
665            SetEvent(vsync_evnet).log_err();
666        }
667    });
668}
669
670fn load_icon() -> Result<HICON> {
671    let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
672    let handle = unsafe {
673        LoadImageW(
674            module,
675            IDI_APPLICATION,
676            IMAGE_ICON,
677            0,
678            0,
679            LR_DEFAULTSIZE | LR_SHARED,
680        )
681        .context("unable to load icon file")?
682    };
683    Ok(HICON(handle.0))
684}