Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file

## Fixed
<!-- Bug fixes -->
- Fix SIGSEGV when the display configuration changes (sleep/wake, monitor connect/disconnect). `ApplicationDidChangeScreenParameters` is dispatched on background goroutines and can fire repeatedly during a single reconfiguration, so `processAndCacheScreens` enumerated `[NSScreen screens]` concurrently off the main thread and crashed. Screen enumeration is now marshalled onto the main thread (which also serialises the event burst), the primary-screen height is resolved once per refresh instead of per screen, and `getAllScreens` returns its count to close a TOCTOU against the buffer size. (#5117)
- Fix minimum width/height constraints not enforced after window unmaximise on Windows (#4593)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use consistent spelling in user-facing changelog text.

Line 27 uses “unmaximise”; consider “unmaximize” for consistency with common US spelling in release notes.

🧰 Tools
🪛 LanguageTool

[grammar] ~27-~27: Ensure spelling is correct
Context: ...t constraints not enforced after window unmaximise on Windows (#4593) - Fix mouse click-th...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/UNRELEASED_CHANGELOG.md` at line 27, Update the user-facing changelog text
to use consistent US spelling: replace “unmaximise” with “unmaximize” in the
UNRELEASED_CHANGELOG.md entry that reads "Fix minimum width/height constraints
not enforced after window unmaximise on Windows (`#4593`)"; ensure the rest of the
line remains unchanged except for that word so the entry reads "Fix minimum
width/height constraints not enforced after window unmaximize on Windows
(`#4593`)".

- Fix mouse click-through in fullscreen mode with Frameless + Transparent window options (#4408)

Expand Down
83 changes: 61 additions & 22 deletions v3/pkg/application/screen_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,22 @@ int GetNumScreens(){
return [[NSScreen screens] count];
}

Screen processScreen(NSScreen* screen){
// primaryScreenHeight returns the height (in points) of the primary screen,
// used to flip NSScreen's Y-up coordinate space to the Y-down convention.
// Callers resolve it once and pass it into processScreen so the screen list is
// not re-enumerated ([NSScreen screens]) once per screen.
CGFloat primaryScreenHeight(){
NSScreen* primaryScreen = [[NSScreen screens] firstObject];
if (primaryScreen == NULL) {
primaryScreen = [NSScreen mainScreen];
}
if (primaryScreen == NULL) {
return 0;
}
return [primaryScreen frame].size.height;
}

Screen processScreen(NSScreen* screen, CGFloat primaryHeight){
Screen returnScreen;
returnScreen.scaleFactor = screen.backingScaleFactor;

Expand All @@ -43,12 +58,8 @@ Screen processScreen(NSScreen* screen){
// of the primary screen so that Bounds matches windowGetPosition /
// windowSetPosition and the public conventions used by Windows, GTK,
// Electron and the web. Screens above the primary therefore have negative
// Y after the flip; Bounds.Y is the screen's top edge.
NSScreen* primaryScreen = [[NSScreen screens] firstObject];
if (primaryScreen == NULL) {
primaryScreen = [NSScreen mainScreen];
}
CGFloat primaryHeight = [primaryScreen frame].size.height;
// Y after the flip; Bounds.Y is the screen's top edge. primaryHeight is
// resolved once by the caller (see primaryScreenHeight).

// screen bounds
returnScreen.height = screen.frame.size.height;
Expand Down Expand Up @@ -92,23 +103,33 @@ Screen processScreen(NSScreen* screen){
Screen GetPrimaryScreen(){
// Get primary screen
NSScreen *mainScreen = [NSScreen mainScreen];
return processScreen(mainScreen);
return processScreen(mainScreen, primaryScreenHeight());
}

Screen* getAllScreens() {
// getAllScreens returns a malloc'd array of Screen and, via outCount, the number
// of entries it contains. The count is captured from the same [NSScreen screens]
// snapshot used to size the allocation, so callers must not query the screen
// count separately (doing so races display changes and can over-read the buffer).
Screen* getAllScreens(int* outCount) {
NSArray<NSScreen *> *screens = [NSScreen screens];
Screen* returnScreens = malloc(sizeof(Screen) * screens.count);
for (int i = 0; i < screens.count; i++) {
NSScreen* screen = [screens objectAtIndex:i];
returnScreens[i] = processScreen(screen);
NSUInteger count = screens.count;
if (outCount != NULL) {
*outCount = (int)count;
}
// Reuse the snapshot above instead of re-enumerating [NSScreen screens];
// screens[0] is the primary screen (matches isPrimary = (i == 0) below).
CGFloat primaryHeight = count > 0 ? [[screens objectAtIndex:0] frame].size.height : 0;
Screen* returnScreens = malloc(sizeof(Screen) * count);
for (NSUInteger i = 0; i < count; i++) {
returnScreens[i] = processScreen([screens objectAtIndex:i], primaryHeight);
returnScreens[i].isPrimary = (i == 0);
}
return returnScreens;
}

Screen getScreenForWindow(void* window){
NSScreen* screen = ((NSWindow*)window).screen;
return processScreen(screen);
return processScreen(screen, primaryScreenHeight());
}

// Get the screen for the system tray
Expand All @@ -124,7 +145,7 @@ Screen getScreenForSystemTray(void* nsStatusItem) {
break;
}
}
return processScreen(associatedScreen);
return processScreen(associatedScreen, primaryScreenHeight());
}

void* getWindowForSystray(void* nsStatusItem) {
Expand Down Expand Up @@ -190,13 +211,31 @@ func cScreenToScreen(screen C.Screen) *Screen {
}

func (m *macosApp) processAndCacheScreens() error {
cScreens := C.getAllScreens()
defer C.free(unsafe.Pointer(cScreens))
numScreens := int(C.GetNumScreens())
screens := make([]*Screen, numScreens)
cScreenHeaders := (*[1 << 30]C.Screen)(unsafe.Pointer(cScreens))[:numScreens:numScreens]
for i := 0; i < numScreens; i++ {
screens[i] = cScreenToScreen(cScreenHeaders[i])
enumerate := func() []*Screen {
var count C.int
cScreens := C.getAllScreens(&count)
defer C.free(unsafe.Pointer(cScreens))
numScreens := int(count)
screens := make([]*Screen, numScreens)
cScreenHeaders := (*[1 << 30]C.Screen)(unsafe.Pointer(cScreens))[:numScreens:numScreens]
for i := 0; i < numScreens; i++ {
screens[i] = cScreenToScreen(cScreenHeaders[i])
}
return screens
}

// NSScreen and other AppKit APIs are not thread-safe and must be accessed on
// the main thread. Application events (including ApplicationDidChangeScreenParameters)
// are dispatched on background goroutines and can fire several times in quick
// succession during a display reconfiguration, so without marshalling this
// enumerates [NSScreen screens] concurrently off the main thread and crashes
// (SIGSEGV). Running on the main run loop also serialises the burst of events.
// Guard against InvokeSync deadlocking when we are already on the main thread.
var screens []*Screen
if m.isOnMainThread() {
screens = enumerate()
} else {
InvokeSync(func() { screens = enumerate() })
}
return m.parent.Screen.LayoutScreens(screens)
}
Expand Down
Loading