diff --git a/changelog.d/overlay-windows.md b/changelog.d/overlay-windows.md new file mode 100644 index 000000000..a70090420 --- /dev/null +++ b/changelog.d/overlay-windows.md @@ -0,0 +1,3 @@ +feature: **Overlay windows**: `WindowOptions`/`WindowCreateOptions` gain `transparent`, `shadow`, `level` (normal/floating/status/screen_saver), `click_through`, `all_workspaces`, `visible`, and `activate`, so apps can build always-on-top desktop companions (pointer followers, heads-up overlays) that never flash opaque or steal focus at create — config is staged before the create call and consumed inside it. +- **Overlay window control**: new runtime verbs `setWindowFrame`, `setWindowVisible` (show without activating), `setWindowClickThrough`, plus `pointerPosition` and `screenWorkArea` queries for overlay placement math, implemented on the AppKit host (Chromium host: everything except staged pre-create config) and recorded by the null platform for headless tests. +- **WebView user scripts**: `WebViewOptions.user_script` injects app JavaScript into a child WebView's main frame at document start (the content-script shape: observe the page, relay through the bridge) — Zig-side only, never exposed to page JS; system engine only. diff --git a/src/platform/macos/appkit_host.h b/src/platform/macos/appkit_host.h index a7b57304f..519728879 100644 --- a/src/platform/macos/appkit_host.h +++ b/src/platform/macos/appkit_host.h @@ -447,12 +447,32 @@ int native_sdk_appkit_start_window_drag(native_sdk_appkit_host_t *host, uint64_t // fullscreen report all-zero. Returns 0 when the window id is unknown // (out-params untouched). int native_sdk_appkit_window_chrome_insets(native_sdk_appkit_host_t *host, uint64_t window_id, double *top, double *left, double *bottom, double *right, double *buttons_x, double *buttons_y, double *buttons_width, double *buttons_height); +/* Overlay-window surface: config staged before a window's create call + * (consumed inside the create so transparency/level/visibility apply + * before first order-front), an existing-window variant for the startup + * window (created before any overlay call can be staged but not ordered + * front until the run loop starts), programmatic frame / visibility / + * click-through control, and the global pointer + primary-display + * work-area queries overlay placement math needs. All geometry is AppKit + * screen coordinates — the same space window-frame events report. Levels: + * 0 normal, 1 floating, 2 status, 3 screen-saver. Each returns 1 on + * success, 0 when the window id is unknown (query out-params untouched). */ +// App-supplied content script for a window's MAIN webview, staged at +// create and injected when the lazy webview is built (empty clears it). +int native_sdk_appkit_set_window_user_script(native_sdk_appkit_host_t *host, uint64_t window_id, const char *script, size_t script_len); +int native_sdk_appkit_set_pending_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces, int visible, int activate); +int native_sdk_appkit_configure_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces); +int native_sdk_appkit_set_window_frame(native_sdk_appkit_host_t *host, uint64_t window_id, double x, double y, double width, double height); +int native_sdk_appkit_set_window_visible(native_sdk_appkit_host_t *host, uint64_t window_id, int visible, int activate); +int native_sdk_appkit_set_window_click_through(native_sdk_appkit_host_t *host, uint64_t window_id, int click_through); +int native_sdk_appkit_pointer_position(native_sdk_appkit_host_t *host, double *x, double *y); +int native_sdk_appkit_screen_work_area(native_sdk_appkit_host_t *host, double *x, double *y, double *width, double *height); /* Per-window child WebViews. Both hosts implement these; declaring them * here keeps the definitions on C linkage (the Objective-C++ Chromium * host would otherwise mangle them and break the platform layer's extern * bindings). Each returns 1 on success, 0 when the window or webview is * unknown. */ -int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled); +int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled, const char *user_script, size_t user_script_len); int native_sdk_appkit_set_webview_frame(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, double x, double y, double width, double height); int native_sdk_appkit_navigate_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len); int native_sdk_appkit_set_webview_zoom(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, double zoom); diff --git a/src/platform/macos/appkit_host.m b/src/platform/macos/appkit_host.m index 3e4a4b512..06195242e 100644 --- a/src/platform/macos/appkit_host.m +++ b/src/platform/macos/appkit_host.m @@ -699,6 +699,15 @@ @interface NativeSdkAppKitHost : NSObject @property(nonatomic, strong) NativeSdkBridgeScriptHandler *bridgeScriptHandler; @property(nonatomic, strong) NativeSdkAssetSchemeHandler *assetSchemeHandler; @property(nonatomic, strong) NSMutableDictionary *windows; +/// Overlay config staged BEFORE a window's create call +/// (`native_sdk_appkit_set_pending_window_overlay`), consumed inside +/// `createWindowWithId:` so transparency/level/visibility apply before the +/// window is ever ordered front — configuring after create would flash an +/// opaque, focus-stealing window for a frame. +@property(nonatomic, strong) NSMutableDictionary *pendingWindowOverlays; +/// Per-window MAIN-webview content script (WindowOptions.user_script), +/// staged at create and consumed when the lazy main webview is built. +@property(nonatomic, strong) NSMutableDictionary *windowMainUserScripts; @property(nonatomic, strong) NSMutableDictionary *webViews; @property(nonatomic, strong) NSMutableDictionary *delegates; @property(nonatomic, strong) NSMutableDictionary *bridgeScriptHandlers; @@ -957,7 +966,8 @@ - (BOOL)adoptViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label surf - (BOOL)viewIsAdoptedSurfaceDescendant:(NSView *)view; - (void)installAdoptedSurfaceClickMonitor; - (BOOL)releaseViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label; -- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled; +- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled userScript:(NSString *)userScript; +- (void)applyWindowOverlay:(NSDictionary *)overlay toWindow:(NSWindow *)window; - (BOOL)setNativeViewCursorInWindow:(uint64_t)windowId label:(NSString *)label cursor:(NSInteger)cursor; - (BOOL)setWebViewFrameInWindow:(uint64_t)windowId label:(NSString *)label x:(double)x y:(double)y width:(double)width height:(double)height; - (BOOL)navigateWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url; @@ -1247,6 +1257,15 @@ - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows: @implementation NativeSdkWebView +// Overlay/companion windows belong to an app that is usually NOT frontmost +// (a desktop pet floating over the user's editor). Without this, the first +// click on such a window is consumed as an app-activation click and never +// reaches the web content — the window appears dead until it is already +// focused. Accepting first mouse delivers that click straight to the page. +- (BOOL)acceptsFirstMouse:(NSEvent *)event { + return YES; +} + - (BOOL)pointIsCovered:(NSPoint)point { for (NSValue *value in self.coveredMouseRects) { if (NSPointInRect(point, value.rectValue)) return YES; @@ -7154,6 +7173,8 @@ - (instancetype)initWithAppName:(NSString *)appName displayName:(NSString *)disp self.iconPath = iconPath ?: @""; self.windowLabel = windowLabel.length > 0 ? windowLabel : @"main"; self.windows = [[NSMutableDictionary alloc] init]; + self.pendingWindowOverlays = [[NSMutableDictionary alloc] init]; + self.windowMainUserScripts = [[NSMutableDictionary alloc] init]; self.webViews = [[NSMutableDictionary alloc] init]; self.delegates = [[NSMutableDictionary alloc] init]; self.bridgeScriptHandlers = [[NSMutableDictionary alloc] init]; @@ -7309,17 +7330,61 @@ - (BOOL)createWindowWithId:(uint64_t)windowId title:(NSString *)title label:(NSS [weakSelf showDeferredWindowIfPending:windowId reason:"fallback-deadline"]; }); } + // Overlay config staged before create applies here — after the window + // exists, before it is ever ordered front — and steers the ordering: + // `visible:NO` windows stay ordered out for a later + // `setWindowVisible`, `activate:NO` windows come up front without + // becoming key or activating the app (companion bubbles must never + // steal focus from what the user is typing in). + NSDictionary *overlay = self.pendingWindowOverlays[key]; + if (overlay) { + [self.pendingWindowOverlays removeObjectForKey:key]; + [self applyWindowOverlay:overlay toWindow:window]; + } + BOOL overlayVisible = overlay[@"visible"] ? [overlay[@"visible"] boolValue] : YES; + BOOL overlayActivate = overlay[@"activate"] ? [overlay[@"activate"] boolValue] : YES; if (makeMain) { self.window = window; self.delegate = delegate; self.windowLabel = label.length > 0 ? label : @"main"; - } else if (showPolicy != 1) { - [window makeKeyAndOrderFront:nil]; - [NSApp activate]; + } else if (showPolicy != 1 && overlayVisible) { + if (overlayActivate) { + [window makeKeyAndOrderFront:nil]; + [NSApp activate]; + } else { + [window orderFrontRegardless]; + } } return YES; } +/// The visual half of an overlay config (the visibility half rides the +/// create/show paths). Transparent windows keep their shadow off-switch +/// separate: a shadow would re-draw the rectangle the transparency hides, +/// but opaque raised-level windows may still want theirs. +- (void)applyWindowOverlay:(NSDictionary *)overlay toWindow:(NSWindow *)window { + if ([overlay[@"transparent"] boolValue]) { + window.opaque = NO; + window.backgroundColor = NSColor.clearColor; + } + if (overlay[@"shadow"] && ![overlay[@"shadow"] boolValue]) { + window.hasShadow = NO; + } + switch ([overlay[@"level"] intValue]) { + case 1: window.level = NSFloatingWindowLevel; break; + case 2: window.level = NSStatusWindowLevel; break; + case 3: window.level = NSScreenSaverWindowLevel; break; + default: break; + } + if ([overlay[@"clickThrough"] boolValue]) { + window.ignoresMouseEvents = YES; + } + if ([overlay[@"allWorkspaces"] boolValue]) { + window.collectionBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces | + NSWindowCollectionBehaviorFullScreenAuxiliary; + } +} + // Order a deferred-show window front exactly once — from the first // gpu-surface present (the contract), or from the fallback deadline. // `NATIVE_SDK_WINDOW_TIMING=1` logs the create→show latency. @@ -7585,6 +7650,15 @@ - (WKWebView *)ensureMainWebViewForWindowId:(uint64_t)windowId { injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [userContentController addUserScript:bridgeScript]; + // App-supplied content script for this window's main webview (a + // remote page loaded here can be observed + relayed through the + // bridge, e.g. the CursorBuddy account page reporting a connection). + NSString *mainUserScript = self.windowMainUserScripts[key]; + if (mainUserScript.length > 0) { + [userContentController addUserScript:[[WKUserScript alloc] initWithSource:mainUserScript + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:YES]]; + } configuration.userContentController = userContentController; if ([configuration.preferences respondsToSelector:NSSelectorFromString(@"setDeveloperExtrasEnabled:")]) { [configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; @@ -8240,7 +8314,7 @@ - (BOOL)releaseViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label { return YES; } -- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled { +- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled userScript:(NSString *)userScript { if (label.length == 0 || url.length == 0 || width <= 0 || height <= 0 || x < 0 || y < 0) return NO; NSWindow *window = self.windows[@(windowId)] ?: (windowId == 1 ? self.window : nil); if (!window || !window.contentView) return NO; @@ -8258,14 +8332,22 @@ - (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NS if (assetSchemeHandler) { [configuration setURLSchemeHandler:assetSchemeHandler forURLScheme:@"zero"]; } - if (bridgeEnabled) { + if (bridgeEnabled || userScript.length > 0) { WKUserContentController *controller = [[WKUserContentController alloc] init]; - NativeSdkBridgeScriptHandler *handler = [[NativeSdkBridgeScriptHandler alloc] init]; - handler.host = self; - handler.windowId = windowId; - handler.webViewLabel = label; - [controller addScriptMessageHandler:handler name:@"nativeSdkBridge"]; - [controller addUserScript:[[WKUserScript alloc] initWithSource:NativeSdkAppKitBridgeScript() injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; + if (bridgeEnabled) { + NativeSdkBridgeScriptHandler *handler = [[NativeSdkBridgeScriptHandler alloc] init]; + handler.host = self; + handler.windowId = windowId; + handler.webViewLabel = label; + [controller addScriptMessageHandler:handler name:@"nativeSdkBridge"]; + [controller addUserScript:[[WKUserScript alloc] initWithSource:NativeSdkAppKitBridgeScript() injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; + } + if (userScript.length > 0) { + // The app's content-script equivalent: page world, document + // start, main frame only — it observes the page and relays + // through the bridge; subframes never see it. + [controller addUserScript:[[WKUserScript alloc] initWithSource:userScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; + } configuration.userContentController = controller; } if ([configuration.preferences respondsToSelector:NSSelectorFromString(@"setDeveloperExtrasEnabled:")]) { @@ -11932,11 +12014,121 @@ int native_sdk_appkit_update_widget_accessibility(native_sdk_appkit_host_t *host return [object updateWidgetAccessibilityInWindow:window_id label:labelString ?: @"" nodes:nodes count:node_count] ? 1 : 0; } -int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled) { +int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled, const char *user_script, size_t user_script_len) { NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; NSString *labelString = label ? [[NSString alloc] initWithBytes:label length:label_len encoding:NSUTF8StringEncoding] : @""; NSString *urlString = url ? [[NSString alloc] initWithBytes:url length:url_len encoding:NSUTF8StringEncoding] : @""; - return [object createWebViewInWindow:window_id label:labelString ?: @"" url:urlString ?: @"" x:x y:y width:width height:height layer:layer transparent:transparent != 0 bridgeEnabled:bridge_enabled != 0] ? 1 : 0; + NSString *userScriptString = NativeSdkStringFromBytes(user_script, user_script_len) ?: @""; + return [object createWebViewInWindow:window_id label:labelString ?: @"" url:urlString ?: @"" x:x y:y width:width height:height layer:layer transparent:transparent != 0 bridgeEnabled:bridge_enabled != 0 userScript:userScriptString] ? 1 : 0; +} + +int native_sdk_appkit_set_window_user_script(native_sdk_appkit_host_t *host, uint64_t window_id, const char *script, size_t script_len) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSString *value = NativeSdkStringFromBytes(script, script_len) ?: @""; + // Staged for the window's lazily-created main webview + // (ensureMainWebViewForWindowId consumes it). Empty clears any prior. + if (value.length > 0) { + object.windowMainUserScripts[@(window_id)] = value; + } else { + [object.windowMainUserScripts removeObjectForKey:@(window_id)]; + } + return 1; +} + +int native_sdk_appkit_set_pending_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces, int visible, int activate) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + // Staged for the UPCOMING create call — createWindowWithId: consumes it + // before the window is ever ordered front. See pendingWindowOverlays. + object.pendingWindowOverlays[@(window_id)] = @{ + @"transparent" : @(transparent != 0), + @"shadow" : @(shadow != 0), + @"level" : @(level), + @"clickThrough" : @(click_through != 0), + @"allWorkspaces" : @(all_workspaces != 0), + @"visible" : @(visible != 0), + @"activate" : @(activate != 0), + }; + return 1; +} + +int native_sdk_appkit_configure_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + // The EXISTING-window path — the main window is created inside + // native_sdk_appkit_create before any overlay call can be staged, but + // it is not ordered front until the run loop starts, so configuring it + // here (before run) is still flash-free. + [object applyWindowOverlay:@{ + @"transparent" : @(transparent != 0), + @"shadow" : @(shadow != 0), + @"level" : @(level), + @"clickThrough" : @(click_through != 0), + @"allWorkspaces" : @(all_workspaces != 0), + } toWindow:window]; + return 1; +} + +int native_sdk_appkit_set_window_frame(native_sdk_appkit_host_t *host, uint64_t window_id, double x, double y, double width, double height) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + // AppKit screen coordinates, exactly the space window-frame events + // report — callers needing a top-left-origin convention convert on + // their side of the boundary. + [window setFrame:NSMakeRect(x, y, width, height) display:YES]; + return 1; +} + +int native_sdk_appkit_set_window_visible(native_sdk_appkit_host_t *host, uint64_t window_id, int visible, int activate) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + if (!visible) { + [window orderOut:nil]; + } else if (activate) { + [window makeKeyAndOrderFront:nil]; + [NSApp activate]; + } else { + // Show WITHOUT stealing focus: companion bubbles appear beside the + // user's work; they become key only when clicked into. + [window orderFrontRegardless]; + } + return 1; +} + +int native_sdk_appkit_set_window_click_through(native_sdk_appkit_host_t *host, uint64_t window_id, int click_through) { + NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + window.ignoresMouseEvents = click_through != 0; + return 1; +} + +int native_sdk_appkit_pointer_position(native_sdk_appkit_host_t *host, double *x, double *y) { + (void)host; + // Global pointer in AppKit screen coordinates (the window-frame space). + // Click-through overlay windows receive no pointer events of their own, + // so followers and hover hit-tests poll this instead. + NSPoint location = [NSEvent mouseLocation]; + if (x) *x = location.x; + if (y) *y = location.y; + return 1; +} + +int native_sdk_appkit_screen_work_area(native_sdk_appkit_host_t *host, double *x, double *y, double *width, double *height) { + (void)host; + // The PRIMARY display (screens[0] — the one owning the menu bar), not + // mainScreen (the key window's screen): overlay placement math needs a + // stable reference frame regardless of where focus sits. + NSScreen *primary = NSScreen.screens.firstObject; + if (!primary) return 0; + NSRect visible = primary.visibleFrame; + if (x) *x = visible.origin.x; + if (y) *y = visible.origin.y; + if (width) *width = visible.size.width; + if (height) *height = visible.size.height; + return 1; } int native_sdk_appkit_set_webview_frame(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, double x, double y, double width, double height) { diff --git a/src/platform/macos/cef_host.mm b/src/platform/macos/cef_host.mm index e8b352a20..9798e3bea 100644 --- a/src/platform/macos/cef_host.mm +++ b/src/platform/macos/cef_host.mm @@ -2493,7 +2493,106 @@ int native_sdk_appkit_window_chrome_insets(native_sdk_appkit_host_t *host, uint6 return 1; } -int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled) { +static void NativeSdkChromiumApplyWindowOverlay(NSWindow *window, int transparent, int shadow, int level, int click_through, int all_workspaces) { + if (transparent) { + window.opaque = NO; + window.backgroundColor = NSColor.clearColor; + } + if (!shadow) window.hasShadow = NO; + switch (level) { + case 1: window.level = NSFloatingWindowLevel; break; + case 2: window.level = NSStatusWindowLevel; break; + case 3: window.level = NSScreenSaverWindowLevel; break; + default: break; + } + if (click_through) window.ignoresMouseEvents = YES; + if (all_workspaces) { + window.collectionBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces | + NSWindowCollectionBehaviorFullScreenAuxiliary; + } +} + +int native_sdk_appkit_set_window_user_script(native_sdk_appkit_host_t *host, uint64_t window_id, const char *script, size_t script_len) { + // Unsupported on the Chromium host (no WKUserScript equivalent wired up). + (void)host; (void)window_id; (void)script; (void)script_len; + return 0; +} + +int native_sdk_appkit_set_pending_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces, int visible, int activate) { + // Unsupported on the Chromium host: its create path does not consume + // staged overlay config, so pretending to accept it would flash an + // opaque focus-stealing window. Callers see 0 and can fall back to + // configure-after-create. + (void)host; (void)window_id; (void)transparent; (void)shadow; (void)level; + (void)click_through; (void)all_workspaces; (void)visible; (void)activate; + return 0; +} + +int native_sdk_appkit_configure_window_overlay(native_sdk_appkit_host_t *host, uint64_t window_id, int transparent, int shadow, int level, int click_through, int all_workspaces) { + NativeSdkChromiumHost *object = (__bridge NativeSdkChromiumHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + NativeSdkChromiumApplyWindowOverlay(window, transparent, shadow, level, click_through, all_workspaces); + return 1; +} + +int native_sdk_appkit_set_window_frame(native_sdk_appkit_host_t *host, uint64_t window_id, double x, double y, double width, double height) { + NativeSdkChromiumHost *object = (__bridge NativeSdkChromiumHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + [window setFrame:NSMakeRect(x, y, width, height) display:YES]; + return 1; +} + +int native_sdk_appkit_set_window_visible(native_sdk_appkit_host_t *host, uint64_t window_id, int visible, int activate) { + NativeSdkChromiumHost *object = (__bridge NativeSdkChromiumHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + if (!visible) { + [window orderOut:nil]; + } else if (activate) { + [window makeKeyAndOrderFront:nil]; + [NSApp activate]; + } else { + [window orderFrontRegardless]; + } + return 1; +} + +int native_sdk_appkit_set_window_click_through(native_sdk_appkit_host_t *host, uint64_t window_id, int click_through) { + NativeSdkChromiumHost *object = (__bridge NativeSdkChromiumHost *)host; + NSWindow *window = object.windows[@(window_id)]; + if (!window) return 0; + window.ignoresMouseEvents = click_through != 0; + return 1; +} + +int native_sdk_appkit_pointer_position(native_sdk_appkit_host_t *host, double *x, double *y) { + (void)host; + NSPoint location = [NSEvent mouseLocation]; + if (x) *x = location.x; + if (y) *y = location.y; + return 1; +} + +int native_sdk_appkit_screen_work_area(native_sdk_appkit_host_t *host, double *x, double *y, double *width, double *height) { + (void)host; + NSScreen *primary = NSScreen.screens.firstObject; + if (!primary) return 0; + NSRect visible = primary.visibleFrame; + if (x) *x = visible.origin.x; + if (y) *y = visible.origin.y; + if (width) *width = visible.size.width; + if (height) *height = visible.size.height; + return 1; +} + +int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled, const char *user_script, size_t user_script_len) { + // Accepted for ABI parity; the Chromium host has no user-script + // injection path yet (WKUserScript is a WebKit concept — the CEF + // equivalent is a render-process hook this host does not wire up). + (void)user_script; + (void)user_script_len; NativeSdkChromiumHost *object = (__bridge NativeSdkChromiumHost *)host; NSString *labelString = label ? [[NSString alloc] initWithBytes:label length:label_len encoding:NSUTF8StringEncoding] : @""; NSString *urlString = url ? [[NSString alloc] initWithBytes:url length:url_len encoding:NSUTF8StringEncoding] : @""; diff --git a/src/platform/macos/root.zig b/src/platform/macos/root.zig index 45b068f04..6f55dc69d 100644 --- a/src/platform/macos/root.zig +++ b/src/platform/macos/root.zig @@ -180,6 +180,14 @@ extern fn native_sdk_appkit_close_window(host: *AppKitHost, window_id: u64) c_in extern fn native_sdk_appkit_minimize_window(host: *AppKitHost, window_id: u64) c_int; extern fn native_sdk_appkit_show_window(host: *AppKitHost, window_id: u64) c_int; extern fn native_sdk_appkit_set_window_close_policy(host: *AppKitHost, window_id: u64, close_policy: c_int) c_int; +extern fn native_sdk_appkit_set_window_user_script(host: *AppKitHost, window_id: u64, script: [*]const u8, script_len: usize) c_int; +extern fn native_sdk_appkit_set_pending_window_overlay(host: *AppKitHost, window_id: u64, transparent: c_int, shadow: c_int, level: c_int, click_through: c_int, all_workspaces: c_int, visible: c_int, activate: c_int) c_int; +extern fn native_sdk_appkit_configure_window_overlay(host: *AppKitHost, window_id: u64, transparent: c_int, shadow: c_int, level: c_int, click_through: c_int, all_workspaces: c_int) c_int; +extern fn native_sdk_appkit_set_window_frame(host: *AppKitHost, window_id: u64, x: f64, y: f64, width: f64, height: f64) c_int; +extern fn native_sdk_appkit_set_window_visible(host: *AppKitHost, window_id: u64, visible: c_int, activate: c_int) c_int; +extern fn native_sdk_appkit_set_window_click_through(host: *AppKitHost, window_id: u64, click_through: c_int) c_int; +extern fn native_sdk_appkit_pointer_position(host: *AppKitHost, x: *f64, y: *f64) c_int; +extern fn native_sdk_appkit_screen_work_area(host: *AppKitHost, x: *f64, y: *f64, width: *f64, height: *f64) c_int; extern fn native_sdk_appkit_start_window_drag(host: *AppKitHost, window_id: u64) c_int; extern fn native_sdk_appkit_window_chrome_insets(host: *AppKitHost, window_id: u64, top: *f64, left: *f64, bottom: *f64, right: *f64, buttons_x: *f64, buttons_y: *f64, buttons_width: *f64, buttons_height: *f64) c_int; extern fn native_sdk_appkit_create_view(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, kind: c_int, parent: [*]const u8, parent_len: usize, x: f64, y: f64, width: f64, height: f64, layer: c_int, visible: c_int, enabled: c_int, role: [*]const u8, role_len: usize, accessibility_label: [*]const u8, accessibility_label_len: usize, text: [*]const u8, text_len: usize, command: [*]const u8, command_len: usize) c_int; @@ -220,7 +228,7 @@ extern fn native_sdk_appkit_present_gpu_surface_packet_binary(host: *AppKitHost, extern fn native_sdk_appkit_upload_gpu_surface_image(host: *AppKitHost, image_id: u64, width: usize, height: usize, rgba8: [*]const u8, rgba8_len: usize) c_int; extern fn native_sdk_appkit_remove_gpu_surface_image(host: *AppKitHost, image_id: u64) c_int; extern fn native_sdk_appkit_update_widget_accessibility(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, nodes: [*]const AppKitWidgetAccessibilityNode, node_count: usize) c_int; -extern fn native_sdk_appkit_create_webview(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, url: [*]const u8, url_len: usize, x: f64, y: f64, width: f64, height: f64, layer: c_int, transparent: c_int, bridge_enabled: c_int) c_int; +extern fn native_sdk_appkit_create_webview(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, url: [*]const u8, url_len: usize, x: f64, y: f64, width: f64, height: f64, layer: c_int, transparent: c_int, bridge_enabled: c_int, user_script: [*]const u8, user_script_len: usize) c_int; extern fn native_sdk_appkit_set_webview_frame(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, x: f64, y: f64, width: f64, height: f64) c_int; extern fn native_sdk_appkit_navigate_webview(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, url: [*]const u8, url_len: usize) c_int; extern fn native_sdk_appkit_set_webview_zoom(host: *AppKitHost, window_id: u64, label: [*]const u8, label_len: usize, zoom: f64) c_int; @@ -624,6 +632,15 @@ pub const MacPlatform = struct { // .hide threads through here so the STARTUP window's red // button hides from the first frame on. applyWindowClosePolicy(host, window_options.id, window_options.close_policy); + + // The startup window exists before any overlay config could be + // staged, but it is not ordered front until the run loop starts — + // configuring it here is still flash-free. (visible/activate are + // create-ordering concerns; the run path owns the main window's + // first show.) + if (windowHasOverlayOptions(window_options)) { + _ = native_sdk_appkit_configure_window_overlay(host, window_options.id, if (window_options.transparent) 1 else 0, if (window_options.shadow) 1 else 0, windowLevelInt(window_options.level), if (window_options.click_through) 1 else 0, if (window_options.all_workspaces) 1 else 0); + } return .{ .host = host, .web_engine = web_engine, @@ -701,6 +718,11 @@ pub const MacPlatform = struct { .minimize_window_fn = minimizeWindow, .show_window_fn = showWindow, .quit_app_fn = quitApp, + .set_window_frame_fn = setWindowFrame, + .set_window_visible_fn = setWindowVisible, + .set_window_click_through_fn = setWindowClickThrough, + .pointer_position_fn = pointerPosition, + .screen_work_area_fn = screenWorkArea, .start_window_drag_fn = startWindowDrag, .window_chrome_fn = windowChrome, .create_view_fn = createView, @@ -1220,6 +1242,24 @@ fn showModeInt(mode: platform_mod.WindowShowMode) c_int { }; } +fn windowLevelInt(level: platform_mod.WindowLevel) c_int { + return switch (level) { + .normal => 0, + .floating => 1, + .status => 2, + .screen_saver => 3, + }; +} + +/// True when any overlay-shape option departs from the plain-window +/// defaults — only then is config staged/applied, so ordinary windows +/// keep the exact legacy create path. +fn windowHasOverlayOptions(options: platform_mod.WindowOptions) bool { + return options.transparent or !options.shadow or options.level != .normal or + options.click_through or options.all_workspaces or !options.visible or + !options.activate; +} + fn closePolicyInt(policy: platform_mod.WindowClosePolicy) c_int { return switch (policy) { .quit => 0, @@ -1251,6 +1291,18 @@ fn createWindow(context: ?*anyopaque, options: platform_mod.WindowOptions) anyer const self: *MacPlatform = @ptrCast(@alignCast(context.?)); const title = options.resolvedTitle(self.app_info.app_name); const frame = options.default_frame; + // Overlay config must be staged BEFORE the create call — the host + // consumes it inside createWindowWithId: so transparency/level apply + // before the window is ever ordered front, and visible:false / + // activate:false windows never flash or steal focus at create. + if (windowHasOverlayOptions(options)) { + _ = native_sdk_appkit_set_pending_window_overlay(self.host, options.id, if (options.transparent) 1 else 0, if (options.shadow) 1 else 0, windowLevelInt(options.level), if (options.click_through) 1 else 0, if (options.all_workspaces) 1 else 0, if (options.visible) 1 else 0, if (options.activate) 1 else 0); + } + // Stage the main-webview content script BEFORE create so the lazily + // built webview picks it up on first load. + if (options.user_script.len > 0) { + _ = native_sdk_appkit_set_window_user_script(self.host, options.id, options.user_script.ptr, options.user_script.len); + } if (native_sdk_appkit_create_window(self.host, options.id, title.ptr, title.len, options.label.ptr, options.label.len, frame.x, frame.y, frame.width, frame.height, if (options.restore_state) 1 else 0, if (options.resizable) 1 else 0, titlebarStyleInt(options.titlebar), showModeInt(options.show)) == 0) return error.CreateFailed; applyWindowContentMinSize(self.host, options.id, options.min_width, options.min_height); applyWindowClosePolicy(self.host, options.id, options.close_policy); @@ -1280,6 +1332,39 @@ fn minimizeWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerr if (native_sdk_appkit_minimize_window(self.host, window_id) == 0) return error.WindowNotFound; } +fn setWindowFrame(context: ?*anyopaque, window_id: platform_mod.WindowId, frame: geometry.RectF) anyerror!void { + const self: *MacPlatform = @ptrCast(@alignCast(context.?)); + if (native_sdk_appkit_set_window_frame(self.host, window_id, frame.x, frame.y, frame.width, frame.height) == 0) return error.WindowNotFound; +} + +fn setWindowVisible(context: ?*anyopaque, window_id: platform_mod.WindowId, visible: bool, activate: bool) anyerror!void { + const self: *MacPlatform = @ptrCast(@alignCast(context.?)); + if (native_sdk_appkit_set_window_visible(self.host, window_id, if (visible) 1 else 0, if (activate) 1 else 0) == 0) return error.WindowNotFound; +} + +fn setWindowClickThrough(context: ?*anyopaque, window_id: platform_mod.WindowId, click_through: bool) anyerror!void { + const self: *MacPlatform = @ptrCast(@alignCast(context.?)); + if (native_sdk_appkit_set_window_click_through(self.host, window_id, if (click_through) 1 else 0) == 0) return error.WindowNotFound; +} + +fn pointerPosition(context: ?*anyopaque) geometry.PointF { + const self: *MacPlatform = @ptrCast(@alignCast(context.?)); + var x: f64 = 0; + var y: f64 = 0; + _ = native_sdk_appkit_pointer_position(self.host, &x, &y); + return .{ .x = @floatCast(x), .y = @floatCast(y) }; +} + +fn screenWorkArea(context: ?*anyopaque) geometry.RectF { + const self: *MacPlatform = @ptrCast(@alignCast(context.?)); + var x: f64 = 0; + var y: f64 = 0; + var width: f64 = 0; + var height: f64 = 0; + if (native_sdk_appkit_screen_work_area(self.host, &x, &y, &width, &height) == 0) return geometry.RectF.init(0, 0, 0, 0); + return geometry.RectF.init(@floatCast(x), @floatCast(y), @floatCast(width), @floatCast(height)); +} + fn showWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerror!void { const self: *MacPlatform = @ptrCast(@alignCast(context.?)); if (native_sdk_appkit_show_window(self.host, window_id) == 0) return error.WindowNotFound; @@ -1932,7 +2017,7 @@ fn appKitCursor(cursor: platform_mod.Cursor) c_int { fn createWebView(context: ?*anyopaque, options: platform_mod.WebViewOptions) anyerror!void { const self: *MacPlatform = @ptrCast(@alignCast(context.?)); const frame = options.frame; - if (native_sdk_appkit_create_webview(self.host, options.window_id, options.label.ptr, options.label.len, options.url.ptr, options.url.len, frame.x, frame.y, frame.width, frame.height, options.layer, if (options.transparent) 1 else 0, if (options.bridge_enabled) 1 else 0) == 0) return error.CreateFailed; + if (native_sdk_appkit_create_webview(self.host, options.window_id, options.label.ptr, options.label.len, options.url.ptr, options.url.len, frame.x, frame.y, frame.width, frame.height, options.layer, if (options.transparent) 1 else 0, if (options.bridge_enabled) 1 else 0, options.user_script.ptr, options.user_script.len) == 0) return error.CreateFailed; } fn setWebViewFrame(context: ?*anyopaque, window_id: platform_mod.WindowId, label: []const u8, frame: geometry.RectF) anyerror!void { diff --git a/src/platform/null_platform.zig b/src/platform/null_platform.zig index d96c7fe0b..c15608bc0 100644 --- a/src/platform/null_platform.zig +++ b/src/platform/null_platform.zig @@ -282,6 +282,18 @@ pub const NullAudio = struct { } }; +/// The overlay-shape half of `WindowOptions`, captured at the create +/// seam (see `NullPlatform.window_overlay`). +pub const WindowOverlayRecord = struct { + transparent: bool = false, + shadow: bool = true, + level: types.WindowLevel = .normal, + click_through: bool = false, + all_workspaces: bool = false, + visible: bool = true, + activate: bool = true, +}; + pub const NullPlatform = struct { surface_value: Surface = .{}, web_engine: WebEngine = .system, @@ -385,6 +397,25 @@ pub const NullPlatform = struct { /// seam-regression purpose as `window_resizable`. window_min_width: [max_windows]f32 = [_]f32{0} ** max_windows, window_min_height: [max_windows]f32 = [_]f32{0} ** max_windows, + /// Captured overlay-shape options per created window + /// (`WindowOptions.transparent/shadow/level/click_through/ + /// all_workspaces/visible/activate`), indexed like `windows` — the + /// observable seam overlay-window tests pin. + window_overlay: [max_windows]WindowOverlayRecord = [_]WindowOverlayRecord{.{}} ** max_windows, + /// Live click-through state per window: starts as the created + /// option, flips on `set_window_click_through_fn` — overlays toggle + /// it as the pointer crosses their visible content. + window_click_through: [max_windows]bool = [_]bool{false} ** max_windows, + /// setWindowFrame calls per window; `windows[i].frame` holds the + /// last applied rect. + window_set_frame_count: [max_windows]u32 = [_]u32{0} ** max_windows, + /// Last `set_window_visible_fn` activate flag per window — the + /// "shown without stealing focus" seam. + window_show_activate: [max_windows]bool = [_]bool{true} ** max_windows, + /// Test-staged global pointer and primary work area answered by + /// `pointer_position_fn`/`screen_work_area_fn`. + pointer_point: geometry.PointF = .{}, + work_area: geometry.RectF = geometry.RectF.init(0, 0, 0, 0), /// Live visibility per window, modeling the macOS host: immediate /// windows are visible at create; `.on_first_present` windows stay /// hidden until their first gpu-surface present (or an explicit @@ -803,6 +834,11 @@ pub const NullPlatform = struct { .minimize_window_fn = minimizeWindow, .show_window_fn = showWindow, .quit_app_fn = quitApp, + .set_window_frame_fn = setWindowFrame, + .set_window_visible_fn = setWindowVisible, + .set_window_click_through_fn = setWindowClickThrough, + .pointer_position_fn = pointerPosition, + .screen_work_area_fn = screenWorkArea, .start_window_drag_fn = startWindowDrag, .window_chrome_fn = windowChrome, .set_window_drag_regions_fn = setWindowDragRegions, @@ -1053,13 +1089,31 @@ pub const NullPlatform = struct { self.window_close_policy[self.window_count] = options.close_policy; self.window_min_width[self.window_count] = options.min_width; self.window_min_height[self.window_count] = options.min_height; + self.window_overlay[self.window_count] = .{ + .transparent = options.transparent, + .shadow = options.shadow, + .level = options.level, + .click_through = options.click_through, + .all_workspaces = options.all_workspaces, + .visible = options.visible, + .activate = options.activate, + }; + self.window_click_through[self.window_count] = options.click_through; + self.window_set_frame_count[self.window_count] = 0; + self.window_show_activate[self.window_count] = options.activate; self.window_first_present_seq[self.window_count] = 0; // Present-before-show: deferred windows are created hidden and - // become visible on their first gpu-surface present. - if (options.show == .immediate) { + // become visible on their first gpu-surface present. An overlay + // window declared `visible:false` is also created hidden — it + // appears only on an explicit `setWindowVisible` (the real hosts + // skip its order-front inside create). + if (options.show == .immediate and options.visible) { self.window_visible[self.window_count] = true; self.show_op_seq += 1; self.window_shown_seq[self.window_count] = self.show_op_seq; + } else if (options.show == .immediate) { + self.window_visible[self.window_count] = false; + self.window_shown_seq[self.window_count] = 0; } else { self.window_visible[self.window_count] = false; self.window_shown_seq[self.window_count] = 0; @@ -1162,6 +1216,48 @@ pub const NullPlatform = struct { self.window_occluded[index] = true; } + fn setWindowFrame(context: ?*anyopaque, window_id: WindowId, frame: geometry.RectF) anyerror!void { + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); + const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound; + self.windows[index].frame = frame; + self.window_set_frame_count[index] += 1; + } + + fn setWindowVisible(context: ?*anyopaque, window_id: WindowId, visible: bool, activate: bool) anyerror!void { + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); + const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound; + if (visible and !self.window_visible[index]) { + self.show_op_seq += 1; + self.window_shown_seq[index] = self.show_op_seq; + } + self.window_visible[index] = visible; + self.window_show_activate[index] = activate; + // Showing WITHOUT activate never takes key (orderFrontRegardless); + // showing with it does, mirroring makeKeyAndOrderFront. + if (visible and activate) { + for (self.windows[0..self.window_count], 0..) |*window, scan| { + window.focused = scan == index; + } + } + if (!visible) self.windows[index].focused = false; + } + + fn setWindowClickThrough(context: ?*anyopaque, window_id: WindowId, click_through: bool) anyerror!void { + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); + const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound; + self.window_click_through[index] = click_through; + } + + fn pointerPosition(context: ?*anyopaque) geometry.PointF { + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); + return self.pointer_point; + } + + fn screenWorkArea(context: ?*anyopaque) geometry.RectF { + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); + return self.work_area; + } + fn showWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void { const self: *NullPlatform = @ptrCast(@alignCast(context.?)); if (self.fail_next_show_window) { diff --git a/src/platform/null_platform_tests.zig b/src/platform/null_platform_tests.zig index 7c2087416..880c49df2 100644 --- a/src/platform/null_platform_tests.zig +++ b/src/platform/null_platform_tests.zig @@ -418,6 +418,65 @@ test "null platform records webview lifecycle" { try std.testing.expectEqual(@as(usize, 0), null_platform.webview_count); } +test "null platform records overlay window options and control calls" { + var null_platform = NullPlatform.init(.{}); + const services = null_platform.platform().services; + + // Overlay shape survives to the create seam, and a visible:false + // window is created hidden — it appears only on setWindowVisible. + _ = try services.createWindow(.{ + .id = 7, + .label = "bubble", + .title = "Bubble", + .default_frame = geometry.RectF.init(40, 50, 560, 200), + .resizable = false, + .titlebar = .chromeless, + .transparent = true, + .shadow = false, + .level = .screen_saver, + .click_through = true, + .all_workspaces = true, + .visible = false, + .activate = false, + }); + const index: usize = null_platform.window_count - 1; + const overlay = null_platform.window_overlay[index]; + try std.testing.expect(overlay.transparent); + try std.testing.expect(!overlay.shadow); + try std.testing.expectEqual(types.WindowLevel.screen_saver, overlay.level); + try std.testing.expect(overlay.click_through); + try std.testing.expect(overlay.all_workspaces); + try std.testing.expect(!overlay.visible); + try std.testing.expect(!overlay.activate); + try std.testing.expect(!null_platform.window_visible[index]); + + // Show WITHOUT activate: visible flips, focus does not move. + try services.setWindowVisible(7, true, false); + try std.testing.expect(null_platform.window_visible[index]); + try std.testing.expect(!null_platform.windows[index].focused); + try std.testing.expect(!null_platform.window_show_activate[index]); + + // Programmatic placement lands in the recorded frame. + try services.setWindowFrame(7, geometry.RectF.init(100, 120, 560, 300)); + try std.testing.expectEqual(@as(f32, 120), null_platform.windows[index].frame.y); + try std.testing.expectEqual(@as(u32, 1), null_platform.window_set_frame_count[index]); + + // Click-through toggles at runtime (pointer enters the pet). + try services.setWindowClickThrough(7, false); + try std.testing.expect(!null_platform.window_click_through[index]); + + // Staged pointer/work-area answer the overlay placement queries. + null_platform.pointer_point = .{ .x = 12, .y = 34 }; + null_platform.work_area = geometry.RectF.init(0, 25, 1512, 920); + try std.testing.expectEqual(@as(f32, 34), services.pointerPosition().?.y); + try std.testing.expectEqual(@as(f32, 1512), services.screenWorkArea().width); + + // Unknown ids fail loudly, like every other window verb. + try std.testing.expectError(error.WindowNotFound, services.setWindowFrame(99, geometry.RectF.init(0, 0, 1, 1))); + try std.testing.expectError(error.WindowNotFound, services.setWindowVisible(99, true, true)); + try std.testing.expectError(error.WindowNotFound, services.setWindowClickThrough(99, true)); +} + test "null platform rejects invalid native view parents" { var null_platform = NullPlatform.init(.{}); const services = null_platform.platform().services; diff --git a/src/platform/root.zig b/src/platform/root.zig index f88976698..eed4d2286 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -74,6 +74,7 @@ pub const validateMenuItem = types.validateMenuItem; pub const isValidShortcutKey = types.isValidShortcutKey; pub const WindowRestorePolicy = types.WindowRestorePolicy; pub const WindowTitlebarStyle = types.WindowTitlebarStyle; +pub const WindowLevel = types.WindowLevel; pub const WindowChrome = types.WindowChrome; pub const FormFactor = types.FormFactor; pub const WindowDragRegion = types.WindowDragRegion; diff --git a/src/platform/types.zig b/src/platform/types.zig index 529f5a5c5..3f17650be 100644 --- a/src/platform/types.zig +++ b/src/platform/types.zig @@ -566,6 +566,22 @@ pub const WindowShowMode = enum { on_first_present, }; +/// The window's stacking band. `.normal` is every ordinary app window. +/// The raised bands exist for OVERLAY surfaces (heads-up companions, +/// screen annotations, pointer followers) that must stay above other +/// apps' windows: `.floating` sits above normal windows of the frontmost +/// app (macOS `NSFloatingWindowLevel`), `.status` above the menu bar +/// band (`NSStatusWindowLevel`), `.screen_saver` above everything +/// including fullscreen video (`NSScreenSaverWindowLevel`). Platforms +/// without distinct bands map every raised level to their single +/// always-on-top flag. +pub const WindowLevel = enum { + normal, + floating, + status, + screen_saver, +}; + /// What the user's close affordance (the red traffic light, cmd+W, the /// caption X) does to this window. /// `.quit` is the default and today's behavior unchanged: the window @@ -604,6 +620,44 @@ pub const WindowOptions = struct { /// What the user's close affordance does — see `WindowClosePolicy`. /// Fixed at create like the titlebar: it is host window state. close_policy: WindowClosePolicy = .quit, + /// Overlay-window shape (desktop companions, pointer followers, + /// screen annotations). `transparent` makes the window surface + /// non-opaque with a clear background — the page/canvas alpha is + /// the window's alpha — and the window's main WebView inherits the + /// treatment so an engine-drawn white page background cannot defeat + /// it. Pair with `.titlebar = .chromeless`; a transparent window + /// with OS chrome is an affordance lie. + transparent: bool = false, + /// OS drop shadow. Transparent overlays normally disable it — the + /// shadow re-draws the window's rectangle the transparency hides. + shadow: bool = true, + /// Stacking band; see `WindowLevel`. + level: WindowLevel = .normal, + /// Start with pointer events falling through to whatever is under + /// the window (macOS `ignoresMouseEvents`). Toggle at runtime via + /// `setWindowClickThrough` — overlays flip this as the pointer + /// enters/leaves their visible content. + click_through: bool = false, + /// Join every Space and stay visible beside fullscreen apps + /// (macOS `NSWindowCollectionBehaviorCanJoinAllSpaces` + + /// `FullScreenAuxiliary`). An overlay that vanishes on Space switch + /// is not an overlay. + all_workspaces: bool = false, + /// Created hidden when false; show later with `setWindowVisible`. + /// Anchored companion windows (a bubble positioned against another + /// window) create hidden and appear only once placed. + visible: bool = true, + /// When false the window is ordered front WITHOUT becoming key or + /// activating the app (macOS `orderFrontRegardless`) — showing it + /// never steals focus from what the user is doing. The window can + /// still become key when the user clicks into it. + activate: bool = true, + /// JavaScript injected into this window's MAIN webview at document + /// start (the content-script shape — observe the page, relay through + /// the bridge). Set from Zig only; the analogue of + /// `WebViewOptions.user_script` for a window's own webview rather + /// than a layered child. Empty = none. + user_script: []const u8 = "", pub fn resolvedTitle(self: WindowOptions, app_name: []const u8) []const u8 { return if (self.title.len > 0) self.title else app_name; @@ -673,6 +727,17 @@ pub const WindowCreateOptions = struct { min_height: f32 = 0, /// See `WindowOptions.close_policy`. close_policy: WindowClosePolicy = .quit, + /// Overlay-window shape; see the `WindowOptions` fields of the same + /// names for the semantics. + transparent: bool = false, + shadow: bool = true, + level: WindowLevel = .normal, + click_through: bool = false, + all_workspaces: bool = false, + visible: bool = true, + activate: bool = true, + /// Main-webview content script; see `WindowOptions.user_script`. + user_script: []const u8 = "", source: ?WebViewSource = null, pub fn windowOptions(self: WindowCreateOptions, id: WindowId, label: []const u8) WindowOptions { @@ -689,6 +754,14 @@ pub const WindowCreateOptions = struct { .min_width = self.min_width, .min_height = self.min_height, .close_policy = self.close_policy, + .transparent = self.transparent, + .shadow = self.shadow, + .level = self.level, + .click_through = self.click_through, + .all_workspaces = self.all_workspaces, + .visible = self.visible, + .activate = self.activate, + .user_script = self.user_script, }; } }; @@ -701,6 +774,13 @@ pub const WebViewOptions = struct { layer: i32 = 0, transparent: bool = false, bridge_enabled: bool = false, + /// JavaScript source injected into the WebView's MAIN frame at + /// document start, before the page's own scripts — the native + /// equivalent of a browser extension's content script (observe the + /// page, relay through the bridge). Set from Zig only; it is never + /// exposed to page JS or the builtin bridge commands, so remote + /// content cannot inject into a sibling WebView. + user_script: []const u8 = "", }; pub const WebViewInfo = struct { @@ -2327,6 +2407,23 @@ pub const PlatformServices = struct { /// the queued shape. Platforms without the concept leave this /// null. quit_app_fn: ?*const fn (context: ?*anyopaque) anyerror!void = null, + /// Programmatic window placement, in the platform's window + /// coordinate space (the same space `WindowState.frame` reports — + /// AppKit bottom-left screen points on macOS). Overlays anchor + /// companion windows and follow the pointer with this. + set_window_frame_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId, frame: geometry.RectF) anyerror!void = null, + /// Show/hide a window after create. `activate=false` orders the + /// window front without making it key or activating the app. + set_window_visible_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId, visible: bool, activate: bool) anyerror!void = null, + /// Runtime toggle for `WindowOptions.click_through`. + set_window_click_through_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId, click_through: bool) anyerror!void = null, + /// Global pointer position in the window coordinate space. Overlay + /// pointer-followers and hover hit-tests poll this; click-through + /// windows receive no pointer events of their own. + pointer_position_fn: ?*const fn (context: ?*anyopaque) geometry.PointF = null, + /// The primary display's usable area (excluding menu bar/Dock/task + /// bar), in the window coordinate space. + screen_work_area_fn: ?*const fn (context: ?*anyopaque) geometry.RectF = null, /// Hand the ACTIVE pointer-down to the platform as a window-drag /// gesture (the hidden-titlebar drag-region channel): the window /// moves once the pointer actually moves — a plain click moves @@ -2733,6 +2830,34 @@ pub const PlatformServices = struct { return quit_fn(self.context); } + pub fn setWindowFrame(self: PlatformServices, window_id: WindowId, frame: geometry.RectF) anyerror!void { + const set_fn = self.set_window_frame_fn orelse return error.UnsupportedService; + return set_fn(self.context, window_id, frame); + } + + pub fn setWindowVisible(self: PlatformServices, window_id: WindowId, visible: bool, activate: bool) anyerror!void { + const set_fn = self.set_window_visible_fn orelse return error.UnsupportedService; + return set_fn(self.context, window_id, visible, activate); + } + + pub fn setWindowClickThrough(self: PlatformServices, window_id: WindowId, click_through: bool) anyerror!void { + const set_fn = self.set_window_click_through_fn orelse return error.UnsupportedService; + return set_fn(self.context, window_id, click_through); + } + + /// Null when the platform cannot report a global pointer (headless, + /// tests) — callers treat that as "pointer unknown", never (0,0). + pub fn pointerPosition(self: PlatformServices) ?geometry.PointF { + const pointer_fn = self.pointer_position_fn orelse return null; + return pointer_fn(self.context); + } + + /// Zero-sized when the platform has no screen-geometry concept. + pub fn screenWorkArea(self: PlatformServices) geometry.RectF { + const area_fn = self.screen_work_area_fn orelse return geometry.RectF.init(0, 0, 0, 0); + return area_fn(self.context); + } + pub fn startWindowDrag(self: PlatformServices, window_id: WindowId) anyerror!void { const drag_fn = self.start_window_drag_fn orelse return error.UnsupportedService; return drag_fn(self.context, window_id); diff --git a/src/root.zig b/src/root.zig index cf7282305..a0c99bdff 100644 --- a/src/root.zig +++ b/src/root.zig @@ -155,6 +155,7 @@ pub const WindowInfo = platform.WindowInfo; pub const WindowState = platform.WindowState; pub const WindowRestorePolicy = platform.WindowRestorePolicy; pub const WindowTitlebarStyle = platform.WindowTitlebarStyle; +pub const WindowLevel = platform.WindowLevel; pub const WindowChrome = platform.WindowChrome; pub const FormFactor = platform.FormFactor; pub const WindowDragRegion = platform.WindowDragRegion; diff --git a/src/runtime/core.zig b/src/runtime/core.zig index 237310da0..a46c56bce 100644 --- a/src/runtime/core.zig +++ b/src/runtime/core.zig @@ -661,6 +661,11 @@ pub const Runtime = struct { pub const minimizeWindow = WindowViewMethods.minimizeWindow; pub const showWindow = WindowViewMethods.showWindow; pub const quitApp = WindowViewMethods.quitApp; + pub const setWindowFrame = WindowViewMethods.setWindowFrame; + pub const setWindowVisible = WindowViewMethods.setWindowVisible; + pub const setWindowClickThrough = WindowViewMethods.setWindowClickThrough; + pub const pointerPosition = WindowViewMethods.pointerPosition; + pub const screenWorkArea = WindowViewMethods.screenWorkArea; pub const createShellWindow = WindowViewMethods.createShellWindow; pub const createSourcelessShellWindow = WindowViewMethods.createSourcelessShellWindow; pub const createShellViews = WindowViewMethods.createShellViews; diff --git a/src/runtime/window_views.zig b/src/runtime/window_views.zig index bfa3ae3fa..b455f8c6e 100644 --- a/src/runtime/window_views.zig +++ b/src/runtime/window_views.zig @@ -146,6 +146,46 @@ pub fn RuntimeWindowViews(comptime Runtime: type) type { try self.options.platform.services.minimizeWindow(window_id); } + /// Programmatic window placement for overlay apps (anchoring a + /// companion window, following the pointer), in the platform's + /// window coordinate space. The runtime mirror updates + /// immediately; the host's frame-changed event echoes the same + /// rect afterwards. + pub fn setWindowFrame(self: *Runtime, window_id: platform.WindowId, frame: geometry.RectF) anyerror!void { + const index = Self.findWindowIndexById(self, window_id) orelse return error.WindowNotFound; + try self.options.platform.services.setWindowFrame(window_id, frame); + self.windows[index].info.frame = frame; + self.invalidated = true; + } + + /// Show/hide a tracked window after create. `activate=false` + /// shows without taking key or activating the app — companion + /// windows appear beside the user's work without stealing focus. + pub fn setWindowVisible(self: *Runtime, window_id: platform.WindowId, visible: bool, activate: bool) anyerror!void { + if (Self.findWindowIndexById(self, window_id) == null) return error.WindowNotFound; + try self.options.platform.services.setWindowVisible(window_id, visible, activate); + } + + /// Runtime toggle for `WindowOptions.click_through` — overlays + /// flip it as the pointer enters/leaves their visible content. + pub fn setWindowClickThrough(self: *Runtime, window_id: platform.WindowId, click_through: bool) anyerror!void { + if (Self.findWindowIndexById(self, window_id) == null) return error.WindowNotFound; + try self.options.platform.services.setWindowClickThrough(window_id, click_through); + } + + /// Global pointer in the window coordinate space, or null when + /// the platform cannot report one (headless). Click-through + /// windows receive no pointer events; overlays poll this. + pub fn pointerPosition(self: *Runtime) ?geometry.PointF { + return self.options.platform.services.pointerPosition(); + } + + /// The primary display's usable area in the window coordinate + /// space; zero-sized when the platform has no screen concept. + pub fn screenWorkArea(self: *Runtime) geometry.RectF { + return self.options.platform.services.screenWorkArea(); + } + /// The real OS show verb: unhide + activate — the counterpart /// to a `close_policy = .hide` hide, and what a tray "Open" /// action resolves to. Like `closeWindow`, the runtime flag