Skip to content
Closed
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
Binary file added docs/demos/common/src/main/css/GuideRootFont.ttf
Binary file not shown.
51 changes: 51 additions & 0 deletions docs/demos/common/src/main/css/guide-snippets-theme.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/

/**
* Compiled developer-guide CSS snippets.
* Complete examples that do not require external assets or generated image borders live here.
Expand Down Expand Up @@ -419,6 +442,34 @@ MyLabel {
}
/* end::css-css-029[] */

/* A relative src URL resolves against the directory holding this CSS file, so
GuideRootFont.ttf sits beside it and GuideDemoFont-Bold.ttf sits in res/.
Both forms compile, which is the point the guide makes with this snippet. */
/* tag::css-css-044[] */
@font-face {
font-family: "GuideRootFont";
src: url(GuideRootFont.ttf);
}

@font-face {
font-family: "GuideDemoFont Bold";
src: url(res/GuideDemoFont-Bold.ttf);
}
/* end::css-css-044[] */

/* Both families come from the snippet above, so this fixture also proves the
root-level font actually resolves and ships: a @font-face is only copied to
the build output when some style references it. */
/* tag::css-css-045[] */
Default {
font-family: "GuideRootFont";
}

Title {
font-family: "GuideDemoFont Bold";
}
/* end::css-css-045[] */

/* tag::css-css-030[] */
@font-face {
font-family: "GuideDownloadedFont";
Expand Down
32 changes: 29 additions & 3 deletions docs/developer-guide/css.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ CN1 resource files support both PNG and JPEG images, but PNG is the default. Mul

=== Fonts

This library supports the https://developer.mozilla.org/en/docs/Web/CSS/font[font], https://developer.mozilla.org/en/docs/Web/CSS/font-size[font-size], https://developer.mozilla.org/en/docs/Web/CSS/font-family[font-family], https://developer.mozilla.org/en/docs/Web/CSS/font-style[font-style], https://developer.mozilla.org/en/docs/Web/CSS/font-weight[font-weight], and https://developer.mozilla.org/en/docs/Web/CSS/text-decoration[text-decoration] properties, as well at the https://developer.mozilla.org/en/docs/Web/CSS/@font-face[@font-face] CSS "at" rule for including TTF/OTF fonts.
This library supports the https://developer.mozilla.org/en/docs/Web/CSS/font[font], https://developer.mozilla.org/en/docs/Web/CSS/font-size[font-size], https://developer.mozilla.org/en/docs/Web/CSS/font-family[font-family], https://developer.mozilla.org/en/docs/Web/CSS/font-style[font-style], https://developer.mozilla.org/en/docs/Web/CSS/font-weight[font-weight], and https://developer.mozilla.org/en/docs/Web/CSS/text-decoration[text-decoration] properties, as well at the https://developer.mozilla.org/en/docs/Web/CSS/@font-face[@font-face] CSS "at" rule for including TTF fonts.

==== `font-family`

Expand Down Expand Up @@ -593,23 +593,49 @@ If you want to use a font other than the built-in fonts, you'll need to define t
include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-028,indent=0]
----

IMPORTANT: Only TrueType (`.ttf`) files are supported. OpenType (`.otf`) files compile without an error, but the runtime rejects any font file whose name doesn't end in `.ttf`, and the iOS build registers only `.ttf` files with the operating system. Convert an OpenType font to TrueType before referencing it.

Then you'll be able to reference the font using the specified `font-family` in any CSS element. For example:

[source,css]
----
include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-029,indent=0]
----

===== Where to put the font file

A relative `src` URL is resolved against the directory that holds the CSS file. You can keep font files directly beside `theme.css`, or in any subdirectory of it, whichever you prefer:

[source,css]
----
include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-044,indent=0]
----

===== Family names, weights and styles

A `font-family` name that contains spaces must be quoted, both in the `@font-face` rule and wherever you reference it. An unquoted name is parsed as a list of separate identifiers, so `font-family: GuideDemoFont Bold` registers the family as `GuideDemoFont` and collides with your regular weight.

`font-weight` and `font-style` select between the built-in `native:` fonts, but they have no effect once `font-family` resolves to a `@font-face` rule. Declare one `@font-face` per weight and style you need, each with its own family name, as in the example above, then reference the right family from each UIID.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct custom-font weight and style behavior

This categorical statement conflates unused font-weight/font-style descriptors inside @font-face with those properties on a UIID. For a UIID using a custom face, CSSTheme.getThemeFont() preserves the requested style, and Resources.createTrueTypeFont() passes it to derive(); Android and JavaSE consequently synthesize bold/italic variants, although iOS currently ignores those flags. Please describe this as platform-dependent and recommend separate files for consistent cross-platform rendering rather than saying the properties have no effect; the same inaccurate claim also appears in scripts/initializr/common/src/main/resources/skill/references/css.md.

Useful? React with 👍 / 👎.


To change the base font of an entire theme, set `font-family` on the special `Default` selector, then override the UIIDs that need a bold or italic face:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Override explicit regular fonts in theme-wide swaps

When includeNativeBool is enabled, as it is in this guide fixture, setting Default.font only changes the fallback style; the bundled native themes explicitly set fonts on regular UIIDs such as Label and Button, so those components remain on native:MainRegular/native:MainLight. This recipe therefore does not change the base font of the entire theme, and users must also override every font-bearing regular UIID (not merely bold or italic ones); the same misleading whole-theme-swap advice appears in scripts/initializr/common/src/main/resources/skill/references/css.md.

Useful? React with 👍 / 👎.


[source,css]
----
include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-045,indent=0]
----

===== Remote and GitHub-hosted fonts

The `@font-face` directive's `src` property will accept both local and remote URLs. The guide fixture below uses a local font so the demo build remains offline and repeatable; application CSS can replace the URL with an HTTPS font URL:

[source,css]
----
include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-030,indent=0]
----

In this case, it will download the `myfont.ttf` file to the same directory as the CSS file. From then on it will use that locally downloaded version of the font so that it doesn't have to make a network request for each build.
In this case, it will download the `myfont.ttf` file into the build directory alongside the merged CSS file, and reuse that copy on later builds so that it doesn't have to make a network request every time. A `mvn clean` discards the cache and the next build downloads the font again.

Fonts are automatically copied to the project's "src" directory when the CSS file is compiled so that they will be distributed with the app and available at runtime.
Fonts are automatically copied next to the compiled `theme.res` when the CSS file is compiled, so that they're distributed with the app and available at runtime. The copy uses the font's file name only, which means two `@font-face` rules that point at identically named files in different directories will collide. The copy is also skipped when a file of that name is already there, so run `mvn clean` after you replace a font file with a different one of the same name.

**GitHub URLs**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,30 @@ void detectsContainmentBeyondADirectParent(@TempDir Path tempDir) throws Excepti
assertFalse((Boolean) invoke("contains", sig, root, sibling), "unrelated directory");
assertFalse((Boolean) invoke("contains", sig, child, root), "containment is not symmetric");
}

/**
* Merge mode re-anchors every relative url() at the synced copy of the CSS
* directory, so an @font-face src that names a file sitting directly in the
* CSS root is as valid as one under a fonts/ subdirectory. Absolute and
* remote URLs have to come through untouched.
*/
@Test
void prefixesRootLevelFontUrlsAlongsideNestedOnes() throws Exception {
String css = "@font-face { font-family: \"A\"; src: url(A-Regular.ttf); }\n"
+ "@font-face { font-family: \"B\"; src: url('fonts/B-Regular.ttf'); }\n"
+ "@font-face { font-family: \"C\"; src: url(\"https://example.com/C.ttf\"); }\n"
+ "@font-face { font-family: \"D\"; src: url(/opt/fonts/D.ttf); }\n";

String out = (String) invoke("prefixUrls", new Class<?>[]{String.class, String.class},
css, "cn1-merged-files/abc123/");

assertTrue(out.contains("url(\"cn1-merged-files/abc123/A-Regular.ttf\")"),
"font in the CSS root is prefixed, was: " + out);
assertTrue(out.contains("url(\"cn1-merged-files/abc123/fonts/B-Regular.ttf\")"),
"font in a subdirectory is prefixed, was: " + out);
assertTrue(out.contains("url(\"https://example.com/C.ttf\")"),
"remote URL is left alone, was: " + out);
assertTrue(out.contains("url(\"/opt/fonts/D.ttf\")"),
"absolute path is left alone, was: " + out);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.designer.css;

import com.codename1.ui.EditorTTFFont;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Hashtable;

/**
* Regression tests for where an {@code @font-face} {@code src:} URL is allowed
* to point. A relative URL resolves against the directory holding the CSS file,
* so a font sitting directly beside {@code theme.css} is just as valid as one in
* a subdirectory. The documentation used to imply a {@code fonts/} subdirectory
* was required, and these tests pin the looser contract down.
*
* <p>They also cover the quoted multi-word family name. An unquoted
* {@code font-family: TestFont Bold} parses as two idents and only the first is
* read back, so quoting is what keeps two weights apart.</p>
*/
public class CSSFontFaceLocationTest {

/**
* The icon font already carried by the CSSFontFaceTest sample, reused here
* so the module doesn't need a second font of its own.
*/
private static final String FIXTURE = "TestFont.ttf";

@BeforeAll
static void installHeadlessImplementation() throws Exception {
HeadlessTestSupport.installHeadlessImplementation();
}

/**
* A font file dropped straight into the CSS directory, with no
* subdirectory, has to resolve and ship.
*/
@Test
void testFontBesideThemeCssResolves() throws Exception {
Path cssDir = Files.createTempDirectory("cn1-font-root");
Path outDir = Files.createTempDirectory("cn1-font-root-out");
try {
copyFixture(cssDir.resolve("TestFont-Regular.ttf"));
Path cssFile = cssDir.resolve("theme.css");
Files.write(cssFile, ("@font-face {"
+ " font-family: \"TestFont\";"
+ " src: url(TestFont-Regular.ttf);"
+ "}"
+ "Label { font-family: \"TestFont\"; font-size: 3mm; }")
.getBytes(StandardCharsets.UTF_8));

Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res"));

EditorTTFFont font = fontFor(themeProps, "Label.font");
assertNotNull(font.getFontFile(), "Label.font resolved to a font file");
assertEquals("TestFont-Regular.ttf", font.getFontFile().getName(), "Resolved font file");
assertTrue(outDir.resolve("TestFont-Regular.ttf").toFile().exists(),
"Font deployed next to theme.res");
} finally {
deleteTree(cssDir);
deleteTree(outDir);
}
}

/**
* The subdirectory form keeps working, and the deployed copy is flattened
* to the bare file name because the runtime forbids a path separator in a
* true type font name.
*/
@Test
void testFontInSubdirectoryResolvesAndDeploysFlat() throws Exception {
Path cssDir = Files.createTempDirectory("cn1-font-sub");
Path outDir = Files.createTempDirectory("cn1-font-sub-out");
try {
Path fontsDir = cssDir.resolve("fonts");
Files.createDirectories(fontsDir);
copyFixture(fontsDir.resolve("TestFont-Regular.ttf"));
Path cssFile = cssDir.resolve("theme.css");
Files.write(cssFile, ("@font-face {"
+ " font-family: \"TestFont\";"
+ " src: url(fonts/TestFont-Regular.ttf);"
+ "}"
+ "Label { font-family: \"TestFont\"; font-size: 3mm; }")
.getBytes(StandardCharsets.UTF_8));

Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res"));

EditorTTFFont font = fontFor(themeProps, "Label.font");
assertNotNull(font.getFontFile(), "Label.font resolved to a font file");
assertEquals("TestFont-Regular.ttf", font.getFontFile().getName(), "Resolved font file");
assertTrue(outDir.resolve("TestFont-Regular.ttf").toFile().exists(),
"Font deployed flat next to theme.res");
} finally {
deleteTree(cssDir);
deleteTree(outDir);
}
}

/**
* Two weights, two quoted family names, two distinct files. This is the
* shape the guide tells people to use, since the {@code font-weight}
* descriptor on {@code @font-face} is not consulted when a family is
* matched.
*/
@Test
void testQuotedMultiWordFamilyKeepsWeightsApart() throws Exception {
Path cssDir = Files.createTempDirectory("cn1-font-weights");
Path outDir = Files.createTempDirectory("cn1-font-weights-out");
try {
copyFixture(cssDir.resolve("TestFont-Regular.ttf"));
copyFixture(cssDir.resolve("TestFont-Bold.ttf"));
Path cssFile = cssDir.resolve("theme.css");
Files.write(cssFile, ("@font-face {"
+ " font-family: \"TestFont\";"
+ " src: url(TestFont-Regular.ttf);"
+ "}"
+ "@font-face {"
+ " font-family: \"TestFont Bold\";"
+ " src: url(TestFont-Bold.ttf);"
+ "}"
+ "Label { font-family: \"TestFont\"; font-size: 3mm; }"
+ "Title { font-family: \"TestFont Bold\"; font-size: 4mm; }")
.getBytes(StandardCharsets.UTF_8));

Hashtable themeProps = compile(cssFile, outDir.resolve("theme.res"));

assertEquals("TestFont-Regular.ttf", fontFor(themeProps, "Label.font").getFontFile().getName(),
"Regular weight");
assertEquals("TestFont-Bold.ttf", fontFor(themeProps, "Title.font").getFontFile().getName(),
"Bold weight resolved through the quoted family name");
} finally {
deleteTree(cssDir);
deleteTree(outDir);
}
}

private static Hashtable compile(Path cssFile, Path resFile) throws Exception {
CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL());
theme.resourceFile = resFile.toFile();
theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile());
theme.res.setTheme("Theme", new Hashtable());
theme.updateResources();
return theme.res.getTheme("Theme");
}

private static EditorTTFFont fontFor(Hashtable themeProps, String key) {
Object font = themeProps.get(key);
assertNotNull(font, "Theme property " + key);
assertTrue(font instanceof EditorTTFFont, key + " is a true type font, was " + font.getClass());
return (EditorTTFFont) font;
}

/**
* Writes the shared TTF fixture out under whatever name the test needs. The
* tests only care about which file a family resolves to, not about what the
* glyphs look like, so one fixture stands in for every weight.
*/
private static void copyFixture(Path dest) throws IOException {
try (InputStream in = CSSFontFaceLocationTest.class.getResourceAsStream(FIXTURE)) {
assertNotNull(in, "Test fixture " + FIXTURE);
Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
}
}

private static void deleteTree(Path path) {
File file = path.toFile();
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteTree(child.toPath());
}
}
file.delete();
}

private static void assertEquals(Object expected, Object actual, String message) {
if (expected == null ? actual != null : !expected.equals(actual)) {
throw new AssertionError(message + " expected=" + expected + " actual=" + actual);
}
}

private static void assertNotNull(Object actual, String message) {
if (actual == null) {
throw new AssertionError(message + " was null");
}
}

private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The EDT/UI-thread rule is identical in spirit to Android: never touch a componen
| `res/values/strings.xml` | `common/src/main/l10n/messages.properties` (and per-locale `messages_de.properties`, etc.) — see `references/build-and-run.md`. |
| `res/drawable/foo.png` | `common/src/main/resources/foo.png` (flat namespace — `references/java-api-subset.md`). |
| `res/values/colors.xml` | Theme constants in `theme.css` under `#Constants { ... }`. |
| `res/font/x.ttf` | `common/src/main/css/fonts/x.ttf`, declared via `@font-face` in `theme.css`. |
| `res/font/x.ttf` | Anywhere under `common/src/main/css/` (beside `theme.css` or in a subdirectory), declared via `@font-face` in `theme.css`. TrueType only — convert `.otf` files first. |
| `res/raw/seed.json` | `common/src/main/resources/seed.json` — read with `Display.getInstance().getResourceAsStream("/seed.json")`. |
| `res/layout/*.xml` | No equivalent — build the layout in Java (`Container` + `Layout` + components). |

Expand Down
Loading
Loading