@zogien/native-sdk 0.11.0-fork.4 → 0.11.0-fork.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/build/app.zig +50 -34
  2. package/package.json +9 -9
  3. package/packages/core/package-lock.json +2 -2
  4. package/packages/core/package.json +1 -1
  5. package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
  6. package/src/platform/linux/cef_host.cpp +22 -0
  7. package/src/platform/linux/gtk_host.c +317 -34
  8. package/src/platform/linux/gtk_host.h +37 -0
  9. package/src/platform/linux/root.zig +114 -20
  10. package/src/platform/macos/appkit_host.h +44 -0
  11. package/src/platform/macos/appkit_host.m +301 -67
  12. package/src/platform/macos/cef_host.mm +21 -0
  13. package/src/platform/macos/root.zig +117 -23
  14. package/src/platform/null_platform.zig +136 -0
  15. package/src/platform/root.zig +6 -0
  16. package/src/platform/types.zig +81 -0
  17. package/src/platform/windows/dialog_test.cpp +303 -0
  18. package/src/platform/windows/host_loop_test.cpp +169 -0
  19. package/src/platform/windows/root.zig +225 -39
  20. package/src/platform/windows/session_end_test.cpp +303 -0
  21. package/src/platform/windows/webview2_host.cpp +1064 -280
  22. package/src/root.zig +7 -0
  23. package/src/runtime/builtin_bridge.zig +108 -13
  24. package/src/runtime/core.zig +24 -3
  25. package/src/runtime/dialog_tests.zig +367 -0
  26. package/src/runtime/dialogs.zig +385 -0
  27. package/src/runtime/effects.zig +71 -2
  28. package/src/runtime/effects_host_tests.zig +69 -1
  29. package/src/runtime/flow.zig +14 -6
  30. package/src/runtime/flow_reference.zig +14 -6
  31. package/src/runtime/platform_bridge_tests.zig +1 -0
  32. package/src/runtime/root.zig +5 -0
  33. package/src/runtime/system_services.zig +64 -82
  34. package/src/runtime/ui_app.zig +67 -0
  35. package/tools/native-sdk/main.zig +1 -1
@@ -356,6 +356,23 @@ const AppKitMessageDialogOpts = extern struct {
356
356
  tertiary_button_len: usize,
357
357
  };
358
358
 
359
+ const AppKitDialogRequest = extern struct {
360
+ id: u64,
361
+ parent_window_id: u64,
362
+ kind: c_int,
363
+ open_opts: ?*const AppKitOpenDialogOpts,
364
+ save_opts: ?*const AppKitSaveDialogOpts,
365
+ message_opts: ?*const AppKitMessageDialogOpts,
366
+ };
367
+
368
+ const AppKitDialogResult = extern struct {
369
+ id: u64,
370
+ outcome: c_int,
371
+ button: c_int,
372
+ count: usize,
373
+ bytes_written: usize,
374
+ };
375
+
359
376
  const AppKitWidgetAccessibilityNode = extern struct {
360
377
  id: u64,
361
378
  parent_id: u64,
@@ -462,6 +479,9 @@ const AppKitTrayChartRow = extern struct {
462
479
  extern fn native_sdk_appkit_show_open_dialog(host: *AppKitHost, opts: *const AppKitOpenDialogOpts, buffer: [*]u8, buffer_len: usize) AppKitOpenDialogResult;
463
480
  extern fn native_sdk_appkit_show_save_dialog(host: *AppKitHost, opts: *const AppKitSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize;
464
481
  extern fn native_sdk_appkit_show_message_dialog(host: *AppKitHost, opts: *const AppKitMessageDialogOpts) c_int;
482
+ extern fn native_sdk_appkit_request_dialog(host: *AppKitHost, request: *const AppKitDialogRequest) c_int;
483
+ extern fn native_sdk_appkit_cancel_dialog(host: *AppKitHost, id: u64) void;
484
+ extern fn native_sdk_appkit_poll_dialog_result(host: *AppKitHost, buffer: [*]u8, buffer_len: usize, result: *AppKitDialogResult) c_int;
465
485
  extern fn native_sdk_appkit_create_tray(host: *AppKitHost, status_item_id: u32, icon_path: [*]const u8, icon_path_len: usize, title: [*]const u8, title_len: usize, tooltip: [*]const u8, tooltip_len: usize, visible: c_int, width: f64, tone: c_int, icon_opacity: f64, monospaced: c_int, font_size: f64, font_weight: c_int, activation_command: [*]const u8, activation_command_len: usize, alternate_activation_command: [*]const u8, alternate_activation_command_len: usize, open_command: [*]const u8, open_command_len: usize) void;
466
486
  extern fn native_sdk_appkit_update_tray_shell(host: *AppKitHost, status_item_id: u32, icon_path: [*]const u8, icon_path_len: usize, tooltip: [*]const u8, tooltip_len: usize, visible: c_int, activation_command: [*]const u8, activation_command_len: usize, alternate_activation_command: [*]const u8, alternate_activation_command_len: usize, open_command: [*]const u8, open_command_len: usize) void;
467
487
  extern fn native_sdk_appkit_update_tray_menu(host: *AppKitHost, status_item_id: u32, item_ids: [*]const u32, labels: [*]const [*]const u8, label_lens: [*]const usize, separators: [*]const c_int, enabled_flags: [*]const c_int, details: [*]const [*]const u8, detail_lens: [*]const usize, roles: [*]const c_int, keys: [*]const [*]const u8, key_lens: [*]const usize, modifiers: [*]const u32, count: usize) void;
@@ -804,6 +824,9 @@ pub const MacPlatform = struct {
804
824
  .show_open_dialog_fn = showOpenDialog,
805
825
  .show_save_dialog_fn = showSaveDialog,
806
826
  .show_message_dialog_fn = showMessageDialog,
827
+ .request_dialog_fn = if (self.web_engine == .system) requestDialog else null,
828
+ .cancel_dialog_fn = if (self.web_engine == .system) cancelDialog else null,
829
+ .poll_dialog_result_fn = if (self.web_engine == .system) pollDialogResult else null,
807
830
  .show_notification_fn = showNotification,
808
831
  .set_credential_fn = setCredential,
809
832
  .get_credential_fn = getCredential,
@@ -2694,11 +2717,9 @@ fn viewKindInt(kind: platform_mod.ViewKind) c_int {
2694
2717
  };
2695
2718
  }
2696
2719
 
2697
- fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
2698
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2699
- var ext_buf: [1024]u8 = undefined;
2700
- const ext_str = flattenFilters(options.filters, &ext_buf);
2701
- const opts = AppKitOpenDialogOpts{
2720
+ fn appKitOpenDialogOpts(options: platform_mod.OpenDialogOptions, ext_buf: []u8) AppKitOpenDialogOpts {
2721
+ const ext_str = flattenFilters(options.filters, ext_buf);
2722
+ return .{
2702
2723
  .title = options.title.ptr,
2703
2724
  .title_len = options.title.len,
2704
2725
  .default_path = options.default_path.ptr,
@@ -2708,19 +2729,11 @@ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions,
2708
2729
  .allow_directories = if (options.allow_directories) 1 else 0,
2709
2730
  .allow_multiple = if (options.allow_multiple) 1 else 0,
2710
2731
  };
2711
- const result = native_sdk_appkit_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
2712
- if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
2713
- return .{
2714
- .count = result.count,
2715
- .paths = buffer[0..result.bytes_written],
2716
- };
2717
2732
  }
2718
2733
 
2719
- fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
2720
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2721
- var ext_buf: [1024]u8 = undefined;
2722
- const ext_str = flattenFilters(options.filters, &ext_buf);
2723
- const opts = AppKitSaveDialogOpts{
2734
+ fn appKitSaveDialogOpts(options: platform_mod.SaveDialogOptions, ext_buf: []u8) AppKitSaveDialogOpts {
2735
+ const ext_str = flattenFilters(options.filters, ext_buf);
2736
+ return .{
2724
2737
  .title = options.title.ptr,
2725
2738
  .title_len = options.title.len,
2726
2739
  .default_path = options.default_path.ptr,
@@ -2730,15 +2743,10 @@ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions,
2730
2743
  .extensions = ext_str.ptr,
2731
2744
  .extensions_len = ext_str.len,
2732
2745
  };
2733
- const written = native_sdk_appkit_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
2734
- if (written > buffer.len) return error.NoSpaceLeft;
2735
- if (written == 0) return null;
2736
- return buffer[0..written];
2737
2746
  }
2738
2747
 
2739
- fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
2740
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2741
- const opts = AppKitMessageDialogOpts{
2748
+ fn appKitMessageDialogOpts(options: platform_mod.MessageDialogOptions) AppKitMessageDialogOpts {
2749
+ return .{
2742
2750
  .style = @intFromEnum(options.style),
2743
2751
  .title = options.title.ptr,
2744
2752
  .title_len = options.title.len,
@@ -2753,10 +2761,96 @@ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOp
2753
2761
  .tertiary_button = options.tertiary_button.ptr,
2754
2762
  .tertiary_button_len = options.tertiary_button.len,
2755
2763
  };
2764
+ }
2765
+
2766
+ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
2767
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2768
+ var ext_buf: [1024]u8 = undefined;
2769
+ const opts = appKitOpenDialogOpts(options, &ext_buf);
2770
+ const result = native_sdk_appkit_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
2771
+ if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
2772
+ return .{
2773
+ .count = result.count,
2774
+ .paths = buffer[0..result.bytes_written],
2775
+ };
2776
+ }
2777
+
2778
+ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
2779
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2780
+ var ext_buf: [1024]u8 = undefined;
2781
+ const opts = appKitSaveDialogOpts(options, &ext_buf);
2782
+ const written = native_sdk_appkit_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
2783
+ if (written > buffer.len) return error.NoSpaceLeft;
2784
+ if (written == 0) return null;
2785
+ return buffer[0..written];
2786
+ }
2787
+
2788
+ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
2789
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2790
+ const opts = appKitMessageDialogOpts(options);
2756
2791
  const result = native_sdk_appkit_show_message_dialog(self.host, &opts);
2757
2792
  return @enumFromInt(result);
2758
2793
  }
2759
2794
 
2795
+ // The host copies every option before returning, so they may live on this
2796
+ // stack; the result comes back through pollDialogResult on a later wake.
2797
+ fn requestDialog(context: ?*anyopaque, request: platform_mod.DialogRequest) anyerror!void {
2798
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2799
+ var ext_buf: [1024]u8 = undefined;
2800
+ var open_opts: AppKitOpenDialogOpts = undefined;
2801
+ var save_opts: AppKitSaveDialogOpts = undefined;
2802
+ var message_opts: AppKitMessageDialogOpts = undefined;
2803
+ var native_request = AppKitDialogRequest{
2804
+ .id = request.id,
2805
+ .parent_window_id = request.parent_window_id orelse 0,
2806
+ .kind = @intFromEnum(std.meta.activeTag(request.options)),
2807
+ .open_opts = null,
2808
+ .save_opts = null,
2809
+ .message_opts = null,
2810
+ };
2811
+ switch (request.options) {
2812
+ .open => |options| {
2813
+ open_opts = appKitOpenDialogOpts(options, &ext_buf);
2814
+ native_request.open_opts = &open_opts;
2815
+ },
2816
+ .save => |options| {
2817
+ save_opts = appKitSaveDialogOpts(options, &ext_buf);
2818
+ native_request.save_opts = &save_opts;
2819
+ },
2820
+ .message => |options| {
2821
+ message_opts = appKitMessageDialogOpts(options);
2822
+ native_request.message_opts = &message_opts;
2823
+ },
2824
+ }
2825
+ if (native_sdk_appkit_request_dialog(self.host, &native_request) == 0) return error.DialogUnavailable;
2826
+ }
2827
+
2828
+ fn cancelDialog(context: ?*anyopaque, id: platform_mod.DialogRequestId) void {
2829
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2830
+ native_sdk_appkit_cancel_dialog(self.host, id);
2831
+ }
2832
+
2833
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?platform_mod.DialogResult {
2834
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2835
+ var result: AppKitDialogResult = undefined;
2836
+ if (native_sdk_appkit_poll_dialog_result(self.host, buffer.ptr, buffer.len, &result) == 0) return null;
2837
+ return .{
2838
+ .id = result.id,
2839
+ .outcome = switch (result.outcome) {
2840
+ 0 => .selected,
2841
+ 1 => .cancelled,
2842
+ else => .failed,
2843
+ },
2844
+ .paths = buffer[0..@min(result.bytes_written, buffer.len)],
2845
+ .count = result.count,
2846
+ .button = switch (result.button) {
2847
+ 1 => .secondary,
2848
+ 2 => .tertiary,
2849
+ else => .primary,
2850
+ },
2851
+ };
2852
+ }
2853
+
2760
2854
  const max_tray_items: usize = 32;
2761
2855
 
2762
2856
  fn createTray(context: ?*anyopaque, status_item_id: platform_mod.StatusItemId, options: platform_mod.TrayOptions) anyerror!void {
@@ -117,8 +117,36 @@ const SaveDialogOptions = types.SaveDialogOptions;
117
117
  const MessageDialogStyle = types.MessageDialogStyle;
118
118
  const MessageDialogResult = types.MessageDialogResult;
119
119
  const MessageDialogOptions = types.MessageDialogOptions;
120
+ const DialogRequestId = types.DialogRequestId;
121
+ const DialogKind = types.DialogKind;
122
+ const DialogRequest = types.DialogRequest;
123
+ const DialogOutcome = types.DialogOutcome;
124
+ const DialogResult = types.DialogResult;
120
125
  const NotificationOptions = types.NotificationOptions;
121
126
  const CredentialKey = types.CredentialKey;
127
+
128
+ const max_null_dialogs = 16;
129
+
130
+ /// One asynchronous dialog the null platform was asked to present.
131
+ pub const NullDialog = struct {
132
+ state: enum { free, presented, finished } = .free,
133
+ id: DialogRequestId = 0,
134
+ kind: DialogKind = .open,
135
+ parent_window_id: ?WindowId = null,
136
+ /// Request order, so `pollDialogResult` reports finishes in order.
137
+ finished_order: u64 = 0,
138
+ outcome: DialogOutcome = .cancelled,
139
+ button: MessageDialogResult = .primary,
140
+ count: usize = 0,
141
+ paths: [512]u8 = undefined,
142
+ paths_len: usize = 0,
143
+ title: [128]u8 = undefined,
144
+ title_len: usize = 0,
145
+
146
+ pub fn titleText(self: *const NullDialog) []const u8 {
147
+ return self.title[0..self.title_len];
148
+ }
149
+ };
122
150
  const Credential = types.Credential;
123
151
  const LocalTimeStyle = types.LocalTimeStyle;
124
152
  const StatusItemId = types.StatusItemId;
@@ -527,6 +555,17 @@ pub const NullPlatform = struct {
527
555
  save_dialog_count: usize = 0,
528
556
  message_dialog_count: usize = 0,
529
557
  message_dialog_result: MessageDialogResult = .primary,
558
+ /// Asynchronous dialogs, once a test opts in (`useAsyncDialogs`):
559
+ /// requests the runtime made, in order, and the results tests finished
560
+ /// them with. Nothing finishes on its own; a test calls `finishDialog`
561
+ /// (or the runtime cancels), which posts a wake like a real host.
562
+ dialogs: [max_null_dialogs]NullDialog = [_]NullDialog{.{}} ** max_null_dialogs,
563
+ dialog_request_count: usize = 0,
564
+ dialog_cancel_count: usize = 0,
565
+ dialog_finish_sequence: u64 = 0,
566
+ /// The next `requestDialog` fails, modelling a host that cannot
567
+ /// present.
568
+ fail_next_dialog_request: bool = false,
530
569
  notification_title: [max_notification_title_bytes]u8 = undefined,
531
570
  notification_title_len: usize = 0,
532
571
  notification_subtitle: [max_notification_subtitle_bytes]u8 = undefined,
@@ -1650,6 +1689,103 @@ pub const NullPlatform = struct {
1650
1689
  return self.message_dialog_result;
1651
1690
  }
1652
1691
 
1692
+ /// Route `services` (this platform's, as the runtime holds them) through
1693
+ /// the asynchronous dialog services. Opt-in: by default the null
1694
+ /// platform answers the blocking dialog calls at once, which app tests
1695
+ /// rely on, and its asynchronous dialogs never finish on their own.
1696
+ pub fn useAsyncDialogs(self: *NullPlatform, services: *PlatformServices) void {
1697
+ std.debug.assert(services.context == @as(?*anyopaque, @ptrCast(self)));
1698
+ services.request_dialog_fn = requestDialog;
1699
+ services.cancel_dialog_fn = cancelDialog;
1700
+ services.poll_dialog_result_fn = pollDialogResult;
1701
+ }
1702
+
1703
+ fn requestDialog(context: ?*anyopaque, request: DialogRequest) anyerror!void {
1704
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1705
+ if (self.fail_next_dialog_request) {
1706
+ self.fail_next_dialog_request = false;
1707
+ return error.DialogUnavailable;
1708
+ }
1709
+ const slot = for (&self.dialogs) |*dialog| {
1710
+ if (dialog.state == .free) break dialog;
1711
+ } else return error.DialogUnavailable;
1712
+ const title = switch (request.options) {
1713
+ .open => |open| open.title,
1714
+ .save => |save| save.title,
1715
+ .message => |message| if (message.title.len > 0) message.title else message.message,
1716
+ };
1717
+ slot.* = .{ .state = .presented, .id = request.id, .kind = request.options, .parent_window_id = request.parent_window_id };
1718
+ slot.title_len = @min(title.len, slot.title.len);
1719
+ @memcpy(slot.title[0..slot.title_len], title[0..slot.title_len]);
1720
+ self.dialog_request_count += 1;
1721
+ }
1722
+
1723
+ fn cancelDialog(context: ?*anyopaque, id: DialogRequestId) void {
1724
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1725
+ const dialog = self.presentedDialog(id) orelse return;
1726
+ self.dialog_cancel_count += 1;
1727
+ self.markDialogFinished(dialog, .cancelled);
1728
+ }
1729
+
1730
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?DialogResult {
1731
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1732
+ var next: ?*NullDialog = null;
1733
+ for (&self.dialogs) |*dialog| {
1734
+ if (dialog.state != .finished) continue;
1735
+ if (next == null or dialog.finished_order < next.?.finished_order) next = dialog;
1736
+ }
1737
+ const dialog = next orelse return null;
1738
+ const len = @min(dialog.paths_len, buffer.len);
1739
+ @memcpy(buffer[0..len], dialog.paths[0..len]);
1740
+ const result: DialogResult = .{ .id = dialog.id, .outcome = dialog.outcome, .paths = buffer[0..len], .count = dialog.count, .button = dialog.button };
1741
+ dialog.* = .{};
1742
+ return result;
1743
+ }
1744
+
1745
+ fn presentedDialog(self: *NullPlatform, id: DialogRequestId) ?*NullDialog {
1746
+ for (&self.dialogs) |*dialog| {
1747
+ if (dialog.state == .presented and dialog.id == id) return dialog;
1748
+ }
1749
+ return null;
1750
+ }
1751
+
1752
+ fn markDialogFinished(self: *NullPlatform, dialog: *NullDialog, outcome: DialogOutcome) void {
1753
+ dialog.state = .finished;
1754
+ dialog.outcome = outcome;
1755
+ dialog.finished_order = self.dialog_finish_sequence;
1756
+ self.dialog_finish_sequence += 1;
1757
+ _ = self.wake_count.fetchAdd(1, .release);
1758
+ }
1759
+
1760
+ /// Test helper: how many dialogs are showing right now.
1761
+ pub fn presentedDialogCount(self: *const NullPlatform) usize {
1762
+ var count: usize = 0;
1763
+ for (self.dialogs) |dialog| {
1764
+ if (dialog.state == .presented) count += 1;
1765
+ }
1766
+ return count;
1767
+ }
1768
+
1769
+ /// Test helper: the presented dialog with `id`, for inspecting its
1770
+ /// kind, parent and title.
1771
+ pub fn findPresentedDialog(self: *NullPlatform, id: DialogRequestId) ?*const NullDialog {
1772
+ return self.presentedDialog(id);
1773
+ }
1774
+
1775
+ /// Test helper: the user picked `paths` (newline-separated) in an
1776
+ /// open or save dialog, or pressed `button` in a message dialog. Posts
1777
+ /// the wake a real host would; `error.DialogNotPresented` names a
1778
+ /// dialog that is not showing.
1779
+ pub fn finishDialog(self: *NullPlatform, id: DialogRequestId, outcome: DialogOutcome, paths: []const u8, button: MessageDialogResult) error{ DialogNotPresented, NoSpaceLeft }!void {
1780
+ const dialog = self.presentedDialog(id) orelse return error.DialogNotPresented;
1781
+ if (paths.len > dialog.paths.len) return error.NoSpaceLeft;
1782
+ @memcpy(dialog.paths[0..paths.len], paths);
1783
+ dialog.paths_len = paths.len;
1784
+ dialog.count = if (paths.len == 0) 0 else std.mem.count(u8, paths, "\n") + 1;
1785
+ dialog.button = button;
1786
+ self.markDialogFinished(dialog, outcome);
1787
+ }
1788
+
1653
1789
  fn showNotification(context: ?*anyopaque, options: NotificationOptions) anyerror!void {
1654
1790
  const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1655
1791
  self.notification_title = undefined;
@@ -137,6 +137,12 @@ pub const SaveDialogOptions = types.SaveDialogOptions;
137
137
  pub const MessageDialogStyle = types.MessageDialogStyle;
138
138
  pub const MessageDialogResult = types.MessageDialogResult;
139
139
  pub const MessageDialogOptions = types.MessageDialogOptions;
140
+ pub const DialogRequestId = types.DialogRequestId;
141
+ pub const DialogKind = types.DialogKind;
142
+ pub const DialogOptions = types.DialogOptions;
143
+ pub const DialogRequest = types.DialogRequest;
144
+ pub const DialogOutcome = types.DialogOutcome;
145
+ pub const DialogResult = types.DialogResult;
140
146
  pub const NotificationOptions = types.NotificationOptions;
141
147
  pub const NotificationCommandEvent = types.NotificationCommandEvent;
142
148
  pub const CredentialKey = types.CredentialKey;
@@ -1454,6 +1454,56 @@ pub const MessageDialogOptions = struct {
1454
1454
  tertiary_button: []const u8 = "",
1455
1455
  };
1456
1456
 
1457
+ /// Identifies one asynchronous dialog (`PlatformServices.requestDialog`).
1458
+ /// The runtime allocates ids; zero is never used.
1459
+ pub const DialogRequestId = u64;
1460
+
1461
+ pub const DialogKind = enum(u8) {
1462
+ open,
1463
+ save,
1464
+ message,
1465
+ };
1466
+
1467
+ pub const DialogOptions = union(DialogKind) {
1468
+ open: OpenDialogOptions,
1469
+ save: SaveDialogOptions,
1470
+ message: MessageDialogOptions,
1471
+ };
1472
+
1473
+ /// One dialog the runtime asks the platform to present without blocking.
1474
+ /// Strings borrow runtime storage only for the duration of the call; a
1475
+ /// platform that presents later copies what it keeps.
1476
+ pub const DialogRequest = struct {
1477
+ id: DialogRequestId,
1478
+ /// The window the dialog belongs to (a macOS sheet, a Windows owner, a
1479
+ /// GTK transient parent). Null presents it without a parent.
1480
+ parent_window_id: ?WindowId = null,
1481
+ options: DialogOptions,
1482
+ };
1483
+
1484
+ pub const DialogOutcome = enum(u8) {
1485
+ /// Open and save: the user chose `paths`. Message: the user pressed
1486
+ /// `button`.
1487
+ selected,
1488
+ /// Closed without a choice: the user cancelled or dismissed it, or it
1489
+ /// was closed for them (its window closed, the request was cancelled,
1490
+ /// the session is ending).
1491
+ cancelled,
1492
+ /// The platform could not present the dialog.
1493
+ failed,
1494
+ };
1495
+
1496
+ pub const DialogResult = struct {
1497
+ id: DialogRequestId,
1498
+ outcome: DialogOutcome,
1499
+ /// Open: `count` newline-separated paths. Save: one path. Empty
1500
+ /// otherwise.
1501
+ paths: []const u8 = "",
1502
+ count: usize = 0,
1503
+ /// Message dialogs reporting `.selected`.
1504
+ button: MessageDialogResult = .primary,
1505
+ };
1506
+
1457
1507
  pub const NotificationOptions = struct {
1458
1508
  /// Stable host identity. Reissuing a non-empty id replaces the
1459
1509
  /// notification with that id and its action binding.
@@ -2820,6 +2870,18 @@ pub const PlatformServices = struct {
2820
2870
  show_open_dialog_fn: ?*const fn (context: ?*anyopaque, options: OpenDialogOptions, buffer: []u8) anyerror!OpenDialogResult = null,
2821
2871
  show_save_dialog_fn: ?*const fn (context: ?*anyopaque, options: SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 = null,
2822
2872
  show_message_dialog_fn: ?*const fn (context: ?*anyopaque, options: MessageDialogOptions) anyerror!MessageDialogResult = null,
2873
+ /// Asynchronous dialogs. `request_dialog_fn` returns promptly and
2874
+ /// never runs a nested event loop on the caller's stack: the platform
2875
+ /// presents the dialog on its own schedule, and when it finishes it
2876
+ /// keeps the result and posts a wake (`wake_fn`). The runtime drains
2877
+ /// finished dialogs with `poll_dialog_result_fn`, whose result paths
2878
+ /// borrow `buffer`. `cancel_dialog_fn` closes a presented dialog,
2879
+ /// which then finishes as `.cancelled`; unknown ids are a no-op.
2880
+ /// Platforms without these keep the synchronous `show_*_dialog_fn`
2881
+ /// family, which the runtime falls back to.
2882
+ request_dialog_fn: ?*const fn (context: ?*anyopaque, request: DialogRequest) anyerror!void = null,
2883
+ cancel_dialog_fn: ?*const fn (context: ?*anyopaque, id: DialogRequestId) void = null,
2884
+ poll_dialog_result_fn: ?*const fn (context: ?*anyopaque, buffer: []u8) ?DialogResult = null,
2823
2885
  show_notification_fn: ?*const fn (context: ?*anyopaque, options: NotificationOptions) anyerror!void = null,
2824
2886
  set_credential_fn: ?*const fn (context: ?*anyopaque, credential: Credential) anyerror!void = null,
2825
2887
  get_credential_fn: ?*const fn (context: ?*anyopaque, key: CredentialKey, buffer: []u8) anyerror![]const u8 = null,
@@ -3377,6 +3439,25 @@ pub const PlatformServices = struct {
3377
3439
  return msg_fn(self.context, options);
3378
3440
  }
3379
3441
 
3442
+ pub fn supportsAsyncDialogs(self: PlatformServices) bool {
3443
+ return self.request_dialog_fn != null and self.poll_dialog_result_fn != null;
3444
+ }
3445
+
3446
+ pub fn requestDialog(self: PlatformServices, request: DialogRequest) anyerror!void {
3447
+ const request_fn = self.request_dialog_fn orelse return error.UnsupportedService;
3448
+ return request_fn(self.context, request);
3449
+ }
3450
+
3451
+ pub fn cancelDialog(self: PlatformServices, id: DialogRequestId) void {
3452
+ const cancel_fn = self.cancel_dialog_fn orelse return;
3453
+ cancel_fn(self.context, id);
3454
+ }
3455
+
3456
+ pub fn pollDialogResult(self: PlatformServices, buffer: []u8) ?DialogResult {
3457
+ const poll_fn = self.poll_dialog_result_fn orelse return null;
3458
+ return poll_fn(self.context, buffer);
3459
+ }
3460
+
3380
3461
  pub fn showNotification(self: PlatformServices, options: NotificationOptions) anyerror!void {
3381
3462
  const notify_fn = self.show_notification_fn orelse return error.UnsupportedService;
3382
3463
  return notify_fn(self.context, options);