|
14 | 14 | import static org.junit.jupiter.api.Assertions.assertNotEquals; |
15 | 15 |
|
16 | 16 | import java.util.Arrays; |
| 17 | +import java.util.List; |
17 | 18 |
|
18 | 19 | import org.junit.jupiter.api.AfterEach; |
19 | 20 | import org.junit.jupiter.api.Assertions; |
|
22 | 23 |
|
23 | 24 | import org.eclipse.swt.SWT; |
24 | 25 | import org.eclipse.swt.custom.StyledText; |
| 26 | +import org.eclipse.swt.graphics.Font; |
| 27 | +import org.eclipse.swt.graphics.FontData; |
25 | 28 | import org.eclipse.swt.graphics.GC; |
26 | 29 | import org.eclipse.swt.graphics.Point; |
27 | 30 | import org.eclipse.swt.layout.FillLayout; |
@@ -123,4 +126,95 @@ public String getLabel() { |
123 | 126 | // https: //github.com/eclipse-platform/eclipse.platform.ui/issues/2786 |
124 | 127 | assertNotEquals(0, cut.getHeight()); // getHeight should not return 0, otherwise editor content starts jumping around |
125 | 128 | } |
| 129 | + |
| 130 | + /** |
| 131 | + * Verifies that a multi-line line-header code mining contributes a height which is an exact |
| 132 | + * multiple of the StyledText's regular line height (plus line spacing). Otherwise the line |
| 133 | + * drawn below the code mining would no longer be vertically aligned with the regular text |
| 134 | + * lines. This was visible on Windows with Consolas at odd point sizes (e.g. 9, 11, 13), where |
| 135 | + * {@code gc.stringExtent(line).y} returned a different value than |
| 136 | + * {@link StyledText#getLineHeight()}, so the lines below a multiline code mining were shifted |
| 137 | + * by a few pixels relative to the regular grid. |
| 138 | + */ |
| 139 | + @Test |
| 140 | + public void testTwoLineCodeMiningHeightMatchesTextWidgetLineHeight() throws Exception { |
| 141 | + var doc= fViewer.getDocument(); |
| 142 | + doc.set("line0\nline1\nline2"); |
| 143 | + var textWidget= fViewer.getTextWidget(); |
| 144 | + // A line spacing != 0 makes a regression in either factor immediately visible. |
| 145 | + textWidget.setLineSpacing(3); |
| 146 | + // Try to use Consolas at an odd size on Windows since this is the configuration in which |
| 147 | + // the original bug was reported. On other platforms / when Consolas is not available the |
| 148 | + // test still runs with the default font - the assertions below must hold for any font. |
| 149 | + Font consolasFont= tryCreateConsolasFont(textWidget, 11); |
| 150 | + try { |
| 151 | + if (consolasFont != null) { |
| 152 | + textWidget.setFont(consolasFont); |
| 153 | + } |
| 154 | + String codeMiningLabel= "code mining line1\ncode mining line2"; |
| 155 | + var mining= new LineHeaderCodeMining(0, doc, null, null) { |
| 156 | + @Override |
| 157 | + public String getLabel() { |
| 158 | + return codeMiningLabel; |
| 159 | + } |
| 160 | + }; |
| 161 | + int normalLineHeight= textWidget.getLineHeight(); |
| 162 | + int lineSpacing= textWidget.getLineSpacing(); |
| 163 | + int numMiningLines= codeMiningLabel.split("\n").length; |
| 164 | + int expectedMiningHeight= numMiningLines * (normalLineHeight + lineSpacing); |
| 165 | + |
| 166 | + // 1) The size returned by LineHeaderCodeMining.draw() must be a multiple of the |
| 167 | + // StyledText's regular line height + line spacing - it must not depend on |
| 168 | + // gc.stringExtent(...).y, which on Consolas/odd sizes differs from getLineHeight(). |
| 169 | + var gc= new GC(textWidget); |
| 170 | + try { |
| 171 | + Point result= mining.draw(gc, textWidget, null, 0, 0); |
| 172 | + assertEquals(expectedMiningHeight, result.y, |
| 173 | + "two-line code mining height must be 2 * (textWidget.getLineHeight() + lineSpacing)"); |
| 174 | + } finally { |
| 175 | + gc.dispose(); |
| 176 | + } |
| 177 | + |
| 178 | + // 2) CodeMiningLineHeaderAnnotation.getHeight(GC) drives the line vertical indent of the |
| 179 | + // line below the mining (see InlinedAnnotationDrawingStrategy). Its result must equal |
| 180 | + // expectedMiningHeight - meaning the first regular text line below the mining ends up |
| 181 | + // at exactly that pixel offset above its un-indented position, on the same vertical |
| 182 | + // grid as all other text lines. |
| 183 | + var annotation= new CodeMiningLineHeaderAnnotation(new Position(0, 0), fViewer); |
| 184 | + var support= new InlinedAnnotationSupport(); |
| 185 | + support.install(fViewer, new AnnotationPainter(fViewer, null)); |
| 186 | + var setSupport= AbstractInlinedAnnotation.class.getDeclaredMethod("setSupport", InlinedAnnotationSupport.class); |
| 187 | + setSupport.setAccessible(true); |
| 188 | + setSupport.invoke(annotation, support); |
| 189 | + annotation.update(List.of(mining), null); |
| 190 | + |
| 191 | + GC gc2= new GC(textWidget); |
| 192 | + try { |
| 193 | + int multilineHeight= annotation.getHeight(gc2); |
| 194 | + assertEquals(expectedMiningHeight, multilineHeight, |
| 195 | + "multiline mining height must be a whole-line multiple of the StyledText line height"); |
| 196 | + // The pixel position of the line directly below the code mining is |
| 197 | + // (multilineHeight) above its un-indented y position, and that offset must be a |
| 198 | + // whole number of regular text line heights so the lines below the mining stay |
| 199 | + // aligned with the regular grid. |
| 200 | + assertEquals(0, multilineHeight % (normalLineHeight + lineSpacing), |
| 201 | + "line below the code mining must land on a regular text-line boundary"); |
| 202 | + } finally { |
| 203 | + gc2.dispose(); |
| 204 | + } |
| 205 | + } finally { |
| 206 | + if (consolasFont != null) { |
| 207 | + textWidget.setFont(null); |
| 208 | + consolasFont.dispose(); |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + private static Font tryCreateConsolasFont(StyledText textWidget, int height) { |
| 214 | + FontData[] available= textWidget.getDisplay().getFontList("Consolas", true); //$NON-NLS-1$ |
| 215 | + if (available == null || available.length == 0) { |
| 216 | + return null; |
| 217 | + } |
| 218 | + return new Font(textWidget.getDisplay(), "Consolas", height, SWT.NORMAL); //$NON-NLS-1$ |
| 219 | + } |
126 | 220 | } |
0 commit comments