also looking at this
feat: finish native customization and platform seams
#401
21 files
+707
-17
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -6509,10 +6509,66 @@ fn handleCreateWebView( | ||
| 6509 | 6509 | return null; |
| 6510 | 6510 | } |
| 6511 | 6511 | |
| 6512 | /// `webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:` | |
| 6513 | /// | |
| 6514 | /// WKWebView does not present an `NSOpenPanel` for an HTML file input on its | |
| 6515 | /// own. Without this delegate callback the input is inert, including the | |
| 6516 | /// hidden-input-and-click pattern used by upload controls. Keep the panel | |
| 6517 | /// synchronous, like Craft's other AppKit file dialogs, so the completion | |
| 6518 | /// block remains valid until WebKit receives the selected URLs (or nil on | |
| 6519 | /// cancellation). | |
| 6520 | fn handleOpenPanel( | |
| 6521 | _: objc.id, | |
| 6522 | _: objc.SEL, | |
| 6523 | webView: objc.id, | |
| 6524 | parameters: objc.id, | |
| 6525 | frame: objc.id, | |
| 6526 | completionHandler: objc.id, | |
| 6527 | ) callconv(.c) void { | |
| 6528 | _ = webView; | |
| 6529 | _ = frame; | |
| 6530 | ||
| 6531 | const handler = completionHandler orelse return; | |
| 6532 | ||
| 6533 | const panel = msgSend0(getClass("NSOpenPanel"), "openPanel"); | |
| 6534 | if (panel == null) { | |
| 6535 | const BlockLayout = extern struct { | |
| 6536 | isa: ?*anyopaque, | |
| 6537 | flags: c_int, | |
| 6538 | reserved: c_int, | |
| 6539 | invoke: *const fn (*anyopaque, objc.id) callconv(.c) void, | |
| 6540 | }; | |
| 6541 | const block: *BlockLayout = @ptrCast(@alignCast(handler)); | |
| 6542 | block.invoke(handler, null); | |
| 6543 | return; | |
| 6544 | } | |
| 6545 | ||
| 6546 | _ = msgSend1Bool(panel, "setCanChooseFiles:", true); | |
| 6547 | _ = msgSend1Bool(panel, "setCanChooseDirectories:", parameters != null and msgSendBool(parameters, "allowsDirectories")); | |
| 6548 | _ = msgSend1Bool(panel, "setAllowsMultipleSelection:", parameters != null and msgSendBool(parameters, "allowsMultipleSelection")); | |
| 6549 | ||
| 6550 | if (parameters != null and msgSendBool1Sel(parameters, "respondsToSelector:", sel("allowedFileExtensions"))) { | |
| 6551 | const extensions = msgSend0(parameters, "allowedFileExtensions"); | |
| 6552 | if (extensions != null) _ = msgSend1(panel, "setAllowedFileTypes:", extensions); | |
| 6553 | } | |
| 6554 | ||
| 6555 | const response = msgSend0(panel, "runModal"); | |
| 6556 | const urls: objc.id = if (@intFromPtr(response) == 1) msgSend0(panel, "URLs") else null; | |
| 6557 | const BlockLayout = extern struct { | |
| 6558 | isa: ?*anyopaque, | |
| 6559 | flags: c_int, | |
| 6560 | reserved: c_int, | |
| 6561 | invoke: *const fn (*anyopaque, objc.id) callconv(.c) void, | |
| 6562 | }; | |
| 6563 | const block: *BlockLayout = @ptrCast(@alignCast(handler)); | |
| 6564 | block.invoke(handler, urls); | |
| 6565 | } | |
| 6566 | ||
| 6512 | 6567 | /// Attach craft's `WKUIDelegate`. |
| 6513 | 6568 | /// |
| 6514 | 6569 | /// Two callbacks, neither of which has anything to do with the other: |
| 6515 | 6570 | /// `getUserMedia()` permission, and the page asking for a second window. |
| 6571 | /// The third callback supplies the AppKit panel behind HTML file inputs. | |
| 6516 | 6572 | /// |
| 6517 | 6573 | /// Installed in every non-benchmark window. It used to be installed only under |
| 6518 | 6574 | /// `--dev-tools`, which meant a packaged app was denied the camera outright |
| @@ -6563,6 +6619,18 @@ pub fn setupUIDelegate(webview: objc.id) !void { | ||
| 6563 | 6619 | } |
| 6564 | 6620 | } |
| 6565 | 6621 | |
| 6622 | // HTML `<input type="file">`. The completion block receives an | |
| 6623 | // NSArray<NSURL *> or nil when the person cancels the panel. | |
| 6624 | if (!objc.class_addMethod( | |
| 6625 | @ptrCast(@alignCast(delegateClass)), | |
| 6626 | objc.sel_registerName("webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"), | |
| 6627 | @as(objc.IMP, @ptrCast(@constCast(&handleOpenPanel))), | |
| 6628 | "v@:@@@?", | |
| 6629 | )) { | |
| 6630 | if (comptime std.ascii.eqlIgnoreCase(@tagName(builtin.mode), "debug")) | |
| 6631 | std.debug.print("[UI] Failed to add file open panel method\n", .{}); | |
| 6632 | } | |
| 6633 | ||
| 6566 | 6634 | // `window.open()` / `target="_blank"`. Returns an object (the child |
| 6567 | 6635 | // webview, or nil): @ = id return, self, _cmd, webView, configuration, |
| 6568 | 6636 | // navigationAction, windowFeatures. |
| @@ -1003,6 +1003,24 @@ test "webview delegate ownership ends with its webview" { | ||
| 1003 | 1003 | try testing.expect(callsFunction(handler_body, "msgSendVoid0(handler, \"release\")")); |
| 1004 | 1004 | } |
| 1005 | 1005 | |
| 1006 | test "the UI delegate exposes the WebKit file-input panel" { | |
| 1007 | const setup_start = std.mem.indexOf(u8, macos_source, "pub fn setupUIDelegate(") orelse | |
| 1008 | return error.WebViewDelegateSetupNotFound; | |
| 1009 | const setup_body = enclosingFnBody(macos_source, setup_start); | |
| 1010 | try testing.expect(std.mem.indexOf(u8, setup_body, "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:") != null); | |
| 1011 | try testing.expect(std.mem.indexOf(u8, setup_body, "v@:@@@?") != null); | |
| 1012 | ||
| 1013 | const panel_start = std.mem.indexOf(u8, macos_source, "fn handleOpenPanel(") orelse | |
| 1014 | return error.OpenPanelHandlerNotFound; | |
| 1015 | const panel_body = enclosingFnBody(macos_source, panel_start); | |
| 1016 | try testing.expect(callsFunction(panel_body, "openPanel")); | |
| 1017 | try testing.expect(callsFunction(panel_body, "setAllowsMultipleSelection:")); | |
| 1018 | try testing.expect(callsFunction(panel_body, "setAllowedFileTypes:")); | |
| 1019 | try testing.expect(callsFunction(panel_body, "runModal")); | |
| 1020 | try testing.expect(std.mem.indexOf(u8, panel_body, "block.invoke(handler, urls)") != null); | |
| 1021 | try testing.expect(std.mem.indexOf(u8, panel_body, "else null") != null); | |
| 1022 | } | |
| 1023 | ||
| 1006 | 1024 | test "the scan can actually find the constructors it claims to check" { |
| 1007 | 1025 | // Guards the guard: every test above depends on `enclosingFnBody` locating |
| 1008 | 1026 | // real function bodies. If the file's formatting changes such that it |