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