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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. 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
+ }