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,1407 @@
1
+ #include "RnView.h"
2
+
3
+ #include "Backface.h"
4
+ #include "ScrollIndicator.h"
5
+
6
+ #include "ControlMetrics.h"
7
+ #include "FocusRing.h"
8
+ #include "GtkTextPeer.h"
9
+
10
+ #include <cstring>
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // RnLayout
14
+ // ---------------------------------------------------------------------------
15
+
16
+ struct _RnLayout {
17
+ GtkLayoutManager parent_instance;
18
+ };
19
+
20
+ G_DEFINE_TYPE(RnLayout, rn_layout, GTK_TYPE_LAYOUT_MANAGER)
21
+
22
+ static void rn_layout_measure(GtkLayoutManager * /*manager*/,
23
+ GtkWidget * /*widget*/,
24
+ GtkOrientation /*orientation*/,
25
+ int /*for_size*/,
26
+ int *minimum,
27
+ int *natural,
28
+ int *minimum_baseline,
29
+ int *natural_baseline) {
30
+ // RN owns sizing. Reporting zero keeps GTK from ever second-guessing Yoga.
31
+ *minimum = 0;
32
+ *natural = 0;
33
+ *minimum_baseline = -1;
34
+ *natural_baseline = -1;
35
+ }
36
+
37
+ // Defined below; lets allocate report the root's size without exposing the
38
+ // struct.
39
+ static void rn_view_notify_allocation(RnView *self, int width, int height);
40
+ static void rn_view_scroll_offset(RnView *self, double *offset_x, double *offset_y);
41
+ // Defined below, for the same reason: allocate runs before the struct is
42
+ // complete, so it reaches the fields through accessors.
43
+ static gboolean rn_view_layout_transform(RnView *self, graphene_matrix_t *out);
44
+ static int rn_view_layout_z_index(RnView *self);
45
+
46
+ static void rn_layout_allocate(GtkLayoutManager * /*manager*/,
47
+ GtkWidget *widget,
48
+ int width,
49
+ int height,
50
+ int /*baseline*/) {
51
+ // A scrolling view moves its children rather than its own frame. Reading the
52
+ // offset here, once, keeps it out of every child's stored frame.
53
+ double scroll_x = 0.0;
54
+ double scroll_y = 0.0;
55
+ if (RN_IS_VIEW(widget)) {
56
+ rn_view_scroll_offset(RN_VIEW(widget), &scroll_x, &scroll_y);
57
+ }
58
+
59
+ for (GtkWidget *child = gtk_widget_get_first_child(widget); child != nullptr;
60
+ child = gtk_widget_get_next_sibling(child)) {
61
+ if (!gtk_widget_should_layout(child)) {
62
+ continue;
63
+ }
64
+
65
+ // A native peer -- the GtkText inside a <TextInput> -- is not an RnView and
66
+ // has no frame of its own. It fills the view that owns it.
67
+ if (!RN_IS_VIEW(child)) {
68
+ GtkBorder insets = {0, 0, 0, 0};
69
+ if (RN_IS_VIEW(widget)) {
70
+ rn_view_get_peer_insets(RN_VIEW(widget), &insets);
71
+ }
72
+ // Clamped: a field narrower than its own padding would otherwise be
73
+ // allocated a negative size, which GTK treats as an error.
74
+ const int inner_width = MAX(width - insets.left - insets.right, 0);
75
+ const int inner_height = MAX(height - insets.top - insets.bottom, 0);
76
+
77
+ int peer_width = inner_width;
78
+ int peer_height = inner_height;
79
+ int peer_x = insets.left;
80
+ int peer_y = insets.top;
81
+
82
+ // A control keeps its own shape and is centred; only the text peer
83
+ // fills. A GtkSwitch stretched to whatever box the app gave it is a
84
+ // rectangle with a circle in it, and a GtkSpinner stretched is an
85
+ // ellipse -- neither of which any other React Native platform draws.
86
+ // `halign` would say this declaratively, and does nothing here: this
87
+ // layout manager allocates children directly rather than measuring them.
88
+ if (RN_IS_VIEW(widget) && rn_view_get_control(RN_VIEW(widget)) == child) {
89
+ int natural_width = 0;
90
+ int natural_height = 0;
91
+ gtk_widget_measure(child, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &natural_width, nullptr, nullptr);
92
+ gtk_widget_measure(child, GTK_ORIENTATION_VERTICAL, natural_width, nullptr, &natural_height, nullptr, nullptr);
93
+ peer_width = MIN(natural_width, inner_width);
94
+ peer_height = MIN(natural_height, inner_height);
95
+ peer_x += (inner_width - peer_width) / 2;
96
+ peer_y += (inner_height - peer_height) / 2;
97
+ }
98
+
99
+ graphene_point_t offset;
100
+ offset.x = static_cast<float>(peer_x);
101
+ offset.y = static_cast<float>(peer_y);
102
+ GskTransform *peer_transform = gsk_transform_translate(nullptr, &offset);
103
+ gtk_widget_allocate(child, peer_width, peer_height, -1, peer_transform);
104
+ continue;
105
+ }
106
+
107
+ graphene_rect_t frame;
108
+ rn_view_get_frame(RN_VIEW(child), &frame);
109
+
110
+ graphene_point_t origin;
111
+ origin.x = frame.origin.x - static_cast<float>(scroll_x);
112
+ origin.y = frame.origin.y - static_cast<float>(scroll_y);
113
+
114
+ GskTransform *transform = gsk_transform_translate(nullptr, &origin);
115
+
116
+ // A transform is anchored on the view's centre, which is where every other
117
+ // React Native platform anchors it and what transformOrigin is measured
118
+ // from. Composing it here rather than in snapshot() is what makes
119
+ // gtk_widget_pick follow it, so a transformed view is hit where it looks.
120
+ graphene_matrix_t matrix;
121
+ if (rn_view_layout_transform(RN_VIEW(child), &matrix)) {
122
+ graphene_point_t centre;
123
+ centre.x = frame.size.width / 2.0f;
124
+ centre.y = frame.size.height / 2.0f;
125
+ graphene_point_t back;
126
+ back.x = -centre.x;
127
+ back.y = -centre.y;
128
+
129
+ transform = gsk_transform_translate(transform, &centre);
130
+ transform = gsk_transform_matrix(transform, &matrix);
131
+ transform = gsk_transform_translate(transform, &back);
132
+ }
133
+
134
+ gtk_widget_allocate(child,
135
+ static_cast<int>(frame.size.width),
136
+ static_cast<int>(frame.size.height),
137
+ -1,
138
+ transform);
139
+ }
140
+
141
+ // Children are placed from frames RN already decided, so this is not part of
142
+ // laying out; it is how the widget tells the host what size the window gave
143
+ // it. On a surface root that becomes the next layout constraint.
144
+ if (RN_IS_VIEW(widget)) {
145
+ rn_view_notify_allocation(RN_VIEW(widget), width, height);
146
+ }
147
+ }
148
+
149
+ static void rn_layout_class_init(RnLayoutClass *klass) {
150
+ GtkLayoutManagerClass *layout_class = GTK_LAYOUT_MANAGER_CLASS(klass);
151
+ layout_class->measure = rn_layout_measure;
152
+ layout_class->allocate = rn_layout_allocate;
153
+ }
154
+
155
+ static void rn_layout_init(RnLayout * /*self*/) {}
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // RnView
159
+ // ---------------------------------------------------------------------------
160
+
161
+ struct _RnView {
162
+ GtkWidget parent_instance;
163
+
164
+ int tag;
165
+ graphene_rect_t frame;
166
+
167
+ gboolean has_background_color;
168
+ GdkRGBA background_color;
169
+ double opacity;
170
+
171
+ PangoLayout *text_layout;
172
+ GdkRGBA text_color;
173
+
174
+ GdkTexture *texture;
175
+ RnImageFit texture_fit;
176
+ double indicator_v_offset;
177
+ double indicator_v_length;
178
+ double indicator_h_offset;
179
+ double indicator_h_length;
180
+ gboolean has_image_tint;
181
+ GdkRGBA image_tint;
182
+ char *role_name;
183
+ // The app's `nativeID`. Read only by the hidden title bar's drag-region hit
184
+ // test; see RnView.h and core/TitleBarRegions.h.
185
+ char *native_id;
186
+
187
+ gboolean clips_children;
188
+ double scroll_x;
189
+ double scroll_y;
190
+
191
+ graphene_size_t border_radii[4];
192
+ gboolean has_border_radii;
193
+ float border_widths[4];
194
+ GdkRGBA border_colors[4];
195
+ gboolean has_borders;
196
+
197
+ graphene_matrix_t transform;
198
+ gboolean has_transform;
199
+ gboolean hides_back_face;
200
+ gboolean hidden_by_app;
201
+ gboolean hidden_by_back_face;
202
+
203
+ int z_index;
204
+
205
+ RnPointerEvents pointer_events;
206
+ gboolean focusable;
207
+
208
+ // Border plus padding, for the native peer below. React Native calls this a
209
+ // content inset and Yoga has already resolved it; a GtkText knows nothing
210
+ // about either and would otherwise sit flush against the border.
211
+ GtkBorder peer_insets;
212
+
213
+ // The <TextInput> peer when this view is one, otherwise NULL: a GtkText for
214
+ // a single-line field and a GtkTextView for a multiline one. Borrowed -- the
215
+ // widget owns it once parented. See GtkTextPeer.h for why it is a GtkWidget
216
+ // rather than either concrete type.
217
+ GtkWidget *editable;
218
+
219
+ // The toolkit control this view stands for -- a GtkSpinner or a GtkSwitch --
220
+ // or NULL. Parented and allocated exactly like `editable` above, and never
221
+ // at the same time as one: a <TextInput> is not a <Switch>.
222
+ GtkWidget *control;
223
+ RnControlKind control_kind;
224
+ gboolean control_disabled;
225
+ // What the tree dump prints for it, written by core/DesktopControls.h.
226
+ char *control_description;
227
+
228
+ // React DevTools' overlay rectangles: eight floats each, plus a fill flag.
229
+ // See rn_view_set_highlights.
230
+ GArray *highlights;
231
+ GArray *highlight_filled;
232
+
233
+ RnViewResizeFunc resize_callback;
234
+ gpointer resize_data;
235
+ int allocated_width;
236
+ int allocated_height;
237
+ };
238
+
239
+ static void rn_view_notify_allocation(RnView *self, int width, int height) {
240
+ if (self->resize_callback == nullptr) {
241
+ return;
242
+ }
243
+ // allocate runs on every layout pass, most of which change nothing. Only a
244
+ // real size change is worth a Fabric commit.
245
+ if (width == self->allocated_width && height == self->allocated_height) {
246
+ return;
247
+ }
248
+ self->allocated_width = width;
249
+ self->allocated_height = height;
250
+ self->resize_callback(self, width, height, self->resize_data);
251
+ }
252
+
253
+ void rn_view_set_resize_callback(RnView *self, RnViewResizeFunc callback, gpointer user_data) {
254
+ g_return_if_fail(RN_IS_VIEW(self));
255
+ self->resize_callback = callback;
256
+ self->resize_data = user_data;
257
+ }
258
+
259
+ G_DEFINE_TYPE(RnView, rn_view, GTK_TYPE_WIDGET)
260
+
261
+ static void rn_view_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) {
262
+ RnView *self = RN_VIEW(widget);
263
+
264
+ const int width = gtk_widget_get_width(widget);
265
+ const int height = gtk_widget_get_height(widget);
266
+
267
+ const gboolean needs_opacity_layer = self->opacity < 1.0;
268
+ if (needs_opacity_layer) {
269
+ gtk_snapshot_push_opacity(snapshot, self->opacity);
270
+ }
271
+
272
+ graphene_rect_t bounds;
273
+ bounds.origin.x = 0.0f;
274
+ bounds.origin.y = 0.0f;
275
+ bounds.size.width = static_cast<float>(width);
276
+ bounds.size.height = static_cast<float>(height);
277
+
278
+ GskRoundedRect box;
279
+ gsk_rounded_rect_init(&box,
280
+ &bounds,
281
+ &self->border_radii[0],
282
+ &self->border_radii[1],
283
+ &self->border_radii[2],
284
+ &self->border_radii[3]);
285
+
286
+ // The background is always clipped to the rounded box, even when children are
287
+ // not: overflow: 'visible' lets a child escape the corner, but the view's own
288
+ // fill still has to respect its border radius.
289
+ if (self->has_background_color) {
290
+ if (self->has_border_radii) {
291
+ gtk_snapshot_push_rounded_clip(snapshot, &box);
292
+ }
293
+ gtk_snapshot_append_color(snapshot, &self->background_color, &bounds);
294
+ if (self->has_border_radii) {
295
+ gtk_snapshot_pop(snapshot);
296
+ }
297
+ }
298
+
299
+ // Content clipping is separate, and only happens with overflow: 'hidden'.
300
+ if (self->clips_children) {
301
+ if (self->has_border_radii) {
302
+ gtk_snapshot_push_rounded_clip(snapshot, &box);
303
+ } else {
304
+ gtk_snapshot_push_clip(snapshot, &bounds);
305
+ }
306
+ }
307
+
308
+ if (self->texture != nullptr && width > 0 && height > 0) {
309
+ const float viewWidth = static_cast<float>(width);
310
+ const float viewHeight = static_cast<float>(height);
311
+ const float imageWidth = static_cast<float>(gdk_texture_get_width(self->texture));
312
+ const float imageHeight = static_cast<float>(gdk_texture_get_height(self->texture));
313
+
314
+ graphene_rect_t destination;
315
+ destination.origin.x = 0.0f;
316
+ destination.origin.y = 0.0f;
317
+ destination.size.width = viewWidth;
318
+ destination.size.height = viewHeight;
319
+
320
+ if (self->texture_fit != RN_IMAGE_FIT_STRETCH && imageWidth > 0 && imageHeight > 0) {
321
+ float scale = 1.0f;
322
+ switch (self->texture_fit) {
323
+ case RN_IMAGE_FIT_CONTAIN:
324
+ scale = MIN(viewWidth / imageWidth, viewHeight / imageHeight);
325
+ break;
326
+ case RN_IMAGE_FIT_COVER:
327
+ scale = MAX(viewWidth / imageWidth, viewHeight / imageHeight);
328
+ break;
329
+ case RN_IMAGE_FIT_CENTER:
330
+ // Centre at natural size, but never larger than the frame -- which is
331
+ // what React Native's `center` does.
332
+ scale = MIN(1.0f, MIN(viewWidth / imageWidth, viewHeight / imageHeight));
333
+ break;
334
+ case RN_IMAGE_FIT_STRETCH:
335
+ break;
336
+ }
337
+ destination.size.width = imageWidth * scale;
338
+ destination.size.height = imageHeight * scale;
339
+ destination.origin.x = (viewWidth - destination.size.width) / 2.0f;
340
+ destination.origin.y = (viewHeight - destination.size.height) / 2.0f;
341
+ }
342
+
343
+ // cover and center can put pixels outside the frame, and an <Image> never
344
+ // paints beyond its own box on iOS or Android.
345
+ const gboolean needs_clip = self->texture_fit == RN_IMAGE_FIT_COVER ||
346
+ self->texture_fit == RN_IMAGE_FIT_CENTER;
347
+ if (needs_clip) {
348
+ graphene_rect_t clip;
349
+ clip.origin.x = 0.0f;
350
+ clip.origin.y = 0.0f;
351
+ clip.size.width = viewWidth;
352
+ clip.size.height = viewHeight;
353
+ gtk_snapshot_push_clip(snapshot, &clip);
354
+ }
355
+ // Tiling wraps everything below it: `gtk_snapshot_push_repeat` takes the
356
+ // area to fill and the rect of one tile, and repeats whatever is recorded
357
+ // until the pop. So the tint's mask node works inside it unchanged.
358
+ const gboolean tiles = self->texture_fit == RN_IMAGE_FIT_REPEAT;
359
+ if (tiles) {
360
+ graphene_rect_t area;
361
+ area.origin.x = 0.0f;
362
+ area.origin.y = 0.0f;
363
+ area.size.width = viewWidth;
364
+ area.size.height = viewHeight;
365
+ gtk_snapshot_push_repeat(snapshot, &area, &destination);
366
+ }
367
+ if (self->has_image_tint) {
368
+ // The image becomes a stencil and the colour is what is actually drawn.
369
+ // GskMaskNode records the mask first and the source second, which is why
370
+ // the texture is appended before the colour and there are two pops.
371
+ //
372
+ // Alpha rather than luminance: `tintColor` recolours a silhouette, so
373
+ // what matters is where the image is opaque, not how bright it is.
374
+ gtk_snapshot_push_mask(snapshot, GSK_MASK_MODE_ALPHA);
375
+ gtk_snapshot_append_texture(snapshot, self->texture, &destination);
376
+ gtk_snapshot_pop(snapshot);
377
+ gtk_snapshot_append_color(snapshot, &self->image_tint, &destination);
378
+ gtk_snapshot_pop(snapshot);
379
+ } else {
380
+ gtk_snapshot_append_texture(snapshot, self->texture, &destination);
381
+ }
382
+ if (tiles) {
383
+ gtk_snapshot_pop(snapshot);
384
+ }
385
+ if (needs_clip) {
386
+ gtk_snapshot_pop(snapshot);
387
+ }
388
+ }
389
+
390
+ // Text sits above the background and below any children, which is the order
391
+ // <Text> with nested views expects.
392
+ if (self->text_layout != nullptr) {
393
+ gtk_snapshot_append_layout(snapshot, self->text_layout, &self->text_color);
394
+ }
395
+
396
+ // zIndex only reorders painting. The child list itself stays in mutation
397
+ // order, because Fabric's Insert and Remove index into it.
398
+ gboolean needs_sorting = FALSE;
399
+ for (GtkWidget *child = gtk_widget_get_first_child(widget); child != nullptr;
400
+ child = gtk_widget_get_next_sibling(child)) {
401
+ if (RN_IS_VIEW(child) && rn_view_layout_z_index(RN_VIEW(child)) != 0) {
402
+ needs_sorting = TRUE;
403
+ break;
404
+ }
405
+ }
406
+
407
+ if (!needs_sorting) {
408
+ for (GtkWidget *child = gtk_widget_get_first_child(widget); child != nullptr;
409
+ child = gtk_widget_get_next_sibling(child)) {
410
+ gtk_widget_snapshot_child(widget, child, snapshot);
411
+ }
412
+ } else {
413
+ GPtrArray *ordered = g_ptr_array_new();
414
+ for (GtkWidget *child = gtk_widget_get_first_child(widget); child != nullptr;
415
+ child = gtk_widget_get_next_sibling(child)) {
416
+ g_ptr_array_add(ordered, child);
417
+ }
418
+ // A stable sort, so equal zIndex keeps document order -- which is what CSS
419
+ // and React Native both promise.
420
+ g_ptr_array_sort_values(ordered, [](gconstpointer a, gconstpointer b) -> int {
421
+ auto *wa = static_cast<GtkWidget *>(const_cast<gpointer>(a));
422
+ auto *wb = static_cast<GtkWidget *>(const_cast<gpointer>(b));
423
+ const int za = RN_IS_VIEW(wa) ? rn_view_layout_z_index(RN_VIEW(wa)) : 0;
424
+ const int zb = RN_IS_VIEW(wb) ? rn_view_layout_z_index(RN_VIEW(wb)) : 0;
425
+ return za - zb;
426
+ });
427
+ for (guint i = 0; i < ordered->len; i++) {
428
+ gtk_widget_snapshot_child(widget, GTK_WIDGET(g_ptr_array_index(ordered, i)), snapshot);
429
+ }
430
+ g_ptr_array_free(ordered, TRUE);
431
+ }
432
+
433
+ if (self->clips_children) {
434
+ gtk_snapshot_pop(snapshot);
435
+ }
436
+
437
+ // The scrollbars: an overlay, so above the content and above any children,
438
+ // which is what "overlay indicator" means -- and below the border, the
439
+ // DevTools overlay and the focus ring, none of which belongs to the app.
440
+ if (self->indicator_v_length > 0.0 || self->indicator_h_length > 0.0) {
441
+ const float thickness = static_cast<float>(basalt::kScrollIndicatorThickness);
442
+ const float inset = static_cast<float>(basalt::kScrollIndicatorInset);
443
+ // Neutral and translucent, so it reads over light and dark content alike.
444
+ const GdkRGBA thumb{0.0F, 0.0F, 0.0F, 0.35F};
445
+
446
+ if (self->indicator_v_length > 0.0) {
447
+ const graphene_rect_t bar =
448
+ GRAPHENE_RECT_INIT(static_cast<float>(width) - thickness - inset,
449
+ static_cast<float>(self->indicator_v_offset),
450
+ thickness,
451
+ static_cast<float>(self->indicator_v_length));
452
+ GskRoundedRect rounded;
453
+ gsk_rounded_rect_init_from_rect(&rounded, &bar, thickness / 2.0F);
454
+ gtk_snapshot_push_rounded_clip(snapshot, &rounded);
455
+ gtk_snapshot_append_color(snapshot, &thumb, &bar);
456
+ gtk_snapshot_pop(snapshot);
457
+ }
458
+ if (self->indicator_h_length > 0.0) {
459
+ const graphene_rect_t bar =
460
+ GRAPHENE_RECT_INIT(static_cast<float>(self->indicator_h_offset),
461
+ static_cast<float>(height) - thickness - inset,
462
+ static_cast<float>(self->indicator_h_length),
463
+ thickness);
464
+ GskRoundedRect rounded;
465
+ gsk_rounded_rect_init_from_rect(&rounded, &bar, thickness / 2.0F);
466
+ gtk_snapshot_push_rounded_clip(snapshot, &rounded);
467
+ gtk_snapshot_append_color(snapshot, &thumb, &bar);
468
+ gtk_snapshot_pop(snapshot);
469
+ }
470
+ }
471
+
472
+ // Borders paint over the content, as they do on every other platform.
473
+ if (self->has_borders) {
474
+ gtk_snapshot_append_border(snapshot, &box, self->border_widths, self->border_colors);
475
+ }
476
+
477
+ // React DevTools' overlay, over everything including the border. Above the
478
+ // app on purpose: it is not part of it, and an inspected element half hidden
479
+ // behind a card would be pointing at the wrong thing.
480
+ if (self->highlights != nullptr) {
481
+ const auto *values = &g_array_index(self->highlights, float, 0);
482
+ const auto *filled = &g_array_index(self->highlight_filled, gboolean, 0);
483
+ const guint count = self->highlight_filled->len;
484
+ for (guint i = 0; i < count; i++) {
485
+ const float *rectangle = values + (i * 8);
486
+ graphene_rect_t area;
487
+ area.origin.x = rectangle[0];
488
+ area.origin.y = rectangle[1];
489
+ area.size.width = rectangle[2];
490
+ area.size.height = rectangle[3];
491
+ const GdkRGBA colour{rectangle[4], rectangle[5], rectangle[6], rectangle[7]};
492
+
493
+ if (filled[i]) {
494
+ gtk_snapshot_append_color(snapshot, &colour, &area);
495
+ }
496
+ GskRoundedRect outline;
497
+ gsk_rounded_rect_init_from_rect(&outline, &area, 0.0f);
498
+ const float widths[4] = {basalt::kHighlightBorderWidth,
499
+ basalt::kHighlightBorderWidth,
500
+ basalt::kHighlightBorderWidth,
501
+ basalt::kHighlightBorderWidth};
502
+ // Opaque on the outline even when the fill is not, so the edge of an
503
+ // inspected element is a line rather than a suggestion.
504
+ const GdkRGBA edge{rectangle[4], rectangle[5], rectangle[6], 1.0f};
505
+ const GdkRGBA colors[4] = {edge, edge, edge, edge};
506
+ gtk_snapshot_append_border(snapshot, &outline, widths, colors);
507
+ }
508
+ }
509
+
510
+ // The focus ring, over everything including the border, because it is the
511
+ // answer to "where am I" and must not be hidden by what it is drawn on.
512
+ //
513
+ // Drawn rather than delegated: GTK's own focus rendering is a CSS outline on
514
+ // a themed widget, and these widgets have no theme -- React Native decided
515
+ // every colour here. The ring follows the view's own corner radii so it hugs
516
+ // a rounded button, and `has-visible-focus` is what keeps it from appearing
517
+ // when focus was moved by a click rather than by the keyboard.
518
+ if (gtk_widget_has_visible_focus(widget)) {
519
+ // Inside the view's own bounds rather than around them. The AppKit side
520
+ // draws its ring in a drawRect: that is clipped to the view, so a ring that
521
+ // sat outside here and inside there would be a difference an app did not
522
+ // ask for -- and one that would only show up as a pixel diff.
523
+ GskRoundedRect ring = box;
524
+ const float widths[4] = {basalt::kFocusRingWidth,
525
+ basalt::kFocusRingWidth,
526
+ basalt::kFocusRingWidth,
527
+ basalt::kFocusRingWidth};
528
+ const GdkRGBA ringColor{basalt::kFocusRingRed,
529
+ basalt::kFocusRingGreen,
530
+ basalt::kFocusRingBlue,
531
+ basalt::kFocusRingAlpha};
532
+ const GdkRGBA colors[4] = {ringColor, ringColor, ringColor, ringColor};
533
+ gtk_snapshot_append_border(snapshot, &ring, widths, colors);
534
+ }
535
+
536
+ if (needs_opacity_layer) {
537
+ gtk_snapshot_pop(snapshot);
538
+ }
539
+ }
540
+
541
+ static void rn_view_dispose(GObject *object) {
542
+ RnView *self = RN_VIEW(object);
543
+ GtkWidget *widget = GTK_WIDGET(object);
544
+
545
+ // A GtkWidget must unparent its children before it goes away, or GTK warns
546
+ // and leaks. Delete mutations can arrive with children still attached.
547
+ GtkWidget *child = gtk_widget_get_first_child(widget);
548
+ while (child != nullptr) {
549
+ GtkWidget *next = gtk_widget_get_next_sibling(child);
550
+ gtk_widget_unparent(child);
551
+ child = next;
552
+ }
553
+
554
+ // The generic loop above already unparented them, so this only drops the
555
+ // borrowed pointers.
556
+ self->editable = nullptr;
557
+ self->control = nullptr;
558
+ self->control_kind = RN_CONTROL_NONE;
559
+ g_clear_pointer(&self->control_description, g_free);
560
+ g_clear_pointer(&self->highlights, g_array_unref);
561
+ g_clear_pointer(&self->highlight_filled, g_array_unref);
562
+
563
+ g_clear_object(&self->text_layout);
564
+ g_clear_object(&self->texture);
565
+ g_clear_pointer(&self->role_name, g_free);
566
+ g_clear_pointer(&self->native_id, g_free);
567
+
568
+ G_OBJECT_CLASS(rn_view_parent_class)->dispose(object);
569
+ }
570
+
571
+ static void rn_view_class_init(RnViewClass *klass) {
572
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
573
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
574
+
575
+ object_class->dispose = rn_view_dispose;
576
+ widget_class->snapshot = rn_view_snapshot;
577
+
578
+ gtk_widget_class_set_layout_manager_type(widget_class, RN_TYPE_LAYOUT);
579
+ }
580
+
581
+ static void rn_view_init(RnView *self) {
582
+ self->tag = 0;
583
+ graphene_rect_init(&self->frame, 0.0f, 0.0f, 0.0f, 0.0f);
584
+ self->has_background_color = FALSE;
585
+ self->background_color = GdkRGBA{0.0f, 0.0f, 0.0f, 0.0f};
586
+ self->opacity = 1.0;
587
+ self->text_layout = nullptr;
588
+ self->text_color = GdkRGBA{0.0f, 0.0f, 0.0f, 1.0f};
589
+ self->texture = nullptr;
590
+ self->texture_fit = RN_IMAGE_FIT_COVER;
591
+ self->role_name = nullptr;
592
+ self->clips_children = FALSE;
593
+ self->scroll_x = 0.0;
594
+ self->scroll_y = 0.0;
595
+ for (int i = 0; i < 4; i++) {
596
+ self->border_radii[i] = graphene_size_t{0.0f, 0.0f};
597
+ self->border_widths[i] = 0.0f;
598
+ self->border_colors[i] = GdkRGBA{0.0f, 0.0f, 0.0f, 0.0f};
599
+ }
600
+ self->has_border_radii = FALSE;
601
+ self->has_borders = FALSE;
602
+ graphene_matrix_init_identity(&self->transform);
603
+ self->has_transform = FALSE;
604
+ self->z_index = 0;
605
+ self->editable = nullptr;
606
+ self->control = nullptr;
607
+ self->control_kind = RN_CONTROL_NONE;
608
+ self->control_disabled = FALSE;
609
+ self->control_description = nullptr;
610
+ self->highlights = nullptr;
611
+ self->highlight_filled = nullptr;
612
+ self->resize_callback = nullptr;
613
+ self->resize_data = nullptr;
614
+ // -1, not 0: a first allocation of 0x0 is a real transition worth reporting.
615
+ self->allocated_width = -1;
616
+ self->allocated_height = -1;
617
+ }
618
+
619
+ RnView *rn_view_new(int tag) {
620
+ return rn_view_new_with_role(tag, GTK_ACCESSIBLE_ROLE_GENERIC);
621
+ }
622
+
623
+ RnView *rn_view_new_with_role(int tag, GtkAccessibleRole role) {
624
+ // accessible-role is construct-only, so it goes here rather than in a setter.
625
+ RnView *self = RN_VIEW(g_object_new(RN_TYPE_VIEW, "accessible-role", role, nullptr));
626
+ self->tag = tag;
627
+ return self;
628
+ }
629
+
630
+ GtkWidget *rn_view_set_editable(RnView *self, gboolean editable, gboolean multiline) {
631
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
632
+
633
+ if (!editable) {
634
+ if (self->editable != nullptr) {
635
+ gtk_widget_unparent(self->editable);
636
+ self->editable = nullptr;
637
+ }
638
+ return nullptr;
639
+ }
640
+
641
+ // `multiline` is not a property either peer has: a field that changes between
642
+ // the two is a different widget, so the old one goes. React Native does this
643
+ // to `secureTextEntry` on AppKit for the same reason.
644
+ char *carried = nullptr;
645
+ if (self->editable != nullptr &&
646
+ rn_peer_is_multiline(self->editable) != (multiline ? TRUE : FALSE)) {
647
+ // The contents come across. Nothing asked for the field to be cleared --
648
+ // React changed one prop -- and the controlled loop will not put the text
649
+ // back by itself, because from its side the `text` prop did not change.
650
+ // AppKit carries the text the same way when secureTextEntry rebuilds its
651
+ // field, and for the same reason.
652
+ carried = rn_peer_get_text(self->editable);
653
+ gtk_widget_unparent(self->editable);
654
+ self->editable = nullptr;
655
+ }
656
+
657
+ if (self->editable == nullptr) {
658
+ self->editable = rn_peer_new(multiline);
659
+ if (carried != nullptr) {
660
+ rn_peer_set_text(self->editable, carried);
661
+ }
662
+ // No frame of its own: the RnView draws the background and border from
663
+ // React Native's props, and a second one underneath would double them.
664
+ gtk_widget_add_css_class(self->editable, "rn-text-input");
665
+ gtk_widget_set_parent(self->editable, GTK_WIDGET(self));
666
+ }
667
+ g_free(carried);
668
+ return self->editable;
669
+ }
670
+
671
+ GtkWidget *rn_view_get_editable(RnView *self) {
672
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
673
+ return self->editable;
674
+ }
675
+
676
+ GtkWidget *rn_view_set_control(RnView *self, RnControlKind kind) {
677
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
678
+
679
+ // A view never changes which control it is -- React remounts rather than
680
+ // turning a switch into a spinner -- but a Delete that arrives as an Update
681
+ // would, and a stale GtkSwitch inside a spinner's frame is the kind of thing
682
+ // that is only visible in a screenshot.
683
+ if (self->control != nullptr && self->control_kind != kind) {
684
+ gtk_widget_unparent(self->control);
685
+ self->control = nullptr;
686
+ self->control_kind = RN_CONTROL_NONE;
687
+ }
688
+
689
+ if (kind == RN_CONTROL_NONE) {
690
+ if (self->control != nullptr) {
691
+ gtk_widget_unparent(self->control);
692
+ self->control = nullptr;
693
+ }
694
+ self->control_kind = RN_CONTROL_NONE;
695
+ return nullptr;
696
+ }
697
+
698
+ if (self->control == nullptr) {
699
+ self->control = kind == RN_CONTROL_SWITCH ? gtk_switch_new() : gtk_spinner_new();
700
+ self->control_kind = kind;
701
+ // Centred at its natural size by rn_layout_allocate, which is where the
702
+ // reason is written down.
703
+ gtk_widget_add_css_class(self->control, "rn-control");
704
+ gtk_widget_set_parent(self->control, GTK_WIDGET(self));
705
+ }
706
+ return self->control;
707
+ }
708
+
709
+ GtkWidget *rn_view_get_control(RnView *self) {
710
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
711
+ return self->control;
712
+ }
713
+
714
+ RnControlKind rn_view_get_control_kind(RnView *self) {
715
+ g_return_val_if_fail(RN_IS_VIEW(self), RN_CONTROL_NONE);
716
+ return self->control_kind;
717
+ }
718
+
719
+ void rn_view_set_highlights(RnView *self,
720
+ const float *rectangles,
721
+ const gboolean *filled,
722
+ int count) {
723
+ g_return_if_fail(RN_IS_VIEW(self));
724
+
725
+ g_clear_pointer(&self->highlights, g_array_unref);
726
+ g_clear_pointer(&self->highlight_filled, g_array_unref);
727
+ if (count > 0 && rectangles != nullptr) {
728
+ self->highlights = g_array_sized_new(FALSE, FALSE, sizeof(float), count * 8);
729
+ g_array_append_vals(self->highlights, rectangles, count * 8);
730
+ self->highlight_filled = g_array_sized_new(FALSE, FALSE, sizeof(gboolean), count);
731
+ g_array_append_vals(self->highlight_filled, filled, count);
732
+ }
733
+ gtk_widget_queue_draw(GTK_WIDGET(self));
734
+ }
735
+
736
+ void rn_view_set_control_disabled(RnView *self, gboolean disabled) {
737
+ g_return_if_fail(RN_IS_VIEW(self));
738
+ self->control_disabled = disabled;
739
+ }
740
+
741
+ gboolean rn_view_get_control_disabled(RnView *self) {
742
+ g_return_val_if_fail(RN_IS_VIEW(self), FALSE);
743
+ return self->control_disabled;
744
+ }
745
+
746
+ void rn_view_set_control_description(RnView *self, const char *description) {
747
+ g_return_if_fail(RN_IS_VIEW(self));
748
+ g_clear_pointer(&self->control_description, g_free);
749
+ if (description != nullptr && *description != '\0') {
750
+ self->control_description = g_strdup(description);
751
+ }
752
+ }
753
+
754
+ void rn_view_set_accessible_text(RnView *self, const char *label, const char *description) {
755
+ g_return_if_fail(RN_IS_VIEW(self));
756
+
757
+ if (label != nullptr && *label != '\0') {
758
+ gtk_accessible_update_property(
759
+ GTK_ACCESSIBLE(self), GTK_ACCESSIBLE_PROPERTY_LABEL, label, -1);
760
+ }
761
+ if (description != nullptr && *description != '\0') {
762
+ gtk_accessible_update_property(
763
+ GTK_ACCESSIBLE(self), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, description, -1);
764
+ }
765
+ }
766
+
767
+ static void rn_view_apply_flag(RnView *self, GtkAccessibleState state, RnAccessibleFlag flag) {
768
+ switch (flag) {
769
+ case RN_A11Y_UNSET:
770
+ // Leaving a state alone is not the same as setting it false: a view that
771
+ // never says anything about "checked" is not an unchecked checkbox.
772
+ gtk_accessible_reset_state(GTK_ACCESSIBLE(self), state);
773
+ return;
774
+ case RN_A11Y_FALSE:
775
+ case RN_A11Y_TRUE:
776
+ break;
777
+ }
778
+
779
+ const gboolean value = flag == RN_A11Y_TRUE ? TRUE : FALSE;
780
+ if (state == GTK_ACCESSIBLE_STATE_CHECKED) {
781
+ gtk_accessible_update_state(GTK_ACCESSIBLE(self),
782
+ state,
783
+ value ? GTK_ACCESSIBLE_TRISTATE_TRUE : GTK_ACCESSIBLE_TRISTATE_FALSE,
784
+ -1);
785
+ return;
786
+ }
787
+ gtk_accessible_update_state(GTK_ACCESSIBLE(self), state, value, -1);
788
+ }
789
+
790
+ void rn_view_set_accessible_state(RnView *self,
791
+ RnAccessibleFlag disabled,
792
+ RnAccessibleFlag checked,
793
+ RnAccessibleFlag selected,
794
+ RnAccessibleFlag expanded,
795
+ RnAccessibleFlag busy) {
796
+ g_return_if_fail(RN_IS_VIEW(self));
797
+
798
+ rn_view_apply_flag(self, GTK_ACCESSIBLE_STATE_DISABLED, disabled);
799
+ rn_view_apply_flag(self, GTK_ACCESSIBLE_STATE_CHECKED, checked);
800
+ rn_view_apply_flag(self, GTK_ACCESSIBLE_STATE_SELECTED, selected);
801
+ rn_view_apply_flag(self, GTK_ACCESSIBLE_STATE_EXPANDED, expanded);
802
+ rn_view_apply_flag(self, GTK_ACCESSIBLE_STATE_BUSY, busy);
803
+ }
804
+
805
+ void rn_view_set_accessible_hidden(RnView *self, gboolean hidden) {
806
+ g_return_if_fail(RN_IS_VIEW(self));
807
+ gtk_accessible_update_state(GTK_ACCESSIBLE(self), GTK_ACCESSIBLE_STATE_HIDDEN, hidden, -1);
808
+ }
809
+
810
+ int rn_view_get_tag(RnView *self) {
811
+ return self->tag;
812
+ }
813
+
814
+ void rn_view_set_peer_insets(RnView *self, const GtkBorder *insets) {
815
+ g_return_if_fail(RN_IS_VIEW(self));
816
+ if (memcmp(&self->peer_insets, insets, sizeof(GtkBorder)) == 0) {
817
+ return;
818
+ }
819
+ self->peer_insets = *insets;
820
+ gtk_widget_queue_allocate(GTK_WIDGET(self));
821
+ }
822
+
823
+ void rn_view_get_peer_insets(RnView *self, GtkBorder *out) {
824
+ g_return_if_fail(RN_IS_VIEW(self));
825
+ *out = self->peer_insets;
826
+ }
827
+
828
+ void rn_view_set_frame(RnView *self, float x, float y, float width, float height) {
829
+ graphene_rect_init(&self->frame, x, y, width, height);
830
+
831
+ // The frame lives in the parent's coordinate space, so it is the parent's
832
+ // allocation that has to be redone.
833
+ GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(self));
834
+ gtk_widget_queue_allocate(parent != nullptr ? parent : GTK_WIDGET(self));
835
+ }
836
+
837
+ void rn_view_get_frame(RnView *self, graphene_rect_t *out) {
838
+ *out = self->frame;
839
+ }
840
+
841
+ void rn_view_set_background_color(RnView *self, gboolean has_color, const GdkRGBA *color) {
842
+ self->has_background_color = has_color;
843
+ if (has_color && color != nullptr) {
844
+ self->background_color = *color;
845
+ }
846
+ gtk_widget_queue_draw(GTK_WIDGET(self));
847
+ }
848
+
849
+ void rn_view_set_opacity(RnView *self, double opacity) {
850
+ self->opacity = CLAMP(opacity, 0.0, 1.0);
851
+ gtk_widget_queue_draw(GTK_WIDGET(self));
852
+ }
853
+
854
+ void rn_view_set_text_layout(RnView *self, PangoLayout *layout, const GdkRGBA *color) {
855
+ g_return_if_fail(RN_IS_VIEW(self));
856
+
857
+ if (layout != nullptr) {
858
+ g_object_ref(layout);
859
+ }
860
+ g_clear_object(&self->text_layout);
861
+ self->text_layout = layout;
862
+
863
+ if (color != nullptr) {
864
+ self->text_color = *color;
865
+ }
866
+
867
+ gtk_widget_queue_draw(GTK_WIDGET(self));
868
+ }
869
+
870
+ void rn_view_set_role_name(RnView *self, const char *name) {
871
+ g_return_if_fail(RN_IS_VIEW(self));
872
+ g_free(self->role_name);
873
+ self->role_name = name != nullptr && *name != '\0' ? g_strdup(name) : nullptr;
874
+ }
875
+
876
+ void rn_view_set_native_id(RnView *self, const char *native_id) {
877
+ g_return_if_fail(RN_IS_VIEW(self));
878
+ g_free(self->native_id);
879
+ // Empty becomes NULL, as the role name above does: React Native sends "" for
880
+ // a view with no nativeID at all, and storing that would make every
881
+ // unmarked view compare equal to an empty marker rather than to nothing.
882
+ self->native_id = native_id != nullptr && *native_id != '\0' ? g_strdup(native_id) : nullptr;
883
+ }
884
+
885
+ const char *rn_view_get_native_id(RnView *self) {
886
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
887
+ return self->native_id;
888
+ }
889
+
890
+ // The spelling React Native uses for the prop, which is also CSS's, so the
891
+ // three hosts' dumps say the same words.
892
+ static const char *rn_pointer_events_name(RnPointerEvents mode) {
893
+ switch (mode) {
894
+ case RN_POINTER_EVENTS_NONE:
895
+ return "none";
896
+ case RN_POINTER_EVENTS_BOX_NONE:
897
+ return "box-none";
898
+ case RN_POINTER_EVENTS_BOX_ONLY:
899
+ return "box-only";
900
+ case RN_POINTER_EVENTS_AUTO:
901
+ break;
902
+ }
903
+ return "auto";
904
+ }
905
+
906
+ static const char *rn_image_fit_name(RnImageFit fit) {
907
+ switch (fit) {
908
+ case RN_IMAGE_FIT_CONTAIN:
909
+ return "contain";
910
+ case RN_IMAGE_FIT_STRETCH:
911
+ return "stretch";
912
+ case RN_IMAGE_FIT_CENTER:
913
+ return "center";
914
+ case RN_IMAGE_FIT_REPEAT:
915
+ return "repeat";
916
+ case RN_IMAGE_FIT_COVER:
917
+ break;
918
+ }
919
+ return "cover";
920
+ }
921
+
922
+ void rn_view_set_image_tint(RnView *self, gboolean has_tint, const GdkRGBA *tint) {
923
+ if (!RN_IS_VIEW(self)) {
924
+ return;
925
+ }
926
+ const gboolean changed = self->has_image_tint != has_tint ||
927
+ (has_tint && tint != nullptr && !gdk_rgba_equal(&self->image_tint, tint));
928
+ self->has_image_tint = has_tint;
929
+ if (has_tint && tint != nullptr) {
930
+ self->image_tint = *tint;
931
+ }
932
+ if (changed) {
933
+ gtk_widget_queue_draw(GTK_WIDGET(self));
934
+ }
935
+ }
936
+
937
+ void rn_view_set_texture(RnView *self, GdkTexture *texture, RnImageFit fit) {
938
+ g_return_if_fail(RN_IS_VIEW(self));
939
+
940
+ if (texture != nullptr) {
941
+ g_object_ref(texture);
942
+ }
943
+ g_clear_object(&self->texture);
944
+ self->texture = texture;
945
+ self->texture_fit = fit;
946
+
947
+ gtk_widget_queue_draw(GTK_WIDGET(self));
948
+ }
949
+
950
+ static void rn_view_scroll_offset(RnView *self, double *offset_x, double *offset_y) {
951
+ *offset_x = self->scroll_x;
952
+ *offset_y = self->scroll_y;
953
+ }
954
+
955
+ static gboolean rn_view_layout_transform(RnView *self, graphene_matrix_t *out) {
956
+ if (!self->has_transform) {
957
+ return FALSE;
958
+ }
959
+ graphene_matrix_init_from_matrix(out, &self->transform);
960
+ return TRUE;
961
+ }
962
+
963
+ static int rn_view_layout_z_index(RnView *self) {
964
+ return self->z_index;
965
+ }
966
+
967
+ void rn_view_set_border_radii(RnView *self, const graphene_size_t radii[4]) {
968
+ g_return_if_fail(RN_IS_VIEW(self));
969
+
970
+ gboolean any = FALSE;
971
+ for (int i = 0; i < 4; i++) {
972
+ self->border_radii[i] = radii != nullptr ? radii[i] : graphene_size_t{0.0f, 0.0f};
973
+ if (self->border_radii[i].width > 0.0f || self->border_radii[i].height > 0.0f) {
974
+ any = TRUE;
975
+ }
976
+ }
977
+ self->has_border_radii = any;
978
+ gtk_widget_queue_draw(GTK_WIDGET(self));
979
+ }
980
+
981
+ void rn_view_set_borders(RnView *self, const float widths[4], const GdkRGBA colors[4]) {
982
+ g_return_if_fail(RN_IS_VIEW(self));
983
+
984
+ gboolean any = FALSE;
985
+ for (int i = 0; i < 4; i++) {
986
+ self->border_widths[i] = widths != nullptr ? widths[i] : 0.0f;
987
+ self->border_colors[i] = colors != nullptr ? colors[i] : GdkRGBA{0.0f, 0.0f, 0.0f, 0.0f};
988
+ if (self->border_widths[i] > 0.0f && self->border_colors[i].alpha > 0.0f) {
989
+ any = TRUE;
990
+ }
991
+ }
992
+ self->has_borders = any;
993
+ gtk_widget_queue_draw(GTK_WIDGET(self));
994
+ }
995
+
996
+ // Hidden through `gtk_widget_set_visible` rather than by skipping the
997
+ // snapshot: it takes the children with it, which is what a back face should
998
+ // do, and it takes hit testing with it too, since `gtk_widget_pick` skips a
999
+ // widget that is not visible.
1000
+ //
1001
+ // The two reasons are kept apart and combined here. Visibility is one flag and
1002
+ // `display: none` writes it too, so whichever of the two spoke last used to
1003
+ // answer for both -- and layout metrics are applied after props, so a card
1004
+ // turned away from the viewer was reliably shown again a moment later.
1005
+ static void rn_view_apply_visibility(RnView *self) {
1006
+ gtk_widget_set_visible(GTK_WIDGET(self),
1007
+ (self->hidden_by_app || self->hidden_by_back_face) ? FALSE : TRUE);
1008
+ }
1009
+
1010
+ void rn_view_set_hidden(RnView *self, gboolean hidden) {
1011
+ g_return_if_fail(RN_IS_VIEW(self));
1012
+ self->hidden_by_app = hidden;
1013
+ rn_view_apply_visibility(self);
1014
+ }
1015
+
1016
+ static void rn_view_update_back_face(RnView *self) {
1017
+ gboolean away = FALSE;
1018
+ if (self->hides_back_face) {
1019
+ float values[16];
1020
+ graphene_matrix_to_float(&self->transform, values);
1021
+ away = basalt::facesAway(values) ? TRUE : FALSE;
1022
+ }
1023
+ // Recomputed rather than returned early on, so that turning the prop off
1024
+ // shows a view it had hidden instead of leaving it hidden forever.
1025
+ self->hidden_by_back_face = away;
1026
+ rn_view_apply_visibility(self);
1027
+ }
1028
+
1029
+ void rn_view_set_transform(RnView *self, const graphene_matrix_t *matrix) {
1030
+ g_return_if_fail(RN_IS_VIEW(self));
1031
+
1032
+ if (matrix == nullptr) {
1033
+ graphene_matrix_init_identity(&self->transform);
1034
+ self->has_transform = FALSE;
1035
+ } else {
1036
+ graphene_matrix_init_from_matrix(&self->transform, matrix);
1037
+ self->has_transform = !graphene_matrix_is_identity(matrix);
1038
+ }
1039
+ rn_view_update_back_face(self);
1040
+ // Composed during the *parent's* allocation, alongside the frame, so it is
1041
+ // the parent that has to be redone. Queueing on this widget leaves a cleared
1042
+ // transform still applied until something else moves the parent.
1043
+ GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(self));
1044
+ gtk_widget_queue_allocate(parent != nullptr ? parent : GTK_WIDGET(self));
1045
+ }
1046
+
1047
+ void rn_view_set_hides_back_face(RnView *self, gboolean hides) {
1048
+ g_return_if_fail(RN_IS_VIEW(self));
1049
+ if (self->hides_back_face == hides) {
1050
+ return;
1051
+ }
1052
+ self->hides_back_face = hides;
1053
+ rn_view_update_back_face(self);
1054
+ }
1055
+
1056
+ void rn_view_set_z_index(RnView *self, int z_index) {
1057
+ g_return_if_fail(RN_IS_VIEW(self));
1058
+ if (self->z_index == z_index) {
1059
+ return;
1060
+ }
1061
+ self->z_index = z_index;
1062
+ GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(self));
1063
+ if (parent != nullptr) {
1064
+ gtk_widget_queue_draw(parent);
1065
+ }
1066
+ }
1067
+
1068
+ void rn_view_set_pointer_events(RnView *self, RnPointerEvents mode) {
1069
+ g_return_if_fail(RN_IS_VIEW(self));
1070
+ if (self->pointer_events == mode) {
1071
+ return;
1072
+ }
1073
+ self->pointer_events = mode;
1074
+ // `none` is the one mode GTK understands: an untargetable widget is skipped
1075
+ // by gtk_widget_pick along with everything inside it, so a press lands on
1076
+ // whatever is behind. The other two are left to the hit test, because a
1077
+ // widget can only be targetable or not and they are neither.
1078
+ gtk_widget_set_can_target(GTK_WIDGET(self), mode != RN_POINTER_EVENTS_NONE);
1079
+ }
1080
+
1081
+ RnPointerEvents rn_view_get_pointer_events(RnView *self) {
1082
+ g_return_val_if_fail(RN_IS_VIEW(self), RN_POINTER_EVENTS_AUTO);
1083
+ return self->pointer_events;
1084
+ }
1085
+
1086
+ void rn_view_set_focusable(RnView *self, gboolean focusable) {
1087
+ g_return_if_fail(RN_IS_VIEW(self));
1088
+ if (self->focusable == focusable) {
1089
+ return;
1090
+ }
1091
+ self->focusable = focusable;
1092
+ // `focusable` is whether the widget can hold focus at all; `can-focus` is
1093
+ // whether Tab will give it any. GTK wants both, and a widget with only the
1094
+ // first is reachable by a click and never by the keyboard.
1095
+ gtk_widget_set_focusable(GTK_WIDGET(self), focusable);
1096
+ gtk_widget_set_can_focus(GTK_WIDGET(self), focusable);
1097
+ gtk_widget_queue_draw(GTK_WIDGET(self));
1098
+ }
1099
+
1100
+ gboolean rn_view_get_focusable(RnView *self) {
1101
+ g_return_val_if_fail(RN_IS_VIEW(self), FALSE);
1102
+ return self->focusable;
1103
+ }
1104
+
1105
+ void rn_view_set_clips_children(RnView *self, gboolean clips) {
1106
+ g_return_if_fail(RN_IS_VIEW(self));
1107
+ if (self->clips_children == clips) {
1108
+ return;
1109
+ }
1110
+ self->clips_children = clips;
1111
+ gtk_widget_queue_draw(GTK_WIDGET(self));
1112
+ }
1113
+
1114
+ void rn_view_set_scroll_indicators(RnView *self,
1115
+ double vertical_offset,
1116
+ double vertical_length,
1117
+ double horizontal_offset,
1118
+ double horizontal_length) {
1119
+ if (!RN_IS_VIEW(self)) {
1120
+ return;
1121
+ }
1122
+ if (self->indicator_v_offset == vertical_offset && self->indicator_v_length == vertical_length &&
1123
+ self->indicator_h_offset == horizontal_offset &&
1124
+ self->indicator_h_length == horizontal_length) {
1125
+ return;
1126
+ }
1127
+ self->indicator_v_offset = vertical_offset;
1128
+ self->indicator_v_length = vertical_length;
1129
+ self->indicator_h_offset = horizontal_offset;
1130
+ self->indicator_h_length = horizontal_length;
1131
+ gtk_widget_queue_draw(GTK_WIDGET(self));
1132
+ }
1133
+
1134
+ void rn_view_set_scroll_offset(RnView *self, double offset_x, double offset_y) {
1135
+ g_return_if_fail(RN_IS_VIEW(self));
1136
+ if (self->scroll_x == offset_x && self->scroll_y == offset_y) {
1137
+ return;
1138
+ }
1139
+ self->scroll_x = offset_x;
1140
+ self->scroll_y = offset_y;
1141
+ // Children move, so the layout has to run again; a redraw alone would leave
1142
+ // them where they were.
1143
+ gtk_widget_queue_allocate(GTK_WIDGET(self));
1144
+ }
1145
+
1146
+ void rn_view_get_scroll_offset(RnView *self, double *offset_x, double *offset_y) {
1147
+ g_return_if_fail(RN_IS_VIEW(self));
1148
+ rn_view_scroll_offset(self, offset_x, offset_y);
1149
+ }
1150
+
1151
+ // Escaping for the tree dump: backslash, quote and newline, and nothing else.
1152
+ //
1153
+ // Not g_strescape, which also turns every byte above 0x7f into an octal escape
1154
+ // -- so a string with a "·" in it comes out as \302\267 on this side and as
1155
+ // itself on the AppKit side, and the two dumps differ on a character neither
1156
+ // platform did anything to. The result is also simply easier to read.
1157
+ static char *rn_escape_for_dump(const char *text) {
1158
+ GString *escaped = g_string_new(nullptr);
1159
+ for (const char *c = text; c != nullptr && *c != '\0'; c++) {
1160
+ switch (*c) {
1161
+ case '\\':
1162
+ g_string_append(escaped, "\\\\");
1163
+ break;
1164
+ case '"':
1165
+ g_string_append(escaped, "\\\"");
1166
+ break;
1167
+ case '\n':
1168
+ g_string_append(escaped, "\\n");
1169
+ break;
1170
+ default:
1171
+ g_string_append_c(escaped, *c);
1172
+ break;
1173
+ }
1174
+ }
1175
+ return g_string_free(escaped, FALSE);
1176
+ }
1177
+
1178
+ static void rn_view_describe_into(RnView *self, GString *out, int depth) {
1179
+ for (int i = 0; i < depth; i++) {
1180
+ g_string_append(out, " ");
1181
+ }
1182
+
1183
+ g_string_append_printf(out,
1184
+ "view tag=%d frame=(%g,%g %gx%g)",
1185
+ self->tag,
1186
+ static_cast<double>(self->frame.origin.x),
1187
+ static_cast<double>(self->frame.origin.y),
1188
+ static_cast<double>(self->frame.size.width),
1189
+ static_cast<double>(self->frame.size.height));
1190
+
1191
+ if (self->has_background_color) {
1192
+ g_string_append_printf(out,
1193
+ " bg=#%02x%02x%02x%02x",
1194
+ static_cast<unsigned>(self->background_color.red * 255.0 + 0.5),
1195
+ static_cast<unsigned>(self->background_color.green * 255.0 + 0.5),
1196
+ static_cast<unsigned>(self->background_color.blue * 255.0 + 0.5),
1197
+ static_cast<unsigned>(self->background_color.alpha * 255.0 + 0.5));
1198
+ }
1199
+ if (self->opacity < 1.0) {
1200
+ g_string_append_printf(out, " opacity=%g", self->opacity);
1201
+ }
1202
+ if (self->clips_children) {
1203
+ g_string_append(out, " clip");
1204
+ }
1205
+ // Whether anything is drawn at all. Without this a view hidden by
1206
+ // `display: none` or by a back face turned away reads exactly like a visible
1207
+ // one, and the only prop in this dump that removes a view entirely was the
1208
+ // only one it could not show.
1209
+ if (!gtk_widget_get_visible(GTK_WIDGET(self))) {
1210
+ g_string_append(out, " hidden");
1211
+ }
1212
+ // Per-corner radii and per-edge borders. These are in the dump for the same
1213
+ // reason `transform=` is: a frame cannot show them, so a view that is
1214
+ // bordered on one desktop and bare on another reads as identical here. That
1215
+ // is exactly how the macOS host went this long applying neither.
1216
+ //
1217
+ // Radii are eight numbers rather than four because React Native's are
1218
+ // elliptical -- a horizontal and a vertical radius per corner -- in the order
1219
+ // top-left, top-right, bottom-right, bottom-left.
1220
+ if (self->has_border_radii) {
1221
+ g_string_append_printf(out,
1222
+ " radii=(%g,%g,%g,%g,%g,%g,%g,%g)",
1223
+ static_cast<double>(self->border_radii[0].width),
1224
+ static_cast<double>(self->border_radii[0].height),
1225
+ static_cast<double>(self->border_radii[1].width),
1226
+ static_cast<double>(self->border_radii[1].height),
1227
+ static_cast<double>(self->border_radii[2].width),
1228
+ static_cast<double>(self->border_radii[2].height),
1229
+ static_cast<double>(self->border_radii[3].width),
1230
+ static_cast<double>(self->border_radii[3].height));
1231
+ }
1232
+ // Widths and colours are top, right, bottom, left -- the order CSS names
1233
+ // them, and the order every platform here stores them in.
1234
+ if (self->has_borders) {
1235
+ g_string_append_printf(out,
1236
+ " borderw=(%g,%g,%g,%g)",
1237
+ static_cast<double>(self->border_widths[0]),
1238
+ static_cast<double>(self->border_widths[1]),
1239
+ static_cast<double>(self->border_widths[2]),
1240
+ static_cast<double>(self->border_widths[3]));
1241
+ g_string_append(out, " borderc=(");
1242
+ for (int i = 0; i < 4; i++) {
1243
+ g_string_append_printf(out,
1244
+ "%s#%02x%02x%02x%02x",
1245
+ i == 0 ? "" : ",",
1246
+ static_cast<unsigned>(self->border_colors[i].red * 255.0 + 0.5),
1247
+ static_cast<unsigned>(self->border_colors[i].green * 255.0 + 0.5),
1248
+ static_cast<unsigned>(self->border_colors[i].blue * 255.0 + 0.5),
1249
+ static_cast<unsigned>(self->border_colors[i].alpha * 255.0 + 0.5));
1250
+ }
1251
+ g_string_append(out, ")");
1252
+ }
1253
+ if (self->has_transform) {
1254
+ // The 2D affine part, in the order CSS writes a matrix(): a, b, c, d, tx,
1255
+ // ty. The AppKit side prints the same six from its CATransform3D. See the
1256
+ // comment there for why this is in the dump at all.
1257
+ float values[16];
1258
+ graphene_matrix_to_float(&self->transform, values);
1259
+ g_string_append_printf(out,
1260
+ " transform=(%g,%g,%g,%g,%g,%g)",
1261
+ values[0],
1262
+ values[1],
1263
+ values[4],
1264
+ values[5],
1265
+ values[12],
1266
+ values[13]);
1267
+ }
1268
+ if (self->scroll_x != 0.0 || self->scroll_y != 0.0) {
1269
+ g_string_append_printf(out, " scroll=(%g,%g)", self->scroll_x, self->scroll_y);
1270
+ }
1271
+ // The overlay scrollbars, which are otherwise pure paint and so invisible to
1272
+ // every test this project has. Printed only when there is one, so a view that
1273
+ // does not scroll stays as short as it was.
1274
+ if (self->indicator_v_length > 0.0) {
1275
+ g_string_append_printf(
1276
+ out, " scrollbar-v=(%g,%g)", self->indicator_v_offset, self->indicator_v_length);
1277
+ }
1278
+ if (self->indicator_h_length > 0.0) {
1279
+ g_string_append_printf(
1280
+ out, " scrollbar-h=(%g,%g)", self->indicator_h_offset, self->indicator_h_length);
1281
+ }
1282
+ // Printed only when it is not the default, like every other field here.
1283
+ // Worth printing at all because it is invisible: a view with
1284
+ // `pointerEvents: none` is drawn exactly like one without, and the only way
1285
+ // the cross-host diff can say the prop arrived on all three is if each one
1286
+ // reports it.
1287
+ if (self->pointer_events != RN_POINTER_EVENTS_AUTO) {
1288
+ g_string_append_printf(out, " pe=%s", rn_pointer_events_name(self->pointer_events));
1289
+ }
1290
+ if (self->texture != nullptr) {
1291
+ // The fit is here because it is the only thing about a drawn image that a
1292
+ // frame cannot show: two views the same size holding the same picture are
1293
+ // identical in every other line of this dump and different on screen.
1294
+ g_string_append_printf(out,
1295
+ " texture=%dx%d fit=%s",
1296
+ gdk_texture_get_width(self->texture),
1297
+ gdk_texture_get_height(self->texture),
1298
+ rn_image_fit_name(self->texture_fit));
1299
+ // Printed for the same reason the fit is: a tinted image and an untinted
1300
+ // one are identical in every other line of this dump and different on
1301
+ // screen, and this is the only thing a test without pixels can read.
1302
+ if (self->has_image_tint) {
1303
+ g_string_append_printf(out,
1304
+ " tint=#%02x%02x%02x%02x",
1305
+ static_cast<unsigned>(self->image_tint.red * 255.0 + 0.5),
1306
+ static_cast<unsigned>(self->image_tint.green * 255.0 + 0.5),
1307
+ static_cast<unsigned>(self->image_tint.blue * 255.0 + 0.5),
1308
+ static_cast<unsigned>(self->image_tint.alpha * 255.0 + 0.5));
1309
+ }
1310
+ }
1311
+ if (self->text_layout != nullptr) {
1312
+ const char *text = pango_layout_get_text(self->text_layout);
1313
+ if (text != nullptr && *text != '\0') {
1314
+ char *escaped = rn_escape_for_dump(text);
1315
+ g_string_append_printf(out, " text=\"%s\"", escaped);
1316
+ g_free(escaped);
1317
+ }
1318
+ }
1319
+
1320
+ // A text field's content lives in its GtkText peer, not in a PangoLayout, so
1321
+ // it would otherwise be invisible to every test that reads this tree.
1322
+ if (self->editable != nullptr) {
1323
+ char *owned = rn_peer_get_text(self->editable);
1324
+ const char *value = owned;
1325
+ char *escaped = rn_escape_for_dump(value != nullptr ? value : "");
1326
+ g_string_append_printf(out, " editable=\"%s\"", escaped);
1327
+ g_free(owned);
1328
+ g_free(escaped);
1329
+ if (gtk_widget_has_focus(self->editable)) {
1330
+ g_string_append(out, " focused");
1331
+ }
1332
+ }
1333
+
1334
+ // How many DevTools highlights this view is drawing. In the dump because they
1335
+ // are otherwise invisible to everything but a screenshot, and because a
1336
+ // command that arrived and drew nothing is exactly the failure worth
1337
+ // catching.
1338
+ if (self->highlight_filled != nullptr && self->highlight_filled->len > 0) {
1339
+ g_string_append_printf(out, " highlights=%u", self->highlight_filled->len);
1340
+ }
1341
+
1342
+ // What kind of control this view is, and what state it is in. Written by
1343
+ // core/DesktopControls.h rather than formatted here, for the same reason the
1344
+ // role name below is React Native's vocabulary and not GTK's: three hosts
1345
+ // describing the same switch in three ways is a diff on every line.
1346
+ if (self->control_description != nullptr) {
1347
+ g_string_append_printf(out, " control=%s", self->control_description);
1348
+ }
1349
+
1350
+ // React Native's role name, not GTK's. This dump is compared line by line
1351
+ // with the AppKit one, and each reporting its own toolkit's vocabulary would
1352
+ // make every accessible view look like a difference. That the *GTK* role was
1353
+ // really applied is asserted in tests/test_accessibility.cpp, which is where
1354
+ // a platform question belongs.
1355
+ if (self->role_name != nullptr && *self->role_name != '\0') {
1356
+ g_string_append_printf(out, " role=%s", self->role_name);
1357
+ }
1358
+ // Whether Tab stops here. GTK's own answer rather than the flag this project
1359
+ // set, which is the stronger statement: it says the widget really did join
1360
+ // the focus chain. Whether it is focused *now* is deliberately not printed --
1361
+ // that depends on what the window manager did when the window opened, which
1362
+ // is not a property of the platform and would make this dump differ between
1363
+ // two machines running the same app.
1364
+ if (gtk_accessible_get_platform_state(GTK_ACCESSIBLE(self), GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSABLE)) {
1365
+ g_string_append(out, " focusable");
1366
+ }
1367
+
1368
+ g_string_append_c(out, '\n');
1369
+
1370
+ for (GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(self)); child != nullptr;
1371
+ child = gtk_widget_get_next_sibling(child)) {
1372
+ if (RN_IS_VIEW(child)) {
1373
+ rn_view_describe_into(RN_VIEW(child), out, depth + 1);
1374
+ }
1375
+ }
1376
+ }
1377
+
1378
+ char *rn_view_describe_tree(RnView *self) {
1379
+ g_return_val_if_fail(RN_IS_VIEW(self), nullptr);
1380
+ GString *out = g_string_new(nullptr);
1381
+ rn_view_describe_into(self, out, 0);
1382
+ return g_string_free(out, FALSE);
1383
+ }
1384
+
1385
+ void rn_view_insert_child(RnView *self, RnView *child, int index) {
1386
+ GtkWidget *child_widget = GTK_WIDGET(child);
1387
+ GtkWidget *parent_widget = GTK_WIDGET(self);
1388
+
1389
+ // Fabric indexes into the parent's current child list. Walk to the sibling
1390
+ // currently at `index` and insert before it; a past-the-end index appends.
1391
+ GtkWidget *sibling = gtk_widget_get_first_child(parent_widget);
1392
+ for (int i = 0; i < index && sibling != nullptr; i++) {
1393
+ sibling = gtk_widget_get_next_sibling(sibling);
1394
+ }
1395
+
1396
+ if (sibling != nullptr) {
1397
+ gtk_widget_insert_before(child_widget, parent_widget, sibling);
1398
+ } else {
1399
+ gtk_widget_set_parent(child_widget, parent_widget);
1400
+ }
1401
+ }
1402
+
1403
+ void rn_view_remove_child(RnView * /*self*/, RnView *child) {
1404
+ // Remove detaches but must not destroy: a Delete mutation for the same tag
1405
+ // follows separately, and until then the view may be re-Inserted elsewhere.
1406
+ gtk_widget_unparent(GTK_WIDGET(child));
1407
+ }