Skip to content

[GTK] Render Tree and Table images crisp at HiDPI zoom - #3631

Open
vogella wants to merge 1 commit into
eclipse-platform:masterfrom
vogella:gtk-tree-cell-surface
Open

vogella wants to merge 1 commit into
eclipse-platform:masterfrom
vogella:gtk-tree-cell-surface

Conversation

@vogella

@vogella vogella commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

On GTK3 at 200% zoom, images set with TreeItem.setImage or TableItem.setImage were rendered blurry, because GtkCellRendererPixbuf treats the bound pixbuf as a scale 1 image.
At a scale factor above 1 the pixbuf renderer now binds its surface property to the CELL_SURFACE model column, which carries the device scale, so native cell images are as sharp as GC.drawImage output.
The binding is swapped with gtk_tree_view_column_add_attribute when the scale or the enabled state changes, so plain Trees and Tables run no Java code per row, and disabled controls keep GTK's dimmed icons.

@vogella

vogella commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author
tasklist_compare tasklist_zoom_compare

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (linux)

  115 files  ±0    115 suites  ±0   13m 12s ⏱️ - 1m 24s
4 665 tests ±0  4 430 ✅ ±0  235 💤 ±0  0 ❌ ±0 
3 557 runs  ±0  3 468 ✅ ±0   89 💤 ±0  0 ❌ ±0 

Results for commit 3117e06. ± Comparison against base commit 8f4fe7a.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Reused column model slots retain stale surfaces, causing removed images to reappear.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Uses scale-aware Cairo surfaces to render crisp Tree/Table images on GTK3 HiDPI displays.

Changes:

  • Binds GTK3 cell renderers to stored image surfaces.
  • Registers pixbuf data callbacks for all Tree/Table renderers.
  • Adds the GTK surface property constant.
File Description
Tree.java Renders Tree images from Cairo surfaces.
Table.java Renders Table images from Cairo surfaces.
OS.java Defines the native surface property name.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java Outdated
Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java Outdated
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

  188 files   -    36    188 suites   - 36   21m 11s ⏱️ - 4m 39s
5 016 tests +    6  4 988 ✅ +    6   28 💤 ± 0  0 ❌ ±0 
6 222 runs   - 1 140  6 067 ✅  - 1 101  155 💤  - 39  0 ❌ ±0 

Results for commit 35961bc. ± Comparison against base commit f87da9a.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Raw surface pointers can become dangling, and GTK4 receives unnecessary per-cell callbacks.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (2)

Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java Outdated
Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

GTK4 receives unnecessary render callbacks, and the painting regressions use zero-sized controls.

Review effort: Balanced
Findings: 2 Medium severity · 2 Low severity

Open (4)
Resolved since last review (2)

Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java Outdated
Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java Outdated
@akurtakov

akurtakov commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

There are all the details to handle here but it also fixes a nasty drawing problem:

public static void main(String[] args) {
      Display display = new Display();
      Shell shell = new Shell(display);
      shell.setText("zoom " + display.getPrimaryMonitor().getZoom() + "%");
      shell.setLayout(new RowLayout());

      // 1 device pixel black/white stripes at every zoom, any resampling makes it grey
      Image stripes = new Image(display, (ImageDataProvider) zoom -> {
              int size = 16 * zoom / 100;
              ImageData data = new ImageData(size, size, 24, new PaletteData(0xFF0000, 0xFF00, 0xFF));
              for (int y = 0; y < size; y++)
                      for (int x = 0; x < size; x++)
                              data.setPixel(x, y, x % 2 == 0 ? 0 : 0xFFFFFF);
              return data;
      });

      Tree tree = new Tree(shell, SWT.BORDER);
      new TreeItem(tree, SWT.NONE).setImage(stripes);

      Label reference = new Label(shell, SWT.NONE);
      reference.setImage(stripes);

      shell.pack();
      shell.open();
      while (!shell.isDisposed()) {
              if (!display.readAndDispatch()) display.sleep();
      }
      stripes.dispose();
      display.dispose();
}

with GDK_SCALE=2 draws a solid gray instead of striped.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The GTK3-specific implementation is coherent, with only minor test resource-cleanup feedback remaining.

Review effort: Balanced
Findings: 2 Low severity

Open (2)
Resolved since last review (4)

@vogella
vogella force-pushed the gtk-tree-cell-surface branch 2 times, most recently from 98c9281 to 010e9df Compare September 30, 2026 04:34
@akurtakov
akurtakov requested a balanced review from Copilot September 30, 2026 04:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Stale non-owning surface addresses can alias replacement surfaces and render the wrong image.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (2)

Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java Outdated
Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java Outdated
@vogella
vogella force-pushed the gtk-tree-cell-surface branch 2 times, most recently from 0ee8728 to 371ae10 Compare September 30, 2026 16:17
@akurtakov
akurtakov requested a balanced review from Copilot September 30, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The rendering and ownership changes are coherent and tested; only a minor class-reference cleanup remains.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java Outdated
@vogella

vogella commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Fixed

@vogella
vogella force-pushed the gtk-tree-cell-surface branch from 371ae10 to 74ebb77 Compare October 2, 2026 04:22
@vogella

vogella commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

@akurtakov let me know if you have more feedback otherwise I plan to merge tomorrow.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Native GTK resource ownership and platform-specific visual behavior require final human validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@akurtakov

akurtakov commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Thanks for this, the crisp icons are a real improvement. I have a few concerns, mostly about the GTK3 path.

  1. Every GTK3 Tree/Table now runs cellDataProc for every row, at any zoom.
    createRenderers now attaches the pixbuf cell data func whenever !GTK.GTK4, so plain widgets (not VIRTUAL, no custom draw) get it too. SWT only enables fixed-height-mode for VIRTUAL widgets. For the others, GtkTreeView's idle row validation (do_validate_rows → validate_row → gtk_tree_view_column_cell_set_cell_data) visits every row, not just the visible ones, and now calls into Java for each.

I measured it on a plain 10,000-row Table on GTK3 at 100%, with nothing done after shell.open(). A TableItem subclass counts the isDisposed() calls that come from cellDataProc:

cellDataProc calls distinct rows visible rows
master 0 0 ~16
this PR 20,039 10,000 ~16

At scale 1 every one of those calls does nothing: item lookup, qdata, a scale and state query, then it falls through. So "unchanged at 100%" holds for what is drawn, but not for cost.

Now that CELL_SURFACE is a boxed CairoSurface column on GTK3, could the renderer's surface property be bound with gtk_tree_view_column_add_attribute instead? The pixbuf/surface binding would be swapped when the scale or sensitivity changes, so nothing would run in Java per row, and plain widgets would keep master's cost.

  1. HiDPI rendering differs from 100% in ways beyond sharpness.
  • In GTK3, -gtk-icon-effect is applied only to pixbuf/gicon sources (ensure_surface_from_pixbuf); ensure_surface_from_surface just references the surface. The INSENSITIVE check covers the disabled case, but other theme effects (e.g. highlight on selected or hovered rows) now apply at 100% but not at 200%.
  • For image surfaces, ImageList.convertSurface just references image.surface. So at 200% the cell draws the Image's live surface, while at 100% it draws the pixbuf copy. An app that draws into an Image after setImage() sees the change at 200% but not at 100%.
  • Setting surface replaces the renderer's image, so its pixbuf property becomes NULL. GtkImageCellAccessible reads image size only from pixbuf, so ATK clients would see no image in cells at scale > 1.
  1. The stale-image mix-up is still there on GTK4.
    On GTK4, CELL_SURFACE stays G_TYPE_LONG, so getImage() still maps a raw (possibly freed) surface address back to an Image through ImageList. The new tests skip GTK4 rather than cover it. The underlying problem is that getImage() finds the Image by surface address. Keeping the Image reference per item on the Java side would fix it for both toolkits.

  2. Smaller points

  • cairoSurfaceType() looks the type up by the name "CairoSurface", relying on the pixbuf renderer class having registered it, and caches 0 if that fails. gtk_list_store_newv/gtk_tree_store_newv would then fail for every Table/Tree. Taking value_type from the renderer's surface pspec (g_object_class_find_property) seems more robust.
  • In the VIRTUAL path, setData = checkData (item); became if (!checkData (item)) return 0;. That changes behavior on GTK4 as well: when checkData fails, the custom-draw background/font/foreground setup further down is now skipped. If that's intended, a note in the commit message would help.

@vogella
vogella force-pushed the gtk-tree-cell-surface branch from 74ebb77 to 73b133a Compare October 6, 2026 12:14
@vogella

vogella commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all addressed and force-pushed. The image is now bound via gtk_tree_view_column_add_attribute (no per-row Java calls) and falls back to the dimmed pixbuf when disabled. Items keep their Image per cell, so getImage() no longer maps surface addresses, on GTK4 too. The surface type now comes from the pspec, and the checkData change is reverted. The remaining differences at >100% are noted in the commit message. Your stripes snippet stays crisp at scale 2.

@akurtakov

Copy link
Copy Markdown
Member

There are conflicts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Monitor-scale transitions retain stale surfaces, and ancestor-disabled controls keep undimmed images.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Runtime DPI changes reuse surfaces cached at the previous scale, leaving moved Trees and Tables blurry.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +1805 to +1807
long dpiChanged (long object, long arg0) {
long result = super.dpiChanged (object, arg0);
updateCellImageBinding ();
Comment on lines +2063 to +2065
long dpiChanged (long object, long arg0) {
long result = super.dpiChanged (object, arg0);
updateCellImageBinding ();
@akurtakov

Copy link
Copy Markdown
Member

I have split the getImage() part of this PR into #3695, so the bug fix can go in on its own while the HiDPI rendering is reviewed. #3695 contains:

  • the per-cell cellImage array in TableItem/TreeItem and its maintenance in createItem/destroyItem,
  • the six new tests,
  • one addition: getTextBounds() and the owner-draw measure/paint code now skip a disposed cell image. Otherwise an app that disposes an image still in use gets an exception on every repaint.

It needs no new natives, behaves the same at every scale, and fixes getImage() returning another item's image once a disposed image's ImageList slot is reused. The Table, Tree, item and column tests (389) pass on GTK3 and GTK4. You're kept as the commit author.
I hope you don't mind this approach as that part of the PR is easy to review and will make this PR easier strictly on the rendering thus easier to review.

akurtakov pushed a commit to akurtakov/eclipse.platform.swt that referenced this pull request Oct 9, 2026
getImage() mapped the cell's surface address back to an Image through
the ImageList. Once the slot of a disposed Image was reused, it returned
the Image of another cell, and a column created after another one was
disposed could report the old column's image. TableItem and TreeItem now
keep the Image of each cell in a per column array, maintained like
cellFont, and getImage() returns from it.

getTextBounds() and the owner draw measure and paint code ignore a
disposed cell image instead of failing in getBounds().

Split from eclipse-platform#3631.

Assisted-by: multiple AI agents and layers of automated tooling 🤖
Assisted-by: Anthropic Claude Code (claude-opus-5-5)
vogella added a commit to vogella/eclipse.platform.swt that referenced this pull request Oct 9, 2026
getImage() mapped the cell's surface address back to an Image through
the ImageList. Once the slot of a disposed Image was reused, it returned
the Image of another cell, and a column created after another one was
disposed could report the old column's image. TableItem and TreeItem now
keep the Image of each cell in a per column array, maintained like
cellFont, and getImage() returns from it.

getTextBounds() and the owner draw measure and paint code ignore a
disposed cell image instead of failing in getBounds().

Split from eclipse-platform#3631.

Assisted-by: multiple AI agents and layers of automated tooling 🤖
Assisted-by: Anthropic Claude Code (claude-opus-5-5)
@vogella
vogella force-pushed the gtk-tree-cell-surface branch from 6f860f9 to 35961bc Compare October 9, 2026 06:19
akurtakov pushed a commit that referenced this pull request Oct 9, 2026
getImage() mapped the cell's surface address back to an Image through
the ImageList. Once the slot of a disposed Image was reused, it returned
the Image of another cell, and a column created after another one was
disposed could report the old column's image. TableItem and TreeItem now
keep the Image of each cell in a per column array, maintained like
cellFont, and getImage() returns from it.

getTextBounds() and the owner draw measure and paint code ignore a
disposed cell image instead of failing in getBounds().

Split from #3631.

Assisted-by: multiple AI agents and layers of automated tooling 🤖
Assisted-by: Anthropic Claude Code (claude-opus-5-5)
@vogella
vogella force-pushed the gtk-tree-cell-surface branch from 35961bc to dd1c1ad Compare October 9, 2026 07:46
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 eclipse-platform#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 🤖

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

DPI transitions rebind stale cached surfaces without regenerating them for the new zoom.

2 open findings

🧠 Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants