跳转到内容

生态与分层

ETAF 由六个包组成,分层的原则是每一类事实只有一个所有者。本章说明什么是文本应用、每个包提供什么、包之间怎样依赖、每个包的公共接口到哪里为止、哪个包拥有哪些事实,以及一次状态变化走到 buffer 的路径。后面各章再分别详述这些主题。

文本应用把整个用户界面画在一个普通的 Emacs buffer 里。buffer 里是带文本属性的文字:face 决定颜色和字体,display 属性负责图片和水平对齐。装在 buffer 里的 keymap 把按键和鼠标事件交给界面。移动、搜索这类读取文字的 Emacs 命令,在这里和在别的 buffer 里一样可用。

这块画布有两种单位。水平方向按像素计:Ebox 测量它排的文字,再用精确到像素宽的空白补足剩余宽度,所以比例字体和全宽的 CJK 字符也能对齐。垂直方向按行计:每个盒子的高度都是整数行。

应用不直接编辑 buffer。它用 View 描述界面,把状态放在响应式源里,然后修改状态;框架只重写内容变了的行,其余的行保持不动。

同一个 buffer 在图形帧和终端帧里都能用。终端帧画出来的样子简单一些,比如圆角变成方角、图片换成文字;具体差别见图形帧与终端帧。

包 拥有 交给其他包
ecss 选择器与优先级、样式表、来源与 important、层叠层、CSS 全局值、自定义属性与 var()、继承、计算样式,以及每个计算值的来源追溯 文本属性、布局、响应式更新和 buffer
ebox 由 text、box、row、column、flex、grid、image 节点组成的保留树;经 ECSS 得到的计算样式;文字测量;盒、flex 与 grid 布局;滚动与视口;行合成与直接的 buffer 更新;焦点、指针目标、keymap、拖拽与文字输入会话 组件、应用状态和响应式
etaf View 与 Component 语法、响应式状态与绑定、Runtime 的生命周期、Context、Theme、Behavior、Action、Data 与 Resource、分作用域的键盘层,以及对已提交 Host 的查询 布局、命中测试、焦点顺序和 buffer 写入,这些都委托给 Ebox
etaf-ui 官方组件目录:带键盘行为的可复用组件,以及它们读取的主题 token 布局、焦点和命中测试:它的组件都是普通的 ETAF 组件
etaf-db ETAF Data Source 契约的数据库后端;目前是 SQLite Data Controller,它属于 ETAF
ekp 中日韩文字与拉丁文字混排段落的 Knuth–Plass 最优断行,含避头尾规则、断词和像素级两端对齐;用 Emacs Lisp 实现,可选用 C 模块 —

ECSS 和 Ebox 可以脱离上层的包单独使用。需要布局和 buffer 渲染、但用不着组件和响应式状态的包,适合直接用 Ebox。任何程序只要注册自己的属性词表,就能用 ECSS 计算样式,结果拿去驱动文字、控件还是诊断信息都可以。EKP 给普通的 Emacs 文本排版,其他包都不使用它。

每个包在入口文件的 Package-Requires 头里声明依赖:

包 版本 依赖
ecss 0.1.0 Emacs 28.1
ekp 1.0.0 Emacs 29.1
ebox 3.0.0 Emacs 29.1、ecss 0.1.0
etaf 0.2.1 Emacs 29.1、ebox 3.0.0、ecss 0.1.0
etaf-ui 0.1.0 Emacs 29.1、etaf 0.2.1、ebox 3.0.0
etaf-db 0.1.0 Emacs 29.1、etaf 0.2.1

依赖只朝一个方向。下图中箭头从一个包指向依赖它的包:

ecss ──▶ ebox ──▶ etaf ──┬──▶ etaf-ui
└──▶ etaf-db
ekp

没有哪个包依赖它上层的包,应用位于 etaf-ui 和 etaf-db 之上。ETAF 还直接依赖 ECSS:解析 Theme token 时,有些值要等到层叠阶段才由 ECSS 计算,直接依赖才能把这些值原样保留下来。

应用使用 ETAF 和组件目录,不调用 Ebox。应用需要只有 Ebox 才有的能力时,比如测量或控制视口,由 ETAF 为它发布接口,应用再使用这个接口。

每个包在仓库根目录有一个入口文件,以它提供的 feature 命名,例如 etaf.el。实现放在 lisp/ 目录下的文件里。加载入口文件时,它会把这个目录加进 load-path 并加载实现,所以程序只需 require 入口 feature:

(require 'etaf)

程序不要 require lisp/ 下的文件。那些 feature 都是内部的,它们的名字以及实现在它们之间的划分,任何版本都可能改变。

入口文件的 Commentary 是包所支持接口的权威清单。每个公共函数、宏、变量、hook、错误条件和组件各有一条记录,记录后面是对该接口的说明:

;; Function: etaf-mount (buffer-or-name view &optional options)

Commentary 没有列出的符号都是内部的,名字里没有双连字符也一样;lisp/ 下的 feature 和文件同样是内部的。模块地图描述这些模块负责什么,但不描述它们的符号。调用内部函数、读取内部变量、加载内部文件、重定义内部符号都不受支持,通过运行时拼出的符号去做也一样。

除了这些记录,各包还发布 View 和样式表按名字使用的词表:ETAF 发布 Host 种类及其属性,Ebox 发布样式属性,组件目录发布主题 token。它们和记录一样是公共的,本书为每一项写一个定义条目。公共接口的变化怎样记录,见层与稳定性。

每一类事实只有一个所有者。其他包通过所有者的公共接口去问,而不是自己留一份副本,所以两个包对同一事实不会有分歧:

事实 所有者
样式属性胜出的声明与计算值,包括继承来的值和变量 ecss
节点树、几何、行合成、哪段 buffer 文字属于哪个节点、焦点顺序、指针事件的目标、拖拽、滚动和文字输入会话 ebox
应用状态、组件实例及其 Scope、Context、Theme、Action、Data 和 Resource etaf
每种控件的含义,比如激活按钮或复选框会发生什么 etaf-ui
数据库存储 etaf-db
业务状态 应用

由此可以推出下面几点,后面各章描述的许多行为都源于它们:

  • ETAF 保留从 Host 到 Ebox 节点的映射,用来分发事件和回答查询,但它自己不保存任何布局、命中测试或焦点状态。几何查询由 Ebox 根据当前布局回答。
  • 可见的变化只经 Ebox 进入 buffer。ETAF 和应用都不往已挂载的 buffer 里写文字。
  • 绑定到响应式状态的 Host 属性不是那份状态的第二份副本:状态变了,属性随之改变。
  • 输出不对时,去找那个出错事实的所有者:颜色不对是层叠的问题,位置不对是布局的问题,值没更新则要查显示它的那个绑定依赖了什么。

已挂载界面的每一次变化都走同一条路,从写入一直到 buffer:

写入 排空 节点更新 flush buffer
ref、Action、 ─▶ 运行变脏的 ─▶ 属性、文字 ─▶ 样式、布局、 ─▶ 重写变化
Data 副作用 与子节点列表 行合成 的行
  1. 挂载。 etaf-mount 把 View 实例化一次。它建立一棵保留树,记住 View 每个部分的身份,并为每个 Host 建一个 Ebox 节点;第一次 flush 写入整个 buffer。View 的静态部分只构建一次,以后不再计算。每个动态部分都成为一个绑定,比如由 Lisp 计算的属性值、写成 (expr FORM) 的文字、条件或列表。绑定是一个副作用,和它负责更新的那部分树绑在一起。
  2. 写入。 应用代码修改状态:写一个 ref,派发一个会写 ref 的 Action,或者收到新的 Data。读取了这份状态的绑定变脏,其他绑定不受影响。
  3. 排空。 ETAF 按阶段顺序运行变脏的副作用,过程见一次写入的完整路径。属性绑定设置它那个 Ebox 节点的属性,文字绑定设置节点的文字,条件或列表只修补自己的子节点;带 key 的子节点保留原来的节点,只移动位置。组件的 props 变了,只有读取它们的绑定会重新运行。用 render 函数写的组件会重新计算整个输出,再把输出修补进同一棵保留树。
  4. flush。 节点有变化的每个 Runtime 提交一次。Ebox 经 ECSS 计算变化节点的样式,按当前视口布局整棵树,只重算内容或约束变了的盒子;固定尺寸的盒子会挡住变化,不让它传给祖先。然后 Ebox 合成行,把变化的区域转换成行编辑,只重写这些行。
  5. 收尾。 post 副作用运行,然后运行用 etaf-next-tick 安排的函数。

输入也进入同一个循环。Ebox 把 Emacs 的按键或鼠标事件解析到获得焦点或被指中的节点,ETAF 把这个节点对应到已提交的 Host,在一个批次里调用这个 Host 的处理函数。处理函数写入状态,批次结束时,第 2 到第 5 步执行一次。

所以,一次更新的代价随变化的绑定数和变化的行数增长,与 View 的大小无关。「框架内部与扩展」部分有一章追踪一次更新,会带着一次写入走完上面每一步。