diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/internal/css/swt/FontWeights.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/internal/css/swt/FontWeights.java index e11b71b170b..efa7c3d2f70 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/internal/css/swt/FontWeights.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/internal/css/swt/FontWeights.java @@ -17,6 +17,7 @@ import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Objects; +import java.util.stream.IntStream; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.FontData; @@ -31,7 +32,7 @@ public final class FontWeights { private static final String[][] FACE_NAMES = { // { "Thin", "Hairline" }, // { "ExtraLight", "Extra Light", "UltraLight", "Ultra Light" }, // - { "Light" }, // + { "Light", "SemiLight", "Semi Light" }, // { "Regular" }, // { "Medium" }, // { "SemiBold", "Semi Bold", "DemiBold", "Demi Bold" }, // @@ -45,6 +46,8 @@ public final class FontWeights { private static final int REGULAR_STEP = 4; + private static final int MEDIUM_STEP = 5; + private static final int BOLD_STEP = 7; private static final Field FACE_FIELD = faceField(); @@ -75,6 +78,27 @@ public static void apply(FontData fontData, int weight) { } } + /** + * The weight steps (weight / 100) to look for in turn when a face of + * step is wanted, nearest first as CSS font matching orders + * them. It stops before regular or bold, which every family has. + */ + public static int[] stepsToTry(int step) { + IntStream.Builder steps = IntStream.builder(); + int direction = step <= MEDIUM_STEP ? -1 : 1; + for (int candidate = step; candidate >= 1 && candidate <= FACE_NAMES.length; candidate += direction) { + if (candidate == REGULAR_STEP || candidate == BOLD_STEP) { + return steps.build().toArray(); + } + steps.add(candidate); + } + for (int candidate = step - direction; candidate != REGULAR_STEP + && candidate != BOLD_STEP; candidate -= direction) { + steps.add(candidate); + } + return steps.build().toArray(); + } + /** * Whether two font data use the same face, which {@link FontData#equals} * ignores on GTK and macOS. @@ -111,21 +135,32 @@ private static void applyPangoWeight(FontData fontData, int step) throws Illegal FACE_FIELD.set(fontData, description.toString().getBytes(StandardCharsets.UTF_8)); } - /** GDI ships other weights as families of their own, like "Segoe UI Semibold". */ + /** + * GDI ships other weights as families of their own, like "Segoe UI Semibold". + * Without such a face the regular or bold of the style is kept, since GDI + * would embolden the regular face for a weight like 600. + */ private static void applyLogFontWeight(FontData fontData, int step) throws ReflectiveOperationException { Display display = Display.getCurrent(); - if (display != null) { - for (String face : FACE_NAMES[step - 1]) { + if (display == null) { + return; + } + for (int candidate : stepsToTry(step)) { + for (String face : FACE_NAMES[candidate - 1]) { FontData[] family = display.getFontList(fontData.getName() + ' ' + face, true); if (family.length > 0) { // GDI matches case-insensitively, take the installed spelling fontData.setName(family[0].getName()); - break; + setLogFontWeight(fontData, candidate * 100); + return; } } } + } + + private static void setLogFontWeight(FontData fontData, int weight) throws ReflectiveOperationException { Object logFont = FontData.class.getField("data").get(fontData); - logFont.getClass().getField("lfWeight").setInt(logFont, step * 100); + logFont.getClass().getField("lfWeight").setInt(logFont, weight); } private static void applyFace(FontData fontData, int step) throws IllegalAccessException { @@ -134,13 +169,16 @@ private static void applyFace(FontData fontData, int step) throws IllegalAccessE return; } boolean italic = (fontData.getStyle() & SWT.ITALIC) != 0; - for (FontData face : display.getFontList(fontData.getName(), true)) { - String nsName = (String) FACE_FIELD.get(face); - if (((face.getStyle() & SWT.ITALIC) != 0) == italic && nsName != null && isFace(nsName, step)) { - // setStyle clears nsName, and bold would embolden the face again - fontData.setStyle(fontData.getStyle() & ~SWT.BOLD); - FACE_FIELD.set(fontData, nsName); - return; + FontData[] faces = display.getFontList(fontData.getName(), true); + for (int candidate : stepsToTry(step)) { + for (FontData face : faces) { + String nsName = (String) FACE_FIELD.get(face); + if (((face.getStyle() & SWT.ITALIC) != 0) == italic && nsName != null && isFace(nsName, candidate)) { + // setStyle clears nsName, and bold would embolden the face again + fontData.setStyle(fontData.getStyle() & ~SWT.BOLD); + FACE_FIELD.set(fontData, nsName); + return; + } } } } diff --git a/docs/CSS.md b/docs/CSS.md index 54302db507b..b91605326d0 100644 --- a/docs/CSS.md +++ b/docs/CSS.md @@ -137,9 +137,10 @@ It also accepts the face names designers use, which are not standard CSS: The names are case-insensitive and may be written without the hyphen, as in `semibold`. They are only understood by `font-weight`, not in the `font` shorthand, where they could be part of a family name. -A weight between regular and bold, such as `500` or `600`, selects the installed face of that weight, for example Semibold. +A weight other than `400` or `700` selects the installed face of that weight, for example Semibold for `600`. On GTK Pango picks the closest face of the family, on Windows a family such as "Segoe UI Semibold" is used when it is installed, and on macOS a face of the family with a matching name such as `HelveticaNeue-Medium`. -Where no such face is found, a weight of `600` or more selects the bold face and anything below it the regular one. +Where the family has no face of that weight, the nearest one is taken in the order of CSS font matching: below `400` lighter faces first, above `500` heavier ones first, so `200` can select Light and `800` Black. +If no face is found before regular or bold, a weight of `600` or more selects the bold face and anything below it the regular one. In the `font` shorthand a bare `100` to `900` is read as a weight, any other bare number as a size in points. diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/CssSwtTestSuite.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/CssSwtTestSuite.java index 7065dba652b..3ab00038eb8 100644 --- a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/CssSwtTestSuite.java +++ b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/CssSwtTestSuite.java @@ -42,6 +42,7 @@ import org.eclipse.e4.ui.tests.css.swt.CompositeTest; import org.eclipse.e4.ui.tests.css.swt.DescendentTest; import org.eclipse.e4.ui.tests.css.swt.FontDefinitionTest; +import org.eclipse.e4.ui.tests.css.swt.FontWeightsTest; import org.eclipse.e4.ui.tests.css.swt.GradientTest; import org.eclipse.e4.ui.tests.css.swt.IEclipsePreferencesPseudoKeyTest; import org.eclipse.e4.ui.tests.css.swt.IEclipsePreferencesTest; @@ -68,6 +69,7 @@ @SelectClasses({ CSSSWTFontHelperTest.class, CSSSWTColorHelperTest.class, SWTStyleHelpersTest.class, CSSResourcesHelpersTest.class, SWTResourceRegistryKeyFactoryTest.class, SWTResourcesRegistryTest.class, FontDefinitionTest.class, + FontWeightsTest.class, ColorDefinitionTest.class, ThemesExtensionTest.class, IEclipsePreferencesTest.class, EclipsePreferencesHelperTest.class, CSSSWTWidgetTest.class, LabelTest.class, LinkTest.class, CTabFolderTest.class, CTabFolderActiveClassTest.class, CTabItemTest.class, CTabItemSelectionTest.class, diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/FontWeightsTest.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/FontWeightsTest.java new file mode 100644 index 00000000000..bd3f73b8580 --- /dev/null +++ b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/FontWeightsTest.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2026 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.tests.css.swt; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import org.eclipse.e4.ui.internal.css.swt.FontWeights; +import org.junit.jupiter.api.Test; + +public class FontWeightsTest { + + @Test + void testLightWeightsTryLighterFacesFirst() { + assertArrayEquals(new int[] { 1, 2, 3 }, FontWeights.stepsToTry(1)); + assertArrayEquals(new int[] { 2, 1, 3 }, FontWeights.stepsToTry(2)); + assertArrayEquals(new int[] { 3, 2, 1 }, FontWeights.stepsToTry(3)); + } + + @Test + void testMediumFallsBackToRegular() { + assertArrayEquals(new int[] { 5 }, FontWeights.stepsToTry(5)); + } + + @Test + void testSemiBoldFallsBackToBold() { + assertArrayEquals(new int[] { 6 }, FontWeights.stepsToTry(6)); + } + + @Test + void testHeavyWeightsTryHeavierFacesFirst() { + assertArrayEquals(new int[] { 8, 9 }, FontWeights.stepsToTry(8)); + assertArrayEquals(new int[] { 9, 8 }, FontWeights.stepsToTry(9)); + } +} diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/LabelTest.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/LabelTest.java index b972e716cfe..6173b603250 100644 --- a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/LabelTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/LabelTest.java @@ -98,6 +98,42 @@ void testFontWeightSelectsTheSemiBoldFaceOnWindows() { assertEquals("Segoe UI Semibold", labelToTest.getFont().getFontData()[0].getName()); } + @Test + void testFontWeightFallsBackToTheNearestLighterFaceOnWindows() { + assumeTrue("win32".equals(SWT.getPlatform())); + assumeTrue(css.getDisplay().getFontList("Segoe UI Light", true).length > 0); + assumeTrue(css.getDisplay().getFontList("Segoe UI ExtraLight", true).length == 0); + + Label labelToTest = css.createTestLabel("Label { font: 200 12pt \"Segoe UI\" }"); + + assertEquals("Segoe UI Light", labelToTest.getFont().getFontData()[0].getName()); + } + + @Test + void testFontWeightFallsBackToTheNearestHeavierFaceOnWindows() { + assumeTrue("win32".equals(SWT.getPlatform())); + assumeTrue(css.getDisplay().getFontList("Segoe UI Black", true).length > 0); + assumeTrue(css.getDisplay().getFontList("Segoe UI ExtraBold", true).length == 0); + + Label labelToTest = css.createTestLabel("Label { font: 800 12pt \"Segoe UI\" }"); + + assertEquals("Segoe UI Black", labelToTest.getFont().getFontData()[0].getName()); + } + + @Test + void testFontWeightWithoutSemiBoldFaceIsBoldOnWindows() { + assumeTrue("win32".equals(SWT.getPlatform())); + assumeTrue(css.getDisplay().getFontList("Calibri", true).length > 0); + assumeTrue(css.getDisplay().getFontList("Calibri Semibold", true).length == 0); + + Label labelToTest = css.createTestLabel("Label { font: 600 12pt Calibri }"); + + FontData fontData = labelToTest.getFont().getFontData()[0]; + assertEquals("Calibri", fontData.getName()); + // GDI reports bold only for a weight of exactly 700 + assertEquals(SWT.BOLD, fontData.getStyle()); + } + @Test void testFontWeightSelectsTheMediumFaceOnMac() throws ReflectiveOperationException { assumeTrue("cocoa".equals(SWT.getPlatform()));