Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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" }, //
Expand All @@ -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();
Expand Down Expand Up @@ -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
* <code>step</code> 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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions docs/CSS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <Lars.Vogel@vogella.com> - 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Loading