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    // todo(windows)
288    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
289        vec![Rc::new(WindowsDisplay::new())]
290    }
291
292    // todo(windows)
293    fn display(&self, id: crate::DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
294        Some(Rc::new(WindowsDisplay::new()))
295    }
296
297    // todo(windows)
298    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
299        Some(Rc::new(WindowsDisplay::new()))
300    }
301
302    // todo(windows)
303    fn active_window(&self) -> Option<AnyWindowHandle> {
304        None
305    }
306
307    fn open_window(
308        &self,
309        handle: AnyWindowHandle,
310        options: WindowParams,
311    ) -> Box<dyn PlatformWindow> {
312        Box::new(WindowsWindow::new(self.inner.clone(), handle, options))
313    }
314
315    // todo(windows)
316    fn window_appearance(&self) -> WindowAppearance {
317        WindowAppearance::Dark
318    }
319
320    fn open_url(&self, url: &str) {
321        let url_string = url.to_string();
322        self.background_executor()
323            .spawn(async move {
324                if url_string.is_empty() {
325                    return;
326                }
327                open_target(url_string.as_str());
328            })
329            .detach();
330    }
331
332    // todo(windows)
333    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
334        self.inner.callbacks.lock().open_urls = Some(callback);
335    }
336
337    fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
338        let (tx, rx) = oneshot::channel();
339
340        self.foreground_executor()
341            .spawn(async move {
342                let tx = Cell::new(Some(tx));
343
344                // create file open dialog
345                let folder_dialog: IFileOpenDialog = unsafe {
346                    CoCreateInstance::<std::option::Option<&IUnknown>, IFileOpenDialog>(
347                        &FileOpenDialog,
348                        None,
349                        CLSCTX_ALL,
350                    )
351                    .unwrap()
352                };
353
354                // dialog options
355                let mut dialog_options: FILEOPENDIALOGOPTIONS = FOS_FILEMUSTEXIST;
356                if options.multiple {
357                    dialog_options |= FOS_ALLOWMULTISELECT;
358                }
359                if options.directories {
360                    dialog_options |= FOS_PICKFOLDERS;
361                }
362
363                unsafe {
364                    folder_dialog.SetOptions(dialog_options).unwrap();
365                    folder_dialog
366                        .SetTitle(&HSTRING::from(OsString::from("Select a folder")))
367                        .unwrap();
368                }
369
370                let hr = unsafe { folder_dialog.Show(None) };
371
372                if hr.is_err() {
373                    if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
374                        // user canceled error
375                        if let Some(tx) = tx.take() {
376                            tx.send(None).unwrap();
377                        }
378                        return;
379                    }
380                }
381
382                let mut results = unsafe { folder_dialog.GetResults().unwrap() };
383
384                let mut paths: Vec<PathBuf> = Vec::new();
385                for i in 0..unsafe { results.GetCount().unwrap() } {
386                    let mut item: IShellItem = unsafe { results.GetItemAt(i).unwrap() };
387                    let mut path: PWSTR =
388                        unsafe { item.GetDisplayName(SIGDN_FILESYSPATH).unwrap() };
389                    let mut path_os_string = OsString::from_wide(unsafe { path.as_wide() });
390
391                    paths.push(PathBuf::from(path_os_string));
392                }
393
394                if let Some(tx) = tx.take() {
395                    if paths.len() == 0 {
396                        tx.send(None).unwrap();
397                    } else {
398                        tx.send(Some(paths)).unwrap();
399                    }
400                }
401            })
402            .detach();
403
404        rx
405    }
406
407    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
408        let directory = directory.to_owned();
409        let (tx, rx) = oneshot::channel();
410        self.foreground_executor()
411            .spawn(async move {
412                unsafe {
413                    let Ok(dialog) = show_savefile_dialog(directory) else {
414                        let _ = tx.send(None);
415                        return;
416                    };
417                    let Ok(_) = dialog.Show(None) else {
418                        let _ = tx.send(None); // user cancel
419                        return;
420                    };
421                    if let Ok(shell_item) = dialog.GetResult() {
422                        if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
423                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
424                            return;
425                        }
426                    }
427                    let _ = tx.send(None);
428                }
429            })
430            .detach();
431
432        rx
433    }
434
435    fn reveal_path(&self, path: &Path) {
436        let Ok(file_full_path) = path.canonicalize() else {
437            log::error!("unable to parse file path");
438            return;
439        };
440        self.background_executor()
441            .spawn(async move {
442                let Some(path) = file_full_path.to_str() else {
443                    return;
444                };
445                if path.is_empty() {
446                    return;
447                }
448                open_target(path);
449            })
450            .detach();
451    }
452
453    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
454        self.inner.callbacks.lock().become_active = Some(callback);
455    }
456
457    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
458        self.inner.callbacks.lock().resign_active = Some(callback);
459    }
460
461    fn on_quit(&self, callback: Box<dyn FnMut()>) {
462        self.inner.callbacks.lock().quit = Some(callback);
463    }
464
465    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
466        self.inner.callbacks.lock().reopen = Some(callback);
467    }
468
469    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
470        self.inner.callbacks.lock().event = Some(callback);
471    }
472
473    // todo(windows)
474    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
475
476    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
477        self.inner.callbacks.lock().app_menu_action = Some(callback);
478    }
479
480    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
481        self.inner.callbacks.lock().will_open_app_menu = Some(callback);
482    }
483
484    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
485        self.inner.callbacks.lock().validate_app_menu_command = Some(callback);
486    }
487
488    fn os_name(&self) -> &'static str {
489        "Windows"
490    }
491
492    fn os_version(&self) -> Result<SemanticVersion> {
493        let mut info = unsafe { std::mem::zeroed() };
494        let status = unsafe { RtlGetVersion(&mut info) };
495        if status.is_ok() {
496            Ok(SemanticVersion {
497                major: info.dwMajorVersion as _,
498                minor: info.dwMinorVersion as _,
499                patch: info.dwBuildNumber as _,
500            })
501        } else {
502            Err(anyhow::anyhow!(
503                "unable to get Windows version: {}",
504                std::io::Error::last_os_error()
505            ))
506        }
507    }
508
509    fn app_version(&self) -> Result<SemanticVersion> {
510        Ok(SemanticVersion {
511            major: 1,
512            minor: 0,
513            patch: 0,
514        })
515    }
516
517    // todo(windows)
518    fn app_path(&self) -> Result<PathBuf> {
519        Err(anyhow!("not yet implemented"))
520    }
521
522    fn local_timezone(&self) -> UtcOffset {
523        let mut info = unsafe { std::mem::zeroed() };
524        let ret = unsafe { GetTimeZoneInformation(&mut info) };
525        if ret == TIME_ZONE_ID_INVALID {
526            log::error!(
527                "Unable to get local timezone: {}",
528                std::io::Error::last_os_error()
529            );
530            return UtcOffset::UTC;
531        }
532        // Windows treat offset as:
533        // UTC = localtime + offset
534        // so we add a minus here
535        let hours = -info.Bias / 60;
536        let minutes = -info.Bias % 60;
537
538        UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap()
539    }
540
541    fn double_click_interval(&self) -> Duration {
542        let millis = unsafe { GetDoubleClickTime() };
543        Duration::from_millis(millis as _)
544    }
545
546    // todo(windows)
547    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
548        Err(anyhow!("not yet implemented"))
549    }
550
551    fn set_cursor_style(&self, style: CursorStyle) {
552        let handle = match style {
553            CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => unsafe {
554                load_cursor(IDC_IBEAM)
555            },
556            CursorStyle::Crosshair => unsafe { load_cursor(IDC_CROSS) },
557            CursorStyle::PointingHand | CursorStyle::DragLink => unsafe { load_cursor(IDC_HAND) },
558            CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => unsafe {
559                load_cursor(IDC_SIZEWE)
560            },
561            CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => unsafe {
562                load_cursor(IDC_SIZENS)
563            },
564            CursorStyle::OperationNotAllowed => unsafe { load_cursor(IDC_NO) },
565            _ => unsafe { load_cursor(IDC_ARROW) },
566        };
567        if handle.is_err() {
568            log::error!(
569                "Error loading cursor image: {}",
570                std::io::Error::last_os_error()
571            );
572            return;
573        }
574        let _ = unsafe { SetCursor(HCURSOR(handle.unwrap().0)) };
575    }
576
577    // todo(windows)
578    fn should_auto_hide_scrollbars(&self) -> bool {
579        false
580    }
581
582    fn write_to_clipboard(&self, item: ClipboardItem) {
583        let mut ctx = ClipboardContext::new().unwrap();
584        ctx.set_contents(item.text().to_owned()).unwrap();
585    }
586
587    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
588        let mut ctx = ClipboardContext::new().unwrap();
589        let content = ctx.get_contents().unwrap();
590        Some(ClipboardItem {
591            text: content,
592            metadata: None,
593        })
594    }
595
596    // todo(windows)
597    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
598        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
599    }
600
601    // todo(windows)
602    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
603        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
604    }
605
606    // todo(windows)
607    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
608        Task::Ready(Some(Err(anyhow!("not implemented yet."))))
609    }
610
611    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
612        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
613    }
614}
615
616impl Drop for WindowsPlatform {
617    fn drop(&mut self) {
618        unsafe {
619            OleUninitialize();
620        }
621    }
622}
623
624unsafe fn load_cursor(name: PCWSTR) -> Result<HANDLE> {
625    LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED).map_err(|e| anyhow!(e))
626}
627
628fn open_target(target: &str) {
629    unsafe {
630        let ret = ShellExecuteW(
631            None,
632            windows::core::w!("open"),
633            &HSTRING::from(target),
634            None,
635            None,
636            SW_SHOWDEFAULT,
637        );
638        if ret.0 <= 32 {
639            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
640        }
641    }
642}
643
644unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
645    let dialog: IFileSaveDialog = CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)?;
646    let bind_context = CreateBindCtx(0)?;
647    let Ok(full_path) = directory.canonicalize() else {
648        return Ok(dialog);
649    };
650    let dir_str = full_path.into_os_string();
651    if dir_str.is_empty() {
652        return Ok(dialog);
653    }
654    let dir_vec = dir_str.encode_wide().collect_vec();
655    let ret = SHCreateItemFromParsingName(PCWSTR::from_raw(dir_vec.as_ptr()), &bind_context)
656        .inspect_err(|e| log::error!("unable to create IShellItem: {}", e));
657    if ret.is_ok() {
658        let dir_shell_item: IShellItem = ret.unwrap();
659        let _ = dialog
660            .SetFolder(&dir_shell_item)
661            .inspect_err(|e| log::error!("unable to set folder for save file dialog: {}", e));
662    }
663
664    Ok(dialog)
665}