diff --git a/CMakeLists.txt b/CMakeLists.txt index a4f3db4..221927b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,8 +288,17 @@ elseif(APPLE) message(FATAL_ERROR "Cocoa framework not found") endif() + find_library(COREVIDEO_FRAMEWORK CoreVideo) + if (NOT COREVIDEO_FRAMEWORK) + message(FATAL_ERROR "CoreVideo framework not found") + endif() + if(ENABLE_METAL) find_library(METAL_FRAMEWORK Metal) + find_library(QUARTZCORE_FRAMEWORK QuartzCore) + if (NOT QUARTZCORE_FRAMEWORK) + message(FATAL_ERROR "QuartzCore framework not found") + endif() endif() if(ENABLE_OPENGL) @@ -298,7 +307,7 @@ elseif(APPLE) message(FATAL_ERROR "OpenGL framework not found") endif() endif() - + find_library(UNIFORMTYPEIDENTIFIERS_FRAMEWORK UniformTypeIdentifiers) if (NOT UNIFORMTYPEIDENTIFIERS_FRAMEWORK) message(FATAL_ERROR "UniformTypeIdentifiers framework not found") @@ -357,13 +366,13 @@ target_compile_definitions(zwidget PRIVATE ${ZWIDGET_DEFINES}) target_include_directories(zwidget PRIVATE ${ZWIDGET_INCLUDE_DIRS}) target_link_options(zwidget PRIVATE ${ZWIDGET_LINK_OPTIONS}) if(APPLE) - target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS} ${COCOA_FRAMEWORK} ${UNIFORMTYPEIDENTIFIERS_FRAMEWORK}) + target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS} ${COCOA_FRAMEWORK} ${COREVIDEO_FRAMEWORK} ${UNIFORMTYPEIDENTIFIERS_FRAMEWORK} ${QUARTZCORE_FRAMEWORK}) if(ENABLE_OPENGL AND OPENGL_FRAMEWORK) target_link_libraries(zwidget PRIVATE ${OPENGL_FRAMEWORK}) target_compile_definitions(zwidget PRIVATE HAVE_OPENGL) endif() if(ENABLE_METAL AND METAL_FRAMEWORK) - target_link_libraries(zwidget PRIVATE ${METAL_FRAMEWORK}) + target_link_libraries(zwidget PRIVATE ${METAL_FRAMEWORK} ${QUARTZCORE_FRAMEWORK}) target_compile_definitions(zwidget PRIVATE HAVE_METAL) endif() else() @@ -373,7 +382,7 @@ if(SDL3_FOUND) target_include_directories(zwidget PRIVATE ${SDL3_INCLUDE_DIRS}) if(TARGET SDL3::SDL3) target_link_libraries(zwidget PRIVATE SDL3::SDL3) - else() # needed for gzdoom compat for now + else() target_link_libraries(zwidget PRIVATE ${SDL3_LIBRARY}) endif() endif() @@ -381,7 +390,7 @@ if(SDL2_FOUND AND NOT SDL3_FOUND) target_include_directories(zwidget PRIVATE ${SDL2_INCLUDE_DIRS}) if(TARGET SDL2::SDL2) target_link_libraries(zwidget PRIVATE SDL2::SDL2) - else() # needed for gzdoom compat for now + else() target_link_libraries(zwidget PRIVATE ${SDL2_LIBRARY}) endif() endif() @@ -417,10 +426,13 @@ if(ZWIDGET_BUILD_EXAMPLE) target_compile_definitions(zwidget_example PRIVATE UNICODE _UNICODE) target_link_libraries(zwidget_example PRIVATE gdi32 user32 shell32 comdlg32) elseif(APPLE) - target_link_libraries(zwidget_example PRIVATE "-framework Cocoa") + target_link_libraries(zwidget_example PRIVATE "-framework Cocoa" "-framework CoreVideo") if(ENABLE_OPENGL AND OPENGL_FRAMEWORK) target_link_libraries(zwidget_example PRIVATE "-framework OpenGL") endif() + if(ENABLE_METAL AND METAL_FRAMEWORK) + target_link_libraries(zwidget_example PRIVATE "-framework Metal" "-framework QuartzCore") + endif() else() target_link_libraries(zwidget_example PRIVATE ${ZWIDGET_LIBS}) endif() diff --git a/src/core/resourcedata_mac.mm b/src/core/resourcedata_mac.mm index 36f0b39..8fe39f0 100644 --- a/src/core/resourcedata_mac.mm +++ b/src/core/resourcedata_mac.mm @@ -49,7 +49,6 @@ } CFRelease(fontURL); - // fontPath is __bridge_transfer, so it's autoreleased(ARC) } return { fontData }; } @@ -59,8 +58,19 @@ SingleFontData fontData; @autoreleasepool { - NSFont* systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size - if (!systemFont) + NSFont* systemFont = nil; + if (@available(macOS 10.15, *)) { + systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size + } + + + if (!systemFont) { + // Fallback for older macOS versions + systemFont = [NSFont systemFontOfSize:13.0]; + } + + if (!systemFont) + // Double check after fallback throw std::runtime_error("Failed to get system font"); CTFontRef ctFont = (__bridge CTFontRef)systemFont; @@ -68,7 +78,6 @@ if (!fontURL) throw std::runtime_error("Failed to get font URL from system font"); - // __bridge_transfer transfers ownership to ARC, so no manual CFRelease is needed NSString* fontPath = (NSString*)CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); if (!fontPath) throw std::runtime_error("Failed to convert font URL to file path"); @@ -90,7 +99,6 @@ } CFRelease(fontURL); - // fontPath is __bridge_transfer, so it's autoreleased(ARC) } return { std::move(fontData) }; } diff --git a/src/window/cocoa/cocoa_display_backend.mm b/src/window/cocoa/cocoa_display_backend.mm index 6a68d7b..8dc0df3 100644 --- a/src/window/cocoa/cocoa_display_backend.mm +++ b/src/window/cocoa/cocoa_display_backend.mm @@ -8,29 +8,36 @@ #include "AppKitWrapper.h" +// Helper struct to hold timer callbacks without requiring a Widget owner +struct CocoaTimerData +{ + std::function callback; + NSTimer* nstimer; +}; + @interface ZWidgetTimerTarget : NSObject { - Timer* timer; + CocoaTimerData* timerData; } -- (id)initWithTimer:(Timer*)t; +- (id)initWithTimerData:(CocoaTimerData*)data; - (void)timerFired:(NSTimer*)nstimer; @end @implementation ZWidgetTimerTarget -- (id)initWithTimer:(Timer*)t; +- (id)initWithTimerData:(CocoaTimerData*)data { self = [super init]; if (self) { - timer = (Timer*)t; + timerData = data; } return self; } - (void)timerFired:(NSTimer*)nstimer { - if (timer->FuncExpired) - timer->FuncExpired(); + if (timerData && timerData->callback) + timerData->callback(); } @end @@ -42,6 +49,10 @@ - (void)timerFired:(NSTimer*)nstimer CocoaDisplayBackend::CocoaDisplayBackend() { + // CRITICAL: Initialize NSApp and set activation policy for keyboard events + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp finishLaunching]; } CocoaDisplayBackend::~CocoaDisplayBackend() @@ -70,26 +81,47 @@ - (void)timerFired:(NSTimer*)nstimer void CocoaDisplayBackend::ExitLoop() { [NSApp stop:nil]; + + // Post a dummy event to wake up the event loop so stop: takes effect + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; } void* CocoaDisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) { - Timer* timer = new Timer(nullptr); - timer->FuncExpired = onTimer; - ZWidgetTimerTarget* target = [[ZWidgetTimerTarget alloc] initWithTimer:timer]; - NSTimer* nstimer = [NSTimer scheduledTimerWithTimeInterval:timeoutMilliseconds / 1000.0 target:target selector:@selector(timerFired:) userInfo:nil repeats:YES]; - timer->SetTimerId((__bridge void*)nstimer); // No retain needed with ARC - // [target release]; // No release needed with ARC - return timer; + CocoaTimerData* timerData = new CocoaTimerData(); + timerData->callback = onTimer; + + ZWidgetTimerTarget* target = [[ZWidgetTimerTarget alloc] initWithTimerData:timerData]; + NSTimer* nstimer = [NSTimer scheduledTimerWithTimeInterval:timeoutMilliseconds / 1000.0 + target:target + selector:@selector(timerFired:) + userInfo:nil + repeats:YES]; + timerData->nstimer = nstimer; + + return timerData; } void CocoaDisplayBackend::StopTimer(void* timerID) { - Timer* timer = static_cast(timerID); - NSTimer* nstimer = (__bridge NSTimer*)timer->GetTimerId(); - [nstimer invalidate]; - // [nstimer release]; // No release needed with ARC - delete timer; + CocoaTimerData* timerData = static_cast(timerID); + if (timerData) + { + if (timerData->nstimer) + { + [timerData->nstimer invalidate]; + } + delete timerData; + } } Size CocoaDisplayBackend::GetScreenSize() diff --git a/src/window/cocoa/cocoa_display_window.mm b/src/window/cocoa/cocoa_display_window.mm index ac91852..4337078 100644 --- a/src/window/cocoa/cocoa_display_window.mm +++ b/src/window/cocoa/cocoa_display_window.mm @@ -205,39 +205,72 @@ static CVReturn DisplayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV @interface ZWidgetView : NSView { +@public CocoaDisplayWindowImpl* impl; + NSTrackingArea* trackingArea; } - (id)initWithImpl:(CocoaDisplayWindowImpl*)impl; @end @implementation ZWidgetView -+ (Class)layerClass -{ - return [CAMetalLayer class]; -} - - (id)initWithImpl:(CocoaDisplayWindowImpl*)d { self = [super init]; if (self) { impl = d; - self.wantsLayer = YES; - self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor; - [self addTrackingArea:[[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow owner:self userInfo:nil]]; + // Only use layer-backing for Metal/OpenGL, not for Bitmap rendering + // For Bitmap rendering, we need drawRect: to be called + if (impl->renderAPI == RenderAPI::Metal || impl->renderAPI == RenderAPI::OpenGL) + { + self.wantsLayer = YES; + self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor; + } + // CRITICAL: Don't access self.layer for Bitmap rendering - accessing it creates a layer! + // Don't create tracking area here - it will be created in updateTrackingAreas + trackingArea = nil; } return self; } +- (void)dealloc +{ + if (trackingArea) { + [self removeTrackingArea:trackingArea]; + trackingArea = nil; + } +} + - (BOOL)isOpaque { return YES; } +- (void)updateTrackingAreas +{ + if (trackingArea) { + [self removeTrackingArea:trackingArea]; + trackingArea = nil; + } + + trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + [super updateTrackingAreas]; +} + - (CALayer *)makeBackingLayer { - return [[self.class layerClass] layer]; + // This is only called for layer-backed views (Metal/OpenGL) + // Create appropriate layer based on render API + if (impl && impl->renderAPI == RenderAPI::Metal) + { + return [CAMetalLayer layer]; + } + return [CALayer layer]; // For OpenGL } - (BOOL)canBecomeKeyView { @@ -247,33 +280,57 @@ - (BOOL)canBecomeKeyView - (void)setFrame:(NSRect)frame { [super setFrame:frame]; - impl->updateDrawableSize(frame.size); +#ifdef HAVE_METAL + if (impl && impl->renderAPI == RenderAPI::Metal) + { + impl->updateDrawableSize(frame.size); + } +#endif } +- (NSView *)hitTest:(NSPoint)point +{ + return [super hitTest:point]; +} + - (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + return YES; // Allow mouse events even if window isn't key +} + +- (BOOL)becomeFirstResponder +{ + return [super becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + return [super resignFirstResponder]; +} + +- (void)setNeedsDisplay:(BOOL)flag +{ + [super setNeedsDisplay:flag]; +} + - (void)drawRect:(NSRect)dirtyRect { - NSLog(@"ZWidgetView: drawRect called"); if (!impl) return; - if (impl->renderAPI == RenderAPI::Bitmap) + if (impl->renderAPI == RenderAPI::Bitmap || impl->renderAPI == RenderAPI::Unspecified) { + // Just draw the existing bitmap (Update() handles regeneration asynchronously) if (impl->cgImage) { - NSLog(@"drawRect: impl->cgImage is NOT null. Drawing image."); CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; - CGContextSaveGState(context); - CGContextTranslateCTM(context, 0, self.bounds.size.height); - CGContextScaleCTM(context, 1.0, -1.0); + if (!context) { + return; + } CGContextDrawImage(context, NSRectToCGRect(self.bounds), impl->cgImage); - CGContextRestoreGState(context); - } - else - { - NSLog(@"drawRect: impl->cgImage IS null. Not drawing image."); } } else if (impl->renderAPI == RenderAPI::OpenGL) @@ -299,9 +356,16 @@ - (void)viewDidMoveToWindow { if ([self window]) { - NSLog(@"ZWidgetView: viewDidMoveToWindow with windowHost: %p", (void*)impl->windowHost); + // Make this view the first responder to receive mouse/keyboard events + [[self window] makeFirstResponder:self]; + impl->windowHost->OnWindowPaint(); - impl->startDisplayLink(); + // Only start displayLink for OpenGL/Metal rendering (continuous refresh needed) + // Bitmap rendering is event-driven, no continuous refresh needed + if (impl->renderAPI == RenderAPI::OpenGL || impl->renderAPI == RenderAPI::Metal) + { + impl->startDisplayLink(); + } } } @@ -345,6 +409,8 @@ - (void)mouseDown:(NSEvent *)theEvent if (impl && impl->windowHost) { NSPoint p = [theEvent locationInWindow]; + double flippedY = [self frame].size.height - p.y; + InputKey mouseKey = InputKey::None; if ([theEvent buttonNumber] == 0) mouseKey = InputKey::LeftMouse; else if ([theEvent buttonNumber] == 1) mouseKey = InputKey::RightMouse; @@ -352,7 +418,7 @@ - (void)mouseDown:(NSEvent *)theEvent if (mouseKey != InputKey::None) { - impl->windowHost->OnWindowMouseDown(Point(p.x, [self frame].size.height - p.y), mouseKey); + impl->windowHost->OnWindowMouseDown(Point(p.x, flippedY), mouseKey); } } } @@ -362,6 +428,8 @@ - (void)mouseUp:(NSEvent *)theEvent if (impl && impl->windowHost) { NSPoint p = [theEvent locationInWindow]; + double flippedY = [self frame].size.height - p.y; + InputKey mouseKey = InputKey::None; if ([theEvent buttonNumber] == 0) mouseKey = InputKey::LeftMouse; else if ([theEvent buttonNumber] == 1) mouseKey = InputKey::RightMouse; @@ -369,7 +437,7 @@ - (void)mouseUp:(NSEvent *)theEvent if (mouseKey != InputKey::None) { - impl->windowHost->OnWindowMouseUp(Point(p.x, [self frame].size.height - p.y), mouseKey); + impl->windowHost->OnWindowMouseUp(Point(p.x, flippedY), mouseKey); } } } @@ -431,15 +499,28 @@ - (void)keyDown:(NSEvent *)theEvent { InputKey key = keycode_to_inputkey([theEvent keyCode]); impl->keyState[key] = true; - impl->windowHost->OnWindowKeyDown(key); // Removed isARepeat as it's not in the ZWidget API - - NSString* characters = [theEvent characters]; - if ([characters length] > 0) - { - impl->windowHost->OnWindowKeyChar([characters UTF8String]); - } + impl->windowHost->OnWindowKeyDown(key); } + // Call interpretKeyEvents to trigger insertText: for text input + [self interpretKeyEvents:@[theEvent]]; +} + +// Text input protocol methods - required for keyboard input on macOS +- (void)insertText:(id)string +{ + if (impl && impl->windowHost) + { + NSString* str = [string isKindOfClass:[NSAttributedString class]] ? + [(NSAttributedString*)string string] : (NSString*)string; + impl->windowHost->OnWindowKeyChar([str UTF8String]); + } +} + +- (void)doCommandBySelector:(SEL)selector +{ + // This handles special keys like Enter, Tab, etc. + // They're already handled by keyDown, so we can ignore them here } - (void)keyUp:(NSEvent *)theEvent @@ -528,6 +609,29 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } return self; } + +- (void)windowDidResize:(NSNotification *)notification +{ + // Throttle resize events to prevent excessive calls that can cause crashes + static int resizeCount = 0; + static NSTimeInterval lastResizeTime = 0; + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + + if (now - lastResizeTime < 0.01) { + resizeCount++; + if (resizeCount > 5) { + return; // Throttle: too many rapid calls + } + } else { + resizeCount = 0; + } + lastResizeTime = now; + + if (impl && impl->windowHost) + { + impl->windowHost->OnWindowGeometryChanged(); + } +} @end void CocoaDisplayWindowImpl::initOpenGL(ZWidgetView* view) @@ -563,7 +667,8 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d CVReturn CocoaDisplayWindowImpl::displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* inNow, const CVTimeStamp* inOutputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { - NSLog(@"displayLinkOutputCallback called"); + // Note: displayLink is only started for OpenGL/Metal rendering + // Bitmap rendering is event-driven and doesn't use displayLink if (renderAPI == RenderAPI::Metal) { #ifdef HAVE_METAL @@ -584,14 +689,6 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #endif } - else if (renderAPI == RenderAPI::Bitmap) - { - NSLog(@"displayLinkOutputCallback: RenderAPI::Bitmap branch executed. Dispatching OnWindowPaint() to main thread."); - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"dispatch_async block executed. Calling OnWindowPaint()."); - if (windowHost) windowHost->OnWindowPaint(); - }); - } return kCVReturnSuccess; } @@ -624,22 +721,27 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #ifdef HAVE_METAL - // Create Metal device and layer for application rendering (not ZWidget rendering) - // Applications can access these via GetMetalDevice() and GetMetalLayer() - impl->metalDevice = MTLCreateSystemDefaultDevice(); - if (impl->metalDevice) - { - impl->metalLayer = [CAMetalLayer layer]; - impl->metalLayer.device = impl->metalDevice; - impl->metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; - impl->metalLayer.framebufferOnly = NO; // Allow reading for screenshots, etc. - impl->metalLayer.presentsWithTransaction = NO; - impl->metalLayer.displaySyncEnabled = YES; - impl->metalLayer.maximumDrawableCount = 3; - impl->metalLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; - impl->metalLayer.contentsScale = [[NSScreen mainScreen] backingScaleFactor]; - impl->metalLayer.frame = view.layer.frame; - [view.layer addSublayer:impl->metalLayer]; + // Only create Metal layer if actually using Metal rendering + // Accessing view.layer forces layer-backing which breaks Bitmap rendering + if (renderAPI == RenderAPI::Metal) + { + // Create Metal device and layer for application rendering + // Applications can access these via GetMetalDevice() and GetMetalLayer() + impl->metalDevice = MTLCreateSystemDefaultDevice(); + if (impl->metalDevice) + { + impl->metalLayer = [CAMetalLayer layer]; + impl->metalLayer.device = impl->metalDevice; + impl->metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + impl->metalLayer.framebufferOnly = NO; // Allow reading for screenshots, etc. + impl->metalLayer.presentsWithTransaction = NO; + impl->metalLayer.displaySyncEnabled = YES; + impl->metalLayer.maximumDrawableCount = 3; + impl->metalLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; + impl->metalLayer.contentsScale = [[NSScreen mainScreen] backingScaleFactor]; + impl->metalLayer.frame = view.layer.frame; + [view.layer addSublayer:impl->metalLayer]; + } } #endif #ifdef HAVE_OPENGL @@ -656,7 +758,6 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d CocoaDisplayWindow::~CocoaDisplayWindow() { - NSLog(@"CocoaDisplayWindow: Destructor entered, this = %p", (void*)this); } void CocoaDisplayWindow::SetWindowTitle(const std::string& text) @@ -718,8 +819,19 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d { if (impl->window) { + // CRITICAL: Activate the application so it can receive keyboard events + [NSApp activateIgnoringOtherApps:YES]; [impl->window makeKeyAndOrderFront:nil]; - Update(); + + // Trigger initial layout + if (impl->windowHost) + { + impl->windowHost->OnWindowGeometryChanged(); + // Paint to create CGImage before any drawRect calls + impl->windowHost->OnWindowPaint(); + } + // Just trigger a redraw without regenerating (bitmap already created above) + [[impl->window contentView] setNeedsDisplay:YES]; } } @@ -837,10 +949,25 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d void CocoaDisplayWindow::Update() { - if (impl->window) - { - [[impl->window contentView] setNeedsDisplay:YES]; - } + if (!impl->window || !impl->windowHost) + return; + + // Queue paint asynchronously to avoid interfering with event processing + // Use weak reference to window to avoid dangling pointer issues + __weak NSWindow* weakWindow = impl->window; + DisplayWindowHost* hostPtr = impl->windowHost; + + dispatch_async(dispatch_get_main_queue(), ^{ + NSWindow* strongWindow = weakWindow; + if (!strongWindow) + return; + + // Regenerate the bitmap + hostPtr->OnWindowPaint(); + + // Trigger redraw + [[strongWindow contentView] setNeedsDisplay:YES]; + }); } bool CocoaDisplayWindow::GetKeyState(InputKey key) @@ -1014,9 +1141,8 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #endif } - else if (impl->renderAPI == RenderAPI::Bitmap) + else if (impl->renderAPI == RenderAPI::Bitmap || impl->renderAPI == RenderAPI::Unspecified) { - NSLog(@"PresentBitmap: RenderAPI::Bitmap path executed."); if (impl->cgImage) CGImageRelease(impl->cgImage); impl->cgImage = nullptr; @@ -1033,26 +1159,16 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d if (context) { impl->cgImage = CGBitmapContextCreateImage(context); - if (impl->cgImage) - { - NSLog(@"PresentBitmap: CGImageRef created successfully."); - } - else - { - NSLog(@"PresentBitmap: Failed to create CGImageRef."); - } CGContextRelease(context); } - else - { - NSLog(@"PresentBitmap: Failed to create CGBitmapContext."); - } CGColorSpaceRelease(colorSpace); if (impl->window) { dispatch_async(dispatch_get_main_queue(), ^{ - [[impl->window contentView] setNeedsDisplay:YES]; + NSView* contentView = [impl->window contentView]; + [contentView setNeedsDisplay:YES]; + [contentView displayIfNeeded]; }); } } diff --git a/src/window/cocoa/cocoa_open_file_dialog.h b/src/window/cocoa/cocoa_open_file_dialog.h index c01f8f5..8133045 100644 --- a/src/window/cocoa/cocoa_open_file_dialog.h +++ b/src/window/cocoa/cocoa_open_file_dialog.h @@ -35,4 +35,7 @@ class CocoaOpenFileDialog : public OpenFileDialog std::string _title; std::vector _filenames; void* panel = nullptr; + + // Helper method to run modal and collect results + bool runModalAndGetResults(); }; diff --git a/src/window/cocoa/cocoa_open_file_dialog.mm b/src/window/cocoa/cocoa_open_file_dialog.mm index 3bcf144..c0bdcb2 100644 --- a/src/window/cocoa/cocoa_open_file_dialog.mm +++ b/src/window/cocoa/cocoa_open_file_dialog.mm @@ -30,7 +30,7 @@ void CocoaOpenFileDialog::SetDefaultExtension(const std::string& extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSString* extensionString = [NSString stringWithUTF8String:extension.c_str()]; UTType* utType = [UTType typeWithFilenameExtension:extensionString]; if (utType) { @@ -68,23 +68,57 @@ [((__bridge NSOpenPanel*)panel) setTitle:[NSString stringWithUTF8String:title.c_str()]]; } +// Helper to run modal and collect results +bool CocoaOpenFileDialog::runModalAndGetResults() +{ + if ([((__bridge NSOpenPanel*)panel) runModal] == NSModalResponseOK) + { + _filenames.clear(); + for (NSURL* url in [((__bridge NSOpenPanel*)panel) URLs]) + { + _filenames.push_back([[url path] UTF8String]); + } + return true; + } + return false; +} + bool CocoaOpenFileDialog::Show() { - if (@available(macOS 11.0, *)) { + // IMPORTANT: Allow selection of files that may not match the exact content type + [((__bridge NSOpenPanel*)panel) setAllowsOtherFileTypes:YES]; + + if (@available(macOS 12.0, *)) { if (!_filters.empty()) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:_filters[_filterIndex].second.c_str()] componentsSeparatedByString:@";"]; NSMutableArray* utTypes = [NSMutableArray array]; for (NSString* typeString in fileTypeStrings) { - UTType* utType = [UTType typeWithFilenameExtension:typeString]; + // Strip wildcard prefix (*.ext -> ext) + NSString* extension = typeString; + if ([extension hasPrefix:@"*."]) { + extension = [extension substringFromIndex:2]; + } else if ([extension hasPrefix:@"*"]) { + extension = [extension substringFromIndex:1]; + } + + // Handle special case for *.* + if ([extension isEqualToString:@".*"] || [extension isEqualToString:@"*"]) { + // Allow all file types - set empty array + [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:@[]]; + return runModalAndGetResults(); + } + + UTType* utType = [UTType typeWithFilenameExtension:extension]; if (utType) { [utTypes addObject:utType]; } + // If UTType doesn't exist (like .wad), allowsOtherFileTypes will handle it } if ([utTypes count] > 0) { [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:utTypes]; } else { - // Fallback if no valid UTTypes could be created + // Fallback if no valid UTTypes could be created - allow all files [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:@[]]; } } @@ -96,25 +130,34 @@ if (!_filters.empty()) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:_filters[_filterIndex].second.c_str()] componentsSeparatedByString:@";"]; - [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:fileTypeStrings]; + NSMutableArray* cleanExtensions = [NSMutableArray array]; + for (NSString* typeString in fileTypeStrings) { + // Strip wildcard prefix (*.ext -> ext) + NSString* extension = typeString; + if ([extension hasPrefix:@"*."]) { + extension = [extension substringFromIndex:2]; + } else if ([extension hasPrefix:@"*"]) { + extension = [extension substringFromIndex:1]; + } + + // Handle special case for *.* + if ([extension isEqualToString:@".*"] || [extension isEqualToString:@"*"]) { + // Allow all file types - set nil + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:nil]; + return runModalAndGetResults(); + } + + [cleanExtensions addObject:extension]; + } + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:cleanExtensions]; } else { - [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:@[]]; // No filters + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:nil]; // No filters - allow all } } - if ([((__bridge NSOpenPanel*)panel) runModal] == NSModalResponseOK) - { - _filenames.clear(); - - for (NSURL* url in [((__bridge NSOpenPanel*)panel) URLs]) - { - _filenames.push_back([[url path] UTF8String]); - } - return true; - } - return false; + return runModalAndGetResults(); } std::string CocoaOpenFileDialog::Filename() const diff --git a/src/window/cocoa/cocoa_save_file_dialog.mm b/src/window/cocoa/cocoa_save_file_dialog.mm index 69d630f..e42da0c 100644 --- a/src/window/cocoa/cocoa_save_file_dialog.mm +++ b/src/window/cocoa/cocoa_save_file_dialog.mm @@ -9,7 +9,7 @@ void CocoaSaveFileDialog::AddFilter(const std::string &filter_description, const std::string &filter_extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:filter_extension.c_str()] componentsSeparatedByString:@";"]; NSMutableArray* utTypes = [NSMutableArray array]; for (NSString* typeString in fileTypeStrings) { @@ -40,7 +40,7 @@ void CocoaSaveFileDialog::SetDefaultExtension(const std::string& extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSString* extensionString = [NSString stringWithUTF8String:extension.c_str()]; UTType* utType = [UTType typeWithFilenameExtension:extensionString]; if (utType) { @@ -77,7 +77,7 @@ void CocoaSaveFileDialog::ClearFilters() { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { [((__bridge NSSavePanel*)panel) setAllowedContentTypes:@[]]; } else { [((__bridge NSSavePanel*)panel) setAllowedFileTypes:@[]]; diff --git a/src/window/window.cpp b/src/window/window.cpp index 5ff1117..bf1b5f8 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -93,6 +93,10 @@ std::unique_ptr DisplayBackend::TryCreateBackend() { backend = TryCreateWin32(); } + else if (backendSelectionStr == "Cocoa") + { + backend = TryCreateCocoa(); + } else if (backendSelectionStr == "X11") { backend = TryCreateX11(); @@ -110,6 +114,7 @@ std::unique_ptr DisplayBackend::TryCreateBackend() if (!backend) { backend = TryCreateWin32(); + if (!backend) backend = TryCreateCocoa(); if (!backend) backend = TryCreateWayland(); if (!backend) backend = TryCreateX11(); if (!backend) backend = TryCreateSDL3(); @@ -223,9 +228,15 @@ std::unique_ptr DisplayBackend::TryCreateWayland() #endif -#ifndef __APPLE__ +#ifdef __APPLE__ + +#include "cocoa/cocoa_display_backend.h" + +// DisplayBackend::TryCreateCocoa() is defined in cocoa_display_backend.mm + +#else -std::unique_ptr TryCreateCocoa() +std::unique_ptr DisplayBackend::TryCreateCocoa() { return nullptr; }