@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
package/build/app.zig CHANGED
@@ -2827,8 +2827,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
2827
2827
  // The vendored WebView2 SDK header (third_party/webview2)
2828
2828
  // turns on the host's embedded-WebView layer; the host
2829
2829
  // fails the compile by design if it cannot be found.
2830
- app_mod.addIncludePath(dep.path("third_party/webview2/include"));
2831
- if (windowsSdkWinrtIncludeDir(b, target)) |winrt| app_mod.addSystemIncludePath(.{ .cwd_relative = winrt });
2830
+ addWebView2HostIncludes(b, app_mod, dep.path("third_party/webview2/include"), target);
2832
2831
  app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
2833
2832
  app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
2834
2833
  // WebView2Loader.dll rides next to the installed app
@@ -2871,36 +2870,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
2871
2870
  app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
2872
2871
  },
2873
2872
  }
2874
- app_mod.linkSystemLibrary("c", .{});
2875
- // The MSVC target compiles the hosts against the toolchain's own
2876
- // STL, which pulls in msvcprt/vcruntime through its default-lib
2877
- // directives; Zig's libc++/libc++abi cannot be built against the
2878
- // MSVC CRT headers (cxxabi.h collides with vcruntime_typeinfo.h).
2879
- if (target.result.abi != .msvc) app_mod.linkSystemLibrary("c++", .{});
2880
- app_mod.linkSystemLibrary("user32", .{});
2881
- app_mod.linkSystemLibrary("gdi32", .{});
2882
- // Retained gpu_surface packets are composited into a hardware
2883
- // Direct2D target; DirectWrite draws the engine-measured text
2884
- // runs (including registered in-memory fonts) on that target.
2885
- app_mod.linkSystemLibrary("d2d1", .{});
2886
- app_mod.linkSystemLibrary("dwrite", .{});
2887
- app_mod.linkSystemLibrary("imm32", .{});
2888
- app_mod.linkSystemLibrary("comctl32", .{});
2889
- app_mod.linkSystemLibrary("ole32", .{});
2890
- app_mod.linkSystemLibrary("oleacc", .{});
2891
- app_mod.linkSystemLibrary("shell32", .{});
2892
- // TypeScript cores link ScriptC's host runtime, whose network-interface
2893
- // helpers use GetAdaptersAddresses and Winsock address conversion; the
2894
- // in-process service archive shares the same runtime and additionally
2895
- // reaches advapi32 (crypto-backed randomness).
2896
- app_mod.linkSystemLibrary("iphlpapi", .{});
2897
- app_mod.linkSystemLibrary("ws2_32", .{});
2898
- app_mod.linkSystemLibrary("advapi32", .{});
2899
- // The audio backend: Media Foundation (session + source resolver
2900
- // + streaming audio renderer) and WinHTTP (the cache fill).
2901
- app_mod.linkSystemLibrary("mf", .{});
2902
- app_mod.linkSystemLibrary("mfplat", .{});
2903
- app_mod.linkSystemLibrary("winhttp", .{});
2873
+ linkWindowsHostLibraries(app_mod, target);
2904
2874
  if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
2905
2875
  }
2906
2876
  }
@@ -2928,6 +2898,53 @@ fn addPlatformLinkSearchPaths(b: *std.Build, platform: PlatformOption, web_engin
2928
2898
 
2929
2899
  /// The vendored WebView2Loader.dll for the target architecture, relative
2930
2900
  /// to the framework root.
2901
+ /// The system libraries the Windows host links, for an app executable
2902
+ /// and for the host's own test harness alike.
2903
+ pub fn linkWindowsHostLibraries(module: *std.Build.Module, target: std.Build.ResolvedTarget) void {
2904
+ module.linkSystemLibrary("c", .{});
2905
+ // The MSVC target compiles the hosts against the toolchain's own
2906
+ // STL, which pulls in msvcprt/vcruntime through its default-lib
2907
+ // directives; Zig's libc++/libc++abi cannot be built against the
2908
+ // MSVC CRT headers (cxxabi.h collides with vcruntime_typeinfo.h).
2909
+ if (target.result.abi != .msvc) module.linkSystemLibrary("c++", .{});
2910
+ module.linkSystemLibrary("user32", .{});
2911
+ module.linkSystemLibrary("gdi32", .{});
2912
+ // Retained gpu_surface packets are composited into a hardware
2913
+ // Direct2D target; DirectWrite draws the engine-measured text
2914
+ // runs (including registered in-memory fonts) on that target.
2915
+ module.linkSystemLibrary("d2d1", .{});
2916
+ module.linkSystemLibrary("dwrite", .{});
2917
+ module.linkSystemLibrary("imm32", .{});
2918
+ module.linkSystemLibrary("comctl32", .{});
2919
+ module.linkSystemLibrary("ole32", .{});
2920
+ module.linkSystemLibrary("oleacc", .{});
2921
+ module.linkSystemLibrary("shell32", .{});
2922
+ // TypeScript cores link ScriptC's host runtime, whose network-interface
2923
+ // helpers use GetAdaptersAddresses and Winsock address conversion; the
2924
+ // in-process service archive shares the same runtime and additionally
2925
+ // reaches advapi32 (crypto-backed randomness).
2926
+ module.linkSystemLibrary("iphlpapi", .{});
2927
+ module.linkSystemLibrary("ws2_32", .{});
2928
+ module.linkSystemLibrary("advapi32", .{});
2929
+ // The audio backend: Media Foundation (session + source resolver
2930
+ // + streaming audio renderer) and WinHTTP (the cache fill).
2931
+ module.linkSystemLibrary("mf", .{});
2932
+ module.linkSystemLibrary("mfplat", .{});
2933
+ module.linkSystemLibrary("winhttp", .{});
2934
+ }
2935
+
2936
+ /// The WebView2 host's include paths: the vendored WebView2 SDK header
2937
+ /// directory, plus WRL for MSVC targets (`windowsSdkWinrtIncludeDir`).
2938
+ pub fn addWebView2HostIncludes(b: *std.Build, module: *std.Build.Module, sdk_include: std.Build.LazyPath, target: std.Build.ResolvedTarget) void {
2939
+ module.addIncludePath(sdk_include);
2940
+ if (windowsSdkWinrtIncludeDir(b, target)) |winrt| module.addSystemIncludePath(.{ .cwd_relative = winrt });
2941
+ }
2942
+
2943
+ /// The vendored WebView2Loader.dll's directory, relative to the SDK root.
2944
+ pub fn webView2LoaderDir(target: std.Build.ResolvedTarget) []const u8 {
2945
+ return std.fs.path.dirname(webView2LoaderSubPath(target)).?;
2946
+ }
2947
+
2931
2948
  fn webView2LoaderSubPath(target: std.Build.ResolvedTarget) []const u8 {
2932
2949
  return if (target.result.cpu.arch == .aarch64)
2933
2950
  "third_party/webview2/arm64/WebView2Loader.dll"
@@ -2957,8 +2974,7 @@ fn addWebView2RuntimeRunFiles(dep: *std.Build.Dependency, target: std.Build.Reso
2957
2974
  if (web_engine != .system) return;
2958
2975
  if (!web_layer) return;
2959
2976
  if (target.result.os.tag != .windows) return;
2960
- const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?;
2961
- run.addPathDir(dep.builder.pathFromRoot(loader_dir));
2977
+ run.addPathDir(dep.builder.pathFromRoot(webView2LoaderDir(target)));
2962
2978
  }
2963
2979
 
2964
2980
  fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zogien/native-sdk",
3
- "version": "0.11.0-fork.4",
3
+ "version": "0.11.0-fork.5",
4
4
  "description": "Community-maintained fork of Native SDK: TypeScript, Native markup, and native desktop tooling",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,14 +41,14 @@
41
41
  "tar": "7.5.22"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.4",
45
- "@zogien/native-sdk-darwin-x64": "0.11.0-fork.4",
46
- "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.4",
47
- "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.4",
48
- "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.4",
49
- "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.4",
50
- "@zogien/native-sdk-win32-arm64": "0.11.0-fork.4",
51
- "@zogien/native-sdk-win32-x64": "0.11.0-fork.4"
44
+ "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.5",
45
+ "@zogien/native-sdk-darwin-x64": "0.11.0-fork.5",
46
+ "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.5",
47
+ "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.5",
48
+ "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.5",
49
+ "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.5",
50
+ "@zogien/native-sdk-win32-arm64": "0.11.0-fork.5",
51
+ "@zogien/native-sdk-win32-x64": "0.11.0-fork.5"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.4",
3
+ "version": "0.11.0-fork.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@native-sdk/core",
9
- "version": "0.11.0-fork.4",
9
+ "version": "0.11.0-fork.5",
10
10
  "hasInstallScript": true,
11
11
  "dependencies": {
12
12
  "@scriptc/types": "0.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.4",
3
+ "version": "0.11.0-fork.5",
4
4
  "description": "The TypeScript authoring tier: the app-core subset, its checker and contract frontend, the exact-pinned core compiler dependency, and the SDK module cores import",
5
5
  "repository": {
6
6
  "type": "git",
@@ -206,6 +206,10 @@ const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
206
206
  });
207
207
  ```
208
208
 
209
+ Each promise resolves when its dialog closes; the app keeps running meanwhile. `openFile` gives an array of paths and `saveFile` a path, or `null` when cancelled; `showMessage` gives `"primary"`, `"secondary"`, `"tertiary"`, or `"dismissed"` when closed without a button.
210
+
211
+ From Zig, use `runtime.requestOpenDialog`, `requestSaveDialog` or `requestMessageDialog` with a `DialogCompletion`: the answer arrives in `on_result` on a later dispatch. The older `runtime.showOpenDialog` family blocks the event dispatch until the dialog closes; do not use it in new code.
212
+
209
213
  Use native dialogs for trusted app UI. Do not expose arbitrary filesystem access to remote or untrusted origins.
210
214
 
211
215
  ## Error handling
@@ -610,4 +610,26 @@ int native_sdk_gtk_show_message_dialog(Host *host, const void *opts) {
610
610
  return 0;
611
611
  }
612
612
 
613
+ /* Asynchronous dialogs are not offered here: root.zig leaves them unbound
614
+ * for the Chromium engine, so the runtime falls back to the blocking
615
+ * calls above. */
616
+ int native_sdk_gtk_request_dialog(Host *host, const void *request) {
617
+ (void)host;
618
+ (void)request;
619
+ return 0;
620
+ }
621
+
622
+ void native_sdk_gtk_cancel_dialog(Host *host, uint64_t id) {
623
+ (void)host;
624
+ (void)id;
625
+ }
626
+
627
+ int native_sdk_gtk_poll_dialog_result(Host *host, char *buffer, size_t buffer_len, void *result) {
628
+ (void)host;
629
+ (void)buffer;
630
+ (void)buffer_len;
631
+ (void)result;
632
+ return 0;
633
+ }
634
+
613
635
  }
@@ -396,6 +396,26 @@ typedef struct native_sdk_gtk_notification_action {
396
396
 
397
397
  static const char native_sdk_notification_action_name[] = "native-sdk-notification-action";
398
398
 
399
+ /* One asynchronous dialog (native_sdk_gtk_request_dialog), from request
400
+ * until poll takes its result. GTK holds it as the finish callback's
401
+ * data; once native_sdk_gtk_destroy detaches it (host NULL) the callback
402
+ * only frees it. */
403
+ typedef struct native_sdk_gtk_dialog {
404
+ struct native_sdk_gtk_dialog *next;
405
+ native_sdk_gtk_host_t *host;
406
+ uint64_t id;
407
+ uint64_t parent_window_id;
408
+ int kind;
409
+ int folders;
410
+ GObject *dialog;
411
+ GCancellable *cancellable;
412
+ int finished;
413
+ uint64_t finish_serial;
414
+ int outcome;
415
+ int button;
416
+ GPtrArray *paths;
417
+ } native_sdk_gtk_dialog_t;
418
+
399
419
  struct native_sdk_gtk_host {
400
420
  GtkApplication *app;
401
421
  /* Display providers are global and the display retains them until
@@ -476,6 +496,14 @@ struct native_sdk_gtk_host {
476
496
  GHashTable *notification_actions;
477
497
  GHashTable *notification_action_tokens_by_id;
478
498
  native_sdk_gtk_audio_t audio;
499
+ /* Asynchronous dialogs not yet polled, newest first; finished ones
500
+ * are handed back in finish order. */
501
+ native_sdk_gtk_dialog_t *dialogs;
502
+ uint64_t dialog_finish_serial;
503
+ /* The queued wake that hands finished dialogs to the runtime, tracked
504
+ * like stop_idle_source: finishes coalesce into it, and destroy
505
+ * removes it so it never fires into a freed host. */
506
+ guint dialog_wake_source;
479
507
  };
480
508
 
481
509
  static void native_sdk_emit(native_sdk_gtk_host_t *host, native_sdk_gtk_event_t event);
@@ -483,6 +511,8 @@ static void native_sdk_context_menu_free(native_sdk_gtk_context_menu_t *menu);
483
511
  static gboolean native_sdk_nudge_chrome_requery(gpointer data);
484
512
  static gboolean native_sdk_on_file_drop(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data);
485
513
  static GtkWindow *native_sdk_parent_window(native_sdk_gtk_host_t *host);
514
+ static void native_sdk_cancel_dialogs_for_window(native_sdk_gtk_host_t *host, uint64_t window_id);
515
+ static void native_sdk_detach_dialogs(native_sdk_gtk_host_t *host);
486
516
  static const char *native_sdk_shortcut_key_for_keyval(guint keyval, char *buffer, size_t buffer_len, int *uses_implicit_shift);
487
517
  static void native_sdk_audio_release(native_sdk_gtk_host_t *host, int cancel_download);
488
518
 
@@ -2781,6 +2811,7 @@ static gboolean on_close_request(GtkWindow *window, gpointer data) {
2781
2811
  (void)window;
2782
2812
  native_sdk_gtk_window_t *win = data;
2783
2813
  native_sdk_gtk_host_t *host = win->host;
2814
+ native_sdk_cancel_dialogs_for_window(host, win->id);
2784
2815
  int closed_index = -1;
2785
2816
  for (int i = 0; i < host->window_count; i++) {
2786
2817
  if (&host->windows[i] == win) {
@@ -3479,6 +3510,8 @@ void native_sdk_gtk_destroy(native_sdk_gtk_host_t *host) {
3479
3510
  /* Retire the audio pipeline (and cancel its cache download) before
3480
3511
  * anything else goes away; everything runs on this thread. */
3481
3512
  native_sdk_audio_release(host, 1);
3513
+ native_sdk_detach_dialogs(host);
3514
+ if (host->dialog_wake_source) g_source_remove(host->dialog_wake_source);
3482
3515
  if (host->frame_timer) g_source_remove(host->frame_timer);
3483
3516
  /* A stop turn still queued at teardown (the loop quit before the
3484
3517
  * idle ran) must not fire into freed memory. */
@@ -6642,6 +6675,53 @@ static char *native_sdk_bytes_to_string(const char *bytes, size_t len) {
6642
6675
  return bytes && len > 0 ? native_sdk_strndup(bytes, len) : NULL;
6643
6676
  }
6644
6677
 
6678
+ /* The dialog builders hand back plain GObjects, keeping the GTK 4.10
6679
+ * dialog types out of file-scope signatures: against an older GTK this
6680
+ * file then fails only with the GTK-age diagnostics the GLib 2.72 receipt
6681
+ * allows (.github/scripts/glib272_error_allowlist.py). */
6682
+ static GObject *native_sdk_new_open_dialog(const native_sdk_gtk_open_dialog_opts_t *opts) {
6683
+ GtkFileDialog *dialog = gtk_file_dialog_new();
6684
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6685
+ if (title) gtk_file_dialog_set_title(dialog, title);
6686
+ free(title);
6687
+ return G_OBJECT(dialog);
6688
+ }
6689
+
6690
+ static GObject *native_sdk_new_save_dialog(const native_sdk_gtk_save_dialog_opts_t *opts) {
6691
+ GtkFileDialog *dialog = gtk_file_dialog_new();
6692
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6693
+ char *default_name = native_sdk_bytes_to_string(opts->default_name, opts->default_name_len);
6694
+ if (title) gtk_file_dialog_set_title(dialog, title);
6695
+ if (default_name) gtk_file_dialog_set_initial_name(dialog, default_name);
6696
+ free(title);
6697
+ free(default_name);
6698
+ return G_OBJECT(dialog);
6699
+ }
6700
+
6701
+ static GObject *native_sdk_new_alert_dialog(const native_sdk_gtk_message_dialog_opts_t *opts) {
6702
+ /* The constructor requires a non-NULL printf-style format string. */
6703
+ GtkAlertDialog *dialog = gtk_alert_dialog_new("");
6704
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6705
+ char *message = native_sdk_bytes_to_string(opts->message, opts->message_len);
6706
+ char *informative = native_sdk_bytes_to_string(opts->informative_text, opts->informative_text_len);
6707
+ char *primary = native_sdk_bytes_to_string(opts->primary_button, opts->primary_button_len);
6708
+ char *secondary = native_sdk_bytes_to_string(opts->secondary_button, opts->secondary_button_len);
6709
+ char *tertiary = native_sdk_bytes_to_string(opts->tertiary_button, opts->tertiary_button_len);
6710
+ gtk_alert_dialog_set_message(dialog, title ? title : (message ? message : ""));
6711
+ if (informative || (title && message)) gtk_alert_dialog_set_detail(dialog, informative ? informative : message);
6712
+ const char *buttons[4] = { primary ? primary : "OK", NULL, NULL, NULL };
6713
+ if (secondary) buttons[1] = secondary;
6714
+ if (tertiary) buttons[2] = tertiary;
6715
+ gtk_alert_dialog_set_buttons(dialog, buttons);
6716
+ free(title);
6717
+ free(message);
6718
+ free(informative);
6719
+ free(primary);
6720
+ free(secondary);
6721
+ free(tertiary);
6722
+ return G_OBJECT(dialog);
6723
+ }
6724
+
6645
6725
  static void native_sdk_open_dialog_done(GObject *source, GAsyncResult *result, gpointer data) {
6646
6726
  native_sdk_file_dialog_state_t *state = data;
6647
6727
  GError *error = NULL;
@@ -6668,13 +6748,10 @@ static void native_sdk_save_dialog_done(GObject *source, GAsyncResult *result, g
6668
6748
 
6669
6749
  native_sdk_gtk_open_dialog_result_t native_sdk_gtk_show_open_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_open_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
6670
6750
  native_sdk_gtk_open_dialog_result_t result = {0};
6671
- GtkFileDialog *dialog = gtk_file_dialog_new();
6672
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6673
- if (title) gtk_file_dialog_set_title(dialog, title);
6751
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_open_dialog(opts));
6674
6752
  GtkWindow *parent = native_sdk_parent_window(host);
6675
6753
  native_sdk_file_dialog_state_t state = { .loop = g_main_loop_new(NULL, FALSE) };
6676
6754
  if (!state.loop) {
6677
- if (title) free(title);
6678
6755
  g_object_unref(dialog);
6679
6756
  return result;
6680
6757
  }
@@ -6711,22 +6788,15 @@ native_sdk_gtk_open_dialog_result_t native_sdk_gtk_show_open_dialog(native_sdk_g
6711
6788
  result.bytes_written = overflow ? native_sdk_overflow_size(buffer_len) : offset;
6712
6789
  g_object_unref(state.files);
6713
6790
  }
6714
- if (title) free(title);
6715
6791
  g_object_unref(dialog);
6716
6792
  return result;
6717
6793
  }
6718
6794
 
6719
6795
  size_t native_sdk_gtk_show_save_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_save_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
6720
- GtkFileDialog *dialog = gtk_file_dialog_new();
6721
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6722
- char *default_name = native_sdk_bytes_to_string(opts->default_name, opts->default_name_len);
6723
- if (title) gtk_file_dialog_set_title(dialog, title);
6724
- if (default_name) gtk_file_dialog_set_initial_name(dialog, default_name);
6796
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_save_dialog(opts));
6725
6797
  GtkWindow *parent = native_sdk_parent_window(host);
6726
6798
  native_sdk_file_dialog_state_t state = { .loop = g_main_loop_new(NULL, FALSE) };
6727
6799
  if (!state.loop) {
6728
- if (title) free(title);
6729
- if (default_name) free(default_name);
6730
6800
  g_object_unref(dialog);
6731
6801
  return 0;
6732
6802
  }
@@ -6748,8 +6818,6 @@ size_t native_sdk_gtk_show_save_dialog(native_sdk_gtk_host_t *host, const native
6748
6818
  }
6749
6819
  g_object_unref(state.file);
6750
6820
  }
6751
- if (title) free(title);
6752
- if (default_name) free(default_name);
6753
6821
  g_object_unref(dialog);
6754
6822
  return written;
6755
6823
  }
@@ -6771,34 +6839,249 @@ static void native_sdk_alert_done(GObject *source, GAsyncResult *result, gpointe
6771
6839
  }
6772
6840
 
6773
6841
  int native_sdk_gtk_show_message_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_message_dialog_opts_t *opts) {
6774
- /* The constructor requires a non-NULL printf-style format string. */
6775
- GtkAlertDialog *dialog = gtk_alert_dialog_new("");
6776
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6777
- char *message = native_sdk_bytes_to_string(opts->message, opts->message_len);
6778
- char *informative = native_sdk_bytes_to_string(opts->informative_text, opts->informative_text_len);
6779
- char *primary = native_sdk_bytes_to_string(opts->primary_button, opts->primary_button_len);
6780
- char *secondary = native_sdk_bytes_to_string(opts->secondary_button, opts->secondary_button_len);
6781
- char *tertiary = native_sdk_bytes_to_string(opts->tertiary_button, opts->tertiary_button_len);
6782
- gtk_alert_dialog_set_message(dialog, title ? title : (message ? message : ""));
6783
- if (informative || (title && message)) gtk_alert_dialog_set_detail(dialog, informative ? informative : message);
6784
- const char *buttons[4] = { primary ? primary : "OK", NULL, NULL, NULL };
6785
- if (secondary) buttons[1] = secondary;
6786
- if (tertiary) buttons[2] = tertiary;
6787
- gtk_alert_dialog_set_buttons(dialog, buttons);
6842
+ GtkAlertDialog *dialog = GTK_ALERT_DIALOG(native_sdk_new_alert_dialog(opts));
6788
6843
  native_sdk_alert_state_t state = { .loop = g_main_loop_new(NULL, FALSE), .response = 0 };
6789
6844
  if (state.loop) {
6790
6845
  gtk_alert_dialog_choose(dialog, native_sdk_parent_window(host), NULL, native_sdk_alert_done, &state);
6791
6846
  g_main_loop_run(state.loop);
6792
6847
  g_main_loop_unref(state.loop);
6793
6848
  }
6794
- if (title) free(title);
6795
- if (message) free(message);
6796
- if (informative) free(informative);
6797
- if (primary) free(primary);
6798
- if (secondary) free(secondary);
6799
- if (tertiary) free(tertiary);
6800
6849
  g_object_unref(dialog);
6801
6850
  if (state.response <= 0) return 0;
6802
6851
  if (state.response == 1) return 1;
6803
6852
  return 2;
6804
6853
  }
6854
+
6855
+ /* GTK's dialogs are asynchronous already; the blocking calls above only
6856
+ * wrap them in a nested loop. The asynchronous ones drop that loop: the
6857
+ * finish callback records the result and schedules a wake, and the
6858
+ * runtime collects it with poll. The callback never emits, because GTask
6859
+ * may run it inside the dispatch that cancelled the dialog. */
6860
+
6861
+ static void native_sdk_dialog_free(native_sdk_gtk_dialog_t *entry) {
6862
+ if (entry->dialog) g_object_unref(entry->dialog);
6863
+ if (entry->cancellable) g_object_unref(entry->cancellable);
6864
+ if (entry->paths) g_ptr_array_unref(entry->paths);
6865
+ free(entry);
6866
+ }
6867
+
6868
+ static void native_sdk_dialog_unlink(native_sdk_gtk_host_t *host, native_sdk_gtk_dialog_t *entry) {
6869
+ for (native_sdk_gtk_dialog_t **link = &host->dialogs; *link; link = &(*link)->next) {
6870
+ if (*link == entry) {
6871
+ *link = entry->next;
6872
+ entry->next = NULL;
6873
+ return;
6874
+ }
6875
+ }
6876
+ }
6877
+
6878
+ static native_sdk_gtk_dialog_t *native_sdk_dialog_find(native_sdk_gtk_host_t *host, uint64_t id) {
6879
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
6880
+ if (entry->id == id) return entry;
6881
+ }
6882
+ return NULL;
6883
+ }
6884
+
6885
+ /* A dialog the app or a window close cancelled reports cancelled, like
6886
+ * one the user dismissed; anything else that went wrong is a failure. */
6887
+ static int native_sdk_dialog_error_outcome(const GError *error) {
6888
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return NATIVE_SDK_GTK_DIALOG_CANCELLED;
6889
+ #if GTK_CHECK_VERSION(4, 10, 0)
6890
+ if (g_error_matches(error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED) ||
6891
+ g_error_matches(error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED)) {
6892
+ return NATIVE_SDK_GTK_DIALOG_CANCELLED;
6893
+ }
6894
+ #endif
6895
+ return NATIVE_SDK_GTK_DIALOG_FAILED;
6896
+ }
6897
+
6898
+ static gboolean native_sdk_dialog_wake_idle(gpointer data) {
6899
+ native_sdk_gtk_host_t *host = data;
6900
+ host->dialog_wake_source = 0;
6901
+ if (!host->did_shutdown) native_sdk_emit(host, (native_sdk_gtk_event_t){ .kind = NATIVE_SDK_GTK_EVENT_WAKE });
6902
+ return G_SOURCE_REMOVE;
6903
+ }
6904
+
6905
+ static void native_sdk_dialog_finish(native_sdk_gtk_dialog_t *entry, GError *error) {
6906
+ if (error) {
6907
+ entry->outcome = native_sdk_dialog_error_outcome(error);
6908
+ g_error_free(error);
6909
+ }
6910
+ native_sdk_gtk_host_t *host = entry->host;
6911
+ if (!host) {
6912
+ /* The host went first (native_sdk_gtk_destroy detached this). */
6913
+ native_sdk_dialog_free(entry);
6914
+ return;
6915
+ }
6916
+ entry->finished = 1;
6917
+ entry->finish_serial = ++host->dialog_finish_serial;
6918
+ if (!host->dialog_wake_source) host->dialog_wake_source = g_idle_add(native_sdk_dialog_wake_idle, host);
6919
+ }
6920
+
6921
+ static void native_sdk_dialog_add_file(native_sdk_gtk_dialog_t *entry, GFile *file) {
6922
+ char *path = file ? g_file_get_path(file) : NULL;
6923
+ if (path) g_ptr_array_add(entry->paths, path);
6924
+ }
6925
+
6926
+ static void native_sdk_dialog_files_done(GObject *source, GAsyncResult *result, gpointer data) {
6927
+ native_sdk_gtk_dialog_t *entry = data;
6928
+ GError *error = NULL;
6929
+ GListModel *files = NULL;
6930
+ if (entry->folders) {
6931
+ files = gtk_file_dialog_select_multiple_folders_finish(GTK_FILE_DIALOG(source), result, &error);
6932
+ } else {
6933
+ files = gtk_file_dialog_open_multiple_finish(GTK_FILE_DIALOG(source), result, &error);
6934
+ }
6935
+ if (files) {
6936
+ guint count = g_list_model_get_n_items(files);
6937
+ for (guint i = 0; i < count; i++) {
6938
+ GFile *file = G_FILE(g_list_model_get_item(files, i));
6939
+ native_sdk_dialog_add_file(entry, file);
6940
+ g_object_unref(file);
6941
+ }
6942
+ g_object_unref(files);
6943
+ }
6944
+ if (!error) entry->outcome = entry->paths->len > 0 ? NATIVE_SDK_GTK_DIALOG_SELECTED : NATIVE_SDK_GTK_DIALOG_CANCELLED;
6945
+ native_sdk_dialog_finish(entry, error);
6946
+ }
6947
+
6948
+ static void native_sdk_dialog_file_done(GObject *source, GAsyncResult *result, gpointer data) {
6949
+ native_sdk_gtk_dialog_t *entry = data;
6950
+ GError *error = NULL;
6951
+ /* Assigned rather than initialized, like `files` above: against an
6952
+ * older GTK that keeps the diagnostic to the receipt's
6953
+ * assignment-from-int shape (see the dialog builders). */
6954
+ GFile *file = NULL;
6955
+ file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &error);
6956
+ if (file) {
6957
+ native_sdk_dialog_add_file(entry, file);
6958
+ g_object_unref(file);
6959
+ }
6960
+ if (!error) entry->outcome = entry->paths->len > 0 ? NATIVE_SDK_GTK_DIALOG_SELECTED : NATIVE_SDK_GTK_DIALOG_CANCELLED;
6961
+ native_sdk_dialog_finish(entry, error);
6962
+ }
6963
+
6964
+ static void native_sdk_dialog_alert_done(GObject *source, GAsyncResult *result, gpointer data) {
6965
+ native_sdk_gtk_dialog_t *entry = data;
6966
+ GError *error = NULL;
6967
+ const int response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
6968
+ if (!error) {
6969
+ entry->outcome = NATIVE_SDK_GTK_DIALOG_SELECTED;
6970
+ entry->button = response > 2 ? 2 : (response < 0 ? 0 : response);
6971
+ }
6972
+ native_sdk_dialog_finish(entry, error);
6973
+ }
6974
+
6975
+ /* A closing window takes its dialogs down with it. */
6976
+ static void native_sdk_cancel_dialogs_for_window(native_sdk_gtk_host_t *host, uint64_t window_id) {
6977
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
6978
+ if (!entry->finished && entry->parent_window_id == window_id) g_cancellable_cancel(entry->cancellable);
6979
+ }
6980
+ }
6981
+
6982
+ /* Host teardown: finished results are dropped, and open dialogs are
6983
+ * cancelled and left to free themselves when GTK reports them. */
6984
+ static void native_sdk_detach_dialogs(native_sdk_gtk_host_t *host) {
6985
+ while (host->dialogs) {
6986
+ native_sdk_gtk_dialog_t *entry = host->dialogs;
6987
+ host->dialogs = entry->next;
6988
+ entry->next = NULL;
6989
+ if (entry->finished) {
6990
+ native_sdk_dialog_free(entry);
6991
+ continue;
6992
+ }
6993
+ entry->host = NULL;
6994
+ /* Last touch: the callback may run, and free entry, right here. */
6995
+ g_cancellable_cancel(entry->cancellable);
6996
+ }
6997
+ }
6998
+
6999
+ int native_sdk_gtk_request_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_dialog_request_t *request) {
7000
+ if (!host || !request || request->id == 0 || host->did_shutdown) return 0;
7001
+ if (native_sdk_dialog_find(host, request->id)) return 0;
7002
+ if ((request->kind == NATIVE_SDK_GTK_DIALOG_OPEN && !request->open_opts) ||
7003
+ (request->kind == NATIVE_SDK_GTK_DIALOG_SAVE && !request->save_opts) ||
7004
+ (request->kind == NATIVE_SDK_GTK_DIALOG_MESSAGE && !request->message_opts) ||
7005
+ request->kind < NATIVE_SDK_GTK_DIALOG_OPEN || request->kind > NATIVE_SDK_GTK_DIALOG_MESSAGE) {
7006
+ return 0;
7007
+ }
7008
+ native_sdk_gtk_dialog_t *entry = calloc(1, sizeof(*entry));
7009
+ if (!entry) return 0;
7010
+ entry->host = host;
7011
+ entry->id = request->id;
7012
+ entry->kind = request->kind;
7013
+ entry->outcome = NATIVE_SDK_GTK_DIALOG_CANCELLED;
7014
+ entry->cancellable = g_cancellable_new();
7015
+ entry->paths = g_ptr_array_new_with_free_func(g_free);
7016
+ native_sdk_gtk_window_t *win = request->parent_window_id != 0 ? native_sdk_find_window(host, request->parent_window_id) : NULL;
7017
+ GtkWindow *parent = win ? win->gtk_window : NULL;
7018
+ if (parent) entry->parent_window_id = request->parent_window_id;
7019
+ entry->next = host->dialogs;
7020
+ host->dialogs = entry;
7021
+ switch (request->kind) {
7022
+ case NATIVE_SDK_GTK_DIALOG_OPEN: {
7023
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_open_dialog(request->open_opts));
7024
+ entry->dialog = G_OBJECT(dialog);
7025
+ entry->folders = request->open_opts->allow_directories != 0;
7026
+ if (entry->folders) {
7027
+ gtk_file_dialog_select_multiple_folders(dialog, parent, entry->cancellable, native_sdk_dialog_files_done, entry);
7028
+ } else {
7029
+ gtk_file_dialog_open_multiple(dialog, parent, entry->cancellable, native_sdk_dialog_files_done, entry);
7030
+ }
7031
+ break;
7032
+ }
7033
+ case NATIVE_SDK_GTK_DIALOG_SAVE: {
7034
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_save_dialog(request->save_opts));
7035
+ entry->dialog = G_OBJECT(dialog);
7036
+ gtk_file_dialog_save(dialog, parent, entry->cancellable, native_sdk_dialog_file_done, entry);
7037
+ break;
7038
+ }
7039
+ default: {
7040
+ GtkAlertDialog *dialog = GTK_ALERT_DIALOG(native_sdk_new_alert_dialog(request->message_opts));
7041
+ entry->dialog = G_OBJECT(dialog);
7042
+ gtk_alert_dialog_choose(dialog, parent, entry->cancellable, native_sdk_dialog_alert_done, entry);
7043
+ break;
7044
+ }
7045
+ }
7046
+ return 1;
7047
+ }
7048
+
7049
+ void native_sdk_gtk_cancel_dialog(native_sdk_gtk_host_t *host, uint64_t id) {
7050
+ if (!host) return;
7051
+ native_sdk_gtk_dialog_t *entry = native_sdk_dialog_find(host, id);
7052
+ if (entry && !entry->finished) g_cancellable_cancel(entry->cancellable);
7053
+ }
7054
+
7055
+ int native_sdk_gtk_poll_dialog_result(native_sdk_gtk_host_t *host, char *buffer, size_t buffer_len, native_sdk_gtk_dialog_result_t *result) {
7056
+ if (!host || !result) return 0;
7057
+ native_sdk_gtk_dialog_t *oldest = NULL;
7058
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
7059
+ if (entry->finished && (!oldest || entry->finish_serial < oldest->finish_serial)) oldest = entry;
7060
+ }
7061
+ if (!oldest) return 0;
7062
+ native_sdk_dialog_unlink(host, oldest);
7063
+ *result = (native_sdk_gtk_dialog_result_t){ .id = oldest->id, .outcome = oldest->outcome, .button = oldest->button };
7064
+ size_t offset = 0;
7065
+ size_t count = 0;
7066
+ for (guint i = 0; i < oldest->paths->len; i++) {
7067
+ const char *path = g_ptr_array_index(oldest->paths, i);
7068
+ const size_t len = strlen(path);
7069
+ const size_t needed = len + (count > 0 ? 1 : 0);
7070
+ if (!buffer || needed > buffer_len - offset) {
7071
+ /* More paths than the runtime holds: a failure, not a
7072
+ * silently shortened selection. */
7073
+ result->outcome = NATIVE_SDK_GTK_DIALOG_FAILED;
7074
+ offset = 0;
7075
+ count = 0;
7076
+ break;
7077
+ }
7078
+ if (count > 0) buffer[offset++] = '\n';
7079
+ memcpy(buffer + offset, path, len);
7080
+ offset += len;
7081
+ count++;
7082
+ }
7083
+ result->count = count;
7084
+ result->bytes_written = offset;
7085
+ native_sdk_dialog_free(oldest);
7086
+ return 1;
7087
+ }