feat(m168): 中文字体 CDN 下载真接线(D28 后半)
M167 调研的结论是「中文字体 CDN 下载至今未实装」:`i18n::font` 只有 `Fetcher` trait 与
假实现,`needs-download` 分支什么也不做。这条把它接成一条真链路。
判据 → 下载 → 注册(一条链,四个 crate 各管一段):
* `unlucky_i18n::font`(零依赖):`Source::Pinned { urls, sha256, bytes }` 把「下什么」
钉死——google/fonts 的 `NotoSansSC[wght].ttf` 钉到 commit `2894aab…`(不是 `@main`),
17 772 300 B / sha256 `a3041811…`。可变字体一份覆盖 wght 100–900,省掉三份静态子集的 25 MiB。
`FetchError` 补第三档 `Disk`(网络 / 校验 / 磁盘,UI 按类别选词)。
* `unlucky_core::download::font`(新):缓存命中 → 共享引擎 `download_bytes` → 复验(sha256 +
sfnt 魔数 + 32 MiB 上限)→ 原子落盘。**复验在写盘之前**,所以「哈希不符」时盘上什么都没有;
缓存按 `<sha256 前 16 位>` 命名,pin 一换自动换名。
* `unlucky_app_iced::fonts::{cdn,install}`(新):`CdnFetcher` 把 pin 接进 core(`ui.font_cdn`
可覆盖 URL,哈希不换);`install` 是状态机 + 后台线程,只有 `NeedsDownload` 才起线程
(英文 / 系统已有 / 探测不出一律零网络)。注册走 `iced_graphics::text::FontSystem::load_font`,
族名从字体库读回来核对。失败只降级(继续用系统字体),不阻塞、不崩。
* 接入两条路径:`fonts::resolve_with`(启动)与 `fonts::cjk_for`(设置页切语言)都调
`install::after_probe`;设置页 General 组多一行状态(下载中 / 完成 / 失败 / 无需下载 + 三类失败
原因),文案进 catalog 金表(链尾追加,既有顺序不动);`App::advance` 每秒比对状态快照后重建
视图模型,所以那一行会自己刷新。
配置:新增 `ui.font_cdn`(`keys/font.rs`,schema 1.7.0),空 = 内置 jsDelivr 镜像(含备用边缘)。
守卫(三条都能变红,变异证明见报告):core 侧 8 条(缓存不联网 / 哈希不符拒落盘 / HTML 不当字体
/ 坏缓存自愈 / 上限 / pin 校验),app 侧 6 条(三种闲置策略零调用、失败不注册、类别不串、幂等、
截图钉子、单行日志),页面侧 1 条(四态 × 两语言无缺译、不写假百分比)。
测试:`cargo test -p unlucky-i18n --lib` 31 绿;`-p unlucky-core --lib` 622 绿;
`-p unlucky-app-iced --lib` 684 绿;`cargo fmt --check` 干净。截图三张见报告。
This commit is contained in:
1 parent
2a004a5754
commit
449ded2cb7
22 files changed
+2191
-184
No files matched your search
@@ -0,0 +1,125 @@
|
||||
//! 下载线程与字体注册(M168):`install.rs` 的「动作」那一半。
|
||||
//!
|
||||
//! ## 为什么拆出来
|
||||
//!
|
||||
//! `install.rs` 是「状态 + 判据 + 触发」,已经贴着 400 行硬线。这里只放真正干活的两件:
|
||||
//!
|
||||
//! 1. [`start`]:起一条后台线程(进程内同时只有一条),把「正在下载 → 结果」写回状态槽;
|
||||
//! 2. [`register_into_font_system`]:把字节交给 iced 的全局字体系统(运行时注册)。
|
||||
//!
|
||||
//! ## 为什么下载在后台线程
|
||||
//!
|
||||
//! 一份 17 MiB 的字体在弱网下要几十秒,而两个触发点(启动编排、设置页切语言)都在 UI 线程上。
|
||||
//! 在 UI 线程同步下完 = 窗口几十秒不响应(启动路径甚至是「窗口还没出来」)。
|
||||
//!
|
||||
//! ## 线程里做了什么(顺序即判据)
|
||||
//!
|
||||
//! ```text
|
||||
//! CdnFetcher(缓存命中 ⇒ 零网络;否则共享引擎下载 + sha256 复验)
|
||||
//! → i18n::font::ensure 只有 NeedsDownload 才调 fetcher
|
||||
//! → 成功:注册进 iced,状态 Ready{family, from_cache}
|
||||
//! → 失败:状态 Failed{class},**什么都不注册**(降级回系统字体,界面照常可用)
|
||||
//! ```
|
||||
//!
|
||||
//! ## 诚实边界
|
||||
//!
|
||||
//! - **抓得住**:状态流转、失败不注册、日志三行(start / ok / failed);
|
||||
//! - **抓不住**:真机上字体注册之后**出字的样子**(cosmic-text 用 `wght` 轴选字重这件事只核对了
|
||||
//! 上游源码,没有真机截图对照;macOS / Linux 完全没有真机)。
|
||||
|
||||
use unlucky_i18n::font::{self, Strategy};
|
||||
|
||||
use super::{ensure_for, update, CjkInstall, FetchClass};
|
||||
use crate::fonts::cdn::CdnFetcher;
|
||||
use crate::platform::data;
|
||||
|
||||
/// 起一条后台下载(进程内同时只允许一条:`install::after_probe` 负责那道闸)。
|
||||
pub(super) fn start(font: &'static font::CjkFont) {
|
||||
let font::Source::Pinned { bytes, .. } = font.source;
|
||||
let Some(cache_root) = data::cache_root() else {
|
||||
update(CjkInstall::Failed {
|
||||
class: FetchClass::Disk,
|
||||
message: "数据根解析不出来,字体无处可落".to_owned(),
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
let fetcher = match CdnFetcher::new(font, cache_root) {
|
||||
Ok(fetcher) => fetcher,
|
||||
Err(error) => {
|
||||
update(CjkInstall::Failed {
|
||||
class: FetchClass::from(&error),
|
||||
message: error.message().to_owned(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let cached = fetcher.has_cached();
|
||||
update(CjkInstall::Downloading { bytes });
|
||||
eprintln!(
|
||||
"[font] cjk-download start family=\"{}\" bytes={bytes} from_cache={cached} file={}",
|
||||
font.family,
|
||||
fetcher.cache_file().display()
|
||||
);
|
||||
|
||||
// 判据说「需要」,这条线程才存在:不需要的三种情形在 `after_probe` 就返回了。
|
||||
std::thread::spawn(move || {
|
||||
let state = ensure_for(
|
||||
Strategy::NeedsDownload(font),
|
||||
&fetcher,
|
||||
®ister_into_font_system,
|
||||
);
|
||||
let state = match state {
|
||||
// 命中缓存的那一份要把 `from_cache` 标出来(状态行靠它区分「等了一会儿」与「真的下了一次」)。
|
||||
CjkInstall::Ready { family, .. } => CjkInstall::Ready {
|
||||
family,
|
||||
from_cache: cached,
|
||||
},
|
||||
other => other,
|
||||
};
|
||||
match &state {
|
||||
CjkInstall::Ready { family, from_cache } => eprintln!(
|
||||
"[font] cjk-download ok family=\"{family}\" from_cache={from_cache} (中文字形现在由它兜底,界面不需要重启)"
|
||||
),
|
||||
CjkInstall::Failed { class, message } => eprintln!(
|
||||
"[font] cjk-download failed class={} {message}(降级:继续用系统字体,界面照常可用)",
|
||||
class.as_str()
|
||||
),
|
||||
other => eprintln!("[font] cjk-download {}", other.describe()),
|
||||
}
|
||||
update(state);
|
||||
});
|
||||
}
|
||||
|
||||
/// 把字体字节注册进 iced 的**全局字体系统**(`iced_graphics` 的 `FontSystem::load_font`)。
|
||||
///
|
||||
/// 返回注册后的族名(从字体库里读回来的):变量字体的 `name` 表里族名是 `Noto Sans SC`、
|
||||
/// 而默认实例是 Thin,所以**不能**假设「注册了就叫 NOTO_SANS_SC.family」——这里读回来核对。
|
||||
///
|
||||
/// # Errors
|
||||
/// 字体系统锁被毒掉、或注册后字体库里找不到那个族(字节不是字体 / `fontdb` 不认)。
|
||||
pub fn register_into_font_system(bytes: Vec<u8>) -> Result<String, String> {
|
||||
let lock = iced::advanced::graphics::text::font_system();
|
||||
let mut font_system = lock
|
||||
.write()
|
||||
.map_err(|_| "字体系统的读写锁被毒掉(另一处注册 panic 过)".to_owned())?;
|
||||
|
||||
font_system.load_font(std::borrow::Cow::Owned(bytes));
|
||||
|
||||
// 先把族名抄出来再判:`faces()` 的迭代器借着 `font_system`,直接链到 `ok_or_else` 会让
|
||||
// 那个借用活过守卫(E0597)。
|
||||
let families: Vec<String> = font_system
|
||||
.raw()
|
||||
.db()
|
||||
.faces()
|
||||
.flat_map(|face| face.families.iter())
|
||||
.map(|(family, _)| family.clone())
|
||||
.collect();
|
||||
|
||||
families
|
||||
.iter()
|
||||
.find(|family| family.as_str() == font::NOTO_SANS_SC.family)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("注册后在字体库里找不到 \"{}\"", font::NOTO_SANS_SC.family))
|
||||
}
|
||||
Reference in new issue
Block a user