-
Notifications
You must be signed in to change notification settings - Fork 434
Correct the CSS font docs: TrueType only, and no fonts/ subdirectory requirement #5502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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` | ||
|
|
||
|
|
@@ -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. | ||
|
|
||
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When 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** | ||
|
|
||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This categorical statement conflates unused
font-weight/font-styledescriptors inside@font-facewith those properties on a UIID. For a UIID using a custom face,CSSTheme.getThemeFont()preserves the requested style, andResources.createTrueTypeFont()passes it toderive(); 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 inscripts/initializr/common/src/main/resources/skill/references/css.md.Useful? React with 👍 / 👎.