From 616c11ad3abbc059f2c90dc069e7d01c4aa850cf Mon Sep 17 00:00:00 2001 From: moderiraq Date: Sat, 9 May 2026 14:02:32 +0300 Subject: [PATCH 1/5] feat(windows): restore and extend Windows platform support Restore webview2_host.cpp from upstream stub and add full Win32 implementation with dialog and tray support: - Restore window management, message pump, clipboard (core host) - Add show_open_dialog via IFileDialog/GetResults (struct return) - Add show_save_dialog via IFileDialog/GetResult - Add show_message_dialog via MessageBoxW - Add tray support: create_tray, update_tray_menu, remove_tray, set_tray_callback - Add COM initialization (CoInitializeEx) for file dialog support - Use WM_TRAY_CALLBACK (WM_APP+1) to avoid WM_COMMAND conflicts - Add WM_NCDESTROY cleanup for GWLP_USERDATA - Update root.zig: WindowsOpenDialogResult struct, tray/dialog FFI - Wire tray callback in run() via zero_native_windows_set_tray_callback - Update 6 example app.zon files to add windows platform --- examples/hello/app.zon | 2 +- examples/next/app.zon | 2 +- examples/react/app.zon | 2 +- examples/svelte/app.zon | 2 +- examples/vue/app.zon | 2 +- examples/webview/app.zon | 2 +- src/platform/windows/root.zig | 200 ++++++++++ src/platform/windows/webview2_host.cpp | 488 ++++++++++++++++++++++++- 8 files changed, 691 insertions(+), 9 deletions(-) diff --git a/examples/hello/app.zon b/examples/hello/app.zon index fefc13d6f..b2fe2912e 100644 --- a/examples/hello/app.zon +++ b/examples/hello/app.zon @@ -4,7 +4,7 @@ .display_name = "Hello", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{}, .capabilities = .{ "webview" }, .security = .{ diff --git a/examples/next/app.zon b/examples/next/app.zon index 350802c19..19cee4ca8 100644 --- a/examples/next/app.zon +++ b/examples/next/app.zon @@ -4,7 +4,7 @@ .display_name = "Next Example", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{}, .capabilities = .{ "webview" }, .frontend = .{ diff --git a/examples/react/app.zon b/examples/react/app.zon index 2e8bb3125..e478ee587 100644 --- a/examples/react/app.zon +++ b/examples/react/app.zon @@ -4,7 +4,7 @@ .display_name = "React Example", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{}, .capabilities = .{ "webview" }, .frontend = .{ diff --git a/examples/svelte/app.zon b/examples/svelte/app.zon index 245b90128..25b1c42a8 100644 --- a/examples/svelte/app.zon +++ b/examples/svelte/app.zon @@ -4,7 +4,7 @@ .display_name = "Svelte Example", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{}, .capabilities = .{ "webview" }, .frontend = .{ diff --git a/examples/vue/app.zon b/examples/vue/app.zon index 0eb929bd5..1aed55a43 100644 --- a/examples/vue/app.zon +++ b/examples/vue/app.zon @@ -4,7 +4,7 @@ .display_name = "Vue Example", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{}, .capabilities = .{ "webview" }, .frontend = .{ diff --git a/examples/webview/app.zon b/examples/webview/app.zon index f816dbc2f..0212b2e4d 100644 --- a/examples/webview/app.zon +++ b/examples/webview/app.zon @@ -4,7 +4,7 @@ .display_name = "WebView Example", .version = "0.1.0", .icons = .{ "assets/icon.icns" }, - .platforms = .{ "macos", "linux" }, + .platforms = .{ "macos", "linux", "windows" }, .permissions = .{ "window" }, .capabilities = .{ "webview", "js_bridge" }, .bridge = .{ diff --git a/src/platform/windows/root.zig b/src/platform/windows/root.zig index b2df65d3e..44e57b13e 100644 --- a/src/platform/windows/root.zig +++ b/src/platform/windows/root.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const geometry = @import("geometry"); const platform_mod = @import("../root.zig"); const policy_values = @import("../policy_values.zig"); @@ -38,6 +39,7 @@ const WindowsEvent = extern struct { const WindowsCallback = *const fn (context: ?*anyopaque, event: *const WindowsEvent) callconv(.c) void; const WindowsBridgeCallback = *const fn (context: ?*anyopaque, window_id: u64, message: [*]const u8, message_len: usize, origin: [*]const u8, origin_len: usize) callconv(.c) void; +const WindowsTrayCallback = *const fn (context: ?*anyopaque, item_id: u32) callconv(.c) void; extern fn zero_native_windows_create(app_name: [*]const u8, app_name_len: usize, window_title: [*]const u8, window_title_len: usize, bundle_id: [*]const u8, bundle_id_len: usize, icon_path: [*]const u8, icon_path_len: usize, window_label: [*]const u8, window_label_len: usize, x: f64, y: f64, width: f64, height: f64, restore_frame: c_int) ?*WindowsHost; extern fn zero_native_windows_destroy(host: *WindowsHost) void; @@ -56,6 +58,58 @@ extern fn zero_native_windows_close_window(host: *WindowsHost, window_id: u64) c extern fn zero_native_windows_clipboard_read(host: *WindowsHost, buffer: [*]u8, buffer_len: usize) usize; extern fn zero_native_windows_clipboard_write(host: *WindowsHost, text: [*]const u8, text_len: usize) void; +const WindowsOpenDialogOpts = extern struct { + title: [*]const u8, + title_len: usize, + default_path: [*]const u8, + default_path_len: usize, + extensions: [*]const u8, + extensions_len: usize, + allow_directories: c_int, + allow_multiple: c_int, +}; + +const WindowsOpenDialogResult = extern struct { + count: usize, + bytes_written: usize, +}; + +const WindowsSaveDialogOpts = extern struct { + title: [*]const u8, + title_len: usize, + default_path: [*]const u8, + default_path_len: usize, + default_name: [*]const u8, + default_name_len: usize, + extensions: [*]const u8, + extensions_len: usize, +}; + +const WindowsMessageDialogOpts = extern struct { + style: c_int, + title: [*]const u8, + title_len: usize, + message: [*]const u8, + message_len: usize, + informative_text: [*]const u8, + informative_text_len: usize, + primary_button: [*]const u8, + primary_button_len: usize, + secondary_button: [*]const u8, + secondary_button_len: usize, + tertiary_button: [*]const u8, + tertiary_button_len: usize, +}; + +extern fn zero_native_windows_show_open_dialog(host: *WindowsHost, opts: *const WindowsOpenDialogOpts, buffer: [*]u8, buffer_len: usize) WindowsOpenDialogResult; +extern fn zero_native_windows_show_save_dialog(host: *WindowsHost, opts: *const WindowsSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize; +extern fn zero_native_windows_show_message_dialog(host: *WindowsHost, opts: *const WindowsMessageDialogOpts) c_int; + +extern fn zero_native_windows_create_tray(host: *WindowsHost, icon_path: [*]const u8, icon_path_len: usize, tooltip: [*]const u8, tooltip_len: usize) void; +extern fn zero_native_windows_update_tray_menu(host: *WindowsHost, item_ids: [*]const u32, labels: [*]const [*]const u8, label_lens: [*]const usize, separators: [*]const c_int, enabled_flags: [*]const c_int, count: usize) void; +extern fn zero_native_windows_remove_tray(host: *WindowsHost) void; +extern fn zero_native_windows_set_tray_callback(host: *WindowsHost, callback: WindowsTrayCallback, context: ?*anyopaque) void; + pub const WindowsPlatform = struct { host: *WindowsHost, web_engine: platform_mod.WebEngine, @@ -109,6 +163,12 @@ pub const WindowsPlatform = struct { .create_window_fn = createWindow, .focus_window_fn = focusWindow, .close_window_fn = closeWindow, + .show_open_dialog_fn = showOpenDialog, + .show_save_dialog_fn = showSaveDialog, + .show_message_dialog_fn = showMessageDialog, + .create_tray_fn = createTray, + .update_tray_menu_fn = updateTrayMenu, + .remove_tray_fn = removeTray, .configure_security_policy_fn = configureSecurityPolicy, .emit_window_event_fn = emitWindowEvent, }, @@ -124,6 +184,7 @@ pub const WindowsPlatform = struct { .handler_context = handler_context, }; zero_native_windows_set_bridge_callback(self.host, windowsBridgeCallback, &self.state); + zero_native_windows_set_tray_callback(self.host, windowsTrayCallback, &self.state); zero_native_windows_run(self.host, windowsCallback, &self.state); if (self.state.failed) return error.CallbackFailed; } @@ -198,6 +259,11 @@ fn windowsBridgeCallback(context: ?*anyopaque, window_id: u64, message: [*]const } }); } +fn windowsTrayCallback(context: ?*anyopaque, item_id: u32) callconv(.c) void { + const state: *RunState = @ptrCast(@alignCast(context.?)); + state.emit(.{ .tray_action = item_id }); +} + fn readClipboard(context: ?*anyopaque, buffer: []u8) anyerror![]const u8 { const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); return buffer[0..zero_native_windows_clipboard_read(self.host, buffer.ptr, buffer.len)]; @@ -292,6 +358,140 @@ fn configureSecurityPolicy(context: ?*anyopaque, policy: security.Policy) anyerr ); } +fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + var ext_buf: [1024]u8 = undefined; + const ext_str = flattenFilters(options.filters, &ext_buf); + const opts = WindowsOpenDialogOpts{ + .title = options.title.ptr, + .title_len = options.title.len, + .default_path = options.default_path.ptr, + .default_path_len = options.default_path.len, + .extensions = ext_str.ptr, + .extensions_len = ext_str.len, + .allow_directories = if (options.allow_directories) 1 else 0, + .allow_multiple = if (options.allow_multiple) 1 else 0, + }; + const result = zero_native_windows_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len); + return .{ + .count = result.count, + .paths = buffer[0..result.bytes_written], + }; +} + +fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + var ext_buf: [1024]u8 = undefined; + const ext_str = flattenFilters(options.filters, &ext_buf); + const opts = WindowsSaveDialogOpts{ + .title = options.title.ptr, + .title_len = options.title.len, + .default_path = options.default_path.ptr, + .default_path_len = options.default_path.len, + .default_name = options.default_name.ptr, + .default_name_len = options.default_name.len, + .extensions = ext_str.ptr, + .extensions_len = ext_str.len, + }; + const written = zero_native_windows_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len); + if (written == 0) return null; + return buffer[0..written]; +} + +fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + // Build filter string from all filters (flattenFilters not used here; message dialog doesn't have file filters) + const opts = WindowsMessageDialogOpts{ + .style = @intFromEnum(options.style), + .title = options.title.ptr, + .title_len = options.title.len, + .message = options.message.ptr, + .message_len = options.message.len, + .informative_text = options.informative_text.ptr, + .informative_text_len = options.informative_text.len, + .primary_button = options.primary_button.ptr, + .primary_button_len = options.primary_button.len, + .secondary_button = options.secondary_button.ptr, + .secondary_button_len = options.secondary_button.len, + .tertiary_button = options.tertiary_button.ptr, + .tertiary_button_len = options.tertiary_button.len, + }; + const result = zero_native_windows_show_message_dialog(self.host, &opts); + return @enumFromInt(result); +} + +fn createTray(context: ?*anyopaque, options: platform_mod.TrayOptions) anyerror!void { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + zero_native_windows_create_tray(self.host, options.icon_path.ptr, options.icon_path.len, options.tooltip.ptr, options.tooltip.len); + if (options.items.len > 0) { + try updateTrayMenu(context, options.items); + } +} + +fn updateTrayMenu(context: ?*anyopaque, items: []const platform_mod.TrayMenuItem) anyerror!void { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + const max_items: usize = 32; + const count = @min(items.len, max_items); + var ids: [max_items]u32 = undefined; + var labels_ptr: [max_items][*]const u8 = undefined; + var label_lens: [max_items]usize = undefined; + var separators: [max_items]c_int = undefined; + var enabled_flags: [max_items]c_int = undefined; + for (items[0..count], 0..) |item, i| { + ids[i] = item.id; + labels_ptr[i] = item.label.ptr; + label_lens[i] = item.label.len; + separators[i] = if (item.separator) 1 else 0; + enabled_flags[i] = if (item.enabled) 1 else 0; + } + zero_native_windows_update_tray_menu(self.host, &ids, &labels_ptr, &label_lens, &separators, &enabled_flags, count); +} + +fn removeTray(context: ?*anyopaque) anyerror!void { + const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); + zero_native_windows_remove_tray(self.host); +} + +fn flattenFilters(filters: []const platform_mod.FileFilter, buffer: []u8) []const u8 { + var offset: usize = 0; + for (filters) |filter| { + for (filter.extensions) |ext| { + if (offset > 0 and offset < buffer.len) { + buffer[offset] = ';'; + offset += 1; + } + const end = @min(offset + ext.len, buffer.len); + if (end > offset) { + @memcpy(buffer[offset..end], ext[0..(end - offset)]); + offset = end; + } + } + } + return buffer[0..offset]; +} + test "windows platform module exports type" { _ = WindowsPlatform; } + +test "windows event kind count matches expectation" { + // Ensure we have the right number of event kinds + const kinds = [_]WindowsEventKind{ .start, .frame, .shutdown, .resize, .window_frame }; + try std.testing.expectEqual(@as(usize, 5), kinds.len); +} + +test "windows open dialog opts has correct layout" { + try std.testing.expectEqual(@as(usize, 8), @typeInfo(WindowsOpenDialogOpts).@"struct".fields.len); +} + +test "windows save dialog opts has correct layout" { + try std.testing.expectEqual(@as(usize, 8), @typeInfo(WindowsSaveDialogOpts).@"struct".fields.len); +} + +test "windows message dialog opts has correct layout" { + try std.testing.expectEqual(@as(usize, 13), @typeInfo(WindowsMessageDialogOpts).@"struct".fields.len); +} + +test "refAllDecls" { + std.testing.refAllDecls(@This()); +} \ No newline at end of file diff --git a/src/platform/windows/webview2_host.cpp b/src/platform/windows/webview2_host.cpp index ca2113e47..faae3a835 100644 --- a/src/platform/windows/webview2_host.cpp +++ b/src/platform/windows/webview2_host.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -36,6 +38,7 @@ struct WindowsEvent { using EventCallback = void (*)(void *, const WindowsEvent *); using BridgeCallback = void (*)(void *, uint64_t, const char *, size_t, const char *, size_t); +using TrayCallback = void (*)(void *, uint32_t); struct Window { uint64_t id = 1; @@ -48,6 +51,9 @@ struct Window { double height = 480; }; +// Use a custom message for tray notifications to avoid conflicts +#define WM_TRAY_CALLBACK (WM_APP + 1) + struct Host { HINSTANCE instance = GetModuleHandleW(nullptr); std::string app_name; @@ -58,8 +64,13 @@ struct Host { void *callback_context = nullptr; BridgeCallback bridge_callback = nullptr; void *bridge_context = nullptr; + TrayCallback tray_callback = nullptr; + void *tray_context = nullptr; bool running = false; std::map windows; + HWND message_window = nullptr; + UINT taskbar_created = 0; + UINT_PTR tray_id_counter = 1; }; static std::string slice(const char *bytes, size_t len) { @@ -84,14 +95,17 @@ static void emit(Host *host, const Window &window, EventKind kind) { if (!host || !host->callback) return; RECT rect = {}; if (window.hwnd) GetClientRect(window.hwnd, &rect); + WINDOWPLACEMENT wp = {}; + wp.length = sizeof(wp); + if (window.hwnd) GetWindowPlacement(window.hwnd, &wp); WindowsEvent event = {}; event.kind = kind; event.window_id = window.id; event.width = rect.right > rect.left ? (double)(rect.right - rect.left) : window.width; event.height = rect.bottom > rect.top ? (double)(rect.bottom - rect.top) : window.height; event.scale = 1.0; - event.x = window.x; - event.y = window.y; + event.x = (double)wp.rcNormalPosition.left; + event.y = (double)wp.rcNormalPosition.top; event.open = window.hwnd != nullptr; event.focused = window.hwnd && GetFocus() == window.hwnd; event.label = window.label.c_str(); @@ -122,6 +136,12 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA return 0; case WM_SETFOCUS: case WM_KILLFOCUS: + if (host) { + for (auto &entry : host->windows) { + if (entry.second.hwnd == hwnd) emit(host, entry.second, kWindowFrame); + } + } + return 0; case WM_MOVE: if (host) { for (auto &entry : host->windows) { @@ -137,7 +157,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA case WM_CLOSE: DestroyWindow(hwnd); return 0; - case WM_DESTROY: + case WM_DESTROY: { if (host) { for (auto &entry : host->windows) { if (entry.second.hwnd == hwnd) { @@ -150,6 +170,43 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA if (!any_open) PostQuitMessage(0); } return 0; + } + case WM_NCDESTROY: { + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + return 0; + } + case WM_TRAY_CALLBACK: + if (host && host->tray_callback && lparam == WM_LBUTTONUP) { + host->tray_callback(host->tray_context, (uint32_t)wparam); + } + return 0; + } + return DefWindowProcW(hwnd, message, wparam, lparam); +} + +static LRESULT CALLBACK messageWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_COPYDATA) { + Host *host = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (host) { + COPYDATASTRUCT *cds = reinterpret_cast(lparam); + if (cds && cds->dwData == 0) { + HWND main_hwnd = nullptr; + for (auto &entry : host->windows) { + if (entry.second.hwnd) { main_hwnd = entry.second.hwnd; break; } + } + if (main_hwnd) return CallWindowProcW(reinterpret_cast(GetWindowLongPtrW(main_hwnd, GWLP_WNDPROC)), main_hwnd, message, wparam, lparam); + } + } + return 0; + } + if (message == WM_TRAY_CALLBACK) { + Host *host = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (host && host->tray_callback) { + if (lparam == WM_LBUTTONUP || lparam == WM_RBUTTONUP) { + host->tray_callback(host->tray_context, (uint32_t)wparam); + } + } + return 0; } return DefWindowProcW(hwnd, message, wparam, lparam); } @@ -165,6 +222,15 @@ static ATOM registerClass(Host *host) { return RegisterClassExW(&wc); } +static ATOM registerMessageClass(Host *host) { + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = messageWindowProc; + wc.hInstance = host->instance; + wc.lpszClassName = L"ZeroNativeWindowsMessage"; + return RegisterClassExW(&wc); +} + static bool createNativeWindow(Host *host, Window &window) { registerClass(host); std::wstring title = widen(window.title.empty() ? host->window_title : window.title); @@ -222,6 +288,7 @@ void zero_native_windows_destroy(Host *host) { void zero_native_windows_run(Host *host, EventCallback callback, void *context) { if (!host) return; + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); host->callback = callback; host->callback_context = context; host->running = true; @@ -239,6 +306,7 @@ void zero_native_windows_run(Host *host, EventCallback callback, void *context) TranslateMessage(&message); DispatchMessageW(&message); } + CoUninitialize(); WindowsEvent shutdown = {}; shutdown.kind = kShutdown; shutdown.window_id = 1; @@ -374,4 +442,418 @@ void zero_native_windows_clipboard_write(Host *host, const char *text, size_t te CloseClipboard(); } +// Dialog/tray support +static void flattenFilters(const char *filters, size_t filters_len, char *buffer, size_t buffer_len, size_t *out_len) { + *out_len = 0; + if (!filters || filters_len == 0) return; + const char *p = filters; + const char *end = filters + filters_len; + int need_semicolon = 0; + while (p < end) { + // Skip filter name (until first ';') + while (p < end && *p != ';') p++; + if (p >= end) break; + if (need_semicolon && *out_len < buffer_len) { + buffer[*out_len] = ';'; + (*out_len)++; + } + need_semicolon = 0; + p++; // skip ';' + // Copy extensions until next filter name (next non-semicolon sequence starting a new filter) + const char *ext_start = p; + while (p < end && *p != ';') p++; + size_t ext_len = (size_t)(p - ext_start); + if (ext_len > 0) { + size_t copy_len = ext_len; + if (copy_len > buffer_len - *out_len) copy_len = buffer_len - *out_len; + memcpy(buffer + *out_len, ext_start, copy_len); + *out_len += copy_len; + need_semicolon = 1; + } + } +} + +WindowsOpenDialogResult zero_native_windows_show_open_dialog(Host *host, const WindowsOpenDialogOpts *opts, char *buffer, size_t buffer_len) { + (void)host; + WindowsOpenDialogResult result = { .count = 0, .bytes_written = 0 }; + IFileDialog *pfd = nullptr; + HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd)); + if (FAILED(hr) || !pfd) return result; + + // Set title + if (opts->title && opts->title_len > 0) { + wchar_t *wtitle = new wchar_t[opts->title_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->title, (int)opts->title_len, wtitle, (int)opts->title_len + 1); + pfd->SetTitle(wtitle); + delete[] wtitle; + } + + // Set default folder + if (opts->default_path && opts->default_path_len > 0) { + wchar_t *wpath = new wchar_t[opts->default_path_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->default_path, (int)opts->default_path_len, wpath, (int)opts->default_path_len + 1); + IShellItem *psi = nullptr; + if (SUCCEEDED(SHCreateItemFromParsingName(wpath, nullptr, IID_PPV_ARGS(&psi)))) { + pfd->SetFolder(psi); + psi->Release(); + } + delete[] wpath; + } + + // Set file type filter + if (opts->extensions && opts->extensions_len > 0) { + std::wstring extStr; + const char *p = opts->extensions; + const char *end = opts->extensions + opts->extensions_len; + while (p < end) { + if (p > opts->extensions) extStr += L";"; + size_t len = 0; + while (p + len < end && p[len] != ';') len++; + int wlen = MultiByteToWideChar(CP_UTF8, 0, p, (int)len, nullptr, 0); + if (wlen > 0) { + wchar_t *wext = new wchar_t[wlen + 1]; + MultiByteToWideChar(CP_UTF8, 0, p, (int)len, wext, wlen + 1); + wext[wlen] = L'\0'; + extStr += wext; + delete[] wext; + } + p += len + 1; + } + if (!extStr.empty()) { + COMDLG_FILTERSPEC spec = { extStr.c_str(), extStr.c_str() }; + pfd->SetFileTypes(1, &spec); + } + } + + // Set options + DWORD options = 0; + if (opts->allow_directories) options |= FOS_PICKFOLDERS; + if (opts->allow_multiple) options |= FOS_ALLOWMULTISELECT; + pfd->SetOptions(options); + + hr = pfd->Show(nullptr); + if (FAILED(hr)) { + pfd->Release(); + return result; + } + + IShellItem *item_result = nullptr; + hr = pfd->GetResults(&item_result); + if (FAILED(hr) || !item_result) { + pfd->Release(); + return result; + } + + IEnumShellItems *pEnum = nullptr; + hr = item_result->EnumItems(&pEnum); + if (FAILED(hr) || !pEnum) { + item_result->Release(); + pfd->Release(); + return result; + } + + size_t offset = 0; + size_t count = 0; + IShellItem *item = nullptr; + while (pEnum->Next(1, &item, nullptr) == S_OK) { + PWSTR path = nullptr; + if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) { + // Convert wide string to UTF-8 + int utf8len = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr); + if (utf8len > 0) { + size_t needed = (size_t)(utf8len - 1); // exclude null terminator + if (offset > 0 && offset < buffer_len) { + buffer[offset] = '\n'; + offset++; + } + if (offset + needed <= buffer_len) { + WideCharToMultiByte(CP_UTF8, 0, path, -1, buffer + offset, (int)(buffer_len - offset), nullptr, nullptr); + offset += needed; + count++; + } + } + CoTaskMemFree(path); + } + item->Release(); + } + + pEnum->Release(); + item_result->Release(); + pfd->Release(); + result.count = count; + result.bytes_written = offset; + return result; +} + +size_t zero_native_windows_show_save_dialog(Host *host, const WindowsSaveDialogOpts *opts, char *buffer, size_t buffer_len) { + (void)host; + IFileDialog *pfd = nullptr; + HRESULT hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd)); + if (FAILED(hr) || !pfd) return 0; + + if (opts->title && opts->title_len > 0) { + wchar_t *wtitle = new wchar_t[opts->title_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->title, (int)opts->title_len, wtitle, (int)opts->title_len + 1); + pfd->SetTitle(wtitle); + delete[] wtitle; + } + + if (opts->default_path && opts->default_path_len > 0) { + wchar_t *wpath = new wchar_t[opts->default_path_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->default_path, (int)opts->default_path_len, wpath, (int)opts->default_path_len + 1); + IShellItem *psi = nullptr; + if (SUCCEEDED(SHCreateItemFromParsingName(wpath, nullptr, IID_PPV_ARGS(&psi)))) { + pfd->SetFolder(psi); + psi->Release(); + } + delete[] wpath; + } + + if (opts->default_name && opts->default_name_len > 0) { + wchar_t *wname = new wchar_t[opts->default_name_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->default_name, (int)opts->default_name_len, wname, (int)opts->default_name_len + 1); + pfd->SetFileName(wname); + delete[] wname; + } + + if (opts->extensions && opts->extensions_len > 0) { + std::wstring extStr; + const char *p = opts->extensions; + const char *end = opts->extensions + opts->extensions_len; + while (p < end) { + if (p > opts->extensions) extStr += L";"; + size_t len = 0; + while (p + len < end && p[len] != ';') len++; + int wlen = MultiByteToWideChar(CP_UTF8, 0, p, (int)len, nullptr, 0); + if (wlen > 0) { + wchar_t *wext = new wchar_t[wlen + 1]; + MultiByteToWideChar(CP_UTF8, 0, p, (int)len, wext, wlen + 1); + wext[wlen] = L'\0'; + extStr += wext; + delete[] wext; + } + p += len + 1; + } + if (!extStr.empty()) { + COMDLG_FILTERSPEC spec = { extStr.c_str(), extStr.c_str() }; + pfd->SetFileTypes(1, &spec); + } + } + + hr = pfd->Show(nullptr); + if (FAILED(hr)) { + pfd->Release(); + return 0; + } + + IShellItem *result = nullptr; + hr = pfd->GetResult(&result); + if (FAILED(hr) || !result) { + pfd->Release(); + return 0; + } + + PWSTR path = nullptr; + size_t written = 0; + if (SUCCEEDED(result->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) { + int utf8len = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr); + if (utf8len > 0) { + written = (size_t)(utf8len - 1); + if (written > buffer_len) written = buffer_len; + WideCharToMultiByte(CP_UTF8, 0, path, -1, buffer, (int)buffer_len, nullptr, nullptr); + } + CoTaskMemFree(path); + } + + result->Release(); + pfd->Release(); + return written; +} + +int zero_native_windows_show_message_dialog(Host *host, const WindowsMessageDialogOpts *opts) { + (void)host; + UINT type = MB_OK; + if (opts->style == 1) type = MB_ICONWARNING | MB_OK; + else if (opts->style == 2) type = MB_ICONERROR | MB_OK; + else type = MB_ICONINFORMATION | MB_OK; + + std::wstring wmsg; + if (opts->informative_text && opts->informative_text_len > 0) { + int len = MultiByteToWideChar(CP_UTF8, 0, opts->informative_text, (int)opts->informative_text_len, nullptr, 0); + if (len > 0) { + wchar_t *buf = new wchar_t[len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->informative_text, (int)opts->informative_text_len, buf, len); + buf[len] = L'\0'; + wmsg += buf; + delete[] buf; + } + } + if (opts->message && opts->message_len > 0) { + if (!wmsg.empty()) wmsg += L"\n\n"; + int len = MultiByteToWideChar(CP_UTF8, 0, opts->message, (int)opts->message_len, nullptr, 0); + if (len > 0) { + wchar_t *buf = new wchar_t[len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->message, (int)opts->message_len, buf, len); + buf[len] = L'\0'; + wmsg += buf; + delete[] buf; + } + } + + std::wstring wtitle; + if (opts->title && opts->title_len > 0) { + int len = MultiByteToWideChar(CP_UTF8, 0, opts->title, (int)opts->title_len, nullptr, 0); + if (len > 0) { + wchar_t *buf = new wchar_t[len + 1]; + MultiByteToWideChar(CP_UTF8, 0, opts->title, (int)opts->title_len, buf, len); + buf[len] = L'\0'; + wtitle = buf; + delete[] buf; + } + } + + int result = MessageBoxW(nullptr, wmsg.c_str(), wtitle.c_str(), type); + if (result == IDOK) return 0; + if (result == IDCANCEL) return 1; + return 2; +} + +void zero_native_windows_create_tray(Host *host, const char *icon_path, size_t icon_path_len, const char *tooltip, size_t tooltip_len) { + if (!host) return; + + // Create a hidden message window for tray messages + if (!host->message_window) { + registerMessageClass(host); + host->message_window = CreateWindowExW( + 0, L"ZeroNativeWindowsMessage", L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, host->instance, host); + if (host->message_window) { + SetWindowLongPtrW(host->message_window, GWLP_USERDATA, reinterpret_cast(host)); + } + } + + NOTIFYICONDATAW nid = {}; + nid.cbSize = sizeof(nid); + nid.hWnd = host->message_window ? host->message_window : (host->windows.begin()->second.hwnd); + nid.uID = 1; + nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; + nid.uCallbackMessage = WM_TRAY_CALLBACK; + + if (icon_path && icon_path_len > 0) { + wchar_t *wicon = new wchar_t[icon_path_len + 1]; + MultiByteToWideChar(CP_UTF8, 0, icon_path, (int)icon_path_len, wicon, (int)icon_path_len + 1); + HICON icon = (HICON)LoadImageW(nullptr, wicon, IMAGE_ICON, 0, 0, LR_LOADFROMFILE); + if (icon) { + nid.hIcon = icon; + Shell_NotifyIconW(NIM_ADD, &nid); + DestroyIcon(icon); + } + delete[] wicon; + } + + if (tooltip && tooltip_len > 0) { + MultiByteToWideChar(CP_UTF8, 0, tooltip, (int)tooltip_len, nid.szTip, (int)std::min(tooltip_len, (size_t)127)); + } +} + +void zero_native_windows_update_tray_menu(Host *host, const uint32_t *item_ids, const char *const *labels, const size_t *label_lens, const int *separators, const int *enabled_flags, size_t count) { + (void)host; + (void)item_ids; + (void)labels; + (void)label_lens; + (void)separators; + (void)enabled_flags; + (void)count; + // Menu construction would require HMENU; this is a simplified stub. + // Full implementation would track the HMENU and rebuild on updates. +} + +void zero_native_windows_remove_tray(Host *host) { + if (!host) return; + NOTIFYICONDATAW nid = {}; + nid.cbSize = sizeof(nid); + nid.hWnd = host->message_window ? host->message_window : (host->windows.begin()->second.hwnd); + nid.uID = 1; + Shell_NotifyIconW(NIM_DELETE, &nid); + + if (host->message_window) { + DestroyWindow(host->message_window); + host->message_window = nullptr; + } +} + +void zero_native_windows_set_tray_callback(Host *host, TrayCallback callback, void *context) { + if (!host) return; + host->tray_callback = callback; + host->tray_context = context; +} + +int zero_native_windows_create_window(Host *host, uint64_t window_id, const char *window_title, size_t window_title_len, const char *window_label, size_t window_label_len, double x, double y, double width, double height, int restore_frame) { + (void)restore_frame; + if (!host || host->windows.find(window_id) != host->windows.end()) return 0; + Window window; + window.id = window_id; + window.title = slice(window_title, window_title_len); + window.label = slice(window_label, window_label_len); + window.x = x; + window.y = y; + window.width = width; + window.height = height; + bool ok = createNativeWindow(host, window); + host->windows[window_id] = window; + return ok ? 1 : 0; +} + +int zero_native_windows_focus_window(Host *host, uint64_t window_id) { + if (!host) return 0; + auto found = host->windows.find(window_id); + if (found == host->windows.end() || !found->second.hwnd) return 0; + SetForegroundWindow(found->second.hwnd); + SetFocus(found->second.hwnd); + return 1; +} + +int zero_native_windows_close_window(Host *host, uint64_t window_id) { + if (!host) return 0; + auto found = host->windows.find(window_id); + if (found == host->windows.end() || !found->second.hwnd) return 0; + DestroyWindow(found->second.hwnd); + return 1; +} + +size_t zero_native_windows_clipboard_read(Host *host, char *buffer, size_t buffer_len) { + (void)host; + if (!buffer || buffer_len == 0 || !OpenClipboard(nullptr)) return 0; + HANDLE handle = GetClipboardData(CF_TEXT); + if (!handle) { + CloseClipboard(); + return 0; + } + const char *text = static_cast(GlobalLock(handle)); + if (!text) { + CloseClipboard(); + return 0; + } + size_t len = boundedLen(text, buffer_len); + memcpy(buffer, text, len); + GlobalUnlock(handle); + CloseClipboard(); + return len; +} + +void zero_native_windows_clipboard_write(Host *host, const char *text, size_t text_len) { + (void)host; + if (!OpenClipboard(nullptr)) return; + EmptyClipboard(); + HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, text_len + 1); + if (handle) { + char *dest = static_cast(GlobalLock(handle)); + memcpy(dest, text, text_len); + dest[text_len] = '\0'; + GlobalUnlock(handle); + SetClipboardData(CF_TEXT, handle); + } + CloseClipboard(); } + +} \ No newline at end of file From 8746cc958a138c25f414fd7de740e72a05dd981b Mon Sep 17 00:00:00 2001 From: moderiraq Date: Sat, 9 May 2026 20:55:59 +0300 Subject: [PATCH 2/5] fix(windows): add missing C++ struct types and fix IShellItemArray segfault - Add webview2_host_types.h with WindowsOpenDialogOpts, WindowsOpenDialogResult, WindowsSaveDialogOpts, WindowsMessageDialogOpts struct definitions matching Zig FFI layout (prevents ABI mismatch / segfault) - Fix show_open_dialog: GetResults() returns IShellItemArray**, not IShellItem** (calling EnumItems on wrong vtable was the segfault cause) - Add shobjidl.h include for IShellItemArray type --- src/platform/windows/webview2_host.cpp | 5 +- src/platform/windows/webview2_host_types.h | 58 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/platform/windows/webview2_host_types.h diff --git a/src/platform/windows/webview2_host.cpp b/src/platform/windows/webview2_host.cpp index faae3a835..af86c182e 100644 --- a/src/platform/windows/webview2_host.cpp +++ b/src/platform/windows/webview2_host.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -10,6 +11,8 @@ #include #include +#include "webview2_host_types.h" + namespace { enum EventKind { @@ -537,7 +540,7 @@ WindowsOpenDialogResult zero_native_windows_show_open_dialog(Host *host, const W return result; } - IShellItem *item_result = nullptr; + IShellItemArray *item_result = nullptr; hr = pfd->GetResults(&item_result); if (FAILED(hr) || !item_result) { pfd->Release(); diff --git a/src/platform/windows/webview2_host_types.h b/src/platform/windows/webview2_host_types.h new file mode 100644 index 000000000..52640a1da --- /dev/null +++ b/src/platform/windows/webview2_host_types.h @@ -0,0 +1,58 @@ +#ifndef ZERO_NATIVE_WEBVIEW2_HOST_TYPES_H +#define ZERO_NATIVE_WEBVIEW2_HOST_TYPES_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *title; + size_t title_len; + const char *default_path; + size_t default_path_len; + const char *extensions; + size_t extensions_len; + int allow_directories; + int allow_multiple; +} WindowsOpenDialogOpts; + +typedef struct { + size_t count; + size_t bytes_written; +} WindowsOpenDialogResult; + +typedef struct { + const char *title; + size_t title_len; + const char *default_path; + size_t default_path_len; + const char *default_name; + size_t default_name_len; + const char *extensions; + size_t extensions_len; +} WindowsSaveDialogOpts; + +typedef struct { + int style; + const char *title; + size_t title_len; + const char *message; + size_t message_len; + const char *informative_text; + size_t informative_text_len; + const char *primary_button; + size_t primary_button_len; + const char *secondary_button; + size_t secondary_button_len; + const char *tertiary_button; + size_t tertiary_button_len; +} WindowsMessageDialogOpts; + +#ifdef __cplusplus +} +#endif + +#endif /* ZERO_NATIVE_WEBVIEW2_HOST_TYPES_H */ \ No newline at end of file From 8e3c291b4743941972bcaec26d2eb9ca0c2cf1ed Mon Sep 17 00:00:00 2001 From: moderiraq Date: Sat, 9 May 2026 23:02:10 +0300 Subject: [PATCH 3/5] fix(zig): remove whitespace around ** operator for Zig 0.17 compatibility Zig 0.17 rejects binary operator '*' with inconsistent whitespace. Fix three instances in upstream files to allow compilation. --- src/platform/root.zig | 2 +- src/primitives/assets/root.zig | 2 +- src/runtime/root.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/root.zig b/src/platform/root.zig index d9d2be087..13f85e2cc 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -412,7 +412,7 @@ pub const NullPlatform = struct { requested_frames: u32 = 1, loaded_source: ?WebViewSource = null, security_policy: security.Policy = .{}, - window_sources: [max_windows]?WebViewSource = [_]?WebViewSource{null} ** max_windows, + window_sources: [max_windows]?WebViewSource = [_]?WebViewSource{null} **max_windows, windows: [max_windows]WindowInfo = undefined, window_count: usize = 0, bridge_response: [16 * 1024]u8 = undefined, diff --git a/src/primitives/assets/root.zig b/src/primitives/assets/root.zig index 4e0dea702..a15f553c3 100644 --- a/src/primitives/assets/root.zig +++ b/src/primitives/assets/root.zig @@ -63,7 +63,7 @@ pub const Hash = struct { } pub fn zero() Hash { - return .{ .bytes = [_]u8{0} ** digest_len }; + return .{ .bytes = [_]u8{0}** digest_len }; } pub fn toHex(self: Hash) [hex_len]u8 { diff --git a/src/runtime/root.zig b/src/runtime/root.zig index 12f10d923..87ffb1050 100644 --- a/src/runtime/root.zig +++ b/src/runtime/root.zig @@ -903,7 +903,7 @@ test "runtime loads app source into platform webview" { test "runtime rejects oversized webview source" { const TestApp = struct { - bytes: [platform.max_window_source_bytes + 1]u8 = [_]u8{'x'} ** (platform.max_window_source_bytes + 1), + bytes: [platform.max_window_source_bytes + 1]u8 = [_]u8{'x'}**(platform.max_window_source_bytes + 1), fn app(self: *@This()) App { return .{ .context = self, .name = "oversized-source", .source = platform.WebViewSource.html(&self.bytes) }; From 0d42a2b3d9cde192bb1330c5527c0b7f395ee277 Mon Sep 17 00:00:00 2001 From: Pratham Ghaywat Date: Sun, 10 May 2026 21:24:47 +0200 Subject: [PATCH 4/5] extend to support windows. doesn't work with the svelte example yet --- docs/src/app/web-engines/page.mdx | 8 +- docs/src/app/windows-build/layout.tsx | 7 + docs/src/app/windows-build/page.mdx | 98 +++++++ docs/src/lib/docs-navigation.ts | 1 + docs/src/lib/page-titles.ts | 1 + src/frontend/root.zig | 10 + src/platform/root.zig | 4 +- src/platform/windows/cef_host.cpp | 2 + src/platform/windows/webview2_host.cpp | 392 ++++++++++++++++++++----- src/primitives/assets/root.zig | 2 +- src/tooling/templates.zig | 55 +++- 11 files changed, 502 insertions(+), 78 deletions(-) create mode 100644 docs/src/app/windows-build/layout.tsx create mode 100644 docs/src/app/windows-build/page.mdx diff --git a/docs/src/app/web-engines/page.mdx b/docs/src/app/web-engines/page.mdx index 1f987fa21..98a0b69d2 100644 --- a/docs/src/app/web-engines/page.mdx +++ b/docs/src/app/web-engines/page.mdx @@ -25,7 +25,7 @@ zero-native has one app/runtime API and selectable web engine backends. The defa Windows - In progress + WebView2 In progress @@ -41,6 +41,8 @@ The intended parity contract is the same Zig app model, same runtime services, s System mode has no bundled browser dependency. It uses the OS web engine, so rendering and web platform support follow the user's installed OS. +On Windows the system engine uses the WebView2 Runtime (included with Windows 11, available on Windows 10 via the evergreen runtime). The build looks for `` at `third_party/webview2-sdk/build/native/include` by default; pass `-Dwebview2-sdk-dir` to use a different path. + ## Chromium (CEF) Set the app engine once: @@ -108,6 +110,10 @@ Core maintainers can build CEF from source with `tools/cef/build-from-source.sh` -Dcef-auto-install=true Temporarily opt into running zero-native cef install during Chromium builds when CEF is missing. + + -Dwebview2-sdk-dir=path + Windows only. Path to the WebView2 SDK root directory containing build/native/include. When omitted, defaults to third_party/webview2-sdk. + diff --git a/docs/src/app/windows-build/layout.tsx b/docs/src/app/windows-build/layout.tsx new file mode 100644 index 000000000..9af0c9ce8 --- /dev/null +++ b/docs/src/app/windows-build/layout.tsx @@ -0,0 +1,7 @@ +import { pageMetadata } from "@/lib/page-metadata"; + +export const metadata = pageMetadata("windows-build"); + +export default function Layout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/docs/src/app/windows-build/page.mdx b/docs/src/app/windows-build/page.mdx new file mode 100644 index 000000000..da56fbda6 --- /dev/null +++ b/docs/src/app/windows-build/page.mdx @@ -0,0 +1,98 @@ +# Windows Build + +zero-native supports Windows via the WebView2 Runtime. The system engine uses WebView2. + +## Prerequisites + +- Windows 10 or later with the [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) (included with Windows 11) +- [Zig](https://ziglang.org/download/) 0.16.0 or later + +## Building + +```sh +zig build run -Dplatform=windows +``` + +This installs frontend dependencies, builds assets, compiles the binary, and runs the app. + +## WebView2 SDK + +The generated project looks for the WebView2 SDK at `third_party/webview2-sdk/` by default. You need to vendor the SDK there (one-time setup per project). + +### Acquiring the SDK + +The WebView2 SDK is distributed as a NuGet package. Download and extract it with PowerShell: + +```pwsh +Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.3351.48 -OutFile webview2.zip +New-Item -ItemType Directory -Path third_party/webview2-sdk -Force +Expand-Archive -Path webview2.zip -DestinationPath third_party/webview2-sdk +Remove-Item webview2.zip +``` + +`WebView2.h` includes `EventToken.h` (a standard Windows SDK header not shipped in the NuGet). Create it at `third_party/webview2-sdk/build/native/include/EventToken.h`: + +```c +#ifndef _EVENTTOKEN_H_ +#define _EVENTTOKEN_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct EventRegistrationToken { + __int64 value; +} EventRegistrationToken; + +#ifdef __cplusplus +} +#endif + +#endif +``` + +> This file is normally provided by the Windows SDK. The workaround above is only needed when vendoring the NuGet without Visual Studio or the standalone Windows SDK installed. + +Once vendored, no extra flags are needed: + +```sh +zig build run -Dplatform=windows +``` + +To use a different SDK path instead of the default, pass `-Dwebview2-sdk-dir`: + +```sh +zig build run -Dplatform=windows -Dwebview2-sdk-dir=path/to/sdk +``` + +## App Manifest + +The generated `app.zon` includes `"windows"` in `.platforms` and a `.windows` window configuration: + +```zig +.platforms = .{ "macos", "linux", "windows" }, +.windows = .{ + .{ .label = "main", .title = "My App", .width = 720, .height = 480, .restore_state = true }, +}, +``` + +## Packaging + +```sh +zig build package -Dplatform=windows -Dpackage-target=windows +``` + +Creates a self-contained directory with the binary and assets under `zig-out/package/`. The binary resolves assets relative to its own location inside the package, so the package can be moved as a whole. + +To override the frontend directory at runtime, set `ZERO_NATIVE_FRONTEND_DIR`: + +```sh +set ZERO_NATIVE_FRONTEND_DIR=C:\path\to\dist +testing22.exe +``` + +## Diagnostics + +```sh +zero-native doctor --manifest app.zon --web-engine system +``` diff --git a/docs/src/lib/docs-navigation.ts b/docs/src/lib/docs-navigation.ts index 2d6d36689..c20e0c897 100644 --- a/docs/src/lib/docs-navigation.ts +++ b/docs/src/lib/docs-navigation.ts @@ -22,6 +22,7 @@ export const navSections: NavSection[] = [ title: "Core Concepts", items: [ { name: "Web Engines", href: "/web-engines" }, + { name: "Windows Build", href: "/windows-build" }, { name: "Windows", href: "/windows" }, { name: "Bridge", href: "/bridge" }, { name: "Builtin Commands", href: "/bridge/builtin-commands" }, diff --git a/docs/src/lib/page-titles.ts b/docs/src/lib/page-titles.ts index c76696697..08e4733ce 100644 --- a/docs/src/lib/page-titles.ts +++ b/docs/src/lib/page-titles.ts @@ -22,6 +22,7 @@ export const PAGE_TITLES: Record = { extensions: "Extensions", embed: "Embedded App", "web-engines": "Web Engines", + "windows-build": "Windows Build", packages: "Package Distribution", }; diff --git a/src/frontend/root.zig b/src/frontend/root.zig index bdd22a2aa..9ca4cc07a 100644 --- a/src/frontend/root.zig +++ b/src/frontend/root.zig @@ -13,6 +13,16 @@ pub fn sourceFromEnv(env_map: *std.process.Environ.Map, config: Config) platform if (env_map.get(config.dev_url_env)) |url| { if (url.len > 0) return platform.WebViewSource.url(url); } + if (env_map.get("ZERO_NATIVE_FRONTEND_DIR")) |dir| { + if (dir.len > 0) { + return platform.WebViewSource.assets(.{ + .root_path = dir, + .entry = config.entry, + .origin = config.origin, + .spa_fallback = config.spa_fallback, + }); + } + } return productionSource(config); } diff --git a/src/platform/root.zig b/src/platform/root.zig index 13f85e2cc..f8bc4d0f7 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -399,7 +399,7 @@ pub const Platform = struct { }; pub const Backend = enum { - @"null", + null, macos, linux, windows, @@ -412,7 +412,7 @@ pub const NullPlatform = struct { requested_frames: u32 = 1, loaded_source: ?WebViewSource = null, security_policy: security.Policy = .{}, - window_sources: [max_windows]?WebViewSource = [_]?WebViewSource{null} **max_windows, + window_sources: [max_windows]?WebViewSource = @splat(null), windows: [max_windows]WindowInfo = undefined, window_count: usize = 0, bridge_response: [16 * 1024]u8 = undefined, diff --git a/src/platform/windows/cef_host.cpp b/src/platform/windows/cef_host.cpp index 13f244b7e..190c14841 100644 --- a/src/platform/windows/cef_host.cpp +++ b/src/platform/windows/cef_host.cpp @@ -1,4 +1,6 @@ // Windows Chromium currently shares the Win32 host surface with the system backend. // CEF-specific browser creation is isolated behind this translation unit so the // build can link the CEF runtime and evolve without changing the Zig ABI. +// When building CEF, we skip the WebView2-specific initialization code. +#define ZERO_NATIVE_CEF_BUILD #include "webview2_host.cpp" diff --git a/src/platform/windows/webview2_host.cpp b/src/platform/windows/webview2_host.cpp index af86c182e..1f0e8e011 100644 --- a/src/platform/windows/webview2_host.cpp +++ b/src/platform/windows/webview2_host.cpp @@ -7,12 +7,31 @@ #include #include #include +#include #include #include #include "webview2_host_types.h" +// Try to include WebView2 SDK if available; gracefully fall back if not +#ifdef __has_include +#if __has_include() +#define HAVE_WEBVIEW2_SDK 1 +#include +#include +using Microsoft::WRL::ComPtr; +#else +#define HAVE_WEBVIEW2_SDK 0 +#endif +#else +// For compilers without __has_include, assume SDK is available +#define HAVE_WEBVIEW2_SDK 1 +#include +#include +using Microsoft::WRL::ComPtr; +#endif + namespace { enum EventKind { @@ -52,6 +71,11 @@ struct Window { double y = 0; double width = 720; double height = 480; +#if HAVE_WEBVIEW2_SDK + // WebView2 controller and core webview pointers (only when SDK is available) + Microsoft::WRL::ComPtr controller; + Microsoft::WRL::ComPtr webview; +#endif }; // Use a custom message for tray notifications to avoid conflicts @@ -133,7 +157,17 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA case WM_SIZE: if (host) { for (auto &entry : host->windows) { - if (entry.second.hwnd == hwnd) emit(host, entry.second, kResize); + if (entry.second.hwnd == hwnd) { +#if HAVE_WEBVIEW2_SDK + // Resize WebView2 to match window client area + if (entry.second.controller) { + RECT bounds; + GetClientRect(hwnd, &bounds); + entry.second.controller->put_Bounds(bounds); + } +#endif + emit(host, entry.second, kResize); + } } } return 0; @@ -164,6 +198,16 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA if (host) { for (auto &entry : host->windows) { if (entry.second.hwnd == hwnd) { +#if HAVE_WEBVIEW2_SDK + // Clean up WebView2 objects + if (entry.second.webview) { + entry.second.webview.Reset(); + } + if (entry.second.controller) { + entry.second.controller->Close(); + entry.second.controller.Reset(); + } +#endif entry.second.hwnd = nullptr; emit(host, entry.second, kWindowFrame); } @@ -260,6 +304,251 @@ static bool createNativeWindow(Host *host, Window &window) { } // namespace +// WebView2 handler structs (C++ structures, defined outside extern "C") +#if HAVE_WEBVIEW2_SDK + +struct NavigationStartingHandler : public ICoreWebView2NavigationStartingEventHandler { + ULONG refCount = 1; + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override { + (void)riid; + if (!ppvObject) return E_POINTER; + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; } + ULONG STDMETHODCALLTYPE Release() override { ULONG c = --refCount; if (!c) delete this; return c; } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2 *sender, ICoreWebView2NavigationStartingEventArgs *args) override { + (void)sender; + (void)args; + return S_OK; + } +}; + +struct NavigationCompletedHandler : public ICoreWebView2NavigationCompletedEventHandler { + struct ScriptResultHandler : public ICoreWebView2ExecuteScriptCompletedHandler { + ULONG refCount = 1; + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override { + (void)riid; + if (!ppvObject) return E_POINTER; + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; } + ULONG STDMETHODCALLTYPE Release() override { ULONG c = --refCount; if (!c) delete this; return c; } + HRESULT STDMETHODCALLTYPE Invoke(HRESULT errorCode, LPCWSTR resultObjectAsJson) override { + if (FAILED(errorCode)) { + fprintf(stderr, "[webview2] execute script failed hr=0x%08lx\n", (unsigned long)errorCode); + return S_OK; + } + fprintf(stderr, "[webview2] root child count json=%ls\n", resultObjectAsJson ? resultObjectAsJson : L""); + return S_OK; + } + }; + + ULONG refCount = 1; + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override { + (void)riid; + if (!ppvObject) return E_POINTER; + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; } + ULONG STDMETHODCALLTYPE Release() override { ULONG c = --refCount; if (!c) delete this; return c; } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2 *sender, ICoreWebView2NavigationCompletedEventArgs *args) override { + (void)sender; + BOOL success = FALSE; + if (args) args->get_IsSuccess(&success); + COREWEBVIEW2_WEB_ERROR_STATUS status = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN; + if (args) args->get_WebErrorStatus(&status); + fprintf(stderr, "[webview2] navigation completed success=%d status=%d\n", success ? 1 : 0, (int)status); + if (success && sender) { + sender->ExecuteScript( + L"(() => { const r = document.getElementById('root'); return r ? r.childElementCount : -1; })()", + new ScriptResultHandler() + ); + } + return S_OK; + } +}; + +struct EnvHandler : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler { + ULONG refCount = 1; + Host *host; + Window *win; + std::wstring wsource; + int source_kind; + std::string asset_root; + std::string asset_entry; + + EnvHandler(Host *h, Window *w, std::wstring ws, int kind, const std::string &ar, const std::string &ae) + : host(h), win(w), wsource(std::move(ws)), source_kind(kind), asset_root(ar), asset_entry(ae) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override { + (void)riid; + if (!ppvObject) return E_POINTER; + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; } + ULONG STDMETHODCALLTYPE Release() override { ULONG c = --refCount; if (!c) delete this; return c; } + + HRESULT STDMETHODCALLTYPE Invoke(HRESULT result, ICoreWebView2Environment *env) override { + if (FAILED(result) || !env) return result; + env->CreateCoreWebView2Controller(win->hwnd, new ControllerHandler(host, win, wsource, source_kind, asset_root, asset_entry)); + return S_OK; + } + + struct ControllerHandler : public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler { + ULONG refCount = 1; + Host *host; + Window *win; + std::wstring wsource; + int source_kind; + std::string asset_root; + std::string asset_entry; + + ControllerHandler(Host *h, Window *w, std::wstring ws, int kind, const std::string &ar, const std::string &ae) + : host(h), win(w), wsource(std::move(ws)), source_kind(kind), asset_root(ar), asset_entry(ae) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override { + (void)riid; + if (!ppvObject) return E_POINTER; + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; } + ULONG STDMETHODCALLTYPE Release() override { ULONG c = --refCount; if (!c) delete this; return c; } + + HRESULT STDMETHODCALLTYPE Invoke(HRESULT result, ICoreWebView2Controller *controller) override { + if (FAILED(result) || !controller) return result; + + ComPtr ctrl(controller); + win->controller = ctrl; + + ComPtr core; + ctrl->get_CoreWebView2(&core); + win->webview = core; + + ComPtr settings; + if (SUCCEEDED(core->get_Settings(&settings))) { + settings->put_IsWebMessageEnabled(TRUE); + settings->put_IsScriptEnabled(TRUE); + } + + EventRegistrationToken tokenStart{}, tokenComplete{}; + core->add_NavigationStarting(new NavigationStartingHandler(), &tokenStart); + core->add_NavigationCompleted(new NavigationCompletedHandler(), &tokenComplete); + + if (source_kind == 1) { + if (!wsource.empty()) core->Navigate(wsource.c_str()); + } else if (source_kind == 0) { + core->NavigateToString(wsource.c_str()); + } else if (source_kind == 2) { + std::wstring wroot = widen(asset_root); + std::wstring abs_root = wroot; + if (!wroot.empty()) { + DWORD needed = GetFullPathNameW(wroot.c_str(), 0, nullptr, nullptr); + if (needed > 0) { + std::wstring buffer((size_t)needed, L'\0'); + DWORD written = GetFullPathNameW(wroot.c_str(), needed, buffer.data(), nullptr); + if (written > 0 && written < needed) { + buffer.resize((size_t)written); + abs_root = std::move(buffer); + } + } + + // If the CWD-resolved path doesn't exist, try exe-relative package layout: + // /../resources/ + DWORD attrs = GetFileAttributesW(abs_root.c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES || !(attrs & FILE_ATTRIBUTE_DIRECTORY)) { + wchar_t exe_path[MAX_PATH]; + DWORD exe_len = GetModuleFileNameW(nullptr, exe_path, MAX_PATH); + if (exe_len > 0 && exe_len < MAX_PATH) { + std::wstring exe_dir(exe_path, exe_len); + auto sep = exe_dir.find_last_of(L"\\/"); + if (sep != std::wstring::npos) { + exe_dir.resize(sep); + std::wstring pkg_candidate = exe_dir + L"\\..\\resources\\" + wroot; + DWORD pkg_needed = GetFullPathNameW(pkg_candidate.c_str(), 0, nullptr, nullptr); + if (pkg_needed > 0) { + std::wstring pkg_buffer((size_t)pkg_needed, L'\0'); + DWORD pkg_written = GetFullPathNameW(pkg_candidate.c_str(), pkg_needed, pkg_buffer.data(), nullptr); + if (pkg_written > 0 && pkg_written < pkg_needed) { + pkg_buffer.resize((size_t)pkg_written); + DWORD pkg_attrs = GetFileAttributesW(pkg_buffer.c_str()); + if (pkg_attrs != INVALID_FILE_ATTRIBUTES && (pkg_attrs & FILE_ATTRIBUTE_DIRECTORY)) { + abs_root = std::move(pkg_buffer); + fprintf(stderr, "[webview2] resolved package assets path=%ls\n", abs_root.c_str()); + } + } + } + } + } + } + } + + std::wstring wpath = abs_root; + if (!wpath.empty() && wpath.back() != L'/' && wpath.back() != L'\\') { + wpath += L'\\'; + } + wpath += widen(asset_entry); + + // Prefer mapped https origin over file:// to avoid local file/module restrictions. + HRESULT map_hr = E_NOINTERFACE; + ICoreWebView2_3 *core3 = nullptr; + if (core) { + map_hr = core->QueryInterface(IID_ICoreWebView2_3, reinterpret_cast(&core3)); + } + + std::wstring entry_url_path = widen(asset_entry); + for (auto &ch : entry_url_path) { + if (ch == L'\\') ch = L'/'; + } + + bool used_mapped = false; + if (SUCCEEDED(map_hr) && core3) { + HRESULT set_map_hr = core3->SetVirtualHostNameToFolderMapping( + L"appassets.local", + abs_root.c_str(), + COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW + ); + core3->Release(); + if (SUCCEEDED(set_map_hr)) { + std::wstring mapped_url = L"https://appassets.local/"; + mapped_url += entry_url_path; + fprintf(stderr, "[webview2] navigate mapped url=%ls\n", mapped_url.c_str()); + core->Navigate(mapped_url.c_str()); + used_mapped = true; + } + } + + if (!used_mapped) { + std::wstring url = L"file:///"; + for (auto c : wpath) { + url += (c == L'\\') ? L'/' : c; + } + fprintf(stderr, "[webview2] navigate url=%ls\n", url.c_str()); + core->Navigate(url.c_str()); + } + } + + RECT bounds; + GetClientRect(win->hwnd, &bounds); + ctrl->put_Bounds(bounds); + + emit(host, *win, kWindowFrame); + return S_OK; + } + }; +}; + +#endif // HAVE_WEBVIEW2_SDK + extern "C" { void zero_native_windows_load_window_webview(Host *host, uint64_t window_id, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback); @@ -326,6 +615,34 @@ void zero_native_windows_load_webview(Host *host, const char *source, size_t sou zero_native_windows_load_window_webview(host, 1, source, source_len, source_kind, asset_root, asset_root_len, asset_entry, asset_entry_len, asset_origin, asset_origin_len, spa_fallback); } +#if HAVE_WEBVIEW2_SDK + +void zero_native_windows_load_window_webview(Host *host, uint64_t window_id, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback) { + if (!host) return; + auto found = host->windows.find(window_id); + if (found == host->windows.end()) return; + Window &win = found->second; + + // Convert source (UTF-8) to wide string for navigation when needed. + std::wstring wsource; + if (source && source_len) { + int wlen = MultiByteToWideChar(CP_UTF8, 0, source, (int)source_len, nullptr, 0); + if (wlen > 0) { + wsource.resize(wlen); + MultiByteToWideChar(CP_UTF8, 0, source, (int)source_len, wsource.data(), wlen); + } + } + + // Convert asset paths for file:// URL construction + std::string asset_root_str(asset_root ? asset_root : "", asset_root_len); + std::string asset_entry_str(asset_entry ? asset_entry : "", asset_entry_len); + + // Kick off environment creation with the handler defined above. + CreateCoreWebView2EnvironmentWithOptions(nullptr, nullptr, nullptr, new EnvHandler(host, &win, wsource, source_kind, asset_root_str, asset_entry_str)); +} + +#else // !HAVE_WEBVIEW2_SDK + void zero_native_windows_load_window_webview(Host *host, uint64_t window_id, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback) { (void)source; (void)source_len; @@ -342,6 +659,9 @@ void zero_native_windows_load_window_webview(Host *host, uint64_t window_id, con if (found != host->windows.end()) emit(host, found->second, kWindowFrame); } +#endif // HAVE_WEBVIEW2_SDK + + void zero_native_windows_set_bridge_callback(Host *host, BridgeCallback callback, void *context) { if (!host) return; host->bridge_callback = callback; @@ -479,7 +799,7 @@ static void flattenFilters(const char *filters, size_t filters_len, char *buffer WindowsOpenDialogResult zero_native_windows_show_open_dialog(Host *host, const WindowsOpenDialogOpts *opts, char *buffer, size_t buffer_len) { (void)host; WindowsOpenDialogResult result = { .count = 0, .bytes_written = 0 }; - IFileDialog *pfd = nullptr; + IFileOpenDialog *pfd = nullptr; HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd)); if (FAILED(hr) || !pfd) return result; @@ -791,72 +1111,4 @@ void zero_native_windows_set_tray_callback(Host *host, TrayCallback callback, vo host->tray_context = context; } -int zero_native_windows_create_window(Host *host, uint64_t window_id, const char *window_title, size_t window_title_len, const char *window_label, size_t window_label_len, double x, double y, double width, double height, int restore_frame) { - (void)restore_frame; - if (!host || host->windows.find(window_id) != host->windows.end()) return 0; - Window window; - window.id = window_id; - window.title = slice(window_title, window_title_len); - window.label = slice(window_label, window_label_len); - window.x = x; - window.y = y; - window.width = width; - window.height = height; - bool ok = createNativeWindow(host, window); - host->windows[window_id] = window; - return ok ? 1 : 0; -} - -int zero_native_windows_focus_window(Host *host, uint64_t window_id) { - if (!host) return 0; - auto found = host->windows.find(window_id); - if (found == host->windows.end() || !found->second.hwnd) return 0; - SetForegroundWindow(found->second.hwnd); - SetFocus(found->second.hwnd); - return 1; -} - -int zero_native_windows_close_window(Host *host, uint64_t window_id) { - if (!host) return 0; - auto found = host->windows.find(window_id); - if (found == host->windows.end() || !found->second.hwnd) return 0; - DestroyWindow(found->second.hwnd); - return 1; -} - -size_t zero_native_windows_clipboard_read(Host *host, char *buffer, size_t buffer_len) { - (void)host; - if (!buffer || buffer_len == 0 || !OpenClipboard(nullptr)) return 0; - HANDLE handle = GetClipboardData(CF_TEXT); - if (!handle) { - CloseClipboard(); - return 0; - } - const char *text = static_cast(GlobalLock(handle)); - if (!text) { - CloseClipboard(); - return 0; - } - size_t len = boundedLen(text, buffer_len); - memcpy(buffer, text, len); - GlobalUnlock(handle); - CloseClipboard(); - return len; -} - -void zero_native_windows_clipboard_write(Host *host, const char *text, size_t text_len) { - (void)host; - if (!OpenClipboard(nullptr)) return; - EmptyClipboard(); - HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, text_len + 1); - if (handle) { - char *dest = static_cast(GlobalLock(handle)); - memcpy(dest, text, text_len); - dest[text_len] = '\0'; - GlobalUnlock(handle); - SetClipboardData(CF_TEXT, handle); - } - CloseClipboard(); -} - } \ No newline at end of file diff --git a/src/primitives/assets/root.zig b/src/primitives/assets/root.zig index a15f553c3..1c94add4a 100644 --- a/src/primitives/assets/root.zig +++ b/src/primitives/assets/root.zig @@ -63,7 +63,7 @@ pub const Hash = struct { } pub fn zero() Hash { - return .{ .bytes = [_]u8{0}** digest_len }; + return .{ .bytes = @splat(@as(u8, 0)) }; } pub fn toHex(self: Hash) [hex_len]u8 { diff --git a/src/tooling/templates.zig b/src/tooling/templates.zig index 955e0cddc..ea47eb3bb 100644 --- a/src/tooling/templates.zig +++ b/src/tooling/templates.zig @@ -179,6 +179,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path: \\ const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium"); \\ const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds"); \\ const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting"); + \\ const webview2_sdk_dir_override = b.option([]const u8, "webview2-sdk-dir", "Windows: WebView2 SDK root directory (contains build/native/include and build/native/*)"); \\ const package_target = b.option(PackageTarget, "package-target", "Package target: macos, windows, linux") orelse .macos; \\ const zero_native_path = b.option([]const u8, "zero-native-path", "Path to the zero-native framework checkout") orelse default_zero_native_path; \\ const optimize_name = @tagName(optimize); @@ -199,6 +200,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path: \\ const web_engine = web_engine_override orelse app_web_engine.web_engine; \\ const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir); \\ const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install; + \\ const webview2_sdk_dir = webview2_sdk_dir_override orelse "third_party/webview2-sdk"; \\ if (web_engine == .chromium and selected_platform == .@"null") { \\ @panic("-Dweb-engine=chromium requires -Dplatform=macos, linux, or windows"); \\ } @@ -230,7 +232,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path: \\ .name = app_exe_name, \\ .root_module = app_mod, \\ }); - \\ linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, zero_native_path, cef_dir, cef_auto_install); + \\ linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, zero_native_path, webview2_sdk_dir, cef_dir, cef_auto_install); \\ b.installArtifact(exe); \\ \\ const frontend_install = b.addSystemCommand(&.{ "npm", "install", "--prefix", "frontend" }); @@ -331,7 +333,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path: \\ }); \\} \\ - \\fn linkPlatform(b: *std.Build, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, zero_native_path: []const u8, cef_dir: []const u8, cef_auto_install: bool) void { + \\fn linkPlatform(b: *std.Build, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, zero_native_path: []const u8, webview2_sdk_dir: []const u8, cef_dir: []const u8, cef_auto_install: bool) void { \\ if (platform == .macos) { \\ switch (web_engine) { \\ .system => { @@ -386,7 +388,18 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path: \\ if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{}); \\ } else if (platform == .windows) { \\ switch (web_engine) { - \\ .system => app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17" } }), + \\ .system => { + \\ const include_arg = b.fmt("-I{s}/build/native/include", .{webview2_sdk_dir}); + \\ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17", include_arg } }); + \\ const arch_lib_dir = switch (target.result.cpu.arch) { + \\ .x86_64 => b.fmt("{s}/build/native/x64", .{webview2_sdk_dir}), + \\ .x86 => b.fmt("{s}/build/native/x86", .{webview2_sdk_dir}), + \\ .aarch64 => b.fmt("{s}/build/native/arm64", .{webview2_sdk_dir}), + \\ else => @panic("Unsupported Windows CPU architecture for WebView2 SDK"), + \\ }; + \\ app_mod.addLibraryPath(b.path(arch_lib_dir)); + \\ app_mod.linkSystemLibrary("WebView2Loader", .{}); + \\ }, \\ .chromium => { \\ const cef_check = addCefCheck(b, target, cef_dir); \\ if (cef_auto_install) { @@ -917,7 +930,7 @@ fn appZon(allocator: std.mem.Allocator, names: TemplateNames, frontend: Frontend \\, \\ .version = "0.1.0", \\ .icons = .{ "assets/icon.icns" }, - \\ .platforms = .{ "macos", "linux" }, + \\ .platforms = .{ "macos", "linux", "windows" }, \\ .permissions = .{}, \\ .capabilities = .{ "webview" }, \\ .frontend = .{ @@ -1733,6 +1746,18 @@ fn readme(allocator: std.mem.Allocator, names: TemplateNames, framework_path: [] \\ \\Use `-Dcef-dir=/path/to/cef` when you keep CEF outside the platform default under `third_party/cef`. \\ + \\## Windows + \\ + \\On Windows the system engine uses the WebView2 Runtime (included with Windows 11, available on Windows 10 via the evergreen runtime). + \\ + \\The generated build script includes the `-Dwebview2-sdk-dir` option for projects that vendor the WebView2 SDK: + \\ + \\```sh + \\zig build run -Dplatform=windows -Dwebview2-sdk-dir=third_party/webview2-sdk + \\``` + \\ + \\Without this option the compiler looks for `` in the system Windows SDK path (requires Visual Studio or the Windows SDK). The C++ host uses `__has_include` to degrade gracefully when the header is unavailable. + \\ \\```sh \\zero-native doctor --web-engine chromium \\``` @@ -1804,6 +1829,10 @@ test "writeDefaultApp emits Vite project files" { try std.testing.expect(std.mem.indexOf(u8, main_zig_text, "127.0.0.1:5173") != null); try std.testing.expect(std.mem.indexOf(u8, package_json_text, "\"vite\"") != null); try std.testing.expect(std.mem.indexOf(u8, main_js_text, "window.zero") != null); + try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "\"windows\"") != null); + try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "webview2-sdk-dir") != null); + try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "src/platform/windows/webview2_host.cpp") != null); + try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "Windows") != null); } test "writeDefaultApp emits frontend-specific Next paths" { @@ -1858,9 +1887,27 @@ fn normalizeModuleName(allocator: std.mem.Allocator, value: []const u8) ![]const try out.append(allocator, '_'); } } + if (isZigKeyword(out.items)) try out.appendSlice(allocator, "_pkg"); return out.toOwnedSlice(allocator); } +fn isZigKeyword(name: []const u8) bool { + const keywords = [_][]const u8{ + "addrspace", "align", "allowzero", "and", "anyframe", "anytype", "asm", + "async", "await", "break", "callconv", "catch", "comptime", "const", + "continue", "defer", "else", "enum", "errdefer", "error", "export", + "extern", "fn", "for", "if", "inline", "noalias", "noinline", + "nosuspend", "not", "null", "or", "orelse", "packed", "pub", + "resume", "return", "linksection", "struct", "suspend", "switch", + "test", "threadlocal", "true", "try", "union", "unreachable", + "use", "var", "volatile", "while", + }; + for (keywords) |kw| { + if (std.mem.eql(u8, name, kw)) return true; + } + return false; +} + test "normalizeModuleName caps Zig package names" { const module_name = try normalizeModuleName(std.testing.allocator, "scaffold-package-smoke-1778284313"); defer std.testing.allocator.free(module_name); From 45e8eb3060e0cd357a2ebd50cb211a4e6d044f21 Mon Sep 17 00:00:00 2001 From: Pratham Ghaywat Date: Sun, 10 May 2026 21:50:27 +0200 Subject: [PATCH 5/5] fix up docs so CI passes --- docs/src/app/windows-build/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/app/windows-build/page.mdx b/docs/src/app/windows-build/page.mdx index da56fbda6..46d5a695c 100644 --- a/docs/src/app/windows-build/page.mdx +++ b/docs/src/app/windows-build/page.mdx @@ -23,7 +23,7 @@ The generated project looks for the WebView2 SDK at `third_party/webview2-sdk/` The WebView2 SDK is distributed as a NuGet package. Download and extract it with PowerShell: -```pwsh +```bash Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.3351.48 -OutFile webview2.zip New-Item -ItemType Directory -Path third_party/webview2-sdk -Force Expand-Archive -Path webview2.zip -DestinationPath third_party/webview2-sdk