@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.
- package/build/app.zig +50 -34
- package/package.json +9 -9
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/src/platform/linux/cef_host.cpp +22 -0
- package/src/platform/linux/gtk_host.c +317 -34
- package/src/platform/linux/gtk_host.h +37 -0
- package/src/platform/linux/root.zig +114 -20
- package/src/platform/macos/appkit_host.h +44 -0
- package/src/platform/macos/appkit_host.m +301 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +117 -23
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +6 -0
- package/src/platform/types.zig +81 -0
- package/src/platform/windows/dialog_test.cpp +303 -0
- package/src/platform/windows/host_loop_test.cpp +169 -0
- package/src/platform/windows/root.zig +225 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1064 -280
- package/src/root.zig +7 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/core.zig +24 -3
- package/src/runtime/dialog_tests.zig +367 -0
- package/src/runtime/dialogs.zig +385 -0
- package/src/runtime/effects.zig +71 -2
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +14 -6
- package/src/runtime/flow_reference.zig +14 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +5 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- 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
|
|
2698
|
-
const
|
|
2699
|
-
|
|
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
|
|
2720
|
-
const
|
|
2721
|
-
|
|
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
|
|
2740
|
-
|
|
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;
|
package/src/platform/root.zig
CHANGED
|
@@ -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;
|
package/src/platform/types.zig
CHANGED
|
@@ -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);
|