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,811 @@
|
|
|
1
|
+
#include "Win32TextInput.h"
|
|
2
|
+
|
|
3
|
+
#include "DirectWriteLayout.h"
|
|
4
|
+
#include "Win32Strings.h"
|
|
5
|
+
|
|
6
|
+
#include <react/renderer/components/iostextinput/TextInputProps.h>
|
|
7
|
+
|
|
8
|
+
#include <commctrl.h>
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <cwctype>
|
|
12
|
+
#include <cmath>
|
|
13
|
+
|
|
14
|
+
// Visual styles, which an EDIT needs for two things: the cue banner that backs
|
|
15
|
+
// `placeholder` is a comctl32 v6 message and silently does nothing without it,
|
|
16
|
+
// and a v5 EDIT looks like Windows 95 next to everything else on the screen.
|
|
17
|
+
// Declared here rather than in a .manifest file so that running the binary
|
|
18
|
+
// straight out of the build directory behaves the same as running an installed
|
|
19
|
+
// one -- the same argument SetProcessDpiAwarenessContext makes in main.
|
|
20
|
+
#pragma comment(linker, \
|
|
21
|
+
"\"/manifestdependency:type='win32' " \
|
|
22
|
+
"name='Microsoft.Windows.Common-Controls' " \
|
|
23
|
+
"version='6.0.0.0' " \
|
|
24
|
+
"processorArchitecture='*' " \
|
|
25
|
+
"publicKeyToken='6595b64144ccf1df' " \
|
|
26
|
+
"language='*'\"")
|
|
27
|
+
|
|
28
|
+
namespace basalt {
|
|
29
|
+
|
|
30
|
+
using facebook::react::AttributedString;
|
|
31
|
+
using facebook::react::ShadowView;
|
|
32
|
+
using facebook::react::Tag;
|
|
33
|
+
using facebook::react::TextInputEventEmitter;
|
|
34
|
+
using facebook::react::TextInputProps;
|
|
35
|
+
using win32::narrow;
|
|
36
|
+
using win32::RnTextAlign;
|
|
37
|
+
using win32::RnTextStyle;
|
|
38
|
+
using win32::RnWin32View;
|
|
39
|
+
using win32::widen;
|
|
40
|
+
|
|
41
|
+
namespace {
|
|
42
|
+
|
|
43
|
+
// One id for every peer. An EDIT's notifications arrive as WM_COMMAND with a
|
|
44
|
+
// control id, and the id is only used to tell "this is one of ours" from "this
|
|
45
|
+
// is a menu item"; which control it was comes from the HWND in lparam.
|
|
46
|
+
constexpr WORD kControlId = 0x4200;
|
|
47
|
+
|
|
48
|
+
// SetWindowSubclass takes an id of its own, distinct from the control id.
|
|
49
|
+
constexpr UINT_PTR kSubclassId = 1;
|
|
50
|
+
|
|
51
|
+
// What a password field shows instead of the character. Windows' own default is
|
|
52
|
+
// a bullet under visual styles; naming it means the two other desktops and this
|
|
53
|
+
// one agree about what secureTextEntry looks like.
|
|
54
|
+
constexpr wchar_t kPasswordCharacter = L'\x25CF';
|
|
55
|
+
|
|
56
|
+
COLORREF toColorRef(const float components[4]) {
|
|
57
|
+
const auto channel = [](float value) {
|
|
58
|
+
return static_cast<int>(std::clamp(value, 0.0F, 1.0F) * 255.0F + 0.5F);
|
|
59
|
+
};
|
|
60
|
+
return RGB(channel(components[0]), channel(components[1]), channel(components[2]));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A view's rectangle in the surface root's coordinates, and whether any of it is
|
|
64
|
+
// still visible.
|
|
65
|
+
//
|
|
66
|
+
// Translation only: an EDIT is a window and a window cannot be rotated, so a
|
|
67
|
+
// transformed field's peer sits where the untransformed one would. That is a
|
|
68
|
+
// real difference from the painted view behind it, and there is no cheap fix --
|
|
69
|
+
// see the header.
|
|
70
|
+
//
|
|
71
|
+
// Visibility is the other half. A child window is not clipped by anything in
|
|
72
|
+
// the React tree, so a field scrolled out of its list would otherwise go on
|
|
73
|
+
// being drawn over the list's neighbours. Any clipping ancestor that the
|
|
74
|
+
// rectangle has left entirely hides it.
|
|
75
|
+
bool peerRect(RnWin32View *view, RnWin32View *root, RECT &out) {
|
|
76
|
+
if (view == nullptr || root == nullptr) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
double x = 0.0;
|
|
81
|
+
double y = 0.0;
|
|
82
|
+
const double width = view->frame().width;
|
|
83
|
+
const double height = view->frame().height;
|
|
84
|
+
|
|
85
|
+
for (RnWin32View *current = view; current != nullptr; current = current->parent()) {
|
|
86
|
+
if (current->hidden()) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (current == root) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
x += current->frame().x;
|
|
93
|
+
y += current->frame().y;
|
|
94
|
+
|
|
95
|
+
RnWin32View *parent = current->parent();
|
|
96
|
+
if (parent == nullptr) {
|
|
97
|
+
// Detached: between a Remove and its Delete, or never inserted. Not on
|
|
98
|
+
// screen either way.
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
x -= parent->scrollX();
|
|
102
|
+
y -= parent->scrollY();
|
|
103
|
+
|
|
104
|
+
if (parent->clipsChildren()) {
|
|
105
|
+
// In the parent's own coordinates the box is 0..width, 0..height, and
|
|
106
|
+
// (x, y) is where this view now sits relative to it.
|
|
107
|
+
if (x + width <= 0 || y + height <= 0 || x >= parent->frame().width ||
|
|
108
|
+
y >= parent->frame().height) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
out.left = static_cast<LONG>(std::lround(x));
|
|
115
|
+
out.top = static_cast<LONG>(std::lround(y));
|
|
116
|
+
out.right = out.left + static_cast<LONG>(std::lround(width));
|
|
117
|
+
out.bottom = out.top + static_cast<LONG>(std::lround(height));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} // namespace
|
|
122
|
+
|
|
123
|
+
Win32TextInputManager::Win32TextInputManager(EmitterLookup lookup) : lookup_(std::move(lookup)) {}
|
|
124
|
+
|
|
125
|
+
Win32TextInputManager::~Win32TextInputManager() {
|
|
126
|
+
for (auto &[tag, entry] : entries_) {
|
|
127
|
+
destroyPeer(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void Win32TextInputManager::setHostWindow(HWND window) {
|
|
132
|
+
host_ = window;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Mutations
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowView) {
|
|
140
|
+
const Tag tag = shadowView.tag;
|
|
141
|
+
auto [it, inserted] = entries_.try_emplace(tag);
|
|
142
|
+
Entry &entry = it->second;
|
|
143
|
+
|
|
144
|
+
entry.view = view;
|
|
145
|
+
entry.tag = tag;
|
|
146
|
+
entry.owner = this;
|
|
147
|
+
|
|
148
|
+
const auto props = std::dynamic_pointer_cast<const TextInputProps>(shadowView.props);
|
|
149
|
+
|
|
150
|
+
if (entry.control == nullptr && host_ != nullptr) {
|
|
151
|
+
// ES_AUTOHSCROLL so the caret can leave the visible box rather than the
|
|
152
|
+
// control refusing further input; WS_CLIPSIBLINGS so two adjacent fields do
|
|
153
|
+
// not paint into each other. No border and no ES_ styles for one: the
|
|
154
|
+
// RnWin32View behind it draws the background, the border and the corner
|
|
155
|
+
// radius, because those are React Native style props and the control knows
|
|
156
|
+
// nothing about them.
|
|
157
|
+
entry.control = CreateWindowEx(0,
|
|
158
|
+
L"EDIT",
|
|
159
|
+
L"",
|
|
160
|
+
WS_CHILD | WS_CLIPSIBLINGS | ES_LEFT | ES_AUTOHSCROLL,
|
|
161
|
+
0,
|
|
162
|
+
0,
|
|
163
|
+
0,
|
|
164
|
+
0,
|
|
165
|
+
host_,
|
|
166
|
+
reinterpret_cast<HMENU>(static_cast<UINT_PTR>(kControlId)),
|
|
167
|
+
GetModuleHandle(nullptr),
|
|
168
|
+
nullptr);
|
|
169
|
+
if (entry.control != nullptr) {
|
|
170
|
+
SetWindowSubclass(entry.control, editProc, kSubclassId, reinterpret_cast<DWORD_PTR>(&entry));
|
|
171
|
+
}
|
|
172
|
+
// So describeTree can report the field content. The view neither owns the
|
|
173
|
+
// control nor draws it; see RnWin32View::setEditablePeer.
|
|
174
|
+
if (view != nullptr) {
|
|
175
|
+
view->setEditablePeer(entry.control);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (props == nullptr) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
applyProps(entry, *props);
|
|
183
|
+
// After the font: the line height is measured against it.
|
|
184
|
+
measureShape(entry, shadowView.layoutMetrics);
|
|
185
|
+
|
|
186
|
+
if (inserted && props->autoFocus && entry.control != nullptr) {
|
|
187
|
+
SetFocus(entry.control);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
void Win32TextInputManager::measureShape(Entry &entry,
|
|
192
|
+
const facebook::react::LayoutMetrics &metrics) {
|
|
193
|
+
const auto &insets = metrics.contentInsets;
|
|
194
|
+
entry.insets = RECT{static_cast<LONG>(std::lround(insets.left)),
|
|
195
|
+
static_cast<LONG>(std::lround(insets.top)),
|
|
196
|
+
static_cast<LONG>(std::lround(insets.right)),
|
|
197
|
+
static_cast<LONG>(std::lround(insets.bottom))};
|
|
198
|
+
|
|
199
|
+
if (entry.control == nullptr) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// From the font the control is actually using rather than from the size that
|
|
203
|
+
// was asked for, because a family substitution changes it.
|
|
204
|
+
if (HDC deviceContext = GetDC(entry.control)) {
|
|
205
|
+
HGDIOBJ previous = SelectObject(deviceContext, entry.font);
|
|
206
|
+
TEXTMETRIC textMetrics{};
|
|
207
|
+
if (GetTextMetrics(deviceContext, &textMetrics) != 0) {
|
|
208
|
+
// Two points of slack, which is where the caret's overhang goes.
|
|
209
|
+
entry.lineHeight = textMetrics.tmHeight + 2;
|
|
210
|
+
}
|
|
211
|
+
SelectObject(deviceContext, previous);
|
|
212
|
+
ReleaseDC(entry.control, deviceContext);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
void Win32TextInputManager::applyProps(Entry &entry, const TextInputProps &props) {
|
|
217
|
+
// The style, through the same translation a <Text> goes through, so a font
|
|
218
|
+
// family or size means the same thing in a field as it does in a label.
|
|
219
|
+
// fontSizeMultiplier is 1: nothing on this platform scales text for
|
|
220
|
+
// accessibility settings yet, and passing 0 would multiply the size away.
|
|
221
|
+
const RnTextStyle style = win32::buildTextStyle(props.getEffectiveTextAttributes(1.0F));
|
|
222
|
+
|
|
223
|
+
entry.textColor = toColorRef(style.color);
|
|
224
|
+
|
|
225
|
+
if (props.backgroundColor) {
|
|
226
|
+
const auto components = facebook::react::colorComponentsFromColor(props.backgroundColor);
|
|
227
|
+
const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
|
|
228
|
+
const COLORREF wanted = toColorRef(rgba);
|
|
229
|
+
if (!entry.hasBackground || wanted != entry.backgroundColor ||
|
|
230
|
+
entry.backgroundBrush == nullptr) {
|
|
231
|
+
if (entry.backgroundBrush != nullptr) {
|
|
232
|
+
DeleteObject(entry.backgroundBrush);
|
|
233
|
+
}
|
|
234
|
+
entry.backgroundColor = wanted;
|
|
235
|
+
entry.backgroundBrush = CreateSolidBrush(wanted);
|
|
236
|
+
}
|
|
237
|
+
entry.hasBackground = true;
|
|
238
|
+
} else {
|
|
239
|
+
entry.hasBackground = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (entry.control == nullptr) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// A negative height is character height rather than cell height, which is
|
|
247
|
+
// what a CSS font-size means and what DirectWrite was given for the same
|
|
248
|
+
// number in a <Text>.
|
|
249
|
+
const int height = -static_cast<int>(std::lround(style.fontSize));
|
|
250
|
+
const std::wstring family = widen(style.fontFamily);
|
|
251
|
+
HFONT font = CreateFont(height,
|
|
252
|
+
0,
|
|
253
|
+
0,
|
|
254
|
+
0,
|
|
255
|
+
style.bold ? FW_BOLD : FW_NORMAL,
|
|
256
|
+
style.italic ? TRUE : FALSE,
|
|
257
|
+
FALSE,
|
|
258
|
+
FALSE,
|
|
259
|
+
DEFAULT_CHARSET,
|
|
260
|
+
OUT_DEFAULT_PRECIS,
|
|
261
|
+
CLIP_DEFAULT_PRECIS,
|
|
262
|
+
CLEARTYPE_QUALITY,
|
|
263
|
+
DEFAULT_PITCH | FF_DONTCARE,
|
|
264
|
+
family.c_str());
|
|
265
|
+
if (font != nullptr) {
|
|
266
|
+
SendMessage(entry.control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
|
|
267
|
+
// After the control has taken the new one: an HFONT in use must not be
|
|
268
|
+
// deleted, and WM_SETFONT does not copy it.
|
|
269
|
+
if (entry.font != nullptr) {
|
|
270
|
+
DeleteObject(entry.font);
|
|
271
|
+
}
|
|
272
|
+
entry.font = font;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// textAlign, which on an EDIT is a style bit rather than a message.
|
|
276
|
+
LONG_PTR windowStyle = GetWindowLongPtr(entry.control, GWL_STYLE);
|
|
277
|
+
const LONG_PTR alignmentBits = static_cast<LONG_PTR>(ES_LEFT | ES_CENTER | ES_RIGHT);
|
|
278
|
+
LONG_PTR alignment = ES_LEFT;
|
|
279
|
+
if (style.align == RnTextAlign::Center) {
|
|
280
|
+
alignment = ES_CENTER;
|
|
281
|
+
} else if (style.align == RnTextAlign::Right) {
|
|
282
|
+
alignment = ES_RIGHT;
|
|
283
|
+
}
|
|
284
|
+
const LONG_PTR wanted = (windowStyle & ~alignmentBits) | alignment;
|
|
285
|
+
if (wanted != windowStyle) {
|
|
286
|
+
SetWindowLongPtr(entry.control, GWL_STYLE, wanted);
|
|
287
|
+
InvalidateRect(entry.control, nullptr, TRUE);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Controlled component: JavaScript owns the value. Two things have to be
|
|
291
|
+
// true at once, and each fails differently.
|
|
292
|
+
//
|
|
293
|
+
// Setting it must not look like typing, or the change reported provokes a
|
|
294
|
+
// re-render that sets it again and the two chase each other -- `applying`.
|
|
295
|
+
//
|
|
296
|
+
// And a prop older than what the user has since typed must not be applied at
|
|
297
|
+
// all, or a fast typist watches characters reorder themselves. That is what
|
|
298
|
+
// `mostRecentEventCount` is for, and dropping the value without recording it
|
|
299
|
+
// is deliberate: the next render, once JavaScript has caught up, applies it.
|
|
300
|
+
const bool stale = props.mostRecentEventCount < entry.eventCount;
|
|
301
|
+
const bool changed = !entry.sawProps || props.text != entry.lastPropText;
|
|
302
|
+
if (changed && !stale) {
|
|
303
|
+
entry.lastPropText = props.text;
|
|
304
|
+
entry.sawProps = true;
|
|
305
|
+
|
|
306
|
+
const int length = GetWindowTextLength(entry.control);
|
|
307
|
+
std::wstring current(static_cast<size_t>(length) + 1, L'\0');
|
|
308
|
+
GetWindowText(entry.control, current.data(), length + 1);
|
|
309
|
+
current.resize(static_cast<size_t>(length));
|
|
310
|
+
|
|
311
|
+
if (props.text != narrow(current)) {
|
|
312
|
+
entry.applying = true;
|
|
313
|
+
// Preserve the caret: setting the text sends it to the start, which would
|
|
314
|
+
// send it home on every keystroke of a controlled field.
|
|
315
|
+
DWORD selectionStart = 0;
|
|
316
|
+
DWORD selectionEnd = 0;
|
|
317
|
+
SendMessage(entry.control,
|
|
318
|
+
EM_GETSEL,
|
|
319
|
+
reinterpret_cast<WPARAM>(&selectionStart),
|
|
320
|
+
reinterpret_cast<LPARAM>(&selectionEnd));
|
|
321
|
+
const std::wstring wide = widen(props.text);
|
|
322
|
+
SetWindowText(entry.control, wide.c_str());
|
|
323
|
+
const auto caret = static_cast<DWORD>(std::min<size_t>(selectionStart, wide.size()));
|
|
324
|
+
SendMessage(entry.control, EM_SETSEL, caret, caret);
|
|
325
|
+
entry.applying = false;
|
|
326
|
+
entry.lastReportedText = props.text;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The cue banner, which is Windows' placeholder and needs visual styles --
|
|
331
|
+
// see the manifest at the top of this file. TRUE keeps it visible while the
|
|
332
|
+
// field has focus and no text, which is what both other desktops do.
|
|
333
|
+
const std::wstring placeholder = widen(props.placeholder);
|
|
334
|
+
SendMessage(entry.control,
|
|
335
|
+
EM_SETCUEBANNER,
|
|
336
|
+
TRUE,
|
|
337
|
+
reinterpret_cast<LPARAM>(placeholder.c_str()));
|
|
338
|
+
|
|
339
|
+
// `editable` is the prop; `readOnly` is the newer spelling of its inverse,
|
|
340
|
+
// and React Native honours both.
|
|
341
|
+
const bool writable = props.traits.editable && !props.readOnly;
|
|
342
|
+
SendMessage(entry.control, EM_SETREADONLY, writable ? FALSE : TRUE, 0);
|
|
343
|
+
|
|
344
|
+
// secureTextEntry is a message here rather than the different *class* AppKit
|
|
345
|
+
// needs, so it can be turned on and off without rebuilding the control and
|
|
346
|
+
// without losing focus -- the one place this platform has the easier job.
|
|
347
|
+
if (props.traits.secureTextEntry != entry.secure) {
|
|
348
|
+
entry.secure = props.traits.secureTextEntry;
|
|
349
|
+
SendMessage(entry.control, EM_SETPASSWORDCHAR, entry.secure ? kPasswordCharacter : 0, 0);
|
|
350
|
+
InvalidateRect(entry.control, nullptr, TRUE);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 0 means no limit, and React Native spells "no limit" as an absent prop --
|
|
354
|
+
// which arrives here as 0 too, so the two agree without a special case.
|
|
355
|
+
SendMessage(entry.control, EM_SETLIMITTEXT, static_cast<WPARAM>(std::max(0, props.maxLength)), 0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
void Win32TextInputManager::destroyPeer(Entry &entry) {
|
|
359
|
+
// Deliberately does not clear the view's back pointer, and must not: by the
|
|
360
|
+
// time this runs the view is already gone. MountingWalk's Delete calls
|
|
361
|
+
// `destroyView` *before* `forgetTag`, and `releaseAllViews` runs before this
|
|
362
|
+
// object's own destructor -- so both paths free the view first and touching
|
|
363
|
+
// it here is a use-after-free. It was one, until the end-to-end suite caught
|
|
364
|
+
// the process exiting 0xC0000005.
|
|
365
|
+
//
|
|
366
|
+
// Nothing needs clearing anyway: the view never outlives its peer. The only
|
|
367
|
+
// moment a live view holds a dead HWND is between the host window being
|
|
368
|
+
// destroyed and the views being freed, and reading a dead HWND's text
|
|
369
|
+
// reports nothing rather than crashing -- which is why `captureBeforeTeardown`
|
|
370
|
+
// takes the dump before the window goes.
|
|
371
|
+
entry.view = nullptr;
|
|
372
|
+
if (entry.control != nullptr) {
|
|
373
|
+
RemoveWindowSubclass(entry.control, editProc, kSubclassId);
|
|
374
|
+
DestroyWindow(entry.control);
|
|
375
|
+
entry.control = nullptr;
|
|
376
|
+
}
|
|
377
|
+
if (entry.font != nullptr) {
|
|
378
|
+
DeleteObject(entry.font);
|
|
379
|
+
entry.font = nullptr;
|
|
380
|
+
}
|
|
381
|
+
if (entry.backgroundBrush != nullptr) {
|
|
382
|
+
DeleteObject(entry.backgroundBrush);
|
|
383
|
+
entry.backgroundBrush = nullptr;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
void Win32TextInputManager::remove(Tag tag) {
|
|
388
|
+
const auto it = entries_.find(tag);
|
|
389
|
+
if (it == entries_.end()) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// The subclass procedure holds a pointer to the Entry, so the window has to
|
|
393
|
+
// go before the map entry does.
|
|
394
|
+
destroyPeer(it->second);
|
|
395
|
+
entries_.erase(it);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Placement
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
403
|
+
for (auto &[tag, entry] : entries_) {
|
|
404
|
+
if (entry.control == nullptr) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
RECT rect{};
|
|
408
|
+
if (!peerRect(entry.view, root, rect)) {
|
|
409
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Inside the content insets, so that `paddingHorizontal` on a field means
|
|
414
|
+
// what it means on a <View>. GTK allocates its GtkText inside the same
|
|
415
|
+
// numbers; here there is no allocation, so the window is placed there.
|
|
416
|
+
rect.left += entry.insets.left;
|
|
417
|
+
rect.top += entry.insets.top;
|
|
418
|
+
rect.right -= entry.insets.right;
|
|
419
|
+
rect.bottom -= entry.insets.bottom;
|
|
420
|
+
|
|
421
|
+
// And no taller than one line, centred in what is left. A single-line EDIT
|
|
422
|
+
// draws its text at the top of its client area -- Windows' own fields are
|
|
423
|
+
// sized to their font, so it never shows -- and in a 44-point field with
|
|
424
|
+
// 16-point text that puts the caret near the top and looks broken. Making
|
|
425
|
+
// the control a strip the height of one line and centring it is what every
|
|
426
|
+
// Win32 application does about this.
|
|
427
|
+
//
|
|
428
|
+
// It also keeps the control away from the corners. The RnWin32View behind
|
|
429
|
+
// it paints the rounded background; a full-height rectangular child window
|
|
430
|
+
// would paint square corners straight over them, and a strip at the middle
|
|
431
|
+
// never reaches the part a radius cuts away.
|
|
432
|
+
const LONG available = rect.bottom - rect.top;
|
|
433
|
+
if (entry.lineHeight > 0 && entry.lineHeight < available) {
|
|
434
|
+
rect.top += (available - entry.lineHeight) / 2;
|
|
435
|
+
rect.bottom = rect.top + entry.lineHeight;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (rect.right <= rect.left || rect.bottom <= rect.top) {
|
|
439
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
SetWindowPos(entry.control,
|
|
444
|
+
nullptr,
|
|
445
|
+
rect.left,
|
|
446
|
+
rect.top,
|
|
447
|
+
rect.right - rect.left,
|
|
448
|
+
rect.bottom - rect.top,
|
|
449
|
+
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Events
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
Win32TextInputManager::Entry *Win32TextInputManager::entryForControl(HWND control) {
|
|
458
|
+
if (control == nullptr) {
|
|
459
|
+
return nullptr;
|
|
460
|
+
}
|
|
461
|
+
for (auto &[tag, entry] : entries_) {
|
|
462
|
+
if (entry.control == control) {
|
|
463
|
+
return &entry;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return nullptr;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
bool Win32TextInputManager::ownsControl(HWND control) const {
|
|
470
|
+
for (const auto &[tag, entry] : entries_) {
|
|
471
|
+
if (entry.control == control) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
bool Win32TextInputManager::focusAt(RnWin32View *root, double x, double y) {
|
|
479
|
+
if (root == nullptr || entries_.empty()) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
RnWin32View *hit = win32::hitTest(root, static_cast<float>(x), static_cast<float>(y));
|
|
483
|
+
for (RnWin32View *view = hit; view != nullptr; view = view->parent()) {
|
|
484
|
+
const auto it = entries_.find(static_cast<Tag>(view->tag()));
|
|
485
|
+
if (it != entries_.end() && it->second.control != nullptr) {
|
|
486
|
+
SetFocus(it->second.control);
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
bool Win32TextInputManager::handleControlCommand(WPARAM wparam, LPARAM lparam) {
|
|
494
|
+
Entry *entry = entryForControl(reinterpret_cast<HWND>(lparam));
|
|
495
|
+
if (entry == nullptr) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
switch (HIWORD(wparam)) {
|
|
500
|
+
case EN_CHANGE:
|
|
501
|
+
reportChange(*entry);
|
|
502
|
+
return true;
|
|
503
|
+
|
|
504
|
+
case EN_SETFOCUS:
|
|
505
|
+
if (const auto emitter = emitterFor(entry->tag)) {
|
|
506
|
+
emitter->onFocus(metricsFor(*entry));
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
|
|
510
|
+
case EN_KILLFOCUS:
|
|
511
|
+
if (const auto emitter = emitterFor(entry->tag)) {
|
|
512
|
+
emitter->onBlur(metricsFor(*entry));
|
|
513
|
+
emitter->onEndEditing(metricsFor(*entry));
|
|
514
|
+
}
|
|
515
|
+
return true;
|
|
516
|
+
|
|
517
|
+
default:
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
// A notification from one of this manager's controls that it does not act on
|
|
521
|
+
// -- EN_UPDATE, EN_MAXTEXT. Still handled, in the sense that the host must
|
|
522
|
+
// not treat it as a menu command.
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
HBRUSH Win32TextInputManager::controlColor(HDC deviceContext, HWND control) {
|
|
527
|
+
Entry *entry = entryForControl(control);
|
|
528
|
+
if (entry == nullptr) {
|
|
529
|
+
return nullptr;
|
|
530
|
+
}
|
|
531
|
+
SetTextColor(deviceContext, entry->textColor);
|
|
532
|
+
if (entry->hasBackground && entry->backgroundBrush != nullptr) {
|
|
533
|
+
SetBkColor(deviceContext, entry->backgroundColor);
|
|
534
|
+
return entry->backgroundBrush;
|
|
535
|
+
}
|
|
536
|
+
// No background prop. The control cannot see through itself to what Direct2D
|
|
537
|
+
// painted behind it, so the window's own ground is the closest honest answer;
|
|
538
|
+
// a field with no backgroundColor over a coloured parent is the case this
|
|
539
|
+
// gets visibly wrong, and the fix is a background prop.
|
|
540
|
+
SetBkColor(deviceContext, GetSysColor(COLOR_WINDOW));
|
|
541
|
+
return GetSysColorBrush(COLOR_WINDOW);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
void Win32TextInputManager::reportChange(Entry &entry) {
|
|
545
|
+
if (entry.applying) {
|
|
546
|
+
// A prop being applied, not the user typing.
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const auto metrics = metricsFor(entry);
|
|
550
|
+
if (metrics.text == entry.lastReportedText) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
entry.lastReportedText = metrics.text;
|
|
554
|
+
entry.eventCount++;
|
|
555
|
+
|
|
556
|
+
if (const auto emitter = emitterFor(entry.tag)) {
|
|
557
|
+
// Rebuilt so it carries the incremented count: React Native drops a prop
|
|
558
|
+
// update whose eventCount is older than the last one it was told about, so
|
|
559
|
+
// an event that under-reports its own count makes the field ignore the
|
|
560
|
+
// value coming back down.
|
|
561
|
+
emitter->onChange(metricsFor(entry));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
std::shared_ptr<const TextInputEventEmitter> Win32TextInputManager::emitterFor(Tag tag) const {
|
|
566
|
+
return std::dynamic_pointer_cast<const TextInputEventEmitter>(lookup_(tag));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
TextInputEventEmitter::Metrics Win32TextInputManager::metricsFor(const Entry &entry) const {
|
|
570
|
+
TextInputEventEmitter::Metrics metrics{};
|
|
571
|
+
metrics.eventCount = entry.eventCount;
|
|
572
|
+
metrics.target = entry.tag;
|
|
573
|
+
metrics.zoomScale = 1.0F;
|
|
574
|
+
|
|
575
|
+
if (entry.control != nullptr) {
|
|
576
|
+
const int length = GetWindowTextLength(entry.control);
|
|
577
|
+
std::wstring text(static_cast<size_t>(length) + 1, L'\0');
|
|
578
|
+
GetWindowText(entry.control, text.data(), length + 1);
|
|
579
|
+
text.resize(static_cast<size_t>(length));
|
|
580
|
+
metrics.text = narrow(text);
|
|
581
|
+
|
|
582
|
+
DWORD start = 0;
|
|
583
|
+
DWORD end = 0;
|
|
584
|
+
SendMessage(entry.control,
|
|
585
|
+
EM_GETSEL,
|
|
586
|
+
reinterpret_cast<WPARAM>(&start),
|
|
587
|
+
reinterpret_cast<LPARAM>(&end));
|
|
588
|
+
metrics.selectionRange =
|
|
589
|
+
AttributedString::Range{static_cast<int>(start), static_cast<int>(end - start)};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// The scroll-shaped fields exist because iOS's text view is a scroll view.
|
|
593
|
+
// Nothing here scrolls yet, so they describe a viewport the size of the
|
|
594
|
+
// field, which is true and keeps JavaScript's arithmetic sane.
|
|
595
|
+
const auto width = static_cast<facebook::react::Float>(
|
|
596
|
+
entry.view != nullptr ? entry.view->frame().width : 0.0F);
|
|
597
|
+
const auto height = static_cast<facebook::react::Float>(
|
|
598
|
+
entry.view != nullptr ? entry.view->frame().height : 0.0F);
|
|
599
|
+
metrics.containerSize = {.width = width, .height = height};
|
|
600
|
+
metrics.contentSize = metrics.containerSize;
|
|
601
|
+
metrics.layoutMeasurement = metrics.containerSize;
|
|
602
|
+
|
|
603
|
+
return metrics;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// The subclass
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
void Win32TextInputManager::reportSelectionIfChanged(Entry &entry) {
|
|
611
|
+
if (entry.control == nullptr) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// Not while a prop is being pushed in. Applying `text` moves the caret, and
|
|
615
|
+
// reporting that as the person selecting something would make a controlled
|
|
616
|
+
// field fight its own render -- the same guard both other hosts have.
|
|
617
|
+
if (entry.applying) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// The caret first, on its own. `metricsFor` copies the field's whole text,
|
|
622
|
+
// and this runs after every mouse move -- so the cheap question is asked
|
|
623
|
+
// first and the expensive one only when the answer has changed.
|
|
624
|
+
DWORD start = 0;
|
|
625
|
+
DWORD end = 0;
|
|
626
|
+
SendMessage(entry.control,
|
|
627
|
+
EM_GETSEL,
|
|
628
|
+
reinterpret_cast<WPARAM>(&start),
|
|
629
|
+
reinterpret_cast<LPARAM>(&end));
|
|
630
|
+
const auto location = static_cast<int>(start);
|
|
631
|
+
const auto length = static_cast<int>(end - start);
|
|
632
|
+
if (location == entry.lastReportedSelection.location &&
|
|
633
|
+
length == entry.lastReportedSelection.length) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
entry.lastReportedSelection = AttributedString::Range{location, length};
|
|
637
|
+
|
|
638
|
+
if (const auto emitter = emitterFor(entry.tag)) {
|
|
639
|
+
emitter->onSelectionChange(metricsFor(entry));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
LRESULT CALLBACK Win32TextInputManager::editProc(
|
|
644
|
+
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR id, DWORD_PTR data) {
|
|
645
|
+
auto *entry = reinterpret_cast<Entry *>(data);
|
|
646
|
+
|
|
647
|
+
if (message == WM_CHAR && entry != nullptr && entry->owner != nullptr) {
|
|
648
|
+
// React Native's contract, the same one both other hosts follow: 'Enter'
|
|
649
|
+
// and 'Backspace' by name, the typed character otherwise -- including ' '
|
|
650
|
+
// for space -- and nothing at all for a key that produces no character.
|
|
651
|
+
// WM_CHAR is already the character, so Escape and Tab fall out as
|
|
652
|
+
// unprintable rather than needing to be named.
|
|
653
|
+
const auto typed = static_cast<wchar_t>(wparam);
|
|
654
|
+
std::string key;
|
|
655
|
+
if (typed == L'\r' || typed == L'\n') {
|
|
656
|
+
key = "Enter";
|
|
657
|
+
} else if (typed == L'\b') {
|
|
658
|
+
key = "Backspace";
|
|
659
|
+
} else if (std::iswprint(static_cast<wint_t>(typed))) {
|
|
660
|
+
key = narrow(std::wstring(1, typed));
|
|
661
|
+
}
|
|
662
|
+
if (!key.empty()) {
|
|
663
|
+
if (const auto emitter = entry->owner->emitterFor(entry->tag)) {
|
|
664
|
+
TextInputEventEmitter::KeyPressMetrics metrics{};
|
|
665
|
+
metrics.text = key;
|
|
666
|
+
metrics.eventCount = entry->eventCount;
|
|
667
|
+
emitter->onKeyPress(metrics);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// A single-line EDIT has nowhere to put a newline, so it beeps at one. That
|
|
672
|
+
// beep is the sound of an unhandled Enter, and Enter is what React Native
|
|
673
|
+
// calls submitEditing -- so it is caught here and swallowed. Escape and Tab
|
|
674
|
+
// beep for the same reason: neither has a meaning inside a field, and
|
|
675
|
+
// nothing on this platform implements a tab order for the second to reach.
|
|
676
|
+
const auto character = typed;
|
|
677
|
+
if (character == L'\r' || character == L'\n') {
|
|
678
|
+
if (const auto emitter = entry->owner->emitterFor(entry->tag)) {
|
|
679
|
+
emitter->onSubmitEditing(entry->owner->metricsFor(*entry));
|
|
680
|
+
}
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
if (character == L'\x1b' || character == L'\t') {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (message == WM_NCDESTROY) {
|
|
689
|
+
RemoveWindowSubclass(hwnd, editProc, id);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const LRESULT result = DefSubclassProc(hwnd, message, wparam, lparam);
|
|
693
|
+
|
|
694
|
+
// Windows does not report the caret moving: EN_SELCHANGE is RichEdit's, and a
|
|
695
|
+
// plain EDIT says nothing. So the selection is read after anything that could
|
|
696
|
+
// have moved it -- which is why this runs *after* DefSubclassProc, once the
|
|
697
|
+
// control has done the moving.
|
|
698
|
+
if (entry != nullptr && entry->owner != nullptr) {
|
|
699
|
+
switch (message) {
|
|
700
|
+
// Its own case rather than part of the list below: `wparam` here is a
|
|
701
|
+
// button mask, and everywhere else in this switch it is something else
|
|
702
|
+
// entirely -- a character for WM_CHAR, a virtual key for WM_KEYDOWN. A
|
|
703
|
+
// fallthrough would test a character against MK_LBUTTON, which passes for
|
|
704
|
+
// odd code points and fails for even ones.
|
|
705
|
+
case WM_MOUSEMOVE:
|
|
706
|
+
// A hover cannot move the caret, and this runs on every mouse move over
|
|
707
|
+
// the field.
|
|
708
|
+
if ((wparam & MK_LBUTTON) != 0) {
|
|
709
|
+
entry->owner->reportSelectionIfChanged(*entry);
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
case WM_CHAR:
|
|
713
|
+
case WM_KEYDOWN:
|
|
714
|
+
case WM_KEYUP:
|
|
715
|
+
case WM_LBUTTONDOWN:
|
|
716
|
+
case WM_LBUTTONUP:
|
|
717
|
+
case WM_SETFOCUS:
|
|
718
|
+
case WM_CUT:
|
|
719
|
+
case WM_PASTE:
|
|
720
|
+
case WM_CLEAR:
|
|
721
|
+
case WM_UNDO:
|
|
722
|
+
case EM_SETSEL:
|
|
723
|
+
entry->owner->reportSelectionIfChanged(*entry);
|
|
724
|
+
break;
|
|
725
|
+
default:
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Commands
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
bool Win32TextInputManager::dispatchCommand(Tag tag,
|
|
738
|
+
const std::string &name,
|
|
739
|
+
const folly::dynamic &args) {
|
|
740
|
+
const auto it = entries_.find(tag);
|
|
741
|
+
if (it == entries_.end()) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
Entry &entry = it->second;
|
|
745
|
+
|
|
746
|
+
if (name == "focus") {
|
|
747
|
+
if (entry.control != nullptr) {
|
|
748
|
+
SetFocus(entry.control);
|
|
749
|
+
}
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (name == "blur") {
|
|
754
|
+
// Focus moves to the host window rather than being dropped: Win32 has no
|
|
755
|
+
// "nothing has focus" within an active window, and SetFocus(nullptr) takes
|
|
756
|
+
// keyboard input away from the process entirely, which is not what blur
|
|
757
|
+
// means. The control still sees EN_KILLFOCUS, so JavaScript sees onBlur.
|
|
758
|
+
if (entry.control != nullptr && GetFocus() == entry.control) {
|
|
759
|
+
SetFocus(host_);
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (name == "setTextAndSelection") {
|
|
765
|
+
// [eventCount, text, start, end]. An eventCount older than what the user
|
|
766
|
+
// has since typed means this command is stale and must be dropped, which is
|
|
767
|
+
// the whole reason React Native counts them.
|
|
768
|
+
if (args.isArray() && args.size() >= 2 && entry.control != nullptr) {
|
|
769
|
+
const int eventCount = static_cast<int>(args[0].asInt());
|
|
770
|
+
if (eventCount < entry.eventCount) {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
entry.applying = true;
|
|
774
|
+
const auto text = args[1].isString() ? args[1].asString() : std::string{};
|
|
775
|
+
const std::wstring wide = widen(text);
|
|
776
|
+
SetWindowText(entry.control, wide.c_str());
|
|
777
|
+
if (args.size() >= 4 && args[2].isInt() && args[3].isInt()) {
|
|
778
|
+
SendMessage(entry.control,
|
|
779
|
+
EM_SETSEL,
|
|
780
|
+
static_cast<WPARAM>(args[2].asInt()),
|
|
781
|
+
static_cast<LPARAM>(args[3].asInt()));
|
|
782
|
+
}
|
|
783
|
+
entry.applying = false;
|
|
784
|
+
entry.lastReportedText = text;
|
|
785
|
+
}
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
// Scripted typing
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
bool Win32TextInputManager::typeIntoFocused(const std::string &text) {
|
|
797
|
+
HWND focused = GetFocus();
|
|
798
|
+
if (!ownsControl(focused)) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
// WM_CHAR per UTF-16 code unit, which is what a keyboard driver would send
|
|
802
|
+
// and what an IME sends for a composed character. So this skips the driver
|
|
803
|
+
// and nothing above it -- the EDIT's own handling, EN_CHANGE, the emitter,
|
|
804
|
+
// the event beat and React all run exactly as they would.
|
|
805
|
+
for (const wchar_t unit : widen(text)) {
|
|
806
|
+
SendMessage(focused, WM_CHAR, static_cast<WPARAM>(unit), 1);
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
} // namespace basalt
|