From 3117e06dbe8e295b391cf29e0ae3fd3fc0fa36d8 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 29 Sep 2026 11:41:03 +0200 Subject: [PATCH] [GTK] Render Tree and Table images crisp at HiDPI zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GtkCellRendererPixbuf treats the "pixbuf" attribute as a scale 1 image, so at 200% zoom native Tree and Table cell images were reduced to 1x and upscaled again, while the same image drawn with GC.drawImage stayed sharp. On GTK3 at a widget scale factor above 1 the pixbuf renderer now binds its "surface" property to the CELL_SURFACE model column, a boxed cairo surface that carries the device scale, instead of "pixbuf". The binding is swapped through gtk_tree_view_column_add_attribute, so plain widgets run no Java code per row: the cell data function is still installed only for VIRTUAL, custom draw and owner draw, as before, and there it sets the surface or the pixbuf according to the same mode. The binding goes back to "pixbuf" when the control is disabled, because the surface bypasses GTK's insensitive icon effect, and when the scale factor drops to 1. Table and Tree rebind from enableWidget and from the existing scale factor notification (dpiChanged). At a scale above 1 the cell differs from the 100% rendering in ways inherent to the surface property: theme icon effects on selected or hovered rows are not applied, GTK's accessible image size is missing because it is read from the pixbuf, and the cell draws the surface of the Image itself rather than a pixbuf copy, so drawing into an Image after setImage shows up in the cell at that scale only. A snapshot copy was not added, since it would duplicate the pixels of every cell image that is shared between many rows. Disposing a column also clears its CELL_SURFACE slot, so a column that reuses the slot does not show the old image. getImage() relies on the per cell Image array from #3695 rather than the surface address. The CELL_SURFACE column type is taken from the value type of the pixbuf renderer's "surface" GParamSpec and a missing type raises ERROR_NO_HANDLES instead of being cached as 0. The VIRTUAL path keeps setData = checkData (item), since binding by attribute needs no early return. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../Eclipse SWT PI/gtk/library/os.c | 38 ++++++++++ .../Eclipse SWT PI/gtk/library/os_stats.h | 3 + .../gtk/org/eclipse/swt/internal/gtk/GTK.java | 5 ++ .../gtk/org/eclipse/swt/internal/gtk/OS.java | 8 +++ .../gtk/org/eclipse/swt/widgets/Display.java | 15 ++++ .../gtk/org/eclipse/swt/widgets/Table.java | 69 +++++++++++++++++-- .../gtk/org/eclipse/swt/widgets/Tree.java | 69 +++++++++++++++++-- 7 files changed, 195 insertions(+), 12 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c index 2dde70fc912..ce16f38691b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c @@ -8360,6 +8360,16 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1tree_1view_1column_1clear) } #endif +#ifndef NO_gtk_1tree_1view_1column_1clear_1attributes +JNIEXPORT void JNICALL GTK_NATIVE(gtk_1tree_1view_1column_1clear_1attributes) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1) +{ + GTK_NATIVE_ENTER(env, that, gtk_1tree_1view_1column_1clear_1attributes_FUNC); + gtk_tree_view_column_clear_attributes((GtkTreeViewColumn *)arg0, (GtkCellRenderer *)arg1); + GTK_NATIVE_EXIT(env, that, gtk_1tree_1view_1column_1clear_1attributes_FUNC); +} +#endif + #ifndef NO_gtk_1tree_1view_1column_1get_1button JNIEXPORT jlong JNICALL GTK_NATIVE(gtk_1tree_1view_1column_1get_1button) (JNIEnv *env, jclass that, jlong arg0) @@ -10193,6 +10203,18 @@ JNIEXPORT jlong JNICALL OS_NATIVE(G_1OBJECT_1TYPE_1NAME) } #endif +#ifndef NO_G_1PARAM_1SPEC_1VALUE_1TYPE +JNIEXPORT jlong JNICALL OS_NATIVE(G_1PARAM_1SPEC_1VALUE_1TYPE) + (JNIEnv *env, jclass that, jlong arg0) +{ + jlong rc = 0; + OS_NATIVE_ENTER(env, that, G_1PARAM_1SPEC_1VALUE_1TYPE_FUNC); + rc = (jlong)G_PARAM_SPEC_VALUE_TYPE((GParamSpec *)arg0); + OS_NATIVE_EXIT(env, that, G_1PARAM_1SPEC_1VALUE_1TYPE_FUNC); + return rc; +} +#endif + #ifndef NO_G_1TYPE_1BOOLEAN JNIEXPORT jlong JNICALL OS_NATIVE(G_1TYPE_1BOOLEAN) (JNIEnv *env, jclass that) @@ -12134,6 +12156,22 @@ JNIEXPORT void JNICALL OS_NATIVE(g_1menu_1remove) } #endif +#ifndef NO_g_1object_1class_1find_1property +JNIEXPORT jlong JNICALL OS_NATIVE(g_1object_1class_1find_1property) + (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) +{ + jbyte *lparg1=NULL; + jlong rc = 0; + OS_NATIVE_ENTER(env, that, g_1object_1class_1find_1property_FUNC); + if (arg1) if ((lparg1 = (*env)->GetByteArrayElements(env, arg1, NULL)) == NULL) goto fail; + rc = (jlong)g_object_class_find_property((GObjectClass *)arg0, (const gchar *)lparg1); +fail: + if (arg1 && lparg1) (*env)->ReleaseByteArrayElements(env, arg1, lparg1, 0); + OS_NATIVE_EXIT(env, that, g_1object_1class_1find_1property_FUNC); + return rc; +} +#endif + #ifndef NO_g_1object_1get__J_3B_3IJ JNIEXPORT void JNICALL OS_NATIVE(g_1object_1get__J_3B_3IJ) (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1, jintArray arg2, jlong arg3) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h index 1f8c28e57c2..a5a2d0b5356 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h @@ -668,6 +668,7 @@ typedef enum { gtk_1tree_1view_1column_1cell_1get_1position_FUNC, gtk_1tree_1view_1column_1cell_1set_1cell_1data_FUNC, gtk_1tree_1view_1column_1clear_FUNC, + gtk_1tree_1view_1column_1clear_1attributes_FUNC, gtk_1tree_1view_1column_1get_1button_FUNC, gtk_1tree_1view_1column_1get_1fixed_1width_FUNC, gtk_1tree_1view_1column_1get_1reorderable_FUNC, @@ -833,6 +834,7 @@ typedef enum { G_1OBJECT_1GET_1CLASS_FUNC, G_1OBJECT_1TYPE_FUNC, G_1OBJECT_1TYPE_1NAME_FUNC, + G_1PARAM_1SPEC_1VALUE_1TYPE_FUNC, G_1TYPE_1BOOLEAN_FUNC, G_1TYPE_1DOUBLE_FUNC, G_1TYPE_1FLOAT_FUNC, @@ -982,6 +984,7 @@ typedef enum { g_1menu_1item_1set_1submenu_FUNC, g_1menu_1new_FUNC, g_1menu_1remove_FUNC, + g_1object_1class_1find_1property_FUNC, g_1object_1get__J_3B_3IJ_FUNC, g_1object_1get__J_3B_3JJ_FUNC, g_1object_1get_1qdata_FUNC, diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java index 5f240f511bc..a98008b4d86 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java @@ -1973,6 +1973,11 @@ public class GTK extends OS { public static final native void gtk_tree_view_column_cell_set_cell_data(long tree_column, long tree_model, long iter, boolean is_expander, boolean is_expanded); /** @param tree_column cast=(GtkTreeViewColumn *) */ public static final native void gtk_tree_view_column_clear(long tree_column); + /** + * @param tree_column cast=(GtkTreeViewColumn *) + * @param cell_renderer cast=(GtkCellRenderer *) + */ + public static final native void gtk_tree_view_column_clear_attributes(long tree_column, long cell_renderer); /** @param column cast=(GtkTreeViewColumn *) */ public static final native long gtk_tree_view_column_get_button(long column); /** @param column cast=(GtkTreeViewColumn *) */ diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java index 26d028d2667..9395928ad18 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java @@ -504,6 +504,7 @@ public static String getEnvironmentalVariable (String envVarName) { public static final byte[] spacing = ascii("spacing"); public static final byte[] pixbuf = ascii("pixbuf"); public static final byte[] gicon = ascii("gicon"); + public static final byte[] surface = ascii("surface"); public static final byte[] text = ascii("text"); public static final byte[] xalign = ascii("xalign"); public static final byte[] ypad = ascii("ypad"); @@ -954,6 +955,8 @@ public static boolean isX11 () { public static final native int Call (long func, long arg0, int arg1, int arg2); public static final native long G_OBJECT_GET_CLASS(long object); public static final native long G_OBJECT_TYPE_NAME(long object); +/** @param pspec cast=(GParamSpec *) */ +public static final native long G_PARAM_SPEC_VALUE_TYPE(long pspec); /** @method flags=const */ public static final native long G_TYPE_INVALID(); /** @method flags=const */ @@ -1270,6 +1273,11 @@ public static boolean isX11 () { public static final native int g_log_writer_default(int log_level, long fields, long n_fields, long user_data); /** @param size cast=(gulong) */ public static final native long g_malloc(long size); +/** + * @param oclass cast=(GObjectClass *) + * @param property_name cast=(const gchar *) + */ +public static final native long g_object_class_find_property(long oclass, byte[] property_name); /** * @param object cast=(GObject *) * @param first_property_name cast=(const gchar *),flags=no_out diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java index cdf9d75c0ac..8664247fb65 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java @@ -385,6 +385,7 @@ public void stop() { /* Renderer Subclass */ static long text_renderer_type, pixbuf_renderer_type, toggle_renderer_type; + static long cairo_surface_type; static long text_renderer_info_ptr, pixbuf_renderer_info_ptr, toggle_renderer_info_ptr; static Callback rendererClassInitCallback, rendererRenderCallback, rendererSnapshotCallback; static Callback rendererGetPreferredWidthCallback; @@ -2184,6 +2185,20 @@ long gtk_cell_renderer_toggle_get_type () { return toggle_renderer_type; } +long cairoSurfaceType () { + if (cairo_surface_type == 0) { + // The boxed type of the pixbuf renderer's "surface" property is the one a model column must use + long rendererClass = OS.g_type_class_ref (GTK.GTK_TYPE_CELL_RENDERER_PIXBUF ()); + if (rendererClass != 0) { + long pspec = OS.g_object_class_find_property (rendererClass, OS.surface); + if (pspec != 0) cairo_surface_type = OS.G_PARAM_SPEC_VALUE_TYPE (pspec); + OS.g_type_class_unref (rendererClass); + } + if (cairo_surface_type == 0) SWT.error (SWT.ERROR_NO_HANDLES); + } + return cairo_surface_type; +} + String gtk_css_create_css_color_string (String background, String foreground, int property) { switch (property) { case SWT.FOREGROUND: diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java index 6b77b808c0c..ed7bb8dc1fd 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java @@ -234,9 +234,15 @@ long cellDataProc (long tree_column, long cell, long tree_model, long iter, long if (setData) { ptr [0] = 0; if (isPixbuf) { - GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_PIXBUF, ptr, -1); - OS.g_object_set (cell, OS.gicon, ptr [0], 0); - if (ptr [0] != 0) OS.g_object_unref (ptr [0]); + if (useCellSurface ()) { + GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_SURFACE, ptr, -1); + OS.g_object_set (cell, OS.surface, ptr [0], 0); + if (ptr [0] != 0) Cairo.cairo_surface_destroy (ptr [0]); + } else { + GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_PIXBUF, ptr, -1); + OS.g_object_set (cell, OS.gicon, ptr [0], 0); + if (ptr [0] != 0) OS.g_object_unref (ptr [0]); + } } else { GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_TEXT, ptr, -1); if (ptr [0] != 0) { @@ -904,9 +910,8 @@ void createRenderers (long columnHandle, int modelIndex, boolean check, int colu * no images. Fix for Bug 457196. NOTE: this change has been ported to Tables since Tables/Trees both * use the same underlying GTK structure. */ - GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.pixbuf, modelIndex + CELL_PIXBUF); + addPixbufAttributes (columnHandle, pixbufRenderer, modelIndex, useCellSurface ()); if (!ownerDraw) { - GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); GTK.gtk_tree_view_column_add_attribute (columnHandle, textRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); } GTK.gtk_tree_view_column_add_attribute (columnHandle, textRenderer, OS.text, modelIndex + CELL_TEXT); @@ -1101,6 +1106,7 @@ void destroyItem (TableColumn column) { GTK.gtk_list_store_set (modelHandle, iter, modelIndex + CELL_FOREGROUND, (long )0, -1); GTK.gtk_list_store_set (modelHandle, iter, modelIndex + CELL_BACKGROUND, (long )0, -1); GTK.gtk_list_store_set (modelHandle, iter, modelIndex + CELL_FONT, (long )0, -1); + GTK.gtk_list_store_set (modelHandle, iter, modelIndex + CELL_SURFACE, (long )0, -1); Font [] cellFont = item.cellFont; if (cellFont != null) { @@ -1306,7 +1312,7 @@ public int getColumnCount () { // per cell data for (int i=FIRST_COLUMN; i GTK.GTK_TREE_VIEW_GRID_LINES_NONE; } +/** + * GtkCellRendererPixbuf treats "pixbuf" as a scale 1 image, so HiDPI cells bind the + * device scaled "surface" instead. The surface bypasses GTK's insensitive icon effect. + */ +boolean useCellSurface () { + return !GTK.GTK4 && GTK.gtk_widget_get_scale_factor (handle) > 1 + && (state & DISABLED) == 0; +} + +void addPixbufAttributes (long columnHandle, long pixbufRenderer, int modelIndex, boolean surface) { + if (surface) { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.surface, modelIndex + CELL_SURFACE); + } else { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.pixbuf, modelIndex + CELL_PIXBUF); + } + if (!ownerDraw) { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); + } +} + +void updateCellImageBinding () { + if (GTK.GTK4 || handle == 0) return; + boolean surface = useCellSurface (); + for (int i=0; iTableItems that are currently * selected in the receiver. The order of the items is unspecified. diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java index 5f5a1e8bda5..ef1bd8840cc 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java @@ -319,9 +319,15 @@ long cellDataProc (long tree_column, long cell, long tree_model, long iter, long if (setData) { if (isPixbuf) { ptr [0] = 0; - GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_PIXBUF, ptr, -1); - OS.g_object_set (cell, OS.gicon, ptr [0], 0); - if (ptr [0] != 0) OS.g_object_unref (ptr [0]); + if (useCellSurface ()) { + GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_SURFACE, ptr, -1); + OS.g_object_set (cell, OS.surface, ptr [0], 0); + if (ptr [0] != 0) Cairo.cairo_surface_destroy (ptr [0]); + } else { + GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_PIXBUF, ptr, -1); + OS.g_object_set (cell, OS.gicon, ptr [0], 0); + if (ptr [0] != 0) OS.g_object_unref (ptr [0]); + } } else { ptr [0] = 0; GTK.gtk_tree_model_get (tree_model, iter, modelIndex + CELL_TEXT, ptr, -1); @@ -1133,9 +1139,8 @@ void createRenderers (long columnHandle, int modelIndex, boolean check, int colu * no images. Fix for Bug 469277 & 476419. NOTE: this change has been ported to Tables since Tables/Trees both * use the same underlying GTK structure. */ - GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.pixbuf, modelIndex + CELL_PIXBUF); + addPixbufAttributes (columnHandle, pixbufRenderer, modelIndex, useCellSurface ()); if (!isOwnerDrawn) { - GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); GTK.gtk_tree_view_column_add_attribute (columnHandle, textRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); } GTK.gtk_tree_view_column_add_attribute (columnHandle, textRenderer, OS.text, modelIndex + CELL_TEXT); @@ -1262,6 +1267,7 @@ void destroyItem (TreeColumn column) { GTK.gtk_tree_store_set (modelHandle, iter, modelIndex + CELL_FOREGROUND, (long )0, -1); GTK.gtk_tree_store_set (modelHandle, iter, modelIndex + CELL_BACKGROUND, (long )0, -1); GTK.gtk_tree_store_set (modelHandle, iter, modelIndex + CELL_FONT, (long )0, -1); + GTK.gtk_tree_store_set (modelHandle, iter, modelIndex + CELL_SURFACE, (long )0, -1); Font [] cellFont = item.cellFont; if (cellFont != null) { @@ -1534,7 +1540,7 @@ public int getColumnCount () { types [i + CELL_FOREGROUND] = GDK.GDK_TYPE_RGBA(); types [i + CELL_BACKGROUND] = GDK.GDK_TYPE_RGBA(); types [i + CELL_FONT] = OS.PANGO_TYPE_FONT_DESCRIPTION (); - types [i + CELL_SURFACE] = OS.G_TYPE_LONG(); + types [i + CELL_SURFACE] = GTK.GTK4 ? OS.G_TYPE_LONG() : display.cairoSurfaceType (); } return types; } @@ -1981,6 +1987,57 @@ public TreeItem getParentItem () { return null; } +/** + * GtkCellRendererPixbuf treats "pixbuf" as a scale 1 image, so HiDPI cells bind the + * device scaled "surface" instead. The surface bypasses GTK's insensitive icon effect. + */ +boolean useCellSurface () { + return !GTK.GTK4 && GTK.gtk_widget_get_scale_factor (handle) > 1 + && (state & DISABLED) == 0; +} + +void addPixbufAttributes (long columnHandle, long pixbufRenderer, int modelIndex, boolean surface) { + if (surface) { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.surface, modelIndex + CELL_SURFACE); + } else { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.pixbuf, modelIndex + CELL_PIXBUF); + } + if (!isOwnerDrawn) { + GTK.gtk_tree_view_column_add_attribute (columnHandle, pixbufRenderer, OS.cell_background_rgba, BACKGROUND_COLUMN); + } +} + +void updateCellImageBinding () { + if (GTK.GTK4 || handle == 0) return; + boolean surface = useCellSurface (); + for (int i=0; iTreeItems that are currently * selected in the receiver. The order of the items is unspecified.