From 6031eecefc2ec8307218ceaaceee6221f49dc01b Mon Sep 17 00:00:00 2001 From: Mis012 Date: Fri, 20 Jun 2025 22:36:09 +0200 Subject: [PATCH] api-impl-jni: add workarounds for Gtk's non-graceful handling of layout changes during the snapshot phase --- .../app/android_app_AlertDialog.c | 2 +- src/api-impl-jni/util.c | 91 +++++++++++++++++++ src/api-impl-jni/util.h | 8 ++ src/api-impl-jni/views/android_view_View.c | 14 +-- src/api-impl-jni/widgets/WrapperWidget.c | 13 ++- .../widgets/android_widget_Button.c | 2 +- .../widgets/android_widget_TextView.c | 2 +- 7 files changed, 120 insertions(+), 12 deletions(-) diff --git a/src/api-impl-jni/app/android_app_AlertDialog.c b/src/api-impl-jni/app/android_app_AlertDialog.c index 06760a9c..dcbd3243 100644 --- a/src/api-impl-jni/app/android_app_AlertDialog.c +++ b/src/api-impl-jni/app/android_app_AlertDialog.c @@ -65,7 +65,7 @@ static void bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item GtkWidget *label = gtk_list_item_get_child(list_item); ListEntry *entry = gtk_list_item_get_item(list_item); - gtk_label_set_text(GTK_LABEL(label), entry->text); + atl_safe_gtk_label_set_text(GTK_LABEL(label), entry->text); } static void activate_cb(GtkListView *list, guint position, struct click_callback_data *d) diff --git a/src/api-impl-jni/util.c b/src/api-impl-jni/util.c index c950ee8b..3a8845fd 100644 --- a/src/api-impl-jni/util.c +++ b/src/api-impl-jni/util.c @@ -2,6 +2,8 @@ #include #include +#include + #include "util.h" #include "src/api-impl-jni/defines.h" @@ -279,3 +281,92 @@ int get_nio_buffer_size(JNIEnv *env, jobject buffer) return limit - position; } + + +/* Calling these functions while snapshotting will cause Gtk to not snapshot the affected widgets. + * Below are "safe" wrappers which will postpone the calls if inside a snapshot. + * Specifically, gtk_widget_add_tick_callback will make sure the calls are made in the next + * Update phase. */ + +/* callbacks */ +static gboolean queue_set_text(GtkWidget *label, GdkFrameClock *frame_clock, gpointer str) +{ + gtk_label_set_text(GTK_LABEL(label), str); + /* we always call strdup so we always want to free */ + free(str); + return G_SOURCE_REMOVE; +} + +static gboolean queue_queue_allocate(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) +{ + gtk_widget_queue_allocate(widget); + return G_SOURCE_REMOVE; +} + +static gboolean queue_queue_resize(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) +{ + gtk_widget_queue_resize(widget); + return G_SOURCE_REMOVE; +} + +/* Some functions call gtk_widget_queue_allocate or similar internally. + * To prevent that from breaking the snapshotting process, when called at the wrong time, + * we have to follow those functions with this pile of hacks that will unset the problematic flags. */ +extern int snapshot_in_progress; +void atl_ensure_widget_snapshotability(GtkWidget *widget) +{ + if(snapshot_in_progress) { + GtkAllocation allocation; + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + /* we probably don't need to use this deprecated function but it sure is convenient */ + gtk_widget_get_allocation(widget, &allocation); + G_GNUC_END_IGNORE_DEPRECATIONS + /* this clears resize request, which seems to be necessary in some cases */ + gtk_widget_get_request_mode(widget); + gtk_widget_size_allocate(widget, &allocation, gtk_widget_get_baseline(widget)); + gtk_widget_add_tick_callback(widget, queue_queue_allocate, NULL, NULL); + + /* the problematic flags get set all the way up the hierarchy */ + GtkWidget *parent = gtk_widget_get_parent(widget); + if (parent) { + atl_ensure_widget_snapshotability(parent); + } + } +} + +void atl_safe_gtk_label_set_text(GtkLabel* label, const char* str) +{ + if(!snapshot_in_progress) { + gtk_label_set_text(label, str); + } else { + /* strdup since the string may not exist by the time the callback runs */ + gtk_widget_add_tick_callback(GTK_WIDGET(label), queue_set_text, (gpointer)strdup(str), NULL); + } +} + +void atl_safe_gtk_widget_set_visible(GtkWidget *widget, gboolean visible) +{ + gtk_widget_set_visible(widget, visible); + GtkWidget *parent = gtk_widget_get_parent(widget); + if (parent) { + atl_ensure_widget_snapshotability(parent); + } +} + +void atl_safe_gtk_widget_queue_allocate(GtkWidget *widget) +{ + if(!snapshot_in_progress) { + gtk_widget_queue_allocate(widget); + } else { + gtk_widget_add_tick_callback(widget, queue_queue_allocate, NULL, NULL); + } +} + +void atl_safe_gtk_widget_queue_resize(GtkWidget *widget) +{ + if(!snapshot_in_progress) { + gtk_widget_queue_resize(widget); + } else { + gtk_widget_add_tick_callback(widget, queue_queue_resize, NULL, NULL); + } +} diff --git a/src/api-impl-jni/util.h b/src/api-impl-jni/util.h index bd9a5365..784a6f27 100644 --- a/src/api-impl-jni/util.h +++ b/src/api-impl-jni/util.h @@ -1,6 +1,8 @@ #ifndef _UTILS_H_ #define _UTILS_H_ +#include + #include #include "defines.h" @@ -186,4 +188,10 @@ void *get_nio_buffer(JNIEnv *env, jobject buffer, jarray *array_ref, jbyte **arr void release_nio_buffer(JNIEnv *env, jarray array_ref, jbyte *array); int get_nio_buffer_size(JNIEnv *env, jobject buffer); +void atl_ensure_widget_snapshotability(GtkWidget *widget); +void atl_safe_gtk_label_set_text(GtkLabel* label, const char* str); +void atl_safe_gtk_widget_set_visible(GtkWidget *widget, gboolean visible); +void atl_safe_gtk_widget_queue_allocate(GtkWidget *widget); +void atl_safe_gtk_widget_queue_resize(GtkWidget *widget); + #endif diff --git a/src/api-impl-jni/views/android_view_View.c b/src/api-impl-jni/views/android_view_View.c index 36b4e59c..0955d2af 100644 --- a/src/api-impl-jni/views/android_view_View.c +++ b/src/api-impl-jni/views/android_view_View.c @@ -446,6 +446,8 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1setLayoutParams(JNIEnv *en android_layout_set_params(ATL_ANDROID_LAYOUT(layout_manager), width, height); wrapper_widget_set_layout_params(WRAPPER_WIDGET(widget), width, height); + + atl_ensure_widget_snapshotability(widget); } #pragma GCC diagnostic push @@ -473,7 +475,7 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1setPadding(JNIEnv *env, jo JNIEXPORT void JNICALL Java_android_view_View_native_1setVisibility(JNIEnv *env, jobject this, jlong widget_ptr, jint visibility, jfloat alpha) { GtkWidget *widget = gtk_widget_get_parent(GTK_WIDGET(_PTR(widget_ptr))); - gtk_widget_set_visible(widget, visibility != android_view_View_GONE); + atl_safe_gtk_widget_set_visible(widget, visibility != android_view_View_GONE); gtk_widget_set_opacity(widget, (visibility != android_view_View_INVISIBLE) * alpha); gtk_widget_set_sensitive(widget, visibility != android_view_View_INVISIBLE && alpha != 0.0f); } @@ -481,7 +483,7 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1setVisibility(JNIEnv *env, /** JavaWidget: * Minimal gtk widget class which does nothing. * Drawing will be overwritten by WrapperWidget. - * If it holds children, they will be layouted by AndroidLayout + * If it holds children, they will be laid out by AndroidLayout */ struct _JavaWidget {GtkWidget parent_instance;}; G_DECLARE_FINAL_TYPE(JavaWidget, java_widget, JAVA, WIDGET, GtkWidget) @@ -511,7 +513,7 @@ JNIEXPORT jlong JNICALL Java_android_view_View_native_1constructor(JNIEnv *env, jclass class = _CLASS(this); jstring nameObj = (*env)->CallObjectMethod(env, class, - _METHOD(_CLASS(class), "getName", "()Ljava/lang/String;")); + _METHOD(_CLASS(class), "getName", "()Ljava/lang/String;")); const char *name = (*env)->GetStringUTFChars(env, nameObj, NULL); gtk_widget_set_name(widget, name); (*env)->ReleaseStringUTFChars(env, nameObj, name); @@ -595,7 +597,7 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1layout(JNIEnv *env, jobjec wrapper->real_width = width; wrapper->real_height = height; if (!wrapper->needs_allocation) - gtk_widget_queue_allocate(widget); + atl_safe_gtk_widget_queue_allocate(widget); } if (wrapper->needs_allocation) { allocation.width = width; @@ -607,7 +609,7 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1layout(JNIEnv *env, jobjec JNIEXPORT void JNICALL Java_android_view_View_native_1requestLayout(JNIEnv *env, jobject this, jlong widget_ptr) { GtkWidget *widget = GTK_WIDGET(_PTR(widget_ptr)); - gtk_widget_queue_resize(widget); + atl_safe_gtk_widget_queue_resize(widget); } /* we kinda need per-widget css */ @@ -711,7 +713,7 @@ JNIEXPORT jboolean JNICALL Java_android_view_View_native_1getMatrix(JNIEnv *env, JNIEXPORT void JNICALL Java_android_view_View_native_1queueAllocate(JNIEnv *env, jobject this, jlong widget_ptr) { - gtk_widget_queue_allocate(GTK_WIDGET(_PTR(widget_ptr))); + atl_safe_gtk_widget_queue_allocate(GTK_WIDGET(_PTR(widget_ptr))); } JNIEXPORT void JNICALL Java_android_view_View_native_1drawBackground(JNIEnv *env, jobject this, jlong widget_ptr, jlong snapshot_ptr) diff --git a/src/api-impl-jni/widgets/WrapperWidget.c b/src/api-impl-jni/widgets/WrapperWidget.c index cb418cfb..00adb9b8 100644 --- a/src/api-impl-jni/widgets/WrapperWidget.c +++ b/src/api-impl-jni/widgets/WrapperWidget.c @@ -168,7 +168,7 @@ void wrapper_widget_allocate(GtkWidget *widget, int width, int height, int basel layout->real_width = width; layout->real_height = height; if (!layout->needs_allocation) - gtk_widget_queue_allocate(wrapper->child); + atl_safe_gtk_widget_queue_allocate(wrapper->child); } if (layout->needs_allocation) gtk_widget_size_allocate(wrapper->child, &allocation, baseline); @@ -181,8 +181,12 @@ void wrapper_widget_allocate(GtkWidget *widget, int width, int height, int basel gtk_widget_size_allocate(wrapper->background, &allocation, baseline); } +/* this is used to avoid queing layout changes in the middle of snapshotting */ +int snapshot_in_progress = 0; static void wrapper_widget_snapshot(GtkWidget *widget, GdkSnapshot *snapshot) { + snapshot_in_progress++; + WrapperWidget *wrapper = WRAPPER_WIDGET(widget); if (wrapper->real_height > 0 && wrapper->real_width > 0) { gtk_snapshot_push_clip(snapshot, &GRAPHENE_RECT_INIT(0, 0, wrapper->real_width, wrapper->real_height)); @@ -204,6 +208,8 @@ static void wrapper_widget_snapshot(GtkWidget *widget, GdkSnapshot *snapshot) if (wrapper->real_height > 0 && wrapper->real_width > 0) { gtk_snapshot_pop(snapshot); } + + snapshot_in_progress--; } static void wrapper_widget_class_init(WrapperWidgetClass *class) @@ -257,8 +263,9 @@ void wrapper_widget_queue_draw(WrapperWidget *wrapper) if(wrapper->child) gtk_widget_queue_draw(wrapper->child); - if (wrapper->computeScroll_method) - gtk_widget_queue_allocate(GTK_WIDGET(wrapper)); + if (wrapper->computeScroll_method) { + atl_safe_gtk_widget_queue_allocate(GTK_WIDGET(wrapper)); + } } static bool on_click(GtkGestureClick *gesture, int n_press, double x, double y, jobject this) diff --git a/src/api-impl-jni/widgets/android_widget_Button.c b/src/api-impl-jni/widgets/android_widget_Button.c index 3eafde3c..b1bf6a82 100644 --- a/src/api-impl-jni/widgets/android_widget_Button.c +++ b/src/api-impl-jni/widgets/android_widget_Button.c @@ -36,7 +36,7 @@ JNIEXPORT void JNICALL Java_android_widget_Button_native_1setText(JNIEnv *env, j GtkButton *button = GTK_BUTTON(_PTR(widget_ptr)); const char *nativeText = ((*env)->GetStringUTFChars(env, text, NULL)); - gtk_label_set_text(box_get_label(env, gtk_button_get_child(button)), nativeText); + atl_safe_gtk_label_set_text(box_get_label(env, gtk_button_get_child(button)), nativeText); ((*env)->ReleaseStringUTFChars(env, text, nativeText)); } diff --git a/src/api-impl-jni/widgets/android_widget_TextView.c b/src/api-impl-jni/widgets/android_widget_TextView.c index c2d82a20..23587b16 100644 --- a/src/api-impl-jni/widgets/android_widget_TextView.c +++ b/src/api-impl-jni/widgets/android_widget_TextView.c @@ -43,7 +43,7 @@ JNIEXPORT jlong JNICALL Java_android_widget_TextView_native_1constructor(JNIEnv JNIEXPORT void JNICALL Java_android_widget_TextView_native_1setText(JNIEnv *env, jobject this, jobject charseq) { const char *text = charseq ? (*env)->GetStringUTFChars(env, charseq, NULL) : NULL; - gtk_label_set_text(box_get_label(env, _PTR(_GET_LONG_FIELD(this, "widget"))), text ?: ""); + atl_safe_gtk_label_set_text(box_get_label(env, _PTR(_GET_LONG_FIELD(this, "widget"))), text ?: ""); if(text) (*env)->ReleaseStringUTFChars(env, charseq, text); }