etaf-ui/README.zh-CN.md

210 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# etaf-ui
`etaf-ui` 是 ETAF 的官方 Component 目录,是建立在 `etaf` 之上的可选独立包。它不增加第二套 Control 或 Widget 模型Button、Checkbox、Label、Panel、DataGrid 都是使用同一套 View、属性、slot、事件和 Data 契约的普通 Component。
所有 View 都使用准确的注册名称:`etaf-panel`、`etaf-label`、`etaf-button` 和
`etaf-checkbox`。把下面完整例子放进启用 lexical-binding 的 `.el` 文件求值,
再切换到 `*etaf-preferences*` 使用控件。
<!-- etaf-example: preferences -->
```elisp
;;; -*- 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、稳定的 `:ref``tab-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
只提供刻意收敛的 `primary`、`secondary`、`ghost` 三种视觉 variant这是
视觉状态集合,不是第二套 widget 分类体系。
启用的 button 和 checkbox 共享同一套交互反馈:小手指针、原生 `mouse-face`
悬停反馈、可读的 `help-echo`、数字 tab stop 和语义 role。指针离开后 hover
自动结束,点击不会产生 retained 视觉状态。禁用控件保留文字和禁用样式,但
没有回调、指针激活或 tab stop。
显式 `:on-*` 回调和 Behavior 回调的组合由 Runtime 负责UI 包只声明控件和
视觉状态。
间距由父布局负责:`row` 和 `column` 使用 `:item-gap`,例如
`(row :item-gap 1 ...)``flex` 和 `grid` 使用 `: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/docs/user-guide.zh.md#interaction-migration)。
`etaf-label` 支持 `:text`、`:class`、`:color`、`:bgcolor`、`:font-weight`、
`:width`、`:border`、`:padding` 和 `:ref`
`etaf-panel` 支持 `:title`、相同的
surface presentation 属性,并投影命名的 `header` slot 和默认 slot。
<!-- M0b1: business-props -->
公共 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` |
<!-- M0b1: single-host-root -->
目录中的每个公共 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 地址。
<!-- M0b1: row-ref-optional -->
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 按钮分别执行自己的动作:
<!-- etaf-example: cells -->
```elisp
;;; -*- 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`