From 4095c6df4bc30390b903bf3ddfb72ca64903f854 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sat, 8 Aug 2026 09:41:45 +0200 Subject: [PATCH 1/2] [win32] Fix collapsed tab stops at fractional zoom levels TextLayout.setTabs() takes the tab stops in points, but computeRuns() converted them to pixels and compared them against a pen position accumulated from raw glyph advances. At zoom levels that are not a multiple of 100 the point/pixel round trip can place a stop one pixel past a pen position that actually sits exactly on that stop, so the tab advanced by a single pixel instead of moving to the next stop. StyledText hits this because it measures its tab width as the width of N spaces in points and passes that back as the only tab stop, so a tab following N spaces lands precisely on the stop. It became visible in 4.36 when monitor-specific scaling turned the effective auto-scale from "integer" into "quarter", exposing zoom 125, 150 and 175. Only the selection of the stop was ever wrong, so both comparisons that ask whether a stop is still ahead of the pen now happen in points, the unit the stops were defined in. Once a stop has been picked, its pixel position is still used unchanged for the run width, and the block that adjusts merged consecutive tabs is untouched. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/3052 --- .../swt/graphics/TextLayoutWin32Tests.java | 59 ++++++++++++++++++- .../org/eclipse/swt/graphics/TextLayout.java | 12 +++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/TextLayoutWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/TextLayoutWin32Tests.java index 33957562d0a..7a384620f92 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/TextLayoutWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/TextLayoutWin32Tests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Yatta Solutions + * Copyright (c) 2024, 2026 Yatta Solutions and others * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -15,16 +15,26 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.swt.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.widgets.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.params.*; +import org.junit.jupiter.params.provider.*; @ExtendWith(PlatformSpecificExecutionExtension.class) @ExtendWith(WithMonitorSpecificScalingExtension.class) class TextLayoutWin32Tests { final static String text = "This is a text for testing."; + private static final String MONOSPACED_FONT = "Courier New"; + private static final int TAB_STOP_TOLERANCE_IN_POINTS = 2; @Test public void testGetBoundPublicAPIshouldReturnTheSameValueRegardlessOfZoomLevel() { @@ -70,4 +80,51 @@ public void testCalculateGetBoundsWithVerticalIndent() { assertEquals(unscaledBounds.height, scaledBounds.height, 1, "The public API for getBounds with vertical indent > 0 should give a similar result for any zoom level"); } + @ParameterizedTest + @ValueSource(ints = { 100, 125, 150, 175, 200 }) + public void testTabAfterSpacesReachesTheNextTabStop(int zoom) { + Display display = Display.getDefault(); + assumeTrue(isFontInstalled(display, MONOSPACED_FONT), MONOSPACED_FONT + " is not installed"); + + List violations = new ArrayList<>(); + for (int fontHeight = 8; fontHeight <= 20; fontHeight++) { + Font font = Font.win32_new(new Font(display, MONOSPACED_FONT, fontHeight, SWT.NORMAL), zoom); + for (int tabLength : new int[] { 2, 3, 4, 8 }) { + String spaces = " ".repeat(tabLength); + // StyledText derives its single tab stop from the width of a run of spaces + int tabWidth = boundsWidth(display, font, null, spaces); + int spacesThenTab = boundsWidth(display, font, new int[] { tabWidth }, spaces + "\t"); + int twiceTheSpaces = boundsWidth(display, font, new int[] { tabWidth }, spaces + spaces); + if (Math.abs(spacesThenTab - twiceTheSpaces) > TAB_STOP_TOLERANCE_IN_POINTS) { + violations.add(fontHeight + "pt with tab length " + tabLength + ": width " + spacesThenTab + + " instead of " + twiceTheSpaces); + } + } + } + + assertTrue(violations.isEmpty(), "A tab placed exactly on a tab stop must advance to the next one, but at zoom " + + zoom + "% it did not for " + violations); + } + + private static int boundsWidth(Display display, Font font, int[] tabs, String content) { + TextLayout layout = new TextLayout(display); + try { + layout.setFont(font); + layout.setTabs(tabs); + layout.setText(content); + return layout.getBounds().width; + } finally { + layout.dispose(); + } + } + + private static boolean isFontInstalled(Display display, String name) { + for (FontData fontData : display.getFontList(null, true)) { + if (name.equalsIgnoreCase(fontData.getName())) { + return true; + } + } + return false; + } + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java index 0dd41a0f02b..10e64785a95 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2022 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -387,9 +387,15 @@ void computeRuns (GC gc) { for (int i=0; i lineWidth) { + if (tabs[j] > lineWidthInPoints) { run.width = tabsInPixels[j] - lineWidth; break; } @@ -398,7 +404,7 @@ void computeRuns (GC gc) { int tabX = tabsInPixels[tabsLength-1]; int lastTabWidth = tabsLength > 1 ? tabsInPixels[tabsLength-1] - tabsInPixels[tabsLength-2] : tabsInPixels[0]; if (lastTabWidth > 0) { - while (tabX <= lineWidth) tabX += lastTabWidth; + while (DPIUtil.pixelToPoint(tabX, getZoom(gc)) <= lineWidthInPoints) tabX += lastTabWidth; run.width = tabX - lineWidth; } } From 2cae3e01f5205f063d7f1f18d7ee9f1107e88d81 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 10 Aug 2026 11:46:55 +0200 Subject: [PATCH 2/2] [win32] Resolve tab stops entirely in points in TextLayout.computeRuns() The tab stop handling mixed units: the stop was selected in points while the resulting position and the adjustment for merged consecutive tabs were accumulated from the pixel-converted stops. That works, but every step past the last stop rounds separately, so the positions can drift by a pixel from the stop the caller defined, and reasoning about the block requires tracking two units at once. Keep the whole computation in points and convert once, when the final position is known. This removes the pixel copy of the stops and makes the merged-tab adjustment operate on the same values as the lookup. The degenerate case of a non-increasing tabs array now leaves the run at its measured glyph advance rather than adding a non-positive stop delta to it, which was meaningless anyway. No behavior change for well-formed tab stops. --- .../org/eclipse/swt/graphics/TextLayout.java | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java index 10e64785a95..6d5bd171da4 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java @@ -379,33 +379,37 @@ void computeRuns (GC gc) { } SCRIPT_LOGATTR logAttr = new SCRIPT_LOGATTR(); SCRIPT_PROPERTIES properties = new SCRIPT_PROPERTIES(); - int wrapIndentInPixels = DPIUtil.pointToPixel(wrapIndent, getZoom(gc)); - int indentInPixels = DPIUtil.pointToPixel(indent, getZoom(gc)); - int wrapWidthInPixels = DPIUtil.pointToPixel(wrapWidth, getZoom(gc)); - int[] tabsInPixels = Win32DPIUtils.pointToPixel(tabs, getZoom(gc)); + int zoom = getZoom(gc); + int wrapIndentInPixels = DPIUtil.pointToPixel(wrapIndent, zoom); + int indentInPixels = DPIUtil.pointToPixel(indent, zoom); + int wrapWidthInPixels = DPIUtil.pointToPixel(wrapWidth, zoom); int lineWidth = indentInPixels, lineStart = 0, lineCount = 1; for (int i=0; i lineWidthInPoints) { - run.width = tabsInPixels[j] - lineWidth; + widthInPoints = tabs[j] - lineWidthInPoints; + stopFound = true; break; } } if (j == tabsLength) { - int tabX = tabsInPixels[tabsLength-1]; - int lastTabWidth = tabsLength > 1 ? tabsInPixels[tabsLength-1] - tabsInPixels[tabsLength-2] : tabsInPixels[0]; + int tabX = tabs[tabsLength-1]; + int lastTabWidth = tabsLength > 1 ? tabs[tabsLength-1] - tabs[tabsLength-2] : tabs[0]; if (lastTabWidth > 0) { - while (DPIUtil.pixelToPoint(tabX, getZoom(gc)) <= lineWidthInPoints) tabX += lastTabWidth; - run.width = tabX - lineWidth; + while (tabX <= lineWidthInPoints) tabX += lastTabWidth; + widthInPoints = tabX - lineWidthInPoints; + stopFound = true; } } @@ -414,19 +418,22 @@ void computeRuns (GC gc) { * The extra tabs are removed in merge. */ int length = run.length; - if (length > 1) { + if (length > 1 && stopFound) { int stop = j + length - 1; if (stop < tabsLength) { - run.width += tabsInPixels[stop] - tabsInPixels[j]; + widthInPoints += tabs[stop] - tabs[j]; } else { if (j < tabsLength) { - run.width += tabsInPixels[tabsLength-1] - tabsInPixels[j]; + widthInPoints += tabs[tabsLength-1] - tabs[j]; length -= (tabsLength - 1) - j; } - int lastTabWidth = tabsLength > 1 ? tabsInPixels[tabsLength-1] - tabsInPixels[tabsLength-2] : tabsInPixels[0]; - run.width += lastTabWidth * (length - 1); + int lastTabWidth = tabsLength > 1 ? tabs[tabsLength-1] - tabs[tabsLength-2] : tabs[0]; + widthInPoints += lastTabWidth * (length - 1); } } + if (stopFound) { + run.width = DPIUtil.pointToPixel(lineWidthInPoints + widthInPoints, zoom) - lineWidth; + } } if (wrapWidth != -1 && lineWidth + run.width > wrapWidthInPixels && !run.tab && !run.lineBreak) { int start = 0;