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()));