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
146 changes: 105 additions & 41 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ public class JavaSEPort extends CodenameOneImplementation {


private static final int ICON_SIZE=24;
private static final Map<String, String[]> IOS_NATIVE_FONT_CANDIDATES = new HashMap<String, String[]>();
private static Set<String> availableFontNamesLowercase;
private static final String PREF_AUTO_UPDATE_DEFAULT_BUNDLE = "cn1.autoDefaultResourceBundle";
public final static boolean IS_MAC;
private static boolean isIOS;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String> getAvailableFontNamesLowercase() {
if (availableFontNamesLowercase == null) {
HashSet<String> out = new HashSet<String>();
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<String> fontNames) {
availableFontNamesLowercase = fontNames;
}

static void clearAvailableFontNamesLowercaseForTest() {
availableFontNamesLowercase = null;
}

static String findFirstInstalledFontCandidate(String[] candidates, Set<String> 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<String> 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) {
Expand Down
2 changes: 1 addition & 1 deletion docs/developer-guide/Theme-Basics.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 14 additions & 0 deletions docs/website/content/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> installed = new HashSet<String>();
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<String> installed = new HashSet<String>();
installed.add("roboto");

String out = JavaSEPort.nativeFontNameForIOS("native:MainRegular", installed);
assertNull(out);
}

@Test
public void testNativeFontNameForIOSReturnsFirstMatchingFamily() {
Set<String> installed = new HashSet<String>();
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<String> installed = new HashSet<String>();
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<String>());
setIsIOS(true);

JavaSEPort port = new JavaSEPort();
Object out = port.loadTrueTypeFont("native:MainRegular", "native:MainRegular");

assertNotNull(out);
assertEquals(java.awt.Font.class, out.getClass());
}
}
Loading