basalt-gtk 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,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, ¢re);
|
|
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
|
+
}
|