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.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +718 -0
- package/native/win32/Win32ScrollView.h +256 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +1296 -0
- package/native/win32/Win32TextInput.h +341 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- 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
|