From ae3cbe4b121c453582648d1ea5eefc1cd16474d9 Mon Sep 17 00:00:00 2001 From: LD <44778133+el-dee@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:04 +0200 Subject: [PATCH] cocoadisplay: Add support for high-dpi screens (#1308) --- direct/src/dist/commands.py | 1 + panda/src/cocoadisplay/cocoaGraphicsPipe.mm | 51 ++- panda/src/cocoadisplay/cocoaGraphicsWindow.h | 1 + panda/src/cocoadisplay/cocoaGraphicsWindow.mm | 314 ++++++++++++------ .../cocoadisplay/cocoaPandaWindowDelegate.h | 1 + .../cocoadisplay/cocoaPandaWindowDelegate.mm | 4 + panda/src/cocoadisplay/config_cocoadisplay.h | 1 + panda/src/cocoadisplay/config_cocoadisplay.mm | 5 + 8 files changed, 268 insertions(+), 110 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 5410199307..3bfafe77c3 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -717,6 +717,7 @@ class build_apps(setuptools.Command): 'CFBundlePackageType': 'APPL', 'CFBundleSignature': '', #TODO 'CFBundleExecutable': self.macos_main_app, + 'NSHighResolutionCapable': 'True', } icon = self.icon_objects.get( diff --git a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm index 0ece9f83a2..83a487e9fd 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsPipe.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsPipe.mm @@ -40,12 +40,36 @@ CocoaGraphicsPipe(CGDirectDisplayID display) : _display(display) { [thread start]; [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 // the application icon to start bouncing, which may be undesirable for // apps that will never open a window. - _display_width = CGDisplayPixelsWide(_display); - _display_height = CGDisplayPixelsHigh(_display); + // Although the name of these functions mention pixels, they actually return + // 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(); if (cocoadisplay_cat.is_debug()) { @@ -64,19 +88,36 @@ load_display_information() { // _display_information->_device_id = CGDisplaySerialNumber(_display); // 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; - CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, NULL); + CFArrayRef modes = CGDisplayCopyAllDisplayModes(_display, options); if (modes != NULL) { num_modes = CFArrayGetCount(modes); _display_information->_total_display_modes = 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) { CGDisplayModeRef mode = (CGDisplayModeRef) CFArrayGetValueAtIndex(modes, i); - _display_information->_display_mode_array[i].width = CGDisplayModeGetWidth(mode); - _display_information->_display_mode_array[i].height = CGDisplayModeGetHeight(mode); + if (dpi_aware) { + _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].fullscreen_only = false; diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.h b/panda/src/cocoadisplay/cocoaGraphicsWindow.h index 19b9b6004a..e555489d24 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.h +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.h @@ -63,6 +63,7 @@ public: void handle_minimize_event(bool minimized); void handle_maximize_event(bool maximized); void handle_foreground_event(bool foreground); + void handle_backing_change_event(); bool handle_close_request(); void handle_close_event(); void handle_key_event(NSEvent *event); diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm index d212c84ebb..e76cf38fa0 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm @@ -132,13 +132,20 @@ move_pointer(int device, int x, int y) { 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) { CGPoint point; if (_properties.get_fullscreen()) { - point = CGPointMake(x, y); + point = CGPointMake(float(x) / contents_scale, + float(y) / contents_scale); } else { - point = CGPointMake(x + _properties.get_x_origin(), - y + _properties.get_y_origin()); + point = CGPointMake((float(x) + _properties.get_x_origin()) / contents_scale, + (float(y) + _properties.get_y_origin()) / contents_scale); } 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 // future, in the case of -1, it should use the origin used in a previous // run of Panda + + // Size of the requested window + NSSize size = NSMakeSize(_properties.get_x_size(), _properties.get_y_size()); NSRect container; + CGFloat backing_scale_factor = screen.backingScaleFactor; 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 { container = [screen frame]; 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 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) { - 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) { - 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) { // Content rectangle NSRect rect; if (_properties.get_fullscreen()) { - rect = container; + rect = [screen convertRectFromBacking:container]; } else { - rect = NSMakeRect(x, container.size.height - _properties.get_y_size() - y, - _properties.get_x_size(), _properties.get_y_size()); + rect = NSMakeRect(x, container.size.height - size.height - y, + size.width, size.height); + if (parent_nsview != NULL) { + rect = [parent_nsview convertRectFromBacking:rect]; + } else { + rect = [screen convertRectFromBacking:rect]; + } } // Configure the window decorations @@ -388,8 +449,10 @@ open_window() { _parent_window_handle->attach_child(_window_handle); } - // Always disable application HiDPI support, Cocoa will do the eventual upscaling for us. - [_view setWantsBestResolutionOpenGLSurface:NO]; + // Configure the view to be high resolution capable using the dpi-aware + // configuration flag. If dpi-aware is false, macOS will upscale the view + // for us. + [_view setWantsBestResolutionOpenGLSurface:dpi_aware]; if (_properties.has_icon_filename()) { NSImage *image = load_image(_properties.get_icon_filename()); if (image != nil) { @@ -596,22 +659,6 @@ set_properties_now(WindowProperties &properties) { } 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. properties.clear_size(); _properties.set_size(width, height); @@ -684,6 +731,9 @@ set_properties_now(WindowProperties &properties) { NSMiniaturizableWindowMask | NSResizableWindowMask ]; } [_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 ]; } [_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(); @@ -715,10 +768,13 @@ set_properties_now(WindowProperties &properties) { int height = properties.get_y_size(); 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) { - [_window setContentSize:NSMakeSize(width, height)]; + [_window setContentSize:size]; } - [_view setFrameSize:NSMakeSize(width, height)]; + [_view setFrameSize:size]; if (cocoadisplay_cat.is_debug()) { cocoadisplay_cat.debug() @@ -768,12 +824,14 @@ set_properties_now(WindowProperties &properties) { // Get the frame for the screen NSRect frame; NSRect container; + // Note again that we are using the view to convert the frame and container + // size from points into pixels. if (_window != nil) { NSRect window_frame = [_window frame]; - frame = [_window contentRectForFrameRect:window_frame]; + frame = [_view convertRectToBacking:[_window contentRectForFrameRect:window_frame]]; NSScreen *screen = [_window screen]; nassertv(screen != nil); - container = [screen frame]; + container = [_view convertRectToBacking:[screen frame]]; // Prevent the centering from overlapping the Dock if (y < 0) { @@ -783,8 +841,8 @@ set_properties_now(WindowProperties &properties) { } } } else { - frame = [_view frame]; - container = [[_view superview] frame]; + frame = [_view convertRectToBacking:[_view frame]]; + container = [[_view superview] convertRectToBacking:[[_view superview] frame]]; } if (x < 0) { @@ -795,22 +853,22 @@ set_properties_now(WindowProperties &properties) { } _properties.set_origin(x, y); - if (!_properties.get_fullscreen()) { - // Remember, Mac OS X coordinates are flipped in the vertical axis. - frame.origin.x = x; - frame.origin.y = container.size.height - y - frame.size.height; + frame.origin.x = x; + // Y coordinate in backing store is not flipped, but origin is still at the bottom left + frame.origin.y = y - container.size.height; - if (cocoadisplay_cat.is_debug()) { - cocoadisplay_cat.debug() - << "Setting window content origin to " - << frame.origin.x << ", " << frame.origin.y << "\n"; - } + if (cocoadisplay_cat.is_debug()) { + cocoadisplay_cat.debug() + << "Setting window content origin to " + << frame.origin.x << ", " << frame.origin.y << "\n"; + } - if (_window != nil) { - [_window setFrame:[_window frameRectForContentRect:frame] display:NO]; - } else { - [_view setFrame:frame]; - } + if (_window != nil) { + frame = [_view convertRectFromBacking:frame]; + [_window setFrame:[_window frameRectForContentRect:frame] display:NO]; + } else { + frame = [_view convertRectFromBacking:frame]; + [_view setFrame:frame]; } properties.clear_origin(); } @@ -957,24 +1015,20 @@ unbind_context() { CFMutableArrayRef CocoaGraphicsWindow:: find_display_modes(int width, int height) { CFDictionaryRef options = NULL; - // On macOS 10.15+ (Catalina), we want to select the display mode with the - // samescaling factor as the current view to avoid cropping or scaling issues. - // This is a workaround until HiDPI display or scaling factor is properly - // handled. CGDisplayCopyAllDisplayModes() does not return upscaled display - // mode unless explicitly asked with kCGDisplayShowDuplicateLowResolutionModes + // We want to select the display mode with the same scaling factor as the + // current view to avoid cropping or scaling issues. + // CGDisplayCopyAllDisplayModes() does not return upscaled display modes + // nor the current mode, unless explicitly asked with + // kCGDisplayShowDuplicateLowResolutionModes // (which is undocumented...). - bool macos_10_15_or_higher = false; - if (@available(macOS 10.15, *)) { - const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; - const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; - options = CFDictionaryCreate(NULL, - (const void **)dictkeys, - (const void **)dictvalues, - 1, - &kCFCopyStringDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks); - macos_10_15_or_higher = true; - } + const CFStringRef dictkeys[] = {kCGDisplayShowDuplicateLowResolutionModes}; + const CFBooleanRef dictvalues[] = {kCFBooleanTrue}; + options = CFDictionaryCreate(NULL, + (const void **)dictkeys, + (const void **)dictvalues, + 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); CFMutableArrayRef valid_modes; valid_modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); @@ -985,29 +1039,39 @@ find_display_modes(int width, int height) { size_t num_modes = CFArrayGetCount(modes); CGDisplayModeRef mode; - - // Get the current refresh rate and pixel encoding. - CFStringRef current_pixel_encoding; - double refresh_rate; 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. - // This test not done for macOS 10.15 and above as the mode resolution is - // not enough to identify a mode. - if (!macos_10_15_or_higher && - CGDisplayModeGetWidth(mode) == width && - CGDisplayModeGetHeight(mode) == height) { + if (CGDisplayModeGetWidth(mode) == display_size.width && + CGDisplayModeGetHeight(mode) == display_size.height && + CGDisplayModeGetPixelWidth(mode) == pixel_size.width && + CGDisplayModeGetPixelHeight(mode) == pixel_size.height) { CFArrayAppendValue(valid_modes, mode); CGDisplayModeRelease(mode); return valid_modes; } + // Get the current refresh rate and pixel encoding. + CFStringRef current_pixel_encoding; + double refresh_rate; + current_pixel_encoding = CGDisplayModeCopyPixelEncoding(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); 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); - // As explained above, we want to select the fullscreen display mode using - // the same scaling factor, but only for MacOS 10.15+ To do this we check - // the mode width and height but also actual pixel widh and height. - if (CGDisplayModeGetWidth(mode) == width && - CGDisplayModeGetHeight(mode) == height && + // We select the fullscreen display mode using he same scaling factor + // To do this we check the mode width and height but also actual pixel widh + // and height. + if (CGDisplayModeGetWidth(mode) == display_size.width && + CGDisplayModeGetHeight(mode) == display_size.height && (int)(CGDisplayModeGetRefreshRate(mode) + 0.5) == (int)(refresh_rate + 0.5) && - (!macos_10_15_or_higher || - (CGDisplayModeGetPixelWidth(mode) == expected_pixel_width && - CGDisplayModeGetPixelHeight(mode) == expected_pixel_height)) && + CGDisplayModeGetPixelWidth(mode) == pixel_size.width && + CGDisplayModeGetPixelHeight(mode) == pixel_size.height && CFStringCompare(pixel_encoding, current_pixel_encoding, 0) == kCFCompareEqualTo) { - if (CGDisplayModeGetRefreshRate(mode) == refresh_rate) { // Exact match for refresh rate, prioritize this. CFArrayInsertValueAtIndex(valid_modes, 0, mode); @@ -1103,14 +1165,25 @@ do_switch_fullscreen(CGDisplayModeRef mode) { NSRect frame = [[[_view window] screen] frame]; if (cocoadisplay_cat.is_debug()) { - NSString *str = NSStringFromRect(frame); + NSString *str = NSStringFromSize([_view convertSizeToBacking:frame.size]); 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) { + // 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]; - [_view setFrame:NSMakeRect(0, 0, frame.size.width, frame.size.height)]; + [_view setFrame:frame]; [_window update]; } } @@ -1252,19 +1325,25 @@ load_cursor(const Filename &filename) { */ void CocoaGraphicsWindow:: handle_move_event() { - // Remember, Mac OS X uses flipped coordinates NSRect frame; + NSRect container; int x, y; + // Again, we are using the view to convert the frame and container size from + // points to pixels. if (_window == nil) { - frame = [_view frame]; - x = frame.origin.x; - y = [[_view superview] bounds].size.height - frame.origin.y - frame.size.height; + frame = [_view convertRectToBacking:[_view frame]]; + container = [_view convertRectToBacking:[[_view superview] frame]]; } else { - frame = [_window contentRectForFrameRect:[_window frame]]; - x = frame.origin.x; - y = [[_window screen] frame].size.height - frame.origin.y - frame.size.height; + frame = [_view convertRectToBacking:[_window contentRectForFrameRect:[_window frame]]]; + NSScreen *screen = [_window screen]; + 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() || y != _properties.get_y_origin()) { @@ -1290,7 +1369,7 @@ handle_resize_event() { [_view setFrameSize:contentRect.size]; } - NSRect frame = [_view convertRect:[_view bounds] toView:nil]; + NSRect frame = [_view convertRectToBacking:[_view bounds]]; WindowProperties properties; 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. * 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) { 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 (cocoadisplay_cat.is_spam()) { 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; - ny = y; + nx = x * contents_scale; + ny = y * contents_scale; } else { // We received deltas, so add it to the current mouse position. PointerData md = _input->get_pointer(); - nx = md.get_x() + x; - ny = md.get_y() + y; + nx = md.get_x() + x * contents_scale; + ny = md.get_y() + y * contents_scale; } 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)); 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()) { - point = CGPointMake(nx, ny); + point = CGPointMake(nx / contents_scale, + ny / contents_scale); } else { - point = CGPointMake(nx + _properties.get_x_origin(), - ny + _properties.get_y_origin()); + point = CGPointMake((nx + _properties.get_x_origin()) / contents_scale, + (ny + _properties.get_y_origin()) / contents_scale); } if (CGWarpMouseCursorPosition(point) == kCGErrorSuccess) { diff --git a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h index 490db59ee8..0ada863fd6 100644 --- a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h +++ b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.h @@ -29,6 +29,7 @@ class CocoaGraphicsWindow; - (void)windowDidDeminiaturize:(NSNotification *)notification; - (void)windowDidBecomeKey:(NSNotification *)notification; - (void)windowDidResignKey:(NSNotification *)notification; +- (void)windowDidChangeBackingProperties:(NSNotification *)notification; - (BOOL)windowShouldClose:(id)sender; - (void)windowWillClose:(id)sender; diff --git a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm index fe67c50c13..b5817f977a 100644 --- a/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm +++ b/panda/src/cocoadisplay/cocoaPandaWindowDelegate.mm @@ -51,6 +51,10 @@ _graphicsWindow->handle_foreground_event(false); } +- (void) windowDidChangeBackingProperties:(NSNotification *)notification { + _graphicsWindow->handle_backing_change_event(); +} + - (BOOL) windowShouldClose:(id)sender { if (cocoadisplay_cat.is_debug()) { cocoadisplay_cat.debug() diff --git a/panda/src/cocoadisplay/config_cocoadisplay.h b/panda/src/cocoadisplay/config_cocoadisplay.h index 43051a67c5..fff6c5ba67 100644 --- a/panda/src/cocoadisplay/config_cocoadisplay.h +++ b/panda/src/cocoadisplay/config_cocoadisplay.h @@ -21,6 +21,7 @@ NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY); extern ConfigVariableBool cocoa_invert_wheel_x; +extern ConfigVariableBool dpi_aware; extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay(); diff --git a/panda/src/cocoadisplay/config_cocoadisplay.mm b/panda/src/cocoadisplay/config_cocoadisplay.mm index 17688586c2..5877f88dd2 100644 --- a/panda/src/cocoadisplay/config_cocoadisplay.mm +++ b/panda/src/cocoadisplay/config_cocoadisplay.mm @@ -32,6 +32,11 @@ ConfigVariableBool cocoa_invert_wheel_x ("cocoa-invert-wheel-x", false, 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.")); +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