@zogien/native-sdk 0.11.0-fork.3 → 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/README.md +1 -1
- package/build/app.zig +53 -34
- package/build/cli.zig +22 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -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 +53 -0
- package/src/platform/macos/appkit_host.m +667 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +146 -29
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +7 -0
- package/src/platform/types.zig +90 -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 +262 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1435 -306
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +8 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/channel_sources.zig +49 -1
- 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 +83 -3
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +20 -6
- package/src/runtime/flow_reference.zig +20 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +9 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +44 -2
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -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
|
|
|
@@ -421,6 +447,9 @@ struct Window {
|
|
|
421
447
|
/* The maximized state last reported to the runtime, so WM_SIZE emits
|
|
422
448
|
* a frame event only when it changes. */
|
|
423
449
|
bool maximized = false;
|
|
450
|
+
/* Same-thread child kept above content children across the top
|
|
451
|
+
* resize band (see syncTopResizeStrip); created on first need. */
|
|
452
|
+
HWND top_resize_strip = nullptr;
|
|
424
453
|
};
|
|
425
454
|
|
|
426
455
|
/* One rectangle of a canvas view's window-drag mirror (runtime push,
|
|
@@ -605,14 +634,70 @@ struct TrayItem {
|
|
|
605
634
|
bool enabled = true;
|
|
606
635
|
};
|
|
607
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. */
|
|
608
642
|
struct HostLifetime {
|
|
609
|
-
std::recursive_mutex mutex;
|
|
610
643
|
bool alive = true;
|
|
611
644
|
};
|
|
612
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
|
+
|
|
613
697
|
/* App timers (runtime `startTimer`) on the Win32 message loop: each slot
|
|
614
698
|
* owns the SetTimer id kAppTimerIdBase + slot index, scheduled on the
|
|
615
|
-
*
|
|
699
|
+
* host's loop window so WM_TIMER lands in loopWindowProc and closing an
|
|
700
|
+
* app window never stops one. */
|
|
616
701
|
constexpr size_t kMaxAppTimers = 64;
|
|
617
702
|
constexpr UINT_PTR kAppTimerIdBase = 0x1000;
|
|
618
703
|
/* The 16 ms per-window frame-pump timer (SetTimer id on each top-level
|
|
@@ -680,11 +765,21 @@ struct AudioCaptureState {
|
|
|
680
765
|
std::thread worker;
|
|
681
766
|
};
|
|
682
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
|
+
|
|
683
779
|
/* The app's single audio player (see the audio section further down for
|
|
684
780
|
* the backend rationale). All fields are message-loop-thread state; the
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
* Media Foundation worker thread. */
|
|
781
|
+
* asynchronous URL source resolver hands its result over through
|
|
782
|
+
* `resolve_mailbox` instead of writing here. */
|
|
688
783
|
struct AudioState {
|
|
689
784
|
/* Bumped on every load/stop: worker-thread stragglers (resolver
|
|
690
785
|
* completions, retired-session events) carry the generation they
|
|
@@ -712,6 +807,7 @@ struct AudioState {
|
|
|
712
807
|
IMFMediaSource *source = nullptr;
|
|
713
808
|
IMFPresentationClock *clock = nullptr;
|
|
714
809
|
std::shared_ptr<AudioDownloadCancel> download_cancel;
|
|
810
|
+
std::shared_ptr<AudioSourceMailbox> resolve_mailbox;
|
|
715
811
|
};
|
|
716
812
|
|
|
717
813
|
/* The one pending app context menu (menus are modal, so one at a time):
|
|
@@ -735,6 +831,38 @@ struct PendingContextMenu {
|
|
|
735
831
|
std::vector<Item> items;
|
|
736
832
|
};
|
|
737
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
|
+
|
|
738
866
|
struct Host {
|
|
739
867
|
bool window_drag_initialization_failed = false;
|
|
740
868
|
uint64_t next_window_drag_generation = 1;
|
|
@@ -743,11 +871,32 @@ struct Host {
|
|
|
743
871
|
std::string window_title;
|
|
744
872
|
std::string bundle_id;
|
|
745
873
|
std::string icon_path;
|
|
874
|
+
/* The WebView2 user data folder every environment in this host
|
|
875
|
+
* shares, resolved once on first use (webViewUserDataFolder). Empty
|
|
876
|
+
* means WebView2's own default. */
|
|
877
|
+
bool webview_user_data_folder_resolved = false;
|
|
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. */
|
|
746
881
|
EventCallback callback = nullptr;
|
|
747
882
|
void *callback_context = nullptr;
|
|
883
|
+
EventCallback runtime_callback = nullptr;
|
|
884
|
+
void *runtime_context = nullptr;
|
|
748
885
|
BridgeCallback bridge_callback = nullptr;
|
|
749
886
|
void *bridge_context = nullptr;
|
|
750
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;
|
|
751
900
|
std::map<uint64_t, Window> windows;
|
|
752
901
|
std::map<std::string, ChildWebView> webviews;
|
|
753
902
|
std::shared_ptr<WindowsGpuRenderer> gpu_renderer;
|
|
@@ -788,9 +937,34 @@ struct Host {
|
|
|
788
937
|
AudioCaptureState captures[2];
|
|
789
938
|
PendingContextMenu context_menu;
|
|
790
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>();
|
|
791
963
|
};
|
|
792
964
|
|
|
793
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);
|
|
794
968
|
static void gpuSurfaceRefreshFrameWakeTimer(Host *host);
|
|
795
969
|
static void gpuSurfaceDestroyFrameWakeTimer(Host *host);
|
|
796
970
|
|
|
@@ -857,6 +1031,13 @@ static HWND parentWindow(Host *host) {
|
|
|
857
1031
|
return nullptr;
|
|
858
1032
|
}
|
|
859
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
|
+
|
|
860
1041
|
static void copyWideField(wchar_t *dest, size_t dest_len, const std::wstring &value) {
|
|
861
1042
|
if (!dest || dest_len == 0) return;
|
|
862
1043
|
size_t count = std::min(value.size(), dest_len - 1);
|
|
@@ -1171,8 +1352,57 @@ static std::string bridgeOriginForWebViewUrl(const ChildWebView &webview, const
|
|
|
1171
1352
|
return isInternalAssetUrl(webview, url) ? assetOrigin(webview) : originForUrl(url);
|
|
1172
1353
|
}
|
|
1173
1354
|
|
|
1355
|
+
/* The resources\ directory of a packaged Windows artifact: `native
|
|
1356
|
+
* package` stages the executable in bin\ and the app-relative asset tree
|
|
1357
|
+
* in resources\ beside it. Empty when the executable is not in a bin\
|
|
1358
|
+
* directory. */
|
|
1359
|
+
static const std::wstring &packagedResourcesDirectory() {
|
|
1360
|
+
static const std::wstring directory = [] {
|
|
1361
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
1362
|
+
for (;;) {
|
|
1363
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
1364
|
+
if (length == 0) return std::wstring();
|
|
1365
|
+
if (length < path.size()) {
|
|
1366
|
+
path.resize(length);
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
if (path.size() >= 32768) return std::wstring();
|
|
1370
|
+
path.resize(path.size() * 2);
|
|
1371
|
+
}
|
|
1372
|
+
const size_t exe_slash = path.find_last_of(L"\\/");
|
|
1373
|
+
if (exe_slash == std::wstring::npos) return std::wstring();
|
|
1374
|
+
const std::wstring bin = path.substr(0, exe_slash);
|
|
1375
|
+
const size_t bin_slash = bin.find_last_of(L"\\/");
|
|
1376
|
+
if (bin_slash == std::wstring::npos || CompareStringOrdinal(bin.c_str() + bin_slash + 1, -1, L"bin", -1, TRUE) != CSTR_EQUAL) return std::wstring();
|
|
1377
|
+
return bin.substr(0, bin_slash) + L"\\resources";
|
|
1378
|
+
}();
|
|
1379
|
+
return directory;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/* A relative WebView asset root names a directory of the packaged asset
|
|
1383
|
+
* tree when the app runs from a package, the tree the macOS host finds
|
|
1384
|
+
* in its bundle's Resources. In development it stays relative to the
|
|
1385
|
+
* working directory. */
|
|
1386
|
+
static std::string assetRootPath(const std::string &root) {
|
|
1387
|
+
const bool absolute = !root.empty() && (root[0] == '\\' || root[0] == '/' || (root.size() > 1 && root[1] == ':'));
|
|
1388
|
+
if (!absolute) {
|
|
1389
|
+
const std::wstring &resources = packagedResourcesDirectory();
|
|
1390
|
+
if (!resources.empty()) {
|
|
1391
|
+
std::wstring candidate = resources;
|
|
1392
|
+
if (!root.empty() && root != ".") {
|
|
1393
|
+
std::wstring relative = widen(root);
|
|
1394
|
+
std::replace(relative.begin(), relative.end(), L'/', L'\\');
|
|
1395
|
+
candidate += L"\\" + relative;
|
|
1396
|
+
}
|
|
1397
|
+
const DWORD attributes = GetFileAttributesW(candidate.c_str());
|
|
1398
|
+
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) return narrow(candidate);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return root.empty() ? std::string(".") : root;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1174
1404
|
static std::wstring assetFilePath(const ChildWebView &webview, const std::string &relative) {
|
|
1175
|
-
std::string path = webview.asset_root
|
|
1405
|
+
std::string path = assetRootPath(webview.asset_root);
|
|
1176
1406
|
if (!path.empty() && path.back() != '/' && path.back() != '\\') path.push_back('\\');
|
|
1177
1407
|
std::string native_relative = relative;
|
|
1178
1408
|
std::replace(native_relative.begin(), native_relative.end(), '/', '\\');
|
|
@@ -1710,7 +1940,10 @@ static void showTrayMenu(Host *host, HWND hwnd) {
|
|
|
1710
1940
|
PostMessageW(hwnd, WM_NULL, 0, 0);
|
|
1711
1941
|
}
|
|
1712
1942
|
|
|
1713
|
-
|
|
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) {
|
|
1714
1947
|
if (!host || (host->shortcuts.empty() && host->menus.empty() && host->tray_items.empty())) return false;
|
|
1715
1948
|
if (!window) return false;
|
|
1716
1949
|
std::string key = shortcutKeyFromWParam(wparam);
|
|
@@ -1722,16 +1955,11 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1722
1955
|
for (const Shortcut &shortcut : host->shortcuts) {
|
|
1723
1956
|
if (shortcut.key != key) continue;
|
|
1724
1957
|
if (!shortcutModifiersMatch(shortcut.modifiers, allow_implicit_shift)) continue;
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
event.shortcut_id_len = shortcut.id.size();
|
|
1731
|
-
event.shortcut_key = shortcut.key.c_str();
|
|
1732
|
-
event.shortcut_key_len = shortcut.key.size();
|
|
1733
|
-
event.shortcut_modifiers = shortcut.modifiers;
|
|
1734
|
-
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;
|
|
1735
1963
|
return true;
|
|
1736
1964
|
}
|
|
1737
1965
|
/* Menu accelerators dispatch from the same keydown path: a menu
|
|
@@ -1744,13 +1972,9 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1744
1972
|
if (item.separator || !item.enabled || item.key.empty()) continue;
|
|
1745
1973
|
if (item.key != key) continue;
|
|
1746
1974
|
if (!shortcutModifiersMatch(item.modifiers, allow_implicit_shift)) continue;
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
event.window_id = window->id;
|
|
1751
|
-
event.command_name = item.command.c_str();
|
|
1752
|
-
event.command_name_len = item.command.size();
|
|
1753
|
-
host->callback(host->callback_context, &event);
|
|
1975
|
+
out->kind = kMenuCommand;
|
|
1976
|
+
out->window_id = window->id;
|
|
1977
|
+
out->command = item.command;
|
|
1754
1978
|
return true;
|
|
1755
1979
|
}
|
|
1756
1980
|
}
|
|
@@ -1761,18 +1985,145 @@ static bool emitShortcutForWindow(Host *host, const Window *window, WPARAM wpara
|
|
|
1761
1985
|
if (item.separator || !item.enabled || item.key.empty()) continue;
|
|
1762
1986
|
if (item.key != key) continue;
|
|
1763
1987
|
if (!shortcutModifiersMatch(item.modifiers, allow_implicit_shift)) continue;
|
|
1764
|
-
|
|
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;
|
|
1765
1993
|
}
|
|
1766
1994
|
}
|
|
1767
1995
|
return false;
|
|
1768
1996
|
}
|
|
1769
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
|
+
|
|
1770
2026
|
static bool emitShortcutForHwnd(Host *host, HWND hwnd, WPARAM wparam) {
|
|
1771
2027
|
return emitShortcutForWindow(host, windowForHwnd(host, hwnd), wparam);
|
|
1772
2028
|
}
|
|
1773
2029
|
|
|
1774
|
-
|
|
1775
|
-
|
|
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;
|
|
1776
2127
|
}
|
|
1777
2128
|
|
|
1778
2129
|
static std::string webViewKey(uint64_t window_id, const std::string &label) {
|
|
@@ -2014,6 +2365,8 @@ static void applyNativeViewState(NativeView &view, bool update_text, const std::
|
|
|
2014
2365
|
applyNativeViewAccessibility(view);
|
|
2015
2366
|
}
|
|
2016
2367
|
|
|
2368
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id);
|
|
2369
|
+
|
|
2017
2370
|
static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
2018
2371
|
if (!host) return;
|
|
2019
2372
|
struct LayerItem {
|
|
@@ -2041,6 +2394,8 @@ static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
|
2041
2394
|
for (const LayerItem &item : items) {
|
|
2042
2395
|
SetWindowPos(item.hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
2043
2396
|
}
|
|
2397
|
+
/* Content layers never rise above the window's own resize band. */
|
|
2398
|
+
syncTopResizeStrip(host, window_id);
|
|
2044
2399
|
}
|
|
2045
2400
|
|
|
2046
2401
|
static bool emitNativeCommandForHwnd(Host *host, HWND hwnd, UINT notification_code) {
|
|
@@ -2328,6 +2683,25 @@ static bool windowIsChromeless(const Window &window) {
|
|
|
2328
2683
|
return window.titlebar_style == 3;
|
|
2329
2684
|
}
|
|
2330
2685
|
|
|
2686
|
+
/* Windows whose top resize frame lives INSIDE the client, answered by
|
|
2687
|
+
* the window's own WM_NCHITTEST and the top resize strip: the hidden
|
|
2688
|
+
* styles, and chromeless windows. On Windows 10 a caption-less frame
|
|
2689
|
+
* with any non-client top draws it as a blank white bar, so chromeless
|
|
2690
|
+
* keeps real left/right/bottom borders (resize and shadow) and hands
|
|
2691
|
+
* the client the top edge, the frame Tauri gives its undecorated
|
|
2692
|
+
* windows. Two chromeless shapes keep the plain popup frame: transparent
|
|
2693
|
+
* windows, whose per-pixel present seeds hit-testable resize bands
|
|
2694
|
+
* around the client, and windows carrying a menu bar, which needs the
|
|
2695
|
+
* non-client top. */
|
|
2696
|
+
static bool windowReclaimsTopFrame(const Window &window, bool has_menu) {
|
|
2697
|
+
if (windowUsesHiddenTitlebar(window)) return true;
|
|
2698
|
+
return windowIsChromeless(window) && !window.transparent && !has_menu;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
static bool windowReclaimsTopFrame(const Window &window) {
|
|
2702
|
+
return windowReclaimsTopFrame(window, window.hwnd && GetMenu(window.hwnd) != nullptr);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2331
2705
|
static Window *hiddenTitlebarWindowForHwnd(Host *host, HWND hwnd) {
|
|
2332
2706
|
if (!host || !hwnd) return nullptr;
|
|
2333
2707
|
for (auto &entry : host->windows) {
|
|
@@ -2438,6 +2812,168 @@ static int hiddenFrameTopThickness(HWND hwnd) {
|
|
|
2438
2812
|
return systemMetricForDpi(SM_CYSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2439
2813
|
}
|
|
2440
2814
|
|
|
2815
|
+
/* The top resize band's hit code for a client point: `thickness` rows
|
|
2816
|
+
* tall, with corner slivers as wide as the side borders so the diagonal
|
|
2817
|
+
* grips survive at the very top; HTNOWHERE outside the band. */
|
|
2818
|
+
static constexpr LRESULT topResizeBandHitCode(int x, int y, int client_width, int thickness, int corner) {
|
|
2819
|
+
if (y < 0 || y >= thickness || x < 0 || x >= client_width) return HTNOWHERE;
|
|
2820
|
+
if (x < corner) return HTTOPLEFT;
|
|
2821
|
+
if (x >= client_width - corner) return HTTOPRIGHT;
|
|
2822
|
+
return HTTOP;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
static_assert(
|
|
2826
|
+
topResizeBandHitCode(3, 2, 1040, 8, 8) == HTTOPLEFT &&
|
|
2827
|
+
topResizeBandHitCode(8, 2, 1040, 8, 8) == HTTOP &&
|
|
2828
|
+
topResizeBandHitCode(1031, 7, 1040, 8, 8) == HTTOP &&
|
|
2829
|
+
topResizeBandHitCode(1032, 7, 1040, 8, 8) == HTTOPRIGHT &&
|
|
2830
|
+
topResizeBandHitCode(520, 8, 1040, 8, 8) == HTNOWHERE &&
|
|
2831
|
+
topResizeBandHitCode(520, -1, 1040, 8, 8) == HTNOWHERE,
|
|
2832
|
+
"the top band is exactly `thickness` rows tall with `corner`-wide diagonal grips");
|
|
2833
|
+
|
|
2834
|
+
/* The top resize band of a window that reclaims its top frame, in the
|
|
2835
|
+
* window's client coordinates. Only a restored resizable window has one:
|
|
2836
|
+
* a maximized window does not resize and a fixed one never does. Every
|
|
2837
|
+
* surface that answers for the band — the window itself, canvas children
|
|
2838
|
+
* handing it back, and the top resize strip — asks here. */
|
|
2839
|
+
static LRESULT topResizeBandHit(const Window &window, POINT client) {
|
|
2840
|
+
if (!window.hwnd || !windowReclaimsTopFrame(window)) return HTNOWHERE;
|
|
2841
|
+
if (!window.resizable || IsZoomed(window.hwnd) || IsIconic(window.hwnd)) return HTNOWHERE;
|
|
2842
|
+
RECT rect = {};
|
|
2843
|
+
GetClientRect(window.hwnd, &rect);
|
|
2844
|
+
const UINT dpi = dpiForWindow(window.hwnd);
|
|
2845
|
+
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2846
|
+
return topResizeBandHitCode(client.x, client.y, rect.right - rect.left, hiddenFrameTopThickness(window.hwnd), corner);
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/* ------------------------------------------------------------------------
|
|
2850
|
+
* Top resize strip.
|
|
2851
|
+
*
|
|
2852
|
+
* A child covering the top band must hand it back to the window. Canvas
|
|
2853
|
+
* surfaces in hidden-titlebar windows do (they answer HTTRANSPARENT
|
|
2854
|
+
* there), but a WebView2 child belongs to the browser process and never
|
|
2855
|
+
* consults its parent, so under WebView content the band was only
|
|
2856
|
+
* resizable where WebView2's own drag-region windows happened to answer
|
|
2857
|
+
* it. The strip is a same-thread, unpainted child kept above every
|
|
2858
|
+
* content child across the band: it answers the window's band hit codes
|
|
2859
|
+
* itself (the cursor follows from them) and forwards the press to the
|
|
2860
|
+
* window, whose DefWindowProc runs the system size loop. Tauri recovers
|
|
2861
|
+
* the top edge of its undecorated windows the same way. WS_EX_TRANSPARENT
|
|
2862
|
+
* keeps the strip out of its siblings' visible regions: the WebView
|
|
2863
|
+
* containers clip siblings, and without it the band shows the window
|
|
2864
|
+
* background instead of the content under it. */
|
|
2865
|
+
|
|
2866
|
+
static Window *windowForTopResizeStrip(Host *host, HWND strip) {
|
|
2867
|
+
if (!host || !strip) return nullptr;
|
|
2868
|
+
for (auto &entry : host->windows) {
|
|
2869
|
+
if (entry.second.top_resize_strip == strip) return &entry.second;
|
|
2870
|
+
}
|
|
2871
|
+
return nullptr;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
static LRESULT CALLBACK topResizeStripProc(HWND strip, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
2875
|
+
Host *host = reinterpret_cast<Host *>(GetWindowLongPtrW(strip, GWLP_USERDATA));
|
|
2876
|
+
switch (message) {
|
|
2877
|
+
case WM_NCHITTEST: {
|
|
2878
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2879
|
+
if (!window || !window->hwnd) return HTTRANSPARENT;
|
|
2880
|
+
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
2881
|
+
ScreenToClient(window->hwnd, &point);
|
|
2882
|
+
const LRESULT hit = topResizeBandHit(*window, point);
|
|
2883
|
+
return hit != HTNOWHERE ? hit : HTTRANSPARENT;
|
|
2884
|
+
}
|
|
2885
|
+
case WM_NCLBUTTONDOWN:
|
|
2886
|
+
case WM_NCLBUTTONDBLCLK:
|
|
2887
|
+
/* Same thread, so the window's own non-client handling runs
|
|
2888
|
+
* synchronously with the original screen point: the size loop
|
|
2889
|
+
* for a press, the vertical maximize for a double-click. */
|
|
2890
|
+
if (wparam == HTTOP || wparam == HTTOPLEFT || wparam == HTTOPRIGHT) {
|
|
2891
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2892
|
+
if (window && window->hwnd) return SendMessageW(window->hwnd, message, wparam, lparam);
|
|
2893
|
+
}
|
|
2894
|
+
break;
|
|
2895
|
+
case WM_ERASEBKGND:
|
|
2896
|
+
return 1;
|
|
2897
|
+
case WM_PAINT: {
|
|
2898
|
+
PAINTSTRUCT paint = {};
|
|
2899
|
+
BeginPaint(strip, &paint);
|
|
2900
|
+
EndPaint(strip, &paint);
|
|
2901
|
+
return 0;
|
|
2902
|
+
}
|
|
2903
|
+
default:
|
|
2904
|
+
break;
|
|
2905
|
+
}
|
|
2906
|
+
return DefWindowProcW(strip, message, wparam, lparam);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
static const wchar_t *topResizeStripClassName(Host *host) {
|
|
2910
|
+
static bool registered = false;
|
|
2911
|
+
if (!registered) {
|
|
2912
|
+
WNDCLASSEXW wc = {};
|
|
2913
|
+
wc.cbSize = sizeof(wc);
|
|
2914
|
+
wc.lpfnWndProc = topResizeStripProc;
|
|
2915
|
+
wc.hInstance = host->instance;
|
|
2916
|
+
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
2917
|
+
wc.lpszClassName = L"NativeSdkTopResizeStrip";
|
|
2918
|
+
registered = RegisterClassExW(&wc) != 0;
|
|
2919
|
+
}
|
|
2920
|
+
return L"NativeSdkTopResizeStrip";
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
/* Whether any WebView container reaches into the window's top band. */
|
|
2924
|
+
static bool webViewCoversTopBand(Host *host, const Window &window, int band) {
|
|
2925
|
+
RECT client = {};
|
|
2926
|
+
GetClientRect(window.hwnd, &client);
|
|
2927
|
+
const RECT band_rect = { 0, 0, client.right - client.left, band };
|
|
2928
|
+
for (auto &entry : host->webviews) {
|
|
2929
|
+
const ChildWebView &webview = entry.second;
|
|
2930
|
+
if (webview.window_id != window.id || !webview.hwnd) continue;
|
|
2931
|
+
if (!(GetWindowLongPtrW(webview.hwnd, GWL_STYLE) & WS_VISIBLE)) continue;
|
|
2932
|
+
RECT bounds = {};
|
|
2933
|
+
GetWindowRect(webview.hwnd, &bounds);
|
|
2934
|
+
POINT top_left = { bounds.left, bounds.top };
|
|
2935
|
+
POINT bottom_right = { bounds.right, bounds.bottom };
|
|
2936
|
+
ScreenToClient(window.hwnd, &top_left);
|
|
2937
|
+
ScreenToClient(window.hwnd, &bottom_right);
|
|
2938
|
+
const RECT local = { top_left.x, top_left.y, bottom_right.x, bottom_right.y };
|
|
2939
|
+
RECT overlap = {};
|
|
2940
|
+
if (IntersectRect(&overlap, &local, &band_rect)) return true;
|
|
2941
|
+
}
|
|
2942
|
+
return false;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
/* Show, size and raise the strip over the band of a restored resizable
|
|
2946
|
+
* window that reclaims its top frame; hide it otherwise. Chromeless
|
|
2947
|
+
* windows always take it: their content children answer the band as
|
|
2948
|
+
* client. Hidden-titlebar windows take it only while a WebView covers the
|
|
2949
|
+
* band, so canvas windows keep their own band and the DWM keeps first
|
|
2950
|
+
* claim on its caption buttons. Runs after every geometry, state, or
|
|
2951
|
+
* child z-order change. */
|
|
2952
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id) {
|
|
2953
|
+
if (!host) return;
|
|
2954
|
+
auto found = host->windows.find(window_id);
|
|
2955
|
+
if (found == host->windows.end() || !found->second.hwnd) return;
|
|
2956
|
+
Window &window = found->second;
|
|
2957
|
+
const int band = hiddenFrameTopThickness(window.hwnd);
|
|
2958
|
+
const bool wanted = windowReclaimsTopFrame(window) && window.resizable && !window.click_through &&
|
|
2959
|
+
!IsZoomed(window.hwnd) && !IsIconic(window.hwnd) &&
|
|
2960
|
+
(windowIsChromeless(window) || webViewCoversTopBand(host, window, band));
|
|
2961
|
+
if (!wanted) {
|
|
2962
|
+
if (window.top_resize_strip) ShowWindow(window.top_resize_strip, SW_HIDE);
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
if (!window.top_resize_strip) {
|
|
2966
|
+
HWND strip = CreateWindowExW(WS_EX_TRANSPARENT, topResizeStripClassName(host), L"", WS_CHILD | WS_CLIPSIBLINGS,
|
|
2967
|
+
0, 0, 0, 0, window.hwnd, nullptr, host->instance, nullptr);
|
|
2968
|
+
if (!strip) return;
|
|
2969
|
+
SetWindowLongPtrW(strip, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(host));
|
|
2970
|
+
window.top_resize_strip = strip;
|
|
2971
|
+
}
|
|
2972
|
+
RECT client = {};
|
|
2973
|
+
GetClientRect(window.hwnd, &client);
|
|
2974
|
+
SetWindowPos(window.top_resize_strip, HWND_TOP, 0, 0, client.right - client.left, band, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2441
2977
|
/* Height of the standard caption band (frame + caption) at the window's
|
|
2442
2978
|
* dpi — the DWM frame extension depth, and the chrome-channel top inset
|
|
2443
2979
|
* fallback when the live button rects are unavailable. */
|
|
@@ -3448,7 +3984,7 @@ static LRESULT CALLBACK gpuSurfaceProc(HWND hwnd, UINT message, WPARAM wparam, L
|
|
|
3448
3984
|
if (!chrome_window) break;
|
|
3449
3985
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
3450
3986
|
ScreenToClient(chrome_window->hwnd, &point);
|
|
3451
|
-
if (
|
|
3987
|
+
if (topResizeBandHit(*chrome_window, point) != HTNOWHERE) return HTTRANSPARENT;
|
|
3452
3988
|
RECT cluster = {};
|
|
3453
3989
|
if (captionButtonsClientRect(chrome_window->hwnd, &cluster) && PtInRect(&cluster, point)) return HTTRANSPARENT;
|
|
3454
3990
|
if (windowDragRegionHit(host, *chrome_window, point)) return HTTRANSPARENT;
|
|
@@ -3715,8 +4251,7 @@ constexpr WPARAM kAudioNoteBufferingStarted = 6;
|
|
|
3715
4251
|
constexpr WPARAM kAudioNoteBufferingStopped = 7;
|
|
3716
4252
|
constexpr WPARAM kAudioNoteError = 8;
|
|
3717
4253
|
|
|
3718
|
-
/* Position tick on the
|
|
3719
|
-
* 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
|
|
3720
4255
|
* platform tick the same), so frame-clock scrubber interpolation
|
|
3721
4256
|
* behaves identically across hosts. */
|
|
3722
4257
|
constexpr UINT_PTR kAudioPositionTimerId = 0x3000;
|
|
@@ -3839,20 +4374,20 @@ struct AudioSessionEventForwarder final : public IMFAsyncCallback {
|
|
|
3839
4374
|
}
|
|
3840
4375
|
};
|
|
3841
4376
|
|
|
3842
|
-
/* Completes an asynchronous URL source resolution
|
|
3843
|
-
*
|
|
3844
|
-
*
|
|
3845
|
-
* 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. */
|
|
3846
4382
|
struct AudioSourceResolveForwarder final : public IMFAsyncCallback {
|
|
3847
4383
|
LONG refs = 1;
|
|
3848
4384
|
IMFSourceResolver *resolver = nullptr;
|
|
3849
|
-
|
|
3850
|
-
std::shared_ptr<HostLifetime> lifetime;
|
|
4385
|
+
std::shared_ptr<AudioSourceMailbox> mailbox;
|
|
3851
4386
|
HWND hwnd = nullptr;
|
|
3852
4387
|
uint64_t generation = 0;
|
|
3853
4388
|
|
|
3854
|
-
AudioSourceResolveForwarder(IMFSourceResolver *resolver_in,
|
|
3855
|
-
: 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) {
|
|
3856
4391
|
resolver->AddRef();
|
|
3857
4392
|
}
|
|
3858
4393
|
~AudioSourceResolveForwarder() { resolver->Release(); }
|
|
@@ -3884,23 +4419,24 @@ struct AudioSourceResolveForwarder final : public IMFAsyncCallback {
|
|
|
3884
4419
|
IMFMediaSource *source = nullptr;
|
|
3885
4420
|
if (SUCCEEDED(hr) && object) object->QueryInterface(kNativeSdkIID_IMFMediaSource, reinterpret_cast<void **>(&source));
|
|
3886
4421
|
if (object) object->Release();
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
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;
|
|
3893
4431
|
}
|
|
3894
|
-
return S_OK;
|
|
3895
4432
|
}
|
|
3896
|
-
if (!source) {
|
|
3897
|
-
|
|
3898
|
-
|
|
4433
|
+
if (!kept && source) {
|
|
4434
|
+
source->Shutdown();
|
|
4435
|
+
source->Release();
|
|
3899
4436
|
}
|
|
3900
|
-
/*
|
|
3901
|
-
*
|
|
3902
|
-
|
|
3903
|
-
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);
|
|
3904
4440
|
return S_OK;
|
|
3905
4441
|
}
|
|
3906
4442
|
};
|
|
@@ -4074,7 +4610,7 @@ static void audioStopPositionTimer(Host *host) {
|
|
|
4074
4610
|
static void audioStartPositionTimer(Host *host) {
|
|
4075
4611
|
AudioState &audio = host->audio;
|
|
4076
4612
|
if (audio.position_timer_armed) return;
|
|
4077
|
-
HWND hwnd =
|
|
4613
|
+
HWND hwnd = loopWindow(host);
|
|
4078
4614
|
if (!hwnd) return;
|
|
4079
4615
|
if (SetTimer(hwnd, kAudioPositionTimerId, kAudioPositionIntervalMs, nullptr)) {
|
|
4080
4616
|
audio.timer_hwnd = hwnd;
|
|
@@ -4471,7 +5007,7 @@ static void audioSpectrumStartCapture(Host *host) {
|
|
|
4471
5007
|
AudioSpectrumState &spectrum = host->spectrum;
|
|
4472
5008
|
if (spectrum.shared) return;
|
|
4473
5009
|
if (!audioSpectrumSupported()) return;
|
|
4474
|
-
HWND hwnd =
|
|
5010
|
+
HWND hwnd = loopWindow(host);
|
|
4475
5011
|
if (!hwnd) return;
|
|
4476
5012
|
spectrum.generation += 1;
|
|
4477
5013
|
spectrum.shared = std::make_shared<AudioSpectrumShared>();
|
|
@@ -4657,7 +5193,6 @@ static void audioCaptureStopAll(Host *host) {
|
|
|
4657
5193
|
* letting a straggler finish installs the cache entry the completed
|
|
4658
5194
|
* play earned. */
|
|
4659
5195
|
static void audioReleaseSession(Host *host, bool cancel_download) {
|
|
4660
|
-
std::lock_guard<std::recursive_mutex> guard(host->lifetime->mutex);
|
|
4661
5196
|
AudioState &audio = host->audio;
|
|
4662
5197
|
audioStopPositionTimer(host);
|
|
4663
5198
|
audioSpectrumStopCapture(host);
|
|
@@ -4684,6 +5219,22 @@ static void audioReleaseSession(Host *host, bool cancel_download) {
|
|
|
4684
5219
|
audio.source->Release();
|
|
4685
5220
|
audio.source = nullptr;
|
|
4686
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
|
+
}
|
|
4687
5238
|
if (cancel_download && audio.download_cancel) audio.download_cancel->cancelled.store(true);
|
|
4688
5239
|
const uint64_t generation = audio.generation;
|
|
4689
5240
|
audio = AudioState{};
|
|
@@ -4698,7 +5249,7 @@ static bool audioAttachSession(Host *host) {
|
|
|
4698
5249
|
AudioState &audio = host->audio;
|
|
4699
5250
|
IMFMediaSource *source = audio.source;
|
|
4700
5251
|
if (!source) return false;
|
|
4701
|
-
HWND hwnd =
|
|
5252
|
+
HWND hwnd = loopWindow(host);
|
|
4702
5253
|
if (!hwnd) return false;
|
|
4703
5254
|
IMFMediaSession *session = nullptr;
|
|
4704
5255
|
if (FAILED(MFCreateMediaSession(nullptr, &session)) || !session) return false;
|
|
@@ -4863,7 +5414,7 @@ static int audioLoadUrlInternal(Host *host, const std::string &url, const std::s
|
|
|
4863
5414
|
}
|
|
4864
5415
|
}
|
|
4865
5416
|
if (!audioEnsureMediaFoundation()) return 2;
|
|
4866
|
-
HWND hwnd =
|
|
5417
|
+
HWND hwnd = loopWindow(host);
|
|
4867
5418
|
if (!hwnd) return 2;
|
|
4868
5419
|
IMFSourceResolver *resolver = nullptr;
|
|
4869
5420
|
if (FAILED(MFCreateSourceResolver(&resolver)) || !resolver) return 2;
|
|
@@ -4873,7 +5424,8 @@ static int audioLoadUrlInternal(Host *host, const std::string &url, const std::s
|
|
|
4873
5424
|
/* A fresh stream has no bytes yet: buffering starts true and drops
|
|
4874
5425
|
* when the session actually starts rolling. */
|
|
4875
5426
|
audio.buffering = true;
|
|
4876
|
-
|
|
5427
|
+
audio.resolve_mailbox = std::make_shared<AudioSourceMailbox>();
|
|
5428
|
+
AudioSourceResolveForwarder *forwarder = new AudioSourceResolveForwarder(resolver, audio.resolve_mailbox, hwnd, audio.generation);
|
|
4877
5429
|
const HRESULT hr = resolver->BeginCreateObjectFromURL(widen(url).c_str(), MF_RESOLUTION_MEDIASOURCE, nullptr, nullptr, forwarder, nullptr);
|
|
4878
5430
|
forwarder->Release();
|
|
4879
5431
|
resolver->Release();
|
|
@@ -4928,16 +5480,26 @@ static void audioFailed(Host *host) {
|
|
|
4928
5480
|
audioEmitReport(host, kAudioEventFailed, 0, 0, 0, 0);
|
|
4929
5481
|
}
|
|
4930
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
|
+
|
|
4931
5494
|
/* Distilled session notes, marshalled from Media Foundation worker
|
|
4932
5495
|
* threads via PostMessage; loop thread. Stale generations (a replaced
|
|
4933
5496
|
* or stopped playback's stragglers) are dropped here. */
|
|
4934
5497
|
static void audioHandleSessionMessage(Host *host, WPARAM note, LPARAM generation) {
|
|
4935
|
-
std::lock_guard<std::recursive_mutex> guard(host->lifetime->mutex);
|
|
4936
5498
|
AudioState &audio = host->audio;
|
|
4937
5499
|
if (!audio.active || (uint64_t)generation != audio.generation) return;
|
|
4938
5500
|
switch (note) {
|
|
4939
5501
|
case kAudioNoteSourceResolved:
|
|
4940
|
-
if (!audioAttachSession(host)) audioFailed(host);
|
|
5502
|
+
if (!audioCollectResolvedSource(host) || !audioAttachSession(host)) audioFailed(host);
|
|
4941
5503
|
break;
|
|
4942
5504
|
case kAudioNoteSourceFailed:
|
|
4943
5505
|
case kAudioNoteError:
|
|
@@ -5010,6 +5572,7 @@ static void destroyAllWindows(Host *host) {
|
|
|
5010
5572
|
if (entry.second.hwnd) {
|
|
5011
5573
|
DestroyWindow(entry.second.hwnd);
|
|
5012
5574
|
entry.second.hwnd = nullptr;
|
|
5575
|
+
entry.second.top_resize_strip = nullptr;
|
|
5013
5576
|
}
|
|
5014
5577
|
}
|
|
5015
5578
|
}
|
|
@@ -5335,13 +5898,90 @@ static CreateEnvironmentFn webView2Factory() {
|
|
|
5335
5898
|
return reinterpret_cast<CreateEnvironmentFn>(GetProcAddress(loader, "CreateCoreWebView2EnvironmentWithOptions"));
|
|
5336
5899
|
}
|
|
5337
5900
|
|
|
5901
|
+
static bool webViewDirectoryExists(const std::wstring &path) {
|
|
5902
|
+
const DWORD attributes = GetFileAttributesW(path.c_str());
|
|
5903
|
+
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
/* The folder WebView2 picks when none is given: "<exe>.WebView2" beside
|
|
5907
|
+
* the executable. Earlier hosts left their data there. */
|
|
5908
|
+
static std::wstring webViewLegacyUserDataFolder() {
|
|
5909
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
5910
|
+
for (;;) {
|
|
5911
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
5912
|
+
if (length == 0) return std::wstring();
|
|
5913
|
+
if (length < path.size()) {
|
|
5914
|
+
path.resize(length);
|
|
5915
|
+
return path + L".WebView2";
|
|
5916
|
+
}
|
|
5917
|
+
if (path.size() >= 32768) return std::wstring();
|
|
5918
|
+
path.resize(path.size() * 2);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5922
|
+
/* WebView2 keeps cookies, storage and caches in its user data folder.
|
|
5923
|
+
* Its default sits beside the executable, which is read-only under
|
|
5924
|
+
* Program Files and is replaced together with the program files when
|
|
5925
|
+
* an installed app updates. Keep it with the app's other per-user data
|
|
5926
|
+
* instead: %LOCALAPPDATA%\<bundle id>\WebView2, next to the Data, Cache,
|
|
5927
|
+
* State and Logs folders app_dirs resolves from the same variable and
|
|
5928
|
+
* bundle id. A folder left beside the executable by an earlier host
|
|
5929
|
+
* moves there once. A folder on another volume cannot be renamed there,
|
|
5930
|
+
* so that install keeps its old folder; when the move fails for another
|
|
5931
|
+
* reason (another process still has the folder open) this launch keeps
|
|
5932
|
+
* the old folder and the next launch tries again. Either way no data is
|
|
5933
|
+
* lost. Without LOCALAPPDATA or a usable bundle id, WebView2 keeps its
|
|
5934
|
+
* default. */
|
|
5935
|
+
static const wchar_t *webViewUserDataFolder(Host *host) {
|
|
5936
|
+
if (host->webview_user_data_folder_resolved) {
|
|
5937
|
+
return host->webview_user_data_folder.empty() ? nullptr : host->webview_user_data_folder.c_str();
|
|
5938
|
+
}
|
|
5939
|
+
host->webview_user_data_folder_resolved = true;
|
|
5940
|
+
const std::string &bundle_id = host->bundle_id;
|
|
5941
|
+
if (bundle_id.empty() || bundle_id == "." || bundle_id == ".." ||
|
|
5942
|
+
bundle_id.find_first_of("\\/:*?\"<>|") != std::string::npos) return nullptr;
|
|
5943
|
+
for (unsigned char ch : bundle_id) {
|
|
5944
|
+
if (ch < 0x20) return nullptr;
|
|
5945
|
+
}
|
|
5946
|
+
const DWORD needed = GetEnvironmentVariableW(L"LOCALAPPDATA", nullptr, 0);
|
|
5947
|
+
if (needed <= 1) return nullptr;
|
|
5948
|
+
std::wstring local_app_data((size_t)needed, L'\0');
|
|
5949
|
+
const DWORD written = GetEnvironmentVariableW(L"LOCALAPPDATA", local_app_data.data(), needed);
|
|
5950
|
+
if (written == 0 || written >= needed) return nullptr;
|
|
5951
|
+
local_app_data.resize(written);
|
|
5952
|
+
while (!local_app_data.empty() && (local_app_data.back() == L'\\' || local_app_data.back() == L'/')) local_app_data.pop_back();
|
|
5953
|
+
if (local_app_data.empty()) return nullptr;
|
|
5954
|
+
|
|
5955
|
+
const std::wstring app_root = local_app_data + L"\\" + widen(bundle_id);
|
|
5956
|
+
const std::wstring folder = app_root + L"\\WebView2";
|
|
5957
|
+
SHCreateDirectoryExW(nullptr, app_root.c_str(), nullptr);
|
|
5958
|
+
if (!webViewDirectoryExists(app_root)) return nullptr;
|
|
5959
|
+
|
|
5960
|
+
if (!webViewDirectoryExists(folder)) {
|
|
5961
|
+
const std::wstring legacy = webViewLegacyUserDataFolder();
|
|
5962
|
+
if (!legacy.empty() && webViewDirectoryExists(legacy) && !MoveFileExW(legacy.c_str(), folder.c_str(), 0)) {
|
|
5963
|
+
const DWORD error = GetLastError();
|
|
5964
|
+
if (error != ERROR_NOT_SAME_DEVICE) {
|
|
5965
|
+
fprintf(stderr, "native-sdk: keeping WebView2 data in %s for this launch; moving it to %s failed (error %lu)\n",
|
|
5966
|
+
narrow(legacy).c_str(), narrow(folder).c_str(), static_cast<unsigned long>(error));
|
|
5967
|
+
}
|
|
5968
|
+
host->webview_user_data_folder = legacy;
|
|
5969
|
+
return host->webview_user_data_folder.c_str();
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
host->webview_user_data_folder = folder;
|
|
5973
|
+
return host->webview_user_data_folder.c_str();
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5338
5976
|
static void cleanupPendingChildWebView(Host *host, const std::string &key) {
|
|
5339
5977
|
if (!host) return;
|
|
5340
5978
|
auto found = host->webviews.find(key);
|
|
5341
5979
|
if (found == host->webviews.end()) return;
|
|
5980
|
+
const uint64_t window_id = found->second.window_id;
|
|
5342
5981
|
if (found->second.controller) found->second.controller->Close();
|
|
5343
5982
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
5344
5983
|
host->webviews.erase(found);
|
|
5984
|
+
syncTopResizeStrip(host, window_id);
|
|
5345
5985
|
}
|
|
5346
5986
|
|
|
5347
5987
|
static void failWindowDragInitialization(Host *host, const std::string &key, HRESULT result) {
|
|
@@ -5361,12 +6001,12 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5361
6001
|
HWND parent = found->second.hwnd;
|
|
5362
6002
|
const uint64_t generation = found->second.window_drag_generation;
|
|
5363
6003
|
std::weak_ptr<HostLifetime> lifetime = host->lifetime;
|
|
5364
|
-
HRESULT hr = factory(nullptr,
|
|
6004
|
+
HRESULT hr = factory(nullptr, webViewUserDataFolder(host), nullptr, Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
|
5365
6005
|
[host, key, parent, lifetime, generation](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
|
|
5366
6006
|
auto token = lifetime.lock();
|
|
5367
6007
|
if (!token) return S_OK;
|
|
5368
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5369
6008
|
if (!token->alive) return S_OK;
|
|
6009
|
+
WebViewCallbackScope scope(host);
|
|
5370
6010
|
auto found = host->webviews.find(key);
|
|
5371
6011
|
if (found == host->webviews.end() || found->second.hwnd != parent || found->second.window_drag_generation != generation || !IsWindow(parent)) return S_OK;
|
|
5372
6012
|
if (FAILED(result) || !environment) {
|
|
@@ -5382,11 +6022,11 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5382
6022
|
if (controller) controller->Close();
|
|
5383
6023
|
return S_OK;
|
|
5384
6024
|
}
|
|
5385
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5386
6025
|
if (!token->alive) {
|
|
5387
6026
|
if (controller) controller->Close();
|
|
5388
6027
|
return S_OK;
|
|
5389
6028
|
}
|
|
6029
|
+
WebViewCallbackScope scope(host);
|
|
5390
6030
|
auto found = host->webviews.find(key);
|
|
5391
6031
|
if (found == host->webviews.end() || found->second.window_drag_generation != generation) {
|
|
5392
6032
|
if (controller) controller->Close();
|
|
@@ -5413,16 +6053,16 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5413
6053
|
[host, key, lifetime](ICoreWebView2Controller *, IUnknown *) -> HRESULT {
|
|
5414
6054
|
auto token = lifetime.lock();
|
|
5415
6055
|
if (!token) return S_OK;
|
|
5416
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5417
6056
|
if (!token->alive || !host->callback) return S_OK;
|
|
6057
|
+
WebViewCallbackScope scope(host);
|
|
5418
6058
|
auto focused = host->webviews.find(key);
|
|
5419
6059
|
if (focused == host->webviews.end()) return S_OK;
|
|
5420
|
-
|
|
5421
|
-
event.kind =
|
|
6060
|
+
DeferredWebViewEvent event;
|
|
6061
|
+
event.kind = DeferredWebViewEvent::Kind::Focus;
|
|
5422
6062
|
event.window_id = focused->second.window_id;
|
|
5423
|
-
event.
|
|
5424
|
-
event.
|
|
5425
|
-
|
|
6063
|
+
event.view_key = key;
|
|
6064
|
+
event.label = focused->second.label;
|
|
6065
|
+
queueWebViewEvent(host, std::move(event));
|
|
5426
6066
|
return S_OK;
|
|
5427
6067
|
}).Get(), &focus_token);
|
|
5428
6068
|
if (found->second.webview) {
|
|
@@ -5451,8 +6091,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5451
6091
|
[host, key, lifetime, drag_generation](HRESULT result, LPCWSTR) -> HRESULT {
|
|
5452
6092
|
auto token = lifetime.lock();
|
|
5453
6093
|
if (!token) return S_OK;
|
|
5454
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5455
6094
|
if (!token->alive) return S_OK;
|
|
6095
|
+
WebViewCallbackScope scope(host);
|
|
5456
6096
|
auto current = host->webviews.find(key);
|
|
5457
6097
|
if (current == host->webviews.end() || current->second.window_drag_generation != drag_generation) return S_OK;
|
|
5458
6098
|
if (FAILED(result)) {
|
|
@@ -5480,8 +6120,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5480
6120
|
[host, key, bridge_window_id, bridge_label, lifetime](ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *args) -> HRESULT {
|
|
5481
6121
|
auto token = lifetime.lock();
|
|
5482
6122
|
if (!token) return S_OK;
|
|
5483
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5484
6123
|
if (!token->alive || !args || !host->bridge_callback) return S_OK;
|
|
6124
|
+
WebViewCallbackScope scope(host);
|
|
5485
6125
|
|
|
5486
6126
|
LPWSTR message_bytes = nullptr;
|
|
5487
6127
|
if (FAILED(args->TryGetWebMessageAsString(&message_bytes)) || !message_bytes) return S_OK;
|
|
@@ -5501,15 +6141,14 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5501
6141
|
: bridgeOriginForWebViewUrl(source_webview->second, source_url);
|
|
5502
6142
|
}
|
|
5503
6143
|
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
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));
|
|
5513
6152
|
return S_OK;
|
|
5514
6153
|
}).Get(), &bridge_token);
|
|
5515
6154
|
}
|
|
@@ -5519,16 +6158,21 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5519
6158
|
[host, accelerator_window_id, lifetime](ICoreWebView2Controller *, ICoreWebView2AcceleratorKeyPressedEventArgs *args) -> HRESULT {
|
|
5520
6159
|
auto token = lifetime.lock();
|
|
5521
6160
|
if (!token) return S_OK;
|
|
5522
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5523
6161
|
if (!token->alive || !args) return S_OK;
|
|
6162
|
+
WebViewCallbackScope scope(host);
|
|
5524
6163
|
COREWEBVIEW2_KEY_EVENT_KIND kind = COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN;
|
|
5525
6164
|
if (FAILED(args->get_KeyEventKind(&kind))) return S_OK;
|
|
5526
6165
|
if (kind != COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN && kind != COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN) return S_OK;
|
|
5527
6166
|
UINT virtual_key = 0;
|
|
5528
6167
|
if (FAILED(args->get_VirtualKey(&virtual_key))) return S_OK;
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
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));
|
|
5532
6176
|
return S_OK;
|
|
5533
6177
|
}).Get(), &accelerator_token);
|
|
5534
6178
|
|
|
@@ -5538,8 +6182,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5538
6182
|
[host, key, environment_ref, lifetime](ICoreWebView2 *, ICoreWebView2WebResourceRequestedEventArgs *args) -> HRESULT {
|
|
5539
6183
|
auto token = lifetime.lock();
|
|
5540
6184
|
if (!token) return S_OK;
|
|
5541
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5542
6185
|
if (!token->alive || !args) return S_OK;
|
|
6186
|
+
WebViewCallbackScope scope(host);
|
|
5543
6187
|
auto found = host->webviews.find(key);
|
|
5544
6188
|
if (found == host->webviews.end()) return S_OK;
|
|
5545
6189
|
ComPtr<ICoreWebView2WebResourceRequest> request;
|
|
@@ -5560,8 +6204,8 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5560
6204
|
[host, key, lifetime](ICoreWebView2 *, ICoreWebView2NavigationStartingEventArgs *args) -> HRESULT {
|
|
5561
6205
|
auto token = lifetime.lock();
|
|
5562
6206
|
if (!token) return S_OK;
|
|
5563
|
-
std::lock_guard<std::recursive_mutex> guard(token->mutex);
|
|
5564
6207
|
if (!token->alive) return S_OK;
|
|
6208
|
+
WebViewCallbackScope scope(host);
|
|
5565
6209
|
LPWSTR uri_bytes = nullptr;
|
|
5566
6210
|
if (!args || FAILED(args->get_Uri(&uri_bytes))) return S_OK;
|
|
5567
6211
|
std::wstring uri_wide = uri_bytes ? std::wstring(uri_bytes) : std::wstring();
|
|
@@ -5571,7 +6215,10 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5571
6215
|
if (found != host->webviews.end() && isInternalAssetUrl(found->second, uri)) return S_OK;
|
|
5572
6216
|
if (uri.empty() || uri.rfind("about:", 0) == 0 || policyListMatches(host->allowed_origins, uri)) return S_OK;
|
|
5573
6217
|
if (host->external_link_action == 1 && policyListMatches(host->allowed_external_urls, uri)) {
|
|
5574
|
-
|
|
6218
|
+
DeferredWebViewEvent event;
|
|
6219
|
+
event.kind = DeferredWebViewEvent::Kind::OpenExternal;
|
|
6220
|
+
event.url = uri_wide;
|
|
6221
|
+
queueWebViewEvent(host, std::move(event));
|
|
5575
6222
|
}
|
|
5576
6223
|
args->put_Cancel(TRUE);
|
|
5577
6224
|
return S_OK;
|
|
@@ -5758,6 +6405,145 @@ static void clampChromelessMaximizedFrame(Host *host, HWND hwnd, WINDOWPOS *pos)
|
|
|
5758
6405
|
pos->flags &= ~(SWP_NOMOVE | SWP_NOSIZE);
|
|
5759
6406
|
}
|
|
5760
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
|
+
|
|
5761
6547
|
static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
5762
6548
|
if (message == WM_NCCREATE) {
|
|
5763
6549
|
auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);
|
|
@@ -5818,21 +6604,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5818
6604
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5819
6605
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5820
6606
|
ScreenToClient(hwnd, &point);
|
|
5821
|
-
RECT client = {};
|
|
5822
|
-
GetClientRect(hwnd, &client);
|
|
5823
6607
|
/* Top resize band: the caption removal moved the top
|
|
5824
6608
|
* frame INSIDE the client area, so hand its band back to
|
|
5825
|
-
* the system
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
* diagonal grips survive at the very top. */
|
|
5829
|
-
if (chrome_window->resizable && !IsZoomed(hwnd) && point.y >= 0 && point.y < hiddenFrameTopThickness(hwnd)) {
|
|
5830
|
-
const UINT dpi = dpiForWindow(hwnd);
|
|
5831
|
-
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
5832
|
-
if (point.x < corner) return HTTOPLEFT;
|
|
5833
|
-
if (point.x >= client.right - corner) return HTTOPRIGHT;
|
|
5834
|
-
return HTTOP;
|
|
5835
|
-
}
|
|
6609
|
+
* the system. */
|
|
6610
|
+
const LRESULT band_hit = topResizeBandHit(*chrome_window, point);
|
|
6611
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5836
6612
|
/* Caption buttons when DwmDefWindowProc stayed silent
|
|
5837
6613
|
* (composition off, older cores): DefWindowProc still
|
|
5838
6614
|
* performs minimize/maximize/close for these hit codes,
|
|
@@ -5865,6 +6641,22 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5865
6641
|
break;
|
|
5866
6642
|
}
|
|
5867
6643
|
}
|
|
6644
|
+
/* Chromeless windows hand the top frame to the client (see
|
|
6645
|
+
* windowReclaimsTopFrame) while restored. A maximized window keeps
|
|
6646
|
+
* DefWindowProc's inset: its frame parks the borders offscreen around
|
|
6647
|
+
* the work area, and the inset is what lands the client exactly on
|
|
6648
|
+
* it. */
|
|
6649
|
+
if (message == WM_NCCALCSIZE && wparam) {
|
|
6650
|
+
Window *chromeless_window = chromelessWindowForHwnd(host, hwnd);
|
|
6651
|
+
if (chromeless_window && windowReclaimsTopFrame(*chromeless_window) && !IsZoomed(hwnd)) {
|
|
6652
|
+
NCCALCSIZE_PARAMS *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(lparam);
|
|
6653
|
+
const LONG original_top = params->rgrc[0].top;
|
|
6654
|
+
const LRESULT def_result = DefWindowProcW(hwnd, WM_NCCALCSIZE, wparam, lparam);
|
|
6655
|
+
if (def_result != 0) return def_result;
|
|
6656
|
+
params->rgrc[0].top = original_top;
|
|
6657
|
+
return 0;
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
5868
6660
|
/* Chromeless windows (WS_POPUP, no caption): DefWindowProc owns the
|
|
5869
6661
|
* resize frame when one exists; the app's window-drag regions are
|
|
5870
6662
|
* the only caption there is, so a client-area hit inside one
|
|
@@ -5877,33 +6669,13 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5877
6669
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5878
6670
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5879
6671
|
ScreenToClient(hwnd, &point);
|
|
6672
|
+
const LRESULT band_hit = topResizeBandHit(*chromeless_window, point);
|
|
6673
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5880
6674
|
if (windowDragRegionHit(host, *chromeless_window, point)) return HTCAPTION;
|
|
5881
6675
|
return HTCLIENT;
|
|
5882
6676
|
}
|
|
5883
6677
|
}
|
|
5884
6678
|
switch (message) {
|
|
5885
|
-
case kWakeMessage:
|
|
5886
|
-
if (host) {
|
|
5887
|
-
WindowsEvent wake = {};
|
|
5888
|
-
wake.kind = kWake;
|
|
5889
|
-
wake.window_id = 1;
|
|
5890
|
-
if (host->callback) host->callback(host->callback_context, &wake);
|
|
5891
|
-
}
|
|
5892
|
-
return 0;
|
|
5893
|
-
case kRequestFrameMessage:
|
|
5894
|
-
if (host) {
|
|
5895
|
-
WindowsEvent frame = {};
|
|
5896
|
-
frame.kind = kFrame;
|
|
5897
|
-
frame.window_id = 1;
|
|
5898
|
-
if (host->callback) host->callback(host->callback_context, &frame);
|
|
5899
|
-
}
|
|
5900
|
-
return 0;
|
|
5901
|
-
case kAudioSessionMessage:
|
|
5902
|
-
if (host) audioHandleSessionMessage(host, wparam, lparam);
|
|
5903
|
-
return 0;
|
|
5904
|
-
case kAudioSpectrumMessage:
|
|
5905
|
-
if (host) audioHandleSpectrumMessage(host, wparam);
|
|
5906
|
-
return 0;
|
|
5907
6679
|
case kShowContextMenuMessage:
|
|
5908
6680
|
if (host) showAppContextMenu(host, hwnd);
|
|
5909
6681
|
return 0;
|
|
@@ -5990,6 +6762,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5990
6762
|
applyWebViewFrame(main->second);
|
|
5991
6763
|
}
|
|
5992
6764
|
#endif
|
|
6765
|
+
/* Size, maximize/restore, minimize, and dpi changes
|
|
6766
|
+
* all land here. */
|
|
6767
|
+
syncTopResizeStrip(host, entry.first);
|
|
5993
6768
|
emit(host, entry.second, kResize);
|
|
5994
6769
|
}
|
|
5995
6770
|
}
|
|
@@ -6084,8 +6859,6 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6084
6859
|
}
|
|
6085
6860
|
break;
|
|
6086
6861
|
case WM_TIMER:
|
|
6087
|
-
if (host && handleAppTimerMessage(host, wparam)) return 0;
|
|
6088
|
-
if (host && handleAudioTimerMessage(host, wparam)) return 0;
|
|
6089
6862
|
if (host && wparam == kFrameTimerId) {
|
|
6090
6863
|
for (auto &entry : host->windows) {
|
|
6091
6864
|
if (entry.second.hwnd == hwnd) {
|
|
@@ -6109,9 +6882,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6109
6882
|
/* The declared floor is a CONTENT size in LOGICAL
|
|
6110
6883
|
* points; scale to physical pixels at this window's
|
|
6111
6884
|
* DPI, then convert to the outer track size for its
|
|
6112
|
-
* current style.
|
|
6113
|
-
* chrome (the custom WM_NCCALCSIZE hands
|
|
6114
|
-
*
|
|
6885
|
+
* current style. Styles that reclaim their top frame
|
|
6886
|
+
* carry no top chrome (the custom WM_NCCALCSIZE hands
|
|
6887
|
+
* it to the client), so their conversion skips it
|
|
6115
6888
|
* too. */
|
|
6116
6889
|
const UINT dpi = dpiForWindow(hwnd);
|
|
6117
6890
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6124,7 +6897,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6124
6897
|
adjustWindowRectForDpi(&frame, style, has_menu, ex_style, dpi);
|
|
6125
6898
|
LONG outer_width = frame.right - frame.left;
|
|
6126
6899
|
LONG outer_height = frame.bottom - frame.top;
|
|
6127
|
-
if (
|
|
6900
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6128
6901
|
const SIZE outer = hiddenOuterSizeForContent(style, ex_style, has_menu, min_content_width, min_content_height, dpi);
|
|
6129
6902
|
outer_width = outer.cx;
|
|
6130
6903
|
outer_height = outer.cy;
|
|
@@ -6141,7 +6914,21 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6141
6914
|
case WM_SYSCOLORCHANGE:
|
|
6142
6915
|
if (host) emitAppearanceIfChanged(host, false);
|
|
6143
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;
|
|
6144
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;
|
|
6145
6932
|
/* close_policy .hide: the user's close (caption X, Alt+F4)
|
|
6146
6933
|
* hides the window instead of destroying it — the hwnd and
|
|
6147
6934
|
* every view stay live, the app keeps running behind its
|
|
@@ -6180,9 +6967,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6180
6967
|
if (host) {
|
|
6181
6968
|
for (auto &entry : host->windows) {
|
|
6182
6969
|
if (entry.second.hwnd == hwnd) {
|
|
6970
|
+
cancelDialogsForWindow(host, entry.first);
|
|
6183
6971
|
destroyNativeViewsForWindow(host, entry.first);
|
|
6184
6972
|
destroyChildWebViewsForWindow(host, entry.first);
|
|
6185
6973
|
entry.second.hwnd = nullptr;
|
|
6974
|
+
entry.second.top_resize_strip = nullptr;
|
|
6186
6975
|
/* Destroyed means closed, never hidden. */
|
|
6187
6976
|
entry.second.policy_hidden = false;
|
|
6188
6977
|
emit(host, entry.second, kWindowFrame);
|
|
@@ -6252,9 +7041,9 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6252
7041
|
* re-derives the frame if it lands on another monitor), then grow
|
|
6253
7042
|
* it to the outer size for this style so the client rect lands at
|
|
6254
7043
|
* the request. The menu bar is attached after creation, so account
|
|
6255
|
-
* for it here when menus are declared.
|
|
6256
|
-
* custom-calc shape (no top chrome) — plain adjustment
|
|
6257
|
-
* their client one caption band tall. */
|
|
7044
|
+
* for it here when menus are declared. Styles that reclaim their top
|
|
7045
|
+
* frame use the custom-calc shape (no top chrome) — plain adjustment
|
|
7046
|
+
* would land their client one caption band (or top border) tall. */
|
|
6258
7047
|
const bool has_menu = !host->menus.empty();
|
|
6259
7048
|
const UINT dpi = systemDpi();
|
|
6260
7049
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6264,7 +7053,7 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6264
7053
|
adjustWindowRectForDpi(&frame, style, has_menu ? TRUE : FALSE, 0, dpi);
|
|
6265
7054
|
LONG outer_width = frame.right - frame.left;
|
|
6266
7055
|
LONG outer_height = frame.bottom - frame.top;
|
|
6267
|
-
if (
|
|
7056
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6268
7057
|
const SIZE outer = hiddenOuterSizeForContent(style, 0, has_menu, content_width, content_height, dpi);
|
|
6269
7058
|
outer_width = outer.cx;
|
|
6270
7059
|
outer_height = outer.cy;
|
|
@@ -6296,13 +7085,14 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6296
7085
|
/* Before the first show, so a dark-scheme launch never flashes a
|
|
6297
7086
|
* light caption. */
|
|
6298
7087
|
applyStandardTitlebarColorScheme(window);
|
|
6299
|
-
if (windowUsesHiddenTitlebar(window))
|
|
6300
|
-
|
|
7088
|
+
if (windowUsesHiddenTitlebar(window)) applyHiddenTitlebarFrame(window);
|
|
7089
|
+
if (windowReclaimsTopFrame(window)) {
|
|
6301
7090
|
/* The create-time WM_NCCALCSIZE ran before this window was
|
|
6302
7091
|
* registered under its HWND, so the custom calc did not apply;
|
|
6303
7092
|
* force one now that the map entry resolves. The callers keep
|
|
6304
7093
|
* `window` referencing the stored map entry for exactly this. */
|
|
6305
7094
|
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
|
7095
|
+
syncTopResizeStrip(host, window.id);
|
|
6306
7096
|
}
|
|
6307
7097
|
/* Initialize WS_EX_LAYERED through the same per-pixel path used by
|
|
6308
7098
|
* canvas presents. A layered HWND is otherwise not displayable until
|
|
@@ -6319,6 +7109,327 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6319
7109
|
return true;
|
|
6320
7110
|
}
|
|
6321
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
|
+
|
|
6322
7433
|
} // namespace
|
|
6323
7434
|
|
|
6324
7435
|
extern "C" {
|
|
@@ -6349,6 +7460,20 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6349
7460
|
* RPC_E_CHANGED_MODE (an MTA got there first) leaves nothing to
|
|
6350
7461
|
* balance. */
|
|
6351
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
|
+
}
|
|
6352
7477
|
/* One hardware Direct2D/DirectWrite resource domain for every canvas
|
|
6353
7478
|
* child. Creation also requires the in-memory font and constrained
|
|
6354
7479
|
* fallback seams: without them packet text cannot match engine layout,
|
|
@@ -6357,6 +7482,14 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6357
7482
|
host->app_name = slice(app_name, app_name_len);
|
|
6358
7483
|
host->window_title = slice(window_title, window_title_len);
|
|
6359
7484
|
host->bundle_id = slice(bundle_id, bundle_id_len);
|
|
7485
|
+
/* The taskbar groups and pins windows by AppUserModelID. Name the
|
|
7486
|
+
* process by its bundle id, the id the installer's Start menu
|
|
7487
|
+
* shortcut carries, so the taskbar follows the app instead of the
|
|
7488
|
+
* path of the executable that happened to start it. */
|
|
7489
|
+
if (!host->bundle_id.empty()) {
|
|
7490
|
+
const std::wstring app_user_model_id = widen(host->bundle_id);
|
|
7491
|
+
SetCurrentProcessExplicitAppUserModelID(app_user_model_id.c_str());
|
|
7492
|
+
}
|
|
6360
7493
|
host->icon_path = slice(icon_path, icon_path_len);
|
|
6361
7494
|
Window window;
|
|
6362
7495
|
window.id = 1;
|
|
@@ -6384,9 +7517,14 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6384
7517
|
void native_sdk_windows_destroy(Host *host) {
|
|
6385
7518
|
if (!host) return;
|
|
6386
7519
|
audioCaptureStopAll(host);
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
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);
|
|
6390
7528
|
/* DestroyWindow below dispatches WM_DESTROY/WM_ACTIVATEAPP
|
|
6391
7529
|
* synchronously through windowProc; the run loop's handler state is
|
|
6392
7530
|
* already gone by the time destroy is called, so those teardown
|
|
@@ -6406,6 +7544,7 @@ void native_sdk_windows_destroy(Host *host) {
|
|
|
6406
7544
|
removeNotificationIcon(host);
|
|
6407
7545
|
destroyAllWindows(host);
|
|
6408
7546
|
gpuSurfaceDestroyFrameWakeTimer(host);
|
|
7547
|
+
if (loop_window) DestroyWindow(loop_window);
|
|
6409
7548
|
const bool com_initialized = host->com_initialized;
|
|
6410
7549
|
delete host;
|
|
6411
7550
|
if (com_initialized) CoUninitialize();
|
|
@@ -6413,14 +7552,16 @@ void native_sdk_windows_destroy(Host *host) {
|
|
|
6413
7552
|
|
|
6414
7553
|
void native_sdk_windows_run(Host *host, EventCallback callback, void *context) {
|
|
6415
7554
|
if (!host) return;
|
|
6416
|
-
host->
|
|
6417
|
-
host->
|
|
7555
|
+
host->runtime_callback = callback;
|
|
7556
|
+
host->runtime_context = context;
|
|
7557
|
+
host->callback = dispatchEventToRuntime;
|
|
7558
|
+
host->callback_context = host;
|
|
6418
7559
|
host->running = true;
|
|
6419
7560
|
if (!host->windows.empty()) createNativeWindow(host, host->windows.begin()->second);
|
|
6420
7561
|
WindowsEvent start = {};
|
|
6421
7562
|
start.kind = kStart;
|
|
6422
7563
|
start.window_id = 1;
|
|
6423
|
-
callback(
|
|
7564
|
+
host->callback(host->callback_context, &start);
|
|
6424
7565
|
emitAppearanceIfChanged(host, true);
|
|
6425
7566
|
for (auto &entry : host->windows) {
|
|
6426
7567
|
emit(host, entry.second, kResize);
|
|
@@ -6445,10 +7586,7 @@ void native_sdk_windows_run(Host *host, EventCallback callback, void *context) {
|
|
|
6445
7586
|
DispatchMessageW(&message);
|
|
6446
7587
|
if (host->running) gpuSurfaceDrainDueFrameEmissions(host);
|
|
6447
7588
|
}
|
|
6448
|
-
|
|
6449
|
-
shutdown.kind = kShutdown;
|
|
6450
|
-
shutdown.window_id = 1;
|
|
6451
|
-
callback(context, &shutdown);
|
|
7589
|
+
deliverShutdown(host);
|
|
6452
7590
|
}
|
|
6453
7591
|
|
|
6454
7592
|
void native_sdk_windows_stop(Host *host) {
|
|
@@ -6457,30 +7595,30 @@ void native_sdk_windows_stop(Host *host) {
|
|
|
6457
7595
|
PostQuitMessage(0);
|
|
6458
7596
|
}
|
|
6459
7597
|
|
|
6460
|
-
/* Thread-safe wake: posts kWakeMessage
|
|
6461
|
-
*
|
|
6462
|
-
*
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
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;
|
|
6471
7609
|
}
|
|
6472
7610
|
|
|
6473
|
-
/* Thread-safe frame request: posts kRequestFrameMessage
|
|
6474
|
-
*
|
|
6475
|
-
* arrival watcher's wake,
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
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;
|
|
6484
7622
|
}
|
|
6485
7623
|
|
|
6486
7624
|
/* Platform image decoder: WIC (Windows Imaging Component) handles PNG,
|
|
@@ -6638,6 +7776,7 @@ int native_sdk_windows_load_window_webview(Host *host, uint64_t window_id, const
|
|
|
6638
7776
|
host->webviews.erase(key);
|
|
6639
7777
|
return 0;
|
|
6640
7778
|
}
|
|
7779
|
+
syncTopResizeStrip(host, window_id);
|
|
6641
7780
|
}
|
|
6642
7781
|
|
|
6643
7782
|
ChildWebView &webview = found->second;
|
|
@@ -6825,7 +7964,7 @@ int native_sdk_windows_create_window(Host *host, uint64_t window_id, const char
|
|
|
6825
7964
|
void native_sdk_windows_start_timer(Host *host, uint64_t timer_id, uint64_t interval_ns, int repeats) {
|
|
6826
7965
|
if (!host) return;
|
|
6827
7966
|
native_sdk_windows_cancel_timer(host, timer_id);
|
|
6828
|
-
HWND hwnd =
|
|
7967
|
+
HWND hwnd = loopWindow(host);
|
|
6829
7968
|
if (!hwnd) return;
|
|
6830
7969
|
AppTimer *slot = nullptr;
|
|
6831
7970
|
UINT_PTR win32_id = 0;
|
|
@@ -7590,167 +8729,154 @@ int native_sdk_windows_close_view(Host *host, uint64_t window_id, const char *la
|
|
|
7590
8729
|
|
|
7591
8730
|
WindowsOpenDialogResult native_sdk_windows_show_open_dialog(Host *host, const WindowsOpenDialogOpts *opts, char *buffer, size_t buffer_len) {
|
|
7592
8731
|
WindowsOpenDialogResult result = {};
|
|
7593
|
-
|
|
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;
|
|
7594
8735
|
bool uninitialize = false;
|
|
7595
8736
|
if (!initializeCom(&uninitialize)) return result;
|
|
7596
8737
|
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
DWORD options = 0;
|
|
7605
|
-
if (SUCCEEDED(dialog->GetOptions(&options))) {
|
|
7606
|
-
options |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
|
|
7607
|
-
if (opts->allow_directories) options |= FOS_PICKFOLDERS;
|
|
7608
|
-
else options |= FOS_FILEMUSTEXIST;
|
|
7609
|
-
if (opts->allow_multiple) options |= FOS_ALLOWMULTISELECT;
|
|
7610
|
-
dialog->SetOptions(options);
|
|
7611
|
-
}
|
|
7612
|
-
setDialogTitle(dialog, opts->title, opts->title_len);
|
|
7613
|
-
setDialogFolder(dialog, opts->default_path, opts->default_path_len);
|
|
7614
|
-
if (!opts->allow_directories) setDialogFilters(dialog, opts->extensions, opts->extensions_len);
|
|
7615
|
-
|
|
7616
|
-
if (SUCCEEDED(dialog->Show(parentWindow(host)))) {
|
|
7617
|
-
IShellItemArray *items = nullptr;
|
|
7618
|
-
if (SUCCEEDED(dialog->GetResults(&items)) && items) {
|
|
7619
|
-
DWORD count = 0;
|
|
7620
|
-
items->GetCount(&count);
|
|
7621
|
-
size_t offset = 0;
|
|
7622
|
-
size_t written_count = 0;
|
|
7623
|
-
for (DWORD index = 0; index < count; ++index) {
|
|
7624
|
-
IShellItem *item = nullptr;
|
|
7625
|
-
if (SUCCEEDED(items->GetItemAt(index, &item)) && item) {
|
|
7626
|
-
bool overflow = false;
|
|
7627
|
-
PWSTR path = nullptr;
|
|
7628
|
-
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
|
|
7629
|
-
overflow = !appendPathToBuffer(buffer, buffer_len, &offset, &written_count, std::wstring(path));
|
|
7630
|
-
CoTaskMemFree(path);
|
|
7631
|
-
}
|
|
7632
|
-
item->Release();
|
|
7633
|
-
if (overflow) break;
|
|
7634
|
-
}
|
|
7635
|
-
}
|
|
7636
|
-
result.count = written_count;
|
|
7637
|
-
result.bytes_written = offset;
|
|
7638
|
-
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;
|
|
7639
8744
|
}
|
|
8745
|
+
result.count = written_count;
|
|
8746
|
+
result.bytes_written = offset;
|
|
7640
8747
|
}
|
|
7641
8748
|
|
|
7642
|
-
dialog->Release();
|
|
7643
8749
|
finishCom(uninitialize);
|
|
7644
8750
|
return result;
|
|
7645
8751
|
}
|
|
7646
8752
|
|
|
7647
8753
|
size_t native_sdk_windows_show_save_dialog(Host *host, const WindowsSaveDialogOpts *opts, char *buffer, size_t buffer_len) {
|
|
7648
|
-
|
|
8754
|
+
/* Cancelled during a session end, like the open dialog. */
|
|
8755
|
+
if (!host || !opts || !buffer || buffer_len == 0 || host->session_ending) return 0;
|
|
7649
8756
|
bool uninitialize = false;
|
|
7650
8757
|
if (!initializeCom(&uninitialize)) return 0;
|
|
7651
8758
|
|
|
7652
|
-
IFileSaveDialog *dialog = nullptr;
|
|
7653
|
-
HRESULT hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
|
|
7654
|
-
if (FAILED(hr) || !dialog) {
|
|
7655
|
-
finishCom(uninitialize);
|
|
7656
|
-
return 0;
|
|
7657
|
-
}
|
|
7658
|
-
|
|
7659
|
-
DWORD options = 0;
|
|
7660
|
-
if (SUCCEEDED(dialog->GetOptions(&options))) dialog->SetOptions(options | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
|
7661
|
-
setDialogTitle(dialog, opts->title, opts->title_len);
|
|
7662
|
-
setDialogFolder(dialog, opts->default_path, opts->default_path_len);
|
|
7663
|
-
setDialogFilters(dialog, opts->extensions, opts->extensions_len);
|
|
7664
|
-
std::wstring default_name = widen(slice(opts->default_name, opts->default_name_len));
|
|
7665
|
-
if (!default_name.empty()) dialog->SetFileName(default_name.c_str());
|
|
7666
|
-
|
|
7667
8759
|
size_t written = 0;
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
} else {
|
|
7677
|
-
written = utf8_path.size();
|
|
7678
|
-
if (written > 0) memcpy(buffer, utf8_path.data(), written);
|
|
7679
|
-
}
|
|
7680
|
-
CoTaskMemFree(path);
|
|
7681
|
-
}
|
|
7682
|
-
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);
|
|
7683
8768
|
}
|
|
7684
8769
|
}
|
|
7685
8770
|
|
|
7686
|
-
dialog->Release();
|
|
7687
8771
|
finishCom(uninitialize);
|
|
7688
8772
|
return written;
|
|
7689
8773
|
}
|
|
7690
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. */
|
|
7691
8780
|
int native_sdk_windows_show_message_dialog(Host *host, const WindowsMessageDialogOpts *opts) {
|
|
7692
8781
|
if (!host || !opts) return 0;
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
std::wstring primary = widen(slice(opts->primary_button, opts->primary_button_len));
|
|
7697
|
-
std::wstring secondary = widen(slice(opts->secondary_button, opts->secondary_button_len));
|
|
7698
|
-
std::wstring tertiary = widen(slice(opts->tertiary_button, opts->tertiary_button_len));
|
|
7699
|
-
if (primary.empty()) primary = L"OK";
|
|
7700
|
-
|
|
7701
|
-
TASKDIALOG_BUTTON buttons[3] = {};
|
|
7702
|
-
int button_count = 0;
|
|
7703
|
-
buttons[button_count++] = { 100, primary.c_str() };
|
|
7704
|
-
if (!secondary.empty()) buttons[button_count++] = { 101, secondary.c_str() };
|
|
7705
|
-
if (!tertiary.empty()) buttons[button_count++] = { 102, tertiary.c_str() };
|
|
7706
|
-
|
|
7707
|
-
TASKDIALOGCONFIG config = {};
|
|
7708
|
-
config.cbSize = sizeof(config);
|
|
7709
|
-
config.hwndParent = parentWindow(host);
|
|
7710
|
-
config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION;
|
|
7711
|
-
config.pszWindowTitle = title.empty() ? L"native-sdk" : title.c_str();
|
|
7712
|
-
config.pszMainInstruction = message.empty() ? config.pszWindowTitle : message.c_str();
|
|
7713
|
-
config.pszContent = informative.empty() ? nullptr : informative.c_str();
|
|
7714
|
-
config.cButtons = static_cast<UINT>(button_count);
|
|
7715
|
-
config.pButtons = buttons;
|
|
7716
|
-
config.nDefaultButton = 100;
|
|
7717
|
-
config.pszMainIcon = opts->style == 2 ? TD_ERROR_ICON : (opts->style == 1 ? TD_WARNING_ICON : TD_INFORMATION_ICON);
|
|
7718
|
-
|
|
7719
|
-
/* TaskDialogIndirect is a comctl32 v6 export: it only resolves when
|
|
7720
|
-
* the process activates the v6 side-by-side assembly (application
|
|
7721
|
-
* manifest). A static import would abort the whole process at load
|
|
7722
|
-
* time with STATUS_ENTRYPOINT_NOT_FOUND on the system default v5, so
|
|
7723
|
-
* resolve it dynamically and fall back to MessageBoxW (fixed button
|
|
7724
|
-
* captions, same 0/1/2 result contract) when only v5 is available. */
|
|
7725
|
-
using TaskDialogIndirectFn = HRESULT(WINAPI *)(const TASKDIALOGCONFIG *, int *, int *, BOOL *);
|
|
7726
|
-
static TaskDialogIndirectFn task_dialog = reinterpret_cast<TaskDialogIndirectFn>(
|
|
7727
|
-
reinterpret_cast<void *>(GetProcAddress(GetModuleHandleW(L"comctl32.dll"), "TaskDialogIndirect")));
|
|
7728
|
-
if (task_dialog) {
|
|
8782
|
+
if (host->session_ending) return -1;
|
|
8783
|
+
const DialogRequest spec = messageDialogSpec(opts);
|
|
8784
|
+
if (TaskDialogIndirectFn task_dialog = taskDialogIndirect()) {
|
|
7729
8785
|
int pressed = 100;
|
|
7730
|
-
HRESULT hr = task_dialog
|
|
8786
|
+
HRESULT hr = runTaskDialog(task_dialog, parentWindow(host), spec, &pressed);
|
|
8787
|
+
if (host->session_ending) return -1;
|
|
7731
8788
|
if (FAILED(hr)) return 0;
|
|
7732
8789
|
if (pressed == 101) return 1;
|
|
7733
8790
|
if (pressed == 102) return 2;
|
|
7734
8791
|
return 0;
|
|
7735
8792
|
}
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
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;
|
|
7745
8823
|
}
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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 (...) {
|
|
7750
8833
|
return 0;
|
|
7751
8834
|
}
|
|
7752
|
-
|
|
7753
|
-
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;
|
|
7754
8880
|
}
|
|
7755
8881
|
|
|
7756
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) {
|
|
@@ -8042,6 +9168,7 @@ int native_sdk_windows_create_webview(Host *host, uint64_t window_id, const char
|
|
|
8042
9168
|
if (!createChildWebView(host, key)) {
|
|
8043
9169
|
DestroyWindow(hwnd);
|
|
8044
9170
|
host->webviews.erase(key);
|
|
9171
|
+
syncTopResizeStrip(host, window_id);
|
|
8045
9172
|
return 0;
|
|
8046
9173
|
}
|
|
8047
9174
|
return 1;
|
|
@@ -8066,6 +9193,7 @@ int native_sdk_windows_set_webview_frame(Host *host, uint64_t window_id, const c
|
|
|
8066
9193
|
found->second.controller->put_Bounds(bounds);
|
|
8067
9194
|
}
|
|
8068
9195
|
#endif
|
|
9196
|
+
syncTopResizeStrip(host, window_id);
|
|
8069
9197
|
return 1;
|
|
8070
9198
|
}
|
|
8071
9199
|
|
|
@@ -8145,6 +9273,7 @@ int native_sdk_windows_close_webview(Host *host, uint64_t window_id, const char
|
|
|
8145
9273
|
#endif
|
|
8146
9274
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
8147
9275
|
host->webviews.erase(found);
|
|
9276
|
+
syncTopResizeStrip(host, window_id);
|
|
8148
9277
|
return 1;
|
|
8149
9278
|
}
|
|
8150
9279
|
|