diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index d894ceef8f..bcf4c861e1 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -188,6 +188,8 @@ public class JavaSEPort extends CodenameOneImplementation { private static final int ICON_SIZE=24; + private static final Map IOS_NATIVE_FONT_CANDIDATES = new HashMap(); + private static Set availableFontNamesLowercase; private static final String PREF_AUTO_UPDATE_DEFAULT_BUNDLE = "cn1.autoDefaultResourceBundle"; public final static boolean IS_MAC; private static boolean isIOS; @@ -198,6 +200,59 @@ public class JavaSEPort extends CodenameOneImplementation { private AutoLocalizationBundle autoLocalizationBundle; private boolean autoUpdateDefaultResourceBundle; + static { + IOS_NATIVE_FONT_CANDIDATES.put("native:MainThin", new String[] { + "SF Pro Display", "SF Pro Text", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:MainLight", new String[] { + "SF Pro Text", "SF Pro Display", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:MainRegular", new String[] { + "SF Pro Text", "SF Pro Display", "SF UI Text", "San Francisco", + ".SF NS Text", ".SF NS Display", + "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:MainBold", new String[] { + "SF Pro Text", "SF Pro Display", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:MainBlack", new String[] { + "SF Pro Display", "SF Pro Text", + ".SF NS Display", ".SF NS Text", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicThin", new String[] { + "SF Pro Display", "SF Pro Text", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicLight", new String[] { + "SF Pro Text", "SF Pro Display", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicRegular", new String[] { + "SF Pro Text", "SF Pro Display", "SF UI Text", "San Francisco", + ".SF NS Text", ".SF NS Display", + "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicBold", new String[] { + "SF Pro Text", "SF Pro Display", + ".SF NS Text", ".SF NS Display", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + IOS_NATIVE_FONT_CANDIDATES.put("native:ItalicBlack", new String[] { + "SF Pro Display", "SF Pro Text", + ".SF NS Display", ".SF NS Text", "SF UI Text", + "San Francisco", "Helvetica Neue", "HelveticaNeue" + }); + } + /** * @return the fullScreen */ @@ -386,9 +441,9 @@ private static int getJavaVersion() { public static boolean isRetina() { boolean isRetina = false; - GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); try { + GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); if (getJavaVersion() >= 9) { // JDK9 Doesn't like the old hack for getting the scale via reflection. // https://bugs.openjdk.java.net/browse/JDK-8172962 @@ -420,10 +475,8 @@ public static boolean isRetina() { } public static double calcRetinaScale() { - - GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); - try { + GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); if (getJavaVersion() >= 9) { // JDK9 Doesn't like the old hack for getting the scale via reflection. // https://bugs.openjdk.java.net/browse/JDK-8172962 @@ -8057,54 +8110,65 @@ public boolean isNativeFontSchemeSupported() { return true; } - private String nativeFontName(String fontName) { - if(fontName != null && fontName.startsWith("native:")) { - if("native:MainThin".equals(fontName)) { - return "HelveticaNeue-UltraLight"; - } - if("native:MainLight".equals(fontName)) { - return "HelveticaNeue-Light"; - } - if("native:MainRegular".equals(fontName)) { - return "HelveticaNeue-Medium"; - } - - if("native:MainBold".equals(fontName)) { - return "HelveticaNeue-Bold"; - } - - if("native:MainBlack".equals(fontName)) { - return "HelveticaNeue-CondensedBlack"; + private static synchronized Set getAvailableFontNamesLowercase() { + if (availableFontNamesLowercase == null) { + HashSet out = new HashSet(); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] families = ge.getAvailableFontFamilyNames(); + for (String family : families) { + out.add(family.toLowerCase(Locale.US)); } - - if("native:ItalicThin".equals(fontName)) { - return "HelveticaNeue-UltraLightItalic"; - } - - if("native:ItalicLight".equals(fontName)) { - return "HelveticaNeue-LightItalic"; - } - - if("native:ItalicRegular".equals(fontName)) { - return "HelveticaNeue-MediumItalic"; - } - - if("native:ItalicBold".equals(fontName) || "native:ItalicBlack".equals(fontName)) { - return "HelveticaNeue-BoldItalic"; + availableFontNamesLowercase = out; + } + return availableFontNamesLowercase; + } + + static void setAvailableFontNamesLowercaseForTest(Set fontNames) { + availableFontNamesLowercase = fontNames; + } + + static void clearAvailableFontNamesLowercaseForTest() { + availableFontNamesLowercase = null; + } + + static String findFirstInstalledFontCandidate(String[] candidates, Set installedFontNames) { + if (candidates == null || installedFontNames == null) { + return null; + } + for (String candidate : candidates) { + if (candidate != null && installedFontNames.contains(candidate.toLowerCase(Locale.US))) { + return candidate; } - } + } return null; } + + static String nativeFontNameForIOS(String fontName, Set installedFontNames) { + if (fontName == null || !fontName.startsWith("native:")) { + return null; + } + String[] candidates = IOS_NATIVE_FONT_CANDIDATES.get(fontName); + return findFirstInstalledFontCandidate(candidates, installedFontNames); + } + + private String nativeFontName(String fontName) { + if (!isIOS || fontName == null || !fontName.startsWith("native:")) { + return null; + } + return nativeFontNameForIOS(fontName, getAvailableFontNamesLowercase()); + } @Override public Object loadTrueTypeFont(String fontName, String fileName) { File fontFile = null; try { if(fontName.startsWith("native:")) { - if(IS_MAC && isIOS) { + if(isIOS) { String nn = nativeFontName(fontName); - java.awt.Font nf = new java.awt.Font(nn, java.awt.Font.PLAIN, medianFontSize); - return nf; + if (nn != null) { + java.awt.Font nf = new java.awt.Font(nn, java.awt.Font.PLAIN, medianFontSize); + return nf; + } } String res; switch(fontName) { diff --git a/docs/developer-guide/Theme-Basics.asciidoc b/docs/developer-guide/Theme-Basics.asciidoc index 31055ade01..5b483c8dbb 100644 --- a/docs/developer-guide/Theme-Basics.asciidoc +++ b/docs/developer-guide/Theme-Basics.asciidoc @@ -450,7 +450,7 @@ Notice that, in code, only pixel sizes are supported, so it’s up to you to dec 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. -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 +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/ The code below demonstrates all the major fonts available in Codename One with the handlee ttf file posing as a standin for arbitrary TTF: diff --git a/docs/website/content/faq.md b/docs/website/content/faq.md index 1fa3a0c774..a138def6e4 100644 --- a/docs/website/content/faq.md +++ b/docs/website/content/faq.md @@ -33,6 +33,20 @@ Not if you use the Codename One cloud build service. The cloud handles iOS compi If you build fully offline, Apple tooling still requires macOS for iOS builds and submission workflows. +### Do `native:*` fonts in the JavaSE simulator match current iOS fonts? +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. + +If those fonts are not installed on the host OS, the simulator falls back to bundled Roboto fonts so behavior remains consistent. + +### Can Codename One bundle Apple's San Francisco fonts? +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/ + +Practical setup guidance: + +- **macOS**: You usually already have the required iOS font families installed with the OS/Xcode toolchain. +- **Windows/Linux**: Install San Francisco/SF Pro fonts from Apple's official distribution channels for licensed developers, then restart the simulator. +- If those fonts are unavailable, simulator rendering still works using Roboto fallback, but text metrics may differ from real iOS devices. + ### How does performance compare to native or HTML-based solutions? Codename One compiles to native targets and is designed for production-level performance, including optimized rendering and modern VM/runtime improvements. diff --git a/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEPortFontMappingTest.java b/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEPortFontMappingTest.java new file mode 100644 index 0000000000..e865aa191b --- /dev/null +++ b/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEPortFontMappingTest.java @@ -0,0 +1,92 @@ +package com.codename1.impl.javase; + +import java.util.HashSet; +import java.util.Set; +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class JavaSEPortFontMappingTest { + + private Boolean originalIsIOS; + + @AfterEach + public void tearDown() throws Exception { + JavaSEPort.clearAvailableFontNamesLowercaseForTest(); + if (originalIsIOS != null) { + setIsIOS(originalIsIOS.booleanValue()); + } + } + + private void setIsIOS(boolean value) throws Exception { + Field f = JavaSEPort.class.getDeclaredField("isIOS"); + f.setAccessible(true); + if (originalIsIOS == null) { + originalIsIOS = Boolean.valueOf(f.getBoolean(null)); + } + f.setBoolean(null, value); + } + + @Test + public void testFindFirstInstalledFontCandidateUsesCandidateOrder() { + Set installed = new HashSet(); + installed.add("sf pro display"); + installed.add("helvetica neue"); + + String out = JavaSEPort.findFirstInstalledFontCandidate( + new String[] {"SF Pro Text", "SF Pro Display", "Helvetica Neue"}, + installed + ); + + assertEquals("SF Pro Display", out); + } + + @Test + public void testNativeFontNameForIOSReturnsNullWhenNoCandidatesInstalled() { + Set installed = new HashSet(); + installed.add("roboto"); + + String out = JavaSEPort.nativeFontNameForIOS("native:MainRegular", installed); + assertNull(out); + } + + @Test + public void testNativeFontNameForIOSReturnsFirstMatchingFamily() { + Set installed = new HashSet(); + installed.add("sf pro text"); + installed.add("helvetica neue"); + + String out = JavaSEPort.nativeFontNameForIOS("native:ItalicRegular", installed); + assertEquals("SF Pro Text", out); + } + + @Test + public void testLoadTrueTypeFontUsesInstalledIOSCandidateWhenPresent() throws Exception { + Set installed = new HashSet(); + installed.add("helvetica neue"); + JavaSEPort.setAvailableFontNamesLowercaseForTest(installed); + setIsIOS(true); + + JavaSEPort port = new JavaSEPort(); + Object out = port.loadTrueTypeFont("native:MainRegular", "native:MainRegular"); + + assertNotNull(out); + assertEquals("Helvetica Neue", ((java.awt.Font) out).getName()); + } + + @Test + public void testLoadTrueTypeFontFallsBackWhenNoIOSFamilyInstalled() throws Exception { + JavaSEPort.setAvailableFontNamesLowercaseForTest(new HashSet()); + setIsIOS(true); + + JavaSEPort port = new JavaSEPort(); + Object out = port.loadTrueTypeFont("native:MainRegular", "native:MainRegular"); + + assertNotNull(out); + assertEquals(java.awt.Font.class, out.getClass()); + } +}