Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/window_migration.rs43.8 KB
1use std::mem::size_of;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::sync::{Arc, Mutex, OnceLock};
4use std::thread;
5use std::thread::JoinHandle;
6use std::time::{Duration, Instant};
7
8use windows::Win32::Foundation::{HWND, LPARAM, POINT, RECT};
9use windows::Win32::Graphics::Dwm::{DWMWA_CLOAKED, DwmGetWindowAttribute};
10use windows::Win32::Graphics::Gdi::{
11 HMONITOR, MONITOR_DEFAULTTONULL, MonitorFromRect, MonitorFromWindow,
12};
13use windows::Win32::System::Threading::GetCurrentProcessId;
14use windows::Win32::UI::WindowsAndMessaging::{
15 EnumWindows, GWL_EXSTYLE, GetClassNameW, GetDesktopWindow, GetShellWindow, GetWindowLongPtrW,
16 GetWindowPlacement, GetWindowRect, GetWindowThreadProcessId, IsIconic, IsWindow,
17 IsWindowVisible, IsZoomed, SHOW_WINDOW_CMD, SW_RESTORE, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE,
18 SWP_NOOWNERZORDER, SWP_NOZORDER, SetWindowPlacement, SetWindowPos, ShowWindowAsync,
19 WINDOWPLACEMENT, WS_EX_APPWINDOW, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
20};
21use windows::core::BOOL;
22
23use crate::display::Display;
24
25const MOVE_TIMEOUT: Duration = Duration::from_millis(1500);
26const POLL_INTERVAL: Duration = Duration::from_millis(20);
27const SCAN_INTERVAL: Duration = Duration::from_millis(250);
28
29static NEXT_MIGRATION_ID: AtomicU64 = AtomicU64::new(1);
30static WINDOW_CLAIMS: OnceLock<Mutex<WindowClaimRegistry>> = OnceLock::new();
31
32pub struct WindowMigration {
33 migration_id: u64,
34 entries: Arc<Mutex<Vec<WindowSnapshot>>>,
35 stop_scanner: Arc<AtomicBool>,
36 scanner: Option<JoinHandle<Vec<String>>>,
37 scanner_errors: Vec<String>,
38 restore_on_drop: bool,
39 target_monitor: HMONITOR,
40 target_rect: RECT,
41}
42
43struct WindowSnapshot {
44 hwnd: HWND,
45 pid: u32,
46 thread_id: u32,
47 class_name: String,
48 placement: WINDOWPLACEMENT,
49 outer_rect: RECT,
50 maximized: bool,
51 moved: bool,
52}
53
54// HWND values are system-wide window identities. The scanner and stop path
55// serialize access through the entries mutex and revalidate HWND/PID/thread/class
56// before every operation.
57unsafe impl Send for WindowSnapshot {}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
60struct WindowIdentity {
61 hwnd: isize,
62 pid: u32,
63 thread_id: u32,
64 class_name: String,
65}
66
67impl WindowIdentity {
68 fn from_snapshot(snapshot: &WindowSnapshot) -> Self {
69 Self {
70 hwnd: snapshot.hwnd.0 as isize,
71 pid: snapshot.pid,
72 thread_id: snapshot.thread_id,
73 class_name: snapshot.class_name.clone(),
74 }
75 }
76}
77
78#[derive(Debug, Eq, PartialEq)]
79struct WindowClaim {
80 owner: u64,
81 identity: WindowIdentity,
82}
83
84#[derive(Default)]
85struct WindowClaimRegistry {
86 claims: Vec<WindowClaim>,
87}
88
89impl WindowClaimRegistry {
90 fn claim(&mut self, owner: u64, identity: WindowIdentity) -> bool {
91 if let Some(claim) = self.claims.iter().find(|claim| claim.identity == identity) {
92 return claim.owner == owner;
93 }
94 self.claims.push(WindowClaim { owner, identity });
95 true
96 }
97
98 fn release(&mut self, owner: u64, identity: &WindowIdentity) {
99 self.claims
100 .retain(|claim| claim.owner != owner || claim.identity != *identity);
101 }
102
103 fn release_owner(&mut self, owner: u64) {
104 self.claims.retain(|claim| claim.owner != owner);
105 }
106}
107
108fn next_migration_id() -> u64 {
109 NEXT_MIGRATION_ID.fetch_add(1, Ordering::Relaxed)
110}
111
112fn with_window_claims<T>(operation: impl FnOnce(&mut WindowClaimRegistry) -> T) -> T {
113 let registry = WINDOW_CLAIMS.get_or_init(|| Mutex::new(WindowClaimRegistry::default()));
114 let mut claims = registry
115 .lock()
116 .unwrap_or_else(|poisoned| poisoned.into_inner());
117 operation(&mut claims)
118}
119
120fn claim_window(owner: u64, identity: WindowIdentity) -> bool {
121 with_window_claims(|claims| claims.claim(owner, identity))
122}
123
124fn release_window_claim(owner: u64, identity: &WindowIdentity) {
125 with_window_claims(|claims| claims.release(owner, identity));
126}
127
128fn release_owner_claims(owner: u64) {
129 with_window_claims(|claims| claims.release_owner(owner));
130}
131
132struct Enumeration {
133 target_monitor: HMONITOR,
134 current_pid: u32,
135 entries: Vec<WindowSnapshot>,
136 error: Option<String>,
137}
138
139impl WindowMigration {
140 pub fn start(target: &Display, source: &Display) -> Result<Self, String> {
141 let target_monitor = monitor_for_rect(target.rect, "target")?;
142 let source_monitor = monitor_for_rect(source.rect, "virtual source")?;
143 let migration_id = next_migration_id();
144 let mut entries = Vec::new();
145 for mut entry in enumerate_target_windows(target_monitor)? {
146 let identity = WindowIdentity::from_snapshot(&entry);
147 if !claim_window(migration_id, identity.clone()) {
148 continue;
149 }
150 let result = move_entry(
151 &mut entry,
152 target_monitor,
153 source_monitor,
154 target.rect,
155 source.rect,
156 );
157 if entry.moved {
158 entries.push(entry);
159 } else {
160 release_window_claim(migration_id, &identity);
161 }
162 if let Err(error) = result {
163 let mut migration = Self {
164 migration_id,
165 entries: Arc::new(Mutex::new(entries)),
166 stop_scanner: Arc::new(AtomicBool::new(true)),
167 scanner: None,
168 scanner_errors: Vec::new(),
169 restore_on_drop: true,
170 target_monitor,
171 target_rect: target.rect,
172 };
173 let rollback = migration.restore();
174 return Err(match rollback {
175 Ok(()) => error,
176 Err(rollback_error) => {
177 format!("{error}; rollback was incomplete: {rollback_error}")
178 }
179 });
180 }
181 }
182
183 let entries = Arc::new(Mutex::new(entries));
184 let stop_scanner = Arc::new(AtomicBool::new(false));
185 let scanner = match spawn_scanner(
186 migration_id,
187 Arc::clone(&entries),
188 Arc::clone(&stop_scanner),
189 target.rect,
190 source.rect,
191 ) {
192 Ok(scanner) => scanner,
193 Err(error) => {
194 let mut migration = Self {
195 migration_id,
196 entries,
197 stop_scanner,
198 scanner: None,
199 scanner_errors: Vec::new(),
200 restore_on_drop: true,
201 target_monitor,
202 target_rect: target.rect,
203 };
204 let rollback = migration.restore();
205 return Err(match rollback {
206 Ok(()) => error,
207 Err(rollback_error) => {
208 format!("{error}; rollback was incomplete: {rollback_error}")
209 }
210 });
211 }
212 };
213 Ok(Self {
214 migration_id,
215 entries,
216 stop_scanner,
217 scanner: Some(scanner),
218 scanner_errors: Vec::new(),
219 restore_on_drop: true,
220 target_monitor,
221 target_rect: target.rect,
222 })
223 }
224
225 fn stop_and_join_scanner(&mut self) -> Vec<String> {
226 self.stop_scanner.store(true, Ordering::Release);
227 match self.scanner.take() {
228 Some(scanner) => {
229 scanner.thread().unpark();
230 scanner
231 .join()
232 .unwrap_or_else(|_| vec!["window scanner thread panicked".into()])
233 }
234 None => Vec::new(),
235 }
236 }
237
238 pub(crate) fn refresh_topology(
239 &mut self,
240 target: &Display,
241 source: &Display,
242 ) -> Result<(), String> {
243 let target_monitor = monitor_for_rect(target.rect, "updated target")?;
244 monitor_for_rect(source.rect, "updated virtual source")?;
245
246 // A display-layout mutation invalidates the rectangles captured by the
247 // scanner. Stop it before publishing the replacement rectangles; errors
248 // produced by that expected transition must not poison eventual cleanup.
249 let _ = self.stop_and_join_scanner();
250 let dx = target
251 .rect
252 .left
253 .checked_sub(self.target_rect.left)
254 .ok_or_else(|| "updated target horizontal offset overflowed".to_string())?;
255 let dy = target
256 .rect
257 .top
258 .checked_sub(self.target_rect.top)
259 .ok_or_else(|| "updated target vertical offset overflowed".to_string())?;
260 let mut entries = self
261 .entries
262 .lock()
263 .unwrap_or_else(|poisoned| poisoned.into_inner());
264 shift_saved_rectangles(&mut entries, dx, dy)?;
265 drop(entries);
266 self.target_monitor = target_monitor;
267 self.target_rect = target.rect;
268 self.stop_scanner = Arc::new(AtomicBool::new(false));
269 let scanner = spawn_scanner(
270 self.migration_id,
271 Arc::clone(&self.entries),
272 Arc::clone(&self.stop_scanner),
273 target.rect,
274 source.rect,
275 )?;
276 self.scanner = Some(scanner);
277 Ok(())
278 }
279
280 pub(crate) fn prepare_restore(&mut self) {
281 let scanner_errors = self.stop_and_join_scanner();
282 for error in scanner_errors {
283 remember_error(&mut self.scanner_errors, error);
284 }
285 }
286
287 pub fn restore(&mut self) -> Result<(), String> {
288 self.prepare_restore();
289 let mut errors = std::mem::take(&mut self.scanner_errors);
290 let mut entries = match self.entries.lock() {
291 Ok(entries) => entries,
292 Err(poisoned) => {
293 errors.push("window migration state lock was poisoned".into());
294 poisoned.into_inner()
295 }
296 };
297 for entry in entries.iter_mut().rev() {
298 if !entry.moved {
299 continue;
300 }
301 if !same_window(entry) {
302 entry.moved = false;
303 continue;
304 }
305 if entry.maximized {
306 unsafe {
307 let _ = ShowWindowAsync(entry.hwnd, SW_RESTORE);
308 }
309 if !wait_for_zoomed_state(entry, false) {
310 errors.push(describe(
311 entry,
312 "maximized window could not be restored for migration",
313 ));
314 continue;
315 }
316 if let Err(error) = set_window_rect(
317 entry,
318 entry.outer_rect.left,
319 entry.outer_rect.top,
320 entry.outer_rect.right - entry.outer_rect.left,
321 entry.outer_rect.bottom - entry.outer_rect.top,
322 ) {
323 errors.push(describe(
324 entry,
325 &format!("maximized window return move failed: {error}"),
326 ));
327 continue;
328 }
329 if let Err(error) = align_normal_rect(entry, entry.placement.rcNormalPosition) {
330 errors.push(error);
331 continue;
332 }
333 if !wait_for_monitor(entry, self.target_monitor) {
334 errors.push(describe(
335 entry,
336 "maximized window did not return to its original monitor",
337 ));
338 continue;
339 }
340 if let Err(error) = unsafe { SetWindowPlacement(entry.hwnd, &entry.placement) } {
341 errors.push(describe(
342 entry,
343 &format!("maximized placement restore failed: {error}"),
344 ));
345 continue;
346 }
347 unsafe {
348 let _ = ShowWindowAsync(
349 entry.hwnd,
350 SHOW_WINDOW_CMD(entry.placement.showCmd as i32),
351 );
352 }
353 if !wait_for_zoomed_state(entry, true)
354 || !wait_for_normal_rect(entry, entry.placement.rcNormalPosition)
355 || !wait_for_monitor(entry, self.target_monitor)
356 {
357 errors.push(describe(entry, "maximized window state was not restored"));
358 continue;
359 }
360 entry.moved = false;
361 continue;
362 }
363
364 let width = entry.outer_rect.right - entry.outer_rect.left;
365 let height = entry.outer_rect.bottom - entry.outer_rect.top;
366 let mut exact = false;
367 for _ in 0..3 {
368 if let Err(error) = set_window_rect(
369 entry,
370 entry.outer_rect.left,
371 entry.outer_rect.top,
372 width,
373 height,
374 ) {
375 errors.push(describe(
376 entry,
377 &format!("SetWindowPos restore failed: {error}"),
378 ));
379 break;
380 }
381 if wait_for_outer_rect(entry, entry.outer_rect) {
382 exact = true;
383 break;
384 }
385 }
386 if exact && wait_for_monitor(entry, self.target_monitor) {
387 entry.moved = false;
388 } else {
389 errors.push(describe(entry, "window placement was not restored"));
390 }
391 }
392
393 release_owner_claims(self.migration_id);
394 self.restore_on_drop = false;
395 if errors.is_empty() {
396 Ok(())
397 } else {
398 Err(errors.join("; "))
399 }
400 }
401
402 pub fn reconcile_after_topology_change(
403 &mut self,
404 target: Option<&Display>,
405 ) -> Result<(), String> {
406 let target = target.ok_or_else(|| {
407 "restored target display could not be resolved by stable ID".to_string()
408 })?;
409 let dx = target
410 .rect
411 .left
412 .checked_sub(self.target_rect.left)
413 .ok_or_else(|| "restored target horizontal offset overflowed".to_string())?;
414 let dy = target
415 .rect
416 .top
417 .checked_sub(self.target_rect.top)
418 .ok_or_else(|| "restored target vertical offset overflowed".to_string())?;
419 let target_monitor = monitor_for_rect(target.rect, "restored target")?;
420 self.prepare_restore();
421 let mut errors = std::mem::take(&mut self.scanner_errors);
422 let mut entries = match self.entries.lock() {
423 Ok(entries) => entries,
424 Err(poisoned) => {
425 errors.push("window migration state lock was poisoned".into());
426 poisoned.into_inner()
427 }
428 };
429 shift_saved_rectangles(&mut entries, dx, dy)?;
430 self.target_rect = target.rect;
431 self.target_monitor = target_monitor;
432 for entry in entries.iter_mut().rev() {
433 if !same_window(entry) {
434 entry.moved = false;
435 continue;
436 }
437 if let Err(error) = unsafe { SetWindowPlacement(entry.hwnd, &entry.placement) } {
438 errors.push(describe(
439 entry,
440 &format!("final SetWindowPlacement failed: {error}"),
441 ));
442 continue;
443 }
444 unsafe {
445 let _ =
446 ShowWindowAsync(entry.hwnd, SHOW_WINDOW_CMD(entry.placement.showCmd as i32));
447 }
448
449 let state_restored = if entry.maximized {
450 wait_for_zoomed_state(entry, true)
451 } else {
452 wait_for_window_state(entry, false) && wait_for_zoomed_state(entry, false)
453 };
454 if !state_restored {
455 errors.push(describe(entry, "final window state was not restored"));
456 continue;
457 }
458
459 if !entry.maximized {
460 let width = entry.outer_rect.right - entry.outer_rect.left;
461 let height = entry.outer_rect.bottom - entry.outer_rect.top;
462 let mut exact = false;
463 for _ in 0..3 {
464 if let Err(error) = set_window_rect(
465 entry,
466 entry.outer_rect.left,
467 entry.outer_rect.top,
468 width,
469 height,
470 ) {
471 errors.push(describe(
472 entry,
473 &format!("final SetWindowPos failed: {error}"),
474 ));
475 break;
476 }
477 if wait_for_outer_rect(entry, entry.outer_rect) {
478 exact = true;
479 break;
480 }
481 }
482 if !exact {
483 errors.push(describe(entry, "final outer rectangle was not restored"));
484 continue;
485 }
486 }
487
488 if !wait_for_normal_rect(entry, entry.placement.rcNormalPosition)
489 || !wait_for_monitor(entry, self.target_monitor)
490 {
491 // Some app frameworks retain their virtual-display rectangle
492 // after the physical topology returns even though placement APIs
493 // report success. Reapply the saved restored rectangle first,
494 // then restore the original show state.
495 unsafe {
496 let _ = ShowWindowAsync(entry.hwnd, SW_RESTORE);
497 }
498 if !wait_for_zoomed_state(entry, false) {
499 errors.push(describe(
500 entry,
501 "final window could not be restored for retry",
502 ));
503 continue;
504 }
505 if let Err(error) = set_window_rect(
506 entry,
507 entry.outer_rect.left,
508 entry.outer_rect.top,
509 entry.outer_rect.right - entry.outer_rect.left,
510 entry.outer_rect.bottom - entry.outer_rect.top,
511 ) {
512 errors.push(describe(
513 entry,
514 &format!("final fallback move failed: {error}"),
515 ));
516 continue;
517 }
518 if let Err(error) = align_normal_rect(entry, entry.placement.rcNormalPosition) {
519 errors.push(error);
520 continue;
521 }
522 if !wait_for_monitor(entry, self.target_monitor) {
523 errors.push(describe(
524 entry,
525 "final fallback move missed the target monitor",
526 ));
527 continue;
528 }
529 if let Err(error) = unsafe { SetWindowPlacement(entry.hwnd, &entry.placement) } {
530 errors.push(describe(
531 entry,
532 &format!("final fallback placement failed: {error}"),
533 ));
534 continue;
535 }
536 unsafe {
537 let _ = ShowWindowAsync(
538 entry.hwnd,
539 SHOW_WINDOW_CMD(entry.placement.showCmd as i32),
540 );
541 }
542 let state_restored = if entry.maximized {
543 wait_for_zoomed_state(entry, true)
544 } else {
545 wait_for_window_state(entry, false) && wait_for_zoomed_state(entry, false)
546 };
547 if !state_restored
548 || !wait_for_normal_rect(entry, entry.placement.rcNormalPosition)
549 || !wait_for_monitor(entry, self.target_monitor)
550 {
551 errors.push(describe(entry, "final window placement was not restored"));
552 continue;
553 }
554 }
555 entry.moved = false;
556 }
557
558 if errors.is_empty() {
559 Ok(())
560 } else {
561 Err(errors.join("; "))
562 }
563 }
564}
565
566fn shift_saved_rectangles(entries: &mut [WindowSnapshot], dx: i32, dy: i32) -> Result<(), String> {
567 let shifted: Result<Vec<(RECT, RECT)>, String> = entries
568 .iter()
569 .map(|entry| {
570 Ok((
571 offset_rect(entry.outer_rect, dx, dy)?,
572 offset_rect(entry.placement.rcNormalPosition, dx, dy)?,
573 ))
574 })
575 .collect();
576 for (entry, (outer_rect, normal_rect)) in entries.iter_mut().zip(shifted?) {
577 entry.outer_rect = outer_rect;
578 entry.placement.rcNormalPosition = normal_rect;
579 }
580 Ok(())
581}
582
583fn offset_rect(rect: RECT, dx: i32, dy: i32) -> Result<RECT, String> {
584 Ok(RECT {
585 left: rect
586 .left
587 .checked_add(dx)
588 .ok_or_else(|| "window rectangle left coordinate overflowed".to_string())?,
589 top: rect
590 .top
591 .checked_add(dy)
592 .ok_or_else(|| "window rectangle top coordinate overflowed".to_string())?,
593 right: rect
594 .right
595 .checked_add(dx)
596 .ok_or_else(|| "window rectangle right coordinate overflowed".to_string())?,
597 bottom: rect
598 .bottom
599 .checked_add(dy)
600 .ok_or_else(|| "window rectangle bottom coordinate overflowed".to_string())?,
601 })
602}
603
604impl Drop for WindowMigration {
605 fn drop(&mut self) {
606 if self.restore_on_drop
607 && let Err(error) = self.restore()
608 {
609 eprintln!("warning: incomplete window restore: {error}");
610 }
611 }
612}
613
614fn spawn_scanner(
615 migration_id: u64,
616 entries: Arc<Mutex<Vec<WindowSnapshot>>>,
617 stop: Arc<AtomicBool>,
618 target_rect: RECT,
619 source_rect: RECT,
620) -> Result<JoinHandle<Vec<String>>, String> {
621 thread::Builder::new()
622 .name("sbms-window-scanner".into())
623 .spawn(move || {
624 let mut errors = Vec::new();
625 while !stop.load(Ordering::Acquire) {
626 thread::park_timeout(SCAN_INTERVAL);
627 if stop.load(Ordering::Acquire) {
628 break;
629 }
630 let target_monitor = match monitor_for_rect(target_rect, "scanner target") {
631 Ok(monitor) => monitor,
632 Err(error) => {
633 remember_error(&mut errors, error);
634 continue;
635 }
636 };
637 let source_monitor = match monitor_for_rect(source_rect, "scanner virtual source") {
638 Ok(monitor) => monitor,
639 Err(error) => {
640 remember_error(&mut errors, error);
641 continue;
642 }
643 };
644 let candidates = match enumerate_target_windows(target_monitor) {
645 Ok(candidates) => candidates,
646 Err(error) => {
647 remember_error(&mut errors, error);
648 continue;
649 }
650 };
651 let mut tracked = match entries.lock() {
652 Ok(entries) => entries,
653 Err(_) => {
654 remember_error(
655 &mut errors,
656 "window migration state lock was poisoned".into(),
657 );
658 break;
659 }
660 };
661 for mut candidate in candidates {
662 if stop.load(Ordering::Acquire) {
663 break;
664 }
665 let identity = WindowIdentity::from_snapshot(&candidate);
666 if !claim_window(migration_id, identity.clone()) {
667 continue;
668 }
669 if let Some(existing) = tracked
670 .iter_mut()
671 .find(|entry| same_identity(entry, &candidate))
672 {
673 let result = move_entry(
674 &mut candidate,
675 target_monitor,
676 source_monitor,
677 target_rect,
678 source_rect,
679 );
680 if candidate.moved {
681 existing.moved = true;
682 }
683 if let Err(error) = result {
684 remember_error(&mut errors, error);
685 }
686 if !candidate.moved {
687 release_window_claim(migration_id, &identity);
688 }
689 continue;
690 }
691 let result = move_entry(
692 &mut candidate,
693 target_monitor,
694 source_monitor,
695 target_rect,
696 source_rect,
697 );
698 if candidate.moved {
699 tracked.push(candidate);
700 } else {
701 release_window_claim(migration_id, &identity);
702 }
703 if let Err(error) = result {
704 remember_error(&mut errors, error);
705 }
706 }
707 }
708 errors
709 })
710 .map_err(|error| format!("failed to start window scanner: {error}"))
711}
712
713fn remember_error(errors: &mut Vec<String>, error: String) {
714 const MAX_REPORTED_ERRORS: usize = 16;
715 if errors.len() < MAX_REPORTED_ERRORS && !errors.contains(&error) {
716 errors.push(error);
717 }
718}
719
720fn enumerate_target_windows(target_monitor: HMONITOR) -> Result<Vec<WindowSnapshot>, String> {
721 let mut enumeration = Enumeration {
722 target_monitor,
723 current_pid: unsafe { GetCurrentProcessId() },
724 entries: Vec::new(),
725 error: None,
726 };
727 let result = unsafe {
728 EnumWindows(
729 Some(enumerate_window),
730 LPARAM((&mut enumeration as *mut Enumeration) as isize),
731 )
732 };
733 if let Some(error) = enumeration.error {
734 return Err(error);
735 }
736 result.map_err(|error| format!("EnumWindows failed: {error}"))?;
737 Ok(enumeration.entries)
738}
739
740unsafe extern "system" fn enumerate_window(hwnd: HWND, parameter: LPARAM) -> BOOL {
741 let state = unsafe { &mut *(parameter.0 as *mut Enumeration) };
742 match snapshot_window(hwnd, state.target_monitor, state.current_pid) {
743 Ok(Some(snapshot)) => state.entries.push(snapshot),
744 Ok(None) => {}
745 Err(error) => {
746 state.error = Some(error);
747 return BOOL(0);
748 }
749 }
750 BOOL(1)
751}
752
753fn snapshot_window(
754 hwnd: HWND,
755 target_monitor: HMONITOR,
756 current_pid: u32,
757) -> Result<Option<WindowSnapshot>, String> {
758 if !unsafe { IsWindowVisible(hwnd) }.as_bool()
759 || hwnd == unsafe { GetDesktopWindow() }
760 || hwnd == unsafe { GetShellWindow() }
761 {
762 return Ok(None);
763 }
764
765 let mut pid = 0;
766 let thread_id = unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
767 if thread_id == 0 || pid == current_pid {
768 return Ok(None);
769 }
770 if unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL) } != target_monitor {
771 return Ok(None);
772 }
773 if unsafe { IsIconic(hwnd) }.as_bool() {
774 return Ok(None);
775 }
776 let class_name = class_name(hwnd);
777 if matches!(
778 class_name.as_str(),
779 "Progman" | "WorkerW" | "Shell_TrayWnd" | "Shell_SecondaryTrayWnd"
780 ) {
781 return Ok(None);
782 }
783
784 let mut cloaked = 0u32;
785 if unsafe {
786 DwmGetWindowAttribute(
787 hwnd,
788 DWMWA_CLOAKED,
789 (&mut cloaked as *mut u32).cast(),
790 size_of::<u32>() as u32,
791 )
792 }
793 .is_ok()
794 && cloaked != 0
795 {
796 return Ok(None);
797 }
798
799 let extended_style = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) } as u32;
800 let auxiliary = extended_style & (WS_EX_TOOLWINDOW.0 | WS_EX_NOACTIVATE.0) != 0;
801 if auxiliary && extended_style & WS_EX_APPWINDOW.0 == 0 {
802 return Ok(None);
803 }
804
805 let mut outer_rect = RECT::default();
806 unsafe { GetWindowRect(hwnd, &mut outer_rect) }.map_err(|error| {
807 format!("GetWindowRect failed for pid {pid} class {class_name}: {error}")
808 })?;
809 if outer_rect.right <= outer_rect.left || outer_rect.bottom <= outer_rect.top {
810 return Ok(None);
811 }
812
813 let mut placement = WINDOWPLACEMENT {
814 length: size_of::<WINDOWPLACEMENT>() as u32,
815 ..Default::default()
816 };
817 unsafe { GetWindowPlacement(hwnd, &mut placement) }.map_err(|error| {
818 format!("GetWindowPlacement failed for pid {pid} class {class_name}: {error}")
819 })?;
820
821 Ok(Some(WindowSnapshot {
822 hwnd,
823 pid,
824 thread_id,
825 class_name,
826 placement,
827 outer_rect,
828 maximized: unsafe { IsZoomed(hwnd) }.as_bool(),
829 moved: false,
830 }))
831}
832
833fn move_entry(
834 entry: &mut WindowSnapshot,
835 target_monitor: HMONITOR,
836 source_monitor: HMONITOR,
837 target_rect: RECT,
838 source_rect: RECT,
839) -> Result<(), String> {
840 if !same_window(entry) {
841 return Ok(());
842 }
843 entry.moved = true;
844 if entry.maximized {
845 unsafe {
846 let _ = ShowWindowAsync(entry.hwnd, SW_RESTORE);
847 }
848 let restored = wait_for_zoomed_state(entry, false);
849 if !restored || !wait_for_monitor(entry, target_monitor) {
850 return Err(describe(
851 entry,
852 "window could not enter a stable restored state for migration",
853 ));
854 }
855 let current = current_rect(entry)?;
856 entry.outer_rect = current;
857 let mut virtual_placement = window_placement(entry)?;
858 virtual_placement.rcNormalPosition = translated_rect(
859 entry,
860 virtual_placement.rcNormalPosition,
861 target_rect,
862 source_rect,
863 )?;
864 virtual_placement.showCmd = SW_RESTORE.0 as u32;
865 unsafe { SetWindowPlacement(entry.hwnd, &virtual_placement) }
866 .map_err(|error| describe(entry, &format!("restored window move failed: {error}")))?;
867 unsafe {
868 let _ = ShowWindowAsync(entry.hwnd, SW_RESTORE);
869 }
870 if !wait_for_monitor(entry, source_monitor) {
871 // Chromium windows can report success from SetWindowPlacement while
872 // leaving both their outer and normal rectangles unchanged. Force the
873 // restored window across with the same SetWindowPos/alignment sequence
874 // used by the return path before treating the migration as failed.
875 let moved_outer = translated_rect(entry, current, target_rect, source_rect)?;
876 set_window_rect(
877 entry,
878 moved_outer.left,
879 moved_outer.top,
880 moved_outer.right - moved_outer.left,
881 moved_outer.bottom - moved_outer.top,
882 )
883 .map_err(|error| {
884 describe(
885 entry,
886 &format!("restored window fallback move failed: {error}"),
887 )
888 })?;
889 align_normal_rect(entry, virtual_placement.rcNormalPosition)?;
890 if !wait_for_monitor(entry, source_monitor) {
891 let actual = current_rect(entry).unwrap_or_default();
892 let normal = window_placement(entry)
893 .map(|placement| placement.rcNormalPosition)
894 .unwrap_or_default();
895 return Err(describe(
896 entry,
897 &format!(
898 "restored window did not move to the virtual display \
899 (outer={},{},{},{}, normal={},{},{},{})",
900 actual.left,
901 actual.top,
902 actual.right,
903 actual.bottom,
904 normal.left,
905 normal.top,
906 normal.right,
907 normal.bottom,
908 ),
909 ));
910 }
911 }
912 virtual_placement.flags = entry.placement.flags;
913 virtual_placement.showCmd = entry.placement.showCmd;
914 unsafe { SetWindowPlacement(entry.hwnd, &virtual_placement) }
915 .map_err(|error| describe(entry, &format!("window state move failed: {error}")))?;
916 unsafe {
917 let _ = ShowWindowAsync(entry.hwnd, SHOW_WINDOW_CMD(entry.placement.showCmd as i32));
918 }
919 let state_restored = wait_for_zoomed_state(entry, true);
920 if !state_restored || !wait_for_monitor(entry, source_monitor) {
921 return Err(describe(
922 entry,
923 "window did not restore its state on the virtual display",
924 ));
925 }
926 return Ok(());
927 }
928
929 let width = entry.outer_rect.right - entry.outer_rect.left;
930 let height = entry.outer_rect.bottom - entry.outer_rect.top;
931 let moved_width = width.min(source_rect.right - source_rect.left).max(1);
932 let moved_height = height.min(source_rect.bottom - source_rect.top).max(1);
933 let destination = translated_position(
934 entry,
935 entry.outer_rect.left,
936 entry.outer_rect.top,
937 moved_width,
938 moved_height,
939 target_rect,
940 source_rect,
941 )?;
942 unsafe {
943 SetWindowPos(
944 entry.hwnd,
945 None,
946 destination.x,
947 destination.y,
948 moved_width,
949 moved_height,
950 SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER,
951 )
952 }
953 .map_err(|error| describe(entry, &format!("window move failed: {error}")))?;
954
955 if !wait_for_monitor(entry, source_monitor) {
956 return Err(describe(
957 entry,
958 "window did not move to the virtual display",
959 ));
960 }
961 Ok(())
962}
963
964fn translated_position(
965 entry: &WindowSnapshot,
966 left: i32,
967 top: i32,
968 width: i32,
969 height: i32,
970 target_rect: RECT,
971 source_rect: RECT,
972) -> Result<POINT, String> {
973 // main() makes this process per-monitor-v2 aware before any display or
974 // window API call, so GetWindowRect and SetWindowPos share physical
975 // virtual-screen coordinates across monitors.
976 let relative_x = left
977 .checked_sub(target_rect.left)
978 .ok_or_else(|| describe(entry, "horizontal window coordinate overflow"))?;
979 let relative_y = top
980 .checked_sub(target_rect.top)
981 .ok_or_else(|| describe(entry, "vertical window coordinate overflow"))?;
982 let max_x = (source_rect.right - source_rect.left - width).max(0);
983 let max_y = (source_rect.bottom - source_rect.top - height).max(0);
984 let x = source_rect
985 .left
986 .checked_add(relative_x.clamp(0, max_x))
987 .ok_or_else(|| describe(entry, "horizontal window coordinate overflow"))?;
988 let y = source_rect
989 .top
990 .checked_add(relative_y.clamp(0, max_y))
991 .ok_or_else(|| describe(entry, "vertical window coordinate overflow"))?;
992 Ok(POINT { x, y })
993}
994
995fn translated_rect(
996 entry: &WindowSnapshot,
997 rect: RECT,
998 target_rect: RECT,
999 source_rect: RECT,
1000) -> Result<RECT, String> {
1001 let width = (rect.right - rect.left)
1002 .min(source_rect.right - source_rect.left)
1003 .max(1);
1004 let height = (rect.bottom - rect.top)
1005 .min(source_rect.bottom - source_rect.top)
1006 .max(1);
1007 let top_left = translated_position(
1008 entry,
1009 rect.left,
1010 rect.top,
1011 width,
1012 height,
1013 target_rect,
1014 source_rect,
1015 )?;
1016 Ok(RECT {
1017 left: top_left.x,
1018 top: top_left.y,
1019 right: top_left.x + width,
1020 bottom: top_left.y + height,
1021 })
1022}
1023
1024fn wait_for_normal_rect(entry: &WindowSnapshot, expected: RECT) -> bool {
1025 let deadline = Instant::now() + MOVE_TIMEOUT;
1026 loop {
1027 if !same_window(entry) {
1028 return true;
1029 }
1030 let mut placement = WINDOWPLACEMENT {
1031 length: size_of::<WINDOWPLACEMENT>() as u32,
1032 ..Default::default()
1033 };
1034 if unsafe { GetWindowPlacement(entry.hwnd, &mut placement) }.is_ok()
1035 && placement.rcNormalPosition == expected
1036 {
1037 return true;
1038 }
1039 if Instant::now() >= deadline {
1040 return false;
1041 }
1042 thread::sleep(POLL_INTERVAL);
1043 }
1044}
1045
1046fn window_placement(entry: &WindowSnapshot) -> Result<WINDOWPLACEMENT, String> {
1047 let mut placement = WINDOWPLACEMENT {
1048 length: size_of::<WINDOWPLACEMENT>() as u32,
1049 ..Default::default()
1050 };
1051 unsafe { GetWindowPlacement(entry.hwnd, &mut placement) }
1052 .map_err(|error| describe(entry, &format!("GetWindowPlacement failed: {error}")))?;
1053 Ok(placement)
1054}
1055
1056fn align_normal_rect(entry: &WindowSnapshot, expected: RECT) -> Result<(), String> {
1057 for _ in 0..3 {
1058 let placement = window_placement(entry)?;
1059 if placement.rcNormalPosition == expected {
1060 return Ok(());
1061 }
1062 let outer = current_rect(entry)?;
1063 let current = placement.rcNormalPosition;
1064 set_window_rect(
1065 entry,
1066 outer.left + expected.left - current.left,
1067 outer.top + expected.top - current.top,
1068 outer.right - outer.left + (expected.right - expected.left)
1069 - (current.right - current.left),
1070 outer.bottom - outer.top + (expected.bottom - expected.top)
1071 - (current.bottom - current.top),
1072 )
1073 .map_err(|error| {
1074 describe(
1075 entry,
1076 &format!("normal rectangle alignment failed: {error}"),
1077 )
1078 })?;
1079 if wait_for_normal_rect(entry, expected) {
1080 return Ok(());
1081 }
1082 }
1083 Err(describe(entry, "normal rectangle alignment did not settle"))
1084}
1085
1086fn wait_for_outer_rect(entry: &WindowSnapshot, expected: RECT) -> bool {
1087 let deadline = Instant::now() + MOVE_TIMEOUT;
1088 loop {
1089 if !same_window(entry) {
1090 return true;
1091 }
1092 if current_rect(entry).is_ok_and(|rect| rect == expected) {
1093 return true;
1094 }
1095 if Instant::now() >= deadline {
1096 return false;
1097 }
1098 thread::sleep(POLL_INTERVAL);
1099 }
1100}
1101
1102fn wait_for_window_state(entry: &WindowSnapshot, minimized: bool) -> bool {
1103 let deadline = Instant::now() + MOVE_TIMEOUT;
1104 loop {
1105 if !same_window(entry) {
1106 return true;
1107 }
1108 if unsafe { IsIconic(entry.hwnd) }.as_bool() == minimized {
1109 return true;
1110 }
1111 if Instant::now() >= deadline {
1112 return false;
1113 }
1114 thread::sleep(POLL_INTERVAL);
1115 }
1116}
1117
1118fn wait_for_zoomed_state(entry: &WindowSnapshot, maximized: bool) -> bool {
1119 let deadline = Instant::now() + MOVE_TIMEOUT;
1120 loop {
1121 if !same_window(entry) {
1122 return true;
1123 }
1124 if unsafe { IsZoomed(entry.hwnd) }.as_bool() == maximized {
1125 return true;
1126 }
1127 if Instant::now() >= deadline {
1128 return false;
1129 }
1130 thread::sleep(POLL_INTERVAL);
1131 }
1132}
1133
1134fn wait_for_monitor(entry: &WindowSnapshot, expected: HMONITOR) -> bool {
1135 let deadline = Instant::now() + MOVE_TIMEOUT;
1136 loop {
1137 if !same_window(entry) {
1138 return true;
1139 }
1140 if unsafe { MonitorFromWindow(entry.hwnd, MONITOR_DEFAULTTONULL) } == expected {
1141 return true;
1142 }
1143 if Instant::now() >= deadline {
1144 return false;
1145 }
1146 thread::sleep(POLL_INTERVAL);
1147 }
1148}
1149
1150fn same_window(entry: &WindowSnapshot) -> bool {
1151 if !unsafe { IsWindow(Some(entry.hwnd)) }.as_bool() {
1152 return false;
1153 }
1154 let mut pid = 0;
1155 let thread_id = unsafe { GetWindowThreadProcessId(entry.hwnd, Some(&mut pid)) };
1156 thread_id == entry.thread_id && pid == entry.pid && class_name(entry.hwnd) == entry.class_name
1157}
1158
1159fn same_identity(left: &WindowSnapshot, right: &WindowSnapshot) -> bool {
1160 left.hwnd == right.hwnd
1161 && left.pid == right.pid
1162 && left.thread_id == right.thread_id
1163 && left.class_name == right.class_name
1164}
1165
1166fn monitor_for_rect(rect: RECT, label: &str) -> Result<HMONITOR, String> {
1167 let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONULL) };
1168 if monitor.0.is_null() {
1169 Err(format!("{label} rectangle is not attached to a monitor"))
1170 } else {
1171 Ok(monitor)
1172 }
1173}
1174
1175fn class_name(hwnd: HWND) -> String {
1176 let mut value = [0u16; 256];
1177 let length = unsafe { GetClassNameW(hwnd, &mut value) };
1178 String::from_utf16_lossy(&value[..length.max(0) as usize])
1179}
1180
1181fn current_rect(entry: &WindowSnapshot) -> Result<RECT, String> {
1182 let mut rect = RECT::default();
1183 unsafe { GetWindowRect(entry.hwnd, &mut rect) }
1184 .map_err(|error| describe(entry, &format!("GetWindowRect failed: {error}")))?;
1185 Ok(rect)
1186}
1187
1188fn set_window_rect(
1189 entry: &WindowSnapshot,
1190 left: i32,
1191 top: i32,
1192 width: i32,
1193 height: i32,
1194) -> windows::core::Result<()> {
1195 unsafe {
1196 SetWindowPos(
1197 entry.hwnd,
1198 None,
1199 left,
1200 top,
1201 width,
1202 height,
1203 SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER,
1204 )
1205 }
1206}
1207
1208fn describe(entry: &WindowSnapshot, message: &str) -> String {
1209 format!(
1210 "{message} (pid {}, class {}, maximized={})",
1211 entry.pid, entry.class_name, entry.maximized
1212 )
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::{RECT, WindowClaimRegistry, WindowIdentity, offset_rect};
1218
1219 fn identity(hwnd: isize, pid: u32, thread_id: u32, class_name: &str) -> WindowIdentity {
1220 WindowIdentity {
1221 hwnd,
1222 pid,
1223 thread_id,
1224 class_name: class_name.into(),
1225 }
1226 }
1227
1228 #[test]
1229 fn a_window_can_only_be_claimed_by_one_owner() {
1230 let mut claims = WindowClaimRegistry::default();
1231 let window = identity(42, 100, 200, "EditorWindow");
1232
1233 assert!(claims.claim(1, window.clone()));
1234 assert!(claims.claim(1, window.clone()));
1235 assert!(!claims.claim(2, window));
1236 assert_eq!(claims.claims.len(), 1);
1237 }
1238
1239 #[test]
1240 fn reused_hwnd_with_a_different_identity_is_not_blocked() {
1241 let mut claims = WindowClaimRegistry::default();
1242 assert!(claims.claim(1, identity(42, 100, 200, "EditorWindow")));
1243
1244 assert!(claims.claim(2, identity(42, 101, 200, "EditorWindow")));
1245 assert!(claims.claim(2, identity(42, 100, 201, "EditorWindow")));
1246 assert!(claims.claim(2, identity(42, 100, 200, "DialogWindow")));
1247 }
1248
1249 #[test]
1250 fn releasing_one_identity_keeps_the_owners_other_claims() {
1251 let mut claims = WindowClaimRegistry::default();
1252 let first = identity(42, 100, 200, "EditorWindow");
1253 let second = identity(43, 100, 200, "EditorWindow");
1254 assert!(claims.claim(1, first.clone()));
1255 assert!(claims.claim(1, second.clone()));
1256
1257 claims.release(1, &first);
1258
1259 assert!(claims.claim(2, first));
1260 assert!(!claims.claim(2, second));
1261 }
1262
1263 #[test]
1264 fn releasing_an_owner_releases_all_of_its_windows_only() {
1265 let mut claims = WindowClaimRegistry::default();
1266 let first = identity(42, 100, 200, "EditorWindow");
1267 let second = identity(43, 100, 200, "EditorWindow");
1268 let other = identity(44, 300, 400, "BrowserWindow");
1269 assert!(claims.claim(1, first.clone()));
1270 assert!(claims.claim(1, second.clone()));
1271 assert!(claims.claim(2, other.clone()));
1272
1273 claims.release_owner(1);
1274
1275 assert!(claims.claim(3, first));
1276 assert!(claims.claim(3, second));
1277 assert!(!claims.claim(3, other));
1278 }
1279
1280 #[test]
1281 fn saved_window_rectangles_follow_target_position_changes() {
1282 let shifted = offset_rect(
1283 RECT {
1284 left: -100,
1285 top: 50,
1286 right: 900,
1287 bottom: 650,
1288 },
1289 -1920,
1290 1080,
1291 )
1292 .unwrap();
1293
1294 assert_eq!(shifted.left, -2020);
1295 assert_eq!(shifted.top, 1130);
1296 assert_eq!(shifted.right, -1020);
1297 assert_eq!(shifted.bottom, 1730);
1298 }
1299}
1300