功能价格文档博客
获取 mote
← 博客
工程·2026年7月2日·阅读需 4 分钟

不直接绘制文字,而是交给平台处理的理由

你是否遇到过韩文看起来很正常,唯独汉字显得有些陌生的情况?当我们决定不亲自绘制文字,而是交给平台的文本引擎时,我们的工作就不再是决定“要画什么”,而是决定“该请求哪种字体”。

m
mote
使用 Noto Sans CJK 的 KR 子集和 JP 子集绘制同一字符串的对比。韩文相同,但骨、直等汉字的字形不同。
使用 Noto Sans CJK 的 KR 子集和 JP 子集绘制同一字符串的对比。韩文相同,但骨、直等汉字的字形不同。

文字要显示在屏幕上,需要经过许多步骤。要把字符转换为形状,进行换行,再绘制成像素。还要处理韩文组合、汉字、表情符号、阿拉伯文字以及字体回退。

我们没有重新实现这一过程。我们把它交给了 Linux 的 Pango 和 HarfBuzz、Windows 的 DirectWrite,以及 macOS 的 TextKit 2。

因为这是操作系统长期以来不断打磨的领域。如果由我们自己实现,在 CJK 字符或无障碍功能等棘手之处,质量反而很可能会下降。我们也认为,产品的差异化并不在渲染器本身,而在于排版设计和符合平台习惯的使用体验。

但是,由我们决定何时创建

文本布局的创建需要时间和内存。大型文档中可能有数万行,因此无法全部创建。

所以我们只创建屏幕上可见的行及其周边的行。远离屏幕的行则立即丢弃。即使长时间滚动一个 1MB 的文档,仍然存活的布局也只有大约四十到六十个。

对于尚未创建的行,我们会预先估算其高度。因为滚动条需要知道整个文档的长度。

当然,估算也可能出错。如果重新计算出的高度与预期不同,画面可能会发生偏移。这时,我们会在同一帧内按照差值校正滚动位置。在用户眼中,应该像什么都没有发生一样。

正在组合韩文的行会不加任何样式地绘制。因为如果输入过程中插入粗体或隐藏等属性,输入法所知道的字符数可能会与屏幕上的字符数不同。

最先遇到的问题是汉字的面孔

交给平台处理,也意味着会沿用平台的默认值。

Ubuntu 的默认 UI 字体中没有韩文。因此,系统代为选择的字体是 Noto Sans CJK JP。韩文显示正常,但部分汉字呈现为日式字形。因为即使是同一个 Unicode 字符,在韩国和日本也可能具有不同的形状。

解决方法很简单。我们在 UI 字体列表中把 Noto Sans CJK KR 放在了前面。

但还有一点更为重要。如果我们不制定标准,平台就会按照自身的默认值,而不是用户的语言来给出答案。

通过字体的顺序塑造不同语言的字形

字体栈会从前往后查找字符。只有第一个字体中没有的字符,才会转到下一个字体。

利用这一特性,就可以让英文显示为衬线体,让韩文显示为无衬线体。也就是说,只需一行设置,就能为不同语言组合适合的字体。

如果阅读字体留空,就直接使用正文字体。如果单独指定,则只在阅读模式中改变。源码模式使用代码字体。

字号会遵循操作系统的无障碍设置。在系统中放大了文字的用户,无需在每个应用中重新设置。

设置卡片的外观选项卡——字体栏中直接显示以逗号连接的字体栈,阅读字体为空,因此以浅色文字标注“与正文相同”。
设置卡片的外观选项卡——字体栏中直接显示以逗号连接的字体栈,阅读字体为空,因此以浅色文字标注“与正文相同”。

屏幕阅读器也利用同一基础。平台的文本层已经与无障碍功能相连,因此我们只需准确传递所请求行的内容。

缺少翻译时,构建会失败

UI 文本使用英语编写,并将该文本作为翻译表的键。没有翻译时,屏幕上会直接显示英语。

问题在于,这不会产生错误。即使遗漏翻译,也只会悄无声息地夹杂着英语显示。

因此,我们编写了检查脚本。它会收集代码中实际使用的文本,并与 12 种语言的翻译表进行比较。如果只在一种语言中添加了文本,或遗漏了翻译,构建就会失败。

每个翻译表有 304 个键,代码中实际使用的文本有 282 条。支持的语言为 English、한국어、日本語、简体中文、繁體中文、Deutsch、Français、Español、Português (Brasil)、Русский、Italiano、Polski。

设置的常规选项卡——最上方一行是语言下拉菜单,已选择韩语,因此下方各项的名称也全部为韩语。
设置的常规选项卡——最上方一行是语言下拉菜单,已选择韩语,因此下方各项的名称也全部为韩语。

切换语言时,会使用同一文档打开一个新窗口,并关闭原有窗口。如果逐一翻译已经显示的数百个控件,很容易出现遗漏。我们判断,与其维护一条几乎不会使用的复杂路径,不如在切换语言的瞬间重新打开一次窗口,这样更加安全。

交给平台处理,结果也会带有平台的特征

即使是同一个文档,不同操作系统上的像素也不会完全相同。因为文字的优化方式和默认字体有所不同。

我们不会强行让它们变得一致。取而代之的是,我们会保持字号比例、行高和边距等文档基准一致。

字体回退的结果也可能因已安装的字体而不同。因此,我们不会承诺特定的结果,而是透明地展示请求字体的顺序。用户也可以自行更改。

不亲自绘制,并不意味着责任也随之消失。我们必须更准确地决定把什么交给平台,以及以怎样的顺序发出请求。

← 较早在一个 Rust 核心上搭载各平台外壳的理由较新 →文档再长,输入也不会变慢的理由
通过RSS持续关注。
只留下文字
简体中文
© 2026 mote