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