From 422d4c21a1893899365822a467e8e907a4305b68 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 8 Oct 2026 13:27:32 +0200 Subject: [PATCH] Fall back to the nearest installed face for CSS font weights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A weight without a face of its own, such as 200 or 800 for Segoe UI, now takes the nearest installed face in the order of CSS font matching instead of the regular or bold one, and SemiLight counts as a Light face. On Windows a 600 without a Semibold face stays bold instead of letting GDI embolden the regular face. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../e4/ui/internal/css/swt/FontWeights.java | 64 +++++++++++++++---- docs/CSS.md | 5 +- .../e4/ui/tests/css/CssSwtTestSuite.java | 2 + .../e4/ui/tests/css/swt/FontWeightsTest.java | 45 +++++++++++++ .../e4/ui/tests/css/swt/LabelTest.java | 36 +++++++++++ 5 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/FontWeightsTest.java 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()));