From aecde507f47e10082d955b64c628b4d79e4d637f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:57:33 +0300 Subject: [PATCH 1/3] Correct the CSS font docs: TrueType only, and no fonts/ subdirectory requirement A customer bundled Nexa as .otf files, put them in common/src/css, and got no font change and no error. Two of the three reasons were things the docs told them. OTF is not supported. It compiles without complaint because the CSS compiler loads fonts through java.awt.Font.createFont(TRUETYPE_FONT, ...), which also parses OpenType/CFF, but Font.createTrueTypeFont rejects any file name that doesn't end in .ttf, and IPhoneBuilder registers only .ttf files in UIAppFonts. The developer guide claimed "TTF/OTF fonts" and the initializr CSS skill reference said ".ttf (or .otf)". Both now say TrueType only and explain the failure mode. The fonts/ subdirectory was never a requirement either. A relative src URL is resolved against the directory holding the CSS file, and merge mode syncs that whole directory, so a font sitting directly beside theme.css works exactly as well as one under fonts/. The guide didn't document the resolution rule at all and three skill references presented common/src/main/css/fonts/ as the location. While in the section, document the parts that make a font silently do nothing: a font-family name with an unquoted space parses as separate identifiers and only the first is read back, so every weight collides under one family; and the @font-face font-weight/font-style descriptors are parsed but never consulted when a family is matched, so each weight needs its own family name and a whole-theme swap goes through the Default selector. Also corrects two stale claims: fonts land next to the compiled theme.res rather than in the project src directory, and the remote-font download cache lives in the build directory. Tests pin both halves of the location contract, since it is now documented: CSSFontFaceLocationTest compiles a theme with the font in the CSS root, in a subdirectory, and with two quoted multi-word families, asserting which file each family resolves to and that it is deployed flat next to theme.res. CN1CSSCLILogicTest covers the merge-mode url() rewrite that Maven actually takes, including that remote and absolute URLs pass through untouched. --- docs/developer-guide/css.asciidoc | 41 +++- .../designer/css/CN1CSSCLILogicTest.java | 26 +++ .../designer/css/CSSFontFaceLocationTest.java | 221 ++++++++++++++++++ .../com/codename1/designer/css/TestFont.ttf | Bin 0 -> 5832 bytes .../skill/references/android-to-cn1.md | 2 +- .../main/resources/skill/references/css.md | 11 +- .../skill/references/react-to-cn1.md | 3 +- 7 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java create mode 100644 maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf diff --git a/docs/developer-guide/css.asciidoc b/docs/developer-guide/css.asciidoc index 589da8ae5da..45125271f3f 100644 --- a/docs/developer-guide/css.asciidoc +++ b/docs/developer-guide/css.asciidoc @@ -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,6 +593,8 @@ 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] @@ -600,6 +602,39 @@ Then you'll be able to reference the font using the specified `font-family` in a 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] +---- +@font-face { + font-family: "MyFont"; + src: url(MyFont-Regular.ttf); +} + +@font-face { + font-family: "MyFont Bold"; + src: url(fonts/MyFont-Bold.ttf); +} +---- + +===== 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: MyFont Bold` registers the family as `MyFont` 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: + +[source,css] +---- +Default { font-family: "MyFont"; } +Title { font-family: "MyFont Bold"; } +---- + +===== 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] @@ -607,9 +642,9 @@ The `@font-face` directive's `src` property will accept both local and remote UR 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** diff --git a/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java b/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java index 3121de5477f..a4ebe7a20e1 100644 --- a/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java +++ b/maven/css-cli/src/test/java/com/codename1/designer/css/CN1CSSCLILogicTest.java @@ -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); + } } diff --git a/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java b/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java new file mode 100644 index 00000000000..8a3c2201755 --- /dev/null +++ b/maven/css-compiler/src/test/java/com/codename1/designer/css/CSSFontFaceLocationTest.java @@ -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. + * + *

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.

+ */ +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); + } + } +} diff --git a/maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf b/maven/css-compiler/src/test/resources/com/codename1/designer/css/TestFont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd39ecf6b74ddcf02175767998f710e0180d6b9d GIT binary patch literal 5832 zcmd^DZERcDc|PY}@{*z?k`gIWHeLBpSCSR+lKP1CQHNF-Qk3G#X=Pb<$)sy|O;H~X zC9)Kg*(q8S-B-Y6S!xtPQM5o?w7~YKIF#EWGrBq*imna3qAM^A#ei);R=_Yepc&d? z*kWPJN9x}5ocBEEocFw6_fm{A#yspZOl9Sz^B0ohPlsQI(#M!~3*$3e5ICMt*BEnPO*XW(Q@zsv zRO2`3kD`C9SjwBqz6$*u3+Q`y%qwN~x1HZZA9I-4HFpZPzC0k%uQI0mUAeSZHFvZ( zu|M(G%aua;7f(gL%UB?R{vR_syNgY*d8RBZo?T<^qFLQ#M;Yx-9~yrz=EdzzlQ~-> z9Ah-fA>mAGOV!`!uQNOLe_6p!?A2z=|CLR01yW{LA$`PWWh1@L61UkqJ?d)XGJ95C zWp98+Iy6666JBD>z=eJJZfk4{QeqisU6$O&;^;f^#g4btUrxT$(`W#(Ho|CAZ#Q#W zNWokiw8kkoo#g-fr^_-xrey|L?vbMUs`&u0Q&e$DG;N~6)Z ztbSGPMpT4ZBr$rNbLQqMSC*`3vauY_rYc;`F-7TC&IEP8uh(ur65%IfT=NI!IrnRW z<6P4RW6Hd3KneOC!GQ7y*M52pe|*3=<-fW9$(3tc2_;^9?dz`<<2-${kAHUan)1fm zU(jAX_~Hk~`fjG@FTeh^*LF|Yk{fS4^U3v_NPbER*7&}fR1R2>jkEql-{^^vVSVtZ z&)&fnFuu{_<#vCB4{*)HXJ@8IPBg!R=*{zh${iQ;`{MD11OLx#v;04lfn#@n?TODFQAhc|c>23KxJ~(|Ks+9db#!<8 zeMkBOolZqnCjwsg!8-@O$8A63ceq2l`@bjJlMel zLAy`!s(Tauw!J^ETvw-HZ^1?qI#W~**Y7Zn zs=`#2Q9V^N2x|4Lum5PkX77*moSdD3wWkJPU%x|x-h(Gp_+i!`n3_2`GN^@)Opbs0 z&p(wr8VdFH{vt5n|8D_z=;zBXEaQ*=d@*r)YJ7S+H=lSlF?!_S8@kS)J3g3LoIbn! z!r8OPtH#|%U435dWL>N$;YKX-61l$5r`Q7#jufD-?U~s@LZv%14VZi zqGAsy-bv3x`t-7YKHOs(eQ03aoou>=73N^4TUdpz3oUE|KHI`})(KBF=W38Y-@+~y zVVf=7$$ab@9La4R*x@q!MhkQ1<(?K+Pza8As6TiDJ5{FxTkAkVk3i(TY< zE!@dQ_>WVi^3}@r=2lgVrXCYh6O(h|(p6D{XnWTz3iHM4R;jWl5@MsYTP+lerC7eS z)5fcX%@>PirQL0}7YmiW?b5E8j7_vfONHG+#jFZS~RVLUdq>rJUg6k&P9MB~&1ei_MHrr(; zD?(zj7cpbY8refXfolW3T}TUH6oF$bkNeI;@>MW4F~5jaDi4i5xXMMaD+r!#Yjpv= zV$kkE))H8|7SpseScg7M-S^l_*lh~ZDrS+E6-z6@7OfTU=__Cl8oP{j@?gg-t5hL9 zg`zzE(4Ot{A*$MWgtw1JejFT+6>j5pu5kx%u0SsD;IY2+r{p(SuRwf z<)XP;h)z1`Rj&$D{%Thc4 zY(}08$?km zl&G0Px>hndNCb_!2+a_h$%S&+Y&Il$B%9SGTgeo%*{DY9%A>dQxh7 z66$9q&qbxp(4m)DuiGyr1&uT}r#r^ed8v+tAy0{#sA28;gnbytI+w|o- zY9?}7qU07VC7s5;qm!l5$+7UEu{(_Wjqhr%!Xr>2g%jsQx~7}t2+L9|MAnod1SQ*A zN_AK_7n)mizRz?ygjtw;c(nIU>Y|v@llz@6HJu5E^l)}89F^ThT~X3<-CT%Dw}Ar) zA-hr^qH_W0$*kZWt{py~grdrfyqb$PUTw zf_^0`dyM6CndS3MVJHl_&yx2Vb>>N3$kaU^_|HsAPlSRLp_{CC(XAUd$pi3&I=qsp zQ{=)}$r{{C+m3~GOl#v%bDX9i_GvVWlPp31r5v1ipR3--fpx}wI!v6BYgNg01F5x zLTwm|P&)`ksMVnewL?&Z+F>X{?Fba1_5>86b`*+G`Jmcgk@=#Vb2`Ux z(sKeFo_;u8Q3W2U7Rg!QTUHHTnAhj(H0Y5eSYjejS><-^yAN>$c|K`P>bn8 z#A{e<=H9S~HP?_ftO=HoIZEclxy*G%s3LS-8BvdBlT_fGD3#a`JL-!_EqL+%JV8?P zNVAeFsoc6Q)s(r88l$Al5O6Mw#C&j$36)TF^+j_oq+^FgoC}S$3)XwYCQyVbp$*v$ z+uIRHsM`-{f)!0n9=4Rg9gh(zVfXezl+2P11a`NNv}~ZqVVXHhES<<+Ar|!|+KjyQ zLCa)xfR;tsS|%pqc<_A?I^pNRNZ(sR>uzmBwqYb3~$~OpFR{@?Do|AIP->_jOUEP`qixg_oj**LCly=gI6U$Kj@;KwPCUwyhrXDcL@uV^N4|(! z%l$q%1-U}Fzbhmea4;wSO(w(&KmpNk~{-oNj^-R zB!)gh90DuEA#jd31XhVN4{)A11lEW{-~w?7TqI5c;G@JLkRc9%EO7`tOPmzIbHpL= zG2#&TJ>n4fIB}i?c%C=}a>OBE5{JMgL(U$WIZqw=1jOqWiUSp_c!KyeIva+ZJ7jNC z$6{|;h}hc}BI;#Be(;d`0(C5E(LzMsu@F&r4SDL2TB43cEnA4FpRf>7E5`K>o6>$W zN=Br!Al0FjEA*&|GQ4E}XcaFY{52Hi=4*Ai&g@B?o{bT>?ozn3iSVnQke0lmyoKQo zU`O%>OEj^{7V1O%m2-G&d?iy?*B9y|)VtyQ41T;OUdiLt2^72>XRmiDyld=D-uNSF M`!hT!@IRve1>`O_E&u=k literal 0 HcmV?d00001 diff --git a/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md index 96654d3b9ed..abc6d44d881 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md +++ b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md @@ -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). | diff --git a/scripts/initializr/common/src/main/resources/skill/references/css.md b/scripts/initializr/common/src/main/resources/skill/references/css.md index 90786b6c9e0..e827e76ec42 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/css.md +++ b/scripts/initializr/common/src/main/resources/skill/references/css.md @@ -333,7 +333,9 @@ For the full Lottie feature matrix and troubleshooting, point users to `docs/dev ### Custom TTF fonts -Drop a `.ttf` (or `.otf`) under `common/src/main/css/fonts/`, then reference its **font name (not file name)** in `font-family`: +**TrueType only.** A `.otf` compiles without an error but is rejected at runtime — `Font.createTrueTypeFont` throws unless the file name ends in `.ttf`, and the iOS build registers only `.ttf` files with the OS. Convert OpenType fonts to TrueType first. + +Drop the `.ttf` anywhere under `common/src/main/css/` — relative `src:` URLs resolve against the directory holding `theme.css`, so both `url("Inter-Regular.ttf")` beside the CSS and `url("fonts/Inter-Regular.ttf")` in a subdirectory work. Then reference its **font name (not file name)** in `font-family`: ```css @font-face { @@ -349,7 +351,9 @@ Title { font-family: "Inter Bold"; font-size: 4mm; } Body { font-family: "Inter"; font-size: 3mm; } ``` -Custom TTF/OTF files are **packaged with the app binary** (placed under the build output so the runtime can `Font.createTrueTypeFont(name, file)` them at startup) — they are **not** embedded inside `theme.res`. That means each font you add increases the deployed app size; choose lean subsets where possible. +A family name containing a space must be quoted everywhere it appears — unquoted, `font-family: Inter Bold` parses as two identifiers and registers under `Inter`, silently colliding with the regular weight. Note also that `font-weight` / `font-style` only select between the built-in `native:` fonts; once `font-family` matches a `@font-face`, they are ignored. That is why each weight needs its own family name. For a whole-theme font swap, set `font-family` on the `Default` selector, then override the UIIDs that need bold or italic. + +Custom TTF files are **packaged with the app binary** (placed under the build output so the runtime can `Font.createTrueTypeFont(name, file)` them at startup) — they are **not** embedded inside `theme.res`. That means each font you add increases the deployed app size; choose lean subsets where possible. To load a TTF programmatically: @@ -555,7 +559,8 @@ Painters are for **drawing** (custom backgrounds, decorations), not for animatin | `text-align` does nothing | Add the `align` fallback (the initializr appends one automatically for `text-align`). | | New CSS only takes effect after restart | The build cache may be stale — `mvn -pl common clean compile`. | | 9-piece border looks blurry on iPhone Pro | Expected — 9-piece images are rasterized at the bundled resolution. Use a vector border (`RoundBorder`/`RoundRectBorder`) instead. | -| Custom TTF doesn't render on device but works in simulator | The `@font-face` `src:` filename and the JS-side `Font.createTrueTypeFont(name, file)` filename must match exactly, and the file must end up packaged with the app. Re-check spelling and confirm the file is under `common/src/main/css/fonts/`. | +| Custom TTF doesn't render on device but works in simulator | The `@font-face` `src:` filename and the JS-side `Font.createTrueTypeFont(name, file)` filename must match exactly, and the file must end up packaged with the app. Re-check spelling and confirm the file is under `common/src/main/css/` (beside `theme.css` or in a subdirectory of it). | +| Custom font is ignored entirely, no error | Either the file is an `.otf` (only `.ttf` works), or the family name has an unquoted space, or `theme.css` isn't at `common/src/main/css/theme.css` — the compiler looks for a `css` directory that is a sibling of a compile source root, and silently does nothing if it isn't there. | ## Reaching beyond the compiler diff --git a/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md index a22514dfa5e..9bf97c74326 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md +++ b/scripts/initializr/common/src/main/resources/skill/references/react-to-cn1.md @@ -80,7 +80,8 @@ Then translate sizes. A web design is in **px**; CN1 sizes in **mm** (density independent). At the desktop/browser scale CN1 effectively renders ~3.78 px/mm, so `16px ~= 4.2mm`, `24px ~= 6.3mm`. Borders/radii in `px` stay crisp; size text and spacing in `mm`. **Bundle the real font** if you want to match the typeface: drop -the `.ttf` files under `common/src/main/css/fonts/` and reference them with +the `.ttf` files anywhere under `common/src/main/css/` (beside `theme.css` or in a +subdirectory of it) and reference them with `@font-face { font-family: "Inter"; src: url("fonts/Inter-Regular.ttf"); }` (one `@font-face` per weight, distinct family names like `"Inter SemiBold"`). From fb7301ce3c7521ac4a03eba72a597dafe73d76ee Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:50:15 +0300 Subject: [PATCH 2/3] Back the new font snippets with demo includes The guide has a gate I missed: validate-guide-snippets.py requires every [source] block to be include-backed from docs/demos, so the two inline CSS examples I added failed the docs build. Moved both into guide-snippets-theme.css as tags css-css-044 and css-css-045. That means the demo build now compiles them, which is the point of the fixture rule -- so the examples had to use real fonts rather than a made-up MyFont. Added GuideRootFont.ttf (the 5.8KB icon font the CSSFontFaceTest sample already carries) at the CSS root, alongside the existing res/GuideDemoFont-Bold.ttf, so the snippet demonstrates the root and subdirectory forms with files that exist. Pointed Default at "GuideRootFont" rather than the regular demo font on purpose. A @font-face is only copied to the build output when some style actually references it, so without a reference the root-level font would compile silently and prove nothing. With it, mvn -f docs/demos/pom.xml process-classes deploys GuideRootFont.ttf next to guide-snippets-theme.res, which is exactly the behaviour the new section documents. --- .../common/src/main/css/GuideRootFont.ttf | Bin 0 -> 5832 bytes .../src/main/css/guide-snippets-theme.css | 28 ++++++++++++++++++ docs/developer-guide/css.asciidoc | 15 ++-------- 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 docs/demos/common/src/main/css/GuideRootFont.ttf diff --git a/docs/demos/common/src/main/css/GuideRootFont.ttf b/docs/demos/common/src/main/css/GuideRootFont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd39ecf6b74ddcf02175767998f710e0180d6b9d GIT binary patch literal 5832 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 M`!hT!@IRve1>`O_E&u=k literal 0 HcmV?d00001 diff --git a/docs/demos/common/src/main/css/guide-snippets-theme.css b/docs/demos/common/src/main/css/guide-snippets-theme.css index 63c674395d1..9be3f311121 100644 --- a/docs/demos/common/src/main/css/guide-snippets-theme.css +++ b/docs/demos/common/src/main/css/guide-snippets-theme.css @@ -419,6 +419,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"; diff --git a/docs/developer-guide/css.asciidoc b/docs/developer-guide/css.asciidoc index 45125271f3f..be293c5836d 100644 --- a/docs/developer-guide/css.asciidoc +++ b/docs/developer-guide/css.asciidoc @@ -608,20 +608,12 @@ A relative `src` URL is resolved against the directory that holds the CSS file. [source,css] ---- -@font-face { - font-family: "MyFont"; - src: url(MyFont-Regular.ttf); -} - -@font-face { - font-family: "MyFont Bold"; - src: url(fonts/MyFont-Bold.ttf); -} +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: MyFont Bold` registers the family as `MyFont` and collides with your regular weight. +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. @@ -629,8 +621,7 @@ To change the base font of an entire theme, set `font-family` on the special `De [source,css] ---- -Default { font-family: "MyFont"; } -Title { font-family: "MyFont Bold"; } +include::../demos/common/src/main/css/guide-snippets-theme.css[tag=css-css-045,indent=0] ---- ===== Remote and GitHub-hosted fonts From f71e01789f96f1968ed3ac552e97268dc8f28352 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:00:17 +0300 Subject: [PATCH 3/3] Add the CN1 copyright header to guide-snippets-theme.css Touching the file pulled it into the copyright gate's scope, and it never had a header. It is first-party CN1 content, so it gets the header rather than an entry in copyright-header-exclusions.txt, which is reserved for third-party sources. --- .../src/main/css/guide-snippets-theme.css | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/demos/common/src/main/css/guide-snippets-theme.css b/docs/demos/common/src/main/css/guide-snippets-theme.css index 9be3f311121..fd01290cfb3 100644 --- a/docs/demos/common/src/main/css/guide-snippets-theme.css +++ b/docs/demos/common/src/main/css/guide-snippets-theme.css @@ -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.