渲染模型
Ebox 把一棵保留节点树渲染进 Emacs 缓冲区。经 ETAF 发布的 View,或直接用 Ebox API 建立的树,都成为节点;布局为每个节点解出边框盒;绘制用这些盒子拼出每条根行;缓冲区保存拼好的行。本章讲后续各布局与渲染章节所依赖的模型:作为唯一事实来源的保留树、纵向行横向像素的坐标系、让布局保持精确的显示原语、避免字体回退破坏行高的办法,以及图形 frame 与终端之间的差异。Hosts定义这个模型所布局的盒子种类,Runtime定义已发布的 View 怎样变成挂载的树;下面三个函数条目是 Ebox 的库接口,通过概念描述。
保留树是唯一的事实来源
Section titled “保留树是唯一的事实来源”一个挂载拥有一棵保留节点树,树就是模型:它的节点、属性、计算样式、几何与内容决定缓冲区显示什么。缓冲区只是该模型的渲染输出。挂载的第一次 flush 会擦除缓冲区并写入整棵树;之后的每次更新只重算样式、重新布局、重绘受影响的盒子,并且只写内容变化的行。ebox-with-batch 之外的更新立即 flush,因此作者调用一返回,缓冲区就是最新的。
因为树是模型,缓冲区文本不是数据。缓冲区里的外来编辑对树不可见;下一次由树驱动的更新会按节点重写受影响的行,该编辑随之消失。因此任何必须跨更新存留的东西都属于树,或属于产生它的状态——响应式值、组件状态、应用自己的记录——绝不能只存在缓冲区文本里。位置同理:缓冲区位置是某次布局的输出,之后的更新会移动它,所以需要跟随某个节点的代码存节点而不是位置,需要时再查询该节点的当前几何或位置。
渲染不要求挂载。ebox-render 对树做布局并把它的行作为字符串返回,预览与测试就是这样在没有缓冲区的情况下显示树的;ebox-mount 把根绑定到缓冲区,并开始在那里写入更新。两条路径跑同一套布局与绘制,所以一棵树无论是否挂载,渲染结果相同。
插图: 从 View 到 Ebox 树再到 buffer 行
(etaf-view (column :width '(ch 44) :item-gap '(lh 0) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-accent-soft-bg "#ddf4ff") (text :color (etaf-theme-token :ui-accent "#0969da") "View:描述")) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text :color (etaf-theme-token :ui-panel-fg "#1f2328") "→ Ebox:保留节点树、几何与计算样式")) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-disabled-bg "#e5e7eb") (text :color (etaf-theme-token :ui-disabled-fg "#687386") "→ buffer:行、文本属性与 display 对象"))))坐标:纵向行、横向像素
Section titled “坐标:纵向行、横向像素”纵向几何按行,横向几何按像素。一个节点的边框盒以 :x、:y、:width、:height 报告::x 与 :width 是像素,:y 与 :height 是行。样式为 :width (px 20)、:height (lh 3) 的盒子报告宽 20、高 3,并渲染成三行;同一个盒子放在把它拉伸的容器里时会取容器的像素宽,而高度仍按行计。
一行就是一个图形文本行。一行的高度等于行内最大 ascent 加最大 descent,所以行高跟随该行的字体而不是该行的内容;若内容需要更高的行,改做拟合,如下节所述。横向尺寸按像素解析——盒子可用的行内空间是包含块的内宽像素数——纵向尺寸按行解析。样式属性参考定义产生这些值的 px、ch、lh、vh 单位与盒子尺寸可以有的约束。
渲染形态保留这一区分:输出的一行就是一个缓冲区行。相邻的缓冲区行通常属于相邻的根行,但该映射是以后更新可能改变的输出细节,而且一个盒子的内部可以跨多行;关心某个盒子的代码查询它的几何,而不是数缓冲区行。
缓冲区文本是字符序列,而布局要把内容放在精确的像素偏移上。两个原语搭起这座桥。像素空格是一个字符,它的 display 规格给出任意宽度,于是布局可以在两段内容之间放下任意像素数。像素宽度测量把一个字符串的字符、face、display 规格与簇换算成它们占用的像素。
两个原语共享同一份测量缓存,以决定字形步进的 face 为键:只在绘制上不同的 face——前景、下划线、点画——共享同一张宽度表,因此不改变度量的重绘永不使其失效。测量按 face、按多字符簇记忆,而 ebox-clear-cache 在度量改变时把两个原语的记忆一起清空。
Function: ebox-pixel-space (pixel-width)
Section titled “Function: ebox-pixel-space (pixel-width)”返回一个宽 PIXEL-WIDTH 像素的 display space。
参数。 PIXEL-WIDTH 是以像素计的宽度,nil 当作 0。
语义。 返回值是个单字符字符串,其 display 属性是指定该宽度的空格规格。非正的宽度返回空字符串,因此算出来为 0 的间隙什么都不加。在测量缓存持有它们期间,相同宽度共享同一个对象,拼好的行因此可以用同一性比较它们;清空缓存不改变任何宽度,只是重建相等的新对象。布局用它写出任何字符都无法表达的间距:间隙、对齐,以及让文本列在像素边界上对齐的空格。
返回。 display space 字符串;非正宽度返回空字符串。
示例。
(ebox-string-pixel-width (ebox-pixel-space 8)) ⇒ 8参见。 ebox-string-pixel-width、ebox-clear-cache。
Function: ebox-string-pixel-width (string)
Section titled “Function: ebox-string-pixel-width (string)”返回 STRING 第一行的显示像素宽度。
参数。 STRING 是要测量的字符串,nil 当作 ""。
语义。 只测量第一行,即第一个换行符之前的部分。测量遵循字符串自身的属性:带 face 属性的一段按那个 face 测量,像素空格按其宽度测量,多字符簇作为自己的单位测量。STRING 不会被插入任何缓冲区,当前缓冲区也不被修改,因此可以在拼布局的过程中调用;重复测量由按 face 的缓存回答。
返回。 以整数计的像素宽度。空字符串测出 0,以换行符开头的字符串同样是 0。
示例。
(= (ebox-string-pixel-width "Save\nDraft") (ebox-string-pixel-width "Save")) ⇒ t参见。 ebox-pixel-space、ebox-clear-cache。
Function: ebox-clear-cache ()
Section titled “Function: ebox-clear-cache ()”在字体或命名 face 的度量变化之后清空像素宽度缓存。
参数。 无。
语义。 被记住的字形宽度、簇宽度、像素空格、图像宽度与默认字体度量全部丢弃,之后每次测量都按当前度量重新计算。已经拼好的行不会重新布局,它们保留自己的文本,直到下一次由树驱动的更新重写。这是一个宿主调用:配置改变了默认字体、某个命名 face 的字体,或经 face remapping 的文本缩放时调用它;Ebox 不会自己调用,重复调用无害。
返回。 新的测量代次,一个每次调用都递增的整数。
示例。
(ebox-clear-cache) ⇒ 1 ; 第一次清理;下一次调用返回 2参见。 ebox-string-pixel-width、ebox-pixel-space。
两个原语可以往返:被 ebox-string-pixel-width 测量的像素空格正好报告它的宽度,因此布局可以用空格预留像素再测量结果而无需特例,而混有单词与空格的文本测出来就是各部分之和。
字体回退与行高
Section titled “字体回退与行高”当一个字符的 face 没有它的字形时,Emacs 用回退字体绘制它。回退字体的 ascent 或 descent 可能大于它所代替的 face,而一行的高度等于行内最大 ascent 加最大 descent,所以未拟合的回退字形会让它所在的行比周围的行更高,从而移动纵向网格。
Ebox 在图形 frame 上通过拟合这类字形来保持行高。回退字体可能超过默认字体行高的字符,会与一个普通字符放在一起测量——比较必须在行里做而不是孤立地做,因为行高结合了最大 ascent 与最大 descent。当候选会让行更高时,Ebox 把它缩小绘制,按固定的 height 缩放序列从 0.9 一直试到 0.3,取仍能放进该行的最大缩放。在 macOS 图形 frame 上,会超出行的彩色 emoji 改为按整行高显示的图像;该路径与它的缓存在图像一章。字符本身绝不被换成别的字符,只是以拟合形态显示,所以从缓冲区复制出来的文本仍是原来的字符。
终端与批处理会话没有可供测量的字体,因此那里不做拟合,每一行就是一个文本行。没有任何缩放能装下的簇以最小缩放绘制,该行仍是单独一行,因为一行变高会让它下面每一行都移位。
图形 frame 与终端
Section titled “图形 frame 与终端”同一棵树在图形 frame 与终端上都能渲染;上面的模型不变,它依赖的能力会变。
| 方面 | 图形 frame | 终端 |
|---|---|---|
| 测量 | 真实字体度量;字符宽度是字体像素 | 每行一个文本行;宽度跟随终端的字符网格 |
| 拟合 | 回退字形被拟合,彩色 emoji 可以用图像 | 不拟合;一行就是文本行 |
| 背景 | 圆角与分数块 padding 以形状绘制 | 背景填满整个边框盒 |
| 图片 | 按 frame 的 cell 度量绘制 | 用字符近似 |
批处理会话没有窗口也没有显示:挂载照常渲染,视口退回显示窗口(有的话),否则退回给定的固定尺寸,测量使用默认字体的名义度量。测试、预览与命令行工具依赖这一点,但这也意味着在批处理里测出的像素宽度不必等于图形 frame 上的宽度,需要精确的布局应当以图形方式检查。
测量原语接受退化输入而不报条件:nil 与空字符串测出 0,非正的像素宽度不产生空格。没有字体可拟合时,拟合被跳过而不是报错,所以同一棵树在每种显示类型上都用同样的公共调用渲染。
测量结果只会在它所依据的度量改变时过期。恢复手段是 ebox-clear-cache,之后下一次测量按新度量重算;在那之前,已经拼好的行保留旧文本,清理之后的那次由树驱动的更新才让新度量在缓冲区里可见。Ebox 从不自己清缓存,所以改了字体却没有清理的宿主会一直报告旧字体的宽度。
缓冲区是输出,所以对它做外来编辑没有自己的恢复路径:下一次由树驱动的更新会重写受影响的行,编辑随之消失。必须接受编辑的文档得把编辑建模在树这一侧,这正是文本输入与增量更新接口所提供的能力;文本输入和增量更新与发布两章讲这些接口。