Skip to content
Open
Show file tree
Hide file tree
Changes from all 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