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.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/window_migration.rs43.8 KB
1
use std::mem::size_of;2
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};3
use std::sync::{Arc, Mutex, OnceLock};4
use std::thread;5
use std::thread::JoinHandle;6
use std::time::{Duration, Instant};7
8
use windows::Win32::Foundation::{HWND, LPARAM, POINT, RECT};9
use windows::Win32::Graphics::Dwm::{DWMWA_CLOAKED, DwmGetWindowAttribute};10
use windows::Win32::Graphics::Gdi::{11
HMONITOR, MONITOR_DEFAULTTONULL, MonitorFromRect, MonitorFromWindow,12
};13
use windows::Win32::System::Threading::GetCurrentProcessId;14
use 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
};21
use windows::core::BOOL;22
23
use crate::display::Display;24
25
const MOVE_TIMEOUT: Duration = Duration::from_millis(1500);26
const POLL_INTERVAL: Duration = Duration::from_millis(20);27
const SCAN_INTERVAL: Duration = Duration::from_millis(250);28
29
static NEXT_MIGRATION_ID: AtomicU64 = AtomicU64::new(1);30
static WINDOW_CLAIMS: OnceLock<Mutex<WindowClaimRegistry>> = OnceLock::new();31
32
pub 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
43
struct 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 path55
// serialize access through the entries mutex and revalidate HWND/PID/thread/class56
// before every operation.57
unsafe impl Send for WindowSnapshot {}58
59
#[derive(Clone, Debug, Eq, PartialEq)]60
struct WindowIdentity {61
hwnd: isize,62
pid: u32,63
thread_id: u32,64
class_name: String,65
}66
67
impl 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)]79
struct WindowClaim {80
owner: u64,81
identity: WindowIdentity,82
}83
84
#[derive(Default)]85
struct WindowClaimRegistry {86
claims: Vec<WindowClaim>,87
}88
89
impl 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
true96
}97
98
fn release(&mut self, owner: u64, identity: &WindowIdentity) {99
self.claims100
.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
108
fn next_migration_id() -> u64 {109
NEXT_MIGRATION_ID.fetch_add(1, Ordering::Relaxed)110
}111
112
fn 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 = registry115
.lock()116
.unwrap_or_else(|poisoned| poisoned.into_inner());117
operation(&mut claims)118
}119
120
fn claim_window(owner: u64, identity: WindowIdentity) -> bool {121
with_window_claims(|claims| claims.claim(owner, identity))122
}123
124
fn release_window_claim(owner: u64, identity: &WindowIdentity) {125
with_window_claims(|claims| claims.release(owner, identity));126
}127
128
fn release_owner_claims(owner: u64) {129
with_window_claims(|claims| claims.release_owner(owner));130
}131
132
struct Enumeration {133
target_monitor: HMONITOR,134
current_pid: u32,135
entries: Vec<WindowSnapshot>,136
error: Option<String>,137
}138
139
impl 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
scanner231
.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 the247
// scanner. Stop it before publishing the replacement rectangles; errors248
// produced by that expected transition must not poison eventual cleanup.249
let _ = self.stop_and_join_scanner();250
let dx = target251
.rect252
.left253
.checked_sub(self.target_rect.left)254
.ok_or_else(|| "updated target horizontal offset overflowed".to_string())?;255
let dy = target256
.rect257
.top258
.checked_sub(self.target_rect.top)259
.ok_or_else(|| "updated target vertical offset overflowed".to_string())?;260
let mut entries = self261
.entries262
.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 = target410
.rect411
.left412
.checked_sub(self.target_rect.left)413
.ok_or_else(|| "restored target horizontal offset overflowed".to_string())?;414
let dy = target415
.rect416
.top417
.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 rectangle492
// after the physical topology returns even though placement APIs493
// 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_restored548
|| !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
566
fn shift_saved_rectangles(entries: &mut [WindowSnapshot], dx: i32, dy: i32) -> Result<(), String> {567
let shifted: Result<Vec<(RECT, RECT)>, String> = entries568
.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
583
fn offset_rect(rect: RECT, dx: i32, dy: i32) -> Result<RECT, String> {584
Ok(RECT {585
left: rect586
.left587
.checked_add(dx)588
.ok_or_else(|| "window rectangle left coordinate overflowed".to_string())?,589
top: rect590
.top591
.checked_add(dy)592
.ok_or_else(|| "window rectangle top coordinate overflowed".to_string())?,593
right: rect594
.right595
.checked_add(dx)596
.ok_or_else(|| "window rectangle right coordinate overflowed".to_string())?,597
bottom: rect598
.bottom599
.checked_add(dy)600
.ok_or_else(|| "window rectangle bottom coordinate overflowed".to_string())?,601
})602
}603
604
impl Drop for WindowMigration {605
fn drop(&mut self) {606
if self.restore_on_drop607
&& let Err(error) = self.restore()608
{609
eprintln!("warning: incomplete window restore: {error}");610
}611
}612
}613
614
fn 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) = tracked670
.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
errors709
})710
.map_err(|error| format!("failed to start window scanner: {error}"))711
}712
713
fn 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
720
fn 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
740
unsafe 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
753
fn 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 != 0795
{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
833
fn 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 while872
// leaving both their outer and normal rectangles unchanged. Force the873
// restored window across with the same SetWindowPos/alignment sequence874
// 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
964
fn 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 or974
// window API call, so GetWindowRect and SetWindowPos share physical975
// virtual-screen coordinates across monitors.976
let relative_x = left977
.checked_sub(target_rect.left)978
.ok_or_else(|| describe(entry, "horizontal window coordinate overflow"))?;979
let relative_y = top980
.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_rect985
.left986
.checked_add(relative_x.clamp(0, max_x))987
.ok_or_else(|| describe(entry, "horizontal window coordinate overflow"))?;988
let y = source_rect989
.top990
.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
995
fn 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
1024
fn 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 == expected1036
{1037
return true;1038
}1039
if Instant::now() >= deadline {1040
return false;1041
}1042
thread::sleep(POLL_INTERVAL);1043
}1044
}1045
1046
fn 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
1056
fn 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
1086
fn 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
1102
fn 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
1118
fn 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
1134
fn 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
1150
fn 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_name1157
}1158
1159
fn same_identity(left: &WindowSnapshot, right: &WindowSnapshot) -> bool {1160
left.hwnd == right.hwnd1161
&& left.pid == right.pid1162
&& left.thread_id == right.thread_id1163
&& left.class_name == right.class_name1164
}1165
1166
fn 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
1175
fn 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
1181
fn 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
1188
fn 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
1208
fn describe(entry: &WindowSnapshot, message: &str) -> String {1209
format!(1210
"{message} (pid {}, class {}, maximized={})",1211
entry.pid, entry.class_name, entry.maximized1212
)1213
}1214
1215
#[cfg(test)]1216
mod 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