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