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