From 84e39e3b48c424cab8e9d64bb6af12aa15adcce8 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:00:41 +0200 Subject: [PATCH] Keep hidden X11 windows unmapped Restack and makeKey no longer map a window AppKit considers hidden, and setIsVisible: NO always unmaps, so the client's splash stays hidden. Co-Authored-By: Claude Sonnet 5.5 --- patches/cocotron.patch | 87 +++++++++++++++++++++++++-------- tests/test_x11_hidden_window.py | 36 ++++++++++++++ 2 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 tests/test_x11_hidden_window.py diff --git a/patches/cocotron.patch b/patches/cocotron.patch index f506180..b8f2125 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -1690,7 +1690,7 @@ index 8ac2bbdc..a0fb4f37 100644 - (BOOL) performDragOperation: (id) sender { diff --git a/AppKit/NSWindow.m b/AppKit/NSWindow.m -index d545120a..79760f47 100644 +index d545120a..1920e594 100644 --- a/AppKit/NSWindow.m +++ b/AppKit/NSWindow.m @@ -27,6 +27,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -2462,15 +2462,28 @@ index d545120a..79760f47 100644 [[NSDraggingManager draggingManager] dragImage: image at: location offset: offset -@@ -2536,6 +3116,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2527,15 +3107,19 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (void) setIsVisible: (BOOL) visible { +- if (_isVisible == visible) { ++ /* darling-lol: hiding always reaches the platform window, which may be mapped while _isVisible is NO. */ ++ if (_isVisible == visible && visible) { + return; + } ++ BOOL changed = _isVisible != visible; + _isVisible = visible; + if (visible) { + [[self platformWindow] showWindowWithoutActivation]; } else { [[self platformWindow] hideWindow]; } -+ [self postNotificationName: NSWindowDidChangeOcclusionStateNotification]; ++ if (changed) ++ [self postNotificationName: NSWindowDidChangeOcclusionStateNotification]; } - (void) orderFront: (id) sender { -@@ -3064,6 +3645,13 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3064,6 +3648,13 @@ static BOOL _allowsAutomaticWindowTabbing; if (didSize) { // Don't redraw everything unless we really have to. [_backgroundView setFrameSize: _frame.size]; @@ -2484,7 +2497,7 @@ index d545120a..79760f47 100644 [_backgroundView setNeedsDisplay: YES]; [self resetCursorRects]; [self saveFrameUsingName: _autosaveFrameName]; -@@ -3260,8 +3848,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3260,8 +3851,7 @@ static BOOL _allowsAutomaticWindowTabbing; mouseWasInside && !mouseIsInside) { [exited addObject: area]; } @@ -2494,7 +2507,7 @@ index d545120a..79760f47 100644 [moved addObject: area]; } if (options & NSTrackingCursorUpdate && !mouseWasInside && -@@ -3459,7 +4046,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3459,7 +4049,7 @@ static BOOL _allowsAutomaticWindowTabbing; } - (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame { @@ -5075,7 +5088,7 @@ index bfe3839a..48cfac4c 100644 - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m -index f4925b66..730d4692 100644 +index f4925b66..e458d96f 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -18,11 +18,14 @@ @@ -5571,7 +5584,7 @@ index f4925b66..730d4692 100644 struct { unsigned long flags; unsigned long functions; -@@ -356,8 +620,14 @@ static NSData *makeWindowIcon() { +@@ -356,11 +620,25 @@ static NSData *makeWindowIcon() { - (void) ensureMapped { if (!_mapped) { @@ -5586,7 +5599,18 @@ index f4925b66..730d4692 100644 } } -@@ -377,15 +647,27 @@ static NSData *makeWindowIcon() { ++/* darling-lol: only map for a window AppKit considers visible. NSWindow sets _isVisible before ++ * ordering, so ordered windows still map; a hidden one (like the client's splash) must not be ++ * revived by a later restack or makeKey. */ ++- (void) ensureMappedIfVisible { ++ if (_delegate == nil || [_delegate isVisible]) ++ [self ensureMapped]; ++} ++ + - (void) showWindowWithoutActivation { + [self ensureMapped]; + } +@@ -377,15 +655,27 @@ static NSData *makeWindowIcon() { - (void) syncDelegateProperties { long mask = KeyPressMask | KeyReleaseMask | ExposureMask | StructureNotifyMask | EnterWindowMask | LeaveWindowMask | @@ -5619,7 +5643,7 @@ index f4925b66..730d4692 100644 // TODO: background color } -@@ -393,6 +675,28 @@ static NSData *makeWindowIcon() { +@@ -393,6 +683,28 @@ static NSData *makeWindowIcon() { // This is essentially dealloc; we release our contexts // and windows, but unlike dealloc, this method can be called // several times, so set everything to nil/NULL/0. @@ -5648,7 +5672,7 @@ index f4925b66..730d4692 100644 [_context release]; _context = nil; -@@ -421,6 +725,392 @@ static NSData *makeWindowIcon() { +@@ -421,6 +733,392 @@ static NSData *makeWindowIcon() { } } @@ -6041,7 +6065,7 @@ index f4925b66..730d4692 100644 - (Window) windowHandle { return _window; } -@@ -470,14 +1160,32 @@ static NSData *makeWindowIcon() { +@@ -470,14 +1168,32 @@ static NSData *makeWindowIcon() { const char *text = [title cString]; XStringListToTextProperty((char **) &text, 1, &prop); XSetWMName(_display, _window, &prop); @@ -6075,7 +6099,7 @@ index f4925b66..730d4692 100644 } - (void) setHasShadow: (BOOL) value { -@@ -488,6 +1196,25 @@ static NSData *makeWindowIcon() { +@@ -488,6 +1204,25 @@ static NSData *makeWindowIcon() { _level = value; } @@ -6101,9 +6125,30 @@ index f4925b66..730d4692 100644 - (void) setOpaque: (BOOL) value { _isOpaque = value; } -@@ -543,6 +1270,21 @@ static NSData *makeWindowIcon() { +@@ -518,7 +1253,7 @@ static NSData *makeWindowIcon() { + + - (void) placeAboveWindow: (NSInteger) otherNumber { + X11Window *other = [X11Window windowWithWindowNumber: otherNumber]; +- [self ensureMapped]; ++ [self ensureMappedIfVisible]; + + if (!other) { + XRaiseWindow(_display, _window); +@@ -530,7 +1265,7 @@ static NSData *makeWindowIcon() { + + - (void) placeBelowWindow: (NSInteger) otherNumber { + X11Window *other = [X11Window windowWithWindowNumber: otherNumber]; +- [self ensureMapped]; ++ [self ensureMappedIfVisible]; + + if (!other) { + XLowerWindow(_display, _window); +@@ -541,8 +1276,23 @@ static NSData *makeWindowIcon() { + } + - (void) makeKey { - [self ensureMapped]; +- [self ensureMapped]; ++ [self ensureMappedIfVisible]; XRaiseWindow(_display, _window); + /* darling-lol: League's game window is managed (not override-redirect), so keyboard focus is + * the WM's to give. Ask for activation once when the game makes it key; the WM's focus-stealing @@ -6123,7 +6168,7 @@ index f4925b66..730d4692 100644 } - (void) makeMain { -@@ -559,7 +1301,29 @@ static NSData *makeWindowIcon() { +@@ -559,7 +1309,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -6153,7 +6198,7 @@ index f4925b66..730d4692 100644 Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); -@@ -593,8 +1357,10 @@ static NSData *makeWindowIcon() { +@@ -593,8 +1365,10 @@ static NSData *makeWindowIcon() { if (_cglContext == NULL) { CGLError error; @@ -6166,7 +6211,7 @@ index f4925b66..730d4692 100644 NSLog(@"CGLCreateContext failed at %s %d with error %d", __FILE__, __LINE__, error); -@@ -615,6 +1381,12 @@ static NSData *makeWindowIcon() { +@@ -615,6 +1389,12 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { @@ -6179,7 +6224,7 @@ index f4925b66..730d4692 100644 CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -652,7 +1424,7 @@ static NSData *makeWindowIcon() { +@@ -652,7 +1432,7 @@ static NSData *makeWindowIcon() { } - (O2Rect) frame { @@ -6188,7 +6233,7 @@ index f4925b66..730d4692 100644 } static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { -@@ -666,6 +1438,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -666,6 +1446,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -6196,7 +6241,7 @@ index f4925b66..730d4692 100644 XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow); @try { Window root, parent; -@@ -710,19 +1483,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -710,19 +1491,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { [_deviceDictionary addEntriesFromDictionary: entries]; } diff --git a/tests/test_x11_hidden_window.py b/tests/test_x11_hidden_window.py new file mode 100644 index 0000000..b0397c4 --- /dev/null +++ b/tests/test_x11_hidden_window.py @@ -0,0 +1,36 @@ +"""Check that the patched X11 backend cannot re-map a window AppKit has hidden.""" +import unittest +import test_runtime_compatibility as sources + + +def body(source, signature): + return source.split(signature + ' {', 1)[1].split('\n}', 1)[0] + + +class HiddenWindowChecks(unittest.TestCase): + patched = sources.CompatibilityChecks.patched + + def test_restack_and_make_key_map_only_visible_windows(self): + source = self.patched('cocotron', 'AppKit/X11.backend/X11Window.m') + self.assertIn('[_delegate isVisible]', body(source, '- (void) ensureMappedIfVisible')) + for signature in ('- (void) placeAboveWindow: (NSInteger) otherNumber', + '- (void) placeBelowWindow: (NSInteger) otherNumber', '- (void) makeKey'): + method = body(source, signature) + self.assertIn('[self ensureMappedIfVisible]', method, signature) + self.assertNotIn('[self ensureMapped]', method, signature) + + def test_ordering_marks_window_visible_before_placing_it(self): + source = self.patched('cocotron', 'AppKit/NSWindow.m') + ordering = source.split('relativeTo: (NSInteger) relativeTo\n{', 1)[1].split('\n}', 1)[0] + for call in ('placeAboveWindow', 'placeBelowWindow'): + self.assertLess(ordering.index('_isVisible = YES'), ordering.index(call)) + + def test_hiding_reaches_the_platform_window_when_already_not_visible(self): + source = self.patched('cocotron', 'AppKit/NSWindow.m') + method = body(source, '- (void) setIsVisible: (BOOL) visible') + self.assertIn('_isVisible == visible && visible', method) + self.assertIn('hideWindow', method) + + +if __name__ == '__main__': + unittest.main()