@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
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#include <cctype>
|
|
26
26
|
#include <climits>
|
|
27
27
|
#include <cmath>
|
|
28
|
+
#include <deque>
|
|
28
29
|
#include <map>
|
|
29
30
|
#include <memory>
|
|
30
31
|
#include <mutex>
|
|
@@ -186,21 +187,21 @@ constexpr uint32_t kMenuCommandBase = 0x4000;
|
|
|
186
187
|
constexpr uint32_t kTrayCommandBase = 0x5000;
|
|
187
188
|
constexpr UINT kNotificationCallbackMessage = WM_APP + 42;
|
|
188
189
|
/* Posted from any thread (effect worker threads) via
|
|
189
|
-
* native_sdk_windows_wake; the window
|
|
190
|
-
*
|
|
190
|
+
* native_sdk_windows_wake; the loop window emits kWake on the message
|
|
191
|
+
* loop thread. */
|
|
191
192
|
constexpr UINT kWakeMessage = WM_APP + 43;
|
|
192
|
-
/* Posted from any thread via native_sdk_windows_request_frame; the
|
|
193
|
-
* window
|
|
194
|
-
*
|
|
195
|
-
*
|
|
193
|
+
/* Posted from any thread via native_sdk_windows_request_frame; the loop
|
|
194
|
+
* window emits ONE kFrame on the message loop thread. The automation
|
|
195
|
+
* arrival watcher uses it so a queued command wakes an idle frame loop
|
|
196
|
+
* without depending on the 16 ms frame pump. */
|
|
196
197
|
constexpr UINT kRequestFrameMessage = WM_APP + 44;
|
|
197
198
|
/* Posted from Media Foundation worker threads (the audio backend's event
|
|
198
|
-
* pump and source resolver); the window
|
|
199
|
-
*
|
|
199
|
+
* pump and source resolver); the loop window hands the distilled note to
|
|
200
|
+
* audioHandleSessionMessage on the message loop thread. */
|
|
200
201
|
constexpr UINT kAudioSessionMessage = WM_APP + 45;
|
|
201
|
-
/* Posted from the spectrum capture thread every ~40 ms; the window
|
|
202
|
-
*
|
|
203
|
-
*
|
|
202
|
+
/* Posted from the spectrum capture thread every ~40 ms; the loop window
|
|
203
|
+
* snapshots the analysis bands and emits one SPECTRUM report on the loop
|
|
204
|
+
* thread. A posted message rather than a loop-thread timer
|
|
204
205
|
* on purpose: WM_TIMER is the lowest-priority message and a busy frame
|
|
205
206
|
* loop starves it far below the contract cadence, while posted messages
|
|
206
207
|
* keep their place in the queue. */
|
|
@@ -210,6 +211,10 @@ constexpr UINT kAudioSpectrumMessage = WM_APP + 46;
|
|
|
210
211
|
* menu on a fresh loop turn — the same deferral the macOS host's
|
|
211
212
|
* dispatch_async performs before popUpMenuPositioningItem. */
|
|
212
213
|
constexpr UINT kShowContextMenuMessage = WM_APP + 47;
|
|
214
|
+
/* Posted by queueWebViewEvent when a WebView2 callback queued something
|
|
215
|
+
* for the app; the loop window delivers the queue on a fresh loop turn,
|
|
216
|
+
* after the WebView2 callback has returned. */
|
|
217
|
+
constexpr UINT kDeliverWebViewEventsMessage = WM_APP + 48;
|
|
213
218
|
constexpr const char *kAssetVirtualOrigin = "https://native-sdk-app.localhost";
|
|
214
219
|
|
|
215
220
|
constexpr int kViewWebView = 0;
|
|
@@ -370,6 +375,27 @@ struct WindowsMessageDialogOpts {
|
|
|
370
375
|
size_t tertiary_button_len;
|
|
371
376
|
};
|
|
372
377
|
|
|
378
|
+
/* An asynchronous dialog request: kind is kDialogOpen, kDialogSave or
|
|
379
|
+
* kDialogMessage, with the matching options set. */
|
|
380
|
+
struct WindowsDialogRequest {
|
|
381
|
+
uint64_t id;
|
|
382
|
+
uint64_t parent_window_id;
|
|
383
|
+
int kind;
|
|
384
|
+
const WindowsOpenDialogOpts *open_opts;
|
|
385
|
+
const WindowsSaveDialogOpts *save_opts;
|
|
386
|
+
const WindowsMessageDialogOpts *message_opts;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/* A finished asynchronous dialog: outcome is kDialogSelected,
|
|
390
|
+
* kDialogCancelled or kDialogFailed; paths are newline-joined. */
|
|
391
|
+
struct WindowsDialogResult {
|
|
392
|
+
uint64_t id;
|
|
393
|
+
int outcome;
|
|
394
|
+
int button;
|
|
395
|
+
size_t count;
|
|
396
|
+
size_t bytes_written;
|
|
397
|
+
};
|
|
398
|
+
|
|
373
399
|
using EventCallback = void (*)(void *, const WindowsEvent *);
|
|
374
400
|
using BridgeCallback = void (*)(void *, uint64_t, const char *, size_t, const char *, size_t, const char *, size_t);
|
|
375
401
|
|
|
@@ -608,14 +634,70 @@ struct TrayItem {
|
|
|
608
634
|
bool enabled = true;
|
|
609
635
|
};
|
|
610
636
|
|
|
637
|
+
/* Whether the host still exists, for WebView2 completions and events
|
|
638
|
+
* that arrive after native_sdk_windows_destroy (they hold a weak
|
|
639
|
+
* reference to this). WebView2 raises every callback on the thread that
|
|
640
|
+
* created it, the loop thread, so a plain flag is enough: nothing off
|
|
641
|
+
* the loop thread reads it. */
|
|
611
642
|
struct HostLifetime {
|
|
612
|
-
std::recursive_mutex mutex;
|
|
613
643
|
bool alive = true;
|
|
614
644
|
};
|
|
615
645
|
|
|
646
|
+
constexpr int kDialogOpen = 0;
|
|
647
|
+
constexpr int kDialogSave = 1;
|
|
648
|
+
constexpr int kDialogMessage = 2;
|
|
649
|
+
constexpr int kDialogSelected = 0;
|
|
650
|
+
constexpr int kDialogCancelled = 1;
|
|
651
|
+
constexpr int kDialogFailed = 2;
|
|
652
|
+
|
|
653
|
+
/* One dialog: its options, copied so they outlive the call that passed
|
|
654
|
+
* them, and for an asynchronous one (native_sdk_windows_request_dialog)
|
|
655
|
+
* the state its thread shares with the loop and the result it hands
|
|
656
|
+
* back. */
|
|
657
|
+
struct DialogRequest {
|
|
658
|
+
uint64_t id = 0;
|
|
659
|
+
uint64_t parent_window_id = 0;
|
|
660
|
+
int kind = kDialogOpen;
|
|
661
|
+
HWND owner = nullptr;
|
|
662
|
+
std::string title;
|
|
663
|
+
std::string default_path;
|
|
664
|
+
std::string default_name;
|
|
665
|
+
std::string extensions;
|
|
666
|
+
bool allow_directories = false;
|
|
667
|
+
bool allow_multiple = false;
|
|
668
|
+
int style = 0;
|
|
669
|
+
std::string message;
|
|
670
|
+
std::string informative_text;
|
|
671
|
+
std::string primary_button;
|
|
672
|
+
std::string secondary_button;
|
|
673
|
+
std::string tertiary_button;
|
|
674
|
+
/* Under DialogQueue::mutex: the dialog's thread while it runs (zero
|
|
675
|
+
* before and after), and whether the app asked to close it. */
|
|
676
|
+
DWORD thread_id = 0;
|
|
677
|
+
bool cancel_requested = false;
|
|
678
|
+
/* Written by the dialog's thread before it queues the request. */
|
|
679
|
+
int outcome = kDialogCancelled;
|
|
680
|
+
int button = 0;
|
|
681
|
+
std::vector<std::string> paths;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/* Where dialog threads report. Each asynchronous dialog runs modally on
|
|
685
|
+
* a thread of its own, owned by its app window, as Chromium's shell
|
|
686
|
+
* dialogs do: Windows disables the owner while it shows, and the loop
|
|
687
|
+
* thread keeps pumping timers, wakes and WebView2. The threads hold this
|
|
688
|
+
* and their request, never the Host, which may be destroyed first. */
|
|
689
|
+
struct DialogQueue {
|
|
690
|
+
std::mutex mutex;
|
|
691
|
+
/* Finished requests, oldest first, until poll takes them. */
|
|
692
|
+
std::deque<std::shared_ptr<DialogRequest>> finished;
|
|
693
|
+
/* The wake target, cleared when the host retires its loop window. */
|
|
694
|
+
HWND loop_window = nullptr;
|
|
695
|
+
};
|
|
696
|
+
|
|
616
697
|
/* App timers (runtime `startTimer`) on the Win32 message loop: each slot
|
|
617
698
|
* owns the SetTimer id kAppTimerIdBase + slot index, scheduled on the
|
|
618
|
-
*
|
|
699
|
+
* host's loop window so WM_TIMER lands in loopWindowProc and closing an
|
|
700
|
+
* app window never stops one. */
|
|
619
701
|
constexpr size_t kMaxAppTimers = 64;
|
|
620
702
|
constexpr UINT_PTR kAppTimerIdBase = 0x1000;
|
|
621
703
|
/* The 16 ms per-window frame-pump timer (SetTimer id on each top-level
|
|
@@ -683,11 +765,21 @@ struct AudioCaptureState {
|
|
|
683
765
|
std::thread worker;
|
|
684
766
|
};
|
|
685
767
|
|
|
768
|
+
/* One URL load's hand-over slot. The Media Foundation resolver (worker
|
|
769
|
+
* thread) leaves the resolved source here and posts a note; the loop
|
|
770
|
+
* thread collects it, or closes the slot when the load is replaced or
|
|
771
|
+
* stopped and releases whatever arrives late. The mutex covers only the
|
|
772
|
+
* hand-over itself, never a call out of the host. */
|
|
773
|
+
struct AudioSourceMailbox {
|
|
774
|
+
std::mutex mutex;
|
|
775
|
+
bool open = true;
|
|
776
|
+
IMFMediaSource *source = nullptr;
|
|
777
|
+
};
|
|
778
|
+
|
|
686
779
|
/* The app's single audio player (see the audio section further down for
|
|
687
780
|
* the backend rationale). All fields are message-loop-thread state; the
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
* Media Foundation worker thread. */
|
|
781
|
+
* asynchronous URL source resolver hands its result over through
|
|
782
|
+
* `resolve_mailbox` instead of writing here. */
|
|
691
783
|
struct AudioState {
|
|
692
784
|
/* Bumped on every load/stop: worker-thread stragglers (resolver
|
|
693
785
|
* completions, retired-session events) carry the generation they
|
|
@@ -715,6 +807,7 @@ struct AudioState {
|
|
|
715
807
|
IMFMediaSource *source = nullptr;
|
|
716
808
|
IMFPresentationClock *clock = nullptr;
|
|
717
809
|
std::shared_ptr<AudioDownloadCancel> download_cancel;
|
|
810
|
+
std::shared_ptr<AudioSourceMailbox> resolve_mailbox;
|
|
718
811
|
};
|
|
719
812
|
|
|
720
813
|
/* The one pending app context menu (menus are modal, so one at a time):
|
|
@@ -738,6 +831,38 @@ struct PendingContextMenu {
|
|
|
738
831
|
std::vector<Item> items;
|
|
739
832
|
};
|
|
740
833
|
|
|
834
|
+
/* A keydown resolved against the app's key bindings, owned so the event
|
|
835
|
+
* can be emitted after the callback that saw the key has returned. The
|
|
836
|
+
* match itself happens while the key is current: the modifier state is
|
|
837
|
+
* read then, and a later turn could see different keys held. */
|
|
838
|
+
struct ShortcutMatch {
|
|
839
|
+
int kind = 0; // kShortcut, kMenuCommand, or kTrayAction
|
|
840
|
+
uint64_t window_id = 0;
|
|
841
|
+
std::string shortcut_id;
|
|
842
|
+
std::string key;
|
|
843
|
+
uint32_t modifiers = 0;
|
|
844
|
+
std::string command;
|
|
845
|
+
uint32_t tray_item_id = 0;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/* Something a WebView2 callback learned that the app must see, copied out
|
|
849
|
+
* of the callback's arguments. WebView2 does not support running a nested
|
|
850
|
+
* message loop or modal UI inside its callbacks, and its other callbacks
|
|
851
|
+
* cannot run until the current one returns, so app code never runs there:
|
|
852
|
+
* the callback queues one of these and the loop window delivers it on a
|
|
853
|
+
* later turn (deliverWebViewEvents). */
|
|
854
|
+
struct DeferredWebViewEvent {
|
|
855
|
+
enum class Kind { Bridge, Focus, Shortcut, OpenExternal };
|
|
856
|
+
Kind kind = Kind::Bridge;
|
|
857
|
+
uint64_t window_id = 0;
|
|
858
|
+
std::string view_key;
|
|
859
|
+
std::string label;
|
|
860
|
+
std::string message;
|
|
861
|
+
std::string origin;
|
|
862
|
+
ShortcutMatch shortcut;
|
|
863
|
+
std::wstring url;
|
|
864
|
+
};
|
|
865
|
+
|
|
741
866
|
struct Host {
|
|
742
867
|
bool window_drag_initialization_failed = false;
|
|
743
868
|
uint64_t next_window_drag_generation = 1;
|
|
@@ -751,11 +876,27 @@ struct Host {
|
|
|
751
876
|
* means WebView2's own default. */
|
|
752
877
|
bool webview_user_data_folder_resolved = false;
|
|
753
878
|
std::wstring webview_user_data_folder;
|
|
879
|
+
/* While the loop runs, callback is dispatchEventToRuntime with this
|
|
880
|
+
* host as its context; it forwards to runtime_callback. */
|
|
754
881
|
EventCallback callback = nullptr;
|
|
755
882
|
void *callback_context = nullptr;
|
|
883
|
+
EventCallback runtime_callback = nullptr;
|
|
884
|
+
void *runtime_context = nullptr;
|
|
756
885
|
BridgeCallback bridge_callback = nullptr;
|
|
757
886
|
void *bridge_context = nullptr;
|
|
758
887
|
bool running = false;
|
|
888
|
+
/* Runtime dispatches on the stack (RuntimeDispatchScope). Above
|
|
889
|
+
* zero, the runtime queues a new event until the dispatch in
|
|
890
|
+
* progress commits, so nothing it is handed runs synchronously. */
|
|
891
|
+
int runtime_dispatch_depth = 0;
|
|
892
|
+
/* kShutdown has been delivered (or is being delivered). The run
|
|
893
|
+
* loop's exit and a session end can both reach it, and the app's
|
|
894
|
+
* stop hook runs exactly once (deliverShutdown). */
|
|
895
|
+
bool shutdown_delivered = false;
|
|
896
|
+
/* A session end arrived inside a runtime dispatch: its shutdown is
|
|
897
|
+
* queued behind that dispatch, and the loop exits once it unwinds
|
|
898
|
+
* (endSession). */
|
|
899
|
+
bool session_ending = false;
|
|
759
900
|
std::map<uint64_t, Window> windows;
|
|
760
901
|
std::map<std::string, ChildWebView> webviews;
|
|
761
902
|
std::shared_ptr<WindowsGpuRenderer> gpu_renderer;
|
|
@@ -796,9 +937,34 @@ struct Host {
|
|
|
796
937
|
AudioCaptureState captures[2];
|
|
797
938
|
PendingContextMenu context_menu;
|
|
798
939
|
std::shared_ptr<HostLifetime> lifetime = std::make_shared<HostLifetime>();
|
|
940
|
+
/* The message loop's own target: a message-only window created with
|
|
941
|
+
* the host on the loop thread and destroyed with it, so nothing
|
|
942
|
+
* posted to the loop depends on which app windows are open. Read
|
|
943
|
+
* from any thread (wake, frame requests), hence atomic; destroy
|
|
944
|
+
* clears it before tearing the window down. */
|
|
945
|
+
std::atomic<HWND> loop_window{nullptr};
|
|
946
|
+
/* A posted kRequestFrameMessage is still queued: requests coalesce
|
|
947
|
+
* until the loop window takes it. */
|
|
948
|
+
std::atomic<bool> frame_request_pending{false};
|
|
949
|
+
/* WebView2 events waiting for the app, in arrival order. One delivery
|
|
950
|
+
* runs at a time: an event queued while the app is inside an earlier
|
|
951
|
+
* one (a Bridge command showing a modal dialog) waits for it, the way
|
|
952
|
+
* WebView2 itself holds later callbacks while one is running. */
|
|
953
|
+
std::deque<DeferredWebViewEvent> webview_events;
|
|
954
|
+
bool webview_events_posted = false;
|
|
955
|
+
bool delivering_webview_events = false;
|
|
956
|
+
/* WebView2 callbacks currently on the stack; app callbacks run only
|
|
957
|
+
* at zero (see DeferredWebViewEvent). */
|
|
958
|
+
int webview_callback_depth = 0;
|
|
959
|
+
/* Asynchronous dialogs not yet polled, by id (loop thread only), and
|
|
960
|
+
* the queue their threads report to. */
|
|
961
|
+
std::map<uint64_t, std::shared_ptr<DialogRequest>> dialogs;
|
|
962
|
+
std::shared_ptr<DialogQueue> dialog_queue = std::make_shared<DialogQueue>();
|
|
799
963
|
};
|
|
800
964
|
|
|
801
965
|
static std::string webViewKey(uint64_t window_id, const std::string &label);
|
|
966
|
+
static void cancelDialogsForWindow(Host *host, uint64_t window_id);
|
|
967
|
+
static void cancelAllDialogs(Host *host);
|
|
802
968
|
static void gpuSurfaceRefreshFrameWakeTimer(Host *host);
|
|
803
969
|
static void gpuSurfaceDestroyFrameWakeTimer(Host *host);
|
|
804
970
|
|
|
@@ -865,6 +1031,13 @@ static HWND parentWindow(Host *host) {
|
|
|
865
1031
|
return nullptr;
|
|
866
1032
|
}
|
|
867
1033
|
|
|
1034
|
+
/* Where loop-bound work goes: posted notes from worker threads and the
|
|
1035
|
+
* app-level timers. Never an app window — those close independently of
|
|
1036
|
+
* the loop, and their timers and queued posts die with them. */
|
|
1037
|
+
static HWND loopWindow(Host *host) {
|
|
1038
|
+
return host ? host->loop_window.load(std::memory_order_acquire) : nullptr;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
868
1041
|
static void copyWideField(wchar_t *dest, size_t dest_len, const std::wstring &value) {
|
|
869
1042
|
if (!dest || dest_len == 0) return;
|
|
870
1043
|
size_t count = std::min(value.size(), dest_len - 1);
|
|
@@ -1767,7 +1940,10 @@ static void showTrayMenu(Host *host, HWND hwnd) {
|
|
|
1767
1940
|
PostMessageW(hwnd, WM_NULL, 0, 0);
|
|
1768
1941
|
}
|
|
1769
1942
|
|
|
1770
|
-
|
|
1943
|
+
/* Resolve a keydown against the app's shortcuts, then its menu and tray
|
|
1944
|
+
* key equivalents. True when a binding claims the key; `out` then holds
|
|
1945
|
+
* the event to emit. */
|
|
1946
|
+
static bool matchShortcutForWindow(Host *host, const Window *window, WPARAM wparam, ShortcutMatch *out) {
|
|
1771
1947
|
if (!host || (host->shortcuts.empty() && host->menus.empty() && host->tray_items.empty())) return false;
|
|
1772
1948
|
if (!window) return false;
|
|
1773
1949
|
std::string key = shortcutKeyFromWParam(wparam);
|
|
@@ -1779,16 +1955,11 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1779
1955
|
for (const Shortcut &shortcut : host->shortcuts) {
|
|
1780
1956
|
if (shortcut.key != key) continue;
|
|
1781
1957
|
if (!shortcutModifiersMatch(shortcut.modifiers, allow_implicit_shift)) continue;
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
event.shortcut_id_len = shortcut.id.size();
|
|
1788
|
-
event.shortcut_key = shortcut.key.c_str();
|
|
1789
|
-
event.shortcut_key_len = shortcut.key.size();
|
|
1790
|
-
event.shortcut_modifiers = shortcut.modifiers;
|
|
1791
|
-
host->callback(host->callback_context, &event);
|
|
1958
|
+
out->kind = kShortcut;
|
|
1959
|
+
out->window_id = window->id;
|
|
1960
|
+
out->shortcut_id = shortcut.id;
|
|
1961
|
+
out->key = shortcut.key;
|
|
1962
|
+
out->modifiers = shortcut.modifiers;
|
|
1792
1963
|
return true;
|
|
1793
1964
|
}
|
|
1794
1965
|
/* Menu accelerators dispatch from the same keydown path: a menu
|
|
@@ -1801,13 +1972,9 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1801
1972
|
if (item.separator || !item.enabled || item.key.empty()) continue;
|
|
1802
1973
|
if (item.key != key) continue;
|
|
1803
1974
|
if (!shortcutModifiersMatch(item.modifiers, allow_implicit_shift)) continue;
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
event.window_id = window->id;
|
|
1808
|
-
event.command_name = item.command.c_str();
|
|
1809
|
-
event.command_name_len = item.command.size();
|
|
1810
|
-
host->callback(host->callback_context, &event);
|
|
1975
|
+
out->kind = kMenuCommand;
|
|
1976
|
+
out->window_id = window->id;
|
|
1977
|
+
out->command = item.command;
|
|
1811
1978
|
return true;
|
|
1812
1979
|
}
|
|
1813
1980
|
}
|
|
@@ -1818,18 +1985,145 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1818
1985
|
if (item.separator || !item.enabled || item.key.empty()) continue;
|
|
1819
1986
|
if (item.key != key) continue;
|
|
1820
1987
|
if (!shortcutModifiersMatch(item.modifiers, allow_implicit_shift)) continue;
|
|
1821
|
-
|
|
1988
|
+
out->kind = kTrayAction;
|
|
1989
|
+
out->window_id = window->id;
|
|
1990
|
+
out->tray_item_id = item.id;
|
|
1991
|
+
/* A tray binding claims the key only while someone listens. */
|
|
1992
|
+
return host->callback != nullptr;
|
|
1822
1993
|
}
|
|
1823
1994
|
}
|
|
1824
1995
|
return false;
|
|
1825
1996
|
}
|
|
1826
1997
|
|
|
1998
|
+
static void emitShortcutMatch(Host *host, const ShortcutMatch &match) {
|
|
1999
|
+
if (!host || !host->callback) return;
|
|
2000
|
+
WindowsEvent event = {};
|
|
2001
|
+
event.kind = match.kind;
|
|
2002
|
+
if (match.kind == kShortcut) {
|
|
2003
|
+
event.window_id = match.window_id;
|
|
2004
|
+
event.shortcut_id = match.shortcut_id.c_str();
|
|
2005
|
+
event.shortcut_id_len = match.shortcut_id.size();
|
|
2006
|
+
event.shortcut_key = match.key.c_str();
|
|
2007
|
+
event.shortcut_key_len = match.key.size();
|
|
2008
|
+
event.shortcut_modifiers = match.modifiers;
|
|
2009
|
+
} else if (match.kind == kMenuCommand) {
|
|
2010
|
+
event.window_id = match.window_id;
|
|
2011
|
+
event.command_name = match.command.c_str();
|
|
2012
|
+
event.command_name_len = match.command.size();
|
|
2013
|
+
} else {
|
|
2014
|
+
event.tray_item_id = match.tray_item_id;
|
|
2015
|
+
}
|
|
2016
|
+
host->callback(host->callback_context, &event);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wparam) {
|
|
2020
|
+
ShortcutMatch match;
|
|
2021
|
+
if (!matchShortcutForWindow(host, window, wparam, &match)) return false;
|
|
2022
|
+
emitShortcutMatch(host, match);
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1827
2026
|
static bool emitShortcutForHwnd(Host *host, HWND hwnd, WPARAM wparam) {
|
|
1828
2027
|
return emitShortcutForWindow(host, windowForHwnd(host, hwnd), wparam);
|
|
1829
2028
|
}
|
|
1830
2029
|
|
|
1831
|
-
|
|
1832
|
-
|
|
2030
|
+
/* Marks a runtime dispatch on the stack for its duration. */
|
|
2031
|
+
struct RuntimeDispatchScope {
|
|
2032
|
+
Host *host;
|
|
2033
|
+
explicit RuntimeDispatchScope(Host *host_in) : host(host_in) { host->runtime_dispatch_depth += 1; }
|
|
2034
|
+
~RuntimeDispatchScope() { host->runtime_dispatch_depth -= 1; }
|
|
2035
|
+
RuntimeDispatchScope(const RuntimeDispatchScope &) = delete;
|
|
2036
|
+
RuntimeDispatchScope &operator=(const RuntimeDispatchScope &) = delete;
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
/* host->callback while the loop runs: every event the host emits enters
|
|
2040
|
+
* the runtime here. Once the shutdown is handed over, nothing else
|
|
2041
|
+
* reaches the app, as on macOS and Linux: an event raised by the stop
|
|
2042
|
+
* hook's own work, or by a dialog closing while a session end unwinds a
|
|
2043
|
+
* dispatch, would queue behind the shutdown and arrive after the hook. */
|
|
2044
|
+
static void dispatchEventToRuntime(void *context, const WindowsEvent *event) {
|
|
2045
|
+
Host *host = static_cast<Host *>(context);
|
|
2046
|
+
if (host->shutdown_delivered && event->kind != kShutdown) return;
|
|
2047
|
+
RuntimeDispatchScope scope(host);
|
|
2048
|
+
host->runtime_callback(host->runtime_context, event);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/* Marks a WebView2 callback on the stack for its duration. */
|
|
2052
|
+
struct WebViewCallbackScope {
|
|
2053
|
+
Host *host;
|
|
2054
|
+
explicit WebViewCallbackScope(Host *host_in) : host(host_in) { host->webview_callback_depth += 1; }
|
|
2055
|
+
~WebViewCallbackScope() { host->webview_callback_depth -= 1; }
|
|
2056
|
+
WebViewCallbackScope(const WebViewCallbackScope &) = delete;
|
|
2057
|
+
WebViewCallbackScope &operator=(const WebViewCallbackScope &) = delete;
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
/* Called from WebView2 callbacks: queue the event and make sure a
|
|
2061
|
+
* delivery is posted to the loop window. */
|
|
2062
|
+
static void queueWebViewEvent(Host *host, DeferredWebViewEvent event) {
|
|
2063
|
+
host->webview_events.push_back(std::move(event));
|
|
2064
|
+
if (host->webview_events_posted) return;
|
|
2065
|
+
HWND target = loopWindow(host);
|
|
2066
|
+
host->webview_events_posted = target && PostMessageW(target, kDeliverWebViewEventsMessage, 0, 0);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
static void deliverWebViewEvent(Host *host, const DeferredWebViewEvent &event) {
|
|
2070
|
+
switch (event.kind) {
|
|
2071
|
+
case DeferredWebViewEvent::Kind::Bridge: {
|
|
2072
|
+
/* Delivered even if the view closed after sending it: the
|
|
2073
|
+
* page sent the message while it was live, and a reply to a
|
|
2074
|
+
* view that is gone is dropped where it is sent. None goes
|
|
2075
|
+
* to an app whose shutdown was handed over. */
|
|
2076
|
+
if (!host->bridge_callback || host->shutdown_delivered) return;
|
|
2077
|
+
RuntimeDispatchScope scope(host);
|
|
2078
|
+
host->bridge_callback(
|
|
2079
|
+
host->bridge_context,
|
|
2080
|
+
event.window_id,
|
|
2081
|
+
event.label.c_str(),
|
|
2082
|
+
event.label.size(),
|
|
2083
|
+
event.message.c_str(),
|
|
2084
|
+
event.message.size(),
|
|
2085
|
+
event.origin.c_str(),
|
|
2086
|
+
event.origin.size());
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
case DeferredWebViewEvent::Kind::Focus: {
|
|
2090
|
+
/* Focus is state, not input: a view that is gone by now has
|
|
2091
|
+
* nothing to report. */
|
|
2092
|
+
if (!host->callback || host->webviews.find(event.view_key) == host->webviews.end()) return;
|
|
2093
|
+
WindowsEvent focused = {};
|
|
2094
|
+
focused.kind = kViewFocused;
|
|
2095
|
+
focused.window_id = event.window_id;
|
|
2096
|
+
focused.view_label = event.label.c_str();
|
|
2097
|
+
focused.view_label_len = event.label.size();
|
|
2098
|
+
host->callback(host->callback_context, &focused);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
case DeferredWebViewEvent::Kind::Shortcut:
|
|
2102
|
+
emitShortcutMatch(host, event.shortcut);
|
|
2103
|
+
return;
|
|
2104
|
+
case DeferredWebViewEvent::Kind::OpenExternal:
|
|
2105
|
+
/* The shell may show UI of its own (an app picker), which is
|
|
2106
|
+
* why this waits for the loop rather than running inside
|
|
2107
|
+
* NavigationStarting. */
|
|
2108
|
+
ShellExecuteW(nullptr, L"open", event.url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/* The loop window's kDeliverWebViewEventsMessage. A delivery that starts
|
|
2114
|
+
* while another is running (the app is inside a modal loop it opened from
|
|
2115
|
+
* an earlier event) leaves the queue to that outer delivery, so the app
|
|
2116
|
+
* sees these events one at a time and in order. */
|
|
2117
|
+
static void deliverWebViewEvents(Host *host) {
|
|
2118
|
+
host->webview_events_posted = false;
|
|
2119
|
+
if (host->delivering_webview_events) return;
|
|
2120
|
+
host->delivering_webview_events = true;
|
|
2121
|
+
while (!host->webview_events.empty()) {
|
|
2122
|
+
DeferredWebViewEvent event = std::move(host->webview_events.front());
|
|
2123
|
+
host->webview_events.pop_front();
|
|
2124
|
+
deliverWebViewEvent(host, event);
|
|
2125
|
+
}
|
|
2126
|
+
host->delivering_webview_events = false;
|
|
1833
2127
|
}
|
|
1834
2128
|
|
|
1835
2129
|
static std::string webViewKey(uint64_t window_id, const std::string &label) {
|
|
@@ -3957,8 +4251,7 @@ constexpr WPARAM kAudioNoteBufferingStarted = 6;
|
|
|
3957
4251
|
constexpr WPARAM kAudioNoteBufferingStopped = 7;
|
|
3958
4252
|
constexpr WPARAM kAudioNoteError = 8;
|
|
3959
4253
|
|
|
3960
|
-
/* Position tick on the
|
|
3961
|
-
* range; 500 ms is the shared coarse cadence (macOS and the null
|
|
4254
|
+
/* Position tick on the loop window, outside the app-timer id range; 500 ms is the shared coarse cadence (macOS and the null
|
|
3962
4255
|
* platform tick the same), so frame-clock scrubber interpolation
|
|
3963
4256
|
* behaves identically across hosts. */
|
|
3964
4257
|
constexpr UINT_PTR kAudioPositionTimerId = 0x3000;
|
|
@@ -4081,20 +4374,20 @@ struct AudioSessionEventForwarder final : public IMFAsyncCallback {
|
|
|
4081
4374
|
}
|
|
4082
4375
|
};
|
|
4083
4376
|
|
|
4084
|
-
/* Completes an asynchronous URL source resolution
|
|
4085
|
-
*
|
|
4086
|
-
*
|
|
4087
|
-
* mid-resolve
|
|
4377
|
+
/* Completes an asynchronous URL source resolution on a Media Foundation
|
|
4378
|
+
* worker thread. The source goes into its load's mailbox and a note goes
|
|
4379
|
+
* to the loop window; nothing here touches host state. A closed mailbox
|
|
4380
|
+
* (the playback was replaced or stopped mid-resolve) means the source is
|
|
4381
|
+
* shut down right here instead. */
|
|
4088
4382
|
struct AudioSourceResolveForwarder final : public IMFAsyncCallback {
|
|
4089
4383
|
LONG refs = 1;
|
|
4090
4384
|
IMFSourceResolver *resolver = nullptr;
|
|
4091
|
-
|
|
4092
|
-
std::shared_ptr<HostLifetime> lifetime;
|
|
4385
|
+
std::shared_ptr<AudioSourceMailbox> mailbox;
|
|
4093
4386
|
HWND hwnd = nullptr;
|
|
4094
4387
|
uint64_t generation = 0;
|
|
4095
4388
|
|
|
4096
|
-
AudioSourceResolveForwarder(IMFSourceResolver *resolver_in,
|
|
4097
|
-
: resolver(resolver_in),
|
|
4389
|
+
AudioSourceResolveForwarder(IMFSourceResolver *resolver_in, std::shared_ptr<AudioSourceMailbox> mailbox_in, HWND hwnd_in, uint64_t generation_in)
|
|
4390
|
+
: resolver(resolver_in), mailbox(std::move(mailbox_in)), hwnd(hwnd_in), generation(generation_in) {
|
|
4098
4391
|
resolver->AddRef();
|
|
4099
4392
|
}
|
|
4100
4393
|
~AudioSourceResolveForwarder() { resolver->Release(); }
|
|
@@ -4126,23 +4419,24 @@ struct AudioSourceResolveForwarder final : public IMFAsyncCallback {
|
|
|
4126
4419
|
IMFMediaSource *source = nullptr;
|
|
4127
4420
|
if (SUCCEEDED(hr) && object) object->QueryInterface(kNativeSdkIID_IMFMediaSource, reinterpret_cast<void **>(&source));
|
|
4128
4421
|
if (object) object->Release();
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4422
|
+
bool open = false;
|
|
4423
|
+
bool kept = false;
|
|
4424
|
+
{
|
|
4425
|
+
std::lock_guard<std::mutex> guard(mailbox->mutex);
|
|
4426
|
+
open = mailbox->open;
|
|
4427
|
+
if (open && source) {
|
|
4428
|
+
/* Reference transferred to the mailbox. */
|
|
4429
|
+
mailbox->source = source;
|
|
4430
|
+
kept = true;
|
|
4135
4431
|
}
|
|
4136
|
-
return S_OK;
|
|
4137
4432
|
}
|
|
4138
|
-
if (!source) {
|
|
4139
|
-
|
|
4140
|
-
|
|
4433
|
+
if (!kept && source) {
|
|
4434
|
+
source->Shutdown();
|
|
4435
|
+
source->Release();
|
|
4141
4436
|
}
|
|
4142
|
-
/*
|
|
4143
|
-
*
|
|
4144
|
-
|
|
4145
|
-
PostMessageW(hwnd, kAudioSessionMessage, kAudioNoteSourceResolved, (LPARAM)generation);
|
|
4437
|
+
/* A destroyed loop window makes this a harmless no-op; a kept
|
|
4438
|
+
* source then stays in the mailbox, which teardown closes. */
|
|
4439
|
+
if (open) PostMessageW(hwnd, kAudioSessionMessage, kept ? kAudioNoteSourceResolved : kAudioNoteSourceFailed, (LPARAM)generation);
|
|
4146
4440
|
return S_OK;
|
|
4147
4441
|
}
|
|
4148
4442
|
};
|
|
@@ -4316,7 +4610,7 @@ static void audioStopPositionTimer(Host *host) {
|
|
|
4316
4610
|
static void audioStartPositionTimer(Host *host) {
|
|
4317
4611
|
AudioState &audio = host->audio;
|
|
4318
4612
|
if (audio.position_timer_armed) return;
|
|
4319
|
-
HWND hwnd =
|
|
4613
|
+
HWND hwnd = loopWindow(host);
|
|
4320
4614
|
if (!hwnd) return;
|
|
4321
4615
|
if (SetTimer(hwnd, kAudioPositionTimerId, kAudioPositionIntervalMs, nullptr)) {
|
|
4322
4616
|
audio.timer_hwnd = hwnd;
|
|
@@ -4713,7 +5007,7 @@ static void audioSpectrumStartCapture(Host *host) {
|
|
|
4713
5007
|
AudioSpectrumState &spectrum = host->spectrum;
|
|
4714
5008
|
if (spectrum.shared) return;
|
|
4715
5009
|
if (!audioSpectrumSupported()) return;
|
|
4716
|
-
HWND hwnd =
|
|
5010
|
+
HWND hwnd = loopWindow(host);
|
|
4717
5011
|
if (!hwnd) return;
|
|
4718
5012
|
spectrum.generation += 1;
|
|
4719
5013
|
spectrum.shared = std::make_shared<AudioSpectrumShared>();
|
|
@@ -4899,7 +5193,6 @@ static void audioCaptureStopAll(Host *host) {
|
|
|
4899
5193
|
* letting a straggler finish installs the cache entry the completed
|
|
4900
5194
|
* play earned. */
|
|
4901
5195
|
static void audioReleaseSession(Host *host, bool cancel_download) {
|
|
4902
|
-
std::lock_guard<std::recursive_mutex> guard(host->lifetime->mutex);
|
|
4903
5196
|
AudioState &audio = host->audio;
|
|
4904
5197
|
audioStopPositionTimer(host);
|
|
4905
5198
|
audioSpectrumStopCapture(host);
|
|
@@ -4926,6 +5219,22 @@ static void audioReleaseSession(Host *host, bool cancel_download) {
|
|
|
4926
5219
|
audio.source->Release();
|
|
4927
5220
|
audio.source = nullptr;
|
|
4928
5221
|
}
|
|
5222
|
+
if (audio.resolve_mailbox) {
|
|
5223
|
+
/* Close the load's mailbox: a resolution still in flight now
|
|
5224
|
+
* shuts its source down itself, and one that already arrived but
|
|
5225
|
+
* was never collected is released here. */
|
|
5226
|
+
IMFMediaSource *uncollected = nullptr;
|
|
5227
|
+
{
|
|
5228
|
+
std::lock_guard<std::mutex> guard(audio.resolve_mailbox->mutex);
|
|
5229
|
+
audio.resolve_mailbox->open = false;
|
|
5230
|
+
uncollected = audio.resolve_mailbox->source;
|
|
5231
|
+
audio.resolve_mailbox->source = nullptr;
|
|
5232
|
+
}
|
|
5233
|
+
if (uncollected) {
|
|
5234
|
+
uncollected->Shutdown();
|
|
5235
|
+
uncollected->Release();
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
4929
5238
|
if (cancel_download && audio.download_cancel) audio.download_cancel->cancelled.store(true);
|
|
4930
5239
|
const uint64_t generation = audio.generation;
|
|
4931
5240
|
audio = AudioState{};
|
|
@@ -4940,7 +5249,7 @@ static bool audioAttachSession(Host *host) {
|
|
|
4940
5249
|
AudioState &audio = host->audio;
|
|
4941
5250
|
IMFMediaSource *source = audio.source;
|
|
4942
5251
|
if (!source) return false;
|
|
4943
|
-
HWND hwnd =
|
|
5252
|
+
HWND hwnd = loopWindow(host);
|
|
4944
5253
|
if (!hwnd) return false;
|
|
4945
5254
|
IMFMediaSession *session = nullptr;
|
|
4946
5255
|
if (FAILED(MFCreateMediaSession(nullptr, &session)) || !session) return false;
|
|
@@ -5105,7 +5414,7 @@ static int audioLoadUrlInternal(Host *host, const std::string &url, const std::s
|
|
|
5105
5414
|
}
|
|
5106
5415
|
}
|
|
5107
5416
|
if (!audioEnsureMediaFoundation()) return 2;
|
|
5108
|
-
HWND hwnd =
|
|
5417
|
+
HWND hwnd = loopWindow(host);
|
|
5109
5418
|
if (!hwnd) return 2;
|
|
5110
5419
|
IMFSourceResolver *resolver = nullptr;
|
|
5111
5420
|
if (FAILED(MFCreateSourceResolver(&resolver)) || !resolver) return 2;
|
|
@@ -5115,7 +5424,8 @@ static int audioLoadUrlInternal(Host *host, const std::string &url, const std::s
|
|
|
5115
5424
|
/* A fresh stream has no bytes yet: buffering starts true and drops
|
|
5116
5425
|
* when the session actually starts rolling. */
|
|
5117
5426
|
audio.buffering = true;
|
|
5118
|
-
|
|
5427
|
+
audio.resolve_mailbox = std::make_shared<AudioSourceMailbox>();
|
|
5428
|
+
AudioSourceResolveForwarder *forwarder = new AudioSourceResolveForwarder(resolver, audio.resolve_mailbox, hwnd, audio.generation);
|
|
5119
5429
|
const HRESULT hr = resolver->BeginCreateObjectFromURL(widen(url).c_str(), MF_RESOLUTION_MEDIASOURCE, nullptr, nullptr, forwarder, nullptr);
|
|
5120
5430
|
forwarder->Release();
|
|
5121
5431
|
resolver->Release();
|
|
@@ -5170,16 +5480,26 @@ static void audioFailed(Host *host) {
|
|
|
5170
5480
|
audioEmitReport(host, kAudioEventFailed, 0, 0, 0, 0);
|
|
5171
5481
|
}
|
|
5172
5482
|
|
|
5483
|
+
/* Loop thread, on kAudioNoteSourceResolved: take the resolved source
|
|
5484
|
+
* out of the current load's mailbox. */
|
|
5485
|
+
static bool audioCollectResolvedSource(Host *host) {
|
|
5486
|
+
AudioState &audio = host->audio;
|
|
5487
|
+
if (!audio.resolve_mailbox) return false;
|
|
5488
|
+
std::lock_guard<std::mutex> guard(audio.resolve_mailbox->mutex);
|
|
5489
|
+
audio.source = audio.resolve_mailbox->source;
|
|
5490
|
+
audio.resolve_mailbox->source = nullptr;
|
|
5491
|
+
return audio.source != nullptr;
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5173
5494
|
/* Distilled session notes, marshalled from Media Foundation worker
|
|
5174
5495
|
* threads via PostMessage; loop thread. Stale generations (a replaced
|
|
5175
5496
|
* or stopped playback's stragglers) are dropped here. */
|
|
5176
5497
|
static void audioHandleSessionMessage(Host *host, WPARAM note, LPARAM generation) {
|
|
5177
|
-
std::lock_guard<std::recursive_mutex> guard(host->lifetime->mutex);
|
|
5178
5498
|
AudioState &audio = host->audio;
|
|
5179
5499
|
if (!audio.active || (uint64_t)generation != audio.generation) return;
|
|
5180
5500
|
switch (note) {
|
|
5181
5501
|
case kAudioNoteSourceResolved:
|
|
5182
|
-
if (!audioAttachSession(host)) audioFailed(host);
|
|
5502
|
+
if (!audioCollectResolvedSource(host) || !audioAttachSession(host)) audioFailed(host);
|
|
5183
5503
|
break;
|
|
5184
5504
|
case kAudioNoteSourceFailed:
|
|
5185
5505
|
case kAudioNoteError:
|
|
@@ -5685,8 +6005,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5685
6005
|
[host, key, parent, lifetime, generation](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
|
|
5686
6006
|
auto token = lifetime.lock();
|
|
5687
6007
|
if (!token) return S_OK;
|
|
5688
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5689
6008
|
if (!token->alive) return S_OK;
|
|
6009
|
+
WebViewCallbackScope scope(host);
|
|
5690
6010
|
auto found = host->webviews.find(key);
|
|
5691
6011
|
if (found == host->webviews.end() || found->second.hwnd != parent || found->second.window_drag_generation != generation || !IsWindow(parent)) return S_OK;
|
|
5692
6012
|
if (FAILED(result) || !environment) {
|
|
@@ -5702,11 +6022,11 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5702
6022
|
if (controller) controller->Close();
|
|
5703
6023
|
return S_OK;
|
|
5704
6024
|
}
|
|
5705
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5706
6025
|
if (!token->alive) {
|
|
5707
6026
|
if (controller) controller->Close();
|
|
5708
6027
|
return S_OK;
|
|
5709
6028
|
}
|
|
6029
|
+
WebViewCallbackScope scope(host);
|
|
5710
6030
|
auto found = host->webviews.find(key);
|
|
5711
6031
|
if (found == host->webviews.end() || found->second.window_drag_generation != generation) {
|
|
5712
6032
|
if (controller) controller->Close();
|
|
@@ -5733,16 +6053,16 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5733
6053
|
[host, key, lifetime](ICoreWebView2Controller *, IUnknown *) -> HRESULT {
|
|
5734
6054
|
auto token = lifetime.lock();
|
|
5735
6055
|
if (!token) return S_OK;
|
|
5736
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5737
6056
|
if (!token->alive || !host->callback) return S_OK;
|
|
6057
|
+
WebViewCallbackScope scope(host);
|
|
5738
6058
|
auto focused = host->webviews.find(key);
|
|
5739
6059
|
if (focused == host->webviews.end()) return S_OK;
|
|
5740
|
-
|
|
5741
|
-
event.kind =
|
|
6060
|
+
DeferredWebViewEvent event;
|
|
6061
|
+
event.kind = DeferredWebViewEvent::Kind::Focus;
|
|
5742
6062
|
event.window_id = focused->second.window_id;
|
|
5743
|
-
event.
|
|
5744
|
-
event.
|
|
5745
|
-
|
|
6063
|
+
event.view_key = key;
|
|
6064
|
+
event.label = focused->second.label;
|
|
6065
|
+
queueWebViewEvent(host, std::move(event));
|
|
5746
6066
|
return S_OK;
|
|
5747
6067
|
}).Get(), &focus_token);
|
|
5748
6068
|
if (found->second.webview) {
|
|
@@ -5771,8 +6091,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5771
6091
|
[host, key, lifetime, drag_generation](HRESULT result, LPCWSTR) -> HRESULT {
|
|
5772
6092
|
auto token = lifetime.lock();
|
|
5773
6093
|
if (!token) return S_OK;
|
|
5774
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5775
6094
|
if (!token->alive) return S_OK;
|
|
6095
|
+
WebViewCallbackScope scope(host);
|
|
5776
6096
|
auto current = host->webviews.find(key);
|
|
5777
6097
|
if (current == host->webviews.end() || current->second.window_drag_generation != drag_generation) return S_OK;
|
|
5778
6098
|
if (FAILED(result)) {
|
|
@@ -5800,8 +6120,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5800
6120
|
[host, key, bridge_window_id, bridge_label, lifetime](ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *args) -> HRESULT {
|
|
5801
6121
|
auto token = lifetime.lock();
|
|
5802
6122
|
if (!token) return S_OK;
|
|
5803
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5804
6123
|
if (!token->alive || !args || !host->bridge_callback) return S_OK;
|
|
6124
|
+
WebViewCallbackScope scope(host);
|
|
5805
6125
|
|
|
5806
6126
|
LPWSTR message_bytes = nullptr;
|
|
5807
6127
|
if (FAILED(args->TryGetWebMessageAsString(&message_bytes)) || !message_bytes) return S_OK;
|
|
@@ -5821,15 +6141,14 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5821
6141
|
: bridgeOriginForWebViewUrl(source_webview->second, source_url);
|
|
5822
6142
|
}
|
|
5823
6143
|
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
origin.size());
|
|
6144
|
+
DeferredWebViewEvent event;
|
|
6145
|
+
event.kind = DeferredWebViewEvent::Kind::Bridge;
|
|
6146
|
+
event.window_id = bridge_window_id;
|
|
6147
|
+
event.view_key = key;
|
|
6148
|
+
event.label = bridge_label;
|
|
6149
|
+
event.message = std::move(message);
|
|
6150
|
+
event.origin = std::move(origin);
|
|
6151
|
+
queueWebViewEvent(host, std::move(event));
|
|
5833
6152
|
return S_OK;
|
|
5834
6153
|
}).Get(), &bridge_token);
|
|
5835
6154
|
}
|
|
@@ -5839,16 +6158,21 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5839
6158
|
[host, accelerator_window_id, lifetime](ICoreWebView2Controller *, ICoreWebView2AcceleratorKeyPressedEventArgs *args) -> HRESULT {
|
|
5840
6159
|
auto token = lifetime.lock();
|
|
5841
6160
|
if (!token) return S_OK;
|
|
5842
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5843
6161
|
if (!token->alive || !args) return S_OK;
|
|
6162
|
+
WebViewCallbackScope scope(host);
|
|
5844
6163
|
COREWEBVIEW2_KEY_EVENT_KIND kind = COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN;
|
|
5845
6164
|
if (FAILED(args->get_KeyEventKind(&kind))) return S_OK;
|
|
5846
6165
|
if (kind != COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN && kind != COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN) return S_OK;
|
|
5847
6166
|
UINT virtual_key = 0;
|
|
5848
6167
|
if (FAILED(args->get_VirtualKey(&virtual_key))) return S_OK;
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
6168
|
+
/* Handled is decided now, from the key state
|
|
6169
|
+
* of this keydown; the app hears it later. */
|
|
6170
|
+
DeferredWebViewEvent event;
|
|
6171
|
+
event.kind = DeferredWebViewEvent::Kind::Shortcut;
|
|
6172
|
+
if (!matchShortcutForWindow(host, windowForId(host, accelerator_window_id), virtual_key, &event.shortcut)) return S_OK;
|
|
6173
|
+
args->put_Handled(TRUE);
|
|
6174
|
+
event.window_id = accelerator_window_id;
|
|
6175
|
+
queueWebViewEvent(host, std::move(event));
|
|
5852
6176
|
return S_OK;
|
|
5853
6177
|
}).Get(), &accelerator_token);
|
|
5854
6178
|
|
|
@@ -5858,8 +6182,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5858
6182
|
[host, key, environment_ref, lifetime](ICoreWebView2 *, ICoreWebView2WebResourceRequestedEventArgs *args) -> HRESULT {
|
|
5859
6183
|
auto token = lifetime.lock();
|
|
5860
6184
|
if (!token) return S_OK;
|
|
5861
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5862
6185
|
if (!token->alive || !args) return S_OK;
|
|
6186
|
+
WebViewCallbackScope scope(host);
|
|
5863
6187
|
auto found = host->webviews.find(key);
|
|
5864
6188
|
if (found == host->webviews.end()) return S_OK;
|
|
5865
6189
|
ComPtr<ICoreWebView2WebResourceRequest> request;
|
|
@@ -5880,8 +6204,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5880
6204
|
[host, key, lifetime](ICoreWebView2 *, ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT {
|
|
5881
6205
|
auto token = lifetime.lock();
|
|
5882
6206
|
if (!token) return S_OK;
|
|
5883
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5884
6207
|
if (!token->alive) return S_OK;
|
|
6208
|
+
WebViewCallbackScope scope(host);
|
|
5885
6209
|
LPWSTR uri_bytes = nullptr;
|
|
5886
6210
|
if (!args || FAILED(args->get_Uri(&uri_bytes))) return S_OK;
|
|
5887
6211
|
std::wstring uri_wide = uri_bytes ? std::wstring(uri_bytes) : std::wstring();
|
|
@@ -5891,7 +6215,10 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5891
6215
|
if (found != host->webviews.end() && isInternalAssetUrl(found->second, uri)) return S_OK;
|
|
5892
6216
|
if (uri.empty() || uri.rfind("about:", 0) == 0 || policyListMatches(host->allowed_origins, uri)) return S_OK;
|
|
5893
6217
|
if (host->external_link_action == 1 && policyListMatches(host->allowed_external_urls, uri)) {
|
|
5894
|
-
|
|
6218
|
+
DeferredWebViewEvent event;
|
|
6219
|
+
event.kind = DeferredWebViewEvent::Kind::OpenExternal;
|
|
6220
|
+
event.url = uri_wide;
|
|
6221
|
+
queueWebViewEvent(host, std::move(event));
|
|
5895
6222
|
}
|
|
5896
6223
|
args->put_Cancel(TRUE);
|
|
5897
6224
|
return S_OK;
|
|
@@ -6078,6 +6405,145 @@ static void clampChromelessMaximizedFrame(Host *host, HWND hwnd, WINDOWPOS *pos)
|
|
|
6078
6405
|
pos->flags &= ~(SWP_NOMOVE | SWP_NOSIZE);
|
|
6079
6406
|
}
|
|
6080
6407
|
|
|
6408
|
+
/* The loop window's procedure: everything marshalled onto the message
|
|
6409
|
+
* loop lands here rather than on an app window, which may close at any
|
|
6410
|
+
* time. Posted messages reach it from nested modal loops (dialogs, menus,
|
|
6411
|
+
* the move/size loop) too, because those dispatch every window message of
|
|
6412
|
+
* the thread; a thread message (PostThreadMessage) would be dropped by
|
|
6413
|
+
* them, and a waitable handle would not be serviced by them at all. */
|
|
6414
|
+
static LRESULT CALLBACK loopWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
6415
|
+
if (message == WM_NCCREATE) {
|
|
6416
|
+
auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);
|
|
6417
|
+
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
|
6418
|
+
}
|
|
6419
|
+
Host *host = hostFromWindow(hwnd);
|
|
6420
|
+
switch (message) {
|
|
6421
|
+
case kWakeMessage:
|
|
6422
|
+
if (host && host->callback) {
|
|
6423
|
+
WindowsEvent wake = {};
|
|
6424
|
+
wake.kind = kWake;
|
|
6425
|
+
wake.window_id = 1;
|
|
6426
|
+
host->callback(host->callback_context, &wake);
|
|
6427
|
+
}
|
|
6428
|
+
return 0;
|
|
6429
|
+
case kRequestFrameMessage:
|
|
6430
|
+
if (host) {
|
|
6431
|
+
/* Cleared before emitting: a request made while this frame
|
|
6432
|
+
* runs posts again instead of folding into a finished one. */
|
|
6433
|
+
host->frame_request_pending.store(false, std::memory_order_release);
|
|
6434
|
+
if (host->callback) {
|
|
6435
|
+
WindowsEvent frame = {};
|
|
6436
|
+
frame.kind = kFrame;
|
|
6437
|
+
frame.window_id = 1;
|
|
6438
|
+
host->callback(host->callback_context, &frame);
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
return 0;
|
|
6442
|
+
case kAudioSessionMessage:
|
|
6443
|
+
if (host) audioHandleSessionMessage(host, wparam, lparam);
|
|
6444
|
+
return 0;
|
|
6445
|
+
case kAudioSpectrumMessage:
|
|
6446
|
+
if (host) audioHandleSpectrumMessage(host, wparam);
|
|
6447
|
+
return 0;
|
|
6448
|
+
case kDeliverWebViewEventsMessage:
|
|
6449
|
+
if (host) deliverWebViewEvents(host);
|
|
6450
|
+
return 0;
|
|
6451
|
+
case WM_TIMER:
|
|
6452
|
+
if (host && handleAppTimerMessage(host, wparam)) return 0;
|
|
6453
|
+
if (host && handleAudioTimerMessage(host, wparam)) return 0;
|
|
6454
|
+
return 0;
|
|
6455
|
+
default:
|
|
6456
|
+
break;
|
|
6457
|
+
}
|
|
6458
|
+
return DefWindowProcW(hwnd, message, wparam, lparam);
|
|
6459
|
+
}
|
|
6460
|
+
|
|
6461
|
+
static HWND createLoopWindow(Host *host) {
|
|
6462
|
+
static bool registered = false;
|
|
6463
|
+
if (!registered) {
|
|
6464
|
+
WNDCLASSEXW wc = {};
|
|
6465
|
+
wc.cbSize = sizeof(wc);
|
|
6466
|
+
wc.lpfnWndProc = loopWindowProc;
|
|
6467
|
+
wc.hInstance = host->instance;
|
|
6468
|
+
wc.lpszClassName = L"NativeSdkLoopWindow";
|
|
6469
|
+
registered = RegisterClassExW(&wc) != 0;
|
|
6470
|
+
}
|
|
6471
|
+
return CreateWindowExW(0, L"NativeSdkLoopWindow", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, host->instance, host);
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
/* Deliver the app's shutdown at most once: the run loop's exit (a
|
|
6475
|
+
* last-window close, quitApp) and a session end (endSession) both lead
|
|
6476
|
+
* here, and the runtime's stop hook must run exactly once. */
|
|
6477
|
+
static void deliverShutdown(Host *host) {
|
|
6478
|
+
if (host->shutdown_delivered || !host->callback) return;
|
|
6479
|
+
host->shutdown_delivered = true;
|
|
6480
|
+
WindowsEvent shutdown = {};
|
|
6481
|
+
shutdown.kind = kShutdown;
|
|
6482
|
+
shutdown.window_id = 1;
|
|
6483
|
+
host->callback(host->callback_context, &shutdown);
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
/* Cancel a dialog the app's windows own, directly or through another
|
|
6487
|
+
* dialog (the overwrite prompt a save dialog raises): its modal loop
|
|
6488
|
+
* holds the dispatch that opened it. The enumeration runs top of the
|
|
6489
|
+
* z-order first, so an inner dialog closes before its owner. */
|
|
6490
|
+
static BOOL CALLBACK closeOwnedDialog(HWND hwnd, LPARAM lparam) {
|
|
6491
|
+
Host *host = reinterpret_cast<Host *>(lparam);
|
|
6492
|
+
if (!IsWindowVisible(hwnd) || windowForHwnd(host, hwnd)) return TRUE;
|
|
6493
|
+
for (HWND owner = GetWindow(hwnd, GW_OWNER); owner; owner = GetWindow(owner, GW_OWNER)) {
|
|
6494
|
+
if (!windowForHwnd(host, owner)) continue;
|
|
6495
|
+
PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
|
6496
|
+
break;
|
|
6497
|
+
}
|
|
6498
|
+
return TRUE;
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6501
|
+
/* A confirmed session end (WM_ENDSESSION, wParam TRUE): logoff, system
|
|
6502
|
+
* shutdown or restart, or a Restart Manager close (ENDSESSION_CLOSEAPP)
|
|
6503
|
+
* from an installer that needs the app's files. It is not the user's
|
|
6504
|
+
* close, so close_policy does not apply: a window hidden behind the
|
|
6505
|
+
* tray gets the message like a visible one.
|
|
6506
|
+
*
|
|
6507
|
+
* Every top-level window receives the message, so only the first acts,
|
|
6508
|
+
* and a quit whose shutdown already ran finishes its own teardown.
|
|
6509
|
+
*
|
|
6510
|
+
* Outside any runtime dispatch (the common case) it delivers the same
|
|
6511
|
+
* shutdown event a quit does, synchronously: at logoff or shutdown
|
|
6512
|
+
* Windows may end the process any time after the message returns, so
|
|
6513
|
+
* the stop hook, and the persistence flush and journal seal that run in
|
|
6514
|
+
* the shutdown dispatch, finish first. The process then ends here
|
|
6515
|
+
* instead of returning to the loop. Restart Manager waits for it to
|
|
6516
|
+
* exit and sends WM_CLOSE as soon as this returns, and Windows considers
|
|
6517
|
+
* a process that hides or destroys its windows ready to be killed, so
|
|
6518
|
+
* the regular teardown would stop at an arbitrary point anyway.
|
|
6519
|
+
* Chromium (SessionEnding), Electron, and tao end the process the same
|
|
6520
|
+
* way. The tray icon goes first: the shell keeps a dead process's icon
|
|
6521
|
+
* until the pointer crosses it, and after a Restart Manager close the
|
|
6522
|
+
* session goes on.
|
|
6523
|
+
*
|
|
6524
|
+
* Inside a runtime dispatch (a dialog the app opened is in its modal
|
|
6525
|
+
* loop) the runtime queues the shutdown until that dispatch commits, to
|
|
6526
|
+
* keep events in causal order, so ending the process here would lose
|
|
6527
|
+
* the stop hook. The host lets the dispatch unwind instead: it cancels
|
|
6528
|
+
* the dialogs the app's windows own, ignores further closes, and
|
|
6529
|
+
* returns. The dispatch finishes, the queued shutdown runs the stop
|
|
6530
|
+
* hook, and the loop exits into the regular teardown. A Restart Manager
|
|
6531
|
+
* close waits for that; at logoff or shutdown it races the session
|
|
6532
|
+
* ending after this returns. */
|
|
6533
|
+
static void endSession(Host *host) {
|
|
6534
|
+
if (host->shutdown_delivered) return;
|
|
6535
|
+
if (host->runtime_dispatch_depth == 0) {
|
|
6536
|
+
deliverShutdown(host);
|
|
6537
|
+
removeNotificationIcon(host);
|
|
6538
|
+
TerminateProcess(GetCurrentProcess(), 0);
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
host->session_ending = true;
|
|
6542
|
+
deliverShutdown(host);
|
|
6543
|
+
host->running = false;
|
|
6544
|
+
EnumThreadWindows(GetCurrentThreadId(), closeOwnedDialog, reinterpret_cast<LPARAM>(host));
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6081
6547
|
static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
6082
6548
|
if (message == WM_NCCREATE) {
|
|
6083
6549
|
auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);
|
|
@@ -6210,28 +6676,6 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6210
6676
|
}
|
|
6211
6677
|
}
|
|
6212
6678
|
switch (message) {
|
|
6213
|
-
case kWakeMessage:
|
|
6214
|
-
if (host) {
|
|
6215
|
-
WindowsEvent wake = {};
|
|
6216
|
-
wake.kind = kWake;
|
|
6217
|
-
wake.window_id = 1;
|
|
6218
|
-
if (host->callback) host->callback(host->callback_context, &wake);
|
|
6219
|
-
}
|
|
6220
|
-
return 0;
|
|
6221
|
-
case kRequestFrameMessage:
|
|
6222
|
-
if (host) {
|
|
6223
|
-
WindowsEvent frame = {};
|
|
6224
|
-
frame.kind = kFrame;
|
|
6225
|
-
frame.window_id = 1;
|
|
6226
|
-
if (host->callback) host->callback(host->callback_context, &frame);
|
|
6227
|
-
}
|
|
6228
|
-
return 0;
|
|
6229
|
-
case kAudioSessionMessage:
|
|
6230
|
-
if (host) audioHandleSessionMessage(host, wparam, lparam);
|
|
6231
|
-
return 0;
|
|
6232
|
-
case kAudioSpectrumMessage:
|
|
6233
|
-
if (host) audioHandleSpectrumMessage(host, wparam);
|
|
6234
|
-
return 0;
|
|
6235
6679
|
case kShowContextMenuMessage:
|
|
6236
6680
|
if (host) showAppContextMenu(host, hwnd);
|
|
6237
6681
|
return 0;
|
|
@@ -6415,8 +6859,6 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6415
6859
|
}
|
|
6416
6860
|
break;
|
|
6417
6861
|
case WM_TIMER:
|
|
6418
|
-
if (host && handleAppTimerMessage(host, wparam)) return 0;
|
|
6419
|
-
if (host && handleAudioTimerMessage(host, wparam)) return 0;
|
|
6420
6862
|
if (host && wparam == kFrameTimerId) {
|
|
6421
6863
|
for (auto &entry : host->windows) {
|
|
6422
6864
|
if (entry.second.hwnd == hwnd) {
|
|
@@ -6472,7 +6914,21 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6472
6914
|
case WM_SYSCOLORCHANGE:
|
|
6473
6915
|
if (host) emitAppearanceIfChanged(host, false);
|
|
6474
6916
|
break;
|
|
6917
|
+
case WM_QUERYENDSESSION:
|
|
6918
|
+
/* Always yes: the SDK has no quit veto (close_policy governs
|
|
6919
|
+
* the user's close only). Nothing happens until WM_ENDSESSION
|
|
6920
|
+
* confirms the end; another application may still cancel. */
|
|
6921
|
+
return TRUE;
|
|
6922
|
+
case WM_ENDSESSION:
|
|
6923
|
+
/* wParam FALSE: the session goes on. Nothing was done at the
|
|
6924
|
+
* query, so there is nothing to undo. */
|
|
6925
|
+
if (host && wparam) endSession(host);
|
|
6926
|
+
return 0;
|
|
6475
6927
|
case WM_CLOSE:
|
|
6928
|
+
/* A session end is unwinding a dispatch (endSession): the
|
|
6929
|
+
* loop exits once it does, so Restart Manager's follow-up
|
|
6930
|
+
* close neither hides nor destroys anything. */
|
|
6931
|
+
if (host && host->session_ending) return 0;
|
|
6476
6932
|
/* close_policy .hide: the user's close (caption X, Alt+F4)
|
|
6477
6933
|
* hides the window instead of destroying it — the hwnd and
|
|
6478
6934
|
* every view stay live, the app keeps running behind its
|
|
@@ -6511,6 +6967,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6511
6967
|
if (host) {
|
|
6512
6968
|
for (auto &entry : host->windows) {
|
|
6513
6969
|
if (entry.second.hwnd == hwnd) {
|
|
6970
|
+
cancelDialogsForWindow(host, entry.first);
|
|
6514
6971
|
destroyNativeViewsForWindow(host, entry.first);
|
|
6515
6972
|
destroyChildWebViewsForWindow(host, entry.first);
|
|
6516
6973
|
entry.second.hwnd = nullptr;
|
|
@@ -6652,6 +7109,327 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6652
7109
|
return true;
|
|
6653
7110
|
}
|
|
6654
7111
|
|
|
7112
|
+
/* ------------------------------------------------------------- dialogs */
|
|
7113
|
+
|
|
7114
|
+
static DialogRequest openDialogSpec(const WindowsOpenDialogOpts *opts) {
|
|
7115
|
+
DialogRequest spec;
|
|
7116
|
+
spec.kind = kDialogOpen;
|
|
7117
|
+
spec.title = slice(opts->title, opts->title_len);
|
|
7118
|
+
spec.default_path = slice(opts->default_path, opts->default_path_len);
|
|
7119
|
+
spec.extensions = slice(opts->extensions, opts->extensions_len);
|
|
7120
|
+
spec.allow_directories = opts->allow_directories != 0;
|
|
7121
|
+
spec.allow_multiple = opts->allow_multiple != 0;
|
|
7122
|
+
return spec;
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7125
|
+
static DialogRequest saveDialogSpec(const WindowsSaveDialogOpts *opts) {
|
|
7126
|
+
DialogRequest spec;
|
|
7127
|
+
spec.kind = kDialogSave;
|
|
7128
|
+
spec.title = slice(opts->title, opts->title_len);
|
|
7129
|
+
spec.default_path = slice(opts->default_path, opts->default_path_len);
|
|
7130
|
+
spec.default_name = slice(opts->default_name, opts->default_name_len);
|
|
7131
|
+
spec.extensions = slice(opts->extensions, opts->extensions_len);
|
|
7132
|
+
return spec;
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
static DialogRequest messageDialogSpec(const WindowsMessageDialogOpts *opts) {
|
|
7136
|
+
DialogRequest spec;
|
|
7137
|
+
spec.kind = kDialogMessage;
|
|
7138
|
+
spec.style = opts->style;
|
|
7139
|
+
spec.title = slice(opts->title, opts->title_len);
|
|
7140
|
+
spec.message = slice(opts->message, opts->message_len);
|
|
7141
|
+
spec.informative_text = slice(opts->informative_text, opts->informative_text_len);
|
|
7142
|
+
spec.primary_button = slice(opts->primary_button, opts->primary_button_len);
|
|
7143
|
+
spec.secondary_button = slice(opts->secondary_button, opts->secondary_button_len);
|
|
7144
|
+
spec.tertiary_button = slice(opts->tertiary_button, opts->tertiary_button_len);
|
|
7145
|
+
return spec;
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7148
|
+
/* Show an open dialog modally on the calling thread, which has COM
|
|
7149
|
+
* initialized. S_OK with the chosen paths, or the failure Show reported
|
|
7150
|
+
* (HRESULT_FROM_WIN32(ERROR_CANCELLED) when the user cancelled). */
|
|
7151
|
+
static HRESULT showOpenDialogOn(HWND owner, const DialogRequest &spec, std::vector<std::wstring> *paths) {
|
|
7152
|
+
IFileOpenDialog *dialog = nullptr;
|
|
7153
|
+
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
|
|
7154
|
+
if (FAILED(hr) || !dialog) return FAILED(hr) ? hr : E_FAIL;
|
|
7155
|
+
|
|
7156
|
+
DWORD options = 0;
|
|
7157
|
+
if (SUCCEEDED(dialog->GetOptions(&options))) {
|
|
7158
|
+
options |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
|
|
7159
|
+
if (spec.allow_directories) options |= FOS_PICKFOLDERS;
|
|
7160
|
+
else options |= FOS_FILEMUSTEXIST;
|
|
7161
|
+
if (spec.allow_multiple) options |= FOS_ALLOWMULTISELECT;
|
|
7162
|
+
dialog->SetOptions(options);
|
|
7163
|
+
}
|
|
7164
|
+
setDialogTitle(dialog, spec.title.data(), spec.title.size());
|
|
7165
|
+
setDialogFolder(dialog, spec.default_path.data(), spec.default_path.size());
|
|
7166
|
+
if (!spec.allow_directories) setDialogFilters(dialog, spec.extensions.data(), spec.extensions.size());
|
|
7167
|
+
|
|
7168
|
+
hr = dialog->Show(owner);
|
|
7169
|
+
if (SUCCEEDED(hr)) {
|
|
7170
|
+
IShellItemArray *items = nullptr;
|
|
7171
|
+
if (SUCCEEDED(dialog->GetResults(&items)) && items) {
|
|
7172
|
+
DWORD count = 0;
|
|
7173
|
+
items->GetCount(&count);
|
|
7174
|
+
for (DWORD index = 0; index < count; ++index) {
|
|
7175
|
+
IShellItem *item = nullptr;
|
|
7176
|
+
if (SUCCEEDED(items->GetItemAt(index, &item)) && item) {
|
|
7177
|
+
PWSTR path = nullptr;
|
|
7178
|
+
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
|
|
7179
|
+
paths->push_back(std::wstring(path));
|
|
7180
|
+
CoTaskMemFree(path);
|
|
7181
|
+
}
|
|
7182
|
+
item->Release();
|
|
7183
|
+
}
|
|
7184
|
+
}
|
|
7185
|
+
items->Release();
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
dialog->Release();
|
|
7189
|
+
return hr;
|
|
7190
|
+
}
|
|
7191
|
+
|
|
7192
|
+
/* The save dialog's counterpart of showOpenDialogOn. */
|
|
7193
|
+
static HRESULT showSaveDialogOn(HWND owner, const DialogRequest &spec, std::wstring *path) {
|
|
7194
|
+
IFileSaveDialog *dialog = nullptr;
|
|
7195
|
+
HRESULT hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
|
|
7196
|
+
if (FAILED(hr) || !dialog) return FAILED(hr) ? hr : E_FAIL;
|
|
7197
|
+
|
|
7198
|
+
DWORD options = 0;
|
|
7199
|
+
if (SUCCEEDED(dialog->GetOptions(&options))) dialog->SetOptions(options | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
|
7200
|
+
setDialogTitle(dialog, spec.title.data(), spec.title.size());
|
|
7201
|
+
setDialogFolder(dialog, spec.default_path.data(), spec.default_path.size());
|
|
7202
|
+
setDialogFilters(dialog, spec.extensions.data(), spec.extensions.size());
|
|
7203
|
+
std::wstring default_name = widen(spec.default_name);
|
|
7204
|
+
if (!default_name.empty()) dialog->SetFileName(default_name.c_str());
|
|
7205
|
+
|
|
7206
|
+
hr = dialog->Show(owner);
|
|
7207
|
+
if (SUCCEEDED(hr)) {
|
|
7208
|
+
IShellItem *item = nullptr;
|
|
7209
|
+
if (SUCCEEDED(dialog->GetResult(&item)) && item) {
|
|
7210
|
+
PWSTR chosen = nullptr;
|
|
7211
|
+
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &chosen)) && chosen) {
|
|
7212
|
+
*path = std::wstring(chosen);
|
|
7213
|
+
CoTaskMemFree(chosen);
|
|
7214
|
+
}
|
|
7215
|
+
item->Release();
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
dialog->Release();
|
|
7219
|
+
return hr;
|
|
7220
|
+
}
|
|
7221
|
+
|
|
7222
|
+
/* TaskDialogIndirect is a comctl32 v6 export: it only resolves when the
|
|
7223
|
+
* process activates the v6 side-by-side assembly (application manifest).
|
|
7224
|
+
* A static import would abort the whole process at load time with
|
|
7225
|
+
* STATUS_ENTRYPOINT_NOT_FOUND on the system default v5, so it is resolved
|
|
7226
|
+
* dynamically, and null sends callers to MessageBoxW (fixed button
|
|
7227
|
+
* captions, same 0/1/2 result contract). */
|
|
7228
|
+
using TaskDialogIndirectFn = HRESULT(WINAPI *)(const TASKDIALOGCONFIG *, int *, int *, BOOL *);
|
|
7229
|
+
|
|
7230
|
+
static TaskDialogIndirectFn taskDialogIndirect() {
|
|
7231
|
+
static TaskDialogIndirectFn task_dialog = reinterpret_cast<TaskDialogIndirectFn>(
|
|
7232
|
+
reinterpret_cast<void *>(GetProcAddress(GetModuleHandleW(L"comctl32.dll"), "TaskDialogIndirect")));
|
|
7233
|
+
return task_dialog;
|
|
7234
|
+
}
|
|
7235
|
+
|
|
7236
|
+
/* Run a message dialog as a task dialog. *pressed is 100, 101 or 102 for
|
|
7237
|
+
* the primary, secondary or tertiary button, and IDCANCEL when it was
|
|
7238
|
+
* closed without one (Esc, the close button, WM_CLOSE). */
|
|
7239
|
+
static HRESULT runTaskDialog(TaskDialogIndirectFn task_dialog, HWND owner, const DialogRequest &spec, int *pressed) {
|
|
7240
|
+
std::wstring title = widen(spec.title);
|
|
7241
|
+
std::wstring message = widen(spec.message);
|
|
7242
|
+
std::wstring informative = widen(spec.informative_text);
|
|
7243
|
+
std::wstring primary = widen(spec.primary_button);
|
|
7244
|
+
std::wstring secondary = widen(spec.secondary_button);
|
|
7245
|
+
std::wstring tertiary = widen(spec.tertiary_button);
|
|
7246
|
+
if (primary.empty()) primary = L"OK";
|
|
7247
|
+
|
|
7248
|
+
TASKDIALOG_BUTTON buttons[3] = {};
|
|
7249
|
+
int button_count = 0;
|
|
7250
|
+
buttons[button_count++] = { 100, primary.c_str() };
|
|
7251
|
+
if (!secondary.empty()) buttons[button_count++] = { 101, secondary.c_str() };
|
|
7252
|
+
if (!tertiary.empty()) buttons[button_count++] = { 102, tertiary.c_str() };
|
|
7253
|
+
|
|
7254
|
+
TASKDIALOGCONFIG config = {};
|
|
7255
|
+
config.cbSize = sizeof(config);
|
|
7256
|
+
config.hwndParent = owner;
|
|
7257
|
+
config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION;
|
|
7258
|
+
config.pszWindowTitle = title.empty() ? L"native-sdk" : title.c_str();
|
|
7259
|
+
config.pszMainInstruction = message.empty() ? config.pszWindowTitle : message.c_str();
|
|
7260
|
+
config.pszContent = informative.empty() ? nullptr : informative.c_str();
|
|
7261
|
+
config.cButtons = static_cast<UINT>(button_count);
|
|
7262
|
+
config.pButtons = buttons;
|
|
7263
|
+
config.nDefaultButton = 100;
|
|
7264
|
+
config.pszMainIcon = spec.style == 2 ? TD_ERROR_ICON : (spec.style == 1 ? TD_WARNING_ICON : TD_INFORMATION_ICON);
|
|
7265
|
+
*pressed = 100;
|
|
7266
|
+
return task_dialog(&config, pressed, nullptr, nullptr);
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
/* The comctl32 v5 fallback: MessageBoxW, returning its IDOK-style code
|
|
7270
|
+
* (0 when it failed). messageBoxButton maps that to 0/1/2. */
|
|
7271
|
+
static int runMessageBox(HWND owner, const DialogRequest &spec) {
|
|
7272
|
+
const int button_count = 1 + (spec.secondary_button.empty() ? 0 : 1) + (spec.tertiary_button.empty() ? 0 : 1);
|
|
7273
|
+
UINT type = MB_OK;
|
|
7274
|
+
if (button_count == 2) type = MB_OKCANCEL;
|
|
7275
|
+
if (button_count == 3) type = MB_YESNOCANCEL;
|
|
7276
|
+
type |= spec.style == 2 ? MB_ICONERROR : (spec.style == 1 ? MB_ICONWARNING : MB_ICONINFORMATION);
|
|
7277
|
+
std::wstring title = widen(spec.title);
|
|
7278
|
+
std::wstring text = widen(spec.message);
|
|
7279
|
+
std::wstring informative = widen(spec.informative_text);
|
|
7280
|
+
if (!informative.empty()) {
|
|
7281
|
+
if (!text.empty()) text += L"\n\n";
|
|
7282
|
+
text += informative;
|
|
7283
|
+
}
|
|
7284
|
+
return MessageBoxW(owner, text.c_str(), title.empty() ? L"native-sdk" : title.c_str(), type);
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
static int messageBoxButton(const DialogRequest &spec, int result) {
|
|
7288
|
+
if (!spec.secondary_button.empty() && !spec.tertiary_button.empty()) {
|
|
7289
|
+
if (result == IDNO) return 1;
|
|
7290
|
+
if (result == IDCANCEL) return 2;
|
|
7291
|
+
return 0;
|
|
7292
|
+
}
|
|
7293
|
+
if (result == IDCANCEL) return 1;
|
|
7294
|
+
return 0;
|
|
7295
|
+
}
|
|
7296
|
+
|
|
7297
|
+
/* The request a dialog thread is running, for dialogCreateHook. */
|
|
7298
|
+
thread_local DialogRequest *t_dialog_request = nullptr;
|
|
7299
|
+
thread_local DialogQueue *t_dialog_queue = nullptr;
|
|
7300
|
+
|
|
7301
|
+
/* A dialog thread's WH_CALLWNDPROC hook: a dialog the app cancelled
|
|
7302
|
+
* before its window existed closes as soon as that window is set up. A
|
|
7303
|
+
* cancel after this point finds the window (cancelDialog). */
|
|
7304
|
+
static LRESULT CALLBACK dialogCreateHook(int code, WPARAM wparam, LPARAM lparam) {
|
|
7305
|
+
if (code == HC_ACTION && t_dialog_request && t_dialog_queue) {
|
|
7306
|
+
const CWPSTRUCT *sent = reinterpret_cast<const CWPSTRUCT *>(lparam);
|
|
7307
|
+
if (sent->message == WM_INITDIALOG && (GetWindowLongPtrW(sent->hwnd, GWL_STYLE) & WS_CHILD) == 0) {
|
|
7308
|
+
std::lock_guard<std::mutex> lock(t_dialog_queue->mutex);
|
|
7309
|
+
if (t_dialog_request->cancel_requested) PostMessageW(sent->hwnd, WM_CLOSE, 0, 0);
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
return CallNextHookEx(nullptr, code, wparam, lparam);
|
|
7313
|
+
}
|
|
7314
|
+
|
|
7315
|
+
static void runDialogRequest(DialogRequest &request) {
|
|
7316
|
+
if (request.kind == kDialogOpen || request.kind == kDialogSave) {
|
|
7317
|
+
std::vector<std::wstring> paths;
|
|
7318
|
+
HRESULT hr = S_OK;
|
|
7319
|
+
if (request.kind == kDialogOpen) {
|
|
7320
|
+
hr = showOpenDialogOn(request.owner, request, &paths);
|
|
7321
|
+
} else {
|
|
7322
|
+
std::wstring path;
|
|
7323
|
+
hr = showSaveDialogOn(request.owner, request, &path);
|
|
7324
|
+
if (!path.empty()) paths.push_back(path);
|
|
7325
|
+
}
|
|
7326
|
+
for (const std::wstring &path : paths) {
|
|
7327
|
+
std::string utf8 = narrow(path);
|
|
7328
|
+
if (!utf8.empty()) request.paths.push_back(utf8);
|
|
7329
|
+
}
|
|
7330
|
+
if (SUCCEEDED(hr)) {
|
|
7331
|
+
request.outcome = request.paths.empty() ? kDialogCancelled : kDialogSelected;
|
|
7332
|
+
} else {
|
|
7333
|
+
request.outcome = hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ? kDialogCancelled : kDialogFailed;
|
|
7334
|
+
}
|
|
7335
|
+
return;
|
|
7336
|
+
}
|
|
7337
|
+
if (TaskDialogIndirectFn task_dialog = taskDialogIndirect()) {
|
|
7338
|
+
int pressed = 100;
|
|
7339
|
+
const HRESULT hr = runTaskDialog(task_dialog, request.owner, request, &pressed);
|
|
7340
|
+
if (FAILED(hr)) {
|
|
7341
|
+
request.outcome = kDialogFailed;
|
|
7342
|
+
} else if (pressed >= 100 && pressed <= 102) {
|
|
7343
|
+
request.outcome = kDialogSelected;
|
|
7344
|
+
request.button = pressed - 100;
|
|
7345
|
+
} else {
|
|
7346
|
+
request.outcome = kDialogCancelled;
|
|
7347
|
+
}
|
|
7348
|
+
return;
|
|
7349
|
+
}
|
|
7350
|
+
const int result = runMessageBox(request.owner, request);
|
|
7351
|
+
if (result == 0) {
|
|
7352
|
+
request.outcome = kDialogFailed;
|
|
7353
|
+
} else {
|
|
7354
|
+
// A message box closed by WM_CLOSE reports a button anyway; one
|
|
7355
|
+
// the app cancelled is no answer.
|
|
7356
|
+
std::lock_guard<std::mutex> lock(t_dialog_queue->mutex);
|
|
7357
|
+
request.outcome = request.cancel_requested ? kDialogCancelled : kDialogSelected;
|
|
7358
|
+
request.button = messageBoxButton(request, result);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
/* A dialog thread: a single-threaded apartment of its own, running the
|
|
7363
|
+
* dialog modally, then queueing the result and waking the loop. */
|
|
7364
|
+
static void runDialogThread(std::shared_ptr<DialogQueue> queue, std::shared_ptr<DialogRequest> request) {
|
|
7365
|
+
bool cancel_requested = false;
|
|
7366
|
+
{
|
|
7367
|
+
std::lock_guard<std::mutex> lock(queue->mutex);
|
|
7368
|
+
request->thread_id = GetCurrentThreadId();
|
|
7369
|
+
cancel_requested = request->cancel_requested;
|
|
7370
|
+
}
|
|
7371
|
+
if (!cancel_requested) {
|
|
7372
|
+
bool uninitialize = false;
|
|
7373
|
+
if (initializeCom(&uninitialize)) {
|
|
7374
|
+
t_dialog_request = request.get();
|
|
7375
|
+
t_dialog_queue = queue.get();
|
|
7376
|
+
HHOOK hook = SetWindowsHookExW(WH_CALLWNDPROC, dialogCreateHook, nullptr, GetCurrentThreadId());
|
|
7377
|
+
runDialogRequest(*request);
|
|
7378
|
+
if (hook) UnhookWindowsHookEx(hook);
|
|
7379
|
+
t_dialog_request = nullptr;
|
|
7380
|
+
t_dialog_queue = nullptr;
|
|
7381
|
+
finishCom(uninitialize);
|
|
7382
|
+
} else {
|
|
7383
|
+
request->outcome = kDialogFailed;
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7386
|
+
std::lock_guard<std::mutex> lock(queue->mutex);
|
|
7387
|
+
request->thread_id = 0;
|
|
7388
|
+
queue->finished.push_back(request);
|
|
7389
|
+
if (queue->loop_window) PostMessageW(queue->loop_window, kWakeMessage, 0, 0);
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
/* Posted to every dialog window of a dialog thread, the top one first,
|
|
7393
|
+
* so a save dialog's overwrite prompt closes before the dialog that
|
|
7394
|
+
* raised it. Other top-level windows of the thread (COM's) are left
|
|
7395
|
+
* alone. */
|
|
7396
|
+
static BOOL CALLBACK closeDialogWindow(HWND hwnd, LPARAM lparam) {
|
|
7397
|
+
(void)lparam;
|
|
7398
|
+
wchar_t class_name[8] = {};
|
|
7399
|
+
if (GetClassNameW(hwnd, class_name, 8) > 0 && wcscmp(class_name, L"#32770") == 0) PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
|
7400
|
+
return TRUE;
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7403
|
+
/* Close a dialog early; it reports through its thread like any other
|
|
7404
|
+
* finish. One whose window does not exist yet closes from
|
|
7405
|
+
* dialogCreateHook, and one whose thread has not started never shows. */
|
|
7406
|
+
static void cancelDialog(Host *host, DialogRequest &dialog) {
|
|
7407
|
+
DWORD thread_id = 0;
|
|
7408
|
+
{
|
|
7409
|
+
std::lock_guard<std::mutex> lock(host->dialog_queue->mutex);
|
|
7410
|
+
dialog.cancel_requested = true;
|
|
7411
|
+
thread_id = dialog.thread_id;
|
|
7412
|
+
}
|
|
7413
|
+
if (thread_id != 0) EnumThreadWindows(thread_id, closeDialogWindow, 0);
|
|
7414
|
+
}
|
|
7415
|
+
|
|
7416
|
+
/* A closing window takes its dialogs down with it. */
|
|
7417
|
+
static void cancelDialogsForWindow(Host *host, uint64_t window_id) {
|
|
7418
|
+
for (auto &entry : host->dialogs) {
|
|
7419
|
+
if (entry.second->parent_window_id == window_id) cancelDialog(host, *entry.second);
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
|
|
7423
|
+
/* Host teardown: every dialog closes, and the threads finish without the
|
|
7424
|
+
* loop. Results already queued are dropped with the queue. */
|
|
7425
|
+
static void cancelAllDialogs(Host *host) {
|
|
7426
|
+
for (auto &entry : host->dialogs) cancelDialog(host, *entry.second);
|
|
7427
|
+
host->dialogs.clear();
|
|
7428
|
+
std::lock_guard<std::mutex> lock(host->dialog_queue->mutex);
|
|
7429
|
+
host->dialog_queue->loop_window = nullptr;
|
|
7430
|
+
host->dialog_queue->finished.clear();
|
|
7431
|
+
}
|
|
7432
|
+
|
|
6655
7433
|
} // namespace
|
|
6656
7434
|
|
|
6657
7435
|
extern "C" {
|
|
@@ -6682,6 +7460,20 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6682
7460
|
* RPC_E_CHANGED_MODE (an MTA got there first) leaves nothing to
|
|
6683
7461
|
* balance. */
|
|
6684
7462
|
host->com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
|
|
7463
|
+
/* The loop window exists before any app window and for the host's
|
|
7464
|
+
* whole life, so wakes and timers have a target from the start. */
|
|
7465
|
+
HWND loop_window = createLoopWindow(host);
|
|
7466
|
+
if (!loop_window) {
|
|
7467
|
+
const bool com_initialized = host->com_initialized;
|
|
7468
|
+
delete host;
|
|
7469
|
+
if (com_initialized) CoUninitialize();
|
|
7470
|
+
return nullptr;
|
|
7471
|
+
}
|
|
7472
|
+
host->loop_window.store(loop_window, std::memory_order_release);
|
|
7473
|
+
{
|
|
7474
|
+
std::lock_guard<std::mutex> lock(host->dialog_queue->mutex);
|
|
7475
|
+
host->dialog_queue->loop_window = loop_window;
|
|
7476
|
+
}
|
|
6685
7477
|
/* One hardware Direct2D/DirectWrite resource domain for every canvas
|
|
6686
7478
|
* child. Creation also requires the in-memory font and constrained
|
|
6687
7479
|
* fallback seams: without them packet text cannot match engine layout,
|
|
@@ -6725,9 +7517,14 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6725
7517
|
void native_sdk_windows_destroy(Host *host) {
|
|
6726
7518
|
if (!host) return;
|
|
6727
7519
|
audioCaptureStopAll(host);
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
7520
|
+
/* Retire the loop window first: a wake or frame request from here on
|
|
7521
|
+
* finds no target and reports that, instead of posting into a queue
|
|
7522
|
+
* nobody will pump again. */
|
|
7523
|
+
HWND loop_window = host->loop_window.exchange(nullptr, std::memory_order_acq_rel);
|
|
7524
|
+
host->lifetime->alive = false;
|
|
7525
|
+
/* Dialog threads outlive the host: close their dialogs and let them
|
|
7526
|
+
* finish on their own; their results have nowhere to go. */
|
|
7527
|
+
cancelAllDialogs(host);
|
|
6731
7528
|
/* DestroyWindow below dispatches WM_DESTROY/WM_ACTIVATEAPP
|
|
6732
7529
|
* synchronously through windowProc; the run loop's handler state is
|
|
6733
7530
|
* already gone by the time destroy is called, so those teardown
|
|
@@ -6747,6 +7544,7 @@ void native_sdk_windows_destroy(Host *host) {
|
|
|
6747
7544
|
removeNotificationIcon(host);
|
|
6748
7545
|
destroyAllWindows(host);
|
|
6749
7546
|
gpuSurfaceDestroyFrameWakeTimer(host);
|
|
7547
|
+
if (loop_window) DestroyWindow(loop_window);
|
|
6750
7548
|
const bool com_initialized = host->com_initialized;
|
|
6751
7549
|
delete host;
|
|
6752
7550
|
if (com_initialized) CoUninitialize();
|
|
@@ -6754,14 +7552,16 @@ void native_sdk_windows_destroy(Host *host) {
|
|
|
6754
7552
|
|
|
6755
7553
|
void native_sdk_windows_run(Host *host, EventCallback callback, void *context) {
|
|
6756
7554
|
if (!host) return;
|
|
6757
|
-
host->
|
|
6758
|
-
host->
|
|
7555
|
+
host->runtime_callback = callback;
|
|
7556
|
+
host->runtime_context = context;
|
|
7557
|
+
host->callback = dispatchEventToRuntime;
|
|
7558
|
+
host->callback_context = host;
|
|
6759
7559
|
host->running = true;
|
|
6760
7560
|
if (!host->windows.empty()) createNativeWindow(host, host->windows.begin()->second);
|
|
6761
7561
|
WindowsEvent start = {};
|
|
6762
7562
|
start.kind = kStart;
|
|
6763
7563
|
start.window_id = 1;
|
|
6764
|
-
callback(
|
|
7564
|
+
host->callback(host->callback_context, &start);
|
|
6765
7565
|
emitAppearanceIfChanged(host, true);
|
|
6766
7566
|
for (auto &entry : host->windows) {
|
|
6767
7567
|
emit(host, entry.second, kResize);
|
|
@@ -6786,10 +7586,7 @@ void native_sdk_windows_run(Host *host, EventCallback callback, void *context) {
|
|
|
6786
7586
|
DispatchMessageW(&message);
|
|
6787
7587
|
if (host->running) gpuSurfaceDrainDueFrameEmissions(host);
|
|
6788
7588
|
}
|
|
6789
|
-
|
|
6790
|
-
shutdown.kind = kShutdown;
|
|
6791
|
-
shutdown.window_id = 1;
|
|
6792
|
-
callback(context, &shutdown);
|
|
7589
|
+
deliverShutdown(host);
|
|
6793
7590
|
}
|
|
6794
7591
|
|
|
6795
7592
|
void native_sdk_windows_stop(Host *host) {
|
|
@@ -6798,30 +7595,30 @@ void native_sdk_windows_stop(Host *host) {
|
|
|
6798
7595
|
PostQuitMessage(0);
|
|
6799
7596
|
}
|
|
6800
7597
|
|
|
6801
|
-
/* Thread-safe wake: posts kWakeMessage
|
|
6802
|
-
*
|
|
6803
|
-
*
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
PostMessageW(
|
|
7598
|
+
/* Thread-safe wake: posts kWakeMessage to the loop window, which emits
|
|
7599
|
+
* the kWake event on the loop thread. Enqueue-only, as the wake contract
|
|
7600
|
+
* requires (PlatformServices.wake_fn): it takes no lock the loop thread
|
|
7601
|
+
* could be holding, so a thread the loop is joining can always finish
|
|
7602
|
+
* its wake. The host itself outlives every caller — the runtime fences
|
|
7603
|
+
* its wake paths before teardown. Returns 0 when nothing was posted (the
|
|
7604
|
+
* host is being destroyed, or the thread's queue is full). */
|
|
7605
|
+
int native_sdk_windows_wake(Host *host) {
|
|
7606
|
+
if (!host) return 0;
|
|
7607
|
+
HWND target = host->loop_window.load(std::memory_order_acquire);
|
|
7608
|
+
return target && PostMessageW(target, kWakeMessage, 0, 0) ? 1 : 0;
|
|
6812
7609
|
}
|
|
6813
7610
|
|
|
6814
|
-
/* Thread-safe frame request: posts kRequestFrameMessage
|
|
6815
|
-
*
|
|
6816
|
-
* arrival watcher's wake,
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
7611
|
+
/* Thread-safe frame request: posts kRequestFrameMessage to the loop
|
|
7612
|
+
* window, which emits one kFrame event on the loop thread — the
|
|
7613
|
+
* automation arrival watcher's wake, enqueue-only like
|
|
7614
|
+
* native_sdk_windows_wake. Requests coalesce while one is queued. */
|
|
7615
|
+
int native_sdk_windows_request_frame(Host *host) {
|
|
7616
|
+
if (!host) return 0;
|
|
7617
|
+
if (host->frame_request_pending.exchange(true, std::memory_order_acq_rel)) return 1;
|
|
7618
|
+
HWND target = host->loop_window.load(std::memory_order_acquire);
|
|
7619
|
+
if (target && PostMessageW(target, kRequestFrameMessage, 0, 0)) return 1;
|
|
7620
|
+
host->frame_request_pending.store(false, std::memory_order_release);
|
|
7621
|
+
return 0;
|
|
6825
7622
|
}
|
|
6826
7623
|
|
|
6827
7624
|
/* Platform image decoder: WIC (Windows Imaging Component) handles PNG,
|
|
@@ -7167,7 +7964,7 @@ int native_sdk_windows_create_window(Host *host, uint64_t window_id, const char
|
|
|
7167
7964
|
void native_sdk_windows_start_timer(Host *host, uint64_t timer_id, uint64_t interval_ns, int repeats) {
|
|
7168
7965
|
if (!host) return;
|
|
7169
7966
|
native_sdk_windows_cancel_timer(host, timer_id);
|
|
7170
|
-
HWND hwnd =
|
|
7967
|
+
HWND hwnd = loopWindow(host);
|
|
7171
7968
|
if (!hwnd) return;
|
|
7172
7969
|
AppTimer *slot = nullptr;
|
|
7173
7970
|
UINT_PTR win32_id = 0;
|
|
@@ -7932,167 +8729,154 @@ int native_sdk_windows_close_view(Host *host, uint64_t window_id, const char *la
|
|
|
7932
8729
|
|
|
7933
8730
|
WindowsOpenDialogResult native_sdk_windows_show_open_dialog(Host *host, const WindowsOpenDialogOpts *opts, char *buffer, size_t buffer_len) {
|
|
7934
8731
|
WindowsOpenDialogResult result = {};
|
|
7935
|
-
|
|
8732
|
+
/* A session end is unwinding the app's dispatch (endSession): a new
|
|
8733
|
+
* dialog would hold it again, so it answers as cancelled. */
|
|
8734
|
+
if (!host || !opts || !buffer || buffer_len == 0 || host->session_ending) return result;
|
|
7936
8735
|
bool uninitialize = false;
|
|
7937
8736
|
if (!initializeCom(&uninitialize)) return result;
|
|
7938
8737
|
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
DWORD options = 0;
|
|
7947
|
-
if (SUCCEEDED(dialog->GetOptions(&options))) {
|
|
7948
|
-
options |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
|
|
7949
|
-
if (opts->allow_directories) options |= FOS_PICKFOLDERS;
|
|
7950
|
-
else options |= FOS_FILEMUSTEXIST;
|
|
7951
|
-
if (opts->allow_multiple) options |= FOS_ALLOWMULTISELECT;
|
|
7952
|
-
dialog->SetOptions(options);
|
|
7953
|
-
}
|
|
7954
|
-
setDialogTitle(dialog, opts->title, opts->title_len);
|
|
7955
|
-
setDialogFolder(dialog, opts->default_path, opts->default_path_len);
|
|
7956
|
-
if (!opts->allow_directories) setDialogFilters(dialog, opts->extensions, opts->extensions_len);
|
|
7957
|
-
|
|
7958
|
-
if (SUCCEEDED(dialog->Show(parentWindow(host)))) {
|
|
7959
|
-
IShellItemArray *items = nullptr;
|
|
7960
|
-
if (SUCCEEDED(dialog->GetResults(&items)) && items) {
|
|
7961
|
-
DWORD count = 0;
|
|
7962
|
-
items->GetCount(&count);
|
|
7963
|
-
size_t offset = 0;
|
|
7964
|
-
size_t written_count = 0;
|
|
7965
|
-
for (DWORD index = 0; index < count; ++index) {
|
|
7966
|
-
IShellItem *item = nullptr;
|
|
7967
|
-
if (SUCCEEDED(items->GetItemAt(index, &item)) && item) {
|
|
7968
|
-
bool overflow = false;
|
|
7969
|
-
PWSTR path = nullptr;
|
|
7970
|
-
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
|
|
7971
|
-
overflow = !appendPathToBuffer(buffer, buffer_len, &offset, &written_count, std::wstring(path));
|
|
7972
|
-
CoTaskMemFree(path);
|
|
7973
|
-
}
|
|
7974
|
-
item->Release();
|
|
7975
|
-
if (overflow) break;
|
|
7976
|
-
}
|
|
7977
|
-
}
|
|
7978
|
-
result.count = written_count;
|
|
7979
|
-
result.bytes_written = offset;
|
|
7980
|
-
items->Release();
|
|
8738
|
+
std::vector<std::wstring> paths;
|
|
8739
|
+
if (SUCCEEDED(showOpenDialogOn(parentWindow(host), openDialogSpec(opts), &paths))) {
|
|
8740
|
+
size_t offset = 0;
|
|
8741
|
+
size_t written_count = 0;
|
|
8742
|
+
for (const std::wstring &path : paths) {
|
|
8743
|
+
if (!appendPathToBuffer(buffer, buffer_len, &offset, &written_count, path)) break;
|
|
7981
8744
|
}
|
|
8745
|
+
result.count = written_count;
|
|
8746
|
+
result.bytes_written = offset;
|
|
7982
8747
|
}
|
|
7983
8748
|
|
|
7984
|
-
dialog->Release();
|
|
7985
8749
|
finishCom(uninitialize);
|
|
7986
8750
|
return result;
|
|
7987
8751
|
}
|
|
7988
8752
|
|
|
7989
8753
|
size_t native_sdk_windows_show_save_dialog(Host *host, const WindowsSaveDialogOpts *opts, char *buffer, size_t buffer_len) {
|
|
7990
|
-
|
|
8754
|
+
/* Cancelled during a session end, like the open dialog. */
|
|
8755
|
+
if (!host || !opts || !buffer || buffer_len == 0 || host->session_ending) return 0;
|
|
7991
8756
|
bool uninitialize = false;
|
|
7992
8757
|
if (!initializeCom(&uninitialize)) return 0;
|
|
7993
8758
|
|
|
7994
|
-
IFileSaveDialog *dialog = nullptr;
|
|
7995
|
-
HRESULT hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
|
|
7996
|
-
if (FAILED(hr) || !dialog) {
|
|
7997
|
-
finishCom(uninitialize);
|
|
7998
|
-
return 0;
|
|
7999
|
-
}
|
|
8000
|
-
|
|
8001
|
-
DWORD options = 0;
|
|
8002
|
-
if (SUCCEEDED(dialog->GetOptions(&options))) dialog->SetOptions(options | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
|
8003
|
-
setDialogTitle(dialog, opts->title, opts->title_len);
|
|
8004
|
-
setDialogFolder(dialog, opts->default_path, opts->default_path_len);
|
|
8005
|
-
setDialogFilters(dialog, opts->extensions, opts->extensions_len);
|
|
8006
|
-
std::wstring default_name = widen(slice(opts->default_name, opts->default_name_len));
|
|
8007
|
-
if (!default_name.empty()) dialog->SetFileName(default_name.c_str());
|
|
8008
|
-
|
|
8009
8759
|
size_t written = 0;
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
} else {
|
|
8019
|
-
written = utf8_path.size();
|
|
8020
|
-
if (written > 0) memcpy(buffer, utf8_path.data(), written);
|
|
8021
|
-
}
|
|
8022
|
-
CoTaskMemFree(path);
|
|
8023
|
-
}
|
|
8024
|
-
item->Release();
|
|
8760
|
+
std::wstring path;
|
|
8761
|
+
if (SUCCEEDED(showSaveDialogOn(parentWindow(host), saveDialogSpec(opts), &path)) && !path.empty()) {
|
|
8762
|
+
std::string utf8_path = narrow(path);
|
|
8763
|
+
if (utf8_path.size() > buffer_len) {
|
|
8764
|
+
written = overflowSize(buffer_len);
|
|
8765
|
+
} else {
|
|
8766
|
+
written = utf8_path.size();
|
|
8767
|
+
if (written > 0) memcpy(buffer, utf8_path.data(), written);
|
|
8025
8768
|
}
|
|
8026
8769
|
}
|
|
8027
8770
|
|
|
8028
|
-
dialog->Release();
|
|
8029
8771
|
finishCom(uninitialize);
|
|
8030
8772
|
return written;
|
|
8031
8773
|
}
|
|
8032
8774
|
|
|
8775
|
+
/* Returns the pressed button (0 primary, 1 secondary, 2 tertiary), or -1
|
|
8776
|
+
* when a session end left the dialog unanswered: it was cancelled to let
|
|
8777
|
+
* the app's dispatch unwind (endSession), or never shown because one is
|
|
8778
|
+
* unwinding. Closing a task dialog otherwise reports the primary button,
|
|
8779
|
+
* which must not stand in for an answer nobody gave. */
|
|
8033
8780
|
int native_sdk_windows_show_message_dialog(Host *host, const WindowsMessageDialogOpts *opts) {
|
|
8034
8781
|
if (!host || !opts) return 0;
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
std::wstring primary = widen(slice(opts->primary_button, opts->primary_button_len));
|
|
8039
|
-
std::wstring secondary = widen(slice(opts->secondary_button, opts->secondary_button_len));
|
|
8040
|
-
std::wstring tertiary = widen(slice(opts->tertiary_button, opts->tertiary_button_len));
|
|
8041
|
-
if (primary.empty()) primary = L"OK";
|
|
8042
|
-
|
|
8043
|
-
TASKDIALOG_BUTTON buttons[3] = {};
|
|
8044
|
-
int button_count = 0;
|
|
8045
|
-
buttons[button_count++] = { 100, primary.c_str() };
|
|
8046
|
-
if (!secondary.empty()) buttons[button_count++] = { 101, secondary.c_str() };
|
|
8047
|
-
if (!tertiary.empty()) buttons[button_count++] = { 102, tertiary.c_str() };
|
|
8048
|
-
|
|
8049
|
-
TASKDIALOGCONFIG config = {};
|
|
8050
|
-
config.cbSize = sizeof(config);
|
|
8051
|
-
config.hwndParent = parentWindow(host);
|
|
8052
|
-
config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION;
|
|
8053
|
-
config.pszWindowTitle = title.empty() ? L"native-sdk" : title.c_str();
|
|
8054
|
-
config.pszMainInstruction = message.empty() ? config.pszWindowTitle : message.c_str();
|
|
8055
|
-
config.pszContent = informative.empty() ? nullptr : informative.c_str();
|
|
8056
|
-
config.cButtons = static_cast<UINT>(button_count);
|
|
8057
|
-
config.pButtons = buttons;
|
|
8058
|
-
config.nDefaultButton = 100;
|
|
8059
|
-
config.pszMainIcon = opts->style == 2 ? TD_ERROR_ICON : (opts->style == 1 ? TD_WARNING_ICON : TD_INFORMATION_ICON);
|
|
8060
|
-
|
|
8061
|
-
/* TaskDialogIndirect is a comctl32 v6 export: it only resolves when
|
|
8062
|
-
* the process activates the v6 side-by-side assembly (application
|
|
8063
|
-
* manifest). A static import would abort the whole process at load
|
|
8064
|
-
* time with STATUS_ENTRYPOINT_NOT_FOUND on the system default v5, so
|
|
8065
|
-
* resolve it dynamically and fall back to MessageBoxW (fixed button
|
|
8066
|
-
* captions, same 0/1/2 result contract) when only v5 is available. */
|
|
8067
|
-
using TaskDialogIndirectFn = HRESULT(WINAPI *)(const TASKDIALOGCONFIG *, int *, int *, BOOL *);
|
|
8068
|
-
static TaskDialogIndirectFn task_dialog = reinterpret_cast<TaskDialogIndirectFn>(
|
|
8069
|
-
reinterpret_cast<void *>(GetProcAddress(GetModuleHandleW(L"comctl32.dll"), "TaskDialogIndirect")));
|
|
8070
|
-
if (task_dialog) {
|
|
8782
|
+
if (host->session_ending) return -1;
|
|
8783
|
+
const DialogRequest spec = messageDialogSpec(opts);
|
|
8784
|
+
if (TaskDialogIndirectFn task_dialog = taskDialogIndirect()) {
|
|
8071
8785
|
int pressed = 100;
|
|
8072
|
-
HRESULT hr = task_dialog
|
|
8786
|
+
HRESULT hr = runTaskDialog(task_dialog, parentWindow(host), spec, &pressed);
|
|
8787
|
+
if (host->session_ending) return -1;
|
|
8073
8788
|
if (FAILED(hr)) return 0;
|
|
8074
8789
|
if (pressed == 101) return 1;
|
|
8075
8790
|
if (pressed == 102) return 2;
|
|
8076
8791
|
return 0;
|
|
8077
8792
|
}
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8793
|
+
const int result = runMessageBox(parentWindow(host), spec);
|
|
8794
|
+
if (host->session_ending) return -1;
|
|
8795
|
+
return messageBoxButton(spec, result);
|
|
8796
|
+
}
|
|
8797
|
+
|
|
8798
|
+
/* Show a dialog without blocking: it runs on a thread of its own and
|
|
8799
|
+
* reports through native_sdk_windows_poll_dialog_result after a wake.
|
|
8800
|
+
* The owner is the request's window unless something else already
|
|
8801
|
+
* disabled it (another modal on it), as Electron does: this dialog would
|
|
8802
|
+
* otherwise re-enable a window still held by that one when it closes.
|
|
8803
|
+
* Returns 0 when nothing was started. */
|
|
8804
|
+
int native_sdk_windows_request_dialog(Host *host, const WindowsDialogRequest *request) {
|
|
8805
|
+
if (!host || !request || request->id == 0 || host->session_ending || host->shutdown_delivered) return 0;
|
|
8806
|
+
if (host->dialogs.count(request->id) != 0) return 0;
|
|
8807
|
+
auto dialog = std::make_shared<DialogRequest>();
|
|
8808
|
+
switch (request->kind) {
|
|
8809
|
+
case kDialogOpen:
|
|
8810
|
+
if (!request->open_opts) return 0;
|
|
8811
|
+
*dialog = openDialogSpec(request->open_opts);
|
|
8812
|
+
break;
|
|
8813
|
+
case kDialogSave:
|
|
8814
|
+
if (!request->save_opts) return 0;
|
|
8815
|
+
*dialog = saveDialogSpec(request->save_opts);
|
|
8816
|
+
break;
|
|
8817
|
+
case kDialogMessage:
|
|
8818
|
+
if (!request->message_opts) return 0;
|
|
8819
|
+
*dialog = messageDialogSpec(request->message_opts);
|
|
8820
|
+
break;
|
|
8821
|
+
default:
|
|
8822
|
+
return 0;
|
|
8087
8823
|
}
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8824
|
+
dialog->id = request->id;
|
|
8825
|
+
auto window = host->windows.find(request->parent_window_id);
|
|
8826
|
+
if (request->parent_window_id != 0 && window != host->windows.end() && window->second.hwnd && IsWindowEnabled(window->second.hwnd)) {
|
|
8827
|
+
dialog->owner = window->second.hwnd;
|
|
8828
|
+
dialog->parent_window_id = request->parent_window_id;
|
|
8829
|
+
}
|
|
8830
|
+
try {
|
|
8831
|
+
std::thread(runDialogThread, host->dialog_queue, dialog).detach();
|
|
8832
|
+
} catch (...) {
|
|
8092
8833
|
return 0;
|
|
8093
8834
|
}
|
|
8094
|
-
|
|
8095
|
-
return
|
|
8835
|
+
host->dialogs[dialog->id] = dialog;
|
|
8836
|
+
return 1;
|
|
8837
|
+
}
|
|
8838
|
+
|
|
8839
|
+
void native_sdk_windows_cancel_dialog(Host *host, uint64_t id) {
|
|
8840
|
+
if (!host) return;
|
|
8841
|
+
auto found = host->dialogs.find(id);
|
|
8842
|
+
if (found != host->dialogs.end()) cancelDialog(host, *found->second);
|
|
8843
|
+
}
|
|
8844
|
+
|
|
8845
|
+
/* Hand back the oldest finished dialog. Returns 0 when none is waiting. */
|
|
8846
|
+
int native_sdk_windows_poll_dialog_result(Host *host, char *buffer, size_t buffer_len, WindowsDialogResult *result) {
|
|
8847
|
+
if (!host || !result) return 0;
|
|
8848
|
+
std::shared_ptr<DialogRequest> dialog;
|
|
8849
|
+
{
|
|
8850
|
+
std::lock_guard<std::mutex> lock(host->dialog_queue->mutex);
|
|
8851
|
+
if (host->dialog_queue->finished.empty()) return 0;
|
|
8852
|
+
dialog = host->dialog_queue->finished.front();
|
|
8853
|
+
host->dialog_queue->finished.pop_front();
|
|
8854
|
+
}
|
|
8855
|
+
host->dialogs.erase(dialog->id);
|
|
8856
|
+
*result = {};
|
|
8857
|
+
result->id = dialog->id;
|
|
8858
|
+
result->outcome = dialog->outcome;
|
|
8859
|
+
result->button = dialog->button;
|
|
8860
|
+
size_t offset = 0;
|
|
8861
|
+
size_t count = 0;
|
|
8862
|
+
for (const std::string &path : dialog->paths) {
|
|
8863
|
+
const size_t needed = path.size() + (count > 0 ? 1 : 0);
|
|
8864
|
+
if (!buffer || needed > buffer_len - offset) {
|
|
8865
|
+
/* More paths than the runtime holds: a failure, not a
|
|
8866
|
+
* silently shortened selection. */
|
|
8867
|
+
result->outcome = kDialogFailed;
|
|
8868
|
+
offset = 0;
|
|
8869
|
+
count = 0;
|
|
8870
|
+
break;
|
|
8871
|
+
}
|
|
8872
|
+
if (count > 0) buffer[offset++] = '\n';
|
|
8873
|
+
memcpy(buffer + offset, path.data(), path.size());
|
|
8874
|
+
offset += path.size();
|
|
8875
|
+
count += 1;
|
|
8876
|
+
}
|
|
8877
|
+
result->count = count;
|
|
8878
|
+
result->bytes_written = offset;
|
|
8879
|
+
return 1;
|
|
8096
8880
|
}
|
|
8097
8881
|
|
|
8098
8882
|
int native_sdk_windows_show_notification(Host *host, const char *title, size_t title_len, const char *subtitle, size_t subtitle_len, const char *body, size_t body_len, const char *notification_id, size_t notification_id_len, const char *action_label, size_t action_label_len, const char *action_command, size_t action_command_len) {
|