diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-14 14:05:19 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-14 14:06:16 +0300 |
| commit | 0eec911bdabb130415aeb2fc6eb4281b891a5d2f (patch) | |
| tree | 0ab5dfa8ee08d1fc5ecc7092fd43d2cda723d2fa /src/platform | |
| parent | 87279f6903343e621c0446b20d1e7d6a495f1125 (diff) | |
Fix Cocoa backend focus, close, live resize, popups and notifications
- Undecorated windows can become key and main; popups never take focus.
- Undecorated windows stay miniaturizable and resizable unless FIXED_SIZE.
- The close button sends a CLOSE request (windowShouldClose:) and does not
close; the app destroys the surface to comply.
- Refresh runs during live resize: the modal flag is set between
windowWillStartLiveResize: and windowDidEndLiveResize:, and CPU surfaces
refresh before the frame is rendered. A destroyed window clears the flag.
- presentsWithTransaction is only changed for CPU presenters.
- Display open and surface creation return EPERM off the main thread.
- Popups are always CPU surfaces with a software queue, also under a Metal
parent.
- Interactive move and drag start use the recorded mouse-down; without a
press they set ESTALE. Interactive resize and window menu return ENOTSUP.
- Notifications: ENOTSUP outside an app bundle; one authorization request,
notifications queued until it is granted, EACCES after a denial.
- Titles use the full string instead of a 159-byte copy that could split a
UTF-8 sequence.
- finishLaunching runs once per process, not once per display open.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/platform')
| -rw-r--r-- | src/platform/cocoa/desktop.m | 57 | ||||
| -rw-r--r-- | src/platform/cocoa/window.m | 167 |
2 files changed, 176 insertions, 48 deletions
diff --git a/src/platform/cocoa/desktop.m b/src/platform/cocoa/desktop.m index 76aa551..25393d5 100644 --- a/src/platform/cocoa/desktop.m +++ b/src/platform/cocoa/desktop.m @@ -4,7 +4,9 @@ #include "common/desktop.h" #include "common/private_desktop.h" +#include <dispatch/dispatch.h> #include <CoreFoundation/CoreFoundation.h> +#include <errno.h> #include <limits.h> #include <mach-o/dyld.h> #include <stdio.h> @@ -44,7 +46,12 @@ bool desktop_open_uri(const char *uri) @end static YaslNotificationDelegate *notification_delegate; +/* Main-thread state. Notifications made before the answer to the one + * authorization request wait in the queue and are posted when it is granted. */ +static NSMutableArray<UNNotificationRequest *> *notification_queue; static bool authorization_requested; +static bool authorization_granted; +static bool authorization_denied; /* UNUserNotificationCenter raises an exception outside an app bundle. */ static bool notifications_available(void) @@ -56,24 +63,26 @@ static bool notifications_available(void) bool desktop_notify(const char *app_id, const char *title, const char *body) { (void)app_id; - if (!notifications_available()) return false; + if (!notifications_available()) { + errno = ENOTSUP; + return false; + } + if (authorization_denied) { + errno = EACCES; + return false; + } @autoreleasepool { NSString *heading = [NSString stringWithUTF8String:title]; NSString *message = [NSString stringWithUTF8String:body]; - if (!heading || !message) return false; + if (!heading || !message) { + errno = EINVAL; + return false; + } UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; if (!notification_delegate) { notification_delegate = [[YaslNotificationDelegate alloc] init]; center.delegate = notification_delegate; } - if (!authorization_requested) { - authorization_requested = true; - [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert - completionHandler:^(BOOL granted, NSError *error) { - (void)granted; - (void)error; - }]; - } UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = heading; content.body = message; @@ -82,7 +91,33 @@ bool desktop_notify(const char *app_id, const char *title, const char *body) content:content trigger:nil]; [content release]; - [center addNotificationRequest:request withCompletionHandler:nil]; + if (authorization_granted) { + [center addNotificationRequest:request withCompletionHandler:nil]; + return true; + } + if (!notification_queue) + notification_queue = [[NSMutableArray alloc] init]; + [notification_queue addObject:request]; + if (!authorization_requested) { + authorization_requested = true; + [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert + completionHandler:^(BOOL granted, NSError *error) { + (void)error; + /* The handler runs on a background queue; the state is main-thread only. */ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!granted) { + authorization_denied = true; + [notification_queue removeAllObjects]; + return; + } + authorization_granted = true; + UNUserNotificationCenter *granted_center = [UNUserNotificationCenter currentNotificationCenter]; + for (UNNotificationRequest *queued in notification_queue) + [granted_center addNotificationRequest:queued withCompletionHandler:nil]; + [notification_queue removeAllObjects]; + }); + }]; + } return true; } } diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m index 8f01365..a78c2ce 100644 --- a/src/platform/cocoa/window.m +++ b/src/platform/cocoa/window.m @@ -118,7 +118,8 @@ struct cocoa_surface { uint64_t pen_device_id, pen_tool_id; uint32_t pen_buttons; - char title[160]; + /* Retained mouse-down of the press in progress, for interactive moves. */ + NSEvent *mouse_down; struct yasl_private_frame_clock clock; }; @@ -150,7 +151,6 @@ struct cocoa_display { } drag; int depth; bool open; - bool launched; bool pumping; bool owns_loop; }; @@ -275,7 +275,9 @@ static void sync_size(struct cocoa_surface *w, bool force) yasl_private_surface_notify_configure(&w->base, (struct yasl_private_geometry){ .size = size, .scale_120 = scale_120, }); - if (w->base.presenter != YASL_PRIVATE_PRESENTER_CPU) + /* During a live resize the app draws the new size here, before the frame is + * rendered and committed below. */ + if (w->base.presenter != YASL_PRIVATE_PRESENTER_CPU || w->view.inLiveResize) yasl_private_surface_notify_refresh(&w->base); if (!w->destroyed && !w->closed && w->view.inLiveResize) render_and_commit(w); @@ -851,7 +853,8 @@ static struct yasl_private_vector2i dragging_pos(YaslView *view, id<NSDraggingIn { [super viewWillStartLiveResize]; struct cocoa_surface *w = self.surface; - if (w) + /* Layers that the app owns through Metal or Vulkan keep their own setting. */ + if (w && w->base.presenter == YASL_PRIVATE_PRESENTER_CPU) w->layer.presentsWithTransaction = YES; } @@ -861,7 +864,8 @@ static struct yasl_private_vector2i dragging_pos(YaslView *view, id<NSDraggingIn struct cocoa_surface *w = self.surface; if (!w) return; - w->layer.presentsWithTransaction = NO; + if (w->base.presenter == YASL_PRIVATE_PRESENTER_CPU) + w->layer.presentsWithTransaction = NO; cocoa_surface_schedule_redraw(w); } @@ -933,6 +937,13 @@ static struct yasl_private_vector2i dragging_pos(YaslView *view, id<NSDraggingIn - (void)mouseDown:(NSEvent *)event { [self.window makeFirstResponder:self]; + /* Right and other buttons also arrive here. Keep the press for interactive moves. */ + struct cocoa_surface *w = self.surface; + if (w) { + [event retain]; + [w->mouse_down release]; + w->mouse_down = event; + } push_pointer(self.surface, event, YASL_PRIVATE_EVENT_POINTER_DOWN, cocoa_button(event.buttonNumber)); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown:event]; } @@ -940,6 +951,11 @@ static struct yasl_private_vector2i dragging_pos(YaslView *view, id<NSDraggingIn - (void)mouseUp:(NSEvent *)event { + struct cocoa_surface *w = self.surface; + if (w) { + [w->mouse_down release]; + w->mouse_down = nil; + } push_pointer(self.surface, event, YASL_PRIVATE_EVENT_POINTER_UP, cocoa_button(event.buttonNumber)); } - (void)rightMouseUp:(NSEvent *)event { [self mouseUp:event]; } @@ -1226,6 +1242,18 @@ static void push_text_edit(struct cocoa_surface *w, const char *text, size_t len } } +/* Borderless windows cannot take focus unless they say so. Toplevels may; popups + * never take focus from their parent. */ +- (BOOL)canBecomeKeyWindow +{ + return self.surface && !self.surface->is_popup; +} + +- (BOOL)canBecomeMainWindow +{ + return self.surface && !self.surface->is_popup; +} + @end @implementation YaslWindowController @@ -1303,6 +1331,32 @@ static void push_text_edit(struct cocoa_surface *w, const char *text, size_t len sync_size(self.surface, false); } +/* The close button is a request, as on Wayland and X11: the app destroys the + * surface to comply. Programmatic closes come through windowWillClose instead. */ +- (BOOL)windowShouldClose:(NSWindow *)sender +{ + (void)sender; + struct cocoa_surface *w = self.surface; + if (w && !w->closed) { + darwin_ctx.depth++; + yasl_private_surface_notify_close(&w->base); + darwin_ctx.depth--; + } + return NO; +} + +- (void)windowWillStartLiveResize:(NSNotification *)notification +{ + (void)notification; + darwin_ctx.base.modal = true; +} + +- (void)windowDidEndLiveResize:(NSNotification *)notification +{ + (void)notification; + darwin_ctx.base.modal = false; +} + - (void)windowWillClose:(NSNotification *)notification { (void)notification; @@ -1584,7 +1638,8 @@ void cocoa_surface_schedule_redraw(struct cocoa_surface *w) static void app_init(const char *app_name) { - if (darwin_ctx.launched) return; + /* finishLaunching runs once per process, even when the display reopens. */ + static bool finished_launching; [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; NSWindow.allowsAutomaticWindowTabbing = NO; @@ -1616,8 +1671,10 @@ static void app_init(const char *app_name) [event.window sendEvent:event]; return nil; }]; - [NSApp finishLaunching]; - darwin_ctx.launched = true; + if (!finished_launching) { + [NSApp finishLaunching]; + finished_launching = true; + } } static void place_window(NSWindow *window) @@ -1633,7 +1690,10 @@ static void place_window(NSWindow *window) struct cocoa_display *cocoa_display_open(struct yasl_private_loop *loop, const struct yasl_private_display_spec *spec) { - assert([NSThread isMainThread]); + if (![NSThread isMainThread]) { + errno = EPERM; + return NULL; + } if (darwin_ctx.open) return NULL; const char *name = spec && spec->app_id ? spec->app_id : "YASL"; @autoreleasepool { app_init(name); } @@ -1691,7 +1751,10 @@ uint32_t cocoa_display_caps(const struct cocoa_display *d) struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d, const struct yasl_private_window_spec *spec, void *graphics_device) { - assert([NSThread isMainThread]); + if (![NSThread isMainThread]) { + errno = EPERM; + return NULL; + } if (!d || !d->open || !spec) return NULL; @autoreleasepool { @@ -1715,14 +1778,15 @@ struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d, cocoa_surface_set_callbacks(w, NULL, NULL); w->clock.refresh_ns = YASL_PRIVATE_REFRESH_DEFAULT_NS; const char *t = spec->title ? spec->title : "YASL"; - strncpy(w->title, t, sizeof(w->title) - 1); int win_w = spec->size.x > 0 ? spec->size.x : 800; int win_h = spec->size.y > 0 ? spec->size.y : 480; + /* Undecorated windows stay minimizable and edge-resizable; only FIXED_SIZE + * removes resizing. */ NSWindowStyleMask style = (spec->flags & YASL_PRIVATE_SURFACE_UNDECORATED) ? - NSWindowStyleMaskBorderless : NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | - NSWindowStyleMaskMiniaturizable; - if (!(spec->flags & (YASL_PRIVATE_SURFACE_UNDECORATED | YASL_PRIVATE_SURFACE_FIXED_SIZE))) + NSWindowStyleMaskBorderless | NSWindowStyleMaskMiniaturizable : + NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + if (!(spec->flags & YASL_PRIVATE_SURFACE_FIXED_SIZE)) style |= NSWindowStyleMaskResizable; w->window = [[YaslWindow alloc] initWithContentRect:NSMakeRect(0, 0, win_w, win_h) styleMask:style @@ -1738,7 +1802,8 @@ struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d, w->window.acceptsMouseMovedEvents = YES; w->window.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary; w->window.titlebarAppearsTransparent = YES; - w->window.title = [NSString stringWithUTF8String:w->title] ?: @"YASL"; + /* The full title: a byte-limited copy could split a UTF-8 sequence. */ + w->window.title = [NSString stringWithUTF8String:t] ?: @"YASL"; w->window.colorSpace = [NSColorSpace sRGBColorSpace]; w->view = [[YaslView alloc] initWithFrame:w->window.contentView.bounds]; @@ -1791,6 +1856,7 @@ struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d, static void surface_free(struct cocoa_surface *w) { + [w->mouse_down release]; [w->link release]; [w->software_buffer release]; [w->software_queue release]; @@ -1843,6 +1909,10 @@ void cocoa_surface_destroy(struct cocoa_surface *w) w->cbs = &no_callbacks; w->ud = NULL; [w->link invalidate]; + /* The delegate is removed before the window can end its live resize, so the + * modal flag would never be cleared there. */ + if (w->window.inLiveResize) + darwin_ctx.base.modal = false; w->window.delegate = nil; w->controller.surface = NULL; w->window.surface = NULL; @@ -1873,8 +1943,8 @@ void cocoa_surface_destroy(struct cocoa_surface *w) void cocoa_surface_set_title(struct cocoa_surface *w, const char *title) { if (!w || w->closed || !title) return; - strncpy(w->title, title, sizeof(w->title) - 1); - w->window.title = [NSString stringWithUTF8String:w->title] ?: @"YASL"; + /* The full title: a byte-limited copy could split a UTF-8 sequence. */ + w->window.title = [NSString stringWithUTF8String:title] ?: @"YASL"; } static bool impl_surface_set_size(struct yasl_surface *surface, struct yasl_private_vector2i size) @@ -2207,7 +2277,10 @@ static NSRect popup_calc_frame(struct cocoa_surface *parent, const struct yasl_p struct cocoa_surface *cocoa_surface_create_popup(struct cocoa_surface *parent, const struct yasl_private_popup_spec *spec) { - assert([NSThread isMainThread]); + if (![NSThread isMainThread]) { + errno = EPERM; + return NULL; + } if (!parent || parent->closed || !parent->window || !parent->view || !popup_spec_valid(parent, spec)) return NULL; @@ -2223,9 +2296,10 @@ struct cocoa_surface *cocoa_surface_create_popup(struct cocoa_surface *parent, struct cocoa_surface *w = calloc(1, sizeof(*w)); if (!w) return NULL; + /* Popups are always CPU surfaces, whatever the parent draws with. */ w->base.display = parent->base.display; - w->base.presenter = parent->base.presenter; - w->base.creation_flags = parent->base.creation_flags; + w->base.presenter = YASL_PRIVATE_PRESENTER_CPU; + w->base.creation_flags = parent->base.creation_flags | YASL_PRIVATE_SURFACE_CPU; cocoa_surface_set_callbacks(w, NULL, NULL); w->clock.refresh_ns = parent->clock.refresh_ns ? parent->clock.refresh_ns : YASL_PRIVATE_REFRESH_DEFAULT_NS; w->parent = parent; @@ -2256,21 +2330,19 @@ struct cocoa_surface *cocoa_surface_create_popup(struct cocoa_surface *parent, w->layer = (CAMetalLayer *)w->view.layer; w->layer.opaque = NO; w->layer.device = parent->layer.device; - if (parent->software_queue) { - w->software_queue = [w->layer.device newCommandQueue]; - if (!w->software_queue) { - w->window.delegate = nil; - w->controller.surface = NULL; - [w->view release]; - [w->controller release]; - [w->window release]; - free(w); - return NULL; - } - w->layer.framebufferOnly = NO; - } else { - w->layer.framebufferOnly = parent->layer.framebufferOnly; + /* A CPU popup needs the same software queue as a CPU toplevel, even when + * the parent presents through Metal. */ + w->software_queue = [w->layer.device newCommandQueue]; + if (!w->software_queue) { + w->window.delegate = nil; + w->controller.surface = NULL; + [w->view release]; + [w->controller release]; + [w->window release]; + free(w); + return NULL; } + w->layer.framebufferOnly = NO; w->link = [[w->view displayLinkWithTarget:w->view selector:@selector(displayLinkFired:)] retain]; @@ -2307,8 +2379,17 @@ bool cocoa_surface_popup_reposition(struct cocoa_surface *popup, bool cocoa_surface_start_interactive_move(struct cocoa_surface *s) { - if (!s || !s->window) return false; - [s->window performWindowDragWithEvent:[NSApp currentEvent]]; + if (!s || !s->window || s->closed) { + errno = EINVAL; + return false; + } + /* The drag must start from a press still in progress. NSApp's current event + * need not be that press, so use the mouse-down recorded for this window. */ + if (!s->mouse_down || [NSEvent pressedMouseButtons] == 0) { + errno = ESTALE; + return false; + } + [s->window performWindowDragWithEvent:s->mouse_down]; return true; } @@ -2344,8 +2425,18 @@ bool cocoa_display_drag_start(struct cocoa_display *d, struct cocoa_surface *ori const struct yasl_private_drag_image *image, const struct yasl_private_drag_listener *l, void *ud) { - if (!d || !origin || d->drag.active || !l) + if (!d || !origin || !l) { + errno = EINVAL; + return false; + } + if (d->drag.active) { + errno = EBUSY; return false; + } + if ([NSEvent pressedMouseButtons] == 0) { + errno = ESTALE; + return false; + } d->drag.active = true; d->drag.origin = origin; d->drag.carried = NULL; @@ -2412,12 +2503,14 @@ bool cocoa_surface_drag_carry(struct cocoa_surface *w, struct yasl_private_vecto bool cocoa_surface_start_interactive_resize(struct cocoa_surface *s, enum yasl_private_resize_edge edge) { (void)s; (void)edge; + errno = ENOTSUP; return false; } bool cocoa_surface_show_window_menu(struct cocoa_surface *s, struct yasl_private_vector2i pos) { (void)s; (void)pos; + errno = ENOTSUP; return false; } |
