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