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    os::windows::ffi::{OsStrExt, OsStringExt},
  8    path::{Path, PathBuf},
  9    rc::Rc,
 10    sync::Arc,
 11    time::Duration,
 12};
 13
 14use anyhow::{anyhow, Result};
 15use async_task::Runnable;
 16use copypasta::{ClipboardContext, ClipboardProvider};
 17use futures::channel::oneshot::{self, Receiver};
 18use itertools::Itertools;
 19use parking_lot::{Mutex, RwLock};
 20use smallvec::SmallVec;
 21use time::UtcOffset;
 22use util::{ResultExt, SemanticVersion};
 23use windows::{
 24    core::{IUnknown, HRESULT, HSTRING, PCWSTR, PWSTR},
 25    Wdk::System::SystemServices::RtlGetVersion,
 26    Win32::{
 27        Foundation::{CloseHandle, HANDLE, HWND},
 28        Graphics::{
 29            DirectComposition::DCompositionWaitForCompositorClock,
 30            Gdi::{RedrawWindow, HRGN, RDW_INVALIDATE, RDW_UPDATENOW},
 31        },
 32        System::{
 33            Com::{CoCreateInstance, CreateBindCtx, CLSCTX_ALL},
 34            Ole::{OleInitialize, OleUninitialize},
 35            Threading::{CreateEventW, INFINITE},
 36            Time::{GetTimeZoneInformation, TIME_ZONE_ID_INVALID},
 37        },
 38        UI::{
 39            Input::KeyboardAndMouse::GetDoubleClickTime,
 40            Shell::{
 41                FileOpenDialog, FileSaveDialog, IFileOpenDialog, IFileSaveDialog, IShellItem,
 42                SHCreateItemFromParsingName, ShellExecuteW, FILEOPENDIALOGOPTIONS,
 43                FOS_ALLOWMULTISELECT, FOS_FILEMUSTEXIST, FOS_PICKFOLDERS, SIGDN_FILESYSPATH,
 44            },
 45            WindowsAndMessaging::{
 46                DispatchMessageW, LoadImageW, PeekMessageW, PostQuitMessage, SetCursor,
 47                SystemParametersInfoW, TranslateMessage, HCURSOR, IDC_ARROW, IDC_CROSS, IDC_HAND,
 48                IDC_IBEAM, IDC_NO, IDC_SIZENS, IDC_SIZEWE, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_SHARED,
 49                MSG, PM_REMOVE, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SW_SHOWDEFAULT,
 50                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WM_QUIT, WM_SETTINGCHANGE,
 51            },
 52        },
 53    },
 54};
 55
 56use crate::{
 57    Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
 58    Keymap, Menu, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem,
 59    PlatformWindow, Task, WindowAppearance, WindowParams, WindowsDispatcher, WindowsDisplay,
 60    WindowsTextSystem, WindowsWindow,
 61};
 62
 63pub(crate) struct WindowsPlatform {
 64    inner: Rc<WindowsPlatformInner>,
 65}
 66
 67/// Windows settings pulled from SystemParametersInfo
 68/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
 69#[derive(Default, Debug)]
 70pub(crate) struct WindowsPlatformSystemSettings {
 71    /// SEE: SPI_GETWHEELSCROLLCHARS
 72    pub(crate) wheel_scroll_chars: u32,
 73
 74    /// SEE: SPI_GETWHEELSCROLLLINES
 75    pub(crate) wheel_scroll_lines: u32,
 76}
 77
 78pub(crate) struct WindowsPlatformInner {
 79    background_executor: BackgroundExecutor,
 80    pub(crate) foreground_executor: ForegroundExecutor,
 81    main_receiver: flume::Receiver<Runnable>,
 82    text_system: Arc<WindowsTextSystem>,
 83    callbacks: Mutex<Callbacks>,
 84    pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
 85    pub(crate) event: HANDLE,
 86    pub(crate) settings: RefCell<WindowsPlatformSystemSettings>,
 87}
 88
 89impl Drop for WindowsPlatformInner {
 90    fn drop(&mut self) {
 91        unsafe { CloseHandle(self.event) }.ok();
 92    }
 93}
 94
 95#[derive(Default)]
 96struct Callbacks {
 97    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 98    become_active: Option<Box<dyn FnMut()>>,
 99    resign_active: Option<Box<dyn FnMut()>>,
100    quit: Option<Box<dyn FnMut()>>,
101    reopen: Option<Box<dyn FnMut()>>,
102    event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
103    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
104    will_open_app_menu: Option<Box<dyn FnMut()>>,
105    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
106}
107
108enum WindowsMessageWaitResult {
109    ForegroundExecution,
110    WindowsMessage(MSG),
111    Error,
112}
113
114impl WindowsPlatformSystemSettings {
115    fn new() -> Self {
116        let mut settings = Self::default();
117        settings.update_all();
118        settings
119    }
120
121    pub(crate) fn update_all(&mut self) {
122        self.update_wheel_scroll_lines();
123        self.update_wheel_scroll_chars();
124    }
125
126    pub(crate) fn update_wheel_scroll_lines(&mut self) {
127        let mut value = c_uint::default();
128        let result = unsafe {
129            SystemParametersInfoW(
130                SPI_GETWHEELSCROLLLINES,
131                0,
132                Some((&mut value) as *mut c_uint as *mut c_void),
133                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
134            )
135        };
136
137        if result.log_err() != None {
138            self.wheel_scroll_lines = value;
139        }
140    }
141
142    pub(crate) fn update_wheel_scroll_chars(&mut self) {
143        let mut value = c_uint::default();
144        let result = unsafe {
145            SystemParametersInfoW(
146                SPI_GETWHEELSCROLLCHARS,
147                0,
148                Some((&mut value) as *mut c_uint as *mut c_void),
149                SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
150            )
151        };
152
153        if result.log_err() != None {
154            self.wheel_scroll_chars = value;
155        }
156    }
157}
158
159impl WindowsPlatform {
160    pub(crate) fn new() -> Self {
161        unsafe {
162            OleInitialize(None).expect("unable to initialize Windows OLE");
163        }
164        let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
165        let event = unsafe { CreateEventW(None, false, false, None) }.unwrap();
166        let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, event));
167        let background_executor = BackgroundExecutor::new(dispatcher.clone());
168        let foreground_executor = ForegroundExecutor::new(dispatcher);
169        let text_system = Arc::new(WindowsTextSystem::new());
170        let callbacks = Mutex::new(Callbacks::default());
171        let raw_window_handles = RwLock::new(SmallVec::new());
172        let settings = RefCell::new(WindowsPlatformSystemSettings::new());
173        let inner = Rc::new(WindowsPlatformInner {
174            background_executor,
175            foreground_executor,
176            main_receiver,
177            text_system,
178            callbacks,
179            raw_window_handles,
180            event,
181            settings,
182        });
183        Self { inner }
184    }
185
186    fn run_foreground_tasks(&self) {
187        for runnable in self.inner.main_receiver.drain() {
188            runnable.run();
189        }
190    }
191
192    fn redraw_all(&self) {
193        for handle in self.inner.raw_window_handles.read().iter() {
194            unsafe {
195                RedrawWindow(
196                    *handle,
197                    None,
198                    HRGN::default(),
199                    RDW_INVALIDATE | RDW_UPDATENOW,
200                );
201            }
202        }
203    }
204}
205
206impl Platform for WindowsPlatform {
207    fn background_executor(&self) -> BackgroundExecutor {
208        self.inner.background_executor.clone()
209    }
210
211    fn foreground_executor(&self) -> ForegroundExecutor {
212        self.inner.foreground_executor.clone()
213    }
214
215    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
216        self.inner.text_system.clone()
217    }
218
219    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
220        on_finish_launching();
221        let dispatch_event = self.inner.event;
222
223        'a: loop {
224            let mut msg = MSG::default();
225            // will be 0 if woken up by self.inner.event or 1 if the compositor clock ticked
226            // SEE: https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock
227            let wait_result =
228                unsafe { DCompositionWaitForCompositorClock(Some(&[dispatch_event]), INFINITE) };
229
230            // compositor clock ticked so we should draw a frame
231            if wait_result == 1 {
232                self.redraw_all();
233                unsafe {
234                    let mut msg = MSG::default();
235
236                    while PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE).as_bool() {
237                        if msg.message == WM_QUIT {
238                            break 'a;
239                        }
240                        if msg.message == WM_SETTINGCHANGE {
241                            self.inner.settings.borrow_mut().update_all();
242                            continue;
243                        }
244                        TranslateMessage(&msg);
245                        DispatchMessageW(&msg);
246                    }
247                }
248            }
249            self.run_foreground_tasks();
250        }
251
252        let mut callbacks = self.inner.callbacks.lock();
253        if let Some(callback) = callbacks.quit.as_mut() {
254            callback()
255        }
256    }
257
258    fn quit(&self) {
259        self.foreground_executor()
260            .spawn(async { unsafe { PostQuitMessage(0) } })
261            .detach();
262    }
263
264    // todo(windows)
265    fn restart(&self) {
266        unimplemented!()
267    }
268
269    // todo(windows)
270    fn activate(&self, ignoring_other_apps: bool) {}
271
272    // todo(windows)
273    fn hide(&self) {
274        unimplemented!()
275    }
276
277    // todo(windows)
278    fn hide_other_apps(&self) {
279        unimplemented!()
280    }
281
282    // todo(windows)
283    fn unhide_other_apps(&self) {
284        unimplemented!()
285    }
286
287    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
288        WindowsDisplay::displays()
289    }
290
291    fn display(&self, id: crate::DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
292        if let Some(display) = WindowsDisplay::new(id) {
293            Some(Rc::new(display) as Rc<dyn PlatformDisplay>)
294        } else {
295            None
296        }
297    }
298
299    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
300        if let Some(display) = WindowsDisplay::primary_monitor() {
301            Some(Rc::new(display) as Rc<dyn PlatformDisplay>)
302        } else {
303            None
304        }
305    }
306
307    // todo(windows)
308    fn active_window(&self) -> Option<AnyWindowHandle> {
309        None
310    }
311
312    fn open_window(
313        &self,
314        handle: AnyWindowHandle,
315        options: WindowParams,
316    ) -> Box<dyn PlatformWindow> {
317        Box::new(WindowsWindow::new(self.inner.clone(), handle, options))
318    }
319
320    // todo(windows)
321    fn window_appearance(&self) -> WindowAppearance {
322        WindowAppearance::Dark
323    }
324
325    fn open_url(&self, url: &str) {
326        let url_string = url.to_string();
327        self.background_executor()
328            .spawn(async move {
329                if url_string.is_empty() {
330                    return;
331                }
332                open_target(url_string.as_str());
333            })
334            .detach();
335    }
336
337    // todo(windows)
338    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
339        self.inner.callbacks.lock().open_urls = Some(callback);
340    }
341
342    fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
343        let (tx, rx) = oneshot::channel();
344
345        self.foreground_executor()
346            .spawn(async move {
347                let tx = Cell::new(Some(tx));
348
349                // create file open dialog
350                let folder_dialog: IFileOpenDialog = unsafe {
351                    CoCreateInstance::<std::option::Option<&IUnknown>, IFileOpenDialog>(
352                        &FileOpenDialog,
353                        None,
354                        CLSCTX_ALL,
355                    )
356                    .unwrap()
357                };
358
359                // dialog options
360                let mut dialog_options: FILEOPENDIALOGOPTIONS = FOS_FILEMUSTEXIST;
361                if options.multiple {
362                    dialog_options |= FOS_ALLOWMULTISELECT;
363                }
364                if options.directories {
365                    dialog_options |= FOS_PICKFOLDERS;
366                }
367
368                unsafe {
369                    folder_dialog.SetOptions(dialog_options).unwrap();
370                    folder_dialog
371                        .SetTitle(&HSTRING::from(OsString::from("Select a folder")))
372                        .unwrap();
373                }
374
375                let hr = unsafe { folder_dialog.Show(None) };
376
377                if hr.is_err() {
378                    if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
379                        // user canceled error
380                        if let Some(tx) = tx.take() {
381                            tx.send(None).unwrap();
382                        }
383                        return;
384                    }
385                }
386
387                let mut results = unsafe { folder_dialog.GetResults().unwrap() };
388
389                let mut paths: Vec<PathBuf> = Vec::new();
390                for i in 0..unsafe { results.GetCount().unwrap() } {
391                    let mut item: IShellItem = unsafe { results.GetItemAt(i).unwrap() };
392                    let mut path: PWSTR =
393                        unsafe { item.GetDisplayName(SIGDN_FILESYSPATH).unwrap() };
394                    let mut path_os_string = OsString::from_wide(unsafe { path.as_wide() });
395
396                    paths.push(PathBuf::from(path_os_string));
397                }
398
399                if let Some(tx) = tx.take() {
400                    if paths.len() == 0 {
401                        tx.send(None).unwrap();
402                    } else {
403                        tx.send(Some(paths)).unwrap();
404                    }
405                }
406            })
407            .detach();
408
409        rx
410    }
411
412    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
413        let directory = directory.to_owned();
414        let (tx, rx) = oneshot::channel();
415        self.foreground_executor()
416            .spawn(async move {
417                unsafe {
418                    let Ok(dialog) = show_savefile_dialog(directory) else {
419                        let _ = tx.send(None);
420                        return;
421                    };
422                    let Ok(_) = dialog.Show(None) else {
423                        let _ = tx.send(None); // user cancel
424                        return;
425                    };
426                    if let Ok(shell_item) = dialog.GetResult() {
427                        if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
428                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
429                            return;
430                        }
431                    }
432                    let _ = tx.send(None);
433                }
434            })
435            .detach();
436
437        rx
438    }
439
440    fn reveal_path(&self, path: &Path) {
441        let Ok(file_full_path) = path.canonicalize() else {
442            log::error!("unable to parse file path");
443            return;
444        };
445        self.background_executor()
446            .spawn(async move {
447                let Some(path) = file_full_path.to_str() else {
448                    return;
449                };
450                if path.is_empty() {
451                    return;
452                }
453                open_target(path);
454            })
455            .detach();
456    }
457
458    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
459        self.inner.callbacks.lock().become_active = Some(callback);
460    }
461
462    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
463        self.inner.callbacks.lock().resign_active = Some(callback);
464    }
465
466    fn on_quit(&self, callback: Box<dyn FnMut()>) {
467        self.inner.callbacks.lock().quit = Some(callback);
468    }
469
470    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
471        self.inner.callbacks.lock().reopen = Some(callback);
472    }
473
474    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
475        self.inner.callbacks.lock().event = Some(callback);
476    }
477
478    // todo(windows)
479    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
480
481    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
482        self.inner.callbacks.lock().app_menu_action = Some(callback);
483    }
484
485    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
486        self.inner.callbacks.lock().will_open_app_menu = Some(callback);
487    }
488
489    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
490        self.inner.callbacks.lock().validate_app_menu_command = Some(callback);
491    }
492
493    fn os_name(&self) -> &'static str {
494        "Windows"
495    }
496
497    fn os_version(&self) -> Result<SemanticVersion> {
498        let mut info = unsafe { std::mem::zeroed() };
499        let status = unsafe { RtlGetVersion(&mut info) };
500        if status.is_ok() {
501            Ok(SemanticVersion {
502                major: info.dwMajorVersion as _,
503                minor: info.dwMinorVersion as _,
504                patch: info.dwBuildNumber as _,
505            })
506        } else {
507            Err(anyhow::anyhow!(
508                "unable to get Windows version: {}",
509                std::io::Error::last_os_error()
510            ))
511        }
512    }
513
514    fn app_version(&self) -> Result<SemanticVersion> {
515        Ok(SemanticVersion {
516            major: 1,
517            minor: 0,
518            patch: 0,
519        })
520    }
521
522    // todo(windows)
523    fn app_path(&self) -> Result<PathBuf> {
524        Err(anyhow!("not yet implemented"))
525    }
526
527    fn local_timezone(&self) -> UtcOffset {
528        let mut info = unsafe { std::mem::zeroed() };
529        let ret = unsafe { GetTimeZoneInformation(&mut info) };
530        if ret == TIME_ZONE_ID_INVALID {
531            log::error!(
532                "Unable to get local timezone: {}",
533                std::io::Error::last_os_error()
534            );
535            return UtcOffset::UTC;
536        }
537        // Windows treat offset as:
538        // UTC = localtime + offset
539        // so we add a minus here
540        let hours = -info.Bias / 60;
541        let minutes = -info.Bias % 60;
542
543        UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap()
544    }
545
546    fn double_click_interval(&self) -> Duration {
547        let millis = unsafe { GetDoubleClickTime() };
548        Duration::from_millis(millis as _)
549    }
550
551    // todo(windows)
552    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
553        Err(anyhow!("not yet implemented"))
554    }
555
556    fn set_cursor_style(&self, style: CursorStyle) {
557        let handle = match style {
558            CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => unsafe {
559                load_cursor(IDC_IBEAM)
560            },
561            CursorStyle::Crosshair => unsafe { load_cursor(IDC_CROSS) },
562            CursorStyle::PointingHand | CursorStyle::DragLink => unsafe { load_cursor(IDC_HAND) },
563            CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => unsafe {
564                load_cursor(IDC_SIZEWE)
565            },
566            CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => unsafe {
567                load_cursor(IDC_SIZENS)
568            },
569            CursorStyle::OperationNotAllowed => unsafe { load_cursor(IDC_NO) },
570            _ => unsafe { load_cursor(IDC_ARROW) },
571        };
572        if handle.is_err() {
573            log::error!(
574                "Error loading cursor image: {}",
575                std::io::Error::last_os_error()
576            );
577            return;
578        }
579        let _ = unsafe { SetCursor(HCURSOR(handle.unwrap().0)) };
580    }
581
582    // todo(windows)
583    fn should_auto_hide_scrollbars(&self) -> bool {
584        false
585    }
586
587    fn write_to_clipboard(&self, item: ClipboardItem) {
588        let mut ctx = ClipboardContext::new().unwrap();
589        ctx.set_contents(item.text().to_owned()).unwrap();
590    }
591
592    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
593        let mut ctx = ClipboardContext::new().unwrap();
594        let content = ctx.get_contents().unwrap();
595        Some(ClipboardItem {
596            text: content,
597            metadata: None,
598        })
599    }
600
601    // todo(windows)
602    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
603        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
604    }
605
606    // todo(windows)
607    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
608        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
609    }
610
611    // todo(windows)
612    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
613        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
614    }
615
616    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
617        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
618    }
619}
620
621impl Drop for WindowsPlatform {
622    fn drop(&mut self) {
623        unsafe {
624            OleUninitialize();
625        }
626    }
627}
628
629unsafe fn load_cursor(name: PCWSTR) -> Result<HANDLE> {
630    LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED).map_err(|e| anyhow!(e))
631}
632
633fn open_target(target: &str) {
634    unsafe {
635        let ret = ShellExecuteW(
636            None,
637            windows::core::w!("open"),
638            &HSTRING::from(target),
639            None,
640            None,
641            SW_SHOWDEFAULT,
642        );
643        if ret.0 <= 32 {
644            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
645        }
646    }
647}
648
649unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
650    let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?;
651    let bind_context = CreateBindCtx(0)?;
652    let Ok(full_path) = directory.canonicalize() else {
653        return Ok(dialog);
654    };
655    let dir_str = full_path.into_os_string();
656    if dir_str.is_empty() {
657        return Ok(dialog);
658    }
659    let dir_vec = dir_str.encode_wide().collect_vec();
660    let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context)
661        .inspect_err(|e| log::error!("unable to create IShellItem: {}", e));
662    if ret.is_ok() {
663        let dir_shell_item: IShellItem = ret.unwrap();
664        let _ = dialog
665            .SetFolder(&dir_shell_item)
666            .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e));
667    }
668
669    Ok(dialog)
670}