basalt-win32 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,203 @@
1
+ // The Windows mounting manager.
2
+ //
3
+ // `IMountingManager` is the one interface `ReactHost` requires in order to put
4
+ // a surface on screen. Almost none of what that takes is here: the mutation
5
+ // walk, the view registry, the event-emitter bookkeeping and the surface roots
6
+ // are in `core/MountingWalk.h`, written once and shared with GTK and AppKit.
7
+ // What is left below is the part that is genuinely Windows -- making a view,
8
+ // parenting it, and turning a ShadowView into view state.
9
+ //
10
+ // Phase 19 made that division and tested it with a second platform. This is the
11
+ // third, and it is the one that says whether the division holds or whether it
12
+ // was two platforms happening to agree: GTK and AppKit are both toolkits with a
13
+ // widget object per view, and this one is not.
14
+ //
15
+ // It holds. What is below is seven operations and a props translation, and the
16
+ // only place Windows differs in kind rather than in spelling is that
17
+ // `destroyView` has to actually delete -- there is no GObject refcount and no
18
+ // ARC, so the registry's `unique_ptr`-shaped ownership is written out.
19
+
20
+ #pragma once
21
+
22
+ #include "RnWin32Image.h"
23
+ #include "RnWin32View.h"
24
+ #include "Win32ImageLoader.h"
25
+ #include "Win32ScrollView.h"
26
+ #include "Win32TextInput.h"
27
+
28
+ #include "MountingWalk.h"
29
+
30
+ #include <react/renderer/uimanager/IMountingManager.h>
31
+
32
+ #include <functional>
33
+ #include <string>
34
+ #include <unordered_map>
35
+
36
+ namespace basalt {
37
+
38
+ class Win32MountingManager final : public facebook::react::IMountingManager,
39
+ public MountingWalk<Win32MountingManager, win32::RnWin32View *> {
40
+ public:
41
+
42
+ // The image loader, for the host to hand to React Native's
43
+ // `ImageLoaderModule` -- which is what `Image.getSize` and `Image.prefetch`
44
+ // reach. Shared so that module can hold a weak reference to it.
45
+ std::shared_ptr<win32::Win32ImageLoader> imageLoader() const { return imageLoader_; }
46
+ Win32MountingManager();
47
+ ~Win32MountingManager() noexcept override;
48
+
49
+ // --- IMountingManager -----------------------------------------------------
50
+
51
+ // Called on the JS thread, from the event loop's "update the rendering" step.
52
+ // Hands the transaction to the UI thread; does not touch a view.
53
+ void executeMount(facebook::react::SurfaceId surfaceId,
54
+ facebook::react::MountingTransaction &&transaction) override;
55
+
56
+ void dispatchCommand(const facebook::react::ShadowView &shadowView,
57
+ const std::string &commandName,
58
+ const folly::dynamic &args) override;
59
+
60
+ facebook::react::ComponentRegistryFactory getComponentRegistryFactory() override;
61
+
62
+ bool hasComponent(const std::string &name) override;
63
+
64
+ // ReactCxxPlatform hands the UIManager to the mounting manager and to nothing
65
+ // else. Reanimated needs it, and is constructed from JavaScript where no host
66
+ // object is in reach, so this passes it on. See core/UIManagerAccess.h.
67
+ void setUIManager(std::weak_ptr<facebook::react::UIManager> uiManager) noexcept override;
68
+
69
+ // --- Host-facing ----------------------------------------------------------
70
+ //
71
+ // createSurfaceRoot, destroySurfaceRoot, getSurfaceRoot, viewForTag and
72
+ // eventEmitterForTag all come from MountingWalk.
73
+
74
+ // Applies a transaction to the view tree. Invoked on the UI thread by
75
+ // executeMount's marshalling -- not part of IMountingManager, and never to be
76
+ // called directly. Public because the posted work has to reach it, and
77
+ // because the tests call it rather than pumping a message loop.
78
+ void applyTransaction(facebook::react::SurfaceId surfaceId,
79
+ facebook::react::MountingTransaction &&transaction);
80
+
81
+ // The UI-thread half of dispatchCommand, public for the same reason
82
+ // applyTransaction is.
83
+ void applyCommand(facebook::react::Tag tag,
84
+ const std::string &commandName,
85
+ const folly::dynamic &args);
86
+
87
+ // Called on the UI thread after every applied transaction, so the host can
88
+ // ask for a repaint.
89
+ //
90
+ // A callback rather than the manager knowing about a window, and needed at
91
+ // all because Windows repaints on demand: a mutation changes the view tree
92
+ // and nothing moves on screen until something invalidates. GTK reaches for
93
+ // `gtk_widget_queue_draw` and AppKit for `setNeedsDisplay:` from inside their
94
+ // own managers, which they can because a view there *is* a widget. Here a
95
+ // view is a plain object and only the host has the HWND.
96
+ void setOnDidMount(std::function<void()> onDidMount);
97
+
98
+ // The wheel. Finds the innermost <ScrollView> under a point in the surface
99
+ // root's coordinates and scrolls it, returning false when nothing there
100
+ // scrolls -- which is what lets the host hand the message to DefWindowProc.
101
+ //
102
+ // Routed through here rather than from the view layer because a Win32 view
103
+ // has no window of its own and there is no responder chain to walk: only the
104
+ // object that knows which tags are ScrollViews can decide. Deltas are pixels
105
+ // already; see Win32ScrollViewManager::kWheelStepPixels for how a notch
106
+ // becomes one.
107
+ bool scrollAt(win32::RnWin32View *root, double x, double y, double deltaX, double deltaY);
108
+
109
+ // --- <TextInput>, which needs more from a host than anything else ----------
110
+ //
111
+ // A text field's peer is a real EDIT control: a child window of the host's,
112
+ // whose notifications arrive at the host and whose colours are answered from
113
+ // the host's WM_CTLCOLOREDIT. None of those has an equivalent on GTK or
114
+ // AppKit, where a React Native view *is* a widget and the toolkit routes all
115
+ // of this on its own. See Win32TextInput.h.
116
+
117
+ // The window every peer is a child of. Must be set before the first
118
+ // TextInput mounts, which the host does right after creating its window.
119
+ void setHostWindow(HWND window);
120
+
121
+ // Moves every text field's peer to where its view now is. A child window does
122
+ // not follow a view that has no widget, so this has to be called after every
123
+ // transaction and after every scroll.
124
+ void syncTextInputBounds(win32::RnWin32View *root);
125
+
126
+ // WM_COMMAND and WM_CTLCOLOREDIT, forwarded from the host's window procedure.
127
+ bool handleControlCommand(WPARAM wparam, LPARAM lparam);
128
+ HBRUSH controlColor(HDC deviceContext, HWND control);
129
+
130
+ // BASALT_TEST_TYPE: real WM_CHARs into whichever field has focus.
131
+ bool typeIntoFocusedTextInput(const std::string &text);
132
+
133
+ // A press ended on this view. The one thing a painted control needs that a
134
+ // mounted one gets for free: a <Switch> on this host is drawn rather than
135
+ // being an HWND, so nothing under it turns a click into a toggle. Called by
136
+ // Win32TouchDispatcher, and a no-op for every view that is not a switch.
137
+ void pressedView(facebook::react::Tag tag);
138
+
139
+ // BASALT_TEST_TAP: focuses the field under a point, which is the one thing a
140
+ // synthesised tap cannot do -- a real click reaches the peer child window
141
+ // directly and never passes through the touch dispatcher at all.
142
+ bool focusTextInputAt(win32::RnWin32View *root, double x, double y);
143
+
144
+ private:
145
+ // --- What MountingWalk asks of a platform ---------------------------------
146
+ friend class MountingWalk<Win32MountingManager, win32::RnWin32View *>;
147
+
148
+ win32::RnWin32View *createView(const facebook::react::ShadowView &shadowView);
149
+ win32::RnWin32View *createRootView(facebook::react::Tag tag);
150
+ void destroyView(win32::RnWin32View *view);
151
+ void insertChild(win32::RnWin32View *parent, win32::RnWin32View *child, int index);
152
+ void removeChild(win32::RnWin32View *parent, win32::RnWin32View *child);
153
+ void updateView(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
154
+ void forgetTag(facebook::react::Tag tag);
155
+
156
+ // --- The Windows half of updateView ---------------------------------------
157
+ void applyProps(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
158
+ void applyText(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
159
+ void applyImage(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
160
+ void applyAccessibility(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
161
+ void applyLayoutMetrics(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
162
+ void applyScrollView(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
163
+ void applyTextInput(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
164
+
165
+ // The Windows half of a control, which on this host is painting rather than
166
+ // mounting -- see RnWin32View.h for why there is no widget to mount.
167
+ void applyControlPeer(win32::RnWin32View *view, const basalt::ControlState &state);
168
+
169
+ // React DevTools' overlay rectangles. The parsing and the clearing are in
170
+ // MountingWalk; this is the drawing.
171
+ void setHighlights(win32::RnWin32View *view,
172
+ const std::vector<basalt::Highlight> &highlights);
173
+
174
+ // The source each <Image> is currently showing, so that a mutation which
175
+ // changed only layout does not restart the load and make the image flicker
176
+ // whenever its parent resizes. Both other platforms keep the same map.
177
+ std::unordered_map<facebook::react::Tag, std::string> imageUris_;
178
+
179
+ // Scroll offsets, the unthrottled state write-back, the wheel and the
180
+ // commands. Constructed with a lookup into this object's own emitter registry
181
+ // rather than a second copy of it.
182
+ Win32ScrollViewManager scrollViews_;
183
+
184
+ // The EDIT peers, their placement, the controlled-value loop and the
185
+ // commands. Same lookup, same reason.
186
+ Win32TextInputManager textInputs_;
187
+
188
+ // Fetching and decoding, off the UI thread. Holds the decoded-pixel cache
189
+ // too, and outlives this object while a load is in flight -- see the header.
190
+ // Shared rather than held by value: React Native's `ImageLoaderModule` takes
191
+ // a `weak_ptr<IImageLoader>`, and this is the loader it gets.
192
+ std::shared_ptr<win32::Win32ImageLoader> imageLoader_ =
193
+ std::make_shared<win32::Win32ImageLoader>();
194
+
195
+ // A <Switch> is a controlled component: the drawing is not allowed to decide
196
+ // its own state, so the value React last sent is kept here and the view is
197
+ // repainted from it after the click that told JavaScript about it.
198
+ std::unordered_map<facebook::react::Tag, bool> switchValues_;
199
+
200
+ std::function<void()> onDidMount_;
201
+ };
202
+
203
+ } // namespace basalt
@@ -0,0 +1,161 @@
1
+ #include "Win32Packaging.h"
2
+
3
+ #include "AppIdentity.h"
4
+ #include "Win32Strings.h"
5
+
6
+ #include <windows.h>
7
+
8
+ #include <knownfolders.h>
9
+ #include <objbase.h>
10
+ #include <propkey.h>
11
+ #include <propsys.h>
12
+ #include <propvarutil.h>
13
+ #include <shlobj.h>
14
+ #include <shobjidl.h>
15
+
16
+ #include <string>
17
+
18
+ namespace basalt::win32 {
19
+
20
+ namespace {
21
+
22
+ std::wstring &applied() {
23
+ static std::wstring id;
24
+ return id;
25
+ }
26
+
27
+ // Where a per-user Start Menu shortcut goes. Per-user rather than per-machine
28
+ // because this is not an installer: writing into the all-users Start Menu needs
29
+ // administrator rights, and an app being run from a build directory has no
30
+ // business asking for them.
31
+ std::wstring startMenuShortcutPath(const std::wstring &name) {
32
+ PWSTR folder = nullptr;
33
+ if (FAILED(SHGetKnownFolderPath(FOLDERID_Programs, 0, nullptr, &folder))) {
34
+ return {};
35
+ }
36
+ std::wstring path(folder);
37
+ CoTaskMemFree(folder);
38
+ path += L"\\";
39
+ path += name;
40
+ path += L".lnk";
41
+ return path;
42
+ }
43
+
44
+ // The executable this process was started from, which is what the shortcut has
45
+ // to point at.
46
+ std::wstring executablePath() {
47
+ std::wstring path(MAX_PATH, L'\0');
48
+ while (true) {
49
+ const DWORD written = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
50
+ if (written == 0) {
51
+ return {};
52
+ }
53
+ if (written < path.size()) {
54
+ path.resize(written);
55
+ return path;
56
+ }
57
+ // Truncated. The only way to know is that it filled the buffer, so grow and
58
+ // ask again rather than shipping a path that is quietly half a path.
59
+ path.resize(path.size() * 2);
60
+ }
61
+ }
62
+
63
+ // Writes the shortcut, with the AppUserModelID on it. Returns false on any
64
+ // failure, all of which are survivable: what is lost is notifications, not the
65
+ // app.
66
+ bool writeShortcut(const std::wstring &shortcutPath,
67
+ const std::wstring &target,
68
+ const std::wstring &id) {
69
+ IShellLinkW *link = nullptr;
70
+ if (FAILED(CoCreateInstance(
71
+ CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void **)&link))) {
72
+ return false;
73
+ }
74
+
75
+ bool ok = SUCCEEDED(link->SetPath(target.c_str()));
76
+ if (ok) {
77
+ // The working directory matters: the host resolves its bundle relative to
78
+ // wherever it was started, so a shortcut with the wrong one launches an app
79
+ // that cannot find its JavaScript.
80
+ const size_t slash = target.find_last_of(L'\\');
81
+ if (slash != std::wstring::npos) {
82
+ link->SetWorkingDirectory(target.substr(0, slash).c_str());
83
+ }
84
+ }
85
+
86
+ // The property that makes the whole thing work. A shortcut without it is
87
+ // just a shortcut, and the shell goes on refusing to show notifications for
88
+ // this id.
89
+ if (ok) {
90
+ IPropertyStore *properties = nullptr;
91
+ ok = SUCCEEDED(link->QueryInterface(IID_IPropertyStore, (void **)&properties));
92
+ if (ok) {
93
+ PROPVARIANT value;
94
+ ok = SUCCEEDED(InitPropVariantFromString(id.c_str(), &value));
95
+ if (ok) {
96
+ ok = SUCCEEDED(properties->SetValue(PKEY_AppUserModel_ID, value));
97
+ if (ok) {
98
+ ok = SUCCEEDED(properties->Commit());
99
+ }
100
+ PropVariantClear(&value);
101
+ }
102
+ properties->Release();
103
+ }
104
+ }
105
+
106
+ if (ok) {
107
+ IPersistFile *file = nullptr;
108
+ ok = SUCCEEDED(link->QueryInterface(IID_IPersistFile, (void **)&file));
109
+ if (ok) {
110
+ ok = SUCCEEDED(file->Save(shortcutPath.c_str(), TRUE));
111
+ file->Release();
112
+ }
113
+ }
114
+
115
+ link->Release();
116
+ return ok;
117
+ }
118
+
119
+ } // namespace
120
+
121
+ void applyPackaging(const AppIdentity &identity) {
122
+ if (identity.empty()) {
123
+ return;
124
+ }
125
+
126
+ const std::wstring id = widen(identity.identifier);
127
+ // Before any window. The shell reads this when a window is first shown and
128
+ // does not notice a later change, which is why this is called from the top of
129
+ // the host rather than from wherever notifications are first used.
130
+ if (FAILED(SetCurrentProcessExplicitAppUserModelID(id.c_str()))) {
131
+ return;
132
+ }
133
+ applied() = id;
134
+
135
+ const std::wstring name = widen(identity.name.empty() ? identity.identifier : identity.name);
136
+ const std::wstring shortcut = startMenuShortcutPath(name);
137
+ if (shortcut.empty()) {
138
+ return;
139
+ }
140
+ // Left alone once written. This is a real file in the person's Start Menu,
141
+ // and a development session that rewrote it on every run would churn their
142
+ // menu for nothing.
143
+ if (GetFileAttributesW(shortcut.c_str()) != INVALID_FILE_ATTRIBUTES) {
144
+ return;
145
+ }
146
+ const std::wstring target = executablePath();
147
+ if (target.empty()) {
148
+ return;
149
+ }
150
+ writeShortcut(shortcut, target, id);
151
+ }
152
+
153
+ bool hasAppUserModelId() {
154
+ return !applied().empty();
155
+ }
156
+
157
+ const wchar_t *appUserModelId() {
158
+ return applied().c_str();
159
+ }
160
+
161
+ } // namespace basalt::win32
@@ -0,0 +1,56 @@
1
+ // Making this process look like an installed application to Windows.
2
+ //
3
+ // Unlike macOS, nothing here is about where the executable sits: Windows has no
4
+ // bundle. What it has is an **AppUserModelID**, a string the shell uses to
5
+ // decide which taskbar button a window belongs to, which jump list it has, and
6
+ // -- the reason this file exists -- which application a notification came from.
7
+ //
8
+ // A desktop application that wants to raise a notification has to do two things,
9
+ // and doing only one of them fails silently:
10
+ //
11
+ // 1. Call `SetCurrentProcessExplicitAppUserModelID` before it shows any UI.
12
+ // Without it the shell derives an id from the process, which changes
13
+ // between a run from a build directory and a run from anywhere else.
14
+ //
15
+ // 2. Have a Start Menu shortcut whose `System.AppUserModel.ID` property is
16
+ // that same string. This is the part people miss. The shell will not
17
+ // surface a notification for an id it has never seen installed, and a
18
+ // shortcut is how an id becomes installed. Microsoft's own toast
19
+ // documentation says so, and it is why this is in the host rather than in
20
+ // `cli/packageApp.js`: setting that property means `IPropertyStore`, which
21
+ // is COM, and doing it in C++ at startup is simpler and more reliable than
22
+ // doing it in PowerShell from Node.
23
+ //
24
+ // Both are driven from `core/AppIdentity.h`, which the CLI writes beside the
25
+ // bundle. An app with no identity file gets neither, and notifications then
26
+ // report that they are unavailable -- which is the answer this host gave before
27
+ // any of this existed.
28
+ //
29
+ // The shortcut is written once and left alone. It is a real file in the
30
+ // person's Start Menu, so rewriting it on every run would mean a development
31
+ // session churning their menu; `ensureStartMenuShortcut` checks first.
32
+
33
+ #pragma once
34
+
35
+ namespace basalt {
36
+
37
+ struct AppIdentity;
38
+
39
+ namespace win32 {
40
+
41
+ // Applies the identity to this process: the AppUserModelID, and a Start Menu
42
+ // shortcut carrying it if there is not one already.
43
+ //
44
+ // Call once, from the host, before any window exists. A no-op for an empty
45
+ // identity.
46
+ void applyPackaging(const AppIdentity &identity);
47
+
48
+ // Whether this process has an AppUserModelID, which is what decides whether a
49
+ // notification can be raised at all. Read by Win32Notifications.cpp.
50
+ bool hasAppUserModelId();
51
+
52
+ // The identity's id, as applied. Empty when none was.
53
+ const wchar_t *appUserModelId();
54
+
55
+ } // namespace win32
56
+ } // namespace basalt
@@ -0,0 +1,48 @@
1
+ #include "Win32RunLoopObserver.h"
2
+
3
+ #include <windows.h>
4
+
5
+ #include <react/utils/RunLoopObserverManager.h>
6
+
7
+ namespace basalt {
8
+
9
+ int runMessageLoopWithBeat(
10
+ std::shared_ptr<facebook::react::RunLoopObserverManager> manager) {
11
+ MSG message{};
12
+ for (;;) {
13
+ // Drain everything queued before beating. Beating between each message
14
+ // would be correct but wasteful, and beating *after* the queue is empty is
15
+ // what "BeforeWaiting" means -- the frame has settled, and whatever the
16
+ // messages produced is now worth flushing to JavaScript in one go.
17
+ while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
18
+ if (message.message == WM_QUIT) {
19
+ return static_cast<int>(message.wParam);
20
+ }
21
+ TranslateMessage(&message);
22
+ DispatchMessageW(&message);
23
+ }
24
+
25
+ if (manager != nullptr) {
26
+ manager->onRender();
27
+ }
28
+
29
+ // Block until something arrives. MsgWaitForMultipleObjectsEx rather than
30
+ // GetMessage, because the beat above may itself have posted work -- a mount
31
+ // marshalled from the JS thread arrives as a posted message -- and
32
+ // GetMessage would be fine, but this is the form that also takes handles
33
+ // when something later needs to wait on one.
34
+ //
35
+ // MWMO_INPUTAVAILABLE matters and is easy to omit: without it, a message
36
+ // that was already in the queue when the wait begins does not count as an
37
+ // event, and the loop sleeps with work pending until the *next* message
38
+ // arrives. With PeekMessage above having just emptied the queue that is
39
+ // nearly always fine, and "nearly always" is how an app hangs once an hour.
40
+ const DWORD result =
41
+ MsgWaitForMultipleObjectsEx(0, nullptr, INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
42
+ if (result == WAIT_FAILED) {
43
+ return 1;
44
+ }
45
+ }
46
+ }
47
+
48
+ } // namespace basalt
@@ -0,0 +1,42 @@
1
+ // Driving React Native's event beat from the message loop.
2
+ //
3
+ // Nothing an EventEmitter produces reaches JavaScript on its own.
4
+ // `EventQueue::onEnqueue` only sets a flag on the EventBeat; the queue is
5
+ // flushed when something calls `RunLoopObserverManager::onRender()`, which
6
+ // induces the beat. Until that is wired, touches are enqueued and silently
7
+ // never delivered -- which looks like an input bug and is not one.
8
+ //
9
+ // What React Native asks for is `Activity::BeforeWaiting`: run once the loop
10
+ // has drained its work and is about to sleep. The three platforms get there
11
+ // differently, and Windows is the one where it cannot be *installed*.
12
+ //
13
+ // iOS/macOS a CFRunLoopObserver on kCFRunLoopBeforeWaiting
14
+ // GTK a GSource whose prepare() does the work and never reports ready
15
+ // Win32 no hook exists, so the loop itself has to be written
16
+ //
17
+ // A Win32 message loop is `GetMessage`/`DispatchMessage`, and there is no
18
+ // callback for "about to block". The equivalent is to stop using `GetMessage`:
19
+ // drain with `PeekMessage` until it comes up empty, beat, and only then block
20
+ // in `MsgWaitForMultipleObjectsEx`. That is exactly "having drained whatever
21
+ // else was pending", and blocking properly rather than polling is what keeps an
22
+ // idle app at zero CPU -- which was the objection to `gtk_widget_add_tick_callback`
23
+ // recorded in `docs/DECISIONS.md`, and applies here for the same reason.
24
+ //
25
+ // So this is a loop rather than an observer, and the host calls it instead of
26
+ // writing its own.
27
+
28
+ #pragma once
29
+
30
+ #include <memory>
31
+
32
+ namespace facebook::react {
33
+ class RunLoopObserverManager;
34
+ }
35
+
36
+ namespace basalt {
37
+
38
+ // Runs until WM_QUIT, inducing the beat each time the queue empties. Returns
39
+ // the exit code from WM_QUIT.
40
+ int runMessageLoopWithBeat(std::shared_ptr<facebook::react::RunLoopObserverManager> manager);
41
+
42
+ } // namespace basalt