Which CGRect was that?

iOS and macOS are different. NSRect and CGRect can be too, apparently.

NSScreen describes the attributes of a computer’s monitor. NSScreen.screens returns an array of all monitors connected. NSScreen.frame is an NSRect defined as

This is the full screen rectangle at the current resolution. This rectangle includes any space currently occupied by the menu bar and dock.

https://developer.apple.com/documentation/appkit/nsscreen/1388387-frame

NSRect is typedef’d to CGRect. Of course, CGRect is defined in Core Graphics as

In the default Core Graphics coordinate space, the origin is located in the lower-left corner of the rectangle and the rectangle extends towards the upper-right corner. If the context has a flipped-coordinate space—often the case on iOS—the origin is in the upper-left corner and the rectangle extends towards the lower-right corner.

https://developer.apple.com/documentation/coregraphics/cgrect

So, there’s the difference between iOS and macOS. One initializer is CGRect.init(origin: CGPoint, size: CGSize)where CGPoint (CGPoint.x and CGPoint.y) are the coordinates of the lower-left corner of the rectangle of size CGSize with width and height (CGSize.width and CGSize.height, respectively)

Using a MacBook Pro, NSScreen.frame returns (0.0, 0.0, 1920.0, 1200.0). The first two values is a CGPoint while the last two are the width and height of CGSize. The combination is a CGRect.

Now, I connect another monitor and align them in System Preferences…>Displays>Arrangement such that the new monitor is above the original monitor. Leaving the menu bar on the original monitor, NSRect is (0.0, 1200.0, 2560.0, 1080.0) for the new monitor and unchanged for the laptop’s screen.

Moving the menu bar to the new monitor, both NSRect change: (0.0, 0.0, 2560.0, 1080.0) and (0.0, -1200.0, 1920.0, 1200.0) for the new and original monitors, respectively. This is consistent with the documentation if the origin is defined to be the lower-left corner of the monitor which has the menu bar, and CGPoint is the lower right corner of the rectangle defined by CGSize. I won’t show, but it’s true also for NSWindow.frame, etc. Fantastic!

In Core Graphics, you can ask about what windows are on screen. For example let windows = CGWindowListCopyWindow( .optionOnScreenOnly, kCGNullWindowID) will return an array of dictionaries, each dictionary (i.e. element of the array) corresponding to a window on the screen (front to back order). One of the required keys in each directory is “kCGWindowBounds“. Apple provides an initializer for CGRect: CGRect.init(dictionaryRepresentation: windows[0]["kCGWindowBounds"] as! CFDictionary)! using this key. The documentation also says

The coordinates of the rectangle are specified in screen space, where the origin is in the upper-left corner of the main display.

https://developer.apple.com/documentation/coregraphics/kcgwindowbounds

So, now we’re back to iOS coordinates? This Core Graphic CGRect has it’s vertical axis flipped and offset from either NSRect or CGRECT we have seen earlier with NSScreen(or NSWindow).frame. Hmmm.

Here, CGPoint.zero is NS*’s height of the monitor (with the menu bar), call it h0. That’s the offset. Therefore, to translate this new CGRect into the former NSRect, only the vertical point of the CGPoint needs to change. We first need to increase the original vertical component by CGSize.height and subtract the result from h0. Now we can compare this new CGRect to NS*.frame to see if they are on the same area of the screen.

In code

// height of monitor that has menu bar
let h0 = NSHeight((NSScreen.screens.filter({$0.frame.origin == CGPoint.zero}).first?.frame)!) // it must exist!

// array of on screen windows (front to back) over all monitors
let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as! [[ String : Any]]

// get rectNS in CG coordinates (not NS* coordinates) for the first on screen window
let rectNS = CGRect.init(dictionaryRepresentation: windows[0]["kCGWindowBounds"] as! CFDictionary)!

// translate that CGRect to NS* coordinates
let rectNS.origin.y = h0 - rectNS.origin.y - rectNS.height

Now rectNS has the same coordinate space as NS*.frame and can be compared directly (==).

Add a Comment

Your email address will not be published. Required fields are marked *