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,425 @@
|
|
|
1
|
+
#include "GtkTextPeer.h"
|
|
2
|
+
|
|
3
|
+
// A GtkTextView that draws a placeholder.
|
|
4
|
+
//
|
|
5
|
+
// GtkText has a placeholder property and GtkTextView has nothing, so the text
|
|
6
|
+
// is drawn -- over what the view drew, which is nothing when the buffer is
|
|
7
|
+
// empty, and only when it is empty.
|
|
8
|
+
//
|
|
9
|
+
// A subclass rather than an overlay because the peer is one widget parented
|
|
10
|
+
// inside the RnView, and the allocation path in RnView.cpp places exactly one.
|
|
11
|
+
|
|
12
|
+
#define RN_TYPE_TEXT_VIEW (rn_text_view_get_type())
|
|
13
|
+
G_DECLARE_FINAL_TYPE(RnTextView, rn_text_view, RN, TEXT_VIEW, GtkTextView)
|
|
14
|
+
|
|
15
|
+
struct _RnTextView {
|
|
16
|
+
GtkTextView parent_instance;
|
|
17
|
+
char *placeholder;
|
|
18
|
+
PangoAttrList *attributes;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
G_DEFINE_TYPE(RnTextView, rn_text_view, GTK_TYPE_TEXT_VIEW)
|
|
22
|
+
|
|
23
|
+
static void rn_text_view_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) {
|
|
24
|
+
GTK_WIDGET_CLASS(rn_text_view_parent_class)->snapshot(widget, snapshot);
|
|
25
|
+
|
|
26
|
+
RnTextView *self = RN_TEXT_VIEW(widget);
|
|
27
|
+
if (self->placeholder == nullptr || *self->placeholder == '\0') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
|
|
31
|
+
if (gtk_text_buffer_get_char_count(buffer) > 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
PangoLayout *layout = gtk_widget_create_pango_layout(widget, self->placeholder);
|
|
36
|
+
if (self->attributes != nullptr) {
|
|
37
|
+
pango_layout_set_attributes(layout, self->attributes);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The text colour, dimmed. GTK's own placeholder is the theme's dim
|
|
41
|
+
// foreground, and this field's foreground came from React Native's props --
|
|
42
|
+
// so dimming that keeps the placeholder legible against whatever background
|
|
43
|
+
// the app chose, which a fixed grey would not.
|
|
44
|
+
GdkRGBA colour = {0.5F, 0.5F, 0.5F, 0.5F};
|
|
45
|
+
if (self->attributes != nullptr) {
|
|
46
|
+
PangoAttrIterator *iter = pango_attr_list_get_iterator(self->attributes);
|
|
47
|
+
if (iter != nullptr) {
|
|
48
|
+
if (const PangoAttribute *found = pango_attr_iterator_get(iter, PANGO_ATTR_FOREGROUND)) {
|
|
49
|
+
const PangoColor &from = reinterpret_cast<const PangoAttrColor *>(found)->color;
|
|
50
|
+
colour.red = static_cast<float>(from.red / 65535.0);
|
|
51
|
+
colour.green = static_cast<float>(from.green / 65535.0);
|
|
52
|
+
colour.blue = static_cast<float>(from.blue / 65535.0);
|
|
53
|
+
}
|
|
54
|
+
pango_attr_iterator_destroy(iter);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
colour.alpha = 0.45F;
|
|
58
|
+
|
|
59
|
+
gtk_snapshot_append_layout(snapshot, layout, &colour);
|
|
60
|
+
g_object_unref(layout);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static void rn_text_view_finalize(GObject *object) {
|
|
64
|
+
RnTextView *self = RN_TEXT_VIEW(object);
|
|
65
|
+
g_clear_pointer(&self->placeholder, g_free);
|
|
66
|
+
g_clear_pointer(&self->attributes, pango_attr_list_unref);
|
|
67
|
+
G_OBJECT_CLASS(rn_text_view_parent_class)->finalize(object);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static void rn_text_view_class_init(RnTextViewClass *klass) {
|
|
71
|
+
G_OBJECT_CLASS(klass)->finalize = rn_text_view_finalize;
|
|
72
|
+
GTK_WIDGET_CLASS(klass)->snapshot = rn_text_view_snapshot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static void rn_text_view_init(RnTextView *self) {
|
|
76
|
+
self->placeholder = nullptr;
|
|
77
|
+
self->attributes = nullptr;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Empty to non-empty and back changes whether the placeholder belongs on
|
|
81
|
+
// screen, and GTK has no reason to know that.
|
|
82
|
+
static void on_buffer_changed_redraw(GtkTextBuffer * /*buffer*/, gpointer widget) {
|
|
83
|
+
gtk_widget_queue_draw(GTK_WIDGET(widget));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A GtkTextView paints a background of its own, from the GTK theme, straight
|
|
87
|
+
// over the one the RnView drew from React Native's props -- which is a white
|
|
88
|
+
// box where a styled field should be, and white text invisible inside it. A
|
|
89
|
+
// bare GtkText has no such background, which is why the single-line path never
|
|
90
|
+
// needed this.
|
|
91
|
+
//
|
|
92
|
+
// Installed once, on the display, and scoped to the class RnView already puts
|
|
93
|
+
// on every peer.
|
|
94
|
+
static void ensure_peer_css(void) {
|
|
95
|
+
static gboolean installed = FALSE;
|
|
96
|
+
if (installed) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
GdkDisplay *display = gdk_display_get_default();
|
|
100
|
+
if (display == nullptr) {
|
|
101
|
+
// No display yet; a peer built before one exists is not going to be drawn
|
|
102
|
+
// either, and the next one will install this.
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
installed = TRUE;
|
|
106
|
+
|
|
107
|
+
GtkCssProvider *provider = gtk_css_provider_new();
|
|
108
|
+
// Both nodes: a GtkTextView draws through a child "text" node, and leaving
|
|
109
|
+
// either opaque leaves the field opaque.
|
|
110
|
+
gtk_css_provider_load_from_string(
|
|
111
|
+
provider,
|
|
112
|
+
".rn-text-input, .rn-text-input text { background: none; background-color: transparent; }");
|
|
113
|
+
gtk_style_context_add_provider_for_display(
|
|
114
|
+
display, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
115
|
+
g_object_unref(provider);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
GtkWidget *rn_peer_new(gboolean multiline) {
|
|
119
|
+
if (!multiline) {
|
|
120
|
+
return gtk_text_new();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ensure_peer_css();
|
|
124
|
+
GtkWidget *view = GTK_WIDGET(g_object_new(RN_TYPE_TEXT_VIEW, nullptr));
|
|
125
|
+
g_signal_connect(gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)), "changed",
|
|
126
|
+
G_CALLBACK(on_buffer_changed_redraw), view);
|
|
127
|
+
// The measured box is already tall enough for the wrapped text -- the shadow
|
|
128
|
+
// node measures a multiline input against the real constraints -- so without
|
|
129
|
+
// wrapping here the text would run off one line inside a box sized for
|
|
130
|
+
// three. WORD_CHAR rather than WORD so a single long word still breaks,
|
|
131
|
+
// which is what every other platform's multiline field does.
|
|
132
|
+
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD_CHAR);
|
|
133
|
+
// No margins of its own: the RnView already allocates the peer inside the
|
|
134
|
+
// content inset Yoga resolved.
|
|
135
|
+
gtk_text_view_set_top_margin(GTK_TEXT_VIEW(view), 0);
|
|
136
|
+
gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(view), 0);
|
|
137
|
+
gtk_text_view_set_left_margin(GTK_TEXT_VIEW(view), 0);
|
|
138
|
+
gtk_text_view_set_right_margin(GTK_TEXT_VIEW(view), 0);
|
|
139
|
+
return view;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
gboolean rn_peer_is_multiline(GtkWidget *peer) {
|
|
143
|
+
return peer != nullptr && GTK_IS_TEXT_VIEW(peer);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static GtkTextBuffer *buffer_of(GtkWidget *peer) {
|
|
147
|
+
return gtk_text_view_get_buffer(GTK_TEXT_VIEW(peer));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
char *rn_peer_get_text(GtkWidget *peer) {
|
|
151
|
+
if (peer == nullptr) {
|
|
152
|
+
return g_strdup("");
|
|
153
|
+
}
|
|
154
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
155
|
+
const char *text = gtk_editable_get_text(GTK_EDITABLE(peer));
|
|
156
|
+
return g_strdup(text != nullptr ? text : "");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
160
|
+
GtkTextIter start;
|
|
161
|
+
GtkTextIter end;
|
|
162
|
+
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
|
163
|
+
// FALSE: invisible characters and widget anchors are not the user's text.
|
|
164
|
+
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void rn_peer_set_text(GtkWidget *peer, const char *text) {
|
|
168
|
+
if (peer == nullptr) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const char *value = text != nullptr ? text : "";
|
|
172
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
173
|
+
gtk_editable_set_text(GTK_EDITABLE(peer), value);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
gtk_text_buffer_set_text(buffer_of(peer), value, -1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
int rn_peer_get_position(GtkWidget *peer) {
|
|
180
|
+
if (peer == nullptr) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
184
|
+
return gtk_editable_get_position(GTK_EDITABLE(peer));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
188
|
+
GtkTextIter iter;
|
|
189
|
+
gtk_text_buffer_get_iter_at_mark(buffer, &iter, gtk_text_buffer_get_insert(buffer));
|
|
190
|
+
return gtk_text_iter_get_offset(&iter);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
void rn_peer_set_position(GtkWidget *peer, int position) {
|
|
194
|
+
if (peer == nullptr) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
198
|
+
gtk_editable_set_position(GTK_EDITABLE(peer), position);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
203
|
+
GtkTextIter iter;
|
|
204
|
+
// Clamped, because an offset past the end is a runtime warning and then a
|
|
205
|
+
// caret nowhere in particular.
|
|
206
|
+
const int length = gtk_text_buffer_get_char_count(buffer);
|
|
207
|
+
gtk_text_buffer_get_iter_at_offset(buffer, &iter, CLAMP(position, 0, length));
|
|
208
|
+
gtk_text_buffer_place_cursor(buffer, &iter);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
gboolean rn_peer_get_selection_bounds(GtkWidget *peer, int *start, int *end) {
|
|
212
|
+
if (peer == nullptr) {
|
|
213
|
+
return FALSE;
|
|
214
|
+
}
|
|
215
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
216
|
+
return gtk_editable_get_selection_bounds(GTK_EDITABLE(peer), start, end);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
GtkTextIter from;
|
|
220
|
+
GtkTextIter to;
|
|
221
|
+
if (!gtk_text_buffer_get_selection_bounds(buffer_of(peer), &from, &to)) {
|
|
222
|
+
return FALSE;
|
|
223
|
+
}
|
|
224
|
+
if (start != nullptr) {
|
|
225
|
+
*start = gtk_text_iter_get_offset(&from);
|
|
226
|
+
}
|
|
227
|
+
if (end != nullptr) {
|
|
228
|
+
*end = gtk_text_iter_get_offset(&to);
|
|
229
|
+
}
|
|
230
|
+
return TRUE;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
void rn_peer_select_region(GtkWidget *peer, int start, int end) {
|
|
234
|
+
if (peer == nullptr) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
238
|
+
gtk_editable_select_region(GTK_EDITABLE(peer), start, end);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
243
|
+
const int length = gtk_text_buffer_get_char_count(buffer);
|
|
244
|
+
GtkTextIter from;
|
|
245
|
+
GtkTextIter to;
|
|
246
|
+
gtk_text_buffer_get_iter_at_offset(buffer, &from, CLAMP(start, 0, length));
|
|
247
|
+
gtk_text_buffer_get_iter_at_offset(buffer, &to, CLAMP(end, 0, length));
|
|
248
|
+
gtk_text_buffer_select_range(buffer, &from, &to);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
void rn_peer_set_editable(GtkWidget *peer, gboolean editable) {
|
|
252
|
+
if (peer == nullptr) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
256
|
+
gtk_editable_set_editable(GTK_EDITABLE(peer), editable);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
gtk_text_view_set_editable(GTK_TEXT_VIEW(peer), editable);
|
|
260
|
+
// Without this a read-only view still shows a blinking caret, which reads as
|
|
261
|
+
// a field that has focus and refuses to type.
|
|
262
|
+
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(peer), editable);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
GObject *rn_peer_signal_source(GtkWidget *peer) {
|
|
266
|
+
if (peer == nullptr) {
|
|
267
|
+
return nullptr;
|
|
268
|
+
}
|
|
269
|
+
// A GtkText emits "changed" and carries the cursor properties itself; a
|
|
270
|
+
// GtkTextView emits neither -- its buffer does. Handing back the right
|
|
271
|
+
// object is what lets the manager connect one set of handlers.
|
|
272
|
+
return rn_peer_is_multiline(peer) ? G_OBJECT(buffer_of(peer)) : G_OBJECT(peer);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
void rn_peer_set_attributes(GtkWidget *peer, PangoAttrList *attributes) {
|
|
276
|
+
if (peer == nullptr) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
280
|
+
gtk_text_set_attributes(GTK_TEXT(peer), attributes);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A tag over the whole buffer. Named, so the same one is reused and the tag
|
|
285
|
+
// table does not grow a new entry on every prop update -- which it would,
|
|
286
|
+
// and a controlled field updates on every keystroke.
|
|
287
|
+
// Kept for the placeholder, which is drawn rather than laid out by the
|
|
288
|
+
// buffer and so needs the font and colour in its own right.
|
|
289
|
+
RnTextView *self = RN_TEXT_VIEW(peer);
|
|
290
|
+
g_clear_pointer(&self->attributes, pango_attr_list_unref);
|
|
291
|
+
self->attributes = attributes != nullptr ? pango_attr_list_ref(attributes) : nullptr;
|
|
292
|
+
|
|
293
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
294
|
+
GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
|
|
295
|
+
GtkTextTag *tag = gtk_text_tag_table_lookup(table, "rn-style");
|
|
296
|
+
if (tag == nullptr) {
|
|
297
|
+
tag = gtk_text_buffer_create_tag(buffer, "rn-style", nullptr);
|
|
298
|
+
}
|
|
299
|
+
// A GtkTextTag has no PangoAttrList property -- it carries the same
|
|
300
|
+
// information as individual properties -- so the two that matter are pulled
|
|
301
|
+
// back out of the list. Font and colour are what the single-line path sets
|
|
302
|
+
// and what a field left in the GTK theme gets wrong: dark text on a dark
|
|
303
|
+
// background.
|
|
304
|
+
if (attributes != nullptr) {
|
|
305
|
+
PangoAttrIterator *iter = pango_attr_list_get_iterator(attributes);
|
|
306
|
+
if (iter != nullptr) {
|
|
307
|
+
PangoFontDescription *font = pango_font_description_new();
|
|
308
|
+
pango_attr_iterator_get_font(iter, font, nullptr, nullptr);
|
|
309
|
+
g_object_set(tag, "font-desc", font, nullptr);
|
|
310
|
+
pango_font_description_free(font);
|
|
311
|
+
|
|
312
|
+
if (const PangoAttribute *found = pango_attr_iterator_get(iter, PANGO_ATTR_FOREGROUND)) {
|
|
313
|
+
const PangoColor &colour = reinterpret_cast<const PangoAttrColor *>(found)->color;
|
|
314
|
+
GdkRGBA rgba;
|
|
315
|
+
rgba.red = colour.red / 65535.0;
|
|
316
|
+
rgba.green = colour.green / 65535.0;
|
|
317
|
+
rgba.blue = colour.blue / 65535.0;
|
|
318
|
+
rgba.alpha = 1.0;
|
|
319
|
+
g_object_set(tag, "foreground-rgba", &rgba, nullptr);
|
|
320
|
+
}
|
|
321
|
+
pango_attr_iterator_destroy(iter);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
GtkTextIter start;
|
|
326
|
+
GtkTextIter end;
|
|
327
|
+
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
|
328
|
+
gtk_text_buffer_apply_tag(buffer, tag, &start, &end);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void rn_peer_set_placeholder(GtkWidget *peer, const char *placeholder) {
|
|
332
|
+
if (peer == nullptr) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const char *value = placeholder != nullptr && *placeholder != '\0' ? placeholder : nullptr;
|
|
336
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
337
|
+
gtk_text_set_placeholder_text(GTK_TEXT(peer), value);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
RnTextView *self = RN_TEXT_VIEW(peer);
|
|
342
|
+
g_clear_pointer(&self->placeholder, g_free);
|
|
343
|
+
self->placeholder = value != nullptr ? g_strdup(value) : nullptr;
|
|
344
|
+
gtk_widget_queue_draw(peer);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const char *rn_peer_get_placeholder(GtkWidget *peer) {
|
|
348
|
+
if (peer == nullptr) {
|
|
349
|
+
return nullptr;
|
|
350
|
+
}
|
|
351
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
352
|
+
return gtk_text_get_placeholder_text(GTK_TEXT(peer));
|
|
353
|
+
}
|
|
354
|
+
return RN_TEXT_VIEW(peer)->placeholder;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
void rn_peer_set_visibility(GtkWidget *peer, gboolean visible) {
|
|
358
|
+
if (peer == nullptr || rn_peer_is_multiline(peer)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
gtk_text_set_visibility(GTK_TEXT(peer), visible);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// The buffer's half of maxLength. GtkText has a property for it; GtkTextBuffer
|
|
365
|
+
// has nothing, so the limit is enforced by refusing the insertion that would
|
|
366
|
+
// cross it -- which is what GtkText does internally too.
|
|
367
|
+
static void on_buffer_insert(GtkTextBuffer *buffer,
|
|
368
|
+
GtkTextIter * /*location*/,
|
|
369
|
+
const char *text,
|
|
370
|
+
int length,
|
|
371
|
+
gpointer /*data*/) {
|
|
372
|
+
const int limit = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(buffer), "rn-max-length"));
|
|
373
|
+
if (limit <= 0) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const int existing = gtk_text_buffer_get_char_count(buffer);
|
|
377
|
+
const long incoming = g_utf8_strlen(text, length);
|
|
378
|
+
if (existing + incoming <= limit) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Refused whole rather than truncated: a paste that would overflow is
|
|
382
|
+
// rejected, which is what GtkText does with its own max-length and what
|
|
383
|
+
// stops a half-pasted string appearing.
|
|
384
|
+
g_signal_stop_emission_by_name(buffer, "insert-text");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
void rn_peer_set_max_length(GtkWidget *peer, int max_length) {
|
|
388
|
+
if (peer == nullptr) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
392
|
+
gtk_text_set_max_length(GTK_TEXT(peer), max_length);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
397
|
+
g_object_set_data(G_OBJECT(buffer), "rn-max-length", GINT_TO_POINTER(max_length));
|
|
398
|
+
// Connected once. The handler reads the limit back out of the buffer, so a
|
|
399
|
+
// changed maxLength needs no reconnection.
|
|
400
|
+
if (g_object_get_data(G_OBJECT(buffer), "rn-max-length-connected") == nullptr) {
|
|
401
|
+
g_object_set_data(G_OBJECT(buffer), "rn-max-length-connected", GINT_TO_POINTER(1));
|
|
402
|
+
g_signal_connect(buffer, "insert-text", G_CALLBACK(on_buffer_insert), nullptr);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
void rn_peer_insert_text(GtkWidget *peer, const char *text, int length, int *position) {
|
|
407
|
+
if (peer == nullptr || text == nullptr) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!rn_peer_is_multiline(peer)) {
|
|
411
|
+
gtk_editable_insert_text(GTK_EDITABLE(peer), text, length, position);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
GtkTextBuffer *buffer = buffer_of(peer);
|
|
416
|
+
const int count = gtk_text_buffer_get_char_count(buffer);
|
|
417
|
+
const int at = position != nullptr ? CLAMP(*position, 0, count) : count;
|
|
418
|
+
GtkTextIter iter;
|
|
419
|
+
gtk_text_buffer_get_iter_at_offset(buffer, &iter, at);
|
|
420
|
+
gtk_text_buffer_insert(buffer, &iter, text, length);
|
|
421
|
+
if (position != nullptr) {
|
|
422
|
+
// Where the caret ended up: insert leaves `iter` past what it wrote.
|
|
423
|
+
*position = gtk_text_iter_get_offset(&iter);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The six things a <TextInput> does to its peer, for either kind of peer.
|
|
2
|
+
//
|
|
3
|
+
// A single-line field is a GtkText, which is a GtkEditable; a multiline one is
|
|
4
|
+
// a GtkTextView, which is not -- it owns a GtkTextBuffer and every one of
|
|
5
|
+
// GtkEditable's calls has a different name and a different shape there. Rather
|
|
6
|
+
// than branch at each of the fifteen call sites in GtkTextInput.cpp, they go
|
|
7
|
+
// through here.
|
|
8
|
+
//
|
|
9
|
+
// C rather than C++ because RnView.cpp uses it too, and that file deliberately
|
|
10
|
+
// knows nothing about React Native or about C++ types crossing its boundary.
|
|
11
|
+
//
|
|
12
|
+
// Offsets are in characters, not bytes. GtkEditable counts characters and
|
|
13
|
+
// GtkTextBuffer offers both; React Native means characters, so that is what
|
|
14
|
+
// crosses this seam.
|
|
15
|
+
|
|
16
|
+
#pragma once
|
|
17
|
+
|
|
18
|
+
#include <gtk/gtk.h>
|
|
19
|
+
|
|
20
|
+
G_BEGIN_DECLS
|
|
21
|
+
|
|
22
|
+
// Builds the peer for a field. Multiline gets a GtkTextView with wrapping on,
|
|
23
|
+
// which is the whole visible difference: the measured box is already the right
|
|
24
|
+
// height, and without wrapping the text would run off the end of one line
|
|
25
|
+
// inside it.
|
|
26
|
+
GtkWidget *rn_peer_new(gboolean multiline);
|
|
27
|
+
|
|
28
|
+
gboolean rn_peer_is_multiline(GtkWidget *peer);
|
|
29
|
+
|
|
30
|
+
// The peer's whole contents. Never NULL; free with g_free.
|
|
31
|
+
char *rn_peer_get_text(GtkWidget *peer);
|
|
32
|
+
|
|
33
|
+
void rn_peer_set_text(GtkWidget *peer, const char *text);
|
|
34
|
+
|
|
35
|
+
// The caret, as a character offset.
|
|
36
|
+
int rn_peer_get_position(GtkWidget *peer);
|
|
37
|
+
void rn_peer_set_position(GtkWidget *peer, int position);
|
|
38
|
+
|
|
39
|
+
// The selection, or FALSE with the out parameters untouched when there is
|
|
40
|
+
// none -- which is GtkEditable's contract, kept for both so callers need only
|
|
41
|
+
// learn one.
|
|
42
|
+
gboolean rn_peer_get_selection_bounds(GtkWidget *peer, int *start, int *end);
|
|
43
|
+
void rn_peer_select_region(GtkWidget *peer, int start, int end);
|
|
44
|
+
|
|
45
|
+
void rn_peer_set_editable(GtkWidget *peer, gboolean editable);
|
|
46
|
+
|
|
47
|
+
// Where the "changed" and cursor signals live: the widget for a GtkText, the
|
|
48
|
+
// buffer for a GtkTextView. Callers connect to this rather than to the peer.
|
|
49
|
+
GObject *rn_peer_signal_source(GtkWidget *peer);
|
|
50
|
+
|
|
51
|
+
G_END_DECLS
|
|
52
|
+
|
|
53
|
+
G_BEGIN_DECLS
|
|
54
|
+
|
|
55
|
+
// The app's font and colour, which neither peer takes the same way.
|
|
56
|
+
//
|
|
57
|
+
// A GtkText takes a PangoAttrList directly. A GtkTextView has no such
|
|
58
|
+
// property: the equivalent is a tag applied over the whole buffer, refreshed
|
|
59
|
+
// whenever the style or the text changes. Both matter -- a peer left in the
|
|
60
|
+
// GTK theme's font and colour is dark text on a dark field, which is the bug
|
|
61
|
+
// the single-line path already had a comment about.
|
|
62
|
+
void rn_peer_set_attributes(GtkWidget *peer, PangoAttrList *attributes);
|
|
63
|
+
|
|
64
|
+
// The placeholder. A GtkText has a property for it; a GtkTextView has none, so
|
|
65
|
+
// its peer is a subclass that draws one when the buffer is empty.
|
|
66
|
+
void rn_peer_set_placeholder(GtkWidget *peer, const char *placeholder);
|
|
67
|
+
const char *rn_peer_get_placeholder(GtkWidget *peer);
|
|
68
|
+
|
|
69
|
+
// Hidden characters, for `secureTextEntry`. Single line only: a multiline
|
|
70
|
+
// secure field is not a thing React Native offers, and GtkTextView has no
|
|
71
|
+
// visibility property.
|
|
72
|
+
void rn_peer_set_visibility(GtkWidget *peer, gboolean visible);
|
|
73
|
+
|
|
74
|
+
// `maxLength`. Single line only -- GtkTextBuffer has no equivalent, and
|
|
75
|
+
// enforcing it by hand would fight the controlled loop.
|
|
76
|
+
void rn_peer_set_max_length(GtkWidget *peer, int max_length);
|
|
77
|
+
|
|
78
|
+
G_END_DECLS
|
|
79
|
+
|
|
80
|
+
G_BEGIN_DECLS
|
|
81
|
+
|
|
82
|
+
// Typing, as the user would. `position` is in/out: it arrives as where to
|
|
83
|
+
// insert and leaves as where the caret ended up, which is GtkEditable's
|
|
84
|
+
// contract and is what makes inserting twice in a row work.
|
|
85
|
+
void rn_peer_insert_text(GtkWidget *peer, const char *text, int length, int *position);
|
|
86
|
+
|
|
87
|
+
G_END_DECLS
|