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}