basalt-win32 0.0.0-stage → 0.2.0

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 (90) 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 +429 -0
  5. package/native/tests/test_win32_accessibility.cpp +585 -0
  6. package/native/tests/test_win32_animated_image.cpp +351 -0
  7. package/native/tests/test_win32_clipboard.cpp +121 -0
  8. package/native/tests/test_win32_focus.cpp +195 -0
  9. package/native/tests/test_win32_hittest.cpp +750 -0
  10. package/native/tests/test_win32_image.cpp +370 -0
  11. package/native/tests/test_win32_imageloader.cpp +208 -0
  12. package/native/tests/test_win32_input.cpp +511 -0
  13. package/native/tests/test_win32_key_events.cpp +84 -0
  14. package/native/tests/test_win32_mounting.cpp +757 -0
  15. package/native/tests/test_win32_paint.cpp +1799 -0
  16. package/native/tests/test_win32_scrollview.cpp +647 -0
  17. package/native/tests/test_win32_selection.cpp +215 -0
  18. package/native/tests/test_win32_text.cpp +1456 -0
  19. package/native/tests/test_win32_textinput.cpp +1594 -0
  20. package/native/tests/test_win32_titlebar.cpp +111 -0
  21. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  22. package/native/tests/test_win32_view.cpp +222 -0
  23. package/native/win32/ColorSchemeWin32.cpp +56 -0
  24. package/native/win32/ComponentRegistryWin32.cpp +156 -0
  25. package/native/win32/DirectWriteLayout.cpp +485 -0
  26. package/native/win32/DirectWriteLayout.h +77 -0
  27. package/native/win32/DirectWriteLayoutManager.cpp +154 -0
  28. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  29. package/native/win32/PlatformServicesWin32.cpp +744 -0
  30. package/native/win32/RnWin32Accessible.cpp +343 -0
  31. package/native/win32/RnWin32Accessible.h +130 -0
  32. package/native/win32/RnWin32Image.cpp +588 -0
  33. package/native/win32/RnWin32Image.h +139 -0
  34. package/native/win32/RnWin32TextLayout.cpp +1379 -0
  35. package/native/win32/RnWin32TextLayout.h +390 -0
  36. package/native/win32/RnWin32View.cpp +2956 -0
  37. package/native/win32/RnWin32View.h +926 -0
  38. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  39. package/native/win32/Win32AnimationChoreographer.h +61 -0
  40. package/native/win32/Win32Clip.h +209 -0
  41. package/native/win32/Win32Cursors.cpp +309 -0
  42. package/native/win32/Win32Cursors.h +121 -0
  43. package/native/win32/Win32DropTarget.cpp +567 -0
  44. package/native/win32/Win32DropTarget.h +42 -0
  45. package/native/win32/Win32Focus.cpp +262 -0
  46. package/native/win32/Win32Focus.h +109 -0
  47. package/native/win32/Win32ImageLoader.cpp +199 -0
  48. package/native/win32/Win32ImageLoader.h +133 -0
  49. package/native/win32/Win32InputScopes.h +92 -0
  50. package/native/win32/Win32KeyEvents.cpp +126 -0
  51. package/native/win32/Win32KeyEvents.h +20 -0
  52. package/native/win32/Win32MenuBar.cpp +382 -0
  53. package/native/win32/Win32MenuBar.h +16 -0
  54. package/native/win32/Win32MountingManager.cpp +1282 -0
  55. package/native/win32/Win32MountingManager.h +243 -0
  56. package/native/win32/Win32Offscreen.h +53 -0
  57. package/native/win32/Win32Packaging.cpp +161 -0
  58. package/native/win32/Win32Packaging.h +56 -0
  59. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  60. package/native/win32/Win32RunLoopObserver.h +42 -0
  61. package/native/win32/Win32ScrollView.cpp +718 -0
  62. package/native/win32/Win32ScrollView.h +256 -0
  63. package/native/win32/Win32SkiaContext.cpp +221 -0
  64. package/native/win32/Win32SkiaContext.h +53 -0
  65. package/native/win32/Win32SkiaModule.cpp +80 -0
  66. package/native/win32/Win32SkiaModule.h +50 -0
  67. package/native/win32/Win32SkiaPeer.cpp +358 -0
  68. package/native/win32/Win32SkiaPeer.h +66 -0
  69. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  70. package/native/win32/Win32Snapshot.cpp +201 -0
  71. package/native/win32/Win32Snapshot.h +66 -0
  72. package/native/win32/Win32Strings.h +54 -0
  73. package/native/win32/Win32TextInput.cpp +1296 -0
  74. package/native/win32/Win32TextInput.h +341 -0
  75. package/native/win32/Win32TitleBar.cpp +562 -0
  76. package/native/win32/Win32TitleBar.h +146 -0
  77. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  78. package/native/win32/Win32TitleBarLayout.h +58 -0
  79. package/native/win32/Win32TouchDispatcher.cpp +577 -0
  80. package/native/win32/Win32TouchDispatcher.h +201 -0
  81. package/native/win32/Win32UiThread.h +27 -0
  82. package/native/win32/Win32WindowControl.cpp +329 -0
  83. package/native/win32/Win32WindowModule.cpp +209 -0
  84. package/native/win32/Win32WindowModule.h +72 -0
  85. package/native/win32/demo_layout_win32.cpp +232 -0
  86. package/native/win32/main_win32.cpp +2722 -0
  87. package/native/win32/mount_harness_win32.cpp +316 -0
  88. package/package.json +25 -4
  89. package/react-native.config.js +22 -0
  90. package/README.md +0 -3
@@ -0,0 +1,1296 @@
1
+ #include "Win32TextInput.h"
2
+
3
+ #include "DirectWriteLayout.h"
4
+ #include "TextChecking.h"
5
+ #include "Win32InputScopes.h"
6
+ #include "Win32Strings.h"
7
+
8
+ #include <react/renderer/components/iostextinput/TextInputProps.h>
9
+
10
+ #include <commctrl.h>
11
+
12
+ #include <algorithm>
13
+ #include <cstdint>
14
+ #include <cwctype>
15
+ #include <cmath>
16
+ #include <vector>
17
+
18
+ // Visual styles, which an EDIT needs for two things: the cue banner that backs
19
+ // `placeholder` is a comctl32 v6 message and silently does nothing without it,
20
+ // and a v5 EDIT looks like Windows 95 next to everything else on the screen.
21
+ // Declared here rather than in a .manifest file so that running the binary
22
+ // straight out of the build directory behaves the same as running an installed
23
+ // one -- the same argument SetProcessDpiAwarenessContext makes in main.
24
+ #pragma comment(linker, \
25
+ "\"/manifestdependency:type='win32' " \
26
+ "name='Microsoft.Windows.Common-Controls' " \
27
+ "version='6.0.0.0' " \
28
+ "processorArchitecture='*' " \
29
+ "publicKeyToken='6595b64144ccf1df' " \
30
+ "language='*'\"")
31
+
32
+ namespace basalt {
33
+
34
+ using facebook::react::AttributedString;
35
+ using facebook::react::ShadowView;
36
+ using facebook::react::Tag;
37
+ using facebook::react::TextInputEventEmitter;
38
+ using facebook::react::TextInputProps;
39
+ using win32::narrow;
40
+ using win32::RnTextAlign;
41
+ using win32::RnTextStyle;
42
+ using win32::RnWin32View;
43
+ using win32::widen;
44
+
45
+ namespace {
46
+
47
+ // One id for every peer. An EDIT's notifications arrive as WM_COMMAND with a
48
+ // control id, and the id is only used to tell "this is one of ours" from "this
49
+ // is a menu item"; which control it was comes from the HWND in lparam.
50
+ constexpr WORD kControlId = 0x4200;
51
+
52
+ // SetWindowSubclass takes an id of its own, distinct from the control id.
53
+ constexpr UINT_PTR kSubclassId = 1;
54
+
55
+ // What a password field shows instead of the character. Windows' own default is
56
+ // a bullet under visual styles; naming it means the two other desktops and this
57
+ // one agree about what secureTextEntry looks like.
58
+ constexpr wchar_t kPasswordCharacter = L'\x25CF';
59
+
60
+ COLORREF toColorRef(const float components[4]) {
61
+ const auto channel = [](float value) {
62
+ return static_cast<int>(std::clamp(value, 0.0F, 1.0F) * 255.0F + 0.5F);
63
+ };
64
+ return RGB(channel(components[0]), channel(components[1]), channel(components[2]));
65
+ }
66
+
67
+ // A view's rectangle in the surface root's coordinates, and whether any of it is
68
+ // still visible.
69
+ //
70
+ // Translation only: an EDIT is a window and a window cannot be rotated, so a
71
+ // transformed field's peer sits where the untransformed one would. That is a
72
+ // real difference from the painted view behind it, and there is no cheap fix --
73
+ // see the header.
74
+ //
75
+ // Visibility is the other half. A child window is not clipped by anything in
76
+ // the React tree, so a field scrolled out of its list would otherwise go on
77
+ // being drawn over the list's neighbours. Any clipping ancestor that the
78
+ // rectangle has left entirely hides it.
79
+ bool peerRect(RnWin32View *view, RnWin32View *root, RECT &out) {
80
+ if (view == nullptr || root == nullptr) {
81
+ return false;
82
+ }
83
+
84
+ double x = 0.0;
85
+ double y = 0.0;
86
+ const double width = view->frame().width;
87
+ const double height = view->frame().height;
88
+
89
+ for (RnWin32View *current = view; current != nullptr; current = current->parent()) {
90
+ if (current->hidden()) {
91
+ return false;
92
+ }
93
+ if (current == root) {
94
+ break;
95
+ }
96
+ x += current->frame().x;
97
+ y += current->frame().y;
98
+
99
+ RnWin32View *parent = current->parent();
100
+ if (parent == nullptr) {
101
+ // Detached: between a Remove and its Delete, or never inserted. Not on
102
+ // screen either way.
103
+ return false;
104
+ }
105
+ x -= parent->scrollX();
106
+ y -= parent->scrollY();
107
+
108
+ if (parent->clipsChildren()) {
109
+ // In the parent's own coordinates the box is 0..width, 0..height, and
110
+ // (x, y) is where this view now sits relative to it.
111
+ if (x + width <= 0 || y + height <= 0 || x >= parent->frame().width ||
112
+ y >= parent->frame().height) {
113
+ return false;
114
+ }
115
+ }
116
+ }
117
+
118
+ out.left = static_cast<LONG>(std::lround(x));
119
+ out.top = static_cast<LONG>(std::lround(y));
120
+ out.right = out.left + static_cast<LONG>(std::lround(width));
121
+ out.bottom = out.top + static_cast<LONG>(std::lround(height));
122
+ return true;
123
+ }
124
+
125
+ } // namespace
126
+
127
+ Win32TextInputManager::Win32TextInputManager(EmitterLookup lookup) : lookup_(std::move(lookup)) {}
128
+
129
+ Win32TextInputManager::~Win32TextInputManager() {
130
+ for (auto &[tag, entry] : entries_) {
131
+ destroyPeer(entry);
132
+ }
133
+ }
134
+
135
+ void Win32TextInputManager::setHostWindow(HWND window) {
136
+ host_ = window;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Mutations
141
+ // ---------------------------------------------------------------------------
142
+
143
+ void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowView) {
144
+ const Tag tag = shadowView.tag;
145
+ auto [it, inserted] = entries_.try_emplace(tag);
146
+ Entry &entry = it->second;
147
+
148
+ entry.view = view;
149
+ entry.tag = tag;
150
+ entry.owner = this;
151
+
152
+ const auto props = std::dynamic_pointer_cast<const TextInputProps>(shadowView.props);
153
+
154
+ // `multiline` decides which kind of EDIT this is, and that is a creation-time
155
+ // decision: `ES_MULTILINE` cannot be added to a live control -- the style bit
156
+ // is read when the window is made, and setting it afterwards leaves a control
157
+ // that still behaves as one line. So a field that changes the prop has its
158
+ // peer rebuilt, which is rare enough to be worth the simplicity: React Native
159
+ // apps choose multiline per field rather than toggling it.
160
+ const bool multiline = props != nullptr && props->multiline;
161
+ if (entry.control != nullptr && multiline != entry.multiline) {
162
+ const bool hadFocus = GetFocus() == entry.control;
163
+ destroyPeer(entry);
164
+ entry.view = view;
165
+ entry.sawProps = false;
166
+ entry.sawInputScope = false;
167
+ if (hadFocus) {
168
+ // Rebuilt under the user's hands, so the keyboard goes back where it was.
169
+ pendingAutoFocus_.push_back(tag);
170
+ }
171
+ }
172
+
173
+ if (entry.control == nullptr && host_ != nullptr) {
174
+ // ES_AUTOHSCROLL so the caret can leave the visible box rather than the
175
+ // control refusing further input; WS_CLIPSIBLINGS so two adjacent fields do
176
+ // not paint into each other. No border and no ES_ styles for one: the
177
+ // RnWin32View behind it draws the background, the border and the corner
178
+ // radius, because those are React Native style props and the control knows
179
+ // nothing about them.
180
+ //
181
+ // A multiline field takes `ES_MULTILINE` and `ES_AUTOVSCROLL` instead of
182
+ // the horizontal one: text wraps rather than scrolling sideways, which is
183
+ // what a multiline <TextInput> does on every platform, and the vertical
184
+ // scroll is what lets the caret leave the bottom of the box. No `WS_VSCROLL`
185
+ // on purpose -- a scrollbar inside a styled field is a Win32 scrollbar
186
+ // drawn over a React Native background, and neither other host shows one.
187
+ entry.multiline = multiline;
188
+ const DWORD style = WS_CHILD | WS_CLIPSIBLINGS | ES_LEFT
189
+ | (multiline ? (ES_MULTILINE | ES_AUTOVSCROLL) : ES_AUTOHSCROLL);
190
+ entry.control = CreateWindowEx(0,
191
+ L"EDIT",
192
+ L"",
193
+ style,
194
+ 0,
195
+ 0,
196
+ 0,
197
+ 0,
198
+ host_,
199
+ reinterpret_cast<HMENU>(static_cast<UINT_PTR>(kControlId)),
200
+ GetModuleHandle(nullptr),
201
+ nullptr);
202
+ if (entry.control != nullptr) {
203
+ SetWindowSubclass(entry.control, editProc, kSubclassId, reinterpret_cast<DWORD_PTR>(&entry));
204
+ }
205
+ // So describeTree can report the field content. The view neither owns the
206
+ // control nor draws it; see RnWin32View::setEditablePeer.
207
+ if (view != nullptr) {
208
+ view->setEditablePeer(entry.control);
209
+ }
210
+ }
211
+
212
+ if (props == nullptr) {
213
+ return;
214
+ }
215
+ applyProps(entry, *props);
216
+ // After the font: the line height is measured against it.
217
+ measureShape(entry, shadowView.layoutMetrics);
218
+
219
+ if (inserted && props->autoFocus) {
220
+ // Recorded, not performed: see flushAutoFocus in the header. Unlike the other
221
+ // two hosts the control may already be focusable here, but it is not yet
222
+ // visible, and having the three hosts agree is worth more than the one call
223
+ // saved.
224
+ pendingAutoFocus_.push_back(tag);
225
+ }
226
+ }
227
+
228
+ void Win32TextInputManager::flushAutoFocus() {
229
+ if (pendingAutoFocus_.empty()) {
230
+ return;
231
+ }
232
+ const std::vector<facebook::react::Tag> pending = std::move(pendingAutoFocus_);
233
+ pendingAutoFocus_.clear();
234
+
235
+ for (const facebook::react::Tag tag : pending) {
236
+ const auto it = entries_.find(tag);
237
+ if (it == entries_.end() || it->second.control == nullptr) {
238
+ continue;
239
+ }
240
+ // The same call the `focus` command makes.
241
+ SetFocus(it->second.control);
242
+ }
243
+ }
244
+
245
+ void Win32TextInputManager::measureShape(Entry &entry,
246
+ const facebook::react::LayoutMetrics &metrics) {
247
+ const auto &insets = metrics.contentInsets;
248
+ entry.insets = RECT{static_cast<LONG>(std::lround(insets.left)),
249
+ static_cast<LONG>(std::lround(insets.top)),
250
+ static_cast<LONG>(std::lround(insets.right)),
251
+ static_cast<LONG>(std::lround(insets.bottom))};
252
+
253
+ if (entry.control == nullptr) {
254
+ return;
255
+ }
256
+ // From the font the control is actually using rather than from the size that
257
+ // was asked for, because a family substitution changes it.
258
+ if (HDC deviceContext = GetDC(entry.control)) {
259
+ HGDIOBJ previous = SelectObject(deviceContext, entry.font);
260
+ TEXTMETRIC textMetrics{};
261
+ if (GetTextMetrics(deviceContext, &textMetrics) != 0) {
262
+ // Two points of slack, which is where the caret's overhang goes.
263
+ entry.lineHeight = textMetrics.tmHeight + 2;
264
+ }
265
+ SelectObject(deviceContext, previous);
266
+ ReleaseDC(entry.control, deviceContext);
267
+ }
268
+ }
269
+
270
+ void Win32TextInputManager::applyProps(Entry &entry, const TextInputProps &props) {
271
+ // The style, through the same translation a <Text> goes through, so a font
272
+ // family or size means the same thing in a field as it does in a label.
273
+ // 1 for fontSizeMultiplier: buildTextStyle applies the text scale itself,
274
+ // through core/FontScaling.h, so passing one here would square it. On this
275
+ // host that scale is 1 anyway; DirectWriteLayout.cpp says why.
276
+ const RnTextStyle style = win32::buildTextStyle(props.getEffectiveTextAttributes(1.0F));
277
+
278
+ entry.textColor = toColorRef(style.color);
279
+
280
+ if (props.backgroundColor) {
281
+ const auto components = facebook::react::colorComponentsFromColor(props.backgroundColor);
282
+ const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
283
+ const COLORREF wanted = toColorRef(rgba);
284
+ if (!entry.hasBackground || wanted != entry.backgroundColor ||
285
+ entry.backgroundBrush == nullptr) {
286
+ if (entry.backgroundBrush != nullptr) {
287
+ DeleteObject(entry.backgroundBrush);
288
+ }
289
+ entry.backgroundColor = wanted;
290
+ entry.backgroundBrush = CreateSolidBrush(wanted);
291
+ }
292
+ entry.hasBackground = true;
293
+ } else {
294
+ entry.hasBackground = false;
295
+ }
296
+
297
+ if (entry.control == nullptr) {
298
+ return;
299
+ }
300
+
301
+ // A negative height is character height rather than cell height, which is
302
+ // what a CSS font-size means and what DirectWrite was given for the same
303
+ // number in a <Text>.
304
+ const int height = -static_cast<int>(std::lround(style.fontSize));
305
+ const std::wstring family = widen(style.fontFamily);
306
+ HFONT font = CreateFont(height,
307
+ 0,
308
+ 0,
309
+ 0,
310
+ style.bold ? FW_BOLD : FW_NORMAL,
311
+ style.italic ? TRUE : FALSE,
312
+ FALSE,
313
+ FALSE,
314
+ DEFAULT_CHARSET,
315
+ OUT_DEFAULT_PRECIS,
316
+ CLIP_DEFAULT_PRECIS,
317
+ CLEARTYPE_QUALITY,
318
+ DEFAULT_PITCH | FF_DONTCARE,
319
+ family.c_str());
320
+ if (font != nullptr) {
321
+ SendMessage(entry.control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
322
+ // After the control has taken the new one: an HFONT in use must not be
323
+ // deleted, and WM_SETFONT does not copy it.
324
+ if (entry.font != nullptr) {
325
+ DeleteObject(entry.font);
326
+ }
327
+ entry.font = font;
328
+ }
329
+
330
+ // textAlign, which on an EDIT is a style bit rather than a message.
331
+ LONG_PTR windowStyle = GetWindowLongPtr(entry.control, GWL_STYLE);
332
+ const LONG_PTR alignmentBits = static_cast<LONG_PTR>(ES_LEFT | ES_CENTER | ES_RIGHT);
333
+ LONG_PTR alignment = ES_LEFT;
334
+ if (style.align == RnTextAlign::Center) {
335
+ alignment = ES_CENTER;
336
+ } else if (style.align == RnTextAlign::Right) {
337
+ alignment = ES_RIGHT;
338
+ }
339
+ const LONG_PTR wanted = (windowStyle & ~alignmentBits) | alignment;
340
+ if (wanted != windowStyle) {
341
+ SetWindowLongPtr(entry.control, GWL_STYLE, wanted);
342
+ InvalidateRect(entry.control, nullptr, TRUE);
343
+ }
344
+
345
+ // Controlled component: JavaScript owns the value. Two things have to be
346
+ // true at once, and each fails differently.
347
+ //
348
+ // Setting it must not look like typing, or the change reported provokes a
349
+ // re-render that sets it again and the two chase each other -- `applying`.
350
+ //
351
+ // And a prop older than what the user has since typed must not be applied at
352
+ // all, or a fast typist watches characters reorder themselves. That is what
353
+ // `mostRecentEventCount` is for, and dropping the value without recording it
354
+ // is deliberate: the next render, once JavaScript has caught up, applies it.
355
+ const bool stale = props.mostRecentEventCount < entry.eventCount;
356
+ const bool changed = !entry.sawProps || props.text != entry.lastPropText;
357
+ if (changed && !stale) {
358
+ entry.lastPropText = props.text;
359
+ entry.sawProps = true;
360
+
361
+ const int length = GetWindowTextLength(entry.control);
362
+ std::wstring current(static_cast<size_t>(length) + 1, L'\0');
363
+ GetWindowText(entry.control, current.data(), length + 1);
364
+ current.resize(static_cast<size_t>(length));
365
+
366
+ if (props.text != narrow(current)) {
367
+ entry.applying = true;
368
+ // Preserve the caret: setting the text sends it to the start, which would
369
+ // send it home on every keystroke of a controlled field.
370
+ DWORD selectionStart = 0;
371
+ DWORD selectionEnd = 0;
372
+ SendMessage(entry.control,
373
+ EM_GETSEL,
374
+ reinterpret_cast<WPARAM>(&selectionStart),
375
+ reinterpret_cast<LPARAM>(&selectionEnd));
376
+ const std::wstring wide = widen(props.text);
377
+ SetWindowText(entry.control, wide.c_str());
378
+ const auto caret = static_cast<DWORD>(std::min<size_t>(selectionStart, wide.size()));
379
+ SendMessage(entry.control, EM_SETSEL, caret, caret);
380
+ entry.applying = false;
381
+ entry.lastReportedText = props.text;
382
+ }
383
+ }
384
+
385
+ // A controlled *selection*, under the same staleness rule the text is under:
386
+ // JavaScript that has not yet seen the last keystroke must not be allowed to
387
+ // drag the caret back to where it thought it was.
388
+ //
389
+ // Applied when it changes rather than whenever it differs, for the reason the
390
+ // text is: a merely uncontrolled field sends no selection at all, and
391
+ // re-asserting one every render would fight the user's own arrow keys.
392
+ //
393
+ // `EM_SETSEL` is the whole of it here, where GTK has `select_region` and
394
+ // AppKit has to reach the field editor the field borrows. Between
395
+ // `entry.applying` so the selection this installs is not reported back as the
396
+ // user having moved the caret, which is the same guard the text uses and the
397
+ // same loop it would otherwise start.
398
+ if (props.selection.has_value() && !stale) {
399
+ const auto &selection = *props.selection;
400
+ if (!entry.lastPropSelection.has_value()
401
+ || entry.lastPropSelection->start != selection.start
402
+ || entry.lastPropSelection->end != selection.end) {
403
+ entry.lastPropSelection = selection;
404
+ entry.applying = true;
405
+ SendMessage(entry.control,
406
+ EM_SETSEL,
407
+ static_cast<WPARAM>(selection.start),
408
+ static_cast<LPARAM>(selection.end));
409
+ // And scroll it into view, which `EM_SETSEL` does not do on its own: a
410
+ // selection set past the visible end of a long single-line field would
411
+ // otherwise be somewhere the user cannot see.
412
+ SendMessage(entry.control, EM_SCROLLCARET, 0, 0);
413
+ entry.applying = false;
414
+ entry.lastReportedSelection = facebook::react::AttributedString::Range{
415
+ selection.start, selection.end - selection.start};
416
+ }
417
+ }
418
+
419
+ // The three colours React Native has for a field: the placeholder's, the
420
+ // selection's and the caret's. None of them is a property of an EDIT.
421
+ //
422
+ // `cursorColor` falls back to `selectionColor`, which is React Native's
423
+ // documented contract rather than an invention here -- `selectionColor` is
424
+ // "the highlight, selection handle and cursor color of the text input" and
425
+ // `cursorColor` overrides the caret alone -- and is how both other hosts read
426
+ // the pair.
427
+ //
428
+ // **`selectionColor` colours the caret and nothing else on this host.** The
429
+ // selection highlight of a classic EDIT is `COLOR_HIGHLIGHT` and there is no
430
+ // message that changes it; backlog/textinput.md records what would, which is
431
+ // hosting a windowless RichEdit and answering `ITextHost::TxGetSysColor`.
432
+ const facebook::react::SharedColor caret =
433
+ props.cursorColor ? props.cursorColor : props.selectionColor;
434
+ entry.hasCaretColour = static_cast<bool>(caret);
435
+ if (entry.hasCaretColour) {
436
+ const auto components = facebook::react::colorComponentsFromColor(caret);
437
+ const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
438
+ entry.caretColour = toColorRef(rgba);
439
+ }
440
+
441
+ // `caretHidden`, which this platform gets from the same XOR the colour uses:
442
+ // a caret whose bitmap is all zeroes leaves every pixel it is drawn over
443
+ // exactly as it was, because anything XOR zero is itself. So "hidden" is a
444
+ // caret of black bits, and `installCaret` has the one branch for it.
445
+ entry.caretHidden = props.traits.caretHidden;
446
+
447
+ // `submitBehavior`, resolved by React Native rather than here: `Default` is
448
+ // `newline` for a multiline field and `blurAndSubmit` for a single-line one.
449
+ entry.submitBehavior = props.getNonDefaultSubmitBehavior();
450
+
451
+ // `contextMenuHidden`, acted on in the subclass: see the WM_CONTEXTMENU case.
452
+ entry.contextMenuHidden = props.traits.contextMenuHidden;
453
+
454
+ entry.hasPlaceholderColour = static_cast<bool>(props.placeholderTextColor);
455
+ if (entry.hasPlaceholderColour) {
456
+ const auto components =
457
+ facebook::react::colorComponentsFromColor(props.placeholderTextColor);
458
+ const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
459
+ entry.placeholderColour = toColorRef(rgba);
460
+ }
461
+
462
+ // The cue banner, which is Windows' placeholder and needs visual styles --
463
+ // see the manifest at the top of this file. TRUE keeps it visible while the
464
+ // field has focus and no text, which is what both other desktops do.
465
+ //
466
+ // Asked for only when the app named no colour. `EM_SETCUEBANNER` has none --
467
+ // it is drawn in the system's grey, with no message to change it -- so a
468
+ // field that asked for a red placeholder is drawn by `drawPlaceholder`
469
+ // instead, and the banner is cleared so the two do not overlap.
470
+ entry.placeholder = widen(props.placeholder);
471
+ SendMessage(entry.control,
472
+ EM_SETCUEBANNER,
473
+ TRUE,
474
+ reinterpret_cast<LPARAM>(entry.hasPlaceholderColour ? L"" : entry.placeholder.c_str()));
475
+ if (entry.hasPlaceholderColour) {
476
+ // The control does not know its own appearance changed, and an empty field
477
+ // showing a stale banner is what a missing invalidation looks like.
478
+ InvalidateRect(entry.control, nullptr, TRUE);
479
+ }
480
+
481
+ // `editable` is the prop; `readOnly` is the newer spelling of its inverse,
482
+ // and React Native honours both.
483
+ const bool writable = props.traits.editable && !props.readOnly;
484
+ SendMessage(entry.control, EM_SETREADONLY, writable ? FALSE : TRUE, 0);
485
+
486
+ // secureTextEntry is a message here rather than the different *class* AppKit
487
+ // needs, so it can be turned on and off without rebuilding the control and
488
+ // without losing focus -- the one place this platform has the easier job.
489
+ if (props.traits.secureTextEntry != entry.secure) {
490
+ entry.secure = props.traits.secureTextEntry;
491
+ SendMessage(entry.control, EM_SETPASSWORDCHAR, entry.secure ? kPasswordCharacter : 0, 0);
492
+ InvalidateRect(entry.control, nullptr, TRUE);
493
+ }
494
+
495
+ // 0 means no limit, and React Native spells "no limit" as an absent prop --
496
+ // which arrives here as 0 too, so the two agree without a special case.
497
+ SendMessage(entry.control, EM_SETLIMITTEXT, static_cast<WPARAM>(std::max(0, props.maxLength)), 0);
498
+
499
+ // `clearTextOnFocus` and `selectTextOnFocus`, remembered for the moment focus
500
+ // arrives. See the EN_SETFOCUS case.
501
+ entry.clearTextOnFocus = props.traits.clearTextOnFocus;
502
+ entry.selectTextOnFocus = props.traits.selectTextOnFocus;
503
+
504
+ // A caret colour that arrives while the field already has focus has missed
505
+ // the WM_SETFOCUS it would have been installed from, and a controlled field
506
+ // is re-rendered constantly -- so the shape is replaced now. Nothing happens
507
+ // for a field that is not focused: it has no caret to replace.
508
+ if ((entry.hasCaretColour || entry.caretHidden) && GetFocus() == entry.control) {
509
+ installCaret(entry);
510
+ }
511
+
512
+ applyTextChecking(entry, props);
513
+ }
514
+
515
+ // `spellCheck`, `autoCorrect`, `autoCapitalize` and `keyboardType`: what a
516
+ // field asked for about its text, of which this platform can honour one and a
517
+ // half.
518
+ //
519
+ // **`keyboardType` becomes an input scope**, which is the Windows equivalent of
520
+ // GTK's input purpose: `SetInputScope` tells the touch keyboard and any
521
+ // text-services IME what kind of text is expected, so a Surface typing into an
522
+ // `email-address` field gets the keyboard with the @ on it. AppKit has nothing
523
+ // of the kind and reports the prop instead. The table is `Win32InputScopes.h`,
524
+ // keyed on the name core prints so that it carries no React Native.
525
+ //
526
+ // **`autoCapitalize` becomes `ES_UPPERCASE`, and only for `characters`.** That
527
+ // style forces every character typed or pasted into upper case, which is
528
+ // exactly what iOS's `characters` does; `words` and `sentences` need to know
529
+ // where a word or a sentence begins, which an `EDIT` does not and no style bit
530
+ // offers. Those two are reported and not approximated: upper-casing everything
531
+ // for `sentences` would be worse than leaving the text alone.
532
+ //
533
+ // **`spellCheck` and `autoCorrect` have nothing here at all.** A classic `EDIT`
534
+ // has no spell checker and no autocorrection; the Windows spell-checking API is
535
+ // `ISpellChecker`, which checks strings and draws nothing, so honouring either
536
+ // prop would mean drawing the squiggles and offering the menu -- a text editor
537
+ // rather than a prop. backlog/platform-windows.md records that, and both props
538
+ // reach the tree dump so an app can see they arrived.
539
+ void Win32TextInputManager::applyTextChecking(Entry &entry, const TextInputProps &props) {
540
+ // The dump first, because it happens whether or not there is a control: the
541
+ // four words are what the three hosts compare, and `core/TextChecking.h`
542
+ // decides that an unset spelling prop is a third state rather than off.
543
+ const auto spellCheck = basalt::textCheckingFlag(props.traits.spellCheck);
544
+ const auto autoCorrect = basalt::textCheckingFlag(props.traits.autoCorrect);
545
+ if (entry.view != nullptr) {
546
+ entry.view->setTextChecking(basalt::textCheckingName(spellCheck),
547
+ basalt::textCheckingName(autoCorrect));
548
+ entry.view->setInputHiding(props.traits.caretHidden, props.traits.contextMenuHidden);
549
+ entry.view->setInputKinds(basalt::autoCapitalizeName(props.traits.autocapitalizationType),
550
+ basalt::keyboardTypeName(props.traits.keyboardType));
551
+ }
552
+
553
+ if (entry.control == nullptr) {
554
+ return;
555
+ }
556
+
557
+ // The input scope, set only when it changes: `SetInputScope` replaces the
558
+ // window's whole scope list, and a controlled field re-sending identical
559
+ // props would otherwise reinstall it on every keystroke.
560
+ const InputScope scope =
561
+ win32::inputScopeForKeyboardType(basalt::keyboardTypeName(props.traits.keyboardType));
562
+ if (!entry.sawInputScope || scope != entry.inputScope) {
563
+ entry.sawInputScope = true;
564
+ entry.inputScope = scope;
565
+ win32::applyInputScope(entry.control, scope);
566
+ }
567
+
568
+ // And the one style bit, applied the way the alignment above is.
569
+ const bool upperCase =
570
+ props.traits.autocapitalizationType == facebook::react::AutocapitalizationType::Characters;
571
+ const LONG_PTR style = GetWindowLongPtr(entry.control, GWL_STYLE);
572
+ const LONG_PTR wantedStyle =
573
+ upperCase ? (style | ES_UPPERCASE) : (style & ~static_cast<LONG_PTR>(ES_UPPERCASE));
574
+ if (wantedStyle != style) {
575
+ SetWindowLongPtr(entry.control, GWL_STYLE, wantedStyle);
576
+ InvalidateRect(entry.control, nullptr, TRUE);
577
+ }
578
+ }
579
+
580
+ void Win32TextInputManager::destroyPeer(Entry &entry) {
581
+ // Deliberately does not clear the view's back pointer, and must not: by the
582
+ // time this runs the view is already gone. MountingWalk's Delete calls
583
+ // `destroyView` *before* `forgetTag`, and `releaseAllViews` runs before this
584
+ // object's own destructor -- so both paths free the view first and touching
585
+ // it here is a use-after-free. It was one, until the end-to-end suite caught
586
+ // the process exiting 0xC0000005.
587
+ //
588
+ // Nothing needs clearing anyway: the view never outlives its peer. The only
589
+ // moment a live view holds a dead HWND is between the host window being
590
+ // destroyed and the views being freed, and reading a dead HWND's text
591
+ // reports nothing rather than crashing -- which is why `captureBeforeTeardown`
592
+ // takes the dump before the window goes.
593
+ entry.view = nullptr;
594
+ if (entry.control != nullptr) {
595
+ RemoveWindowSubclass(entry.control, editProc, kSubclassId);
596
+ DestroyWindow(entry.control);
597
+ entry.control = nullptr;
598
+ }
599
+ if (entry.font != nullptr) {
600
+ DeleteObject(entry.font);
601
+ entry.font = nullptr;
602
+ }
603
+ if (entry.backgroundBrush != nullptr) {
604
+ DeleteObject(entry.backgroundBrush);
605
+ entry.backgroundBrush = nullptr;
606
+ }
607
+ // After the window, so the caret it may have owned is gone: the control
608
+ // destroys its caret while handling WM_KILLFOCUS, which DestroyWindow
609
+ // provokes, and a bitmap must outlive the caret it is the shape of.
610
+ releaseCaret(entry);
611
+ }
612
+
613
+ void Win32TextInputManager::remove(Tag tag) {
614
+ const auto it = entries_.find(tag);
615
+ if (it == entries_.end()) {
616
+ return;
617
+ }
618
+ // The subclass procedure holds a pointer to the Entry, so the window has to
619
+ // go before the map entry does.
620
+ destroyPeer(it->second);
621
+ entries_.erase(it);
622
+ }
623
+
624
+ // ---------------------------------------------------------------------------
625
+ // Placement
626
+ // ---------------------------------------------------------------------------
627
+
628
+ void Win32TextInputManager::syncBounds(RnWin32View *root) {
629
+ for (auto &[tag, entry] : entries_) {
630
+ if (entry.control == nullptr) {
631
+ continue;
632
+ }
633
+ RECT rect{};
634
+ if (!peerRect(entry.view, root, rect)) {
635
+ ShowWindow(entry.control, SW_HIDE);
636
+ continue;
637
+ }
638
+
639
+ // Inside the content insets, so that `paddingHorizontal` on a field means
640
+ // what it means on a <View>. GTK allocates its GtkText inside the same
641
+ // numbers; here there is no allocation, so the window is placed there.
642
+ rect.left += entry.insets.left;
643
+ rect.top += entry.insets.top;
644
+ rect.right -= entry.insets.right;
645
+ rect.bottom -= entry.insets.bottom;
646
+
647
+ // And no taller than one line, centred in what is left. A single-line EDIT
648
+ // draws its text at the top of its client area -- Windows' own fields are
649
+ // sized to their font, so it never shows -- and in a 44-point field with
650
+ // 16-point text that puts the caret near the top and looks broken. Making
651
+ // the control a strip the height of one line and centring it is what every
652
+ // Win32 application does about this.
653
+ //
654
+ // It also keeps the control away from the corners. The RnWin32View behind
655
+ // it paints the rounded background; a full-height rectangular child window
656
+ // would paint square corners straight over them, and a strip at the middle
657
+ // never reaches the part a radius cuts away.
658
+ //
659
+ // A multiline field is the exception and fills the box: its text starts at
660
+ // the top, which is where a paragraph belongs, and a strip one line tall
661
+ // would hide every line after the first. The corners are the cost -- a
662
+ // full-height child window paints square over a rounded background -- and
663
+ // that is recorded rather than worked around, because the alternative is
664
+ // drawing the field ourselves.
665
+ const LONG available = rect.bottom - rect.top;
666
+ if (!entry.multiline && entry.lineHeight > 0 && entry.lineHeight < available) {
667
+ rect.top += (available - entry.lineHeight) / 2;
668
+ rect.bottom = rect.top + entry.lineHeight;
669
+ }
670
+
671
+ if (rect.right <= rect.left || rect.bottom <= rect.top) {
672
+ ShowWindow(entry.control, SW_HIDE);
673
+ continue;
674
+ }
675
+
676
+ SetWindowPos(entry.control,
677
+ nullptr,
678
+ rect.left,
679
+ rect.top,
680
+ rect.right - rect.left,
681
+ rect.bottom - rect.top,
682
+ SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
683
+ }
684
+ }
685
+
686
+ // ---------------------------------------------------------------------------
687
+ // Events
688
+ // ---------------------------------------------------------------------------
689
+
690
+ Win32TextInputManager::Entry *Win32TextInputManager::entryForControl(HWND control) {
691
+ if (control == nullptr) {
692
+ return nullptr;
693
+ }
694
+ for (auto &[tag, entry] : entries_) {
695
+ if (entry.control == control) {
696
+ return &entry;
697
+ }
698
+ }
699
+ return nullptr;
700
+ }
701
+
702
+ bool Win32TextInputManager::ownsControl(HWND control) const {
703
+ for (const auto &[tag, entry] : entries_) {
704
+ if (entry.control == control) {
705
+ return true;
706
+ }
707
+ }
708
+ return false;
709
+ }
710
+
711
+ bool Win32TextInputManager::focusAt(RnWin32View *root, double x, double y) {
712
+ if (root == nullptr || entries_.empty()) {
713
+ return false;
714
+ }
715
+ RnWin32View *hit = win32::hitTest(root, static_cast<float>(x), static_cast<float>(y));
716
+ for (RnWin32View *view = hit; view != nullptr; view = view->parent()) {
717
+ const auto it = entries_.find(static_cast<Tag>(view->tag()));
718
+ if (it != entries_.end() && it->second.control != nullptr) {
719
+ SetFocus(it->second.control);
720
+ return true;
721
+ }
722
+ }
723
+ return false;
724
+ }
725
+
726
+ bool Win32TextInputManager::handleControlCommand(WPARAM wparam, LPARAM lparam) {
727
+ Entry *entry = entryForControl(reinterpret_cast<HWND>(lparam));
728
+ if (entry == nullptr) {
729
+ return false;
730
+ }
731
+
732
+ switch (HIWORD(wparam)) {
733
+ case EN_CHANGE:
734
+ reportChange(*entry);
735
+ return true;
736
+
737
+ case EN_SETFOCUS:
738
+ if (const auto emitter = emitterFor(entry->tag)) {
739
+ emitter->onFocus(metricsFor(*entry));
740
+ }
741
+ // `clearTextOnFocus` before `selectTextOnFocus`, which is the only order
742
+ // that means anything: selecting what has just been cleared selects
743
+ // nothing, and clearing what has just been selected throws the selection
744
+ // away. All three hosts do them in this order.
745
+ if (entry->clearTextOnFocus && GetWindowTextLength(entry->control) > 0) {
746
+ // Reported rather than applied silently, through the same path a
747
+ // keystroke takes: a controlled field cleared behind JavaScript's back
748
+ // is a field that puts the old value back on the next unrelated render
749
+ // with the user having done nothing.
750
+ //
751
+ // `SetWindowText` sends its own EN_CHANGE, which arrives here and
752
+ // reports it; the call after is belt to that brace and costs nothing,
753
+ // `reportChange` comparing against what was last reported.
754
+ SetWindowText(entry->control, L"");
755
+ reportChange(*entry);
756
+ }
757
+ if (entry->selectTextOnFocus) {
758
+ // Everything, which is what `EM_SETSEL` with -1 as its end means. A
759
+ // bare `SetFocus` on an EDIT selects nothing -- it is not
760
+ // dialog-managed, which is what made `autoFocus` need no fixing here --
761
+ // so this is the whole of the prop on this host.
762
+ SendMessage(entry->control, EM_SETSEL, 0, -1);
763
+ }
764
+ return true;
765
+
766
+ case EN_KILLFOCUS:
767
+ if (const auto emitter = emitterFor(entry->tag)) {
768
+ emitter->onBlur(metricsFor(*entry));
769
+ emitter->onEndEditing(metricsFor(*entry));
770
+ }
771
+ return true;
772
+
773
+ default:
774
+ break;
775
+ }
776
+ // A notification from one of this manager's controls that it does not act on
777
+ // -- EN_UPDATE, EN_MAXTEXT. Still handled, in the sense that the host must
778
+ // not treat it as a menu command.
779
+ return true;
780
+ }
781
+
782
+ HBRUSH Win32TextInputManager::controlColor(HDC deviceContext, HWND control) {
783
+ Entry *entry = entryForControl(control);
784
+ if (entry == nullptr) {
785
+ return nullptr;
786
+ }
787
+ SetTextColor(deviceContext, entry->textColor);
788
+ if (entry->hasBackground && entry->backgroundBrush != nullptr) {
789
+ SetBkColor(deviceContext, entry->backgroundColor);
790
+ return entry->backgroundBrush;
791
+ }
792
+ // No background prop. The control cannot see through itself to what Direct2D
793
+ // painted behind it, so the window's own ground is the closest honest answer;
794
+ // a field with no backgroundColor over a coloured parent is the case this
795
+ // gets visibly wrong, and the fix is a background prop.
796
+ SetBkColor(deviceContext, GetSysColor(COLOR_WINDOW));
797
+ return GetSysColorBrush(COLOR_WINDOW);
798
+ }
799
+
800
+ // ---------------------------------------------------------------------------
801
+ // The three colours
802
+ //
803
+ // React Native gives a field a `placeholderTextColor`, a `selectionColor` and a
804
+ // `cursorColor`. A classic EDIT has a property for none of them: the cue banner
805
+ // is drawn in the system's grey, the selection in `COLOR_HIGHLIGHT`, and the
806
+ // caret is a shape rather than a colour. Two of the three are reachable anyway,
807
+ // and the third is written down in backlog/textinput.md with the route that
808
+ // would reach it.
809
+ // ---------------------------------------------------------------------------
810
+
811
+ void Win32TextInputManager::drawPlaceholder(const Entry &entry, HDC deviceContext) const {
812
+ if (entry.control == nullptr || deviceContext == nullptr || entry.placeholder.empty()) {
813
+ return;
814
+ }
815
+ // Only while the field is empty, which is the whole of what a placeholder is.
816
+ if (GetWindowTextLength(entry.control) > 0) {
817
+ return;
818
+ }
819
+
820
+ // The formatting rectangle rather than the client area: an EDIT insets its
821
+ // text by a margin of its own, and a placeholder that ignored it would not
822
+ // start where the text it stands in for starts. `EM_GETRECT` answers for a
823
+ // single-line control too -- it is `EM_SETRECT` that is documented
824
+ // multiline-only and silently does nothing here, which `insets` exists
825
+ // because of.
826
+ RECT box{};
827
+ SendMessage(entry.control, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&box));
828
+ if (IsRectEmpty(&box)) {
829
+ GetClientRect(entry.control, &box);
830
+ }
831
+
832
+ const int saved = SaveDC(deviceContext);
833
+ if (entry.font != nullptr) {
834
+ // The control's own font, so a placeholder in a 20pt field is not 13pt --
835
+ // the same rule the AppKit host spells by styling its placeholder like its
836
+ // text.
837
+ SelectObject(deviceContext, entry.font);
838
+ }
839
+ SetTextColor(deviceContext, entry.placeholderColour);
840
+ // Transparent, so what the control painted stays: the background is the app's
841
+ // own `backgroundColor`, answered from WM_CTLCOLOREDIT, and an opaque text
842
+ // background here would paint a box of the system's colour over it.
843
+ SetBkMode(deviceContext, TRANSPARENT);
844
+ const UINT format = entry.multiline
845
+ ? static_cast<UINT>(DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX)
846
+ : static_cast<UINT>(DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
847
+ DrawTextW(deviceContext,
848
+ entry.placeholder.c_str(),
849
+ static_cast<int>(entry.placeholder.size()),
850
+ &box,
851
+ format);
852
+ if (saved != 0) {
853
+ RestoreDC(deviceContext, saved);
854
+ }
855
+ }
856
+
857
+ void Win32TextInputManager::installCaret(Entry &entry) {
858
+ if (entry.control == nullptr || (!entry.hasCaretColour && !entry.caretHidden)) {
859
+ return;
860
+ }
861
+ releaseCaret(entry);
862
+
863
+ // **The caret is XOR-ed onto the field.** `CreateCaret` says so -- "the caret
864
+ // is drawn to the screen via the XOR operation" -- which is how a caret with
865
+ // no colour at all is visible against any background, and why one with a
866
+ // colour cannot simply be asked for.
867
+ //
868
+ // So the bitmap carries the asked-for colour XOR the ground it will be drawn
869
+ // against, and the system's XOR cancels the ground back out. The ground is
870
+ // known: it is the `backgroundColor` the control is painted with, or the
871
+ // window's own when the field asked for none, which is the same pair
872
+ // `controlColor` answers WM_CTLCOLOREDIT with.
873
+ //
874
+ // Where the caret crosses a glyph the XOR gives something else, exactly as
875
+ // the system's own caret does over text. That is the shape of this platform's
876
+ // caret rather than a shortcut taken here.
877
+ const COLORREF ground =
878
+ entry.hasBackground ? entry.backgroundColor : GetSysColor(COLOR_WINDOW);
879
+ //
880
+ // Zero for `caretHidden`, which is less a special case than the arithmetic's
881
+ // own answer: a bitmap of zeroes XOR-ed onto the field changes nothing, so
882
+ // the caret is installed and invisible. Which is also why hiding one by
883
+ // giving it the background's colour would not work over the text it crosses
884
+ // -- the XOR is per pixel, not per field.
885
+ const COLORREF bits = entry.caretHidden ? 0 : (entry.caretColour ^ ground);
886
+
887
+ DWORD width = 0;
888
+ if (SystemParametersInfo(SPI_GETCARETWIDTH, 0, &width, 0) == FALSE || width == 0) {
889
+ width = 1;
890
+ }
891
+ const LONG height = entry.lineHeight > 0 ? entry.lineHeight : 16;
892
+
893
+ // 32 bits per pixel through an explicit DIB header rather than `CreateBitmap`
894
+ // with a device-dependent buffer: the whole point is a known colour, and
895
+ // "device-dependent format" is not one. `CreateCaret` accepts a
896
+ // `CreateDIBitmap` bitmap by name.
897
+ const std::uint32_t pixel = static_cast<std::uint32_t>(GetBValue(bits))
898
+ | (static_cast<std::uint32_t>(GetGValue(bits)) << 8)
899
+ | (static_cast<std::uint32_t>(GetRValue(bits)) << 16);
900
+ std::vector<std::uint32_t> pixels(static_cast<size_t>(width) * static_cast<size_t>(height),
901
+ pixel);
902
+
903
+ BITMAPINFO info{};
904
+ info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
905
+ info.bmiHeader.biWidth = static_cast<LONG>(width);
906
+ // Negative for top-down, which costs nothing to be explicit about even though
907
+ // every row of this bitmap is the same.
908
+ info.bmiHeader.biHeight = -height;
909
+ info.bmiHeader.biPlanes = 1;
910
+ info.bmiHeader.biBitCount = 32;
911
+ info.bmiHeader.biCompression = BI_RGB;
912
+
913
+ const HDC screen = GetDC(nullptr);
914
+ if (screen == nullptr) {
915
+ return;
916
+ }
917
+ entry.caret = CreateDIBitmap(
918
+ screen, &info.bmiHeader, CBM_INIT, pixels.data(), &info, DIB_RGB_COLORS);
919
+ ReleaseDC(nullptr, screen);
920
+ if (entry.caret == nullptr) {
921
+ return;
922
+ }
923
+
924
+ // `CreateCaret` destroys whatever shape came before, which is the EDIT's own:
925
+ // it creates a solid caret while handling WM_SETFOCUS, so this has to run
926
+ // after that and does. The width and height are ignored for a bitmap, which
927
+ // defines its own.
928
+ if (CreateCaret(entry.control, entry.caret, 0, 0) != FALSE) {
929
+ ShowCaret(entry.control);
930
+ }
931
+ }
932
+
933
+ void Win32TextInputManager::releaseCaret(Entry &entry) {
934
+ if (entry.caret == nullptr) {
935
+ return;
936
+ }
937
+ // No `DestroyCaret` here, deliberately. There is one caret per queue, so
938
+ // destroying it from a field that has just lost focus would destroy the one
939
+ // whatever took the focus has already created. The EDIT destroys its own
940
+ // caret while handling WM_KILLFOCUS, and this runs after that; the only other
941
+ // caller is `installCaret`, where `CreateCaret` replaces the shape itself.
942
+ DeleteObject(entry.caret);
943
+ entry.caret = nullptr;
944
+ }
945
+
946
+ HBITMAP Win32TextInputManager::caretBitmapFor(Tag tag) const {
947
+ const auto it = entries_.find(tag);
948
+ return it == entries_.end() ? nullptr : it->second.caret;
949
+ }
950
+
951
+ void Win32TextInputManager::moveFocusToHost() {
952
+ if (host_ != nullptr) {
953
+ SetFocus(host_);
954
+ }
955
+ }
956
+
957
+ void Win32TextInputManager::reportChange(Entry &entry) {
958
+ if (entry.applying) {
959
+ // A prop being applied, not the user typing.
960
+ return;
961
+ }
962
+ const auto metrics = metricsFor(entry);
963
+ if (metrics.text == entry.lastReportedText) {
964
+ return;
965
+ }
966
+ entry.lastReportedText = metrics.text;
967
+ entry.eventCount++;
968
+
969
+ if (const auto emitter = emitterFor(entry.tag)) {
970
+ // Rebuilt so it carries the incremented count: React Native drops a prop
971
+ // update whose eventCount is older than the last one it was told about, so
972
+ // an event that under-reports its own count makes the field ignore the
973
+ // value coming back down.
974
+ emitter->onChange(metricsFor(entry));
975
+ }
976
+ }
977
+
978
+ std::shared_ptr<const TextInputEventEmitter> Win32TextInputManager::emitterFor(Tag tag) const {
979
+ return std::dynamic_pointer_cast<const TextInputEventEmitter>(lookup_(tag));
980
+ }
981
+
982
+ TextInputEventEmitter::Metrics Win32TextInputManager::metricsFor(const Entry &entry) const {
983
+ TextInputEventEmitter::Metrics metrics{};
984
+ metrics.eventCount = entry.eventCount;
985
+ metrics.target = entry.tag;
986
+ metrics.zoomScale = 1.0F;
987
+
988
+ if (entry.control != nullptr) {
989
+ const int length = GetWindowTextLength(entry.control);
990
+ std::wstring text(static_cast<size_t>(length) + 1, L'\0');
991
+ GetWindowText(entry.control, text.data(), length + 1);
992
+ text.resize(static_cast<size_t>(length));
993
+ metrics.text = narrow(text);
994
+
995
+ DWORD start = 0;
996
+ DWORD end = 0;
997
+ SendMessage(entry.control,
998
+ EM_GETSEL,
999
+ reinterpret_cast<WPARAM>(&start),
1000
+ reinterpret_cast<LPARAM>(&end));
1001
+ metrics.selectionRange =
1002
+ AttributedString::Range{static_cast<int>(start), static_cast<int>(end - start)};
1003
+ }
1004
+
1005
+ // The scroll-shaped fields exist because iOS's text view is a scroll view.
1006
+ // Nothing here scrolls yet, so they describe a viewport the size of the
1007
+ // field, which is true and keeps JavaScript's arithmetic sane.
1008
+ const auto width = static_cast<facebook::react::Float>(
1009
+ entry.view != nullptr ? entry.view->frame().width : 0.0F);
1010
+ const auto height = static_cast<facebook::react::Float>(
1011
+ entry.view != nullptr ? entry.view->frame().height : 0.0F);
1012
+ metrics.containerSize = {.width = width, .height = height};
1013
+ metrics.contentSize = metrics.containerSize;
1014
+ metrics.layoutMeasurement = metrics.containerSize;
1015
+
1016
+ return metrics;
1017
+ }
1018
+
1019
+ // ---------------------------------------------------------------------------
1020
+ // The subclass
1021
+ // ---------------------------------------------------------------------------
1022
+
1023
+ void Win32TextInputManager::reportSelectionIfChanged(Entry &entry) {
1024
+ if (entry.control == nullptr) {
1025
+ return;
1026
+ }
1027
+ // Not while a prop is being pushed in. Applying `text` moves the caret, and
1028
+ // reporting that as the person selecting something would make a controlled
1029
+ // field fight its own render -- the same guard both other hosts have.
1030
+ if (entry.applying) {
1031
+ return;
1032
+ }
1033
+
1034
+ // The caret first, on its own. `metricsFor` copies the field's whole text,
1035
+ // and this runs after every mouse move -- so the cheap question is asked
1036
+ // first and the expensive one only when the answer has changed.
1037
+ DWORD start = 0;
1038
+ DWORD end = 0;
1039
+ SendMessage(entry.control,
1040
+ EM_GETSEL,
1041
+ reinterpret_cast<WPARAM>(&start),
1042
+ reinterpret_cast<LPARAM>(&end));
1043
+ const auto location = static_cast<int>(start);
1044
+ const auto length = static_cast<int>(end - start);
1045
+ if (location == entry.lastReportedSelection.location &&
1046
+ length == entry.lastReportedSelection.length) {
1047
+ return;
1048
+ }
1049
+ entry.lastReportedSelection = AttributedString::Range{location, length};
1050
+
1051
+ if (const auto emitter = emitterFor(entry.tag)) {
1052
+ emitter->onSelectionChange(metricsFor(entry));
1053
+ }
1054
+ }
1055
+
1056
+ LRESULT CALLBACK Win32TextInputManager::editProc(
1057
+ HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR id, DWORD_PTR data) {
1058
+ auto *entry = reinterpret_cast<Entry *>(data);
1059
+
1060
+ if (message == WM_CHAR && entry != nullptr && entry->owner != nullptr) {
1061
+ // React Native's contract, the same one both other hosts follow: 'Enter'
1062
+ // and 'Backspace' by name, the typed character otherwise -- including ' '
1063
+ // for space -- and nothing at all for a key that produces no character.
1064
+ // WM_CHAR is already the character, so Escape and Tab fall out as
1065
+ // unprintable rather than needing to be named.
1066
+ const auto typed = static_cast<wchar_t>(wparam);
1067
+ std::string key;
1068
+ if (typed == L'\r' || typed == L'\n') {
1069
+ key = "Enter";
1070
+ } else if (typed == L'\b') {
1071
+ key = "Backspace";
1072
+ } else if (std::iswprint(static_cast<wint_t>(typed))) {
1073
+ key = narrow(std::wstring(1, typed));
1074
+ }
1075
+ if (!key.empty()) {
1076
+ if (const auto emitter = entry->owner->emitterFor(entry->tag)) {
1077
+ TextInputEventEmitter::KeyPressMetrics metrics{};
1078
+ metrics.text = key;
1079
+ metrics.eventCount = entry->eventCount;
1080
+ emitter->onKeyPress(metrics);
1081
+ }
1082
+ }
1083
+
1084
+ // A single-line EDIT has nowhere to put a newline, so it beeps at one. That
1085
+ // beep is the sound of an unhandled Enter, and Enter is what React Native
1086
+ // calls submitEditing -- so it is caught here and swallowed. Escape and Tab
1087
+ // beep for the same reason: neither has a meaning inside a field, and
1088
+ // nothing on this platform implements a tab order for the second to reach.
1089
+ const auto character = typed;
1090
+ if (character == L'\r' || character == L'\n') {
1091
+ // **`submitBehavior` decides what Enter does**, already resolved to one of
1092
+ // the three: `newline` inserts one, `submit` reports a submit, and
1093
+ // `blurAndSubmit` reports one and gives up focus. React Native's default
1094
+ // is the last for a single-line field and the first for a multiline one,
1095
+ // so a field that never set the prop behaves as it does on iOS.
1096
+ //
1097
+ // The key press was already reported above, so an app watching
1098
+ // `onKeyPress` sees the Enter whichever of the three it is.
1099
+ const bool newline =
1100
+ entry->submitBehavior == facebook::react::SubmitBehavior::Newline;
1101
+ if (!newline) {
1102
+ if (const auto emitter = entry->owner->emitterFor(entry->tag)) {
1103
+ emitter->onSubmitEditing(entry->owner->metricsFor(*entry));
1104
+ }
1105
+ if (entry->submitBehavior == facebook::react::SubmitBehavior::BlurAndSubmit) {
1106
+ // The same move the `blur` command makes, and for the same reason it
1107
+ // gives: Win32 has no "nothing is focused" inside an active window,
1108
+ // and taking focus away from the process is not what blur means. The
1109
+ // control still sees EN_KILLFOCUS, so JavaScript sees onBlur.
1110
+ entry->owner->moveFocusToHost();
1111
+ }
1112
+ return 0;
1113
+ }
1114
+ // `newline` in a single-line field is a newline there is nowhere to put.
1115
+ // Swallowed rather than passed on, which is the difference between doing
1116
+ // nothing and beeping.
1117
+ if (!entry->multiline) {
1118
+ return 0;
1119
+ }
1120
+ }
1121
+ if (character == L'\x1b' || character == L'\t') {
1122
+ return 0;
1123
+ }
1124
+ }
1125
+
1126
+ // `contextMenuHidden`: swallowed rather than emptied, and before the control
1127
+ // sees it. WM_CONTEXTMENU is the one message to catch -- a right-click, the
1128
+ // Menu key and Shift+F10 all arrive as it -- so unlike the other two hosts
1129
+ // this covers the keyboard route as well as the pointer.
1130
+ if (message == WM_CONTEXTMENU && entry != nullptr && entry->contextMenuHidden) {
1131
+ return 0;
1132
+ }
1133
+
1134
+ if (message == WM_NCDESTROY) {
1135
+ RemoveWindowSubclass(hwnd, editProc, id);
1136
+ }
1137
+
1138
+ const LRESULT result = DefSubclassProc(hwnd, message, wparam, lparam);
1139
+
1140
+ // Windows does not report the caret moving: EN_SELCHANGE is RichEdit's, and a
1141
+ // plain EDIT says nothing. So the selection is read after anything that could
1142
+ // have moved it -- which is why this runs *after* DefSubclassProc, once the
1143
+ // control has done the moving.
1144
+ if (entry != nullptr && entry->owner != nullptr) {
1145
+ switch (message) {
1146
+ // Its own case rather than part of the list below: `wparam` here is a
1147
+ // button mask, and everywhere else in this switch it is something else
1148
+ // entirely -- a character for WM_CHAR, a virtual key for WM_KEYDOWN. A
1149
+ // fallthrough would test a character against MK_LBUTTON, which passes for
1150
+ // odd code points and fails for even ones.
1151
+ case WM_MOUSEMOVE:
1152
+ // A hover cannot move the caret, and this runs on every mouse move over
1153
+ // the field.
1154
+ if ((wparam & MK_LBUTTON) != 0) {
1155
+ entry->owner->reportSelectionIfChanged(*entry);
1156
+ }
1157
+ break;
1158
+ case WM_CHAR:
1159
+ case WM_KEYDOWN:
1160
+ case WM_KEYUP:
1161
+ case WM_LBUTTONDOWN:
1162
+ case WM_LBUTTONUP:
1163
+ case WM_SETFOCUS:
1164
+ case WM_CUT:
1165
+ case WM_PASTE:
1166
+ case WM_CLEAR:
1167
+ case WM_UNDO:
1168
+ case EM_SETSEL:
1169
+ entry->owner->reportSelectionIfChanged(*entry);
1170
+ break;
1171
+ default:
1172
+ break;
1173
+ }
1174
+
1175
+ // The placeholder and the caret, both after the control has had the
1176
+ // message: the placeholder goes on top of what the EDIT painted, and the
1177
+ // caret replaces the one the EDIT creates for itself on focus.
1178
+ switch (message) {
1179
+ case WM_PAINT:
1180
+ if (entry->hasPlaceholderColour) {
1181
+ // Its own DC rather than BeginPaint: the control's WM_PAINT has
1182
+ // finished and validated the update region, so there is nothing left
1183
+ // to begin.
1184
+ if (const HDC painted = GetDC(hwnd)) {
1185
+ entry->owner->drawPlaceholder(*entry, painted);
1186
+ ReleaseDC(hwnd, painted);
1187
+ }
1188
+ }
1189
+ break;
1190
+
1191
+ // The same drawing into a device context somebody else owns, which is
1192
+ // how a control is rendered anywhere but the screen. It is also the only
1193
+ // way anything can read the pixels of a field on this host: the Direct2D
1194
+ // snapshot cannot see a child window at all, which is its own backlog
1195
+ // entry.
1196
+ case WM_PRINTCLIENT:
1197
+ if (entry->hasPlaceholderColour) {
1198
+ entry->owner->drawPlaceholder(*entry, reinterpret_cast<HDC>(wparam));
1199
+ }
1200
+ break;
1201
+
1202
+ case WM_SETFOCUS:
1203
+ entry->owner->installCaret(*entry);
1204
+ break;
1205
+
1206
+ case WM_KILLFOCUS:
1207
+ entry->owner->releaseCaret(*entry);
1208
+ break;
1209
+
1210
+ default:
1211
+ break;
1212
+ }
1213
+ }
1214
+
1215
+ return result;
1216
+ }
1217
+
1218
+ // ---------------------------------------------------------------------------
1219
+ // Commands
1220
+ // ---------------------------------------------------------------------------
1221
+
1222
+ bool Win32TextInputManager::dispatchCommand(Tag tag,
1223
+ const std::string &name,
1224
+ const folly::dynamic &args) {
1225
+ const auto it = entries_.find(tag);
1226
+ if (it == entries_.end()) {
1227
+ return false;
1228
+ }
1229
+ Entry &entry = it->second;
1230
+
1231
+ if (name == "focus") {
1232
+ if (entry.control != nullptr) {
1233
+ SetFocus(entry.control);
1234
+ }
1235
+ return true;
1236
+ }
1237
+
1238
+ if (name == "blur") {
1239
+ // Focus moves to the host window rather than being dropped: Win32 has no
1240
+ // "nothing has focus" within an active window, and SetFocus(nullptr) takes
1241
+ // keyboard input away from the process entirely, which is not what blur
1242
+ // means. The control still sees EN_KILLFOCUS, so JavaScript sees onBlur.
1243
+ if (entry.control != nullptr && GetFocus() == entry.control) {
1244
+ moveFocusToHost();
1245
+ }
1246
+ return true;
1247
+ }
1248
+
1249
+ if (name == "setTextAndSelection") {
1250
+ // [eventCount, text, start, end]. An eventCount older than what the user
1251
+ // has since typed means this command is stale and must be dropped, which is
1252
+ // the whole reason React Native counts them.
1253
+ if (args.isArray() && args.size() >= 2 && entry.control != nullptr) {
1254
+ const int eventCount = static_cast<int>(args[0].asInt());
1255
+ if (eventCount < entry.eventCount) {
1256
+ return true;
1257
+ }
1258
+ entry.applying = true;
1259
+ const auto text = args[1].isString() ? args[1].asString() : std::string{};
1260
+ const std::wstring wide = widen(text);
1261
+ SetWindowText(entry.control, wide.c_str());
1262
+ if (args.size() >= 4 && args[2].isInt() && args[3].isInt()) {
1263
+ SendMessage(entry.control,
1264
+ EM_SETSEL,
1265
+ static_cast<WPARAM>(args[2].asInt()),
1266
+ static_cast<LPARAM>(args[3].asInt()));
1267
+ }
1268
+ entry.applying = false;
1269
+ entry.lastReportedText = text;
1270
+ }
1271
+ return true;
1272
+ }
1273
+
1274
+ return false;
1275
+ }
1276
+
1277
+ // ---------------------------------------------------------------------------
1278
+ // Scripted typing
1279
+ // ---------------------------------------------------------------------------
1280
+
1281
+ bool Win32TextInputManager::typeIntoFocused(const std::string &text) {
1282
+ HWND focused = GetFocus();
1283
+ if (!ownsControl(focused)) {
1284
+ return false;
1285
+ }
1286
+ // WM_CHAR per UTF-16 code unit, which is what a keyboard driver would send
1287
+ // and what an IME sends for a composed character. So this skips the driver
1288
+ // and nothing above it -- the EDIT's own handling, EN_CHANGE, the emitter,
1289
+ // the event beat and React all run exactly as they would.
1290
+ for (const wchar_t unit : widen(text)) {
1291
+ SendMessage(focused, WM_CHAR, static_cast<WPARAM>(unit), 1);
1292
+ }
1293
+ return true;
1294
+ }
1295
+
1296
+ } // namespace basalt