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,194 @@
|
|
|
1
|
+
// Keyboard focus: what Tab stops on, in what order, and what Enter does.
|
|
2
|
+
//
|
|
3
|
+
// The manager is built standalone against a real GtkMountingManager and a real
|
|
4
|
+
// window, because focus is the one part of input that needs both: GTK will not
|
|
5
|
+
// give focus to a widget that is not mapped, and the chain this exercises is
|
|
6
|
+
// GTK's own rather than this project's.
|
|
7
|
+
//
|
|
8
|
+
// No emitter is attached to these hand-built shadow views, so the focus and
|
|
9
|
+
// blur events have nowhere to go and the click dispatch does nothing. What is
|
|
10
|
+
// observable is the part that decides where focus actually is, which is the
|
|
11
|
+
// part this project can get wrong on its own.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "GtkFocus.h"
|
|
16
|
+
#include "GtkMountingManager.h"
|
|
17
|
+
|
|
18
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
19
|
+
|
|
20
|
+
#include <cstdint>
|
|
21
|
+
#include <sstream>
|
|
22
|
+
|
|
23
|
+
using facebook::react::LayoutMetrics;
|
|
24
|
+
using facebook::react::MountingTransaction;
|
|
25
|
+
using facebook::react::ShadowView;
|
|
26
|
+
using facebook::react::ShadowViewMutation;
|
|
27
|
+
using facebook::react::ShadowViewMutationList;
|
|
28
|
+
using facebook::react::SurfaceId;
|
|
29
|
+
using facebook::react::Tag;
|
|
30
|
+
using facebook::react::TransactionTelemetry;
|
|
31
|
+
using facebook::react::ViewProps;
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
36
|
+
|
|
37
|
+
ShadowView makeView(Tag tag, float y, bool accessible) {
|
|
38
|
+
auto props = std::make_shared<ViewProps>();
|
|
39
|
+
props->accessible = accessible;
|
|
40
|
+
|
|
41
|
+
LayoutMetrics metrics;
|
|
42
|
+
metrics.frame = {.origin = {.x = 0, .y = y}, .size = {.width = 200, .height = 40}};
|
|
43
|
+
|
|
44
|
+
ShadowView view;
|
|
45
|
+
view.componentName = "View";
|
|
46
|
+
view.surfaceId = kSurfaceId;
|
|
47
|
+
view.tag = tag;
|
|
48
|
+
view.props = props;
|
|
49
|
+
view.layoutMetrics = metrics;
|
|
50
|
+
return view;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A window with a focus manager, in the order the host builds them: the manager
|
|
54
|
+
// exists before the window is shown, and the first transaction arrives after.
|
|
55
|
+
//
|
|
56
|
+
// The order matters and is not incidental. GTK gives focus to the first
|
|
57
|
+
// focusable widget when a window is shown, so a tree mounted *before* that
|
|
58
|
+
// would open with something already focused -- which the AppKit host does not
|
|
59
|
+
// do, and which would make an app see onFocus at startup on one desktop only.
|
|
60
|
+
// The real host shows an empty window and mounts into it, and so does this.
|
|
61
|
+
struct Scene {
|
|
62
|
+
basalt::GtkMountingManager manager;
|
|
63
|
+
RnView *root;
|
|
64
|
+
GtkWidget *window;
|
|
65
|
+
basalt::GtkFocusManager focus;
|
|
66
|
+
|
|
67
|
+
Scene()
|
|
68
|
+
: root(manager.createSurfaceRoot(kSurfaceId)),
|
|
69
|
+
window(gtk_window_new()),
|
|
70
|
+
focus(&manager, root) {
|
|
71
|
+
|
|
72
|
+
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
|
|
73
|
+
gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));
|
|
74
|
+
gtk_widget_set_visible(window, TRUE);
|
|
75
|
+
pump();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
~Scene() {
|
|
79
|
+
gtk_window_destroy(GTK_WINDOW(window));
|
|
80
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Scene(const Scene &) = delete;
|
|
84
|
+
Scene &operator=(const Scene &) = delete;
|
|
85
|
+
|
|
86
|
+
void mount(std::initializer_list<ShadowView> views) {
|
|
87
|
+
ShadowViewMutationList mutations;
|
|
88
|
+
int index = 0;
|
|
89
|
+
for (const ShadowView &view : views) {
|
|
90
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(view));
|
|
91
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, view, index++));
|
|
92
|
+
}
|
|
93
|
+
manager.applyTransaction(
|
|
94
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
95
|
+
// GTK will not give focus to a widget that is not mapped, and mapping
|
|
96
|
+
// happens in the frame clock's layout phase rather than when a child is
|
|
97
|
+
// added.
|
|
98
|
+
pump();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static void pump() {
|
|
102
|
+
const gint64 deadline = g_get_monotonic_time() + 500000; // 500ms
|
|
103
|
+
while (g_get_monotonic_time() < deadline) {
|
|
104
|
+
while (g_main_context_iteration(nullptr, FALSE)) {
|
|
105
|
+
}
|
|
106
|
+
g_usleep(1000);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
} // namespace
|
|
112
|
+
|
|
113
|
+
TEST(focus_accessible_views_are_the_ones_tab_stops_on) {
|
|
114
|
+
Scene scene;
|
|
115
|
+
// React Native's `focusable` prop never reaches this platform, so `accessible`
|
|
116
|
+
// is the signal -- and it is what <Pressable> sets on everything it renders.
|
|
117
|
+
scene.mount({makeView(10, 0, true), makeView(11, 50, false), makeView(12, 100, true)});
|
|
118
|
+
|
|
119
|
+
EXPECT(rn_view_get_focusable(scene.manager.viewForTag(10)));
|
|
120
|
+
EXPECT(!rn_view_get_focusable(scene.manager.viewForTag(11)));
|
|
121
|
+
EXPECT(rn_view_get_focusable(scene.manager.viewForTag(12)));
|
|
122
|
+
// And it shows up in the tree dump, so the cross-host diff can say the prop
|
|
123
|
+
// arrived on all three.
|
|
124
|
+
char *tree = rn_view_describe_tree(scene.root);
|
|
125
|
+
const std::string dump(tree);
|
|
126
|
+
g_free(tree);
|
|
127
|
+
EXPECT(dump.find("focusable") != std::string::npos);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
TEST(focus_tab_visits_focusable_views_in_tree_order) {
|
|
131
|
+
Scene scene;
|
|
132
|
+
scene.mount({makeView(10, 0, true), makeView(11, 50, false), makeView(12, 100, true)});
|
|
133
|
+
// Nothing is focused when a window opens, on either desktop.
|
|
134
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 0L);
|
|
135
|
+
|
|
136
|
+
EXPECT(scene.focus.moveFocus(true));
|
|
137
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 10L);
|
|
138
|
+
// 11 is not accessible, so Tab goes past it.
|
|
139
|
+
EXPECT(scene.focus.moveFocus(true));
|
|
140
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 12L);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
TEST(focus_tab_wraps_at_the_end) {
|
|
144
|
+
Scene scene;
|
|
145
|
+
scene.mount({makeView(10, 0, true), makeView(11, 50, true)});
|
|
146
|
+
scene.focus.moveFocus(true);
|
|
147
|
+
scene.focus.moveFocus(true);
|
|
148
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 11L);
|
|
149
|
+
// GTK's own `gtk_widget_child_focus` stops at the last child, because a real
|
|
150
|
+
// window would carry on into whatever is next -- and here the surface root
|
|
151
|
+
// *is* the content. The AppKit side wraps, so this one has to as well.
|
|
152
|
+
EXPECT(scene.focus.moveFocus(true));
|
|
153
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 10L);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
TEST(focus_shift_tab_goes_back) {
|
|
157
|
+
Scene scene;
|
|
158
|
+
scene.mount({makeView(10, 0, true), makeView(11, 50, true)});
|
|
159
|
+
scene.focus.moveFocus(true);
|
|
160
|
+
scene.focus.moveFocus(true);
|
|
161
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 11L);
|
|
162
|
+
EXPECT(scene.focus.moveFocus(false));
|
|
163
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 10L);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
TEST(focus_activating_nothing_is_not_a_click) {
|
|
167
|
+
Scene scene;
|
|
168
|
+
scene.mount({makeView(10, 0, true)});
|
|
169
|
+
// Nothing has focus yet, so there is nothing to activate -- and reporting a
|
|
170
|
+
// press with no target is how a keystroke reaches the wrong view.
|
|
171
|
+
EXPECT(!scene.focus.activateFocused());
|
|
172
|
+
|
|
173
|
+
scene.focus.moveFocus(true);
|
|
174
|
+
// No emitter is attached to these hand-built shadow views, so the click has
|
|
175
|
+
// nowhere to deliver. It must not crash: that is exactly the shape of Enter
|
|
176
|
+
// arriving between a Remove and its Delete.
|
|
177
|
+
EXPECT(scene.focus.activateFocused());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
TEST(focus_a_view_that_stops_being_accessible_leaves_the_tab_order) {
|
|
181
|
+
Scene scene;
|
|
182
|
+
scene.mount({makeView(10, 0, true), makeView(11, 50, true)});
|
|
183
|
+
|
|
184
|
+
ShadowViewMutationList updates;
|
|
185
|
+
updates.push_back(
|
|
186
|
+
ShadowViewMutation::UpdateMutation(makeView(10, 0, true), makeView(10, 0, false), kSurfaceId));
|
|
187
|
+
scene.manager.applyTransaction(
|
|
188
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 2, std::move(updates), TransactionTelemetry{}));
|
|
189
|
+
|
|
190
|
+
EXPECT(!rn_view_get_focusable(scene.manager.viewForTag(10)));
|
|
191
|
+
scene.focus.moveFocus(true);
|
|
192
|
+
// Tab now goes straight to 11, because 10 has left the chain.
|
|
193
|
+
EXPECT_EQ((long)scene.focus.focusedTag(), 11L);
|
|
194
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Tests for hit testing.
|
|
2
|
+
//
|
|
3
|
+
// This is the part of input most likely to be quietly wrong, and it is a pure
|
|
4
|
+
// function of the widget tree, so it can be tested without a gesture, a
|
|
5
|
+
// display server, or permission to synthesise a pointer event. That matters:
|
|
6
|
+
// synthesising a real click needs accessibility permission an automated run
|
|
7
|
+
// does not have, so everything below GDK is tested here instead.
|
|
8
|
+
|
|
9
|
+
#include "TestHarness.h"
|
|
10
|
+
|
|
11
|
+
#include "GtkMountingManager.h"
|
|
12
|
+
#include "GtkTouchDispatcher.h"
|
|
13
|
+
#include "RnView.h"
|
|
14
|
+
|
|
15
|
+
#include <sstream>
|
|
16
|
+
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
RnView *addChild(RnView *parent, int tag, float x, float y, float width, float height) {
|
|
20
|
+
RnView *child = rn_view_new(tag);
|
|
21
|
+
g_object_ref_sink(child);
|
|
22
|
+
rn_view_set_frame(child, x, y, width, height);
|
|
23
|
+
rn_view_insert_child(parent, child, 0);
|
|
24
|
+
return child;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// gtk_widget_pick skips widgets that are not mapped, and a widget is only
|
|
28
|
+
// mapped inside a window that has been shown. So these tests put the tree in a
|
|
29
|
+
// real window rather than allocating it in isolation the way the layout tests
|
|
30
|
+
// do -- which is also closer to what happens when a pointer actually arrives.
|
|
31
|
+
//
|
|
32
|
+
// Allocation happens in the frame clock's layout phase, not synchronously when
|
|
33
|
+
// a child is added, so every structural change is followed by a pump that
|
|
34
|
+
// spins the main loop until a frame has actually gone through. Draining the
|
|
35
|
+
// context without waiting is not enough: with no frame due, the iteration
|
|
36
|
+
// returns immediately and the child is still unallocated.
|
|
37
|
+
struct Scene {
|
|
38
|
+
GtkWidget *window;
|
|
39
|
+
RnView *root;
|
|
40
|
+
|
|
41
|
+
explicit Scene(int width, int height) {
|
|
42
|
+
root = rn_view_new(1);
|
|
43
|
+
window = gtk_window_new();
|
|
44
|
+
gtk_window_set_default_size(GTK_WINDOW(window), width, height);
|
|
45
|
+
gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
~Scene() { gtk_window_destroy(GTK_WINDOW(window)); }
|
|
49
|
+
|
|
50
|
+
Scene(const Scene &) = delete;
|
|
51
|
+
Scene &operator=(const Scene &) = delete;
|
|
52
|
+
|
|
53
|
+
// Call once the tree is built.
|
|
54
|
+
void show() {
|
|
55
|
+
gtk_widget_set_visible(window, TRUE);
|
|
56
|
+
pump();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Spins the main loop for long enough that a frame is drawn and the tree is
|
|
60
|
+
// allocated. Bounded, so a machine that never produces a frame fails the
|
|
61
|
+
// assertion rather than hanging the suite.
|
|
62
|
+
static void pump() {
|
|
63
|
+
const gint64 deadline = g_get_monotonic_time() + 500000; // 500ms
|
|
64
|
+
while (g_get_monotonic_time() < deadline) {
|
|
65
|
+
while (g_main_context_iteration(nullptr, FALSE)) {
|
|
66
|
+
}
|
|
67
|
+
g_usleep(1000);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
} // namespace
|
|
73
|
+
|
|
74
|
+
TEST(hit_test_finds_the_view_under_a_point) {
|
|
75
|
+
Scene scene(400, 400);
|
|
76
|
+
addChild(scene.root, 20, 10.0F, 10.0F, 100.0F, 100.0F);
|
|
77
|
+
scene.show();
|
|
78
|
+
|
|
79
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 20);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TEST(hit_test_misses_return_the_root_not_a_child) {
|
|
83
|
+
Scene scene(400, 400);
|
|
84
|
+
addChild(scene.root, 21, 10.0F, 10.0F, 50.0F, 50.0F);
|
|
85
|
+
scene.show();
|
|
86
|
+
|
|
87
|
+
// Outside the child but inside the root.
|
|
88
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 300.0, 300.0)), 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
TEST(hit_test_returns_the_deepest_view) {
|
|
92
|
+
Scene scene(400, 400);
|
|
93
|
+
RnView *outer = addChild(scene.root, 30, 0.0F, 0.0F, 200.0F, 200.0F);
|
|
94
|
+
addChild(outer, 31, 20.0F, 20.0F, 60.0F, 60.0F);
|
|
95
|
+
scene.show();
|
|
96
|
+
|
|
97
|
+
// Inside the inner view: the inner one wins.
|
|
98
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 31);
|
|
99
|
+
// Inside the outer view but outside the inner one.
|
|
100
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 150.0, 150.0)), 30);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
TEST(hit_test_respects_sibling_order) {
|
|
104
|
+
Scene scene(400, 400);
|
|
105
|
+
|
|
106
|
+
// Two overlapping siblings. GTK paints in child order, so the later one is on
|
|
107
|
+
// top and should win a hit in the overlap.
|
|
108
|
+
addChild(scene.root, 40, 0.0F, 0.0F, 100.0F, 100.0F);
|
|
109
|
+
RnView *second = rn_view_new(41);
|
|
110
|
+
g_object_ref_sink(second);
|
|
111
|
+
rn_view_set_frame(second, 50.0F, 50.0F, 100.0F, 100.0F);
|
|
112
|
+
rn_view_insert_child(scene.root, second, 1);
|
|
113
|
+
scene.show();
|
|
114
|
+
|
|
115
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 25.0, 25.0)), 40);
|
|
116
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 125.0, 125.0)), 41);
|
|
117
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 75.0, 75.0)), 41);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
TEST(hit_test_follows_a_scroll_offset) {
|
|
121
|
+
// The reason scrolling is expressed as an offset applied during allocation
|
|
122
|
+
// rather than as a paint-time translation: children really do move, so GTK's
|
|
123
|
+
// own picking follows them and hit testing needs no special case.
|
|
124
|
+
Scene scene(400, 400);
|
|
125
|
+
RnView *scroller = addChild(scene.root, 50, 0.0F, 0.0F, 200.0F, 200.0F);
|
|
126
|
+
addChild(scroller, 51, 0.0F, 300.0F, 200.0F, 100.0F);
|
|
127
|
+
scene.show();
|
|
128
|
+
|
|
129
|
+
// The row sits below the viewport, so nothing of it is under this point.
|
|
130
|
+
EXPECT(basalt::hitTestTag(scene.root, 100.0, 50.0) != 51);
|
|
131
|
+
|
|
132
|
+
rn_view_set_scroll_offset(scroller, 0.0, 300.0);
|
|
133
|
+
Scene::pump();
|
|
134
|
+
|
|
135
|
+
// Scrolled into view, the same point now lands on it.
|
|
136
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 100.0, 50.0)), 51);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
TEST(hit_test_on_a_null_root_is_a_miss) {
|
|
140
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(nullptr, 0.0, 0.0)), 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hover walks the same widget tree from the same pick, and has to survive
|
|
144
|
+
// everything the touch path does: a point that hits nothing, a view with no
|
|
145
|
+
// emitter, and the cursor leaving the surface. No emitter is attached to
|
|
146
|
+
// anything here, so nothing is delivered -- which is also what a cursor moving
|
|
147
|
+
// during a surface teardown looks like from inside the dispatcher.
|
|
148
|
+
TEST(a_synthesised_hover_walks_the_tree_without_an_emitter) {
|
|
149
|
+
basalt::GtkMountingManager manager;
|
|
150
|
+
RnView *root = manager.createSurfaceRoot(1);
|
|
151
|
+
|
|
152
|
+
GtkWidget *window = gtk_window_new();
|
|
153
|
+
gtk_window_set_default_size(GTK_WINDOW(window), 400, 400);
|
|
154
|
+
gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));
|
|
155
|
+
addChild(root, 30, 10.0F, 10.0F, 100.0F, 100.0F);
|
|
156
|
+
gtk_widget_set_visible(window, TRUE);
|
|
157
|
+
Scene::pump();
|
|
158
|
+
|
|
159
|
+
basalt::GtkTouchDispatcher dispatcher(&manager, root);
|
|
160
|
+
dispatcher.synthesiseHover(50.0, 50.0);
|
|
161
|
+
dispatcher.synthesiseHover(300.0, 300.0);
|
|
162
|
+
// Negative means the cursor left the surface.
|
|
163
|
+
dispatcher.synthesiseHover(-1.0, -1.0);
|
|
164
|
+
|
|
165
|
+
gtk_window_destroy(GTK_WINDOW(window));
|
|
166
|
+
manager.destroySurfaceRoot(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// `pointerEvents`, which decides what a press can land on rather than what is
|
|
170
|
+
// drawn. GTK expresses exactly one of the four values -- `none`, through
|
|
171
|
+
// can-target -- so these are also the tests for the resolution
|
|
172
|
+
// GtkTouchDispatcher does for the other two.
|
|
173
|
+
TEST(pointer_events_none_passes_a_press_through_to_what_is_behind) {
|
|
174
|
+
Scene scene(400, 400);
|
|
175
|
+
RnView *behind = addChild(scene.root, 60, 0.0F, 0.0F, 200.0F, 200.0F);
|
|
176
|
+
RnView *over = rn_view_new(61);
|
|
177
|
+
g_object_ref_sink(over);
|
|
178
|
+
rn_view_set_frame(over, 0.0F, 0.0F, 200.0F, 200.0F);
|
|
179
|
+
rn_view_insert_child(scene.root, over, 1);
|
|
180
|
+
scene.show();
|
|
181
|
+
(void)behind;
|
|
182
|
+
|
|
183
|
+
// Painted last, so it is on top and takes the press.
|
|
184
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 61);
|
|
185
|
+
|
|
186
|
+
rn_view_set_pointer_events(over, RN_POINTER_EVENTS_NONE);
|
|
187
|
+
Scene::pump();
|
|
188
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 60);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
TEST(pointer_events_none_takes_the_children_with_it) {
|
|
192
|
+
Scene scene(400, 400);
|
|
193
|
+
RnView *over = addChild(scene.root, 62, 0.0F, 0.0F, 200.0F, 200.0F);
|
|
194
|
+
addChild(over, 63, 0.0F, 0.0F, 100.0F, 100.0F);
|
|
195
|
+
scene.show();
|
|
196
|
+
|
|
197
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 63);
|
|
198
|
+
// The whole subtree leaves hit testing, not just the view the prop is on --
|
|
199
|
+
// which is GTK's own can-target semantics and the reason `none` needs no
|
|
200
|
+
// help from the dispatcher.
|
|
201
|
+
rn_view_set_pointer_events(over, RN_POINTER_EVENTS_NONE);
|
|
202
|
+
Scene::pump();
|
|
203
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
TEST(pointer_events_box_none_is_transparent_and_its_children_are_not) {
|
|
207
|
+
Scene scene(400, 400);
|
|
208
|
+
addChild(scene.root, 64, 0.0F, 0.0F, 300.0F, 300.0F);
|
|
209
|
+
RnView *overlay = rn_view_new(65);
|
|
210
|
+
g_object_ref_sink(overlay);
|
|
211
|
+
rn_view_set_frame(overlay, 0.0F, 0.0F, 300.0F, 300.0F);
|
|
212
|
+
rn_view_insert_child(scene.root, overlay, 1);
|
|
213
|
+
addChild(overlay, 66, 0.0F, 0.0F, 100.0F, 100.0F);
|
|
214
|
+
rn_view_set_pointer_events(overlay, RN_POINTER_EVENTS_BOX_NONE);
|
|
215
|
+
scene.show();
|
|
216
|
+
|
|
217
|
+
// Over the child: the overlay's children are still targets.
|
|
218
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 66);
|
|
219
|
+
// Over the overlay and nothing inside it: the press belongs to the view
|
|
220
|
+
// *behind*, not to the overlay's parent. Reaching that answer is what the
|
|
221
|
+
// re-pick in pickTarget is for; returning the parent would look right until
|
|
222
|
+
// something was underneath.
|
|
223
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 200.0, 200.0)), 64);
|
|
224
|
+
// And the view is targetable again afterwards -- the suppression lasts for
|
|
225
|
+
// one pick, not for good.
|
|
226
|
+
EXPECT(gtk_widget_get_can_target(GTK_WIDGET(overlay)));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
TEST(pointer_events_box_only_swallows_presses_meant_for_its_children) {
|
|
230
|
+
Scene scene(400, 400);
|
|
231
|
+
RnView *panel = addChild(scene.root, 67, 0.0F, 0.0F, 300.0F, 300.0F);
|
|
232
|
+
addChild(panel, 68, 0.0F, 0.0F, 100.0F, 100.0F);
|
|
233
|
+
scene.show();
|
|
234
|
+
|
|
235
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 68);
|
|
236
|
+
rn_view_set_pointer_events(panel, RN_POINTER_EVENTS_BOX_ONLY);
|
|
237
|
+
Scene::pump();
|
|
238
|
+
// The press lands on the panel even though it is over the child, which is
|
|
239
|
+
// what makes a disabled panel disable everything in it.
|
|
240
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 50.0, 50.0)), 67);
|
|
241
|
+
EXPECT_EQ(static_cast<int>(basalt::hitTestTag(scene.root, 200.0, 200.0)), 67);
|
|
242
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Tests for the image loader.
|
|
2
|
+
//
|
|
3
|
+
// A base64 data: URI keeps these self-contained: no file on disk, no network,
|
|
4
|
+
// no assets directory to keep in step. The loader answers synchronously for a
|
|
5
|
+
// cached URI and through the main loop otherwise, so the async cases pump the
|
|
6
|
+
// loop until the callback lands.
|
|
7
|
+
|
|
8
|
+
#include "TestHarness.h"
|
|
9
|
+
|
|
10
|
+
#include "GtkImageLoader.h"
|
|
11
|
+
|
|
12
|
+
#include <sstream>
|
|
13
|
+
#include <string>
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
|
|
17
|
+
// A 2x1 PNG: one red pixel, one blue. Small enough to inline, real enough to
|
|
18
|
+
// decode.
|
|
19
|
+
const char *kTinyPngBase64 =
|
|
20
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAADUlEQVR42mP4zwAE/wEHAAH/PX2MSQAAAABJRU5ErkJggg==";
|
|
21
|
+
|
|
22
|
+
std::string tinyPngUri() {
|
|
23
|
+
return std::string("data:image/png;base64,") + kTinyPngBase64;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Runs the main loop until `done`, or until a generous number of iterations
|
|
27
|
+
// have passed. Returns false on timeout so a hang fails rather than blocks.
|
|
28
|
+
bool pumpUntil(const bool &done) {
|
|
29
|
+
for (int i = 0; i < 20000 && !done; ++i) {
|
|
30
|
+
g_main_context_iteration(nullptr, FALSE);
|
|
31
|
+
g_usleep(500);
|
|
32
|
+
}
|
|
33
|
+
return done;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} // namespace
|
|
37
|
+
|
|
38
|
+
TEST(image_loader_decodes_a_data_uri) {
|
|
39
|
+
basalt::GtkImageLoader loader;
|
|
40
|
+
|
|
41
|
+
bool done = false;
|
|
42
|
+
GdkTexture *result = nullptr;
|
|
43
|
+
std::string error;
|
|
44
|
+
|
|
45
|
+
loader.load(tinyPngUri(), [&](GdkTexture *texture, const std::string &message) {
|
|
46
|
+
result = texture;
|
|
47
|
+
error = message;
|
|
48
|
+
done = true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
EXPECT(pumpUntil(done));
|
|
52
|
+
EXPECT(error.empty());
|
|
53
|
+
EXPECT(result != nullptr);
|
|
54
|
+
if (result != nullptr) {
|
|
55
|
+
EXPECT_EQ(gdk_texture_get_width(result), 2);
|
|
56
|
+
EXPECT_EQ(gdk_texture_get_height(result), 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
TEST(image_loader_serves_a_second_request_from_cache) {
|
|
61
|
+
basalt::GtkImageLoader loader;
|
|
62
|
+
|
|
63
|
+
bool first = false;
|
|
64
|
+
GdkTexture *firstTexture = nullptr;
|
|
65
|
+
loader.load(tinyPngUri(), [&](GdkTexture *texture, const std::string &) {
|
|
66
|
+
firstTexture = texture;
|
|
67
|
+
first = true;
|
|
68
|
+
});
|
|
69
|
+
EXPECT(pumpUntil(first));
|
|
70
|
+
|
|
71
|
+
// The second call must answer without going through the main loop at all,
|
|
72
|
+
// which is what stops an <Image> restarting its load on every mutation.
|
|
73
|
+
bool secondCalledSynchronously = false;
|
|
74
|
+
GdkTexture *secondTexture = nullptr;
|
|
75
|
+
loader.load(tinyPngUri(), [&](GdkTexture *texture, const std::string &) {
|
|
76
|
+
secondTexture = texture;
|
|
77
|
+
secondCalledSynchronously = true;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
EXPECT(secondCalledSynchronously);
|
|
81
|
+
EXPECT(secondTexture == firstTexture);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TEST(image_loader_reports_a_missing_file) {
|
|
85
|
+
basalt::GtkImageLoader loader;
|
|
86
|
+
|
|
87
|
+
bool done = false;
|
|
88
|
+
GdkTexture *result = nullptr;
|
|
89
|
+
std::string error;
|
|
90
|
+
|
|
91
|
+
loader.load("/definitely/not/a/real/image.png", [&](GdkTexture *texture, const std::string &message) {
|
|
92
|
+
result = texture;
|
|
93
|
+
error = message;
|
|
94
|
+
done = true;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
EXPECT(pumpUntil(done));
|
|
98
|
+
EXPECT(result == nullptr);
|
|
99
|
+
EXPECT(!error.empty());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
TEST(image_loader_rejects_an_empty_uri_without_touching_the_loop) {
|
|
103
|
+
basalt::GtkImageLoader loader;
|
|
104
|
+
|
|
105
|
+
bool called = false;
|
|
106
|
+
std::string error;
|
|
107
|
+
loader.load("", [&](GdkTexture *texture, const std::string &message) {
|
|
108
|
+
EXPECT(texture == nullptr);
|
|
109
|
+
error = message;
|
|
110
|
+
called = true;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
EXPECT(called);
|
|
114
|
+
EXPECT(!error.empty());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
TEST(image_loader_reports_undecodable_bytes) {
|
|
118
|
+
basalt::GtkImageLoader loader;
|
|
119
|
+
|
|
120
|
+
bool done = false;
|
|
121
|
+
GdkTexture *result = nullptr;
|
|
122
|
+
std::string error;
|
|
123
|
+
|
|
124
|
+
// Valid base64, not a valid image.
|
|
125
|
+
loader.load("data:image/png;base64,bm90YW5pbWFnZQ==",
|
|
126
|
+
[&](GdkTexture *texture, const std::string &message) {
|
|
127
|
+
result = texture;
|
|
128
|
+
error = message;
|
|
129
|
+
done = true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
EXPECT(pumpUntil(done));
|
|
133
|
+
EXPECT(result == nullptr);
|
|
134
|
+
EXPECT(!error.empty());
|
|
135
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// A GDK key press as a key combination.
|
|
2
|
+
//
|
|
3
|
+
// The same ten questions appkit/tests/test_appkit_key_events.mm asks, because the
|
|
4
|
+
// answers have to agree: an app that binds "ArrowLeft" or metaKey must get the
|
|
5
|
+
// same key on both desktops. Where the two hosts differ is what they start from
|
|
6
|
+
// -- a keyval here, an NSEvent there -- and that is the whole reason the names
|
|
7
|
+
// are constants in core rather than literals in each.
|
|
8
|
+
|
|
9
|
+
#include "TestHarness.h"
|
|
10
|
+
|
|
11
|
+
#include "GtkKeyEvents.h"
|
|
12
|
+
|
|
13
|
+
#include <sstream>
|
|
14
|
+
#include <string>
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
std::string keyOf(guint keyval, GdkModifierType state = static_cast<GdkModifierType>(0)) {
|
|
19
|
+
const auto combination = basalt::keyCombinationFrom(keyval, state);
|
|
20
|
+
return combination.has_value() ? combination->key : std::string("(none)");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
basalt::KeyModifiers modifiersOf(guint keyval, GdkModifierType state) {
|
|
24
|
+
const auto combination = basalt::keyCombinationFrom(keyval, state);
|
|
25
|
+
return combination.has_value() ? combination->modifiers : basalt::KeyModifiers{};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
} // namespace
|
|
29
|
+
|
|
30
|
+
TEST(gtk_keys_a_plain_letter_is_itself) {
|
|
31
|
+
EXPECT_EQ(keyOf(GDK_KEY_a), std::string("a"));
|
|
32
|
+
EXPECT(modifiersOf(GDK_KEY_a, static_cast<GdkModifierType>(0)) == basalt::KeyModifiers{});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
TEST(gtk_keys_case_comes_from_the_keyval) {
|
|
36
|
+
// GDK gives Shift+A its own keyval, so the case is already right and nothing
|
|
37
|
+
// here has to apply a modifier to get it. AppKit needs `characters` for the
|
|
38
|
+
// same result, which is the difference between the two hosts in one line.
|
|
39
|
+
EXPECT_EQ(keyOf(GDK_KEY_A, GDK_SHIFT_MASK), std::string("A"));
|
|
40
|
+
EXPECT(modifiersOf(GDK_KEY_A, GDK_SHIFT_MASK).shift);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
TEST(gtk_keys_super_is_meta_and_not_control) {
|
|
44
|
+
// W3C names the Windows and Command keys Meta, and Super is the key in that
|
|
45
|
+
// position here -- so an app binding metaKey presses the same physical key on
|
|
46
|
+
// all three desktops.
|
|
47
|
+
const auto modifiers = modifiersOf(GDK_KEY_z, GDK_SUPER_MASK);
|
|
48
|
+
EXPECT(modifiers.meta);
|
|
49
|
+
EXPECT(!modifiers.ctrl);
|
|
50
|
+
EXPECT_EQ(keyOf(GDK_KEY_z, GDK_SUPER_MASK), std::string("z"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
TEST(gtk_keys_control_is_control) {
|
|
54
|
+
const auto modifiers = modifiersOf(GDK_KEY_a, GDK_CONTROL_MASK);
|
|
55
|
+
EXPECT(modifiers.ctrl);
|
|
56
|
+
EXPECT(!modifiers.meta);
|
|
57
|
+
// And the letter, not a control character: a keyval is the key rather than
|
|
58
|
+
// what it would insert, so this comes out right without the correction the
|
|
59
|
+
// AppKit host needs.
|
|
60
|
+
EXPECT_EQ(keyOf(GDK_KEY_a, GDK_CONTROL_MASK), std::string("a"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
TEST(gtk_keys_the_arrows_have_browser_names) {
|
|
64
|
+
EXPECT_EQ(keyOf(GDK_KEY_Left), std::string("ArrowLeft"));
|
|
65
|
+
EXPECT_EQ(keyOf(GDK_KEY_Right), std::string("ArrowRight"));
|
|
66
|
+
EXPECT_EQ(keyOf(GDK_KEY_Up), std::string("ArrowUp"));
|
|
67
|
+
EXPECT_EQ(keyOf(GDK_KEY_Down), std::string("ArrowDown"));
|
|
68
|
+
// The keypad's arrows are the same keys to an app. A person with Num Lock off
|
|
69
|
+
// pressing the 4 key means left.
|
|
70
|
+
EXPECT_EQ(keyOf(GDK_KEY_KP_Left), std::string("ArrowLeft"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST(gtk_keys_the_navigation_block_has_browser_names) {
|
|
74
|
+
EXPECT_EQ(keyOf(GDK_KEY_Page_Up), std::string("PageUp"));
|
|
75
|
+
EXPECT_EQ(keyOf(GDK_KEY_Home), std::string("Home"));
|
|
76
|
+
EXPECT_EQ(keyOf(GDK_KEY_End), std::string("End"));
|
|
77
|
+
// Forward delete against backspace, which is the pair most easily conflated.
|
|
78
|
+
EXPECT_EQ(keyOf(GDK_KEY_Delete), std::string("Delete"));
|
|
79
|
+
EXPECT_EQ(keyOf(GDK_KEY_BackSpace), std::string("Backspace"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TEST(gtk_keys_escape_enter_tab_and_space) {
|
|
83
|
+
EXPECT_EQ(keyOf(GDK_KEY_Escape), std::string("Escape"));
|
|
84
|
+
EXPECT_EQ(keyOf(GDK_KEY_Return), std::string("Enter"));
|
|
85
|
+
EXPECT_EQ(keyOf(GDK_KEY_KP_Enter), std::string("Enter"));
|
|
86
|
+
EXPECT_EQ(keyOf(GDK_KEY_Tab), std::string("Tab"));
|
|
87
|
+
// A space is a character, named " ". gdk_keyval_to_unicode gives it directly,
|
|
88
|
+
// so this is the same answer by a different route than AppKit's.
|
|
89
|
+
EXPECT_EQ(keyOf(GDK_KEY_space), std::string(" "));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
TEST(gtk_keys_shift_tab_is_tab_with_shift) {
|
|
93
|
+
// Its own keyval here, as back-tab is its own character on AppKit.
|
|
94
|
+
EXPECT_EQ(keyOf(GDK_KEY_ISO_Left_Tab, GDK_SHIFT_MASK), std::string("Tab"));
|
|
95
|
+
EXPECT(modifiersOf(GDK_KEY_ISO_Left_Tab, GDK_SHIFT_MASK).shift);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
TEST(gtk_keys_a_bare_modifier_is_not_a_combination) {
|
|
99
|
+
// Pressing Control on its own arrives as a keyval with no character. Offering
|
|
100
|
+
// it as a combination would fire a shortcut on the modifier alone.
|
|
101
|
+
EXPECT_EQ(keyOf(GDK_KEY_Control_L, GDK_CONTROL_MASK), std::string("(none)"));
|
|
102
|
+
EXPECT_EQ(keyOf(GDK_KEY_Shift_L, GDK_SHIFT_MASK), std::string("(none)"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
TEST(gtk_keys_several_modifiers_at_once) {
|
|
106
|
+
const auto modifiers =
|
|
107
|
+
modifiersOf(GDK_KEY_z, static_cast<GdkModifierType>(GDK_SUPER_MASK | GDK_SHIFT_MASK));
|
|
108
|
+
EXPECT(modifiers.meta);
|
|
109
|
+
EXPECT(modifiers.shift);
|
|
110
|
+
EXPECT(!modifiers.alt);
|
|
111
|
+
EXPECT(!modifiers.ctrl);
|
|
112
|
+
}
|