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.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- 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
|