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