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";