Skip to content

Commit 089b9c4

Browse files
authored
Map native:* font handles to installed iOS-family fonts in JavaSE simulator and add tests/docs (#4708)
* Add integration-style simulator font loading tests * Handle headless environments in retina scale detection
1 parent 91f1977 commit 089b9c4

4 files changed

Lines changed: 212 additions & 42 deletions

File tree

Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java

Lines changed: 105 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ public class JavaSEPort extends CodenameOneImplementation {
188188

189189

190190
private static final int ICON_SIZE=24;
191+
private static final Map<String, String[]> IOS_NATIVE_FONT_CANDIDATES = new HashMap<String, String[]>();
192+
private static Set<String> availableFontNamesLowercase;
191193
private static final String PREF_AUTO_UPDATE_DEFAULT_BUNDLE = "cn1.autoDefaultResourceBundle";
192194
public final static boolean IS_MAC;
193195
private static boolean isIOS;
@@ -198,6 +200,59 @@ public class JavaSEPort extends CodenameOneImplementation {
198200
private AutoLocalizationBundle autoLocalizationBundle;
199201
private boolean autoUpdateDefaultResourceBundle;
200202

203+
static {
204+
IOS_NATIVE_FONT_CANDIDATES.put("native:MainThin", new String[] {
205+
"SF Pro Display", "SF Pro Text",
206+
".SF NS Text", ".SF NS Display", "SF UI Text",
207+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
208+
});
209+
IOS_NATIVE_FONT_CANDIDATES.put("native:MainLight", new String[] {
210+
"SF Pro Text", "SF Pro Display",
211+
".SF NS Text", ".SF NS Display", "SF UI Text",
212+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
213+
});
214+
IOS_NATIVE_FONT_CANDIDATES.put("native:MainRegular", new String[] {
215+
"SF Pro Text", "SF Pro Display", "SF UI Text", "San Francisco",
216+
".SF NS Text", ".SF NS Display",
217+
"Helvetica Neue", "HelveticaNeue"
218+
});
219+
IOS_NATIVE_FONT_CANDIDATES.put("native:MainBold", new String[] {
220+
"SF Pro Text", "SF Pro Display",
221+
".SF NS Text", ".SF NS Display", "SF UI Text",
222+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
223+
});
224+
IOS_NATIVE_FONT_CANDIDATES.put("native:MainBlack", new String[] {
225+
"SF Pro Display", "SF Pro Text",
226+
".SF NS Display", ".SF NS Text", "SF UI Text",
227+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
228+
});
229+
IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicThin", new String[] {
230+
"SF Pro Display", "SF Pro Text",
231+
".SF NS Text", ".SF NS Display", "SF UI Text",
232+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
233+
});
234+
IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicLight", new String[] {
235+
"SF Pro Text", "SF Pro Display",
236+
".SF NS Text", ".SF NS Display", "SF UI Text",
237+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
238+
});
239+
IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicRegular", new String[] {
240+
"SF Pro Text", "SF Pro Display", "SF UI Text", "San Francisco",
241+
".SF NS Text", ".SF NS Display",
242+
"Helvetica Neue", "HelveticaNeue"
243+
});
244+
IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicBold", new String[] {
245+
"SF Pro Text", "SF Pro Display",
246+
".SF NS Text", ".SF NS Display", "SF UI Text",
247+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
248+
});
249+
IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicBlack", new String[] {
250+
"SF Pro Display", "SF Pro Text",
251+
".SF NS Display", ".SF NS Text", "SF UI Text",
252+
"San Francisco", "Helvetica Neue", "HelveticaNeue"
253+
});
254+
}
255+
201256
/**
202257
* @return the fullScreen
203258
*/
@@ -386,9 +441,9 @@ private static int getJavaVersion() {
386441

387442
public static boolean isRetina() {
388443
boolean isRetina = false;
389-
GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
390444

391445
try {
446+
GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
392447
if (getJavaVersion() >= 9) {
393448
// JDK9 Doesn't like the old hack for getting the scale via reflection.
394449
// https://bugs.openjdk.java.net/browse/JDK-8172962
@@ -420,10 +475,8 @@ public static boolean isRetina() {
420475
}
421476

422477
public static double calcRetinaScale() {
423-
424-
GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
425-
426478
try {
479+
GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
427480
if (getJavaVersion() >= 9) {
428481
// JDK9 Doesn't like the old hack for getting the scale via reflection.
429482
// https://bugs.openjdk.java.net/browse/JDK-8172962
@@ -8057,54 +8110,65 @@ public boolean isNativeFontSchemeSupported() {
80578110
return true;
80588111
}
80598112

8060-
private String nativeFontName(String fontName) {
8061-
if(fontName != null && fontName.startsWith("native:")) {
8062-
if("native:MainThin".equals(fontName)) {
8063-
return "HelveticaNeue-UltraLight";
8064-
}
8065-
if("native:MainLight".equals(fontName)) {
8066-
return "HelveticaNeue-Light";
8067-
}
8068-
if("native:MainRegular".equals(fontName)) {
8069-
return "HelveticaNeue-Medium";
8070-
}
8071-
8072-
if("native:MainBold".equals(fontName)) {
8073-
return "HelveticaNeue-Bold";
8074-
}
8075-
8076-
if("native:MainBlack".equals(fontName)) {
8077-
return "HelveticaNeue-CondensedBlack";
8113+
private static synchronized Set<String> getAvailableFontNamesLowercase() {
8114+
if (availableFontNamesLowercase == null) {
8115+
HashSet<String> out = new HashSet<String>();
8116+
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
8117+
String[] families = ge.getAvailableFontFamilyNames();
8118+
for (String family : families) {
8119+
out.add(family.toLowerCase(Locale.US));
80788120
}
8079-
8080-
if("native:ItalicThin".equals(fontName)) {
8081-
return "HelveticaNeue-UltraLightItalic";
8082-
}
8083-
8084-
if("native:ItalicLight".equals(fontName)) {
8085-
return "HelveticaNeue-LightItalic";
8086-
}
8087-
8088-
if("native:ItalicRegular".equals(fontName)) {
8089-
return "HelveticaNeue-MediumItalic";
8090-
}
8091-
8092-
if("native:ItalicBold".equals(fontName) || "native:ItalicBlack".equals(fontName)) {
8093-
return "HelveticaNeue-BoldItalic";
8121+
availableFontNamesLowercase = out;
8122+
}
8123+
return availableFontNamesLowercase;
8124+
}
8125+
8126+
static void setAvailableFontNamesLowercaseForTest(Set<String> fontNames) {
8127+
availableFontNamesLowercase = fontNames;
8128+
}
8129+
8130+
static void clearAvailableFontNamesLowercaseForTest() {
8131+
availableFontNamesLowercase = null;
8132+
}
8133+
8134+
static String findFirstInstalledFontCandidate(String[] candidates, Set<String> installedFontNames) {
8135+
if (candidates == null || installedFontNames == null) {
8136+
return null;
8137+
}
8138+
for (String candidate : candidates) {
8139+
if (candidate != null && installedFontNames.contains(candidate.toLowerCase(Locale.US))) {
8140+
return candidate;
80948141
}
8095-
}
8142+
}
80968143
return null;
80978144
}
8145+
8146+
static String nativeFontNameForIOS(String fontName, Set<String> installedFontNames) {
8147+
if (fontName == null || !fontName.startsWith("native:")) {
8148+
return null;
8149+
}
8150+
String[] candidates = IOS_NATIVE_FONT_CANDIDATES.get(fontName);
8151+
return findFirstInstalledFontCandidate(candidates, installedFontNames);
8152+
}
8153+
8154+
private String nativeFontName(String fontName) {
8155+
if (!isIOS || fontName == null || !fontName.startsWith("native:")) {
8156+
return null;
8157+
}
8158+
return nativeFontNameForIOS(fontName, getAvailableFontNamesLowercase());
8159+
}
80988160

80998161
@Override
81008162
public Object loadTrueTypeFont(String fontName, String fileName) {
81018163
File fontFile = null;
81028164
try {
81038165
if(fontName.startsWith("native:")) {
8104-
if(IS_MAC && isIOS) {
8166+
if(isIOS) {
81058167
String nn = nativeFontName(fontName);
8106-
java.awt.Font nf = new java.awt.Font(nn, java.awt.Font.PLAIN, medianFontSize);
8107-
return nf;
8168+
if (nn != null) {
8169+
java.awt.Font nf = new java.awt.Font(nn, java.awt.Font.PLAIN, medianFontSize);
8170+
return nf;
8171+
}
81088172
}
81098173
String res;
81108174
switch(fontName) {

docs/developer-guide/Theme-Basics.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ Notice that, in code, only pixel sizes are supported, so it’s up to you to dec
450450

451451
The font name is the difficult bit, iOS requires the name of the font in order to load the font. This font name doesn't always correlate to the file name making this task rather "tricky". The actual font name is sometimes viewable within a font viewer. It isn't always intuitive, so be sure to test that on the device to make sure you got it right.
452452

453-
IMPORTANT: due to copyright restrictions we cannot distribute Helvetica and thus can't simulate it. In the simulator you will see Roboto and not the device font unless you are running on a Mac
453+
IMPORTANT: Due to licensing restrictions Codename One doesn't bundle Apple's iOS fonts. In the simulator with an iOS skin we try to use installed San Francisco/SF Pro (or Helvetica Neue) fonts when available on your machine; otherwise we fall back to bundled Roboto. You can obtain Apple's font downloads and terms at https://developer.apple.com/fonts/
454454

455455
The code below demonstrates all the major fonts available in Codename One with the handlee ttf file posing as a standin for arbitrary TTF:
456456

docs/website/content/faq.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ Not if you use the Codename One cloud build service. The cloud handles iOS compi
3333

3434
If you build fully offline, Apple tooling still requires macOS for iOS builds and submission workflows.
3535

36+
### Do `native:*` fonts in the JavaSE simulator match current iOS fonts?
37+
They can. The simulator now tries to use installed iOS-family fonts (San Francisco/SF Pro first, then Helvetica Neue) when the app runs with an iOS simulator skin.
38+
39+
If those fonts are not installed on the host OS, the simulator falls back to bundled Roboto fonts so behavior remains consistent.
40+
41+
### Can Codename One bundle Apple's San Francisco fonts?
42+
No. Codename One doesn't bundle Apple's proprietary iOS fonts. If you want exact iOS typography in the simulator on Windows/Linux, install the fonts separately under your own Apple license terms. Apple publishes font information and downloads at: https://developer.apple.com/fonts/
43+
44+
Practical setup guidance:
45+
46+
- **macOS**: You usually already have the required iOS font families installed with the OS/Xcode toolchain.
47+
- **Windows/Linux**: Install San Francisco/SF Pro fonts from Apple's official distribution channels for licensed developers, then restart the simulator.
48+
- If those fonts are unavailable, simulator rendering still works using Roboto fallback, but text metrics may differ from real iOS devices.
49+
3650
### How does performance compare to native or HTML-based solutions?
3751
Codename One compiles to native targets and is designed for production-level performance, including optimized rendering and modern VM/runtime improvements.
3852

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.codename1.impl.javase;
2+
3+
import java.util.HashSet;
4+
import java.util.Set;
5+
import java.lang.reflect.Field;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.AfterEach;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
import static org.junit.jupiter.api.Assertions.assertNull;
12+
13+
public class JavaSEPortFontMappingTest {
14+
15+
private Boolean originalIsIOS;
16+
17+
@AfterEach
18+
public void tearDown() throws Exception {
19+
JavaSEPort.clearAvailableFontNamesLowercaseForTest();
20+
if (originalIsIOS != null) {
21+
setIsIOS(originalIsIOS.booleanValue());
22+
}
23+
}
24+
25+
private void setIsIOS(boolean value) throws Exception {
26+
Field f = JavaSEPort.class.getDeclaredField("isIOS");
27+
f.setAccessible(true);
28+
if (originalIsIOS == null) {
29+
originalIsIOS = Boolean.valueOf(f.getBoolean(null));
30+
}
31+
f.setBoolean(null, value);
32+
}
33+
34+
@Test
35+
public void testFindFirstInstalledFontCandidateUsesCandidateOrder() {
36+
Set<String> installed = new HashSet<String>();
37+
installed.add("sf pro display");
38+
installed.add("helvetica neue");
39+
40+
String out = JavaSEPort.findFirstInstalledFontCandidate(
41+
new String[] {"SF Pro Text", "SF Pro Display", "Helvetica Neue"},
42+
installed
43+
);
44+
45+
assertEquals("SF Pro Display", out);
46+
}
47+
48+
@Test
49+
public void testNativeFontNameForIOSReturnsNullWhenNoCandidatesInstalled() {
50+
Set<String> installed = new HashSet<String>();
51+
installed.add("roboto");
52+
53+
String out = JavaSEPort.nativeFontNameForIOS("native:MainRegular", installed);
54+
assertNull(out);
55+
}
56+
57+
@Test
58+
public void testNativeFontNameForIOSReturnsFirstMatchingFamily() {
59+
Set<String> installed = new HashSet<String>();
60+
installed.add("sf pro text");
61+
installed.add("helvetica neue");
62+
63+
String out = JavaSEPort.nativeFontNameForIOS("native:ItalicRegular", installed);
64+
assertEquals("SF Pro Text", out);
65+
}
66+
67+
@Test
68+
public void testLoadTrueTypeFontUsesInstalledIOSCandidateWhenPresent() throws Exception {
69+
Set<String> installed = new HashSet<String>();
70+
installed.add("helvetica neue");
71+
JavaSEPort.setAvailableFontNamesLowercaseForTest(installed);
72+
setIsIOS(true);
73+
74+
JavaSEPort port = new JavaSEPort();
75+
Object out = port.loadTrueTypeFont("native:MainRegular", "native:MainRegular");
76+
77+
assertNotNull(out);
78+
assertEquals("Helvetica Neue", ((java.awt.Font) out).getName());
79+
}
80+
81+
@Test
82+
public void testLoadTrueTypeFontFallsBackWhenNoIOSFamilyInstalled() throws Exception {
83+
JavaSEPort.setAvailableFontNamesLowercaseForTest(new HashSet<String>());
84+
setIsIOS(true);
85+
86+
JavaSEPort port = new JavaSEPort();
87+
Object out = port.loadTrueTypeFont("native:MainRegular", "native:MainRegular");
88+
89+
assertNotNull(out);
90+
assertEquals(java.awt.Font.class, out.getClass());
91+
}
92+
}

0 commit comments

Comments
 (0)