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/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/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..af86c182e 100644 --- a/src/platform/windows/webview2_host.cpp +++ b/src/platform/windows/webview2_host.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include #include @@ -8,6 +11,8 @@ #include #include +#include "webview2_host_types.h" + namespace { enum EventKind { @@ -36,6 +41,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 +54,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 +67,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 +98,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 +139,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 +160,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 +173,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 +225,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 +291,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 +309,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 +445,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; + } + + IShellItemArray *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 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 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) };