gardesk/ers / fa8412b

Browse files

Replace SLS overlays with NSWindow + sharingType=.none

BorderMap now stores nswindow_overlay::OverlayWindow per target. The
event-processing loop moves from a background thread to a CFRunLoopTimer
on the main thread, since AppKit calls (NSWindow, CALayer) must originate
from the main thread.

Verified empirically: screencapture -l <ers_wid> returns &#39;could not
create image from window&#39; for all 5 overlays — Tahoe&#39;s screencaptureui
honors NSWindow.sharingType where it ignores SLS sharing-state for
SLS-only windows. Full-screen screencapture cleanly captures app
contents without the borders blocking them.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
fa8412b2dd49242ca6af15715fdd23473ca7fd55
Parents
72496e3
Tree
3dd8e2b

1 changed file

StatusFile+-
M src/main.rs 270 264
src/main.rsmodified
@@ -25,12 +25,31 @@ const WINDOW_TAG_IGNORES_CYCLE: u64 = 1 << 18;
2525
 const WINDOW_TAG_MODAL: u64 = 1 << 31;
2626
 const WINDOW_TAG_REAL_SURFACE: u64 = 1 << 58;
2727
 
28
-/// Per-overlay state: the connection it was created on + its wid.
28
+/// Per-overlay state: an NSWindow drawing the rounded-rect border via
29
+/// CAShapeLayer. Replaces the old SLS-only overlay window — see
30
+/// nswindow_overlay.rs for the rationale (screencaptureui on Tahoe
31
+/// only honors NSWindow.sharingType, not SLS sharing-state nor tag
32
+/// bits, for raw SLS-only windows).
2933
 struct Overlay {
30
-    cid: CGSConnectionID,
31
-    wid: u32,
32
-    bounds: CGRect,
33
-    scale: f64,
34
+    window: nswindow_overlay::OverlayWindow,
35
+}
36
+
37
+impl Overlay {
38
+    fn wid(&self) -> u32 {
39
+        self.window.wid()
40
+    }
41
+    fn bounds(&self) -> CGRect {
42
+        CGRect {
43
+            origin: CGPoint {
44
+                x: self.window.bounds_cg_x,
45
+                y: self.window.bounds_cg_y,
46
+            },
47
+            size: CGSize {
48
+                width: self.window.bounds_cg_w,
49
+                height: self.window.bounds_cg_h,
50
+            },
51
+        }
52
+    }
3453
 }
3554
 
3655
 fn window_area(bounds: CGRect) -> f64 {
@@ -234,12 +253,19 @@ struct BorderMap {
234253
     active_color: (f64, f64, f64, f64),
235254
     inactive_color: (f64, f64, f64, f64),
236255
     active_only: bool,
256
+    mtm: objc2::MainThreadMarker,
237257
 }
238258
 
239259
 impl BorderMap {
240
-    fn new(cid: CGSConnectionID, own_pid: i32, border_width: f64) -> Self {
260
+    fn new(
261
+        cid: CGSConnectionID,
262
+        own_pid: i32,
263
+        border_width: f64,
264
+        mtm: objc2::MainThreadMarker,
265
+    ) -> Self {
241266
         Self {
242267
             overlays: HashMap::new(),
268
+            mtm,
243269
             main_cid: cid,
244270
             own_pid,
245271
             border_width,
@@ -260,7 +286,7 @@ impl BorderMap {
260286
     }
261287
 
262288
     fn is_overlay(&self, wid: u32) -> bool {
263
-        self.overlays.values().any(|o| o.wid == wid)
289
+        self.overlays.values().any(|o| o.wid() == wid)
264290
     }
265291
 
266292
     /// Add border using the standard filtering path.
@@ -338,63 +364,39 @@ impl BorderMap {
338364
         }
339365
 
340366
         let color = self.color_for(target_wid);
341
-        if let Some((cid, wid, bounds, scale)) = create_overlay(
342
-            self.main_cid,
343
-            target_wid,
367
+        if let Some(window) = nswindow_overlay::OverlayWindow::new(
368
+            bounds.origin.x,
369
+            bounds.origin.y,
370
+            bounds.size.width,
371
+            bounds.size.height,
344372
             self.border_width,
345373
             self.radius,
346374
             color,
375
+            self.mtm,
347376
         ) {
348
-            self.overlays.insert(
349
-                target_wid,
350
-                Overlay {
351
-                    cid,
352
-                    wid,
353
-                    bounds,
354
-                    scale,
355
-                },
356
-            );
377
+            window.order_above(target_wid);
378
+            self.overlays.insert(target_wid, Overlay { window });
357379
         }
358380
     }
359381
 
360382
     fn remove_all(&mut self) {
361
-        let wids: Vec<u32> = self.overlays.keys().copied().collect();
362
-        for wid in wids {
363
-            self.remove(wid);
364
-        }
383
+        // OverlayWindow's Drop closes the NSWindow.
384
+        self.overlays.clear();
365385
     }
366386
 
367387
     fn remove(&mut self, target_wid: u32) {
368
-        if let Some(overlay) = self.overlays.remove(&target_wid) {
369
-            unsafe {
370
-                // Move off-screen first (most reliable hide on Tahoe)
371
-                let offscreen = CGPoint {
372
-                    x: -99999.0,
373
-                    y: -99999.0,
374
-                };
375
-                SLSMoveWindow(overlay.cid, overlay.wid, &offscreen);
376
-                SLSSetWindowAlpha(overlay.cid, overlay.wid, 0.0);
377
-                SLSOrderWindow(overlay.cid, overlay.wid, 0, 0);
378
-                SLSReleaseWindow(overlay.cid, overlay.wid);
379
-                if overlay.cid != self.main_cid {
380
-                    SLSReleaseConnection(overlay.cid);
381
-                }
382
-            }
383
-        }
388
+        // OverlayWindow's Drop closes the NSWindow.
389
+        self.overlays.remove(&target_wid);
384390
     }
385391
 
386392
     /// Reconcile a tracked overlay against its target window.
387393
     fn sync_overlay(&mut self, target_wid: u32) -> bool {
388
-        let Some((overlay_cid, overlay_wid, overlay_bounds, overlay_scale)) = self
389
-            .overlays
390
-            .get(&target_wid)
391
-            .map(|overlay| (overlay.cid, overlay.wid, overlay.bounds, overlay.scale))
392
-        else {
394
+        if !self.overlays.contains_key(&target_wid) {
393395
             return false;
394
-        };
396
+        }
395397
 
398
+        let mut bounds = CGRect::default();
396399
         unsafe {
397
-            let mut bounds = CGRect::default();
398400
             if SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds) != kCGErrorSuccess {
399401
                 return false;
400402
             }
@@ -408,42 +410,30 @@ impl BorderMap {
408410
                 self.remove(target_wid);
409411
                 return true;
410412
             }
413
+        }
411414
 
412
-            let scale = display_scale_for_bounds(bounds);
413
-            if size_changed(overlay_bounds, bounds) || (scale - overlay_scale).abs() > SCALE_EPSILON
414
-            {
415
+        if let Some(overlay) = self.overlays.get_mut(&target_wid) {
416
+            let prev = overlay.bounds();
417
+            if size_changed(prev, bounds) || origin_changed(prev, bounds) {
415418
                 debug!(
416
-                    "[sync_overlay] target={} geometry changed bounds=({:.1},{:.1},{:.1},{:.1}) -> ({:.1},{:.1},{:.1},{:.1}) scale {:.2} -> {:.2}, recreating",
419
+                    "[sync_overlay] target={} geometry ({:.1},{:.1},{:.1},{:.1}) -> ({:.1},{:.1},{:.1},{:.1})",
417420
                     target_wid,
418
-                    overlay_bounds.origin.x,
419
-                    overlay_bounds.origin.y,
420
-                    overlay_bounds.size.width,
421
-                    overlay_bounds.size.height,
421
+                    prev.origin.x,
422
+                    prev.origin.y,
423
+                    prev.size.width,
424
+                    prev.size.height,
425
+                    bounds.origin.x,
426
+                    bounds.origin.y,
427
+                    bounds.size.width,
428
+                    bounds.size.height
429
+                );
430
+                overlay.window.set_bounds(
422431
                     bounds.origin.x,
423432
                     bounds.origin.y,
424433
                     bounds.size.width,
425434
                     bounds.size.height,
426
-                    overlay_scale,
427
-                    scale
428435
                 );
429
-                self.recreate(target_wid);
430
-                return true;
431
-            }
432
-
433
-            if origin_changed(overlay_bounds, bounds) {
434
-                let bw = self.border_width;
435
-                let origin = CGPoint {
436
-                    x: bounds.origin.x - bw,
437
-                    y: bounds.origin.y - bw,
438
-                };
439
-                SLSMoveWindow(overlay_cid, overlay_wid, &origin);
440
-            }
441
-
442
-            if let Some(overlay) = self.overlays.get_mut(&target_wid) {
443
-                overlay.bounds = bounds;
444
-                overlay.scale = scale;
445
-                overlay.cid = overlay_cid;
446
-                overlay.wid = overlay_wid;
436
+                overlay.window.order_above(target_wid);
447437
             }
448438
         }
449439
 
@@ -461,33 +451,22 @@ impl BorderMap {
461451
         changed
462452
     }
463453
 
464
-    /// Recreate overlay at new size.
454
+    /// With NSWindow.setFrame_display we no longer need a destroy-and-
455
+    /// recreate path on resize. Kept as a thin alias so existing call
456
+    /// sites keep working.
465457
     fn recreate(&mut self, target_wid: u32) {
466
-        if !self.overlays.contains_key(&target_wid) {
467
-            return;
468
-        }
469
-        self.remove(target_wid);
470
-        self.add_fresh(target_wid);
471
-        if self.active_only && target_wid != self.focused_wid {
472
-            self.hide(target_wid);
473
-        }
474
-        self.subscribe_target(target_wid);
458
+        self.sync_overlay(target_wid);
475459
     }
476460
 
477461
     fn hide(&self, target_wid: u32) {
478462
         if let Some(o) = self.overlays.get(&target_wid) {
479
-            unsafe {
480
-                SLSOrderWindow(o.cid, o.wid, 0, 0);
481
-            }
463
+            o.window.order_out();
482464
         }
483465
     }
484466
 
485467
     fn unhide(&self, target_wid: u32) {
486468
         if let Some(o) = self.overlays.get(&target_wid) {
487
-            unsafe {
488
-                SLSSetWindowLevel(o.cid, o.wid, 0);
489
-                SLSOrderWindow(o.cid, o.wid, 1, target_wid);
490
-            }
469
+            o.window.order_above(target_wid);
491470
         }
492471
     }
493472
 
@@ -514,25 +493,7 @@ impl BorderMap {
514493
     /// Redraw an existing overlay with a new color (no destroy/recreate).
515494
     fn redraw(&self, target_wid: u32) {
516495
         if let Some(overlay) = self.overlays.get(&target_wid) {
517
-            unsafe {
518
-                let mut bounds = CGRect::default();
519
-                if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess {
520
-                    return;
521
-                }
522
-                let bw = self.border_width;
523
-                let ow = bounds.size.width + 2.0 * bw;
524
-                let oh = bounds.size.height + 2.0 * bw;
525
-
526
-                let ctx = SLWindowContextCreate(overlay.cid, overlay.wid, ptr::null());
527
-                if ctx.is_null() {
528
-                    return;
529
-                }
530
-
531
-                let color = self.color_for(target_wid);
532
-                draw_border(ctx, ow, oh, bw, self.radius, color);
533
-                SLSFlushWindowContentRegion(overlay.cid, overlay.wid, ptr::null());
534
-                CGContextRelease(ctx);
535
-            }
496
+            overlay.window.set_color(self.color_for(target_wid));
536497
         }
537498
     }
538499
 
@@ -589,14 +550,9 @@ impl BorderMap {
589550
         }
590551
         for (&target_wid, o) in &self.overlays {
591552
             if target_wid == self.focused_wid {
592
-                unsafe {
593
-                    SLSSetWindowLevel(o.cid, o.wid, 0);
594
-                    SLSOrderWindow(o.cid, o.wid, 1, target_wid);
595
-                }
553
+                o.window.order_above(target_wid);
596554
             } else {
597
-                unsafe {
598
-                    SLSOrderWindow(o.cid, o.wid, 0, 0);
599
-                }
555
+                o.window.order_out();
600556
             }
601557
         }
602558
     }
@@ -794,6 +750,11 @@ fn main() {
794750
 
795751
     let active_only = args.iter().any(|s| s == "--active-only");
796752
 
753
+    // Initialize NSApplication on the main thread before we touch any
754
+    // AppKit APIs. NSWindow operations (used by nswindow_overlay) all
755
+    // require a main-thread context.
756
+    let mtm = nswindow_overlay::init_application();
757
+
797758
     let cid = unsafe { SLSMainConnectionID() };
798759
     let own_pid = unsafe {
799760
         let mut pid: i32 = 0;
@@ -808,7 +769,7 @@ fn main() {
808769
     setup_event_port(cid);
809770
 
810771
     // Discover and create borders
811
-    let mut borders = BorderMap::new(cid, own_pid, border_width);
772
+    let mut borders = BorderMap::new(cid, own_pid, border_width, mtm);
812773
     borders.radius = radius;
813774
     borders.active_color = active_color;
814775
     borders.inactive_color = inactive_color;
@@ -872,178 +833,223 @@ fn main() {
872833
         );
873834
     }
874835
 
875
-    // Process events on background thread with coalescing
876
-    let running_bg = Arc::clone(&running);
877
-    let handle = std::thread::spawn(move || {
878
-        use std::collections::HashSet;
879
-        use std::time::{Duration, Instant};
880
-
881
-        // Persist across batches: windows we know about but haven't bordered yet.
882
-        // Value is the time the window was first seen — only promote after 100ms
883
-        // so tarmac has time to position them.
884
-        let mut pending: HashMap<u32, Instant> = HashMap::new();
885
-
886
-        while running_bg.load(Ordering::Relaxed) {
887
-            let first = match rx.recv_timeout(Duration::from_millis(100)) {
888
-                Ok(e) => e,
889
-                Err(mpsc::RecvTimeoutError::Timeout) => continue,
890
-                Err(mpsc::RecvTimeoutError::Disconnected) => break,
891
-            };
892
-
893
-            std::thread::sleep(std::time::Duration::from_millis(16));
894
-
895
-            let mut events = vec![first];
896
-            while let Ok(e) = rx.try_recv() {
897
-                events.push(e);
898
-            }
836
+    // Process events on the main thread via a CFRunLoopTimer.
837
+    // BorderMap holds Retained<NSWindow> handles, which are
838
+    // !Send/!Sync — AppKit calls must originate from the main thread.
839
+    // Stash state in thread_local for the C callback to access.
840
+    MAIN_STATE.with(|cell| {
841
+        *cell.borrow_mut() = Some(MainState {
842
+            borders,
843
+            rx,
844
+            pending: HashMap::new(),
845
+            batch_events: Vec::new(),
846
+            batch_first_seen: None,
847
+        });
848
+    });
899849
 
900
-            let mut moved: HashSet<u32> = HashSet::new();
901
-            let mut resized: HashSet<u32> = HashSet::new();
902
-            let mut destroyed: HashSet<u32> = HashSet::new();
903
-            let mut needs_resubscribe = false;
904
-
905
-            for event in events {
906
-                match event {
907
-                    Event::Move(wid) => {
908
-                        if !borders.is_overlay(wid) {
909
-                            moved.insert(wid);
910
-                        }
911
-                    }
912
-                    Event::Resize(wid) => {
913
-                        if !borders.is_overlay(wid) {
914
-                            resized.insert(wid);
915
-                        }
916
-                    }
917
-                    Event::Close(wid) | Event::Destroy(wid) => {
918
-                        if !borders.is_overlay(wid) {
919
-                            destroyed.insert(wid);
920
-                            pending.remove(&wid);
921
-                        }
922
-                    }
923
-                    Event::Create(wid) => {
924
-                        if !borders.is_overlay(wid) {
925
-                            pending.entry(wid).or_insert_with(Instant::now);
926
-                            borders.subscribe_target(wid);
927
-                        }
928
-                    }
929
-                    Event::Hide(wid) => borders.hide(wid),
930
-                    Event::Unhide(wid) => {
931
-                        if !borders.active_only || wid == borders.focused_wid {
932
-                            borders.unhide(wid);
933
-                        }
934
-                    }
935
-                    Event::FrontChange => {
936
-                        needs_resubscribe = true;
937
-                    }
938
-                    Event::SpaceChange => {
939
-                        needs_resubscribe = true;
850
+    unsafe {
851
+        let mut ctx = CFRunLoopTimerContext {
852
+            version: 0,
853
+            info: ptr::null_mut(),
854
+            retain: None,
855
+            release: None,
856
+            copy_description: None,
857
+        };
858
+        let timer = CFRunLoopTimerCreate(
859
+            ptr::null(),
860
+            CFAbsoluteTimeGetCurrent() + 0.05,
861
+            0.016,
862
+            0,
863
+            0,
864
+            timer_callback,
865
+            &mut ctx,
866
+        );
867
+        CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode);
868
+    }
869
+
870
+    unsafe { CFRunLoopRun() };
871
+
872
+    // Drop everything on the main thread (NSWindow.close in Drop).
873
+    MAIN_STATE.with(|cell| cell.borrow_mut().take());
874
+
875
+    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
876
+    let _ = signal_watcher.join();
877
+    drop(running);
878
+}
879
+
880
+struct MainState {
881
+    borders: BorderMap,
882
+    rx: mpsc::Receiver<Event>,
883
+    pending: HashMap<u32, std::time::Instant>,
884
+    batch_events: Vec<Event>,
885
+    batch_first_seen: Option<std::time::Instant>,
886
+}
887
+
888
+thread_local! {
889
+    static MAIN_STATE: std::cell::RefCell<Option<MainState>> = const { std::cell::RefCell::new(None) };
890
+}
891
+
892
+extern "C" fn timer_callback(_timer: *mut std::ffi::c_void, _info: *mut std::ffi::c_void) {
893
+    use std::time::{Duration, Instant};
894
+    MAIN_STATE.with(|cell| {
895
+        let mut state_opt = cell.borrow_mut();
896
+        let s = match state_opt.as_mut() {
897
+            Some(s) => s,
898
+            None => return,
899
+        };
900
+        loop {
901
+            match s.rx.try_recv() {
902
+                Ok(e) => {
903
+                    if s.batch_events.is_empty() {
904
+                        s.batch_first_seen = Some(Instant::now());
940905
                     }
906
+                    s.batch_events.push(e);
941907
                 }
908
+                Err(mpsc::TryRecvError::Empty) => break,
909
+                Err(mpsc::TryRecvError::Disconnected) => break,
942910
             }
911
+        }
912
+        if let Some(first_seen) = s.batch_first_seen
913
+            && first_seen.elapsed() >= Duration::from_millis(100)
914
+        {
915
+            let events = std::mem::take(&mut s.batch_events);
916
+            s.batch_first_seen = None;
917
+            process_event_batch(&mut s.borders, &mut s.pending, events);
918
+        }
919
+    });
920
+}
943921
 
944
-            // Destroys
945
-            for wid in &destroyed {
946
-                borders.remove(*wid);
922
+fn process_event_batch(
923
+    borders: &mut BorderMap,
924
+    pending: &mut HashMap<u32, std::time::Instant>,
925
+    events: Vec<Event>,
926
+) {
927
+    use std::collections::HashSet;
928
+    use std::time::{Duration, Instant};
929
+
930
+    let mut moved: HashSet<u32> = HashSet::new();
931
+    let mut resized: HashSet<u32> = HashSet::new();
932
+    let mut destroyed: HashSet<u32> = HashSet::new();
933
+    let mut needs_resubscribe = false;
934
+
935
+    for event in events {
936
+        match event {
937
+            Event::Move(wid) => {
938
+                if !borders.is_overlay(wid) {
939
+                    moved.insert(wid);
940
+                }
947941
             }
948
-
949
-            // Promote pending creates that have waited ≥100ms (tarmac positioning time)
950
-            let now = Instant::now();
951
-            let ready: Vec<u32> = pending
952
-                .iter()
953
-                .filter(|(wid, seen_at)| {
954
-                    !destroyed.contains(wid)
955
-                        && now.duration_since(**seen_at) >= Duration::from_millis(100)
956
-                })
957
-                .map(|(wid, _)| *wid)
958
-                .collect();
959
-            // Filter overlapping creates: if two windows overlap, keep smaller one
960
-            let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
961
-            for &wid in &ready {
962
-                unsafe {
963
-                    let mut b = CGRect::default();
964
-                    SLSGetWindowBounds(borders.main_cid, wid, &mut b);
965
-                    bounds_map.push((wid, b));
942
+            Event::Resize(wid) => {
943
+                if !borders.is_overlay(wid) {
944
+                    resized.insert(wid);
966945
                 }
967946
             }
968
-
969
-            // If two new windows overlap closely, skip the larger one (container)
970
-            let mut skip: std::collections::HashSet<u32> = HashSet::new();
971
-            for i in 0..bounds_map.len() {
972
-                for j in (i + 1)..bounds_map.len() {
973
-                    let (wid_a, a) = &bounds_map[i];
974
-                    let (wid_b, b) = &bounds_map[j];
975
-                    if let Some(preference) = surface_preference(*a, *b) {
976
-                        match preference {
977
-                            SurfacePreference::KeepExisting => {
978
-                                skip.insert(*wid_b);
979
-                            }
980
-                            SurfacePreference::ReplaceExisting => {
981
-                                skip.insert(*wid_a);
982
-                            }
983
-                        }
984
-                    }
947
+            Event::Close(wid) | Event::Destroy(wid) => {
948
+                if !borders.is_overlay(wid) {
949
+                    destroyed.insert(wid);
950
+                    pending.remove(&wid);
985951
                 }
986952
             }
987
-
988
-            for &wid in &ready {
989
-                pending.remove(&wid);
990
-                if !skip.contains(&wid) {
991
-                    borders.add_fresh(wid);
992
-                    if borders.active_only && wid != borders.focused_wid {
993
-                        borders.hide(wid);
994
-                    }
995
-                    needs_resubscribe = true;
953
+            Event::Create(wid) => {
954
+                if !borders.is_overlay(wid) {
955
+                    pending.entry(wid).or_insert_with(Instant::now);
956
+                    borders.subscribe_target(wid);
996957
                 }
997958
             }
998
-
999
-            // Moves: reposition overlay (no destroy/create)
1000
-            for wid in &moved {
1001
-                if !resized.contains(wid) && !ready.contains(wid) && borders.sync_overlay(*wid) {
1002
-                    needs_resubscribe = true;
959
+            Event::Hide(wid) => borders.hide(wid),
960
+            Event::Unhide(wid) => {
961
+                if !borders.active_only || wid == borders.focused_wid {
962
+                    borders.unhide(wid);
1003963
                 }
1004964
             }
965
+            Event::FrontChange => {
966
+                needs_resubscribe = true;
967
+            }
968
+            Event::SpaceChange => {
969
+                needs_resubscribe = true;
970
+            }
971
+        }
972
+    }
1005973
 
1006
-            // Resizes: must recreate (can't reshape windows on Tahoe)
1007
-            // Skip windows just created this batch — already at correct size
1008
-            for wid in &resized {
1009
-                if !ready.contains(wid)
1010
-                    && borders.overlays.contains_key(wid)
1011
-                    && borders.sync_overlay(*wid)
1012
-                {
1013
-                    needs_resubscribe = true;
974
+    for wid in &destroyed {
975
+        borders.remove(*wid);
976
+    }
977
+
978
+    let now = Instant::now();
979
+    let ready: Vec<u32> = pending
980
+        .iter()
981
+        .filter(|(wid, seen_at)| {
982
+            !destroyed.contains(wid) && now.duration_since(**seen_at) >= Duration::from_millis(100)
983
+        })
984
+        .map(|(wid, _)| *wid)
985
+        .collect();
986
+
987
+    let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
988
+    for &wid in &ready {
989
+        unsafe {
990
+            let mut b = CGRect::default();
991
+            SLSGetWindowBounds(borders.main_cid, wid, &mut b);
992
+            bounds_map.push((wid, b));
993
+        }
994
+    }
995
+
996
+    let mut skip: std::collections::HashSet<u32> = HashSet::new();
997
+    for i in 0..bounds_map.len() {
998
+        for j in (i + 1)..bounds_map.len() {
999
+            let (wid_a, a) = &bounds_map[i];
1000
+            let (wid_b, b) = &bounds_map[j];
1001
+            if let Some(preference) = surface_preference(*a, *b) {
1002
+                match preference {
1003
+                    SurfacePreference::KeepExisting => {
1004
+                        skip.insert(*wid_b);
1005
+                    }
1006
+                    SurfacePreference::ReplaceExisting => {
1007
+                        skip.insert(*wid_a);
1008
+                    }
10141009
                 }
10151010
             }
1011
+        }
1012
+    }
10161013
 
1017
-            // On space change, discover windows we haven't seen yet
1018
-            if needs_resubscribe {
1019
-                borders.discover_untracked();
1014
+    for &wid in &ready {
1015
+        pending.remove(&wid);
1016
+        if !skip.contains(&wid) {
1017
+            borders.add_fresh(wid);
1018
+            if borders.active_only && wid != borders.focused_wid {
1019
+                borders.hide(wid);
10201020
             }
1021
+            needs_resubscribe = true;
1022
+        }
1023
+    }
10211024
 
1022
-            needs_resubscribe |= borders.reconcile_tracked();
1025
+    for wid in &moved {
1026
+        if !resized.contains(wid) && !ready.contains(wid) && borders.sync_overlay(*wid) {
1027
+            needs_resubscribe = true;
1028
+        }
1029
+    }
10231030
 
1024
-            // Update focus (redraws borders in-place if changed)
1025
-            borders.update_focus();
1031
+    for wid in &resized {
1032
+        if !ready.contains(wid)
1033
+            && borders.overlays.contains_key(wid)
1034
+            && borders.sync_overlay(*wid)
1035
+        {
1036
+            needs_resubscribe = true;
1037
+        }
1038
+    }
10261039
 
1027
-            // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends)
1028
-            if needs_resubscribe || !destroyed.is_empty() {
1029
-                borders.subscribe_all();
1030
-            }
1040
+    if needs_resubscribe {
1041
+        borders.discover_untracked();
1042
+    }
10311043
 
1032
-            // After all processing, enforce active-only visibility
1033
-            borders.enforce_active_only();
1034
-        }
1044
+    needs_resubscribe |= borders.reconcile_tracked();
10351045
 
1036
-        // Clean up all overlays before exiting
1037
-        borders.remove_all();
1038
-    });
1046
+    borders.update_focus();
10391047
 
1040
-    unsafe { CFRunLoopRun() };
1048
+    if needs_resubscribe || !destroyed.is_empty() {
1049
+        borders.subscribe_all();
1050
+    }
10411051
 
1042
-    // SIGINT received — signal background thread to stop and wait
1043
-    running.store(false, Ordering::Relaxed);
1044
-    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
1045
-    let _ = signal_watcher.join();
1046
-    let _ = handle.join();
1052
+    borders.enforce_active_only();
10471053
 }
10481054
 
10491055
 fn setup_event_port(cid: CGSConnectionID) {