diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 5d1df4bfca..f1ae2573ad 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -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 |
diff --git a/apps/solidcraft-web/index.html b/apps/solidcraft-web/index.html
index 90030947f6..e4354798a8 100644
--- a/apps/solidcraft-web/index.html
+++ b/apps/solidcraft-web/index.html
@@ -22,6 +22,10 @@
+
+
+
+
diff --git a/apps/solidcraft-web/src/main.rs b/apps/solidcraft-web/src/main.rs
index 08443ca6a2..6ebe14513d 100644
--- a/apps/solidcraft-web/src/main.rs
+++ b/apps/solidcraft-web/src/main.rs
@@ -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, String>>>>;
+ type FontLoadResult = Rc), 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>,
+ /// The fetched Chinese font's bytes (Simplified or Traditional for this session).
+ chinese_bytes: Option>,
+ /// 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(),
diff --git a/assets/fonts/craft-ui-sc/CraftUI-SC.otf b/assets/fonts/craft-ui-sc/CraftUI-SC.otf
new file mode 100644
index 0000000000..4763938afe
Binary files /dev/null and b/assets/fonts/craft-ui-sc/CraftUI-SC.otf differ
diff --git a/assets/fonts/craft-ui-sc/OFL.txt b/assets/fonts/craft-ui-sc/OFL.txt
new file mode 100644
index 0000000000..d71df9fdcd
--- /dev/null
+++ b/assets/fonts/craft-ui-sc/OFL.txt
@@ -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.
diff --git a/assets/fonts/craft-ui-tc/CraftUI-TC.otf b/assets/fonts/craft-ui-tc/CraftUI-TC.otf
new file mode 100644
index 0000000000..3dae5a0201
Binary files /dev/null and b/assets/fonts/craft-ui-tc/CraftUI-TC.otf differ
diff --git a/assets/fonts/craft-ui-tc/OFL.txt b/assets/fonts/craft-ui-tc/OFL.txt
new file mode 100644
index 0000000000..5041faaf5e
--- /dev/null
+++ b/assets/fonts/craft-ui-tc/OFL.txt
@@ -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.
diff --git a/crates/ui-egui/src/fonts.rs b/crates/ui-egui/src/fonts.rs
index bdffc5ac25..598f7fe564 100644
--- a/crates/ui-egui/src/fonts.rs
+++ b/crates/ui-egui/src/fonts.rs
@@ -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, language: &str) -> bool {
- let fonts = ctx.data(|d| d.get_temp::(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>, chinese: Option>, 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::(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> {
.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::(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) -> 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::(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");
}
}
diff --git a/docs/localization.md b/docs/localization.md
index 53419c56b9..aa052c80d8 100644
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -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
diff --git a/xtask/src/licences.rs b/xtask/src/licences.rs
index b43914071e..462cab6514 100644
--- a/xtask/src/licences.rs
+++ b/xtask/src/licences.rs
@@ -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\
diff --git a/xtask/src/web_offline.rs b/xtask/src/web_offline.rs
index 86fa54b963..24d2f7f4d1 100644
--- a/xtask/src/web_offline.rs
+++ b/xtask/src/web_offline.rs
@@ -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";