cocoadisplay: Add support for high-dpi screens (#1308)

This commit is contained in:
LD 2023-10-10 16:56:04 +02:00 committed by GitHub
parent e55bb94996
commit ae3cbe4b12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 268 additions and 110 deletions

View File

@ -717,6 +717,7 @@ class build_apps(setuptools.Command):
'CFBundlePackageType': 'APPL', 'CFBundlePackageType': 'APPL',
'CFBundleSignature': '', #TODO 'CFBundleSignature': '', #TODO
'CFBundleExecutable': self.macos_main_app, 'CFBundleExecutable': self.macos_main_app,
'NSHighResolutionCapable': 'True',
} }
icon = self.icon_objects.get( icon = self.icon_objects.get(

View File

@ -40,12 +40,36 @@ CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) {
[thread start]; [thread start];
[thread autorelease]; [thread autorelease];
// If the application is dpi-aware, iterate over all the screens to find the
// one with our display ID and get the backing scale factor to configure the
// detected display zoom. Otherwise the detected display zoom keeps its
// default value of 1.0
if (dpi_aware) {
NSScreen *screen;
NSEnumerator *e = [[NSScreen screens] objectEnumerator];
while (screen = (NSScreen *) [e nextObject]) {
NSNumber *num = [[screen deviceDescription] objectForKey: @"NSScreenNumber"];
if (_display == (CGDirectDisplayID) [num longValue]) {
set_detected_display_zoom([screen backingScaleFactor]);
if (cocoadisplay_cat.is_debug()) {
cocoadisplay_cat.debug()
<< "Display zoom is " << [screen backingScaleFactor] << "\n";
}
break;
}
}
}
// We used to also obtain the corresponding NSScreen here, but this causes // We used to also obtain the corresponding NSScreen here, but this causes
// the application icon to start bouncing, which may be undesirable for // the application icon to start bouncing, which may be undesirable for
// apps that will never open a window. // apps that will never open a window.
_display_width = CGDisplayPixelsWide(_display); // Although the name of these functions mention pixels, they actually return
_display_height = CGDisplayPixelsHigh(_display); // display points, we use the detected display zoom to transform the values
// into pixels.
_display_width = CGDisplayPixelsWide(_display) * _detected_display_zoom;
_display_height = CGDisplayPixelsHigh(_display) * _detected_display_zoom;
load_display_information(); load_display_information();
if (cocoadisplay_cat.is_debug()) { if (cocoadisplay_cat.is_debug()) {
@ -64,19 +88,36 @@ load_display_information() {
// _display_information->_device_id = CGDisplaySerialNumber(_display); // _display_information->_device_id = CGDisplaySerialNumber(_display);
// Display modes // Display modes
CFDictionaryRef options = NULL;
const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
options = CFDictionaryCreate(NULL,
(const void **)dictkeys,
(const void **)dictvalues,
1,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
size_t num_modes = 0; size_t num_modes = 0;
CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, NULL); CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, options);
if (modes != NULL) { if (modes != NULL) {
num_modes = CFArrayGetCount(modes); num_modes = CFArrayGetCount(modes);
_display_information->_total_display_modes = num_modes; _display_information->_total_display_modes = num_modes;
_display_information->_display_mode_array = new DisplayMode[num_modes]; _display_information->_display_mode_array = new DisplayMode[num_modes];
} }
if (options != NULL) {
CFRelease(options);
}
for (size_t i = 0; i < num_modes; ++i) { for (size_t i = 0; i < num_modes; ++i) {
CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i); CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i);
_display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode); if (dpi_aware) {
_display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode); _display_information->_display_mode_array[i].width = CGDisplayModeGetPixelWidth(mode);
_display_information->_display_mode_array[i].height = CGDisplayModeGetPixelHeight(mode);
} else {
_display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode);
_display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode);
}
_display_information->_display_mode_array[i].refresh_rate = CGDisplayModeGetRefreshRate(mode); _display_information->_display_mode_array[i].refresh_rate = CGDisplayModeGetRefreshRate(mode);
_display_information->_display_mode_array[i].fullscreen_only = false; _display_information->_display_mode_array[i].fullscreen_only = false;

View File

@ -63,6 +63,7 @@ public:
void handle_minimize_event(bool minimized); void handle_minimize_event(bool minimized);
void handle_maximize_event(bool maximized); void handle_maximize_event(bool maximized);
void handle_foreground_event(bool foreground); void handle_foreground_event(bool foreground);
void handle_backing_change_event();
bool handle_close_request(); bool handle_close_request();
void handle_close_event(); void handle_close_event();
void handle_key_event(NSEvent *event); void handle_key_event(NSEvent *event);

View File

@ -132,13 +132,20 @@ move_pointer(int device, int x, int y) {
return true; return true;
} }
// Mouse position is expressed in screen points and not pixels, but in Panda3D
// we are using pixel coordinates.
// Instead of using convertPointFromBacking and have complex logic to cope with
// the change of coordinate system, we cheat and directly use the contents scale
// of the view layer to convert pixel coordinates into screen point coordinates.
CGFloat contents_scale = _view.layer.contentsScale;
if (device == 0) { if (device == 0) {
CGPoint point; CGPoint point;
if (_properties.get_fullscreen()) { if (_properties.get_fullscreen()) {
point = CGPointMake(x, y); point = CGPointMake(float(x) / contents_scale,
float(y) / contents_scale);
} else { } else {
point = CGPointMake(x + _properties.get_x_origin(), point = CGPointMake((float(x) + _properties.get_x_origin()) / contents_scale,
y + _properties.get_y_origin()); (float(y) + _properties.get_y_origin()) / contents_scale);
} }
if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) { if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {
@ -302,35 +309,89 @@ open_window() {
} }
} }
// Configure the origin and the size of the window.
// On macOS, screen coordinates are expressed in "points" which are independant
// of the pixel density of the screen. Panda3D however, expresses the size and
// origin of a window in pixels.
// So, when opening a window, we need to convert the origin and size from pixel
// units into point units. However, this conversion depends on the pixel density
// of the screen, the backing scale factor.
// As the origin and size of a window depends on the size of the screen of the
// parent view, their size must be converted first from points to pixels.
// If a window (or a view) is not configured to support high-dpi screen, macOS
// will upscale the window (or view) when displayed on a high-dpi screen.
// Therefore its backing scale factor will always be 1.0
// In a Panda3D application, windows are always configured as high resolution
// capable, but the view is only configured as high resolution if the dpi-aware
// configuration flag is set.
// If the app is not dpi-aware, we must upscale its size and origin from points
// into pixels as the window is always high resolution capable.
// Center the window if coordinates were set to -1 or -2 TODO: perhaps in // Center the window if coordinates were set to -1 or -2 TODO: perhaps in
// future, in the case of -1, it should use the origin used in a previous // future, in the case of -1, it should use the origin used in a previous
// run of Panda // run of Panda
// Size of the requested window
NSSize size = NSMakeSize(_properties.get_x_size(), _properties.get_y_size());
NSRect container; NSRect container;
CGFloat backing_scale_factor = screen.backingScaleFactor;
if (parent_nsview != NULL) { if (parent_nsview != NULL) {
container = [parent_nsview bounds]; // Convert parent view bounds into pixel units.
container = [parent_nsview convertRectToBacking:[parent_nsview bounds]];
// If the app is not dpi-aware, we must convert its size from points into
// pixels as the window is always high resolution capable
if (!dpi_aware) {
size = [parent_nsview convertSizeToBacking:size];
}
} else { } else {
container = [screen frame]; container = [screen frame];
container.origin = NSMakePoint(0, 0); container.origin = NSMakePoint(0, 0);
container = [screen convertRectToBacking:container];
if (!dpi_aware) {
// Weirdly NSScreen does not respond to convertSizeToBacking, so we have to
// create a dummy rect just for converting the window size.
NSRect rect;
rect.origin = NSMakePoint(0, 0);
rect.size = size;
rect = [screen convertRectToBacking:rect];
size = rect.size;
}
} }
int x = _properties.get_x_origin(); int x = _properties.get_x_origin();
int y = _properties.get_y_origin(); int y = _properties.get_y_origin();
// As we are converting a single value and the view is not created yet, it's
// easier to simply use the backing scale factor and don't bother with
// coordinate system transformations.
if (x < 0) { if (x < 0) {
x = floor(container.size.width / 2 - _properties.get_x_size() / 2); x = floor(container.size.width / 2 - size.width / 2);
} else if (!dpi_aware) {
x *= backing_scale_factor;
} }
if (y < 0) { if (y < 0) {
y = floor(container.size.height / 2 - _properties.get_y_size() / 2); y = floor(container.size.height / 2 - size.height / 2);
} else if (!dpi_aware) {
y *= backing_scale_factor;
}
if (dpi_aware) {
_properties.set_origin(x, y);
} else {
_properties.set_origin(x / backing_scale_factor, y / backing_scale_factor);
} }
_properties.set_origin(x, y);
if (_parent_window_handle == (WindowHandle *)NULL) { if (_parent_window_handle == (WindowHandle *)NULL) {
// Content rectangle // Content rectangle
NSRect rect; NSRect rect;
if (_properties.get_fullscreen()) { if (_properties.get_fullscreen()) {
rect = container; rect = [screen convertRectFromBacking:container];
} else { } else {
rect = NSMakeRect(x, container.size.height - _properties.get_y_size() - y, rect = NSMakeRect(x, container.size.height - size.height - y,
_properties.get_x_size(), _properties.get_y_size()); size.width, size.height);
if (parent_nsview != NULL) {
rect = [parent_nsview convertRectFromBacking:rect];
} else {
rect = [screen convertRectFromBacking:rect];
}
} }
// Configure the window decorations // Configure the window decorations
@ -388,8 +449,10 @@ open_window() {
_parent_window_handle->attach_child(_window_handle); _parent_window_handle->attach_child(_window_handle);
} }
// Always disable application HiDPI support, Cocoa will do the eventual upscaling for us. // Configure the view to be high resolution capable using the dpi-aware
[_view setWantsBestResolutionOpenGLSurface:NO]; // configuration flag. If dpi-aware is false, macOS will upscale the view
// for us.
[_view setWantsBestResolutionOpenGLSurface:dpi_aware];
if (_properties.has_icon_filename()) { if (_properties.has_icon_filename()) {
NSImage *image = load_image(_properties.get_icon_filename()); NSImage *image = load_image(_properties.get_icon_filename());
if (image != nil) { if (image != nil) {
@ -596,22 +659,6 @@ set_properties_now(WindowProperties &properties) {
} }
if (switched) { if (switched) {
if (_window != nil) {
// For some reason, setting the style mask makes it give up its
// first-responder status. And for some reason, we need to first
// restore the window to normal level before we switch fullscreen,
// otherwise we may get a black bar if we're currently on Z_top.
if (_properties.get_z_order() != WindowProperties::Z_normal) {
[_window setLevel: NSNormalWindowLevel];
}
if ([_window respondsToSelector:@selector(setStyleMask:)]) {
[_window setStyleMask:NSBorderlessWindowMask];
}
[_window makeFirstResponder:_view];
[_window setLevel:CGShieldingWindowLevel()];
[_window makeKeyAndOrderFront:nil];
}
// We've already set the size property this way; clear it. // We've already set the size property this way; clear it.
properties.clear_size(); properties.clear_size();
_properties.set_size(width, height); _properties.set_size(width, height);
@ -684,6 +731,9 @@ set_properties_now(WindowProperties &properties) {
NSMiniaturizableWindowMask | NSResizableWindowMask ]; NSMiniaturizableWindowMask | NSResizableWindowMask ];
} }
[_window makeFirstResponder:_view]; [_window makeFirstResponder:_view];
// Resize event fired by makeFirstResponder has an invalid backing scale factor
// The actual size must be reset afterward
handle_resize_event();
} }
} }
@ -705,6 +755,9 @@ set_properties_now(WindowProperties &properties) {
NSMiniaturizableWindowMask | NSResizableWindowMask ]; NSMiniaturizableWindowMask | NSResizableWindowMask ];
} }
[_window makeFirstResponder:_view]; [_window makeFirstResponder:_view];
// Resize event fired by makeFirstResponder has an invalid backing scale factor
// The actual size must be reset afterward
handle_resize_event();
} }
properties.clear_undecorated(); properties.clear_undecorated();
@ -715,10 +768,13 @@ set_properties_now(WindowProperties &properties) {
int height = properties.get_y_size(); int height = properties.get_y_size();
if (!_properties.get_fullscreen()) { if (!_properties.get_fullscreen()) {
// We use the view, not the window, to convert the frame size, expressed
// in pixels, into points as the "dpi awareness" is managed by the view.
NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)];
if (_window != nil) { if (_window != nil) {
[_window setContentSize:NSMakeSize(width, height)]; [_window setContentSize:size];
} }
[_view setFrameSize:NSMakeSize(width, height)]; [_view setFrameSize:size];
if (cocoadisplay_cat.is_debug()) { if (cocoadisplay_cat.is_debug()) {
cocoadisplay_cat.debug() cocoadisplay_cat.debug()
@ -768,12 +824,14 @@ set_properties_now(WindowProperties &properties) {
// Get the frame for the screen // Get the frame for the screen
NSRect frame; NSRect frame;
NSRect container; NSRect container;
// Note again that we are using the view to convert the frame and container
// size from points into pixels.
if (_window != nil) { if (_window != nil) {
NSRect window_frame = [_window frame]; NSRect window_frame = [_window frame];
frame = [_window contentRectForFrameRect:window_frame]; frame = [_view convertRectToBacking:[_window contentRectForFrameRect:window_frame]];
NSScreen *screen = [_window screen]; NSScreen *screen = [_window screen];
nassertv(screen != nil); nassertv(screen != nil);
container = [screen frame]; container = [_view convertRectToBacking:[screen frame]];
// Prevent the centering from overlapping the Dock // Prevent the centering from overlapping the Dock
if (y < 0) { if (y < 0) {
@ -783,8 +841,8 @@ set_properties_now(WindowProperties &properties) {
} }
} }
} else { } else {
frame = [_view frame]; frame = [_view convertRectToBacking:[_view frame]];
container = [[_view superview] frame]; container = [[_view superview] convertRectToBacking:[[_view superview] frame]];
} }
if (x < 0) { if (x < 0) {
@ -795,22 +853,22 @@ set_properties_now(WindowProperties &properties) {
} }
_properties.set_origin(x, y); _properties.set_origin(x, y);
if (!_properties.get_fullscreen()) { frame.origin.x = x;
// Remember, Mac OS X coordinates are flipped in the vertical axis. // Y coordinate in backing store is not flipped, but origin is still at the bottom left
frame.origin.x = x; frame.origin.y = y - container.size.height;
frame.origin.y = container.size.height - y - frame.size.height;
if (cocoadisplay_cat.is_debug()) { if (cocoadisplay_cat.is_debug()) {
cocoadisplay_cat.debug() cocoadisplay_cat.debug()
<< "Setting window content origin to " << "Setting window content origin to "
<< frame.origin.x << ", " << frame.origin.y << "\n"; << frame.origin.x << ", " << frame.origin.y << "\n";
} }
if (_window != nil) { if (_window != nil) {
[_window setFrame:[_window frameRectForContentRect:frame] display:NO]; frame = [_view convertRectFromBacking:frame];
} else { [_window setFrame:[_window frameRectForContentRect:frame] display:NO];
[_view setFrame:frame]; } else {
} frame = [_view convertRectFromBacking:frame];
[_view setFrame:frame];
} }
properties.clear_origin(); properties.clear_origin();
} }
@ -957,24 +1015,20 @@ unbind_context() {
CFMutableArrayRef CocoaGraphicsWindow:: CFMutableArrayRef CocoaGraphicsWindow::
find_display_modes(int width, int height) { find_display_modes(int width, int height) {
CFDictionaryRef options = NULL; CFDictionaryRef options = NULL;
// On macOS 10.15+ (Catalina), we want to select the display mode with the // We want to select the display mode with the same scaling factor as the
// samescaling factor as the current view to avoid cropping or scaling issues. // current view to avoid cropping or scaling issues.
// This is a workaround until HiDPI display or scaling factor is properly // CGDisplayCopyAllDisplayModes() does not return upscaled display modes
// handled. CGDisplayCopyAllDisplayModes() does not return upscaled display // nor the current mode, unless explicitly asked with
// mode unless explicitly asked with kCGDisplayShowDuplicateLowResolutionModes // kCGDisplayShowDuplicateLowResolutionModes
// (which is undocumented...). // (which is undocumented...).
bool macos_10_15_or_higher = false; const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes};
if (@available(macOS 10.15, *)) { const CFBooleanRef dictvalues[] = {kCFBooleanTrue};
const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; options = CFDictionaryCreate(NULL,
const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; (const void **)dictkeys,
options = CFDictionaryCreate(NULL, (const void **)dictvalues,
(const void **)dictkeys, 1,
(const void **)dictvalues, &kCFCopyStringDictionaryKeyCallBacks,
1, &kCFTypeDictionaryValueCallBacks);
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
macos_10_15_or_higher = true;
}
CFMutableArrayRef valid_modes; CFMutableArrayRef valid_modes;
valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
@ -985,29 +1039,39 @@ find_display_modes(int width, int height) {
size_t num_modes = CFArrayGetCount(modes); size_t num_modes = CFArrayGetCount(modes);
CGDisplayModeRef mode; CGDisplayModeRef mode;
// Get the current refresh rate and pixel encoding.
CFStringRef current_pixel_encoding;
double refresh_rate;
mode = CGDisplayCopyDisplayMode(_display); mode = CGDisplayCopyDisplayMode(_display);
// Calculate requested display size and pixel size
CGSize display_size;
CGSize pixel_size;
if (dpi_aware) {
pixel_size = NSMakeSize(width, height);
display_size = [_view convertSizeFromBacking:pixel_size];
} else {
display_size = NSMakeSize(width, height);
// Calculate the pixel width and height of the fullscreen mode we want using
// the current display mode dimensions and pixel dimensions.
size_t pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
size_t pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
pixel_size = NSMakeSize(pixel_width, pixel_height);
}
// First check if the current mode is adequate. // First check if the current mode is adequate.
// This test not done for macOS 10.15 and above as the mode resolution is if (CGDisplayModeGetWidth(mode) == display_size.width &&
// not enough to identify a mode. CGDisplayModeGetHeight(mode) == display_size.height &&
if (!macos_10_15_or_higher && CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
CGDisplayModeGetWidth(mode) == width && CGDisplayModeGetPixelHeight(mode) == pixel_size.height) {
CGDisplayModeGetHeight(mode) == height) {
CFArrayAppendValue(valid_modes, mode); CFArrayAppendValue(valid_modes, mode);
CGDisplayModeRelease(mode); CGDisplayModeRelease(mode);
return valid_modes; return valid_modes;
} }
// Get the current refresh rate and pixel encoding.
CFStringRef current_pixel_encoding;
double refresh_rate;
current_pixel_encoding = CGDisplayModeCopyPixelEncoding(mode); current_pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
refresh_rate = CGDisplayModeGetRefreshRate(mode); refresh_rate = CGDisplayModeGetRefreshRate(mode);
// Calculate the pixel width and height of the fullscreen mode we want using
// the currentdisplay mode dimensions and pixel dimensions.
size_t expected_pixel_width = (size_t(width) * CGDisplayModeGetPixelWidth(mode)) / CGDisplayModeGetWidth(mode);
size_t expected_pixel_height = (size_t(height) * CGDisplayModeGetPixelHeight(mode)) / CGDisplayModeGetHeight(mode);
CGDisplayModeRelease(mode); CGDisplayModeRelease(mode);
for (size_t i = 0; i < num_modes; ++i) { for (size_t i = 0; i < num_modes; ++i) {
@ -1015,17 +1079,15 @@ find_display_modes(int width, int height) {
CFStringRef pixel_encoding = CGDisplayModeCopyPixelEncoding(mode); CFStringRef pixel_encoding = CGDisplayModeCopyPixelEncoding(mode);
// As explained above, we want to select the fullscreen display mode using // We select the fullscreen display mode using he same scaling factor
// the same scaling factor, but only for MacOS 10.15+ To do this we check // To do this we check the mode width and height but also actual pixel widh
// the mode width and height but also actual pixel widh and height. // and height.
if (CGDisplayModeGetWidth(mode) == width && if (CGDisplayModeGetWidth(mode) == display_size.width &&
CGDisplayModeGetHeight(mode) == height && CGDisplayModeGetHeight(mode) == display_size.height &&
(int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) && (int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) &&
(!macos_10_15_or_higher || CGDisplayModeGetPixelWidth(mode) == pixel_size.width &&
(CGDisplayModeGetPixelWidth(mode) == expected_pixel_width && CGDisplayModeGetPixelHeight(mode) == pixel_size.height &&
CGDisplayModeGetPixelHeight(mode) == expected_pixel_height)) &&
CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) { CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) {
if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) { if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) {
// Exact match for refresh rate, prioritize this. // Exact match for refresh rate, prioritize this.
CFArrayInsertValueAtIndex(valid_modes, 0, mode); CFArrayInsertValueAtIndex(valid_modes, 0, mode);
@ -1103,14 +1165,25 @@ do_switch_fullscreen(CGDisplayModeRef mode) {
NSRect frame = [[[_view window] screen] frame]; NSRect frame = [[[_view window] screen] frame];
if (cocoadisplay_cat.is_debug()) { if (cocoadisplay_cat.is_debug()) {
NSString *str = NSStringFromRect(frame); NSString *str = NSStringFromSize([_view convertSizeToBacking:frame.size]);
cocoadisplay_cat.debug() cocoadisplay_cat.debug()
<< "Switched to fullscreen, screen rect is now " << [str UTF8String] << "\n"; << "Switched to fullscreen, screen size is now " << [str UTF8String] << "\n";
} }
if (_window != nil) { if (_window != nil) {
// For some reason, setting the style mask makes it give up its
// first-responder status.
if ([_window respondsToSelector:@selector(setStyleMask:)]) {
[_window setStyleMask:NSBorderlessWindowMask];
}
[_window makeFirstResponder:_view];
[_window setLevel:CGShieldingWindowLevel()];
[_window makeKeyAndOrderFront:nil];
// Window and view frame must be updated *after* the window reconfiguration
// or the size is not set properly !
[_window setFrame:frame display:YES]; [_window setFrame:frame display:YES];
[_view setFrame:NSMakeRect(0, 0, frame.size.width, frame.size.height)]; [_view setFrame:frame];
[_window update]; [_window update];
} }
} }
@ -1252,19 +1325,25 @@ load_cursor(const Filename &filename) {
*/ */
void CocoaGraphicsWindow:: void CocoaGraphicsWindow::
handle_move_event() { handle_move_event() {
// Remember, Mac OS X uses flipped coordinates
NSRect frame; NSRect frame;
NSRect container;
int x, y; int x, y;
// Again, we are using the view to convert the frame and container size from
// points to pixels.
if (_window == nil) { if (_window == nil) {
frame = [_view frame]; frame = [_view convertRectToBacking:[_view frame]];
x = frame.origin.x; container = [_view convertRectToBacking:[[_view superview] frame]];
y = [[_view superview] bounds].size.height - frame.origin.y - frame.size.height;
} else { } else {
frame = [_window contentRectForFrameRect:[_window frame]]; frame = [_view convertRectToBacking:[_window contentRectForFrameRect:[_window frame]]];
x = frame.origin.x; NSScreen *screen = [_window screen];
y = [[_window screen] frame].size.height - frame.origin.y - frame.size.height; nassertv(screen != nil);
container = [_view convertRectToBacking:[screen frame]];
} }
// Y coordinate in backing store is not flipped, but origin is still at the bottom left
x = frame.origin.x;
y = container.size.height + frame.origin.y;
if (x != _properties.get_x_origin() || if (x != _properties.get_x_origin() ||
y != _properties.get_y_origin()) { y != _properties.get_y_origin()) {
@ -1290,7 +1369,7 @@ handle_resize_event() {
[_view setFrameSize:contentRect.size]; [_view setFrameSize:contentRect.size];
} }
NSRect frame = [_view convertRect:[_view bounds] toView:nil]; NSRect frame = [_view convertRectToBacking:[_view bounds]];
WindowProperties properties; WindowProperties properties;
bool changed = false; bool changed = false;
@ -1403,6 +1482,22 @@ handle_foreground_event(bool foreground) {
} }
} }
/**
* Called by the window delegate when the properties of backing store of the
* window have changed.
*/
void CocoaGraphicsWindow::
handle_backing_change_event() {
if (cocoadisplay_cat.is_debug()) {
cocoadisplay_cat.debug() << "Backing store properties have changed\n";
}
// Trigger a resize event to update the window size in case the backing scale
// factor did change.
handle_resize_event();
}
/** /**
* Called by the window delegate when the user requests to close the window. * Called by the window delegate when the user requests to close the window.
* This may not always be called, which is why there is also a * This may not always be called, which is why there is also a
@ -1693,6 +1788,13 @@ void CocoaGraphicsWindow::
handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) { handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
double nx, ny; double nx, ny;
// Mouse position is received in screen points and not pixels, but in Panda3D
// we want to have the coordinates expressed in pixels.
// Instead of using convertPointFrom/toBackingStore and have complex logic to
// cope with the change of coordinate system, we cheat and directly use the
// contents scale of the view layer to convert screen point into pixels and
// vice-versa.
CGFloat contents_scale = _view.layer.contentsScale;
if (absolute) { if (absolute) {
if (cocoadisplay_cat.is_spam()) { if (cocoadisplay_cat.is_spam()) {
if (in_window != _input->get_pointer().get_in_window()) { if (in_window != _input->get_pointer().get_in_window()) {
@ -1704,14 +1806,14 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
} }
} }
nx = x; nx = x * contents_scale;
ny = y; ny = y * contents_scale;
} else { } else {
// We received deltas, so add it to the current mouse position. // We received deltas, so add it to the current mouse position.
PointerData md = _input->get_pointer(); PointerData md = _input->get_pointer();
nx = md.get_x() + x; nx = md.get_x() + x * contents_scale;
ny = md.get_y() + y; ny = md.get_y() + y * contents_scale;
} }
if (_properties.get_mouse_mode() == WindowProperties::M_confined if (_properties.get_mouse_mode() == WindowProperties::M_confined
@ -1721,11 +1823,13 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
nx = std::max(0., std::min((double) get_x_size() - 1, nx)); nx = std::max(0., std::min((double) get_x_size() - 1, nx));
ny = std::max(0., std::min((double) get_y_size() - 1, ny)); ny = std::max(0., std::min((double) get_y_size() - 1, ny));
// Convert back mouse position to screen space using point units
if (_properties.get_fullscreen()) { if (_properties.get_fullscreen()) {
point = CGPointMake(nx, ny); point = CGPointMake(nx / contents_scale,
ny / contents_scale);
} else { } else {
point = CGPointMake(nx + _properties.get_x_origin(), point = CGPointMake((nx + _properties.get_x_origin()) / contents_scale,
ny + _properties.get_y_origin()); (ny + _properties.get_y_origin()) / contents_scale);
} }
if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) { if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) {

View File

@ -29,6 +29,7 @@ class CocoaGraphicsWindow;
- (void)windowDidDeminiaturize:(NSNotification *)notification; - (void)windowDidDeminiaturize:(NSNotification *)notification;
- (void)windowDidBecomeKey:(NSNotification *)notification; - (void)windowDidBecomeKey:(NSNotification *)notification;
- (void)windowDidResignKey:(NSNotification *)notification; - (void)windowDidResignKey:(NSNotification *)notification;
- (void)windowDidChangeBackingProperties:(NSNotification *)notification;
- (BOOL)windowShouldClose:(id)sender; - (BOOL)windowShouldClose:(id)sender;
- (void)windowWillClose:(id)sender; - (void)windowWillClose:(id)sender;

View File

@ -51,6 +51,10 @@
_graphicsWindow->handle_foreground_event(false); _graphicsWindow->handle_foreground_event(false);
} }
- (void) windowDidChangeBackingProperties:(NSNotification *)notification {
_graphicsWindow->handle_backing_change_event();
}
- (BOOL) windowShouldClose:(id)sender { - (BOOL) windowShouldClose:(id)sender {
if (cocoadisplay_cat.is_debug()) { if (cocoadisplay_cat.is_debug()) {
cocoadisplay_cat.debug() cocoadisplay_cat.debug()

View File

@ -21,6 +21,7 @@
NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY); NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY);
extern ConfigVariableBool cocoa_invert_wheel_x; extern ConfigVariableBool cocoa_invert_wheel_x;
extern ConfigVariableBool dpi_aware;
extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay(); extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay();

View File

@ -32,6 +32,11 @@ ConfigVariableBool cocoa_invert_wheel_x
("cocoa-invert-wheel-x", false, ("cocoa-invert-wheel-x", false,
PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse " PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse "
"button events, to restore to the pre-1.10.12 behavior.")); "button events, to restore to the pre-1.10.12 behavior."));
ConfigVariableBool dpi_aware
("dpi-aware", false,
PRC_DESC("The default behavior on macOS is for Panda3D to use upscaling on"
"high DPI screen. Set this to true to let the application use the"
"actual pixel density of the screen."));
/** /**
* Initializes the library. This must be called at least once before any of * Initializes the library. This must be called at least once before any of