mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-02 18:16:19 +00:00
* 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:
@@ -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 |
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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\
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user