etaf-ui/README.zh-CN.md

12 KiB
Raw Permalink Blame History

etaf-ui

etaf-ui 是 ETAF 的官方 Component 目录,是建立在 etaf 之上的可选独立包。它不增加第二套 Control 或 Widget 模型Button、Checkbox、Label、Panel、DataGrid 都是使用同一套 View、属性、slot、事件和 Data 契约的普通 Component。

所有 View 都使用准确的注册名称:etaf-paneletaf-labeletaf-buttonetaf-checkbox。把下面完整例子放进启用 lexical-binding 的 .el 文件求值, 再切换到 *etaf-preferences* 使用控件。

;;; -*- lexical-binding: t; -*-
(require 'etaf-ui)

(etaf-define-component demo-preferences ()
  :setup (list (etaf-ref nil) (etaf-ref "Unsaved"))
  :render
  (let* ((state (etaf-state))
         (done (car state))
         (saved (cadr state)))
    (etaf-view
     (etaf-panel :title "Account"
       (slot :name 'header
         (etaf-button :label "Save" :ref 'save-button
                      :on-press (lambda ()
                                  (setf (etaf-value saved) "Saved"))))
       (etaf-label :text (etaf-value saved))
       (etaf-checkbox :checked (etaf-value done) :label "Done"
                      :ref 'done-checkbox
                      :on-change (lambda (next)
                                   (setf (etaf-value done) next)))
       (text (expr (if (etaf-value done) "Complete" "Pending")))))))

(etaf-mount "*etaf-preferences*" (etaf-view (demo-preferences)))

Core 不会自动加载 .etaf。可选的 Playground 读取 inert 结构源,并显式加载 已注册的 Elisp companion这个文件格式不会生成更短的 Component alias。 普通本地回调不需要命名 Action目录组件与应用自己定义的组件使用相同的 props 和 slot API。

Button 和 Checkbox 都是受控组件。etaf-button 支持 :label:on-press:disabled:ref,以及最小 presentation 属性 :class:color:bgcolor:border:padding:font-weight:tab-index:aria-label 和公共 Behavior 列表 :use。启用按钮默认使用 button role 和 tab-index 0;禁用按钮仍显示标签和禁用样式,但没有 press 回调、Behavior也不进入 tab 顺序。etaf-checkbox 支持 :checked:label:on-change:disabled:ref 及相同的 presentation 属性。启用时它提供 checkbox role、稳定的 :reftab-index 0:on-change 接收下一个 布尔值。禁用 checkbox 没有回调或 tab stop。

etaf-number-input 是一个受控的轻量输入组件;需要编辑数值时,它使用 Emacs 原生 minibuffer。它支持 :value:label:on-change:min:max:ref:disabled回调接收校验后的整数ref 仍由调用方持有。

目录统一使用语义化的 Theme 色彩 vocabulary主要 token 包括 :ui-fg:ui-bg:ui-border:ui-muted-fg:ui-disabled-*:ui-button-primary-*:ui-button-secondary-*:ui-button-ghost-*:ui-checkbox-*:ui-table-*:ui-data-grid-error-fg:ui-pagination-muted-fg:ui-panel-*。应用通过 ETAF 的 etaf-theme-provide 提供这些 token旧的 :ui-button-color 一类 alias 仍兼容。etaf-ui 不依赖 TP也不直接依赖 renderer-specific palette registry如果应用想使用 TP 的亮/暗 palette 数据,应先通过可选的 etaf-theme-tp adapter 转成同一份 ETAF Theme plist再交给 UI 目录。

Component 的 :styles 声明负责默认外观。调用者提供的非 nil presentation 属性会覆盖默认值;省略或传 nil 时,继承的 Theme/默认样式仍可生效。Button 只提供刻意收敛的 primarysecondaryghost 三种视觉 variant这是 视觉状态集合,不是第二套 widget 分类体系。

启用的 button 和 checkbox 共享同一套交互反馈:小手指针、原生 mouse-face 悬停反馈、可读的 help-echo、数字 tab stop 和语义 role。指针离开后 hover 自动结束,点击不会产生 retained 视觉状态。禁用控件保留文字和禁用样式,但 没有回调、指针激活或 tab stop。 显式 :on-* 回调和 Behavior 回调的组合由 Runtime 负责UI 包只声明控件和 视觉状态。

间距由父布局负责:rowcolumn 使用 :item-gap,例如 (row :item-gap 1 ...)flexgrid 使用 :gap。尤其需要将 row :gap 迁移为 row :item-gap。相邻控件保留明确间距,让 hover 和命中范围各自独立。

根事件透传按顺序追加:业务动作、由内到外的 wrapper callback、最后按声明顺序 运行的 Behavior。给 Checkbox 增加 :on-press 不会替换它的 :on-change 转换。 每个声明执行一次错误会中断后续回调。wrapper 的 :use 列表顺序连接,重复 Behavior name 在安装前报错。

禁用输入取 OR外层 nil 不能启用内部已经禁用的控件。禁用时 dispatch 和 focus 抛出 etaf-event-error,禁用 cell 控件会阻止指针激活落到父行,输入 Behavior 也不会安装。透传 :role:aria-checked 等组件所有的 aria 状态时,冲突值会 导致输入错误;调用方仍可覆盖 :aria-label:aria-description。完整规则见 交互与迁移说明

etaf-label 支持 :text:class:color:bgcolor:font-weight:width:border:padding:refetaf-panel 支持 :title、相同的 surface presentation 属性,并投影命名的 header slot 和默认 slot。

公共 Component 的 business props 由实际定义导出,并与透传的 Host 属性分开:

Component Business props
etaf-label :text:variant
etaf-button :label:on-press:disabled:ref:class:color:bgcolor:border:padding:font-weight:tab-index:aria-label:use:variant
etaf-checkbox :checked:label:on-change:disabled
etaf-panel :title:variant
etaf-number-input :value:label:on-change:disabled:min:max
etaf-table :columns:rows:row-key:row-ref:on-row-press:row-selected-p
etaf-data-grid :controller:columns:row-key:on-row-press:row-ref:row-selected-p:loading-label:error-label:empty-label
etaf-pagination :controller:previous-ref:next-ref:previous-label:next-label:class:color:bgcolor:border:padding:aria-label

目录中的每个公共 Component 都只挂载一个 Host root。未被作为 business prop 消费的所有合法 Host 属性(例如 :ref:class:aria-label)都会透传到 该 root透传规则并不意味着这些属性都是每个 Component 的 business prop。

DataGrid 支持列描述、函数型 :row-key,以及可选的 :loading-label:error-label:empty-label 或函数型 :row-selected-p。提供 :on-row-press 后,每一行都是带 button role 和 tab-index 0 的可交互 button且回调接收该行没有 :on-row-press 时,行 没有回调或 tab stop。

etaf-table 的交互行也可省略 :row-ref,框架会提供属于当前保留实例的稳定引用。 只有外部 focus、测试或集成需要直接寻址时才提供 ref不要根据行文字拼接全局 symbol。row key 负责数据身份ref 负责交互 Host 地址。

DataGrid 的 :row-ref 是可选项。交互式 grid 省略它时DataGrid 会根据 controller 持有的 row identity 派生稳定的内部 Host ref。fallback ref 不会 进入全局 symbol table并按每个 retained DataGrid instance 隔离,因此多个 grid 可以安全复用相同行 identity显式提供它时该回调拥有 identity并且必须为 每一行返回非 nil 的稳定 Host ref。 :row-key 仍然是 retained row 必须具备的非 nil 稳定标量 identity。 DataGrid 按 row key 保留唯一 action closure因此 selection 或 Data Range 更新不会为未变化的行重建 handler。 整数列 :width 表示可读字符容量不是裸像素DataGrid 原样使用 Ebox 原生字符单位, 并在非末列之间保留一个原生字符间距。使用 :width '(fr 1) 让列填充剩余宽度; 多个 fractional 列按正数权重分配这部分空间。例如让 (:key :title :width (fr 1))(:key :actions :width 22 :cell render-actions) 配合,可以在保留操作控件空间的 同时自适应标题宽度。表头和数据行使用相同的轨道及间距。自适应 cell 按分配的 宽度裁剪,不会挤宽相邻列。固定列仍保留声明的空间,父布局需要容纳这些控件 及列间距。

自定义 cell 的列宽在固定值与 fractional 值之间切换时,其 Component 状态保持。 全固定列的纯文本表格仍保留紧凑的文本渲染路径。

自定义 cell 与列迁移

Table 与 DataGrid 共用列契约。每一列都必须有唯一且非 nil 的 :key symbol、整数或字符串纯展示列也一样。默认 cell 将该字段显示为文字。 :cell 函数接收一个 row返回 nil、字符串、typed Host 或 Component View 或这些值组成的 proper sequence。Table 拥有 cell 宽度并裁剪溢出;纯文本表格 继续使用原有的紧凑表示。

下面将 :name 字段用两个不同的 column key 展示,并在第三列放入普通 Button。 行选择与 Open 按钮分别执行自己的动作:

;;; -*- lexical-binding: t; -*-
(require 'etaf-ui)

(etaf-mount
 "*etaf-cell-demo*"
 (etaf-view
  (etaf-table
   :rows '((:id 1 :name "Ada"))
   :row-key (lambda (row) (plist-get row :id))
   :on-row-press (lambda (row) (message "Select %s" (plist-get row :name)))
   :columns
   (list
    '(:key :name :label "Name" :width 12)
    (list :key :name-upper :label "Uppercase" :width 12
          :cell (lambda (row) (upcase (plist-get row :name))))
    (list :key :open :label "Action" :width 10
          :cell
          (lambda (row)
            (etaf-view
             (etaf-button
              :label "Open"
              :on-press (lambda ()
                          (message "Open %s" (plist-get row :name)))))))))))

迁移重复字段列时,保留一列的 :key :name,为另一列指定 :name-upper 等唯一 key再通过 :cell 读取原来的 :name 字段。缺失或重复 column key 都会报错。 row key 与 column key 让 cell 身份在重排时保留;改变 key 会替换该 cell 实例。

cell 函数使用 row 参数与普通词法捕获Context 来自消费它的 Table/Grid 挂载 位置,不会恢复作者的 props、etaf-state 或私有样式。cell 需要自己的状态、样式 或生命周期时,返回普通 Component在它的 :setup 中创建持久 ref 和 watch。 若有意共享作者的 ref应在创建回调前捕获稳定句柄不要在每次 cell 函数调用时 创建持久状态。

etaf-pagination 是受控 Data Component。它接收 Data controller 以及稳定的 :previous-ref:next-ref,使用带内边距的 secondary Button Previous / Next ),中间显示 Page N / M 与条数摘要。空间不足时,完整控件和摘要 自然换行。加载中或已经位于首/末页时会禁用对应动作。它不拥有页码状态, 并复用 Button 的交互契约和 Theme token。可选的 :previous-label:next-label 替换完整按钮文案,例如 " 上一页""下一页 ";稳定的英文 无障碍标签保持不变。

键盘焦点使用 ETAF 的公共 Runtime 端口:用 (etaf-focus-next runtime) 在带数值且非负 :tab-index 的可见 Host 之间 移动;需要代码触发动作时使用 (etaf-dispatch-event runtime ref 'press)。禁用控件和不可交互的 DataGrid 行不会进入 tab 顺序。

在该目录运行 make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs