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,232 @@
1
+ // <TextInput> on Win32.
2
+ //
3
+ // The native half is React Native's own *iOS* TextInput: its shadow node,
4
+ // props, state and event emitter are pure C++ and measure through a
5
+ // TextLayoutManager, which on this platform is the DirectWrite one. Android's
6
+ // variant includes fbjni and calls into a Java FabricUIManager, so it is
7
+ // unusable here; see docs/DECISIONS.md. The component name is therefore
8
+ // "TextInput", which is what packages/basalt-core's
9
+ // src/overrides/TextInput.js asks for -- the same file both other desktops use,
10
+ // because that override was never about Linux.
11
+ //
12
+ // The editing itself is a real EDIT control, not a caret drawn on a DirectWrite
13
+ // layout. That brings the IME, selection, the clipboard, undo, double-click
14
+ // word selection, Ctrl+arrow, and every other binding a Windows user expects --
15
+ // none of it worth reimplementing and all of it easy to get subtly wrong. GTK
16
+ // embeds a GtkText and AppKit an NSTextField for exactly the same reason.
17
+ //
18
+ // ## The peer is a window, and that is the whole difficulty
19
+ //
20
+ // On the other two desktops a React Native view *is* a widget, so a text field
21
+ // is a widget inside a widget and the toolkit handles the rest. Here every view
22
+ // is a plain C++ object painted with Direct2D into one HWND, and an EDIT is a
23
+ // real child window. Three consequences follow, and none of them has an
24
+ // equivalent on GTK or AppKit:
25
+ //
26
+ // - **It has to be positioned by hand.** The manager walks the view up to the
27
+ // surface root to find its rectangle in client coordinates, and has to redo
28
+ // that whenever anything moves it -- which includes a scroll, where no
29
+ // mutation arrives at all. `syncBounds` is that, and the host calls it after
30
+ // every transaction and every wheel.
31
+ //
32
+ // - **It is always on top.** Child windows paint above whatever Direct2D drew,
33
+ // in an order the React tree has no say in. A field cannot be covered by a
34
+ // later sibling, and it does not clip to a scrolled ancestor -- it is hidden
35
+ // when it leaves the ancestor's box instead, which is close enough to look
36
+ // right and is not the same thing.
37
+ //
38
+ // - **Its events arrive at the parent.** EN_CHANGE, EN_SETFOCUS and
39
+ // EN_KILLFOCUS are WM_COMMAND notifications to the host window, and the
40
+ // text and background colours are answered from WM_CTLCOLOREDIT. So the
41
+ // host forwards both, which is why this class has methods a mounting
42
+ // manager would not otherwise need.
43
+ //
44
+ // ## The controlled-value loop, which is not about a toolkit
45
+ //
46
+ // React Native's <TextInput> is controlled: JavaScript owns the value, the
47
+ // control reports every change, JavaScript re-renders and pushes the value back
48
+ // down as a prop. If applying that prop looks like the user typing, the two
49
+ // chase each other forever. If a prop that arrives late is applied anyway, a
50
+ // fast typist watches characters reorder themselves.
51
+ //
52
+ // `applying` breaks the first and `eventCount` the second, exactly as on both
53
+ // other desktops, because neither is a Windows problem.
54
+
55
+ #pragma once
56
+
57
+ #include "RnWin32View.h"
58
+
59
+ #include <react/renderer/components/iostextinput/TextInputShadowNode.h>
60
+ #include <react/renderer/components/textinput/TextInputEventEmitter.h>
61
+ #include <react/renderer/core/EventEmitter.h>
62
+ #include <react/renderer/mounting/ShadowView.h>
63
+
64
+ #include <windows.h>
65
+
66
+ #include <functional>
67
+ #include <string>
68
+ #include <unordered_map>
69
+
70
+ namespace basalt {
71
+
72
+ class Win32TextInputManager {
73
+ public:
74
+ using EmitterLookup = std::function<facebook::react::EventEmitter::Shared(facebook::react::Tag)>;
75
+
76
+ explicit Win32TextInputManager(EmitterLookup lookup);
77
+ ~Win32TextInputManager();
78
+
79
+ Win32TextInputManager(const Win32TextInputManager &) = delete;
80
+ Win32TextInputManager &operator=(const Win32TextInputManager &) = delete;
81
+ Win32TextInputManager(Win32TextInputManager &&) = delete;
82
+ Win32TextInputManager &operator=(Win32TextInputManager &&) = delete;
83
+
84
+ // The window every peer is a child of. Nothing can be created before this is
85
+ // set, which is why `update` tolerates being called without it: the mounting
86
+ // manager exists before the host's window does, and the tests never have one.
87
+ void setHostWindow(HWND window);
88
+
89
+ // Called for every mutation touching a TextInput. Creates the control the
90
+ // first time, then applies props.
91
+ void update(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
92
+
93
+ void remove(facebook::react::Tag tag);
94
+
95
+ // focus, blur, and setTextAndSelection. Returns false for anything else.
96
+ bool dispatchCommand(facebook::react::Tag tag,
97
+ const std::string &name,
98
+ const folly::dynamic &args);
99
+
100
+ // Moves every peer to where its view now is, in client coordinates, and hides
101
+ // the ones that have gone out of sight. Called after each transaction and
102
+ // each scroll: a child window does not follow a view that has no widget.
103
+ void syncBounds(win32::RnWin32View *root);
104
+
105
+ // WM_COMMAND from the host: EN_CHANGE, EN_SETFOCUS, EN_KILLFOCUS. True when
106
+ // the notification was one of this manager's controls.
107
+ bool handleControlCommand(WPARAM wparam, LPARAM lparam);
108
+
109
+ // WM_CTLCOLOREDIT / WM_CTLCOLORSTATIC from the host. Returns the brush to
110
+ // paint the control's background with, or null when the control is not one of
111
+ // this manager's. An EDIT otherwise draws in the system's colours, which have
112
+ // nothing to do with the `style` the component was given -- on a dark field
113
+ // that is dark text on dark, the same trap GtkText has.
114
+ HBRUSH controlColor(HDC deviceContext, HWND control);
115
+
116
+ // True when the control is one of this manager's peers, which is how the host
117
+ // tells a key meant for a field from one meant for the window.
118
+ bool ownsControl(HWND control) const;
119
+
120
+ // Focuses the field under a point in the surface root's coordinates, if there
121
+ // is one. True when there was.
122
+ //
123
+ // Only BASALT_TEST_TAP needs this, and the reason is the one thing a
124
+ // synthesised tap cannot reproduce. A real click at these coordinates never
125
+ // reaches the host's window procedure at all: the peer is a child window, so
126
+ // USER32 routes the click to it and the control focuses itself. A tap
127
+ // injected at the touch dispatcher skips all of that, because the dispatcher
128
+ // is above the point where the two paths diverge.
129
+ bool focusAt(win32::RnWin32View *root, double x, double y);
130
+
131
+ // Types into whichever field has focus, for BASALT_TEST_TYPE. Real WM_CHAR
132
+ // messages to the real control, so what it skips is the keyboard driver and
133
+ // nothing above it.
134
+ bool typeIntoFocused(const std::string &text);
135
+
136
+ private:
137
+ struct Entry {
138
+ win32::RnWin32View *view{nullptr};
139
+ HWND control{nullptr};
140
+ HFONT font{nullptr};
141
+ facebook::react::Tag tag{0};
142
+ Win32TextInputManager *owner{nullptr};
143
+ // The subclass has to be able to find its way back, and SetWindowSubclass's
144
+ // reference data is the only channel; this is what is put there.
145
+
146
+ // React Native counts events so it can ignore a prop update older than what
147
+ // the user has since typed. Every emitted metric carries it.
148
+ int eventCount{0};
149
+
150
+ // True while a prop is being pushed into the control, so the EN_CHANGE it
151
+ // provokes is not reported back as the user typing.
152
+ bool applying{false};
153
+ // The selection last reported to JavaScript. A plain EDIT has no
154
+ // notification for the caret moving -- EN_SELCHANGE belongs to RichEdit --
155
+ // so the selection is read after anything that could have moved it and
156
+ // compared with this. See reportSelectionIfChanged.
157
+ facebook::react::AttributedString::Range lastReportedSelection{};
158
+
159
+ std::string lastReportedText;
160
+
161
+ // The last `text` prop actually seen, and whether one has been seen at all.
162
+ //
163
+ // This is what tells a *controlled* field from an uncontrolled one, which
164
+ // props alone cannot: React Native's TextInput.js sends
165
+ // `text={value ?? defaultValue}`, and an uncontrolled field with no default
166
+ // sends undefined, which arrives here as the empty string -- exactly what a
167
+ // controlled field that JavaScript has cleared sends. Applying it on every
168
+ // render therefore wipes an uncontrolled field the moment anything else in
169
+ // the tree re-renders, because RN re-sends `mostRecentEventCount` on every
170
+ // change and that alone produces an Update mutation.
171
+ //
172
+ // So the prop is applied when it *changes*, not when it differs from the
173
+ // control. A controlled field's value changes as the user types; an
174
+ // uncontrolled one's never does.
175
+ std::string lastPropText;
176
+ bool sawProps{false};
177
+
178
+ COLORREF textColor{RGB(0, 0, 0)};
179
+ COLORREF backgroundColor{RGB(255, 255, 255)};
180
+ HBRUSH backgroundBrush{nullptr};
181
+ // Whether the view behind the control has a background of its own. With
182
+ // none, the control is painted over whatever Direct2D drew there -- which
183
+ // it cannot see, so the closest honest answer is the window's own ground.
184
+ bool hasBackground{false};
185
+
186
+ bool secure{false};
187
+
188
+ // Yoga resolves border and padding into contentInsets, so the same twelve
189
+ // points mean the same thing in a field as in a <View>. Applied by placing
190
+ // the control inside them rather than by any EDIT message: EM_SETRECT is
191
+ // documented as multiline-only and silently does nothing here, which is a
192
+ // afternoon nobody should spend twice.
193
+ RECT insets{0, 0, 0, 0};
194
+
195
+ // What the control's font actually occupies, measured rather than assumed:
196
+ // a family substitution changes it, and this is what the control's height
197
+ // is set to so that the text sits in the middle of the field.
198
+ LONG lineHeight{0};
199
+ };
200
+
201
+ Entry *entryForControl(HWND control);
202
+ void applyProps(Entry &entry, const facebook::react::TextInputProps &props);
203
+
204
+ // Remembers the content box and the line height, which is what `syncBounds`
205
+ // needs to place the control and cannot work out from the view alone.
206
+ void measureShape(Entry &entry, const facebook::react::LayoutMetrics &metrics);
207
+
208
+ void destroyPeer(Entry &entry);
209
+
210
+ // Enter, and the keys an EDIT would otherwise beep at. Installed with
211
+ // SetWindowSubclass rather than SetWindowLongPtr so that a later subclass by
212
+ // anything else still chains correctly.
213
+ static LRESULT CALLBACK
214
+ editProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR id, DWORD_PTR data);
215
+
216
+ void reportChange(Entry &entry);
217
+ facebook::react::TextInputEventEmitter::Metrics metricsFor(const Entry &entry) const;
218
+ // Reads the caret and emits onSelectionChange if it moved. Called after any
219
+ // message that could have moved it, because Windows will not say.
220
+ void reportSelectionIfChanged(Entry &entry);
221
+ std::shared_ptr<const facebook::react::TextInputEventEmitter> emitterFor(
222
+ facebook::react::Tag tag) const;
223
+
224
+ EmitterLookup lookup_;
225
+ HWND host_{nullptr};
226
+ // Stable addresses: the subclass procedure holds an Entry pointer, so the map
227
+ // must not move its values. std::unordered_map does not, which is the reason
228
+ // it is this container and not a vector.
229
+ std::unordered_map<facebook::react::Tag, Entry> entries_;
230
+ };
231
+
232
+ } // namespace basalt