basalt-gtk 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// Tests for <TextInput>: the GtkText peer and the controlled-value loop.
|
|
2
|
+
//
|
|
3
|
+
// The interesting bug in a text field is not typing, it is the loop. React
|
|
4
|
+
// Native's <TextInput> is controlled: JavaScript owns the value, the widget
|
|
5
|
+
// reports every change, JavaScript re-renders and sends the value back down. If
|
|
6
|
+
// applying that prop looks like the user typing, the two chase each other
|
|
7
|
+
// forever; if the cursor is not preserved while applying it, the caret jumps
|
|
8
|
+
// home on every keystroke. Both are asserted below, because both were wrong
|
|
9
|
+
// first.
|
|
10
|
+
//
|
|
11
|
+
// Props are built through React Native's own RawProps parser rather than by
|
|
12
|
+
// assigning fields, because the fields that matter -- `traits.editable`,
|
|
13
|
+
// `traits.secureTextEntry` -- are const and only reachable that way. That also
|
|
14
|
+
// means these tests exercise the same parse the real thing does.
|
|
15
|
+
|
|
16
|
+
#include "TestHarness.h"
|
|
17
|
+
#include "EventRecorder.h"
|
|
18
|
+
|
|
19
|
+
#include "GtkMountingManager.h"
|
|
20
|
+
#include "GtkTextInput.h"
|
|
21
|
+
|
|
22
|
+
#include "GtkTextPeer.h"
|
|
23
|
+
|
|
24
|
+
#include <react/renderer/components/iostextinput/TextInputProps.h>
|
|
25
|
+
#include <react/renderer/components/textinput/TextInputEventEmitter.h>
|
|
26
|
+
#include <react/renderer/core/RawPropsParser.h>
|
|
27
|
+
|
|
28
|
+
#include <folly/dynamic.h>
|
|
29
|
+
|
|
30
|
+
using facebook::react::ContextContainer;
|
|
31
|
+
using facebook::react::EventEmitter;
|
|
32
|
+
using facebook::react::LayoutMetrics;
|
|
33
|
+
using facebook::react::PropsParserContext;
|
|
34
|
+
using facebook::react::RawProps;
|
|
35
|
+
using facebook::react::RawPropsParser;
|
|
36
|
+
using facebook::react::ShadowView;
|
|
37
|
+
using facebook::react::SurfaceId;
|
|
38
|
+
using facebook::react::Tag;
|
|
39
|
+
using facebook::react::TextInputProps;
|
|
40
|
+
|
|
41
|
+
namespace {
|
|
42
|
+
|
|
43
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
44
|
+
|
|
45
|
+
// One parser, prepared once: preparing walks every prop TextInputProps knows,
|
|
46
|
+
// which is not something to redo per test.
|
|
47
|
+
const RawPropsParser &textInputParser() {
|
|
48
|
+
static const RawPropsParser parser = []() {
|
|
49
|
+
RawPropsParser prepared;
|
|
50
|
+
prepared.prepare<TextInputProps>();
|
|
51
|
+
return prepared;
|
|
52
|
+
}();
|
|
53
|
+
return parser;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
std::shared_ptr<const TextInputProps> makeProps(folly::dynamic raw) {
|
|
57
|
+
static const auto contextContainer = std::make_shared<const ContextContainer>();
|
|
58
|
+
PropsParserContext context{kSurfaceId, *contextContainer};
|
|
59
|
+
|
|
60
|
+
RawProps rawProps{std::move(raw)};
|
|
61
|
+
rawProps.parse(textInputParser());
|
|
62
|
+
return std::make_shared<const TextInputProps>(context, TextInputProps{}, rawProps);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ShadowView makeTextInput(Tag tag, folly::dynamic raw, float inset = 0.0F) {
|
|
66
|
+
LayoutMetrics metrics;
|
|
67
|
+
metrics.frame = {.origin = {.x = 0.0F, .y = 0.0F}, .size = {.width = 200.0F, .height = 40.0F}};
|
|
68
|
+
metrics.contentInsets = {.left = inset, .top = inset, .right = inset, .bottom = inset};
|
|
69
|
+
|
|
70
|
+
ShadowView view;
|
|
71
|
+
view.componentName = "TextInput";
|
|
72
|
+
view.surfaceId = kSurfaceId;
|
|
73
|
+
view.tag = tag;
|
|
74
|
+
view.props = makeProps(std::move(raw));
|
|
75
|
+
view.layoutMetrics = metrics;
|
|
76
|
+
return view;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// No event emitter, which is right for everything asserted on the widget side
|
|
80
|
+
// of the loop: the field's own state does not depend on anyone listening.
|
|
81
|
+
basalt::GtkTextInputManager makeManager() {
|
|
82
|
+
return basalt::GtkTextInputManager([](Tag) { return EventEmitter::Shared{}; });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// And one that is listened to. A real emitter over a real EventDispatcher --
|
|
86
|
+
// see tests/EventRecorder.h for why that is possible without a JavaScript
|
|
87
|
+
// runtime, which the comment here used to say it was not.
|
|
88
|
+
basalt::GtkTextInputManager makeRecordedManager(const basalt::testing::EventRecorder &recorder) {
|
|
89
|
+
auto emitter = recorder.emitter<facebook::react::TextInputEventEmitter>();
|
|
90
|
+
return basalt::GtkTextInputManager([emitter](Tag) { return emitter; });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Through the seam, because a multiline peer is not a GtkEditable and asking
|
|
94
|
+
// it for one is a runtime critical rather than a compile error.
|
|
95
|
+
std::string textOf(RnView *view) {
|
|
96
|
+
GtkWidget *editable = rn_view_get_editable(view);
|
|
97
|
+
if (editable == nullptr) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
char *owned = rn_peer_get_text(editable);
|
|
101
|
+
std::string text = owned != nullptr ? owned : "";
|
|
102
|
+
g_free(owned);
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// What a person typing does, as far as GtkText is concerned: an insertion the
|
|
107
|
+
// widget did not come from a prop.
|
|
108
|
+
void typeInto(RnView *view, const char *text, int position) {
|
|
109
|
+
int cursor = position;
|
|
110
|
+
gtk_editable_insert_text(
|
|
111
|
+
GTK_EDITABLE(rn_view_get_editable(view)), text, static_cast<int>(strlen(text)), &cursor);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} // namespace
|
|
115
|
+
|
|
116
|
+
TEST(textinput_mounts_a_real_editable) {
|
|
117
|
+
RnView *view = rn_view_new(10);
|
|
118
|
+
g_object_ref_sink(view);
|
|
119
|
+
auto manager = makeManager();
|
|
120
|
+
|
|
121
|
+
EXPECT(rn_view_get_editable(view) == nullptr);
|
|
122
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")));
|
|
123
|
+
|
|
124
|
+
// A GtkText, not a PangoLayout with a cursor drawn on it: input methods,
|
|
125
|
+
// selection, the clipboard and every Linux keybinding come with it.
|
|
126
|
+
EXPECT(rn_view_get_editable(view) != nullptr);
|
|
127
|
+
EXPECT(GTK_IS_TEXT(rn_view_get_editable(view)));
|
|
128
|
+
EXPECT_EQ(textOf(view), std::string("hello"));
|
|
129
|
+
|
|
130
|
+
g_object_unref(view);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
TEST(textinput_applies_props_without_reporting_them_as_typing) {
|
|
134
|
+
RnView *view = rn_view_new(10);
|
|
135
|
+
g_object_ref_sink(view);
|
|
136
|
+
auto manager = makeManager();
|
|
137
|
+
|
|
138
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "a")));
|
|
139
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "ab")));
|
|
140
|
+
EXPECT_EQ(textOf(view), std::string("ab"));
|
|
141
|
+
|
|
142
|
+
// The event count is the observable half of the loop: applying a prop must
|
|
143
|
+
// not raise it, or the next command from JavaScript looks stale and is
|
|
144
|
+
// dropped. Nothing typed here, so it must still be zero -- which
|
|
145
|
+
// setTextAndSelection at count 0 proves by being accepted.
|
|
146
|
+
EXPECT(manager.dispatchCommand(
|
|
147
|
+
10, "setTextAndSelection", folly::dynamic::array(0, "from js", 0, 0)));
|
|
148
|
+
EXPECT_EQ(textOf(view), std::string("from js"));
|
|
149
|
+
|
|
150
|
+
g_object_unref(view);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
TEST(textinput_preserves_the_cursor_when_a_prop_arrives) {
|
|
154
|
+
RnView *view = rn_view_new(10);
|
|
155
|
+
g_object_ref_sink(view);
|
|
156
|
+
auto manager = makeManager();
|
|
157
|
+
|
|
158
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcd")));
|
|
159
|
+
gtk_editable_set_position(GTK_EDITABLE(rn_view_get_editable(view)), 2);
|
|
160
|
+
|
|
161
|
+
// A controlled field re-renders on every keystroke, so this happens
|
|
162
|
+
// constantly. Assigning the text resets GtkText's cursor to the start, which
|
|
163
|
+
// sends the caret home mid-word unless it is put back.
|
|
164
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abXcd")));
|
|
165
|
+
EXPECT_EQ(gtk_editable_get_position(GTK_EDITABLE(rn_view_get_editable(view))), 2);
|
|
166
|
+
|
|
167
|
+
g_object_unref(view);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
TEST(textinput_drops_a_command_older_than_what_was_typed) {
|
|
171
|
+
RnView *view = rn_view_new(10);
|
|
172
|
+
g_object_ref_sink(view);
|
|
173
|
+
auto manager = makeManager();
|
|
174
|
+
|
|
175
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
|
|
176
|
+
typeInto(view, "fast", 0);
|
|
177
|
+
|
|
178
|
+
// The whole reason React Native counts events. JavaScript rendered against an
|
|
179
|
+
// empty field and is only now asking for that state; the user has since
|
|
180
|
+
// typed, so this must be ignored rather than undoing their work.
|
|
181
|
+
EXPECT(manager.dispatchCommand(
|
|
182
|
+
10, "setTextAndSelection", folly::dynamic::array(0, "stale", 0, 0)));
|
|
183
|
+
EXPECT_EQ(textOf(view), std::string("fast"));
|
|
184
|
+
|
|
185
|
+
// A command that knows about the typing is applied.
|
|
186
|
+
EXPECT(manager.dispatchCommand(
|
|
187
|
+
10, "setTextAndSelection", folly::dynamic::array(99, "current", 0, 0)));
|
|
188
|
+
EXPECT_EQ(textOf(view), std::string("current"));
|
|
189
|
+
|
|
190
|
+
g_object_unref(view);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// An uncontrolled field keeps what was typed into it.
|
|
194
|
+
//
|
|
195
|
+
// React Native re-sends `mostRecentEventCount` on every change, which is an
|
|
196
|
+
// Update mutation on its own, and `text` for an uncontrolled field is the empty
|
|
197
|
+
// string forever -- `TextInput.js` sends `text={value ?? defaultValue}` and an
|
|
198
|
+
// uncontrolled field with no default sends undefined. So a manager that applies
|
|
199
|
+
// the prop whenever it differs from the widget wipes the field on the user's
|
|
200
|
+
// second keystroke. Applying it only when the *prop* changes is what makes this
|
|
201
|
+
// work; found on Windows in phase 46, and this platform had the same bug.
|
|
202
|
+
TEST(textinput_keeps_what_was_typed_into_an_uncontrolled_field) {
|
|
203
|
+
RnView *view = rn_view_new(10);
|
|
204
|
+
g_object_ref_sink(view);
|
|
205
|
+
auto manager = makeManager();
|
|
206
|
+
|
|
207
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
|
|
208
|
+
typeInto(view, "abc", 0);
|
|
209
|
+
EXPECT_EQ(textOf(view), std::string("abc"));
|
|
210
|
+
|
|
211
|
+
// The re-render an uncontrolled field produces: no text, a bumped count.
|
|
212
|
+
manager.update(
|
|
213
|
+
view,
|
|
214
|
+
makeTextInput(10, folly::dynamic::object("text", "")("mostRecentEventCount", 1)));
|
|
215
|
+
EXPECT_EQ(textOf(view), std::string("abc"));
|
|
216
|
+
|
|
217
|
+
g_object_unref(view);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The same staleness rule the commands already follow, applied to the prop. A
|
|
221
|
+
// value React rendered before the user's latest keystroke must not be applied,
|
|
222
|
+
// or a fast typist watches characters reorder themselves -- and it must not be
|
|
223
|
+
// *forgotten* either, or the value is lost once JavaScript catches up.
|
|
224
|
+
// The whole selection reaches JavaScript, not only the caret. Before this the
|
|
225
|
+
// metrics reported {cursor, 0} whatever was selected, so `onSelectionChange`
|
|
226
|
+
// could not tell a caret from a selected word.
|
|
227
|
+
TEST(textinput_reports_the_whole_selection_not_only_the_caret) {
|
|
228
|
+
RnView *view = rn_view_new(10);
|
|
229
|
+
g_object_ref_sink(view);
|
|
230
|
+
auto manager = makeManager();
|
|
231
|
+
|
|
232
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
|
|
233
|
+
GtkWidget *editable = rn_view_get_editable(view);
|
|
234
|
+
rn_peer_select_region(editable, 1, 4);
|
|
235
|
+
|
|
236
|
+
int start = 0;
|
|
237
|
+
int end = 0;
|
|
238
|
+
EXPECT(rn_peer_get_selection_bounds(editable, &start, &end));
|
|
239
|
+
EXPECT_EQ(start, 1);
|
|
240
|
+
EXPECT_EQ(end, 4);
|
|
241
|
+
|
|
242
|
+
g_object_unref(view);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The `selection` prop is controlled the way `text` is, and under the same
|
|
246
|
+
// staleness rule.
|
|
247
|
+
TEST(textinput_applies_a_selection_prop) {
|
|
248
|
+
RnView *view = rn_view_new(10);
|
|
249
|
+
g_object_ref_sink(view);
|
|
250
|
+
auto manager = makeManager();
|
|
251
|
+
|
|
252
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
|
|
253
|
+
manager.update(view,
|
|
254
|
+
makeTextInput(10,
|
|
255
|
+
folly::dynamic::object("text", "abcdef")(
|
|
256
|
+
"selection", folly::dynamic::object("start", 2)("end", 5))));
|
|
257
|
+
|
|
258
|
+
GtkWidget *editable = rn_view_get_editable(view);
|
|
259
|
+
int start = 0;
|
|
260
|
+
int end = 0;
|
|
261
|
+
EXPECT(rn_peer_get_selection_bounds(editable, &start, &end));
|
|
262
|
+
EXPECT_EQ(start, 2);
|
|
263
|
+
EXPECT_EQ(end, 5);
|
|
264
|
+
|
|
265
|
+
g_object_unref(view);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A selection from JavaScript that predates the last keystroke is dropped, for
|
|
269
|
+
// the same reason a stale `text` is: it would drag the caret back to where
|
|
270
|
+
// JavaScript thought it was, mid-word.
|
|
271
|
+
TEST(textinput_drops_a_selection_prop_older_than_what_was_typed) {
|
|
272
|
+
RnView *view = rn_view_new(10);
|
|
273
|
+
g_object_ref_sink(view);
|
|
274
|
+
auto manager = makeManager();
|
|
275
|
+
|
|
276
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
|
|
277
|
+
GtkWidget *editable = rn_view_get_editable(view);
|
|
278
|
+
|
|
279
|
+
// Type, which raises the event count past what the next props carry. The
|
|
280
|
+
// position is in/out and must be a real variable: GtkEditable writes the
|
|
281
|
+
// caret's new home back through it.
|
|
282
|
+
int position = 6;
|
|
283
|
+
rn_peer_insert_text(editable, "g", 1, &position);
|
|
284
|
+
|
|
285
|
+
rn_peer_select_region(editable, 0, 0);
|
|
286
|
+
manager.update(view,
|
|
287
|
+
makeTextInput(10,
|
|
288
|
+
folly::dynamic::object("text", "abcdef")(
|
|
289
|
+
"selection", folly::dynamic::object("start", 2)("end", 5))));
|
|
290
|
+
|
|
291
|
+
int start = 0;
|
|
292
|
+
int end = 0;
|
|
293
|
+
rn_peer_get_selection_bounds(editable, &start, &end);
|
|
294
|
+
EXPECT_EQ(start, 0);
|
|
295
|
+
EXPECT_EQ(end, 0);
|
|
296
|
+
|
|
297
|
+
g_object_unref(view);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
TEST(textinput_drops_a_text_prop_older_than_what_was_typed) {
|
|
301
|
+
RnView *view = rn_view_new(10);
|
|
302
|
+
g_object_ref_sink(view);
|
|
303
|
+
auto manager = makeManager();
|
|
304
|
+
|
|
305
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
|
|
306
|
+
typeInto(view, "fast", 0);
|
|
307
|
+
|
|
308
|
+
manager.update(
|
|
309
|
+
view,
|
|
310
|
+
makeTextInput(10, folly::dynamic::object("text", "F")("mostRecentEventCount", 0)));
|
|
311
|
+
EXPECT_EQ(textOf(view), std::string("fast"));
|
|
312
|
+
|
|
313
|
+
manager.update(
|
|
314
|
+
view,
|
|
315
|
+
makeTextInput(10, folly::dynamic::object("text", "F")("mostRecentEventCount", 1)));
|
|
316
|
+
EXPECT_EQ(textOf(view), std::string("F"));
|
|
317
|
+
|
|
318
|
+
g_object_unref(view);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
TEST(textinput_honours_editable_and_secure_entry) {
|
|
322
|
+
RnView *view = rn_view_new(10);
|
|
323
|
+
g_object_ref_sink(view);
|
|
324
|
+
auto manager = makeManager();
|
|
325
|
+
|
|
326
|
+
manager.update(view,
|
|
327
|
+
makeTextInput(10,
|
|
328
|
+
folly::dynamic::object("text", "secret")("editable", false)(
|
|
329
|
+
"secureTextEntry", true)("placeholder", "type here")));
|
|
330
|
+
|
|
331
|
+
GtkWidget *editable = rn_view_get_editable(view);
|
|
332
|
+
EXPECT(gtk_editable_get_editable(GTK_EDITABLE(editable)) == FALSE);
|
|
333
|
+
EXPECT(gtk_text_get_visibility(GTK_TEXT(editable)) == FALSE);
|
|
334
|
+
EXPECT_EQ(std::string(gtk_text_get_placeholder_text(GTK_TEXT(editable))), std::string("type here"));
|
|
335
|
+
|
|
336
|
+
// editable is a prop, not a permanent trait; turning it back on must work.
|
|
337
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "secret")));
|
|
338
|
+
EXPECT(gtk_editable_get_editable(GTK_EDITABLE(editable)) == TRUE);
|
|
339
|
+
EXPECT(gtk_text_get_visibility(GTK_TEXT(editable)) == TRUE);
|
|
340
|
+
|
|
341
|
+
g_object_unref(view);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
TEST(textinput_allocates_its_peer_inside_the_content_inset) {
|
|
345
|
+
RnView *view = rn_view_new(10);
|
|
346
|
+
g_object_ref_sink(view);
|
|
347
|
+
auto manager = makeManager();
|
|
348
|
+
|
|
349
|
+
// Yoga resolves border and padding into the content inset. A GtkText knows
|
|
350
|
+
// nothing about either, so without this the text sits flush against the
|
|
351
|
+
// field's own border and paddingHorizontal does nothing.
|
|
352
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "x"), 12.0F));
|
|
353
|
+
rn_view_set_frame(view, 0.0F, 0.0F, 200.0F, 40.0F);
|
|
354
|
+
gtk_widget_allocate(GTK_WIDGET(view), 200, 40, -1, nullptr);
|
|
355
|
+
|
|
356
|
+
GtkWidget *peer = GTK_WIDGET(rn_view_get_editable(view));
|
|
357
|
+
EXPECT_EQ(gtk_widget_get_width(peer), 176);
|
|
358
|
+
EXPECT_EQ(gtk_widget_get_height(peer), 16);
|
|
359
|
+
|
|
360
|
+
graphene_point_t origin;
|
|
361
|
+
const graphene_point_t zero = {0.0F, 0.0F};
|
|
362
|
+
EXPECT(gtk_widget_compute_point(peer, GTK_WIDGET(view), &zero, &origin));
|
|
363
|
+
EXPECT_NEAR(origin.x, 12.0, 0.5);
|
|
364
|
+
EXPECT_NEAR(origin.y, 12.0, 0.5);
|
|
365
|
+
|
|
366
|
+
g_object_unref(view);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
TEST(textinput_reports_its_value_in_the_widget_tree) {
|
|
370
|
+
RnView *view = rn_view_new(10);
|
|
371
|
+
g_object_ref_sink(view);
|
|
372
|
+
auto manager = makeManager();
|
|
373
|
+
|
|
374
|
+
// The end-to-end suite reads this tree, and a field's text lives in its peer
|
|
375
|
+
// rather than in a PangoLayout, so it would otherwise be invisible there.
|
|
376
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "in the tree")));
|
|
377
|
+
char *tree = rn_view_describe_tree(view);
|
|
378
|
+
EXPECT(std::string(tree).find("editable=\"in the tree\"") != std::string::npos);
|
|
379
|
+
g_free(tree);
|
|
380
|
+
|
|
381
|
+
g_object_unref(view);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// --- multiline ---------------------------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// A multiline field is a different widget, not a property: GtkText is a
|
|
387
|
+
// GtkEditable and GtkTextView is not. These pin that the seam in GtkTextPeer.h
|
|
388
|
+
// really does hide that, because every one of the behaviours above goes
|
|
389
|
+
// through it.
|
|
390
|
+
|
|
391
|
+
TEST(textinput_multiline_builds_a_text_view) {
|
|
392
|
+
RnView *view = rn_view_new(10);
|
|
393
|
+
g_object_ref_sink(view);
|
|
394
|
+
auto manager = makeManager();
|
|
395
|
+
|
|
396
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")("multiline", true)));
|
|
397
|
+
|
|
398
|
+
GtkWidget *peer = rn_view_get_editable(view);
|
|
399
|
+
EXPECT(peer != nullptr);
|
|
400
|
+
EXPECT(rn_peer_is_multiline(peer));
|
|
401
|
+
EXPECT(GTK_IS_TEXT_VIEW(peer));
|
|
402
|
+
|
|
403
|
+
// And a plain field is still a GtkText, which is the half that must not have
|
|
404
|
+
// moved.
|
|
405
|
+
RnView *single = rn_view_new(11);
|
|
406
|
+
g_object_ref_sink(single);
|
|
407
|
+
manager.update(single, makeTextInput(11, folly::dynamic::object("text", "hello")));
|
|
408
|
+
EXPECT(!rn_peer_is_multiline(rn_view_get_editable(single)));
|
|
409
|
+
|
|
410
|
+
g_object_unref(single);
|
|
411
|
+
g_object_unref(view);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
TEST(textinput_multiline_text_round_trips) {
|
|
415
|
+
RnView *view = rn_view_new(10);
|
|
416
|
+
g_object_ref_sink(view);
|
|
417
|
+
auto manager = makeManager();
|
|
418
|
+
|
|
419
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "one")("multiline", true)));
|
|
420
|
+
EXPECT_EQ(textOf(view), std::string("one"));
|
|
421
|
+
|
|
422
|
+
// Newlines are the point of a multiline field, and are what a GtkText would
|
|
423
|
+
// have refused to hold.
|
|
424
|
+
manager.update(view,
|
|
425
|
+
makeTextInput(10, folly::dynamic::object("text", "one\ntwo")("multiline", true)));
|
|
426
|
+
EXPECT_EQ(textOf(view), std::string("one\ntwo"));
|
|
427
|
+
|
|
428
|
+
g_object_unref(view);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
TEST(textinput_multiline_selection_and_caret_work) {
|
|
432
|
+
RnView *view = rn_view_new(10);
|
|
433
|
+
g_object_ref_sink(view);
|
|
434
|
+
auto manager = makeManager();
|
|
435
|
+
|
|
436
|
+
manager.update(view,
|
|
437
|
+
makeTextInput(10, folly::dynamic::object("text", "abcdef")("multiline", true)));
|
|
438
|
+
GtkWidget *peer = rn_view_get_editable(view);
|
|
439
|
+
|
|
440
|
+
rn_peer_select_region(peer, 1, 4);
|
|
441
|
+
int start = 0;
|
|
442
|
+
int end = 0;
|
|
443
|
+
EXPECT(rn_peer_get_selection_bounds(peer, &start, &end));
|
|
444
|
+
EXPECT_EQ(start, 1);
|
|
445
|
+
EXPECT_EQ(end, 4);
|
|
446
|
+
|
|
447
|
+
rn_peer_set_position(peer, 2);
|
|
448
|
+
EXPECT_EQ(rn_peer_get_position(peer), 2);
|
|
449
|
+
|
|
450
|
+
// Offsets are characters, not bytes: the seam says so and a buffer counts
|
|
451
|
+
// both, so a non-ASCII string is where that promise is kept or broken.
|
|
452
|
+
manager.update(view,
|
|
453
|
+
makeTextInput(10, folly::dynamic::object("text", "a\u00e9c")("multiline", true)));
|
|
454
|
+
rn_peer_set_position(peer, 2);
|
|
455
|
+
EXPECT_EQ(rn_peer_get_position(peer), 2);
|
|
456
|
+
|
|
457
|
+
g_object_unref(view);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
TEST(textinput_switching_multiline_rebuilds_the_peer) {
|
|
461
|
+
RnView *view = rn_view_new(10);
|
|
462
|
+
g_object_ref_sink(view);
|
|
463
|
+
auto manager = makeManager();
|
|
464
|
+
|
|
465
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")));
|
|
466
|
+
EXPECT(!rn_peer_is_multiline(rn_view_get_editable(view)));
|
|
467
|
+
|
|
468
|
+
// Not a property that can be flipped, so the widget is replaced -- and the
|
|
469
|
+
// text has to survive that, because React did not ask for it to be cleared.
|
|
470
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")("multiline", true)));
|
|
471
|
+
EXPECT(rn_peer_is_multiline(rn_view_get_editable(view)));
|
|
472
|
+
EXPECT_EQ(textOf(view), std::string("hello"));
|
|
473
|
+
|
|
474
|
+
g_object_unref(view);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// maxLength, on the peer that has no property for it. GtkText enforces its own;
|
|
478
|
+
// a buffer refuses the insertion that would cross the limit instead.
|
|
479
|
+
TEST(textinput_multiline_refuses_text_past_max_length) {
|
|
480
|
+
RnView *view = rn_view_new(10);
|
|
481
|
+
g_object_ref_sink(view);
|
|
482
|
+
auto manager = makeManager();
|
|
483
|
+
|
|
484
|
+
manager.update(view,
|
|
485
|
+
makeTextInput(10, folly::dynamic::object("text", "")("multiline", true)(
|
|
486
|
+
"maxLength", 5)));
|
|
487
|
+
GtkWidget *peer = rn_view_get_editable(view);
|
|
488
|
+
|
|
489
|
+
int at = 0;
|
|
490
|
+
rn_peer_insert_text(peer, "abcd", 4, &at);
|
|
491
|
+
EXPECT_EQ(textOf(view), std::string("abcd"));
|
|
492
|
+
|
|
493
|
+
// Refused whole rather than truncated, which is what GtkText does with its
|
|
494
|
+
// own limit -- a paste that would overflow leaves what was there.
|
|
495
|
+
at = 4;
|
|
496
|
+
rn_peer_insert_text(peer, "efgh", 4, &at);
|
|
497
|
+
EXPECT_EQ(textOf(view), std::string("abcd"));
|
|
498
|
+
|
|
499
|
+
// And one that fits still lands.
|
|
500
|
+
at = 4;
|
|
501
|
+
rn_peer_insert_text(peer, "e", 1, &at);
|
|
502
|
+
EXPECT_EQ(textOf(view), std::string("abcde"));
|
|
503
|
+
|
|
504
|
+
g_object_unref(view);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// A placeholder on the peer that has no property for it. Drawn rather than set,
|
|
508
|
+
// so what a test can reach is the string the peer was given -- the drawing
|
|
509
|
+
// itself is in the screenshot that went with phase 57.
|
|
510
|
+
TEST(textinput_multiline_takes_a_placeholder) {
|
|
511
|
+
RnView *view = rn_view_new(10);
|
|
512
|
+
g_object_ref_sink(view);
|
|
513
|
+
auto manager = makeManager();
|
|
514
|
+
|
|
515
|
+
manager.update(view,
|
|
516
|
+
makeTextInput(10, folly::dynamic::object("text", "")("multiline", true)(
|
|
517
|
+
"placeholder", "type here")));
|
|
518
|
+
GtkWidget *peer = rn_view_get_editable(view);
|
|
519
|
+
EXPECT(rn_peer_is_multiline(peer));
|
|
520
|
+
const char *got = rn_peer_get_placeholder(peer);
|
|
521
|
+
EXPECT_EQ(std::string(got != nullptr ? got : ""), std::string("type here"));
|
|
522
|
+
|
|
523
|
+
// And the single-line peer still takes its own, which is a property rather
|
|
524
|
+
// than something drawn.
|
|
525
|
+
RnView *single = rn_view_new(11);
|
|
526
|
+
g_object_ref_sink(single);
|
|
527
|
+
manager.update(single,
|
|
528
|
+
makeTextInput(11, folly::dynamic::object("text", "")("placeholder", "type here")));
|
|
529
|
+
const char *single_got = rn_peer_get_placeholder(rn_view_get_editable(single));
|
|
530
|
+
EXPECT_EQ(std::string(single_got != nullptr ? single_got : ""), std::string("type here"));
|
|
531
|
+
|
|
532
|
+
g_object_unref(single);
|
|
533
|
+
g_object_unref(view);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
TEST(textinput_drops_its_peer_when_the_field_is_removed) {
|
|
537
|
+
RnView *view = rn_view_new(10);
|
|
538
|
+
g_object_ref_sink(view);
|
|
539
|
+
auto manager = makeManager();
|
|
540
|
+
|
|
541
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "x")));
|
|
542
|
+
manager.remove(10);
|
|
543
|
+
|
|
544
|
+
// The signal handlers hold a pointer into the manager's own map, so the
|
|
545
|
+
// widget has to go with the entry or a later `changed` writes through a
|
|
546
|
+
// dangling pointer.
|
|
547
|
+
EXPECT(rn_view_get_editable(view) == nullptr);
|
|
548
|
+
EXPECT(!manager.dispatchCommand(10, "focus", folly::dynamic::array()));
|
|
549
|
+
|
|
550
|
+
g_object_unref(view);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- What React actually hears ---------------------------------------------
|
|
554
|
+
//
|
|
555
|
+
// Nothing below the end-to-end suite could see an event until
|
|
556
|
+
// tests/EventRecorder.h, so every assertion above is about the widget and
|
|
557
|
+
// none is about the loop. These two are about the loop.
|
|
558
|
+
|
|
559
|
+
TEST(textinput_change_reaches_the_emitter) {
|
|
560
|
+
basalt::testing::EventRecorder recorder;
|
|
561
|
+
RnView *view = rn_view_new(10);
|
|
562
|
+
g_object_ref_sink(view);
|
|
563
|
+
auto manager = makeRecordedManager(recorder);
|
|
564
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
|
|
565
|
+
|
|
566
|
+
// Mounting a field is not the user doing anything, so React hears nothing
|
|
567
|
+
// yet. Worth pinning: a recorder wired up wrong would be just as quiet.
|
|
568
|
+
EXPECT(recorder.seen().empty());
|
|
569
|
+
|
|
570
|
+
typeInto(view, "a", 0);
|
|
571
|
+
|
|
572
|
+
const auto seen = recorder.seen();
|
|
573
|
+
EXPECT_EQ(seen.size(), 1U);
|
|
574
|
+
EXPECT_EQ(seen.empty() ? std::string{} : seen[0], std::string("topChange"));
|
|
575
|
+
|
|
576
|
+
g_object_unref(view);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// A value pushed down as a prop is React talking to the field, not the field
|
|
580
|
+
// talking back -- so it must not come back up as a change. That is the
|
|
581
|
+
// controlled-value loop the top of this file describes, and the half of it
|
|
582
|
+
// that could not be checked until an event could be observed at all: the
|
|
583
|
+
// tests above could see that the text was right, not that the field stayed
|
|
584
|
+
// quiet about it.
|
|
585
|
+
TEST(textinput_a_prop_does_not_report_itself_as_a_change) {
|
|
586
|
+
basalt::testing::EventRecorder recorder;
|
|
587
|
+
RnView *view = rn_view_new(10);
|
|
588
|
+
g_object_ref_sink(view);
|
|
589
|
+
auto manager = makeRecordedManager(recorder);
|
|
590
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
|
|
591
|
+
|
|
592
|
+
manager.update(view, makeTextInput(10, folly::dynamic::object("text", "from React")));
|
|
593
|
+
|
|
594
|
+
EXPECT_EQ(textOf(view), std::string("from React"));
|
|
595
|
+
EXPECT(recorder.seen().empty());
|
|
596
|
+
|
|
597
|
+
g_object_unref(view);
|
|
598
|
+
}
|