Web: Chinese UI loads a Chinese font (#8894) (#8981)

* claim: #8894

Co-authored-by: Cursor <cursoragent@cursor.com>

* Web: load Simplified/Traditional Chinese fonts for zh and zh-TW (#8894)

BIZ UDPGothic only covers Japanese, so Chinese UI showed tofu. Ship locale-glyph Craft UI SC/TC subsets and fetch the matching face on demand.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SilentWolf75
2026-10-30 13:33:23 +00:00
committed by GitHub
parent 8b25fa0ae4
commit 21340ba019
11 changed files with 436 additions and 81 deletions
+4
View File
@@ -84,6 +84,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| assets/fonts/craft-ui-sc/CraftUI-SC.otf | Adobe / Google Inc. (Noto Sans SC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/SC Regular, glyph-subset for assets/locales/zh.json and renamed Craft UI SC (OFL reserved-name); licence text in assets/fonts/craft-ui-sc/OFL.txt; web UI Simplified Chinese | SIL OFL 1.1 |
| assets/fonts/craft-ui-tc/CraftUI-TC.otf | Adobe / Google Inc. (Noto Sans TC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/TC Regular, glyph-subset for assets/locales/zh-TW.json and renamed Craft UI TC (OFL reserved-name); licence text in assets/fonts/craft-ui-tc/OFL.txt; web UI Traditional Chinese | SIL OFL 1.1 |
| tests/step-conformance/cone_point.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cone_point_vertex.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
@@ -125,6 +127,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/appearances/opaque_finishes.rs | SolidCraft contributors | original illustrative opaque finish values; no external dataset, textures or measured engineering data | MIT OR Apache-2.0 |
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/craft-ui-sc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-SC.otf | SIL OFL 1.1 |
| assets/fonts/craft-ui-tc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-TC.otf | SIL OFL 1.1 |
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
+4
View File
@@ -22,6 +22,10 @@
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/CraftUI-SC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
+75 -25
View File
@@ -273,8 +273,10 @@ mod web {
}
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
const CHINESE_SC_FONT_PATH: &str = "./fonts/CraftUI-SC.otf";
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
struct Shell {
app: SolveApp,
@@ -292,11 +294,17 @@ mod web {
rename_focus: bool,
/// The start page's recent designs were filled from the stored ones.
recents_synced: bool,
/// On-demand CJK font: fetched only when a CJK language is selected.
/// On-demand script fonts: fetched only when a CJK language is selected.
font: solidcraft_ui_egui::fonts::CjkFontLoader,
font_result: FontLoadResult,
/// The fetched CJK font's bytes: kept so a language change can re-order the fallbacks.
/// The fetched Japanese font's bytes: kept so a language change can re-order the fallbacks.
cjk_bytes: Option<Vec<u8>>,
/// The fetched Chinese font's bytes (Simplified or Traditional for this session).
chinese_bytes: Option<Vec<u8>>,
/// Which Chinese face `chinese_bytes` is: `"zh"` or `"zh-TW"`.
chinese_kind: Option<&'static str>,
/// Kind requested by the in-flight fetch (`"ja"`, `"zh"`, or `"zh-TW"`).
font_fetch_kind: Option<&'static str>,
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
font_language: String,
space_mouse: SpaceMouse,
@@ -674,32 +682,60 @@ mod web {
files::autosave(&self.files, None);
}
/// Fetch and install the CJK font when a CJK language is selected. If that fails, this
/// Whether the shell already has the on-demand face the live language needs.
fn script_font_ready(&self) -> bool {
match solidcraft_ui_egui::fonts::web_script_font_kind(&self.app.preferences.language) {
Some("zh") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh"),
Some("zh-TW") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh-TW"),
Some("ja") => self.cjk_bytes.is_some(),
_ => true,
}
}
/// Fetch and install a script font when a CJK language is selected. If that fails, this
/// session falls back to English (no tofu); the saved language preference is kept.
fn tick_font(&mut self, ctx: &egui::Context) {
let done = self.font_result.borrow_mut().take();
if let Some(res) = done {
let kind = self.font_fetch_kind.take();
let installed = match res {
Ok(bytes) => {
self.cjk_bytes = Some(bytes);
Ok((path, bytes)) => {
match kind {
Some("zh") => {
self.chinese_bytes = Some(bytes);
self.chinese_kind = Some("zh");
}
Some("zh-TW") => {
self.chinese_bytes = Some(bytes);
self.chinese_kind = Some("zh-TW");
}
_ => {
self.cjk_bytes = Some(bytes);
}
}
log::info!("loaded web script font from {path}");
true
}
Err(err) => {
log::warn!("failed to fetch CJK font from {JAPANESE_FONT_PATH}: {err}");
let path = match kind {
Some("zh") => CHINESE_SC_FONT_PATH,
Some("zh-TW") => CHINESE_TC_FONT_PATH,
_ => JAPANESE_FONT_PATH,
};
log::warn!("failed to fetch script font from {path}: {err}");
false
}
};
if installed {
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
log::warn!("CJK font unavailable; showing English for this session");
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
log::warn!("script font unavailable; showing English for this session");
}
self.font.finished(installed, &mut self.app.preferences.language);
self.apply_fonts(ctx);
ctx.request_repaint();
}
if self.font.tick(&mut self.app.preferences.language) {
self.fetch_font(ctx);
let ready = self.script_font_ready();
if self.font.tick(&mut self.app.preferences.language, ready) {
self.fetch_font(ctx, solidcraft_ui_egui::fonts::web_script_font_kind(&self.app.preferences.language).unwrap_or("ja"));
}
// The CJK face leads only a CJK UI: its Cyrillic glyphs are full-width, so switching
// to Ukrainian (or English) must put the Latin face back in front.
@@ -708,31 +744,42 @@ mod web {
}
}
/// Set up the UI fonts for the live language, keeping the fetched CJK font as a fallback
/// (it leads only for a CJK UI, see [`solidcraft_ui_egui::fonts::install_cjk_font_for`]).
/// Set up the UI fonts for the live language, keeping fetched script fonts as fallbacks
/// (Chinese leads a Chinese UI; see [`solidcraft_ui_egui::fonts::install_web_script_fonts`]).
fn apply_fonts(&mut self, ctx: &egui::Context) {
match self.cjk_bytes.clone() {
Some(bytes) => {
solidcraft_ui_egui::fonts::install_cjk_font_for(ctx, bytes, &self.app.preferences.language);
}
None => solidcraft_ui_egui::fonts::setup_fonts(ctx),
if self.cjk_bytes.is_none() && self.chinese_bytes.is_none() {
solidcraft_ui_egui::fonts::setup_fonts(ctx);
} else {
solidcraft_ui_egui::fonts::install_web_script_fonts(
ctx,
self.cjk_bytes.clone(),
self.chinese_bytes.clone(),
&self.app.preferences.language,
);
}
self.font_language.clone_from(&self.app.preferences.language);
}
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
/// fetch the font, so the text does not stay tofu (#720).
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
/// fetch the Japanese font, so the text does not stay tofu (#720).
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
self.fetch_font(ctx);
self.fetch_font(ctx, "ja");
}
}
fn fetch_font(&self, ctx: &egui::Context) {
fn fetch_font(&mut self, ctx: &egui::Context, kind: &'static str) {
let path = match kind {
"zh" => CHINESE_SC_FONT_PATH,
"zh-TW" => CHINESE_TC_FONT_PATH,
_ => JAPANESE_FONT_PATH,
};
self.font_fetch_kind = Some(kind);
let slot = self.font_result.clone();
let ctx = ctx.clone();
let path = path.to_owned();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
let res = fetch_font_bytes(&path).await.map(|bytes| (path, bytes));
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
@@ -962,6 +1009,9 @@ mod web {
font: Default::default(),
font_result: Rc::new(std::cell::RefCell::new(None)),
cjk_bytes: None,
chinese_bytes: None,
chinese_kind: None,
font_fetch_kind: None,
font_language,
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
Binary file not shown.
+96
View File
@@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
Subset for SolidCraft web UI (glyphs used by the zh locale); renamed Craft UI SC (no Reserved Font Name).
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+96
View File
@@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
Subset for SolidCraft web UI (glyphs used by the zh-TW locale); renamed Craft UI TC (no Reserved Font Name).
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+137 -43
View File
@@ -1,6 +1,7 @@
//! Bundled IBM Plex interface faces, user overrides, and language-specific system fallbacks
//! (CJK, Hangul, Thai, Hebrew).
//! The same definitions are used on desktop and web; the web shell adds Japanese on demand.
//! The same definitions are used on desktop and web; the web shell adds Japanese and Chinese
//! faces on demand.
use std::sync::Arc;
@@ -171,8 +172,7 @@ pub fn with_symbol_fallback(mut fonts: egui::FontDefinitions) -> egui::FontDefin
fonts
}
/// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only;
/// add Chinese or Korean here together with a font that covers them.
/// UI languages whose text needs a CJK script font on the web.
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "zh-TW", "ko"];
/// Whether the UI language needs a CJK font (the web app fetches one only then).
@@ -180,6 +180,22 @@ pub fn needs_cjk_font(language: &str) -> bool {
CJK_LANGUAGES.contains(&language)
}
/// Whether the UI language needs the Simplified or Traditional Chinese web font (#8894).
pub fn needs_chinese_font(language: &str) -> bool {
matches!(language, "zh" | "zh-TW")
}
/// Which on-demand web face a CJK UI language loads: `"zh"`, `"zh-TW"`, or `"ja"` (also for
/// Korean until a Hangul face ships). `None` when the language uses only the bundled Latin faces.
pub fn web_script_font_kind(language: &str) -> Option<&'static str> {
match language {
"zh" => Some("zh"),
"zh-TW" => Some("zh-TW"),
"ja" | "ko" => Some("ja"),
_ => None,
}
}
/// Whether `text` has characters only the CJK fallback draws: kana, CJK ideographs and their
/// punctuation, and full-width forms. Hangul is left out: the web app's font has none.
pub fn has_cjk(text: &str) -> bool {
@@ -245,19 +261,48 @@ fn promote_font_to_front(fonts: &mut FontDefinitions, name: &str) {
}
}
/// Install the downloaded font with the CJK UI's shared mixed-script baseline.
/// Install the downloaded Japanese font with the CJK UI's shared mixed-script baseline.
pub fn install_cjk_font_for(ctx: &egui::Context, bytes: Vec<u8>, language: &str) -> bool {
let fonts = ctx.data(|d| d.get_temp::<FontDefinitions>(installed_fonts_id())).unwrap_or_else(base_font_definitions);
match with_cjk_fallback(fonts, bytes) {
Some(mut fonts) => {
if needs_cjk_font(language) {
promote_font_to_front(&mut fonts, CJK_FALLBACK);
}
install(ctx, fonts);
true
}
None => false,
install_web_script_fonts(ctx, Some(bytes), None, language)
}
/// Install the web shell's on-demand Japanese and/or Chinese faces, ordered for `language`.
/// Returns false only when at least one byte blob was supplied and none of them parsed as a font.
pub fn install_web_script_fonts(ctx: &egui::Context, cjk: Option<Vec<u8>>, chinese: Option<Vec<u8>>, language: &str) -> bool {
if cjk.is_none() && chinese.is_none() {
setup_fonts_for(ctx, language, "");
return true;
}
let mut fonts = ctx.data(|d| d.get_temp::<FontDefinitions>(installed_fonts_id())).unwrap_or_else(base_font_definitions);
// Drop prior script faces so a language change can replace them without stacking duplicates.
for name in [CJK_FALLBACK, CHINESE_FALLBACK] {
fonts.font_data.remove(name);
for family in all_families() {
if let Some(list) = fonts.families.get_mut(&family) {
list.retain(|entry| entry != name);
}
}
}
let mut any = false;
for (name, bytes) in [(CJK_FALLBACK, cjk), (CHINESE_FALLBACK, chinese)] {
let Some(bytes) = bytes else { continue };
any = true;
if let Some(next) = with_fallback(fonts.clone(), name, bytes) {
fonts = next;
}
}
if any && !fonts.font_data.contains_key(CJK_FALLBACK) && !fonts.font_data.contains_key(CHINESE_FALLBACK) {
return false;
}
if needs_cjk_font(language)
&& let Some(primary) = fallback_order(language)
.into_iter()
.find(|name| [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK].contains(name) && fonts.font_data.contains_key(*name))
{
promote_font_to_front(&mut fonts, primary);
}
install(ctx, fonts);
true
}
/// Lower-case file-name fragments that identify CJK fonts. Deliberately specific: generic
@@ -319,8 +364,8 @@ pub fn is_chinese_font_name(file_name: &str) -> bool {
CHINESE_FONT_KEYWORDS.iter().any(|k| name.contains(k))
}
/// Session state of the web app's on-demand CJK font. Pure state so it can be tested off the web:
/// the shell does the fetch when [`CjkFontLoader::tick`] asks for it.
/// Session state of the web app's on-demand script fonts. Pure state so it can be tested off the
/// web: the shell does the fetch when [`CjkFontLoader::tick`] asks for it.
#[derive(Debug, Default)]
pub struct CjkFontLoader {
status: CjkFontStatus,
@@ -342,19 +387,28 @@ impl CjkFontLoader {
self.status
}
/// Called every frame with the live language. Returns true when the font fetch should start
/// now (only for a CJK language, only once). After a failed load a CJK language falls back to
/// English for this session; the saved preference is kept (see [`Self::persisted_language`]).
pub fn tick(&mut self, language: &mut String) -> bool {
/// Called every frame with the live language. `ready` is true when the shell already has the
/// on-demand face that language needs. Returns true when a fetch should start now. After a
/// failed load a CJK language falls back to English for this session; the saved preference is
/// kept (see [`Self::persisted_language`]). Switching to another CJK language that still needs
/// a face starts another fetch (#8894).
pub fn tick(&mut self, language: &mut String, ready: bool) -> bool {
if self.saved_language.is_some() && language != "en" {
// The user picked a language themselves since the fallback: that is the preference now.
self.saved_language = None;
if self.status == CjkFontStatus::Failed {
self.status = CjkFontStatus::Unloaded;
}
}
if !needs_cjk_font(language) {
return false;
}
if ready {
self.status = CjkFontStatus::Installed;
return false;
}
match self.status {
CjkFontStatus::Unloaded => {
CjkFontStatus::Unloaded | CjkFontStatus::Installed => {
self.status = CjkFontStatus::Loading;
true
}
@@ -362,12 +416,12 @@ impl CjkFontLoader {
self.fall_back(language);
false
}
CjkFontStatus::Loading | CjkFontStatus::Installed => false,
CjkFontStatus::Loading => false,
}
}
/// CJK text is on screen (whatever the UI language). Returns true when the font fetch should
/// start now: once, and never again after a failed load (the UI language stays as it is).
/// CJK text is on screen (whatever the UI language). Returns true when the Japanese font fetch
/// should start now: once, and never again after a failed load (the UI language stays as it is).
pub fn text_needs_font(&mut self) -> bool {
if self.status == CjkFontStatus::Unloaded {
self.status = CjkFontStatus::Loading;
@@ -929,8 +983,8 @@ fn arabic_font() -> Option<Vec<u8>> {
.find_map(|path| try_read_font(&path).filter(|bytes| covers(bytes, 'ب')))
}
/// Web installs the bundled faces synchronously; Japanese is fetched by the shell on demand.
/// Preserve that downloaded face when preferences change later in the session.
/// Web installs the bundled faces synchronously; Japanese and Chinese are fetched by the shell
/// on demand. Preserve those downloaded faces when preferences change later in the session.
#[cfg(target_arch = "wasm32")]
pub fn load_system_fonts_in_background(_ctx: &egui::Context) {}
@@ -948,8 +1002,12 @@ pub fn setup_fonts_without_waiting(ctx: &egui::Context, language: &str, ui_font:
pub fn setup_fonts_for(ctx: &egui::Context, language: &str, ui_font: &str) {
let previous = ctx.data(|d| d.get_temp::<FontDefinitions>(installed_fonts_id()));
let mut fonts = web_font_definitions(ui_font, previous);
if needs_cjk_font(language) {
promote_font_to_front(&mut fonts, CJK_FALLBACK);
if needs_cjk_font(language)
&& let Some(primary) = fallback_order(language)
.into_iter()
.find(|name| [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK].contains(name) && fonts.font_data.contains_key(*name))
{
promote_font_to_front(&mut fonts, primary);
}
install(ctx, fonts);
}
@@ -960,16 +1018,20 @@ fn web_font_definitions(ui_font: &str, previous: Option<FontDefinitions>) -> Fon
if let Some(font) = self::ui_font(ui_font) {
fonts = with_ui_font(fonts, font);
}
if let Some(data) = previous.and_then(|f| f.font_data.get(CJK_FALLBACK).cloned()) {
fonts.font_data.insert(CJK_FALLBACK.to_owned(), data);
for family in all_families() {
fonts.families.entry(family).or_default().push(CJK_FALLBACK.to_owned());
if let Some(prev) = previous {
for name in [CJK_FALLBACK, CHINESE_FALLBACK] {
if let Some(data) = prev.font_data.get(name).cloned() {
fonts.font_data.insert(name.to_owned(), data);
for family in all_families() {
fonts.families.entry(family).or_default().push(name.to_owned());
}
}
}
}
with_symbol_fallback(fonts)
}
/// Web starts with the bundled faces and symbol fallback; Japanese loads on demand.
/// Web starts with the bundled faces and symbol fallback; script fonts load on demand.
#[cfg(target_arch = "wasm32")]
pub fn setup_fonts(ctx: &egui::Context) {
setup_fonts_for(ctx, "en", "");
@@ -1504,14 +1566,46 @@ mod tests {
let mut loader = CjkFontLoader::default();
for lang in ["en", "de"] {
let mut language = lang.to_owned();
assert!(!loader.tick(&mut language), "no CJK font fetch for {lang}");
assert!(!loader.tick(&mut language, false), "no CJK font fetch for {lang}");
}
let mut language = "ja".to_owned();
assert!(loader.tick(&mut language), "Japanese starts the fetch");
assert!(!loader.tick(&mut language), "the fetch starts once");
assert!(loader.tick(&mut language, false), "Japanese starts the fetch");
assert!(!loader.tick(&mut language, false), "the fetch starts once");
loader.finished(true, &mut language);
assert_eq!(loader.status(), CjkFontStatus::Installed);
assert_eq!(language, "ja");
assert!(!loader.tick(&mut language, true), "ready face needs no fetch");
}
#[test]
fn chinese_ui_fetches_again_after_japanese_is_installed() {
let mut loader = CjkFontLoader::default();
let mut language = "ja".to_owned();
assert!(loader.tick(&mut language, false));
loader.finished(true, &mut language);
language = "zh".to_owned();
assert!(loader.tick(&mut language, false), "Chinese needs its own face (#8894)");
loader.finished(true, &mut language);
assert!(!loader.tick(&mut language, true));
assert_eq!(web_script_font_kind("zh"), Some("zh"));
assert_eq!(web_script_font_kind("zh-TW"), Some("zh-TW"));
assert_eq!(web_script_font_kind("ja"), Some("ja"));
assert!(needs_chinese_font("zh"));
assert!(!needs_chinese_font("ja"));
}
#[test]
fn web_chinese_subset_draws_simplified_settings() {
let bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/craft-ui-sc/CraftUI-SC.otf")).to_vec();
let ctx = test_context();
assert!(install_web_script_fonts(&ctx, None, Some(bytes), "zh"));
let installed = ctx.data(|d| d.get_temp::<FontDefinitions>(installed_fonts_id())).unwrap();
for family in sans_families() {
assert_eq!(installed.families[&family].first().map(String::as_str), Some(CHINESE_FALLBACK));
}
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
output.textures_delta.clear();
assert!(ctx.fonts_mut(|f| f.has_glyphs(&semibold(14.0), "设置")));
}
#[test]
@@ -1530,11 +1624,11 @@ mod tests {
fn cjk_text_fetches_the_font_with_an_english_ui() {
let mut loader = CjkFontLoader::default();
let mut language = "en".to_owned();
assert!(!loader.tick(&mut language));
assert!(!loader.tick(&mut language, false));
assert!(loader.watching_text());
assert!(loader.text_needs_font(), "CJK text starts the fetch");
assert!(!loader.text_needs_font(), "the fetch starts once");
assert!(!loader.tick(&mut language), "no second fetch from the language");
assert!(!loader.tick(&mut language, false), "no second fetch from the language");
assert!(!loader.watching_text());
loader.finished(false, &mut language);
assert_eq!(loader.status(), CjkFontStatus::Failed);
@@ -1545,9 +1639,9 @@ mod tests {
assert!(loader.text_needs_font());
loader.finished(true, &mut language);
assert_eq!(loader.status(), CjkFontStatus::Installed);
// Switching to Japanese afterwards needs no second fetch.
// Switching to Japanese afterwards needs no second fetch when the face is ready.
let mut ja = "ja".to_owned();
assert!(!loader.tick(&mut ja));
assert!(!loader.tick(&mut ja, true));
}
/// The painted-text check finds CJK in the frame's labels and nothing else.
@@ -1571,16 +1665,16 @@ mod tests {
fn failed_font_fetch_falls_back_for_the_session_without_changing_the_saved_language() {
let mut loader = CjkFontLoader::default();
let mut language = "ja".to_owned();
assert!(loader.tick(&mut language));
assert!(loader.tick(&mut language, false));
loader.finished(false, &mut language);
assert_eq!(language, "en", "this session shows English instead of tofu");
assert_eq!(loader.persisted_language(&language), "ja", "the saved preference stays Japanese");
assert!(!loader.tick(&mut language));
assert!(!loader.tick(&mut language, false));
assert_eq!(loader.persisted_language(&language), "ja");
// Choosing a language afterwards makes that the saved preference.
language = "de".to_owned();
assert!(!loader.tick(&mut language));
assert!(!loader.tick(&mut language, false));
assert_eq!(loader.persisted_language(&language), "de");
}
}
+15 -10
View File
@@ -117,16 +117,21 @@ Chinese font-name list (`PingFang`, `Microsoft YaHei`, `Noto Sans CJK SC`, …)
glyph. No fonts are redistributed in native desktop distribution packages; native builds rely
entirely on existing operating system fonts or craft-fonts.
In the browser, `solidcraft-web` bundles a self-hosted Japanese font (`BIZUDPGothic-Regular.ttf`, licensed
under SIL OFL 1.1) within the web distribution artifacts only. The font is loaded asynchronously from the same
origin (`./fonts/BIZUDPGothic-Regular.ttf`) on-demand when the interface language is a CJK one, or, with
any other interface language, the first time Japanese text (kana, kanji or full-width forms in a design name,
file name, body name or typed text) is drawn, without contacting any external CDNs or third-party servers.
A session that never shows such text downloads no font. If font loading fails with a CJK interface, the web
interface shows English for that session only to prevent missing glyphs (tofu); the saved language
preference is kept, so the next visit tries again. The bundled font specifically targets Japanese; it also
renders the kanji Chinese shares with Japanese, while Korean (Hangul) and simplified-only Chinese characters
are not covered, so a self-hosted SC/Korean font bundle remains follow-up work for full web coverage.
In the browser, `solidcraft-web` ships self-hosted script fonts (SIL OFL 1.1) in the web distribution
artifacts only, loaded asynchronously from the same origin without contacting any external CDNs or
third-party servers:
- Japanese (and Hangul-less Korean UI for now): `./fonts/BIZUDPGothic-Regular.ttf`
- Simplified Chinese (`zh`): `./fonts/CraftUI-SC.otf` (a locale-glyph subset of Noto Sans SC, renamed
under the OFL so it does not use the reserved name Noto)
- Traditional Chinese (`zh-TW`): `./fonts/CraftUI-TC.otf` (same for Noto Sans TC)
A CJK interface language fetches the matching face on demand. With any other interface language, the
first time Japanese text (kana, kanji or full-width forms in a design name, file name, body name or
typed text) is drawn, the Japanese face is fetched (#720). A session that never shows such text
downloads no script font. If the needed face fails to load, the web interface shows English for that
session only to prevent missing glyphs (tofu); the saved language preference is kept, so the next
visit tries again. Korean Hangul coverage on the web remains follow-up work.
## Right-to-left text
+6
View File
@@ -173,6 +173,12 @@ fn render(root: &Path, crates: &[Crate]) -> String {
- BIZUDPGothic-Regular.ttf (assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf)\n \
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)\n \
Source: https://github.com/googlefonts/morisawa-biz-ud-gothic\n\n\
- CraftUI-SC.otf (assets/fonts/craft-ui-sc/CraftUI-SC.otf)\n \
Copyright 2014-2021 Adobe / Google Inc. (Noto Sans SC subset for SolidCraft web zh)\n \
Source: https://github.com/notofonts/noto-cjk (Sans SubsetOTF/SC)\n\n\
- CraftUI-TC.otf (assets/fonts/craft-ui-tc/CraftUI-TC.otf)\n \
Copyright 2014-2021 Adobe / Google Inc. (Noto Sans TC subset for SolidCraft web zh-TW)\n \
Source: https://github.com/notofonts/noto-cjk (Sans SubsetOTF/TC)\n\n\
- IBMPlexSans-Variable.ttf (assets/fonts/ibm-plex-sans/IBMPlexSans-Variable.ttf)\n \
Copyright 2017 IBM Corp. with Reserved Font Name \"Plex\"\n \
Source: https://github.com/IBM/plex\n\n\
+3 -3
View File
@@ -19,9 +19,9 @@ use sha2::{Digest, Sha256};
const MARKER: &str = "/* @build */";
const SW: &str = "sw.js";
const BUILD: &str = "build.json";
/// Files the app fetches only when it needs them (the CJK font, ~4.7 MB: `JAPANESE_FONT_PATH`
/// in apps/solidcraft-web/src/main.rs): not kept for offline use, so not every user downloads
/// them. The worker leaves them to the network.
/// Files the app fetches only when it needs them (Japanese ~4.7 MB and Chinese subsets under
/// `fonts/` in apps/solidcraft-web): not kept for offline use, so not every user downloads them.
/// The worker leaves them to the network.
const ON_DEMAND: &[&str] = &["fonts/"];
/// The version that makes sw.js retire (the template's own).
const RETIRE_VERSION: &str = "dev";