basalt-win32 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,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