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,143 @@
|
|
|
1
|
+
// Drives real ShadowViewMutations through the real GtkMountingManager into
|
|
2
|
+
// real GTK widgets, with no JS runtime, no Hermes and no Metro.
|
|
3
|
+
//
|
|
4
|
+
// Note that executeMount only *queues*: in a real host it is called on the JS
|
|
5
|
+
// thread and marshals to the GTK main thread. Calling it from the main thread
|
|
6
|
+
// here takes the same path, one main-loop turn later.
|
|
7
|
+
//
|
|
8
|
+
// This is the checkpoint between "the mounting manager compiles" and "React
|
|
9
|
+
// Native runs": it exercises the actual mutation walk against the actual
|
|
10
|
+
// Fabric types, so a mistake in the Create/Insert/Update/Remove/Delete
|
|
11
|
+
// handling shows up on screen rather than in review.
|
|
12
|
+
|
|
13
|
+
#include "GtkMountingManager.h"
|
|
14
|
+
|
|
15
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
16
|
+
#include <react/renderer/core/LayoutMetrics.h>
|
|
17
|
+
#include <react/renderer/graphics/Color.h>
|
|
18
|
+
#include <react/renderer/mounting/MountingTransaction.h>
|
|
19
|
+
#include <react/renderer/mounting/ShadowViewMutation.h>
|
|
20
|
+
|
|
21
|
+
#include <memory>
|
|
22
|
+
#include <utility>
|
|
23
|
+
|
|
24
|
+
using facebook::react::ColorComponents;
|
|
25
|
+
using facebook::react::LayoutMetrics;
|
|
26
|
+
using facebook::react::MountingTransaction;
|
|
27
|
+
using facebook::react::ShadowView;
|
|
28
|
+
using facebook::react::ShadowViewMutation;
|
|
29
|
+
using facebook::react::ShadowViewMutationList;
|
|
30
|
+
using facebook::react::SurfaceId;
|
|
31
|
+
using facebook::react::Tag;
|
|
32
|
+
using facebook::react::TransactionTelemetry;
|
|
33
|
+
using facebook::react::ViewProps;
|
|
34
|
+
|
|
35
|
+
namespace {
|
|
36
|
+
|
|
37
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
38
|
+
|
|
39
|
+
ShadowView makeShadowView(Tag tag,
|
|
40
|
+
float x, float y, float width, float height,
|
|
41
|
+
float red, float green, float blue,
|
|
42
|
+
float opacity = 1.0f) {
|
|
43
|
+
auto props = std::make_shared<ViewProps>();
|
|
44
|
+
props->backgroundColor = facebook::react::colorFromComponents(
|
|
45
|
+
ColorComponents{.red = red, .green = green, .blue = blue, .alpha = 1.0f});
|
|
46
|
+
props->opacity = opacity;
|
|
47
|
+
|
|
48
|
+
LayoutMetrics layoutMetrics;
|
|
49
|
+
layoutMetrics.frame = {.origin = {.x = x, .y = y},
|
|
50
|
+
.size = {.width = width, .height = height}};
|
|
51
|
+
|
|
52
|
+
ShadowView shadowView;
|
|
53
|
+
shadowView.componentName = "View";
|
|
54
|
+
shadowView.surfaceId = kSurfaceId;
|
|
55
|
+
shadowView.tag = tag;
|
|
56
|
+
shadowView.props = props;
|
|
57
|
+
shadowView.layoutMetrics = layoutMetrics;
|
|
58
|
+
return shadowView;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
MountingTransaction makeTransaction(MountingTransaction::Number number,
|
|
62
|
+
ShadowViewMutationList &&mutations) {
|
|
63
|
+
return MountingTransaction(kSurfaceId, number, std::move(mutations), TransactionTelemetry{});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
basalt::GtkMountingManager *g_mountingManager = nullptr;
|
|
67
|
+
|
|
68
|
+
// Second transaction: recolour one view, and remove another entirely. Fabric
|
|
69
|
+
// always emits Remove before Delete, and this reproduces that ordering.
|
|
70
|
+
gboolean applySecondTransaction(gpointer /*user_data*/) {
|
|
71
|
+
g_message("--- transaction 2: update tag 2, remove+delete tag 3 ---");
|
|
72
|
+
|
|
73
|
+
ShadowViewMutationList mutations;
|
|
74
|
+
|
|
75
|
+
// Update: same tag, new props. Recolour blue -> purple and shrink.
|
|
76
|
+
const ShadowView oldBlue = makeShadowView(2, 32, 32, 240, 160, 0.30f, 0.55f, 0.95f);
|
|
77
|
+
const ShadowView newBlue = makeShadowView(2, 32, 32, 240, 100, 0.60f, 0.35f, 0.90f);
|
|
78
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(oldBlue, newBlue, kSurfaceId));
|
|
79
|
+
|
|
80
|
+
// Remove then Delete, the order Fabric guarantees.
|
|
81
|
+
const ShadowView orange = makeShadowView(3, 296, 32, 240, 160, 0.95f, 0.45f, 0.35f);
|
|
82
|
+
mutations.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, orange, 1));
|
|
83
|
+
mutations.push_back(ShadowViewMutation::DeleteMutation(orange));
|
|
84
|
+
|
|
85
|
+
g_mountingManager->executeMount(kSurfaceId, makeTransaction(2, std::move(mutations)));
|
|
86
|
+
|
|
87
|
+
g_message("--- transaction 2 queued ---");
|
|
88
|
+
return G_SOURCE_REMOVE;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void onActivate(GtkApplication *app, gpointer /*user_data*/) {
|
|
92
|
+
GtkWidget *window = gtk_application_window_new(app);
|
|
93
|
+
gtk_window_set_title(GTK_WINDOW(window), "basalt-core — mount harness");
|
|
94
|
+
gtk_window_set_default_size(GTK_WINDOW(window), 640, 420);
|
|
95
|
+
|
|
96
|
+
// The host owns the root: Fabric never emits a Create for it.
|
|
97
|
+
RnView *root = g_mountingManager->createSurfaceRoot(kSurfaceId);
|
|
98
|
+
gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));
|
|
99
|
+
|
|
100
|
+
g_message("--- transaction 1: create + insert 4 views ---");
|
|
101
|
+
|
|
102
|
+
ShadowViewMutationList mutations;
|
|
103
|
+
|
|
104
|
+
const ShadowView blue = makeShadowView(2, 32, 32, 240, 160, 0.30f, 0.55f, 0.95f);
|
|
105
|
+
const ShadowView orange = makeShadowView(3, 296, 32, 240, 160, 0.95f, 0.45f, 0.35f);
|
|
106
|
+
const ShadowView green = makeShadowView(4, 32, 224, 504, 140, 0.35f, 0.80f, 0.55f);
|
|
107
|
+
const ShadowView nested = makeShadowView(5, 24, 24, 120, 90, 1.0f, 1.0f, 1.0f, 0.85f);
|
|
108
|
+
|
|
109
|
+
// Fabric emits every Create before the Inserts that place them.
|
|
110
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(blue));
|
|
111
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(orange));
|
|
112
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(green));
|
|
113
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(nested));
|
|
114
|
+
|
|
115
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, blue, 0));
|
|
116
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, orange, 1));
|
|
117
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, green, 2));
|
|
118
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(blue.tag, nested, 0));
|
|
119
|
+
|
|
120
|
+
g_mountingManager->executeMount(kSurfaceId, makeTransaction(1, std::move(mutations)));
|
|
121
|
+
|
|
122
|
+
g_message("--- transaction 1 queued (applies on the next main-loop turn) ---");
|
|
123
|
+
|
|
124
|
+
gtk_window_present(GTK_WINDOW(window));
|
|
125
|
+
|
|
126
|
+
g_timeout_add(2000, applySecondTransaction, nullptr);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
} // namespace
|
|
130
|
+
|
|
131
|
+
int main(int argc, char **argv) {
|
|
132
|
+
// Constructed on the main thread: GtkMountingManager records this thread and
|
|
133
|
+
// asserts every executeMount arrives on it.
|
|
134
|
+
basalt::GtkMountingManager mountingManager;
|
|
135
|
+
g_mountingManager = &mountingManager;
|
|
136
|
+
|
|
137
|
+
GtkApplication *app =
|
|
138
|
+
gtk_application_new("dev.basalt.mountharness", G_APPLICATION_DEFAULT_FLAGS);
|
|
139
|
+
g_signal_connect(app, "activate", G_CALLBACK(onActivate), nullptr);
|
|
140
|
+
const int status = g_application_run(G_APPLICATION(app), argc, argv);
|
|
141
|
+
g_object_unref(app);
|
|
142
|
+
return status;
|
|
143
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The GTK test suite's entry point.
|
|
2
|
+
//
|
|
3
|
+
// Separate from the runner because it is the only part of the suite that knows
|
|
4
|
+
// about a toolkit: every test here touches widgets, and a GtkWidget cannot be
|
|
5
|
+
// constructed before GTK is initialised. The macOS suite has its own.
|
|
6
|
+
|
|
7
|
+
#include "TestHarness.h"
|
|
8
|
+
|
|
9
|
+
#include <gtk/gtk.h>
|
|
10
|
+
|
|
11
|
+
#include <iostream>
|
|
12
|
+
|
|
13
|
+
int main(int argc, char **argv) {
|
|
14
|
+
|
|
15
|
+
// No window is ever presented, but GTK still needs a display connection: on a
|
|
16
|
+
// headless machine run this under a nested or virtual display server. See
|
|
17
|
+
// docs/TESTING.md.
|
|
18
|
+
if (gtk_init_check() == FALSE) {
|
|
19
|
+
std::cerr << "could not initialise GTK; these tests need a display.\n"
|
|
20
|
+
<< "On a headless Linux box: xvfb-run -a ./build/basalt_gtk_tests\n";
|
|
21
|
+
return 77; // The convention automake uses for "skipped", not "failed".
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return basalt::testing::runAllTests(argc, argv);
|
|
25
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// A stand-in for a desktop's notification daemon.
|
|
2
|
+
//
|
|
3
|
+
// The one thing missing from testing notifications: `GtkNotifications.cpp`
|
|
4
|
+
// sends `Notify` over the session bus, and neither a Mac nor a CI runner has
|
|
5
|
+
// anything listening for it. Without something here the only paths that could
|
|
6
|
+
// be exercised were the two that report why nothing can be sent.
|
|
7
|
+
//
|
|
8
|
+
// It owns `org.freedesktop.Notifications` on whatever bus it is started on,
|
|
9
|
+
// answers `Notify` with an increasing id, and prints what it was asked to show
|
|
10
|
+
// so the end-to-end suite can assert on it. Everything a real daemon does
|
|
11
|
+
// beyond that -- the window, the timeout, the actions, the signals back -- is
|
|
12
|
+
// what a test does not need and what would make this a second implementation
|
|
13
|
+
// rather than a fixture.
|
|
14
|
+
//
|
|
15
|
+
// Started by scripts/integration_test.py, which also starts the bus. See
|
|
16
|
+
// docs/TESTING.md.
|
|
17
|
+
#include <gio/gio.h>
|
|
18
|
+
#include <stdio.h>
|
|
19
|
+
|
|
20
|
+
static const char *kXml =
|
|
21
|
+
"<node><interface name='org.freedesktop.Notifications'>"
|
|
22
|
+
" <method name='Notify'>"
|
|
23
|
+
" <arg type='s' name='app_name' direction='in'/>"
|
|
24
|
+
" <arg type='u' name='replaces_id' direction='in'/>"
|
|
25
|
+
" <arg type='s' name='app_icon' direction='in'/>"
|
|
26
|
+
" <arg type='s' name='summary' direction='in'/>"
|
|
27
|
+
" <arg type='s' name='body' direction='in'/>"
|
|
28
|
+
" <arg type='as' name='actions' direction='in'/>"
|
|
29
|
+
" <arg type='a{sv}' name='hints' direction='in'/>"
|
|
30
|
+
" <arg type='i' name='timeout' direction='in'/>"
|
|
31
|
+
" <arg type='u' name='id' direction='out'/>"
|
|
32
|
+
" </method>"
|
|
33
|
+
" <method name='CloseNotification'>"
|
|
34
|
+
" <arg type='u' name='id' direction='in'/>"
|
|
35
|
+
" </method>"
|
|
36
|
+
" <method name='GetCapabilities'>"
|
|
37
|
+
" <arg type='as' name='caps' direction='out'/>"
|
|
38
|
+
" </method>"
|
|
39
|
+
"</interface></node>";
|
|
40
|
+
|
|
41
|
+
static guint32 nextId = 1;
|
|
42
|
+
|
|
43
|
+
static void onCall(GDBusConnection *connection, const gchar *sender, const gchar *path,
|
|
44
|
+
const gchar *interface, const gchar *method, GVariant *parameters,
|
|
45
|
+
GDBusMethodInvocation *invocation, gpointer user_data) {
|
|
46
|
+
(void)connection; (void)sender; (void)path; (void)interface; (void)user_data;
|
|
47
|
+
|
|
48
|
+
if (g_strcmp0(method, "Notify") == 0) {
|
|
49
|
+
const gchar *app = nullptr, *icon = nullptr, *summary = nullptr, *body = nullptr;
|
|
50
|
+
guint32 replaces = 0;
|
|
51
|
+
gint timeout = 0;
|
|
52
|
+
GVariantIter *actions = nullptr;
|
|
53
|
+
GVariant *hints = nullptr;
|
|
54
|
+
g_variant_get(parameters, "(&su&s&s&sas@a{sv}i)", &app, &replaces, &icon, &summary, &body,
|
|
55
|
+
&actions, &hints, &timeout);
|
|
56
|
+
printf("STUB Notify app=%s summary=%s body=%s\n", app, summary, body);
|
|
57
|
+
fflush(stdout);
|
|
58
|
+
if (actions != nullptr) g_variant_iter_free(actions);
|
|
59
|
+
if (hints != nullptr) g_variant_unref(hints);
|
|
60
|
+
g_dbus_method_invocation_return_value(invocation, g_variant_new("(u)", nextId++));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (g_strcmp0(method, "CloseNotification") == 0) {
|
|
64
|
+
guint32 id = 0;
|
|
65
|
+
g_variant_get(parameters, "(u)", &id);
|
|
66
|
+
printf("STUB CloseNotification id=%u\n", id);
|
|
67
|
+
fflush(stdout);
|
|
68
|
+
g_dbus_method_invocation_return_value(invocation, nullptr);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (g_strcmp0(method, "GetCapabilities") == 0) {
|
|
72
|
+
GVariantBuilder caps;
|
|
73
|
+
g_variant_builder_init(&caps, G_VARIANT_TYPE("as"));
|
|
74
|
+
g_variant_builder_add(&caps, "s", "body");
|
|
75
|
+
g_dbus_method_invocation_return_value(invocation, g_variant_new("(as)", &caps));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.DBus.Error.UnknownMethod",
|
|
79
|
+
"no");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static const GDBusInterfaceVTable kVTable = {onCall, nullptr, nullptr, {nullptr}};
|
|
83
|
+
|
|
84
|
+
static void onBusAcquired(GDBusConnection *connection, const gchar *name, gpointer user_data) {
|
|
85
|
+
(void)name;
|
|
86
|
+
auto *info = static_cast<GDBusNodeInfo *>(user_data);
|
|
87
|
+
g_dbus_connection_register_object(connection, "/org/freedesktop/Notifications",
|
|
88
|
+
info->interfaces[0], &kVTable, nullptr, nullptr, nullptr);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static void onNameAcquired(GDBusConnection *c, const gchar *name, gpointer d) {
|
|
92
|
+
(void)c; (void)d;
|
|
93
|
+
printf("STUB ready %s\n", name);
|
|
94
|
+
fflush(stdout);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
int main(void) {
|
|
98
|
+
GDBusNodeInfo *info = g_dbus_node_info_new_for_xml(kXml, nullptr);
|
|
99
|
+
g_bus_own_name(G_BUS_TYPE_SESSION, "org.freedesktop.Notifications", G_BUS_NAME_OWNER_FLAGS_NONE,
|
|
100
|
+
onBusAcquired, onNameAcquired, nullptr, info, nullptr);
|
|
101
|
+
g_main_loop_run(g_main_loop_new(nullptr, FALSE));
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Tests for the accessibility mapping.
|
|
2
|
+
//
|
|
3
|
+
// GTK ships assertion helpers for exactly this (gtktestatcontext.h), so what a
|
|
4
|
+
// screen reader would be told can be read back rather than inferred. That is
|
|
5
|
+
// unusual and worth using: accessibility is otherwise the easiest thing in a UI
|
|
6
|
+
// to believe you have done.
|
|
7
|
+
|
|
8
|
+
#include "TestHarness.h"
|
|
9
|
+
|
|
10
|
+
#include "GtkMountingManager.h"
|
|
11
|
+
|
|
12
|
+
#include <react/renderer/components/iostextinput/TextInputProps.h>
|
|
13
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
14
|
+
#include <react/renderer/core/RawProps.h>
|
|
15
|
+
#include <react/renderer/core/RawPropsParser.h>
|
|
16
|
+
|
|
17
|
+
#include <sstream>
|
|
18
|
+
|
|
19
|
+
using facebook::react::AccessibilityState;
|
|
20
|
+
using facebook::react::ImportantForAccessibility;
|
|
21
|
+
using facebook::react::LayoutMetrics;
|
|
22
|
+
using facebook::react::MountingTransaction;
|
|
23
|
+
using facebook::react::ShadowView;
|
|
24
|
+
using facebook::react::ShadowViewMutation;
|
|
25
|
+
using facebook::react::ShadowViewMutationList;
|
|
26
|
+
using facebook::react::SurfaceId;
|
|
27
|
+
using facebook::react::Tag;
|
|
28
|
+
using facebook::react::TransactionTelemetry;
|
|
29
|
+
using facebook::react::ViewProps;
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
34
|
+
|
|
35
|
+
// Builds a view with whatever accessibility props the caller wants to set.
|
|
36
|
+
ShadowView makeAccessibleView(Tag tag, const std::function<void(ViewProps &)> &configure) {
|
|
37
|
+
auto props = std::make_shared<ViewProps>();
|
|
38
|
+
configure(*props);
|
|
39
|
+
|
|
40
|
+
LayoutMetrics metrics;
|
|
41
|
+
metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 50, .height = 50}};
|
|
42
|
+
|
|
43
|
+
ShadowView view;
|
|
44
|
+
view.componentName = "View";
|
|
45
|
+
view.surfaceId = kSurfaceId;
|
|
46
|
+
view.tag = tag;
|
|
47
|
+
view.props = props;
|
|
48
|
+
view.layoutMetrics = metrics;
|
|
49
|
+
return view;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The same, for a <TextInput>. Its props have to be real TextInputProps or the
|
|
53
|
+
// mounting manager builds no peer, and those are only reachable through React
|
|
54
|
+
// Native's own parser -- the fields that matter are const.
|
|
55
|
+
ShadowView makeTextInputView(Tag tag, const std::function<void(ViewProps &)> & /*configure*/,
|
|
56
|
+
const std::string &label) {
|
|
57
|
+
static const facebook::react::RawPropsParser parser = []() {
|
|
58
|
+
facebook::react::RawPropsParser prepared;
|
|
59
|
+
prepared.prepare<facebook::react::TextInputProps>();
|
|
60
|
+
return prepared;
|
|
61
|
+
}();
|
|
62
|
+
|
|
63
|
+
static const auto contextContainer =
|
|
64
|
+
std::make_shared<const facebook::react::ContextContainer>();
|
|
65
|
+
const facebook::react::PropsParserContext context{kSurfaceId, *contextContainer};
|
|
66
|
+
|
|
67
|
+
folly::dynamic raw = folly::dynamic::object("text", "")("accessibilityLabel", label);
|
|
68
|
+
facebook::react::RawProps rawProps{std::move(raw)};
|
|
69
|
+
rawProps.parse(parser);
|
|
70
|
+
|
|
71
|
+
LayoutMetrics metrics;
|
|
72
|
+
metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 200, .height = 40}};
|
|
73
|
+
|
|
74
|
+
ShadowView view;
|
|
75
|
+
view.componentName = "TextInput";
|
|
76
|
+
view.surfaceId = kSurfaceId;
|
|
77
|
+
view.tag = tag;
|
|
78
|
+
view.props = std::make_shared<const facebook::react::TextInputProps>(
|
|
79
|
+
context, facebook::react::TextInputProps{}, rawProps);
|
|
80
|
+
view.layoutMetrics = metrics;
|
|
81
|
+
return view;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mounts one view and hands back the widget.
|
|
85
|
+
RnView *mountOne(basalt::GtkMountingManager &manager, const ShadowView &view) {
|
|
86
|
+
ShadowViewMutationList mutations;
|
|
87
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(view));
|
|
88
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, view, 0));
|
|
89
|
+
manager.applyTransaction(
|
|
90
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
91
|
+
|
|
92
|
+
RnView *root = manager.getSurfaceRoot(kSurfaceId);
|
|
93
|
+
return RN_VIEW(gtk_widget_get_first_child(GTK_WIDGET(root)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} // namespace
|
|
97
|
+
|
|
98
|
+
TEST(accessibility_role_maps_to_a_gtk_role) {
|
|
99
|
+
basalt::GtkMountingManager manager;
|
|
100
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
101
|
+
|
|
102
|
+
RnView *button = mountOne(manager, makeAccessibleView(10, [](ViewProps &props) {
|
|
103
|
+
props.accessibilityRole = "button";
|
|
104
|
+
}));
|
|
105
|
+
EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(button))),
|
|
106
|
+
static_cast<int>(GTK_ACCESSIBLE_ROLE_BUTTON));
|
|
107
|
+
|
|
108
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
TEST(unknown_accessibility_role_falls_back_to_generic) {
|
|
112
|
+
basalt::GtkMountingManager manager;
|
|
113
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
114
|
+
|
|
115
|
+
// A wrong role is worse than no role: it makes a widget announce itself as
|
|
116
|
+
// something it is not.
|
|
117
|
+
RnView *view = mountOne(manager, makeAccessibleView(11, [](ViewProps &props) {
|
|
118
|
+
props.accessibilityRole = "somethingReactNativeInvented";
|
|
119
|
+
}));
|
|
120
|
+
EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(view))),
|
|
121
|
+
static_cast<int>(GTK_ACCESSIBLE_ROLE_GENERIC));
|
|
122
|
+
|
|
123
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
TEST(accessibility_label_and_hint_reach_the_accessible) {
|
|
127
|
+
basalt::GtkMountingManager manager;
|
|
128
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
129
|
+
|
|
130
|
+
RnView *view = mountOne(manager, makeAccessibleView(12, [](ViewProps &props) {
|
|
131
|
+
props.accessibilityLabel = "Play";
|
|
132
|
+
props.accessibilityHint = "Starts the track";
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
char *mismatch = gtk_test_accessible_check_property(
|
|
136
|
+
GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_LABEL, "Play");
|
|
137
|
+
EXPECT(mismatch == nullptr);
|
|
138
|
+
g_free(mismatch);
|
|
139
|
+
|
|
140
|
+
mismatch = gtk_test_accessible_check_property(
|
|
141
|
+
GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, "Starts the track");
|
|
142
|
+
EXPECT(mismatch == nullptr);
|
|
143
|
+
g_free(mismatch);
|
|
144
|
+
|
|
145
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A field's label has to be on the field, not on the view wrapping it. The
|
|
149
|
+
// peer is a real GtkText and is what AT-SPI presents as a text box, so a label
|
|
150
|
+
// left on the wrapper is a labelled field that announces no name.
|
|
151
|
+
TEST(a_text_input_label_lands_on_the_peer_not_the_wrapper) {
|
|
152
|
+
basalt::GtkMountingManager manager;
|
|
153
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
154
|
+
|
|
155
|
+
RnView *view = mountOne(manager, makeTextInputView(13, nullptr, "Your name"));
|
|
156
|
+
GtkWidget *peer = rn_view_get_editable(view);
|
|
157
|
+
EXPECT(peer != nullptr);
|
|
158
|
+
|
|
159
|
+
char *mismatch = gtk_test_accessible_check_property(
|
|
160
|
+
GTK_ACCESSIBLE(peer), GTK_ACCESSIBLE_PROPERTY_LABEL, "Your name");
|
|
161
|
+
EXPECT(mismatch == nullptr);
|
|
162
|
+
g_free(mismatch);
|
|
163
|
+
|
|
164
|
+
// And not announced twice: the wrapper does not also carry the name. Checked
|
|
165
|
+
// by asking for a mismatch rather than for an empty string, because an unset
|
|
166
|
+
// GTK property is not the empty one and does not compare equal to it.
|
|
167
|
+
mismatch = gtk_test_accessible_check_property(
|
|
168
|
+
GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_LABEL, "Your name");
|
|
169
|
+
EXPECT(mismatch != nullptr);
|
|
170
|
+
g_free(mismatch);
|
|
171
|
+
|
|
172
|
+
// The wrapper is scaffolding rather than a control, so it is presentational
|
|
173
|
+
// and a screen reader walks past it to the peer. Decided at construction
|
|
174
|
+
// because a GtkAccessible role cannot be changed later.
|
|
175
|
+
EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(view))),
|
|
176
|
+
static_cast<int>(GTK_ACCESSIBLE_ROLE_PRESENTATION));
|
|
177
|
+
|
|
178
|
+
// And the tree dump says nothing about it. React Native has no role here to
|
|
179
|
+
// report, AppKit prints none either, and a name invented for GTK's benefit
|
|
180
|
+
// would make the two hosts disagree on this view alone.
|
|
181
|
+
std::ostringstream dump;
|
|
182
|
+
char *text = rn_view_describe_tree(view);
|
|
183
|
+
dump << text;
|
|
184
|
+
g_free(text);
|
|
185
|
+
EXPECT(dump.str().find("role=") == std::string::npos);
|
|
186
|
+
|
|
187
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
TEST(accessibility_state_reaches_the_accessible) {
|
|
191
|
+
basalt::GtkMountingManager manager;
|
|
192
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
193
|
+
|
|
194
|
+
RnView *view = mountOne(manager, makeAccessibleView(13, [](ViewProps &props) {
|
|
195
|
+
AccessibilityState state;
|
|
196
|
+
state.disabled = true;
|
|
197
|
+
state.selected = true;
|
|
198
|
+
state.checked = AccessibilityState::Checked;
|
|
199
|
+
props.accessibilityState = state;
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_DISABLED));
|
|
203
|
+
EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_SELECTED));
|
|
204
|
+
EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_CHECKED));
|
|
205
|
+
|
|
206
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
TEST(no_accessibility_state_leaves_checked_unset) {
|
|
210
|
+
basalt::GtkMountingManager manager;
|
|
211
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
212
|
+
|
|
213
|
+
// A view that never says anything about "checked" is not an unchecked
|
|
214
|
+
// checkbox, and a screen reader should not announce it as one.
|
|
215
|
+
RnView *view = mountOne(manager, makeAccessibleView(14, [](ViewProps &props) { (void)props; }));
|
|
216
|
+
|
|
217
|
+
EXPECT(!gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_CHECKED));
|
|
218
|
+
|
|
219
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
TEST(elements_hidden_marks_the_view_hidden) {
|
|
223
|
+
basalt::GtkMountingManager manager;
|
|
224
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
225
|
+
|
|
226
|
+
RnView *view = mountOne(manager, makeAccessibleView(15, [](ViewProps &props) {
|
|
227
|
+
props.accessibilityElementsHidden = true;
|
|
228
|
+
}));
|
|
229
|
+
EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_HIDDEN));
|
|
230
|
+
|
|
231
|
+
RnView *shown = mountOne(manager, makeAccessibleView(16, [](ViewProps &props) { (void)props; }));
|
|
232
|
+
char *mismatch = gtk_test_accessible_check_state(
|
|
233
|
+
GTK_ACCESSIBLE(shown), GTK_ACCESSIBLE_STATE_HIDDEN, FALSE);
|
|
234
|
+
EXPECT(mismatch == nullptr);
|
|
235
|
+
g_free(mismatch);
|
|
236
|
+
|
|
237
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
TEST(important_for_accessibility_no_hide_descendants_hides_too) {
|
|
241
|
+
basalt::GtkMountingManager manager;
|
|
242
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
243
|
+
|
|
244
|
+
RnView *view = mountOne(manager, makeAccessibleView(17, [](ViewProps &props) {
|
|
245
|
+
props.importantForAccessibility =
|
|
246
|
+
ImportantForAccessibility::NoHideDescendants;
|
|
247
|
+
}));
|
|
248
|
+
EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_HIDDEN));
|
|
249
|
+
|
|
250
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
251
|
+
}
|