refactor: lower hosts through canonical Ebox properties
This commit is contained in:
parent
0923925f1e
commit
8e305b172e
@ -19,7 +19,7 @@ The only child computation bridge is `expr :value`; attribute values are ordinar
|
|||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text
|
(text
|
||||||
:color "#687386"
|
:color "#687386"
|
||||||
(expr :value (if ready "Ready" "Waiting")))))
|
(expr :value (if ready "Ready" "Waiting")))))
|
||||||
@ -31,7 +31,7 @@ Define a Component:
|
|||||||
(etaf-define-component status-label (&key label)
|
(etaf-define-component status-label (&key label)
|
||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text :face 'bold (expr :value label)))
|
(text :font-weight 'bold (expr :value label)))
|
||||||
|
|
||||||
(etaf-mount
|
(etaf-mount
|
||||||
"*etaf-demo*"
|
"*etaf-demo*"
|
||||||
|
|||||||
@ -19,7 +19,7 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
|
|||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text
|
(text
|
||||||
:color "#687386"
|
:color "#687386"
|
||||||
(expr :value (if ready "Ready" "Waiting")))))
|
(expr :value (if ready "Ready" "Waiting")))))
|
||||||
@ -31,7 +31,7 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
|
|||||||
(etaf-define-component status-label (&key label)
|
(etaf-define-component status-label (&key label)
|
||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text :face 'bold (expr :value label)))
|
(text :font-weight 'bold (expr :value label)))
|
||||||
|
|
||||||
(etaf-mount
|
(etaf-mount
|
||||||
"*etaf-demo*"
|
"*etaf-demo*"
|
||||||
|
|||||||
@ -45,7 +45,6 @@ These are mechanisms rather than additional visual node types:
|
|||||||
- `watch` observes reactive state.
|
- `watch` observes reactive state.
|
||||||
- `Context` provides inherited dependencies.
|
- `Context` provides inherited dependencies.
|
||||||
- `Data` owns application-data state and source requests.
|
- `Data` owns application-data state and source requests.
|
||||||
- `raw-ebox` is the explicit low-level escape at the ETAF/Ebox boundary.
|
|
||||||
|
|
||||||
## 3. The unified View grammar
|
## 3. The unified View grammar
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ Properties must be complete before the first child. A child can be a string, a n
|
|||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
:class "welcome"
|
:class "welcome"
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text
|
(text
|
||||||
:color "#687386"
|
:color "#687386"
|
||||||
(expr :value (if ready "Ready" "Waiting")))))
|
(expr :value (if ready "Ready" "Waiting")))))
|
||||||
@ -78,12 +77,12 @@ not be interleaved.
|
|||||||
The rule is simple:
|
The rule is simple:
|
||||||
|
|
||||||
1. Structural View positions are not quoted. This includes `etaf-view`, Hosts, Component calls, children, slot forms, and static Component styles.
|
1. Structural View positions are not quoted. This includes `etaf-view`, Hosts, Component calls, children, slot forms, and static Component styles.
|
||||||
2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr :value`, `:setup`, Context values, Behavior constructors, Actions, and `raw-ebox :value`.
|
2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr :value`, `:setup`, Context values, Behavior constructors, and Actions.
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text
|
(text
|
||||||
:face (if dark 'light 'dark)
|
:color (if dark "#F4F6FB" "#1F2328")
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
@ -92,7 +91,7 @@ The rule is simple:
|
|||||||
:value
|
:value
|
||||||
(when open
|
(when open
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text :face 'bold "Details"))))))
|
(text :font-weight 'bold "Details"))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
`'bold` is an ordinary Elisp literal symbol. `'(text "Details")` is ordinary data, not a View; use `(etaf-view (text "Details"))` when an Elisp expression must construct a View. ETAF does not run `eval` on quoted View data and does not add separate literal/eval nodes.
|
`'bold` is an ordinary Elisp literal symbol. `'(text "Details")` is ordinary data, not a View; use `(etaf-view (text "Details"))` when an Elisp expression must construct a View. ETAF does not run `eval` on quoted View data and does not add separate literal/eval nodes.
|
||||||
@ -138,7 +137,7 @@ The Component definition is the current structure/style/behavior boundary. Keep
|
|||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text
|
(text
|
||||||
:face 'bold
|
:font-weight 'bold
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -165,7 +164,7 @@ The Component definition is the current structure/style/behavior boundary. Keep
|
|||||||
|
|
||||||
`:setup` runs once for a retained Component instance and must return a zero-argument render function. Re-render reads current props and refs without rerunning setup. Setup is the owner for local refs, computed values, watches, Effects, and cleanup registration.
|
`:setup` runs once for a retained Component instance and must return a zero-argument render function. Re-render reads current props and refs without rerunning setup. Setup is the owner for local refs, computed values, watches, Effects, and cleanup registration.
|
||||||
|
|
||||||
`:key` is stable identity metadata, not a business prop. On a Component call it selects the retained Component instance within the sibling scope; on a Host or `raw-ebox` it is forwarded as the Ebox node key. If a render candidate fails, the Runtime restores the previous instance, handlers, behaviors, and buffer.
|
`:key` is stable identity metadata, not a business prop. On a Component call it selects the retained Component instance within the sibling scope; on a Host it is forwarded as the Ebox node key. If a render candidate fails, the Runtime restores the previous instance, handlers, behaviors, and buffer.
|
||||||
|
|
||||||
In View syntax, canonical Component names may omit the `etaf-` prefix. If the short name would collide with an Elisp function, special form, or Host, the registry assigns a semantic `-view` alias. Ordinary Elisp APIs such as `etaf-value`, `etaf-ref`, and `etaf-mount` always keep their prefix.
|
In View syntax, canonical Component names may omit the `etaf-` prefix. If the short name would collide with an Elisp function, special form, or Host, the registry assigns a semantic `-view` alias. Ordinary Elisp APIs such as `etaf-value`, `etaf-ref`, and `etaf-mount` always keep their prefix.
|
||||||
|
|
||||||
@ -193,7 +192,7 @@ Named content uses the same structural shape:
|
|||||||
```elisp
|
```elisp
|
||||||
(card
|
(card
|
||||||
:title "Account"
|
:title "Account"
|
||||||
(slot :name 'header (text :face 'bold "Account settings"))
|
(slot :name 'header (text :font-weight 'bold "Account settings"))
|
||||||
(text "Card body"))
|
(text "Card body"))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -201,13 +200,13 @@ Inside a Component View, the default outlet and its fallback are:
|
|||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(slot)
|
(slot)
|
||||||
(slot (text :face 'shadow "No content"))
|
(slot (text :color "#687386" "No content"))
|
||||||
```
|
```
|
||||||
|
|
||||||
The explicit normalized spelling is:
|
The explicit normalized spelling is:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(slot :name 'default (text :face 'shadow "No content"))
|
(slot :name 'default (text :color "#687386" "No content"))
|
||||||
```
|
```
|
||||||
|
|
||||||
Named slot names are stable non-keyword symbols. Strings, numbers, variables, and runtime expressions are rejected because a slot name is part of retained structure. A named input may appear only once. An explicitly empty input `(slot :name 'header)` suppresses the outlet fallback. Slot forms do not create Ebox wrappers.
|
Named slot names are stable non-keyword symbols. Strings, numbers, variables, and runtime expressions are rejected because a slot name is part of retained structure. A named input may appear only once. An explicitly empty input `(slot :name 'header)` suppresses the outlet fallback. Slot forms do not create Ebox wrappers.
|
||||||
@ -240,16 +239,7 @@ Strings are the smallest text View and lower to Ebox content. Nested `text` View
|
|||||||
|
|
||||||
ETAF's Renderer is the only framework module that calls Ebox. It uses Ebox public constructors, property readers, host-reference queries, and publication APIs. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data.
|
ETAF's Renderer is the only framework module that calls Ebox. It uses Ebox public constructors, property readers, host-reference queries, and publication APIs. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data.
|
||||||
|
|
||||||
`raw-ebox` is the one deliberate escape:
|
The public View grammar does not accept raw Ebox Nodes. Framework integrations construct canonical TextNode and BoxNode values through Ebox's typed integration port, while ordinary ETAF applications stay on Hosts and Components. This keeps measurement, identity, and rollback ownership inside one lowering path.
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-view
|
|
||||||
(raw-ebox
|
|
||||||
:key 'backend-row
|
|
||||||
:value (ebox-create :content "Low-level")))
|
|
||||||
```
|
|
||||||
|
|
||||||
It accepts only `:value` and optional `:key`. The returned Ebox Node remains opaque and does not receive Component props, slots, events, or Behaviors. Using it transfers measurement, identity, rollback, and backend responsibility to the caller.
|
|
||||||
|
|
||||||
## 7. Runtime and reactive state
|
## 7. Runtime and reactive state
|
||||||
|
|
||||||
@ -470,6 +460,5 @@ Before adding a new concept, choose the smallest existing owner:
|
|||||||
| Cross-depth dependency | Context |
|
| Cross-depth dependency | Context |
|
||||||
| Request or mutation state | Data / Resource |
|
| Request or mutation state | Data / Resource |
|
||||||
| Geometry or layout algorithm | Ebox |
|
| Geometry or layout algorithm | Ebox |
|
||||||
| Low-level backend escape | `raw-ebox` |
|
|
||||||
|
|
||||||
Add a new public concept only when an existing owner cannot express the behavior, the new owner can state identity/lifecycle/error/rollback rules, and a public-path test can prove it. This keeps the model small while retaining full Elisp expressiveness.
|
Add a new public concept only when an existing owner cannot express the behavior, the new owner can state identity/lifecycle/error/rollback rules, and a public-path test can prove it. This keeps the model small while retaining full Elisp expressiveness.
|
||||||
|
|||||||
@ -45,7 +45,6 @@ ETAF 是面向文本应用的统一 View 与 Component 层。Elisp 仍然是完
|
|||||||
- `watch` 观察响应式状态。
|
- `watch` 观察响应式状态。
|
||||||
- `Context` 提供继承的依赖。
|
- `Context` 提供继承的依赖。
|
||||||
- `Data` 管理应用数据状态和数据源请求。
|
- `Data` 管理应用数据状态和数据源请求。
|
||||||
- `raw-ebox` 是 ETAF/Ebox 边界上的明确底层出口。
|
|
||||||
|
|
||||||
## 3. 统一 View 语法
|
## 3. 统一 View 语法
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ ATTRIBUTE = :KEY VALUE
|
|||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
:class "welcome"
|
:class "welcome"
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text
|
(text
|
||||||
:color "#687386"
|
:color "#687386"
|
||||||
(expr :value (if ready "Ready" "Waiting")))))
|
(expr :value (if ready "Ready" "Waiting")))))
|
||||||
@ -77,12 +76,12 @@ ATTRIBUTE = :KEY VALUE
|
|||||||
规则只有两条:
|
规则只有两条:
|
||||||
|
|
||||||
1. 结构性 View 位置不需要 quote,包括 `etaf-view`、Host、Component 调用、子节点、slot 和静态 Component styles。
|
1. 结构性 View 位置不需要 quote,包括 `etaf-view`、Host、Component 调用、子节点、slot 和静态 Component styles。
|
||||||
2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr :value`、`:setup`、Context 值、Behavior 构造器、Action 以及 `raw-ebox :value`。
|
2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr :value`、`:setup`、Context 值、Behavior 构造器和 Action。
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text
|
(text
|
||||||
:face (if dark 'light 'dark)
|
:color (if dark "#F4F6FB" "#1F2328")
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
@ -91,7 +90,7 @@ ATTRIBUTE = :KEY VALUE
|
|||||||
:value
|
:value
|
||||||
(when open
|
(when open
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text :face 'bold "Details"))))))
|
(text :font-weight 'bold "Details"))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
`'bold` 是普通 Elisp 字面量 symbol。`'(text "Details")` 只是普通数据,不是 View;当 Elisp 表达式需要构造 View 时,使用 `(etaf-view (text "Details"))`。ETAF 不会对被 quote 的 View 数据再次 `eval`,也不增加单独的 literal/eval 节点。
|
`'bold` 是普通 Elisp 字面量 symbol。`'(text "Details")` 只是普通数据,不是 View;当 Elisp 表达式需要构造 View 时,使用 `(etaf-view (text "Details"))`。ETAF 不会对被 quote 的 View 数据再次 `eval`,也不增加单独的 literal/eval 节点。
|
||||||
@ -137,7 +136,7 @@ Component definition 是当前结构/样式/行为边界:动态状态、Action
|
|||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text
|
(text
|
||||||
:face 'bold
|
:font-weight 'bold
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -164,7 +163,7 @@ Component definition 是当前结构/样式/行为边界:动态状态、Action
|
|||||||
|
|
||||||
`:setup` 对一个 retained Component instance 只运行一次,并且必须返回零参数 render 函数。重新渲染读取当前 props 和 ref,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup 的创建。
|
`:setup` 对一个 retained Component instance 只运行一次,并且必须返回零参数 render 函数。重新渲染读取当前 props 和 ref,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup 的创建。
|
||||||
|
|
||||||
`:key` 是稳定的 identity metadata,不是业务 prop。放在 Component 调用上时,它选择同级作用域内要保留的 Component instance;放在 Host 或 `raw-ebox` 上时,它会作为 Ebox node key 向下传递。候选渲染失败时,Runtime 恢复旧 instance、handlers、Behaviors 和 buffer。
|
`:key` 是稳定的 identity metadata,不是业务 prop。放在 Component 调用上时,它选择同级作用域内要保留的 Component instance;放在 Host 上时,它会作为 Ebox node key 向下传递。候选渲染失败时,Runtime 恢复旧 instance、handlers、Behaviors 和 buffer。
|
||||||
|
|
||||||
在 View 语法中,Component 的规范名称可以省略 `etaf-` 前缀。如果短名称会与 Elisp 函数、special form 或 Host 冲突,注册表会分配语义明确的 `-view` alias。普通 Elisp API,例如 `etaf-value`、`etaf-ref` 和 `etaf-mount`,始终保留前缀。
|
在 View 语法中,Component 的规范名称可以省略 `etaf-` 前缀。如果短名称会与 Elisp 函数、special form 或 Host 冲突,注册表会分配语义明确的 `-view` alias。普通 Elisp API,例如 `etaf-value`、`etaf-ref` 和 `etaf-mount`,始终保留前缀。
|
||||||
|
|
||||||
@ -192,7 +191,7 @@ children 只是匿名/默认 slot 的便捷写法,不是第二套内容模型
|
|||||||
```elisp
|
```elisp
|
||||||
(card
|
(card
|
||||||
:title "Account"
|
:title "Account"
|
||||||
(slot :name 'header (text :face 'bold "Account settings"))
|
(slot :name 'header (text :font-weight 'bold "Account settings"))
|
||||||
(text "Card body"))
|
(text "Card body"))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -200,13 +199,13 @@ children 只是匿名/默认 slot 的便捷写法,不是第二套内容模型
|
|||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(slot)
|
(slot)
|
||||||
(slot (text :face 'shadow "No content"))
|
(slot (text :color "#687386" "No content"))
|
||||||
```
|
```
|
||||||
|
|
||||||
完整的内部规范写法是:
|
完整的内部规范写法是:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(slot :name 'default (text :face 'shadow "No content"))
|
(slot :name 'default (text :color "#687386" "No content"))
|
||||||
```
|
```
|
||||||
|
|
||||||
对用户来说,优先使用前两个简写;只有需要明确名字时才写 `:name`。Slot 名称必须是稳定的、非 keyword 的 symbol。字符串、数字、变量和运行时表达式都会被拒绝,因为 slot 名称属于 retained 结构。同名输入只能出现一次。显式空输入 `(slot :name 'header)` 会抑制 outlet fallback。Slot form 不创建 Ebox wrapper。
|
对用户来说,优先使用前两个简写;只有需要明确名字时才写 `:name`。Slot 名称必须是稳定的、非 keyword 的 symbol。字符串、数字、变量和运行时表达式都会被拒绝,因为 slot 名称属于 retained 结构。同名输入只能出现一次。显式空输入 `(slot :name 'header)` 会抑制 outlet fallback。Slot form 不创建 Ebox wrapper。
|
||||||
@ -239,16 +238,7 @@ text · fragment · container · row · column · stack · flex · grid · space
|
|||||||
|
|
||||||
ETAF 的 Renderer 是唯一调用 Ebox 的框架模块,并且只使用 Ebox 公共构造器、属性读取器、Host 引用查询和发布 API。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。
|
ETAF 的 Renderer 是唯一调用 Ebox 的框架模块,并且只使用 Ebox 公共构造器、属性读取器、Host 引用查询和发布 API。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。
|
||||||
|
|
||||||
`raw-ebox` 是唯一明确的底层出口:
|
公共 View 语法不接受裸 Ebox Node。框架集成层通过 Ebox 的 typed integration port 构造规范的 TextNode 和 BoxNode;普通 ETAF 应用只使用 Host 与 Component。这样 measurement、identity 和 rollback 始终由同一条 lowering 路径负责。
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-view
|
|
||||||
(raw-ebox
|
|
||||||
:key 'backend-row
|
|
||||||
:value (ebox-create :content "Low-level")))
|
|
||||||
```
|
|
||||||
|
|
||||||
它只接受 `:value` 和可选的 `:key`。返回的 Ebox Node 保持 opaque,不获得 Component props、slot、事件或 Behavior 语义。使用它意味着调用方承担 measurement、identity、rollback 和 backend 契约。
|
|
||||||
|
|
||||||
## 7. Runtime 与响应式状态
|
## 7. Runtime 与响应式状态
|
||||||
|
|
||||||
@ -464,6 +454,5 @@ registry 决定当前 authority。失败 candidate 只移除 staged Behavior res
|
|||||||
| 跨层级依赖 | Context |
|
| 跨层级依赖 | Context |
|
||||||
| 请求或变更状态 | Data / Resource |
|
| 请求或变更状态 | Data / Resource |
|
||||||
| 几何或布局算法 | Ebox |
|
| 几何或布局算法 | Ebox |
|
||||||
| 低层 backend 出口 | `raw-ebox` |
|
|
||||||
|
|
||||||
只有在现有 owner 无法表达、能够明确 identity/lifecycle/error/rollback 规则,并且可以用公共路径测试证明时,才增加新的公共概念。这样既保留完整的 Elisp 表达能力,又让用户模型保持干净。
|
只有在现有 owner 无法表达、能够明确 identity/lifecycle/error/rollback 规则,并且可以用公共路径测试证明时,才增加新的公共概念。这样既保留完整的 Elisp 表达能力,又让用户模型保持干净。
|
||||||
|
|||||||
@ -57,8 +57,8 @@ Do not split a file merely to create a shorter name. Split only when a stable re
|
|||||||
- Attribute values are ordinary Elisp expressions.
|
- Attribute values are ordinary Elisp expressions.
|
||||||
- The only child computation bridge is `(expr :value ELISP-EXPRESSION)`.
|
- The only child computation bridge is `(expr :value ELISP-EXPRESSION)`.
|
||||||
- A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed.
|
- A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed.
|
||||||
- Core Hosts are `text`, `fragment`, `container`, `row`, `column`, `stack`, `flex`, `grid`, and `spacer`.
|
- The six visible Host forms are `text`, `box`, `row`, `column`, `flex`, and `grid`; `fragment`, `slot`, and `expr` are transparent framework structure.
|
||||||
- `raw-ebox` accepts only `:value` and optional `:key` and remains opaque to ETAF semantics.
|
- There is no raw Ebox View escape hatch. Framework integrations use Ebox's evaluated typed-constructor port outside the author DSL.
|
||||||
|
|
||||||
### 4.2 Component and slots
|
### 4.2 Component and slots
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ Do not split a file merely to create a shorter name. Split only when a stable re
|
|||||||
- Trailing children are `slots.default`; named slot input uses `(slot :name 'NAME CHILD...)`.
|
- Trailing children are `slots.default`; named slot input uses `(slot :name 'NAME CHILD...)`.
|
||||||
- Default outlet shorthand is `(slot)` or `(slot FALLBACK...)`.
|
- Default outlet shorthand is `(slot)` or `(slot FALLBACK...)`.
|
||||||
- Slot names are stable, non-keyword symbols only.
|
- Slot names are stable, non-keyword symbols only.
|
||||||
- `:key` is identity metadata, not a business prop: Component calls use it for retained identity, while Hosts and `raw-ebox` forward it as an Ebox node key.
|
- `:key` is identity metadata, not a business prop: Component calls use it for retained identity, while Hosts project the same parsed fact into the Ebox source index.
|
||||||
|
|
||||||
### 4.3 Runtime and non-visual capabilities
|
### 4.3 Runtime and non-visual capabilities
|
||||||
|
|
||||||
|
|||||||
@ -57,8 +57,8 @@
|
|||||||
- 属性值是普通 Elisp 表达式。
|
- 属性值是普通 Elisp 表达式。
|
||||||
- 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)`。
|
- 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)`。
|
||||||
- Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。
|
- Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。
|
||||||
- Core Host 是 `text`、`fragment`、`container`、`row`、`column`、`stack`、`flex`、`grid` 和 `spacer`。
|
- 六个可见 Host form 是 `text`、`box`、`row`、`column`、`flex`、`grid`;`fragment`、`slot`、`expr` 是透明的框架结构。
|
||||||
- `raw-ebox` 只接受 `:value` 和可选 `:key`,并且对 ETAF 语义保持 opaque。
|
- View 作者 DSL 不提供 raw Ebox 逃生口;框架集成在作者 DSL 之外使用 Ebox evaluated typed-constructor 端口。
|
||||||
|
|
||||||
### 4.2 Component 与 slot
|
### 4.2 Component 与 slot
|
||||||
|
|
||||||
@ -69,7 +69,7 @@
|
|||||||
- 尾部 children 是 `slots.default`;命名 slot 输入使用 `(slot :name 'NAME CHILD...)`。
|
- 尾部 children 是 `slots.default`;命名 slot 输入使用 `(slot :name 'NAME CHILD...)`。
|
||||||
- 默认 outlet 简写是 `(slot)` 或 `(slot FALLBACK...)`。
|
- 默认 outlet 简写是 `(slot)` 或 `(slot FALLBACK...)`。
|
||||||
- Slot name 只能是稳定的、非 keyword 的 symbol。
|
- Slot name 只能是稳定的、非 keyword 的 symbol。
|
||||||
- `:key` 是 identity metadata,不是业务 prop:Component 调用用它选择 retained identity,Host 和 `raw-ebox` 则把它作为 Ebox node key 向下传递。
|
- `:key` 是 identity metadata,不是业务 prop:Component 调用用它选择 retained identity,Host 则把同一个解析后的事实投影到 Ebox source index。
|
||||||
|
|
||||||
### 4.3 Runtime 与非视觉能力
|
### 4.3 Runtime 与非视觉能力
|
||||||
|
|
||||||
|
|||||||
@ -432,7 +432,7 @@ inapplicable combinations are errors.
|
|||||||
|
|
||||||
| Owner | Property category | Representative properties |
|
| Owner | Property category | Representative properties |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| ETAF Runtime metadata | identity/semantics/events/Behavior | `:key`, `:ref`, `:role`, `:aria-*`, `:on-*`, `:use` |
|
| ETAF Runtime metadata | identity/semantics/events/Behavior | `:key`, `:ref`, `:role`, `:disabled`, `:tab-index`, `:aria-*`, `:on-*`, `:use` |
|
||||||
| ECSS author style source | selector/inline declarations | `:class`, `:id`, style declarations |
|
| ECSS author style source | selector/inline declarations | `:class`, `:id`, style declarations |
|
||||||
| Ebox TextNode | canonical text measurement | font metrics, wrap policy, intrinsic constraints |
|
| Ebox TextNode | canonical text measurement | font metrics, wrap policy, intrinsic constraints |
|
||||||
| Ebox BoxNode | external/internal layout | `:outer`, typed Layout variant |
|
| Ebox BoxNode | external/internal layout | `:outer`, typed Layout variant |
|
||||||
@ -496,7 +496,7 @@ or impact rules.
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
SharedSourceMetadata = :key :class :id
|
SharedSourceMetadata = :key :class :id
|
||||||
ETAFOnlyMetadata = :ref :role :aria-* :on-* :use
|
ETAFOnlyMetadata = :ref :role :disabled :tab-index :aria-* :on-* :use
|
||||||
ETAFSourceMetadata = SharedSourceMetadata + ETAFOnlyMetadata
|
ETAFSourceMetadata = SharedSourceMetadata + ETAFOnlyMetadata
|
||||||
EboxSourceMetadata = SharedSourceMetadata
|
EboxSourceMetadata = SharedSourceMetadata
|
||||||
|
|
||||||
@ -656,6 +656,14 @@ foreground. Use standard `:border-top` or its longhands when full control is req
|
|||||||
Mixing `:border-top-p` with its width/style outputs in the same declaration is an error;
|
Mixing `:border-top-p` with its width/style outputs in the same declaration is an error;
|
||||||
different cascade sources still override one another normally through ECSS.
|
different cascade sources still override one another normally through ECSS.
|
||||||
|
|
||||||
|
Canonical border widths are always nonnegative integer pixels, and `style=none` has a
|
||||||
|
used width of zero. Every successful adapter projection must be exact; it may not collapse
|
||||||
|
width to a boolean or character column. The current Emacs text adapter can exactly express
|
||||||
|
arbitrary left/right widths and 0/1px top/bottom lines. Thicker horizontal borders raise a
|
||||||
|
capability error before publication and preserve the old generation instead of degrading
|
||||||
|
silently. Core retains all four width/style/color facts so a future pixel-coordinate adapter
|
||||||
|
can widen the success domain.
|
||||||
|
|
||||||
`:grid-column-span` and `:grid-row-span` are likewise not aliases. They accept only a
|
`:grid-column-span` and `:grid-row-span` are likewise not aliases. They accept only a
|
||||||
positive integer and respectively expand to the corresponding Grid shorthand's `span N`
|
positive integer and respectively expand to the corresponding Grid shorthand's `span N`
|
||||||
value; mixing either with `:grid-column` or `:grid-row` in the same declaration is an
|
value; mixing either with `:grid-column` or `:grid-row` in the same declaration is an
|
||||||
|
|||||||
@ -402,7 +402,7 @@ ETAF 也不提供 `spacer` 类型。没有子节点的 Box 就是空 Box。
|
|||||||
|
|
||||||
| Owner | 属性类别 | 代表属性 |
|
| Owner | 属性类别 | 代表属性 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| ETAF Runtime metadata | identity/语义/事件/Behavior | `:key`、`:ref`、`:role`、`:aria-*`、`:on-*`、`:use` |
|
| ETAF Runtime metadata | identity/语义/事件/Behavior | `:key`、`:ref`、`:role`、`:disabled`、`:tab-index`、`:aria-*`、`:on-*`、`:use` |
|
||||||
| ECSS author style source | selector/inline declarations | `:class`、`:id`、style declarations |
|
| ECSS author style source | selector/inline declarations | `:class`、`:id`、style declarations |
|
||||||
| Ebox TextNode | canonical 文本测量 | font metrics、wrap policy、intrinsic constraints |
|
| Ebox TextNode | canonical 文本测量 | font metrics、wrap policy、intrinsic constraints |
|
||||||
| Ebox BoxNode | 外部/内部布局 | `:outer`、typed Layout variant |
|
| Ebox BoxNode | 外部/内部布局 | `:outer`、typed Layout variant |
|
||||||
@ -455,7 +455,7 @@ Layout 和 child participation 时只验证最终 candidate,任何失败保留
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
SharedSourceMetadata = :key :class :id
|
SharedSourceMetadata = :key :class :id
|
||||||
ETAFOnlyMetadata = :ref :role :aria-* :on-* :use
|
ETAFOnlyMetadata = :ref :role :disabled :tab-index :aria-* :on-* :use
|
||||||
ETAFSourceMetadata = SharedSourceMetadata + ETAFOnlyMetadata
|
ETAFSourceMetadata = SharedSourceMetadata + ETAFOnlyMetadata
|
||||||
EboxSourceMetadata = SharedSourceMetadata
|
EboxSourceMetadata = SharedSourceMetadata
|
||||||
|
|
||||||
@ -603,6 +603,12 @@ schema scope,不能污染全局 Ebox visual property domain。
|
|||||||
`:border-top` 或 longhands。同一声明混写 `:border-top-p` 与其 width/style 输出必须
|
`:border-top` 或 longhands。同一声明混写 `:border-top-p` 与其 width/style 输出必须
|
||||||
报错;不同 cascade source 仍按 ECSS 正常覆盖。
|
报错;不同 cascade source 仍按 ECSS 正常覆盖。
|
||||||
|
|
||||||
|
Border 的 canonical width 始终是非负整数像素,`style=none` 的 used width 为 0。
|
||||||
|
成功的 adapter projection 必须精确,不允许把声明宽度降成 boolean 或字符列。当前
|
||||||
|
Emacs 文本 adapter 可精确表达任意左右像素宽度和 0/1px 上下线;更厚的上下边框在
|
||||||
|
publication 前明确报告 capability error 并保留旧 generation,不静默近似。Core
|
||||||
|
仍保留完整四边 width/style/color fact,后续 pixel-coordinate adapter 可扩大成功域。
|
||||||
|
|
||||||
`:grid-column-span`/`:grid-row-span` 同样不是 alias;它们只接受正整数并分别展开
|
`:grid-column-span`/`:grid-row-span` 同样不是 alias;它们只接受正整数并分别展开
|
||||||
为对应 Grid shorthand 的 `span N` value,与 `:grid-column`/`:grid-row` 在同一声明
|
为对应 Grid shorthand 的 `span N` value,与 `:grid-column`/`:grid-row` 在同一声明
|
||||||
混写时报错。`:font-slant` 只有在值 grammar 与 `:font-style` 完全相同时才作为 exact
|
混写时报错。`:font-slant` 只有在值 grammar 与 `:font-style` 完全相同时才作为 exact
|
||||||
|
|||||||
@ -27,7 +27,7 @@ Properties come first and children come last. `etaf-view` receives an unquoted s
|
|||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text :color "#687386" "Welcome to ETAF")))
|
(text :color "#687386" "Welcome to ETAF")))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ Mount it into an Emacs buffer:
|
|||||||
"*etaf-hello*"
|
"*etaf-hello*"
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text "This is a text application."))))
|
(text "This is a text application."))))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ that viewport directly instead of immediately rerendering:
|
|||||||
```elisp
|
```elisp
|
||||||
(ebox-render
|
(ebox-render
|
||||||
(etaf-render
|
(etaf-render
|
||||||
(etaf-view (text :face 'bold "Pure View"))))
|
(etaf-view (text :font-weight 'bold "Pure View"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `etaf-mount` whenever a View contains a stateful Component, reactive data, events, or lifecycle.
|
Use `etaf-mount` whenever a View contains a stateful Component, reactive data, events, or lifecycle.
|
||||||
@ -78,8 +78,8 @@ Attribute values are ordinary Elisp expressions. They do not need an extra `expr
|
|||||||
(label "Theme"))
|
(label "Theme"))
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text
|
(text
|
||||||
:face (if dark 'light 'dark)
|
:color (if dark "#F4F6FB" "#1F2328")
|
||||||
:color "#F4F6FB"
|
:background-color "#20242B"
|
||||||
(expr :value label))))
|
(expr :value label))))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ The child region is structural. `expr` is the one explicit bridge for ordinary E
|
|||||||
(expr
|
(expr
|
||||||
:value
|
:value
|
||||||
(when open
|
(when open
|
||||||
(etaf-view (text :face 'italic "Details"))))))
|
(etaf-view (text :font-style 'italic "Details"))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
`expr` accepts exactly `:value` and no children. Its result can be a string, View, sequence, or `nil`. `if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp inside the value.
|
`expr` accepts exactly `:value` and no children. Its result can be a string, View, sequence, or `nil`. `if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp inside the value.
|
||||||
@ -103,7 +103,7 @@ Quote has one ordinary Elisp meaning:
|
|||||||
- Quote literal symbols and data lists when Elisp requires data.
|
- Quote literal symbols and data lists when Elisp requires data.
|
||||||
- Use `(etaf-view ...)` inside an expression when the expression must construct a View.
|
- Use `(etaf-view ...)` inside an expression when the expression must construct a View.
|
||||||
|
|
||||||
For example, `'bold` is the face symbol, while `'(text "data")` is only data and will not render. A dynamic View must be written as `(etaf-view (text "data"))`.
|
For example, `'bold` is the `:font-weight` symbol, while `'(text "data")` is only data and will not render. A dynamic View must be written as `(etaf-view (text "data"))`.
|
||||||
|
|
||||||
The core `grid` Host is the two-dimensional layout choice:
|
The core `grid` Host is the two-dimensional layout choice:
|
||||||
|
|
||||||
@ -113,16 +113,16 @@ The core `grid` Host is the two-dimensional layout choice:
|
|||||||
(etaf-view
|
(etaf-view
|
||||||
(grid
|
(grid
|
||||||
:width '(640)
|
:width '(640)
|
||||||
:grid-template-columns '((200) 1fr)
|
:grid-template-columns '((200) (fr 1))
|
||||||
:grid-template-rows '(1 1)
|
:grid-template-rows '(1 1)
|
||||||
:gap '(1 (12))
|
:gap '(1 (12))
|
||||||
(text :face 'bold "Name")
|
(text :font-weight 'bold "Name")
|
||||||
(text "Value")
|
(text "Value")
|
||||||
(text "Ada")
|
(text "Ada")
|
||||||
(text "Lovelace"))))
|
(text "Lovelace"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `auto`, `(fr FACTOR)`, symbols such as `1fr`, `(minmax MIN MAX)`, and `(repeat COUNT TRACK)` in track templates. `:grid-auto-columns` and `:grid-auto-rows` size implicit tracks; `:grid-auto-flow` accepts `row` or `column`. Children may use `:grid-column`, `:grid-row`, `:grid-column-span`, and `:grid-row-span`; Ebox performs measurement, placement, and item/content alignment. The optional native backend falls back to the Elisp Ebox renderer for Grid trees.
|
Use `auto`, `min-content`, `max-content`, `(fr FACTOR)`, `(minmax MIN MAX)`, and `(repeat COUNT TRACK-LIST)` in track templates. `:grid-auto-columns` and `:grid-auto-rows` size implicit tracks; `:grid-auto-flow` accepts `row` or `column`. Children may use `:grid-column`, `:grid-row`, `:grid-column-span`, and `:grid-row-span`; Ebox performs measurement, placement, and item/content alignment. The optional native backend falls back to the Elisp Ebox renderer for Grid trees.
|
||||||
|
|
||||||
## 4. Define a Component
|
## 4. Define a Component
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ The beginner form is a stateless `:view` Component:
|
|||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text
|
(text
|
||||||
:face 'bold
|
:font-weight 'bold
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
|
|
||||||
(etaf-mount
|
(etaf-mount
|
||||||
@ -176,7 +176,7 @@ Use `:setup` when the Component owns local state:
|
|||||||
(lambda ()
|
(lambda ()
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold (expr :value title))
|
(text :font-weight 'bold (expr :value title))
|
||||||
(text (expr :value (format "Count: %d" (etaf-value count))))
|
(text (expr :value (format "Count: %d" (etaf-value count))))
|
||||||
(text
|
(text
|
||||||
:role 'button
|
:role 'button
|
||||||
@ -219,8 +219,8 @@ Trailing children are the anonymous/default slot:
|
|||||||
"Render a titled panel."
|
"Render a titled panel."
|
||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold (expr :value title))
|
(text :font-weight 'bold (expr :value title))
|
||||||
(slot (text :face 'shadow "No content"))))
|
(slot (text :color "#687386" "No content"))))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(panel
|
(panel
|
||||||
@ -236,8 +236,8 @@ Named slots use `:name` and must use a stable non-keyword symbol:
|
|||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
(slot :name 'header
|
(slot :name 'header
|
||||||
(text :face 'bold (expr :value title)))
|
(text :font-weight 'bold (expr :value title)))
|
||||||
(slot (text :face 'shadow "No body"))))
|
(slot (text :color "#687386" "No body"))))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(card
|
(card
|
||||||
@ -259,8 +259,8 @@ Static Component styles use one declaration form:
|
|||||||
(styles
|
(styles
|
||||||
("&"
|
("&"
|
||||||
:padding (1 2)
|
:padding (1 2)
|
||||||
:border ((1) solid "#687386"))
|
:border (1 solid "#687386"))
|
||||||
(".title" :face bold)
|
(".title" :font-weight bold)
|
||||||
(".danger" :color "#FF6B6B"))
|
(".danger" :color "#FF6B6B"))
|
||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
@ -559,16 +559,7 @@ Loader errors are stored in `etaf-resource-error`; cleanup/type errors remain vi
|
|||||||
nil))
|
nil))
|
||||||
```
|
```
|
||||||
|
|
||||||
The only low-level escape is `raw-ebox`:
|
The public View grammar accepts Hosts and Components, not raw Ebox Nodes. Framework integrations that need the lower-level port use Ebox's typed TextNode and BoxNode constructors directly; application Views remain on the normal Host and Component lowering path.
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-view
|
|
||||||
(raw-ebox
|
|
||||||
:key 'manual-node
|
|
||||||
:value (ebox-create :content "Backend node")))
|
|
||||||
```
|
|
||||||
|
|
||||||
Use it only when the normal Host and Component lowering path cannot express a real Ebox requirement. The returned Node is opaque to ETAF semantics.
|
|
||||||
|
|
||||||
## 12. Playgrounds
|
## 12. Playgrounds
|
||||||
|
|
||||||
@ -669,7 +660,7 @@ Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`,
|
|||||||
## 15. Common mistakes
|
## 15. Common mistakes
|
||||||
|
|
||||||
- Put every property before the first child.
|
- Put every property before the first child.
|
||||||
- Use `:face 'bold`, not `:face :bold`; a face is an Elisp symbol value, not a property keyword.
|
- Use `:font-weight 'bold`, not `:font-weight :bold`; the weight is an Elisp symbol value, not a property keyword.
|
||||||
- Do not quote a structural View form.
|
- Do not quote a structural View form.
|
||||||
- Use `expr :value` for `if`, `when`, `let`, `mapcar`, or a View returned by ordinary Elisp.
|
- Use `expr :value` for `if`, `when`, `let`, `mapcar`, or a View returned by ordinary Elisp.
|
||||||
- Use `(slot)` or `(slot FALLBACK...)` for the default outlet; use `:name 'header` for named slot content.
|
- Use `(slot)` or `(slot FALLBACK...)` for the default outlet; use `:name 'header` for named slot content.
|
||||||
|
|||||||
@ -27,7 +27,7 @@ ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`
|
|||||||
```elisp
|
```elisp
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text :color "#687386" "Welcome to ETAF")))
|
(text :color "#687386" "Welcome to ETAF")))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`
|
|||||||
"*etaf-hello*"
|
"*etaf-hello*"
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold "Hello")
|
(text :font-weight 'bold "Hello")
|
||||||
(text "This is a text application."))))
|
(text "This is a text application."))))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ Headless host 或已经知道最终布局上下文的调用方,可以通过可
|
|||||||
```elisp
|
```elisp
|
||||||
(ebox-render
|
(ebox-render
|
||||||
(etaf-render
|
(etaf-render
|
||||||
(etaf-view (text :face 'bold "Pure View"))))
|
(etaf-view (text :font-weight 'bold "Pure View"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
View 含有状态型 Component、响应式数据、事件或生命周期时,使用 `etaf-mount`。
|
View 含有状态型 Component、响应式数据、事件或生命周期时,使用 `etaf-mount`。
|
||||||
@ -78,8 +78,8 @@ View 含有状态型 Component、响应式数据、事件或生命周期时,
|
|||||||
(label "Theme"))
|
(label "Theme"))
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text
|
(text
|
||||||
:face (if dark 'light 'dark)
|
:color (if dark "#F4F6FB" "#1F2328")
|
||||||
:color "#F4F6FB"
|
:background-color "#20242B"
|
||||||
(expr :value label))))
|
(expr :value label))))
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ View 含有状态型 Component、响应式数据、事件或生命周期时,
|
|||||||
(expr
|
(expr
|
||||||
:value
|
:value
|
||||||
(when open
|
(when open
|
||||||
(etaf-view (text :face 'italic "Details"))))))
|
(etaf-view (text :font-style 'italic "Details"))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
`expr` 只接受 `:value`,不能有子节点。返回值可以是字符串、View、序列或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和 `cl-loop` 仍然是 value 中的普通 Elisp。
|
`expr` 只接受 `:value`,不能有子节点。返回值可以是字符串、View、序列或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和 `cl-loop` 仍然是 value 中的普通 Elisp。
|
||||||
@ -103,7 +103,7 @@ quote 只有普通 Elisp 的含义:
|
|||||||
- 字面量 symbol 和数据列表在 Elisp 需要时才 quote。
|
- 字面量 symbol 和数据列表在 Elisp 需要时才 quote。
|
||||||
- Elisp 表达式需要构造 View 时,在其中使用 `(etaf-view ...)`。
|
- Elisp 表达式需要构造 View 时,在其中使用 `(etaf-view ...)`。
|
||||||
|
|
||||||
例如 `'bold` 是 face symbol,而 `'(text "data")` 只是数据,不会渲染。动态 View 必须写成 `(etaf-view (text "data"))`。
|
例如 `'bold` 是 `:font-weight` 的 symbol 值,而 `'(text "data")` 只是数据,不会渲染。动态 View 必须写成 `(etaf-view (text "data"))`。
|
||||||
|
|
||||||
核心 `grid` Host 用于二维布局:
|
核心 `grid` Host 用于二维布局:
|
||||||
|
|
||||||
@ -113,16 +113,16 @@ quote 只有普通 Elisp 的含义:
|
|||||||
(etaf-view
|
(etaf-view
|
||||||
(grid
|
(grid
|
||||||
:width '(640)
|
:width '(640)
|
||||||
:grid-template-columns '((200) 1fr)
|
:grid-template-columns '((200) (fr 1))
|
||||||
:grid-template-rows '(1 1)
|
:grid-template-rows '(1 1)
|
||||||
:gap '(1 (12))
|
:gap '(1 (12))
|
||||||
(text :face 'bold "Name")
|
(text :font-weight 'bold "Name")
|
||||||
(text "Value")
|
(text "Value")
|
||||||
(text "Ada")
|
(text "Ada")
|
||||||
(text "Lovelace"))))
|
(text "Lovelace"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
轨道模板支持 `auto`、`(fr FACTOR)`、`1fr` 等符号、`(minmax MIN MAX)` 和 `(repeat COUNT TRACK)`。`:grid-auto-columns` 与 `:grid-auto-rows` 负责隐式轨道尺寸;`:grid-auto-flow` 支持 `row` 或 `column`。子节点可以使用 `:grid-column`、`:grid-row`、`:grid-column-span` 和 `:grid-row-span`;测量、放置以及 item/content 对齐由 Ebox 完成。可选 native backend 不支持 Grid 时会回退到 Elisp Ebox renderer。
|
轨道模板支持 `auto`、`min-content`、`max-content`、`(fr FACTOR)`、`(minmax MIN MAX)` 和 `(repeat COUNT TRACK-LIST)`。`:grid-auto-columns` 与 `:grid-auto-rows` 负责隐式轨道尺寸;`:grid-auto-flow` 支持 `row` 或 `column`。子节点可以使用 `:grid-column`、`:grid-row`、`:grid-column-span` 和 `:grid-row-span`;测量、放置以及 item/content 对齐由 Ebox 完成。可选 native backend 不支持 Grid 时会回退到 Elisp Ebox renderer。
|
||||||
|
|
||||||
## 4. 定义 Component
|
## 4. 定义 Component
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ quote 只有普通 Elisp 的含义:
|
|||||||
"Render a status label."
|
"Render a status label."
|
||||||
:view
|
:view
|
||||||
(text
|
(text
|
||||||
:face 'bold
|
:font-weight 'bold
|
||||||
(expr :value label)))
|
(expr :value label)))
|
||||||
|
|
||||||
(etaf-mount
|
(etaf-mount
|
||||||
@ -176,7 +176,7 @@ Component 自己拥有状态时使用 `:setup`:
|
|||||||
(lambda ()
|
(lambda ()
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold (expr :value title))
|
(text :font-weight 'bold (expr :value title))
|
||||||
(text (expr :value (format "Count: %d" (etaf-value count))))
|
(text (expr :value (format "Count: %d" (etaf-value count))))
|
||||||
(text
|
(text
|
||||||
:role 'button
|
:role 'button
|
||||||
@ -219,8 +219,8 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
|||||||
"Render a titled panel."
|
"Render a titled panel."
|
||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
(text :face 'bold (expr :value title))
|
(text :font-weight 'bold (expr :value title))
|
||||||
(slot (text :face 'shadow "No content"))))
|
(slot (text :color "#687386" "No content"))))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(panel
|
(panel
|
||||||
@ -236,8 +236,8 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
|||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
(slot :name 'header
|
(slot :name 'header
|
||||||
(text :face 'bold (expr :value title)))
|
(text :font-weight 'bold (expr :value title)))
|
||||||
(slot (text :face 'shadow "No body"))))
|
(slot (text :color "#687386" "No body"))))
|
||||||
|
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(card
|
(card
|
||||||
@ -259,8 +259,8 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
|||||||
(styles
|
(styles
|
||||||
("&"
|
("&"
|
||||||
:padding (1 2)
|
:padding (1 2)
|
||||||
:border ((1) solid "#687386"))
|
:border (1 solid "#687386"))
|
||||||
(".title" :face bold)
|
(".title" :font-weight bold)
|
||||||
(".danger" :color "#FF6B6B"))
|
(".danger" :color "#FF6B6B"))
|
||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
@ -553,16 +553,7 @@ Loader 错误会保存在 `etaf-resource-error`;cleanup/type 错误保持可
|
|||||||
nil))
|
nil))
|
||||||
```
|
```
|
||||||
|
|
||||||
唯一的低层出口是 `raw-ebox`:
|
公共 View 语法只接受 Host 和 Component,不接受裸 Ebox Node。需要更低层端口的框架集成直接使用 Ebox typed TextNode 和 BoxNode constructor;应用 View 始终走普通 Host/Component lowering 路径。
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-view
|
|
||||||
(raw-ebox
|
|
||||||
:key 'manual-node
|
|
||||||
:value (ebox-create :content "Backend node")))
|
|
||||||
```
|
|
||||||
|
|
||||||
只有普通 Host 和 Component lowering 无法表达真实 Ebox 需求时才使用它;返回的 Node 对 ETAF 语义保持 opaque。
|
|
||||||
|
|
||||||
## 12. Playgrounds
|
## 12. Playgrounds
|
||||||
|
|
||||||
@ -657,7 +648,7 @@ observer,不修改任何函数。
|
|||||||
## 15. 常见错误
|
## 15. 常见错误
|
||||||
|
|
||||||
- 所有属性必须放在第一个子节点之前。
|
- 所有属性必须放在第一个子节点之前。
|
||||||
- 使用 `:face 'bold`,不要使用 `:face :bold`;face 是 Elisp symbol 值,不是属性 keyword。
|
- 使用 `:font-weight 'bold`,不要使用 `:font-weight :bold`;weight 是 Elisp symbol 值,不是属性 keyword。
|
||||||
- 结构性 View form 不要 quote。
|
- 结构性 View form 不要 quote。
|
||||||
- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 View 时,使用 `expr :value`。
|
- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 View 时,使用 `expr :value`。
|
||||||
- 默认 outlet 使用 `(slot)` 或 `(slot FALLBACK...)`;命名内容使用 `:name 'header`。
|
- 默认 outlet 使用 `(slot)` 或 `(slot FALLBACK...)`;命名内容使用 `:name 'header`。
|
||||||
|
|||||||
163
etaf-renderer.el
163
etaf-renderer.el
@ -12,10 +12,12 @@
|
|||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
|
(require 'subr-x)
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
(require 'etaf-view)
|
(require 'etaf-view)
|
||||||
(require 'etaf-component)
|
(require 'etaf-component)
|
||||||
(require 'etaf-context)
|
(require 'etaf-context)
|
||||||
|
(require 'etaf-behavior)
|
||||||
|
|
||||||
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
|
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
|
||||||
|
|
||||||
@ -52,11 +54,68 @@
|
|||||||
(define-error 'etaf-renderer-error "ETAF rendering error")
|
(define-error 'etaf-renderer-error "ETAF rendering error")
|
||||||
|
|
||||||
(defconst etaf--semantic-props
|
(defconst etaf--semantic-props
|
||||||
'(:class :id :role :disabled :tab-index :ref :use :aria-label
|
'(:class :id :role :disabled :tab-index :ref :use)
|
||||||
:aria-description :on-press :on-key-down :on-mouse-down :on-mouse-drag
|
|
||||||
:on-wheel :on-input :on-focus :on-blur)
|
|
||||||
"ETAF semantic properties that are not Ebox box properties.")
|
"ETAF semantic properties that are not Ebox box properties.")
|
||||||
|
|
||||||
|
(defun etaf--aria-property-p (property)
|
||||||
|
"Return non-nil when PROPERTY is in the ETAF aria namespace."
|
||||||
|
(and (keywordp property)
|
||||||
|
(string-prefix-p ":aria-" (symbol-name property))))
|
||||||
|
|
||||||
|
(defun etaf--semantic-property-p (property)
|
||||||
|
"Return non-nil when PROPERTY belongs to ETAF Runtime metadata."
|
||||||
|
(or (memq property etaf--semantic-props)
|
||||||
|
(etaf--aria-property-p property)
|
||||||
|
(etaf--event-property-p property)))
|
||||||
|
|
||||||
|
(defun etaf--behavior-source-p (value)
|
||||||
|
"Return non-nil when VALUE has the public `:use' source grammar."
|
||||||
|
(or (null value)
|
||||||
|
(symbolp value)
|
||||||
|
(etaf-behavior-spec-p value)
|
||||||
|
(and (proper-list-p value)
|
||||||
|
(cl-every (lambda (entry)
|
||||||
|
(or (symbolp entry) (etaf-behavior-spec-p entry)))
|
||||||
|
value))))
|
||||||
|
|
||||||
|
(defun etaf--validate-semantic-properties (props)
|
||||||
|
"Return PROPS after validating ETAF Runtime-owned metadata values."
|
||||||
|
(cl-loop for (property value) on props by #'cddr
|
||||||
|
do
|
||||||
|
(cond
|
||||||
|
((eq property :ref)
|
||||||
|
(unless (or (null value) (symbolp value) (functionp value))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list ":ref must be a symbol, function, or nil"))))
|
||||||
|
((eq property :role)
|
||||||
|
(unless (or (null value) (symbolp value) (stringp value))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list ":role must be a symbol, string, or nil"))))
|
||||||
|
((etaf--aria-property-p property)
|
||||||
|
(unless (or (null value) (stringp value) (numberp value)
|
||||||
|
(symbolp value))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list (format "%S has an invalid semantic value"
|
||||||
|
property)))))
|
||||||
|
((etaf--event-property-p property)
|
||||||
|
(unless (or (null value) (functionp value))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list (format "%S must be a function or nil"
|
||||||
|
property)))))
|
||||||
|
((eq property :disabled)
|
||||||
|
(unless (memq value '(nil t))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list ":disabled must be boolean"))))
|
||||||
|
((eq property :tab-index)
|
||||||
|
(unless (or (null value) (integerp value))
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list ":tab-index must be an integer or nil"))))
|
||||||
|
((eq property :use)
|
||||||
|
(unless (etaf--behavior-source-p value)
|
||||||
|
(signal 'etaf-renderer-error
|
||||||
|
(list ":use must be a Behavior symbol, spec, or proper list"))))))
|
||||||
|
props)
|
||||||
|
|
||||||
(defvar etaf--render-runtime nil
|
(defvar etaf--render-runtime nil
|
||||||
"Runtime currently lowering a View tree, or nil for a pure render.")
|
"Runtime currently lowering a View tree, or nil for a pure render.")
|
||||||
|
|
||||||
@ -263,63 +322,22 @@ tokens, not Ebox Host properties, and therefore are not materialized here."
|
|||||||
|
|
||||||
(defun etaf--theme-ebox-property-p (key)
|
(defun etaf--theme-ebox-property-p (key)
|
||||||
"Return non-nil when Theme KEY is a known Ebox property.
|
"Return non-nil when Theme KEY is a known Ebox property.
|
||||||
The public `ebox-property-rules' table is the shared property vocabulary;
|
The immutable Ebox schema registry is the sole shared property vocabulary;
|
||||||
custom Theme tokens remain Context data and are never copied onto Hosts."
|
custom Theme tokens remain Context data and are never copied onto Hosts."
|
||||||
(and (keywordp key)
|
(and (keywordp key)
|
||||||
(boundp 'ebox-property-rules)
|
(ebox-style-schema-id key)))
|
||||||
(assq key ebox-property-rules)))
|
|
||||||
|
|
||||||
(defun etaf--inline-text-surface-properties (props)
|
|
||||||
"Return text properties represented by inline text PROPS."
|
|
||||||
(let ((surface (copy-sequence (or (plist-get props :surface-properties) nil))))
|
|
||||||
(when (plist-member props :face)
|
|
||||||
(setq surface (append surface (list 'face (plist-get props :face)))))
|
|
||||||
(when (plist-member props :color)
|
|
||||||
(setq surface (append surface
|
|
||||||
(list 'foreground (plist-get props :color)))))
|
|
||||||
(when (plist-member props :bgcolor)
|
|
||||||
(setq surface (append surface
|
|
||||||
(list 'background (plist-get props :bgcolor)))))
|
|
||||||
surface))
|
|
||||||
|
|
||||||
(defun etaf--apply-inline-surface-properties (content surface)
|
|
||||||
"Return CONTENT with resolved inline SURFACE properties applied."
|
|
||||||
(let ((result (copy-sequence content)))
|
|
||||||
(when (and surface (not (zerop (length result))))
|
|
||||||
(add-text-properties 0 (length result) surface result))
|
|
||||||
result))
|
|
||||||
|
|
||||||
(defun etaf--inline-text-content (value)
|
(defun etaf--inline-text-content (value)
|
||||||
"Return `(SUCCESS . STRING)' for inline-compatible text VALUE.
|
"Return `(SUCCESS . STRING)' for inline-compatible text VALUE.
|
||||||
|
|
||||||
Nested `text' Hosts become propertized runs in one Ebox content surface.
|
Only strings and `expr' values that resolve to strings are compatible. View
|
||||||
Other View Hosts and Components return a failed result so the caller can use
|
structure must use the ordinary typed lowering path."
|
||||||
the ordinary layout lowering path instead."
|
|
||||||
(cond
|
(cond
|
||||||
((stringp value) (cons t value))
|
((stringp value) (cons t value))
|
||||||
((etaf--expr-p value)
|
((etaf--expr-p value)
|
||||||
(etaf--inline-text-content (funcall (etaf--expr-thunk value))))
|
(let ((result (funcall (etaf--expr-thunk value))))
|
||||||
((etaf--view-node-p value)
|
(if (stringp result) (cons t result) (cons nil nil))))
|
||||||
(if (eq (etaf--view-node-name value) 'text)
|
((etaf--view-node-p value) (cons nil nil))
|
||||||
(let ((children (etaf--view-node-children value))
|
|
||||||
(result "")
|
|
||||||
(valid-p t))
|
|
||||||
(dolist (child children)
|
|
||||||
(pcase (etaf--inline-text-content child)
|
|
||||||
(`(t . ,content) (setq result (concat result content)))
|
|
||||||
(_ (setq valid-p nil))))
|
|
||||||
(if valid-p
|
|
||||||
(let ((surface
|
|
||||||
(etaf--inline-text-surface-properties
|
|
||||||
(etaf--resolve-property-plist
|
|
||||||
(etaf--view-node-props value)))))
|
|
||||||
(when surface
|
|
||||||
(setq result (copy-sequence result))
|
|
||||||
(unless (zerop (length result))
|
|
||||||
(add-text-properties 0 (length result) surface result)))
|
|
||||||
(cons t result))
|
|
||||||
(cons nil nil)))
|
|
||||||
(cons nil nil)))
|
|
||||||
((proper-list-p value)
|
((proper-list-p value)
|
||||||
(let ((result "")
|
(let ((result "")
|
||||||
(valid-p t))
|
(valid-p t))
|
||||||
@ -334,10 +352,7 @@ the ordinary layout lowering path instead."
|
|||||||
"Return non-nil when VALUE can be owned by mounted inline effects."
|
"Return non-nil when VALUE can be owned by mounted inline effects."
|
||||||
(cond
|
(cond
|
||||||
((or (null value) (stringp value) (etaf--expr-p value)) t)
|
((or (null value) (stringp value) (etaf--expr-p value)) t)
|
||||||
((etaf--view-node-p value)
|
((etaf--view-node-p value) nil)
|
||||||
(and (eq (etaf--view-node-name value) 'text)
|
|
||||||
(cl-every #'etaf--inline-text-structural-p
|
|
||||||
(etaf--view-node-children value))))
|
|
||||||
((proper-list-p value)
|
((proper-list-p value)
|
||||||
(cl-every #'etaf--inline-text-structural-p value))
|
(cl-every #'etaf--inline-text-structural-p value))
|
||||||
(t nil)))
|
(t nil)))
|
||||||
@ -357,6 +372,7 @@ the ordinary layout lowering path instead."
|
|||||||
"Project ETAF PROPS at PATH to canonical Ebox author facts.
|
"Project ETAF PROPS at PATH to canonical Ebox author facts.
|
||||||
SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
||||||
(let* ((source-props (etaf--resolve-property-plist props))
|
(let* ((source-props (etaf--resolve-property-plist props))
|
||||||
|
(_validated (etaf--validate-semantic-properties source-props))
|
||||||
(props (copy-sequence source-props))
|
(props (copy-sequence source-props))
|
||||||
ebox-props)
|
ebox-props)
|
||||||
(while props
|
(while props
|
||||||
@ -370,9 +386,8 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
|||||||
((eq key :content)
|
((eq key :content)
|
||||||
(signal 'etaf-renderer-error
|
(signal 'etaf-renderer-error
|
||||||
(list "Use View children for content, not :content")))
|
(list "Use View children for content, not :content")))
|
||||||
((or (and (memq key etaf--semantic-props)
|
((or (and (etaf--semantic-property-p key)
|
||||||
(not (memq key '(:class :id))))
|
(not (memq key '(:class :id))))
|
||||||
(etaf--event-property-p key)
|
|
||||||
(eq key :styles))
|
(eq key :styles))
|
||||||
nil)
|
nil)
|
||||||
(t
|
(t
|
||||||
@ -405,12 +420,6 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
|||||||
(defconst etaf--ebox-layout-config-cache-limit 128
|
(defconst etaf--ebox-layout-config-cache-limit 128
|
||||||
"Maximum typed LayoutConfig entries retained across renders.")
|
"Maximum typed LayoutConfig entries retained across renders.")
|
||||||
|
|
||||||
(defun etaf--ebox-keep-properties (props names)
|
|
||||||
"Return PROPS whose keys occur in NAMES."
|
|
||||||
(cl-loop for (key value) on props by #'cddr
|
|
||||||
when (memq key names)
|
|
||||||
append (list key value)))
|
|
||||||
|
|
||||||
(defun etaf--ebox-remove-properties (props names)
|
(defun etaf--ebox-remove-properties (props names)
|
||||||
"Return PROPS without keys in NAMES."
|
"Return PROPS without keys in NAMES."
|
||||||
(cl-loop for (key value) on props by #'cddr
|
(cl-loop for (key value) on props by #'cddr
|
||||||
@ -420,12 +429,9 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
|||||||
(defun etaf--ebox-declarations (tag props)
|
(defun etaf--ebox-declarations (tag props)
|
||||||
"Compile TAG style declarations from canonical ETAF PROPS once."
|
"Compile TAG style declarations from canonical ETAF PROPS once."
|
||||||
(let* ((style-props
|
(let* ((style-props
|
||||||
(etaf--ebox-remove-properties
|
(etaf--ebox-remove-properties
|
||||||
props
|
props
|
||||||
(append etaf--ebox-source-fields
|
etaf--ebox-source-fields))
|
||||||
(unless (eq tag 'text) '(:outer))
|
|
||||||
(when (memq tag '(row column))
|
|
||||||
'(:item-gap :cross-align)))))
|
|
||||||
(key (cons tag style-props))
|
(key (cons tag style-props))
|
||||||
(missing (make-symbol "etaf-ebox-declarations-missing"))
|
(missing (make-symbol "etaf-ebox-declarations-missing"))
|
||||||
(cached (gethash key etaf--ebox-declaration-cache missing)))
|
(cached (gethash key etaf--ebox-declaration-cache missing)))
|
||||||
@ -438,16 +444,14 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
|||||||
(puthash (copy-tree key) compiled etaf--ebox-declaration-cache)
|
(puthash (copy-tree key) compiled etaf--ebox-declaration-cache)
|
||||||
compiled))))
|
compiled))))
|
||||||
|
|
||||||
(defun etaf--ebox-layout-config (tag props declarations)
|
(defun etaf--ebox-layout-config (tag declarations)
|
||||||
"Return TAG LayoutConfig from PROPS and normalized DECLARATIONS."
|
"Return TAG LayoutConfig from normalized DECLARATIONS."
|
||||||
(let* ((layout-props
|
(let* ((layout-props
|
||||||
(if (memq tag '(row column))
|
(ebox-style-declaration-properties
|
||||||
(etaf--ebox-keep-properties props '(:item-gap :cross-align))
|
declarations
|
||||||
(ebox-style-declaration-properties
|
(lambda (property)
|
||||||
declarations
|
(memq (plist-get property :name)
|
||||||
(lambda (property)
|
(ebox-layout-config-property-names tag)))))
|
||||||
(memq (plist-get property :name)
|
|
||||||
(ebox-layout-config-property-names tag))))))
|
|
||||||
(key (cons tag layout-props))
|
(key (cons tag layout-props))
|
||||||
(config (gethash key etaf--ebox-layout-config-cache)))
|
(config (gethash key etaf--ebox-layout-config-cache)))
|
||||||
(or config
|
(or config
|
||||||
@ -488,7 +492,7 @@ SITE-TOKEN supplies the stable generated Host identity when non-nil."
|
|||||||
(signal 'etaf-renderer-error
|
(signal 'etaf-renderer-error
|
||||||
(list "ETAF Box lowering requires one source builder")))
|
(list "ETAF Box lowering requires one source builder")))
|
||||||
(let* ((declarations (etaf--ebox-declarations tag props))
|
(let* ((declarations (etaf--ebox-declarations tag props))
|
||||||
(layout (etaf--ebox-layout-config tag props declarations))
|
(layout (etaf--ebox-layout-config tag declarations))
|
||||||
(outer (if (plist-member props :outer)
|
(outer (if (plist-member props :outer)
|
||||||
(plist-get props :outer)
|
(plist-get props :outer)
|
||||||
'block))
|
'block))
|
||||||
@ -703,8 +707,7 @@ multi-root forest; a single material root is returned unchanged."
|
|||||||
(etaf--inline-text-structural-p children)
|
(etaf--inline-text-structural-p children)
|
||||||
(etaf--runtime-render-inline-content
|
(etaf--runtime-render-inline-content
|
||||||
etaf--render-runtime semantic-id children child-path)))
|
etaf--render-runtime semantic-id children child-path)))
|
||||||
(flat (unless runtime-inline
|
(flat (unless runtime-inline children))
|
||||||
(cl-mapcan #'etaf--flatten-view-value children)))
|
|
||||||
(inline (if runtime-inline
|
(inline (if runtime-inline
|
||||||
(cons t (car runtime-inline))
|
(cons t (car runtime-inline))
|
||||||
(etaf--inline-text-content flat))))
|
(etaf--inline-text-content flat))))
|
||||||
|
|||||||
114
etaf-runtime.el
114
etaf-runtime.el
@ -32,10 +32,6 @@
|
|||||||
"etaf-renderer" (nodes source-identity))
|
"etaf-renderer" (nodes source-identity))
|
||||||
(declare-function etaf--ebox-import-input "etaf-renderer" (input))
|
(declare-function etaf--ebox-import-input "etaf-renderer" (input))
|
||||||
(declare-function etaf--ebox-input-for-nodes "etaf-renderer" (nodes))
|
(declare-function etaf--ebox-input-for-nodes "etaf-renderer" (nodes))
|
||||||
(declare-function etaf--inline-text-surface-properties
|
|
||||||
"etaf-renderer" (props))
|
|
||||||
(declare-function etaf--apply-inline-surface-properties
|
|
||||||
"etaf-renderer" (content surface))
|
|
||||||
(declare-function etaf-events-enable-input "etaf-events" (buffer))
|
(declare-function etaf-events-enable-input "etaf-events" (buffer))
|
||||||
(declare-function etaf-events-disable-input "etaf-events" (buffer))
|
(declare-function etaf-events-disable-input "etaf-events" (buffer))
|
||||||
(declare-function ebox-range-ref-present-p "ebox" (buffer-or-name range-ref))
|
(declare-function ebox-range-ref-present-p "ebox" (buffer-or-name range-ref))
|
||||||
@ -365,13 +361,16 @@ the sequential `etaf--pvec-put' contract."
|
|||||||
|
|
||||||
(defun etaf--theme-border-paint-parts (value)
|
(defun etaf--theme-border-paint-parts (value)
|
||||||
"Return `(SHAPE COLOR)' for a stable color-only Ebox border VALUE."
|
"Return `(SHAPE COLOR)' for a stable color-only Ebox border VALUE."
|
||||||
(cond
|
(condition-case nil
|
||||||
((stringp value) (list 'implicit value))
|
(let* ((declarations
|
||||||
((and (proper-list-p value) (= (length value) 3)
|
(ebox-style-compile-declarations (list :border value) t))
|
||||||
(memq (nth 1 value)
|
(width (plist-get declarations 'ebox/border-top-width))
|
||||||
'(none hidden dotted dashed solid double groove ridge
|
(style (plist-get declarations 'ebox/border-top-style))
|
||||||
inset outset)))
|
(color (plist-get declarations 'ebox/border-top-color)))
|
||||||
(list (list (copy-tree (nth 0 value)) (nth 1 value)) (nth 2 value)))))
|
(if (stringp value)
|
||||||
|
(list 'implicit color)
|
||||||
|
(list (list width style) color)))
|
||||||
|
(error nil)))
|
||||||
|
|
||||||
(defun etaf--runtime-theme-paint-value
|
(defun etaf--runtime-theme-paint-value
|
||||||
(runtime property token source resolved)
|
(runtime property token source resolved)
|
||||||
@ -1435,7 +1434,7 @@ OLD-SEMANTIC supplies the already-read committed Host, when any."
|
|||||||
(while tail
|
(while tail
|
||||||
(let ((key (pop tail))
|
(let ((key (pop tail))
|
||||||
(value (pop tail)))
|
(value (pop tail)))
|
||||||
(when (and value (memq key etaf--semantic-props))
|
(when (and value (etaf--semantic-property-p key))
|
||||||
(setq semantic-p t))
|
(setq semantic-p t))
|
||||||
(when (and (keywordp key)
|
(when (and (keywordp key)
|
||||||
(string-prefix-p ":on-" (symbol-name key))
|
(string-prefix-p ":on-" (symbol-name key))
|
||||||
@ -1446,7 +1445,7 @@ OLD-SEMANTIC supplies the already-read committed Host, when any."
|
|||||||
(while old-props
|
(while old-props
|
||||||
(let ((key (pop old-props))
|
(let ((key (pop old-props))
|
||||||
(value (pop old-props)))
|
(value (pop old-props)))
|
||||||
(when (and value (memq key etaf--semantic-props))
|
(when (and value (etaf--semantic-property-p key))
|
||||||
(setq old-semantic-p t))
|
(setq old-semantic-p t))
|
||||||
(when (and (keywordp key)
|
(when (and (keywordp key)
|
||||||
(string-prefix-p ":on-" (symbol-name key))
|
(string-prefix-p ":on-" (symbol-name key))
|
||||||
@ -1496,8 +1495,7 @@ OLD-SEMANTIC supplies the already-read committed Host, when any."
|
|||||||
(defun etaf--runtime-host-property-effect-p (property)
|
(defun etaf--runtime-host-property-effect-p (property)
|
||||||
"Return non-nil when PROPERTY can update without changing Host identity."
|
"Return non-nil when PROPERTY can update without changing Host identity."
|
||||||
(and (keywordp property)
|
(and (keywordp property)
|
||||||
(or (and (boundp 'ebox-property-rules)
|
(or (ebox-style-schema-id property)
|
||||||
(assq property ebox-property-rules))
|
|
||||||
(memq property '(:class)))
|
(memq property '(:class)))
|
||||||
(not (memq property '(:key :ref :use)))
|
(not (memq property '(:key :ref :use)))
|
||||||
(not (string-prefix-p ":on-" (symbol-name property)))))
|
(not (string-prefix-p ":on-" (symbol-name property)))))
|
||||||
@ -1623,17 +1621,32 @@ receive an independent Host effect."
|
|||||||
(defun etaf--runtime-behavior-specs (value)
|
(defun etaf--runtime-behavior-specs (value)
|
||||||
"Normalize `:use' VALUE to a list of Behavior specs."
|
"Normalize `:use' VALUE to a list of Behavior specs."
|
||||||
(setq value (etaf--resolve-property-value value))
|
(setq value (etaf--resolve-property-value value))
|
||||||
(cond
|
(cl-labels
|
||||||
((null value) nil)
|
((resolve
|
||||||
((etaf-behavior-spec-p value) (list value))
|
(entry)
|
||||||
((proper-list-p value)
|
(cond
|
||||||
(unless (cl-every #'etaf-behavior-spec-p value)
|
((etaf-behavior-spec-p entry) entry)
|
||||||
|
((and (symbolp entry) (fboundp entry))
|
||||||
|
(let ((spec (funcall entry)))
|
||||||
|
(unless (etaf-behavior-spec-p spec)
|
||||||
|
(signal 'etaf-behavior-error
|
||||||
|
(list (format "Behavior constructor %S returned %S"
|
||||||
|
entry spec))))
|
||||||
|
spec))
|
||||||
|
((symbolp entry)
|
||||||
|
(signal 'etaf-behavior-error
|
||||||
|
(list (format "Unknown Behavior constructor: %S" entry))))
|
||||||
|
(t
|
||||||
|
(signal 'etaf-behavior-error
|
||||||
|
(list (format "Invalid :use entry: %S" entry)))))))
|
||||||
|
(cond
|
||||||
|
((null value) nil)
|
||||||
|
((or (symbolp value) (etaf-behavior-spec-p value))
|
||||||
|
(list (resolve value)))
|
||||||
|
((proper-list-p value) (mapcar #'resolve value))
|
||||||
|
(t
|
||||||
(signal 'etaf-behavior-error
|
(signal 'etaf-behavior-error
|
||||||
(list "Each :use entry must be a Behavior spec")))
|
(list ":use must be a Behavior symbol, spec, or proper list"))))))
|
||||||
value)
|
|
||||||
(t
|
|
||||||
(signal 'etaf-behavior-error
|
|
||||||
(list ":use must evaluate to a Behavior spec or list")))))
|
|
||||||
|
|
||||||
(defun etaf--runtime-target-value-equal-p (left right)
|
(defun etaf--runtime-target-value-equal-p (left right)
|
||||||
"Compare LEFT and RIGHT with reactive/function identity rules."
|
"Compare LEFT and RIGHT with reactive/function identity rules."
|
||||||
@ -3130,33 +3143,14 @@ generated descendant Host reference."
|
|||||||
(nreverse result)))
|
(nreverse result)))
|
||||||
|
|
||||||
(defun etaf--runtime-inline-value-string (value surface)
|
(defun etaf--runtime-inline-value-string (value surface)
|
||||||
"Resolve inline VALUE to one string under inherited SURFACE properties."
|
"Return inline VALUE when it is a string.
|
||||||
(cond
|
SURFACE is retained in the Range record for ABI stability but never applies
|
||||||
((null value) "")
|
raw Emacs properties; Text presentation is projected by Ebox."
|
||||||
((stringp value)
|
(ignore surface)
|
||||||
(etaf--apply-inline-surface-properties value surface))
|
(if (stringp value)
|
||||||
((etaf--expr-p value)
|
value
|
||||||
(etaf--runtime-inline-value-string
|
(signal 'etaf-runtime-error
|
||||||
(funcall (etaf--expr-thunk value)) surface))
|
(list "Text expr must resolve to one string"))))
|
||||||
((etaf--view-node-p value)
|
|
||||||
(unless (eq (etaf--view-node-name value) 'text)
|
|
||||||
(signal 'etaf-runtime-error
|
|
||||||
(list "Inline expr requires Step4b non-text output")))
|
|
||||||
(let* ((props (etaf--resolve-property-plist
|
|
||||||
(etaf--view-node-props value)))
|
|
||||||
(inner (etaf--inline-text-surface-properties props))
|
|
||||||
(content
|
|
||||||
(mapconcat
|
|
||||||
(lambda (child)
|
|
||||||
(etaf--runtime-inline-value-string child inner))
|
|
||||||
(etaf--view-node-children value) "")))
|
|
||||||
(etaf--apply-inline-surface-properties content surface)))
|
|
||||||
((proper-list-p value)
|
|
||||||
(mapconcat (lambda (item)
|
|
||||||
(etaf--runtime-inline-value-string item surface))
|
|
||||||
value ""))
|
|
||||||
(t (signal 'etaf-runtime-error
|
|
||||||
(list "Inline expr requires Step4b output")))))
|
|
||||||
|
|
||||||
(defun etaf--runtime-render-inline-range
|
(defun etaf--runtime-render-inline-range
|
||||||
(runtime host-id expr path surface)
|
(runtime host-id expr path surface)
|
||||||
@ -3245,24 +3239,14 @@ generated descendant Host reference."
|
|||||||
(cond
|
(cond
|
||||||
((null value) nil)
|
((null value) nil)
|
||||||
((stringp value)
|
((stringp value)
|
||||||
(let ((text (etaf--apply-inline-surface-properties value inherited)))
|
(push value parts) (push value strings))
|
||||||
(push text parts) (push text strings)))
|
|
||||||
((etaf--expr-p value)
|
((etaf--expr-p value)
|
||||||
(let ((entry (etaf--runtime-render-inline-range
|
(let ((entry (etaf--runtime-render-inline-range
|
||||||
runtime host-id value current-path inherited)))
|
runtime host-id value current-path inherited)))
|
||||||
(push (car entry) parts) (push (cdr entry) strings)))
|
(push (car entry) parts) (push (cdr entry) strings)))
|
||||||
((etaf--view-node-p value)
|
((etaf--view-node-p value)
|
||||||
(unless (eq (etaf--view-node-name value) 'text)
|
(signal 'etaf-runtime-error
|
||||||
(signal 'etaf-runtime-error
|
(list "Text payload cannot contain a View node")))
|
||||||
(list "Inline text requires Step4b Host output")))
|
|
||||||
(let* ((props (etaf--resolve-property-plist
|
|
||||||
(etaf--view-node-props value)))
|
|
||||||
(inner (append (etaf--inline-text-surface-properties props)
|
|
||||||
inherited))
|
|
||||||
(index 0))
|
|
||||||
(dolist (child (etaf--view-node-children value))
|
|
||||||
(walk child (append current-path (list index)) inner)
|
|
||||||
(cl-incf index))))
|
|
||||||
((proper-list-p value)
|
((proper-list-p value)
|
||||||
(let ((index 0))
|
(let ((index 0))
|
||||||
(dolist (item value)
|
(dolist (item value)
|
||||||
|
|||||||
@ -28,11 +28,12 @@
|
|||||||
(defun etaf-counter-example--header (title)
|
(defun etaf-counter-example--header (title)
|
||||||
"Return the counter header for TITLE."
|
"Return the counter header for TITLE."
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column :class "hero"
|
(box :class "hero"
|
||||||
(text :class "eyebrow" "BEST PRACTICE / RETAINED STATE")
|
(column
|
||||||
(text :font-weight 'bold (expr :value title))
|
(text :class "eyebrow" "BEST PRACTICE / RETAINED STATE")
|
||||||
(text :color "#66706A"
|
(text :font-weight 'bold (expr :value title))
|
||||||
"State belongs to setup; rendering only reads it."))))
|
(text :color "#66706A"
|
||||||
|
"State belongs to setup; rendering only reads it.")))))
|
||||||
|
|
||||||
(defun etaf-counter-example--metrics (count double status)
|
(defun etaf-counter-example--metrics (count double status)
|
||||||
"Return metric cards for COUNT, DOUBLE, and STATUS."
|
"Return metric cards for COUNT, DOUBLE, and STATUS."
|
||||||
|
|||||||
@ -76,15 +76,16 @@
|
|||||||
(let ((total (etaf-value (etaf-data-total controller)))
|
(let ((total (etaf-value (etaf-data-total controller)))
|
||||||
(selection (etaf-value (etaf-data-selection controller))))
|
(selection (etaf-value (etaf-data-selection controller))))
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column :width '(720) :padding '(1 (18)) :border "#8F432F"
|
(box :width '(720) :padding '(1 (18)) :border "#8F432F"
|
||||||
:bgcolor "#FFFDF8" :text-align 'center
|
:bgcolor "#FFFDF8" :text-align 'center
|
||||||
(text :color "#8F432F" :font-weight 'bold
|
(column
|
||||||
"BEST PRACTICE / DATA OWNERSHIP")
|
(text :color "#8F432F" :font-weight 'bold
|
||||||
(text :font-weight 'bold "Task controller")
|
"BEST PRACTICE / DATA OWNERSHIP")
|
||||||
(text :color "#66706A"
|
(text :font-weight 'bold "Task controller")
|
||||||
(expr :value
|
(text :color "#66706A"
|
||||||
(format "%d records · %d selected" total
|
(expr :value
|
||||||
(length selection))))))))
|
(format "%d records · %d selected" total
|
||||||
|
(length selection)))))))))
|
||||||
|
|
||||||
(defun etaf-data-example--filter-control
|
(defun etaf-data-example--filter-control
|
||||||
(controller label host-ref query border background)
|
(controller label host-ref query border background)
|
||||||
|
|||||||
@ -25,13 +25,14 @@
|
|||||||
(defun etaf-resource-example--header ()
|
(defun etaf-resource-example--header ()
|
||||||
"Return the Resource example header."
|
"Return the Resource example header."
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column :width '(680) :padding '(1 (18)) :border "#8F432F"
|
(box :width '(680) :padding '(1 (18)) :border "#8F432F"
|
||||||
:bgcolor "#FFFDF8" :text-align 'center
|
:bgcolor "#FFFDF8" :text-align 'center
|
||||||
(text :color "#8F432F" :font-weight 'bold
|
(column
|
||||||
"BEST PRACTICE / RESOURCE LIFECYCLE")
|
(text :color "#8F432F" :font-weight 'bold
|
||||||
(text :font-weight 'bold "Service health")
|
"BEST PRACTICE / RESOURCE LIFECYCLE")
|
||||||
(text :color "#66706A"
|
(text :font-weight 'bold "Service health")
|
||||||
"Loader errors become explicit state; cleanup stays scoped."))))
|
(text :color "#66706A"
|
||||||
|
"Loader errors become explicit state; cleanup stays scoped.")))))
|
||||||
|
|
||||||
(defun etaf-resource-example--status (resource cleanup-count)
|
(defun etaf-resource-example--status (resource cleanup-count)
|
||||||
"Return the status card for RESOURCE and CLEANUP-COUNT."
|
"Return the status card for RESOURCE and CLEANUP-COUNT."
|
||||||
@ -41,14 +42,15 @@
|
|||||||
(border (if success "#6D8A73" "#C97252"))
|
(border (if success "#6D8A73" "#C97252"))
|
||||||
(ink (if success "#24422D" "#6B3020")))
|
(ink (if success "#24422D" "#6B3020")))
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(column :width '(680) :padding '(2 (18)) :border border
|
(box :width '(680) :padding '(2 (18)) :border border
|
||||||
:bgcolor surface :color ink :text-align 'center
|
:bgcolor surface :color ink :text-align 'center
|
||||||
(text :font-weight 'bold
|
(column
|
||||||
(expr :value (upcase (symbol-name status))))
|
(text :font-weight 'bold
|
||||||
(text (expr :value (etaf-resource-example--message resource)))
|
(expr :value (upcase (symbol-name status))))
|
||||||
(text :color "#66706A"
|
(text (expr :value (etaf-resource-example--message resource)))
|
||||||
(expr :value
|
(text :color "#66706A"
|
||||||
(format "CLEANUPS %d" (etaf-value cleanup-count))))))))
|
(expr :value
|
||||||
|
(format "CLEANUPS %d" (etaf-value cleanup-count)))))))))
|
||||||
|
|
||||||
(defun etaf-resource-example--actions (resource fail-next)
|
(defun etaf-resource-example--actions (resource fail-next)
|
||||||
"Return action controls for RESOURCE and FAIL-NEXT."
|
"Return action controls for RESOURCE and FAIL-NEXT."
|
||||||
|
|||||||
@ -737,8 +737,8 @@
|
|||||||
:view
|
:view
|
||||||
(column
|
(column
|
||||||
(text (expr :value title))
|
(text (expr :value title))
|
||||||
(slot :name 'header (text :font-weight 'shadow "Default header"))
|
(slot :name 'header (text :color "#777777" "Default header"))
|
||||||
(slot (text :font-weight 'shadow "Default body"))))
|
(slot (text :color "#777777" "Default body"))))
|
||||||
|
|
||||||
(etaf-define-component etaf-test-named-slot-consumer ()
|
(etaf-define-component etaf-test-named-slot-consumer ()
|
||||||
"Render the named header slot for forwarding tests."
|
"Render the named header slot for forwarding tests."
|
||||||
@ -1204,6 +1204,34 @@
|
|||||||
(let ((view (etaf-view (text :font-weight face (expr :value label)))))
|
(let ((view (etaf-view (text :font-weight face (expr :value label)))))
|
||||||
(should (equal "Ready" (etaf-test--render-text view))))))
|
(should (equal "Ready" (etaf-test--render-text view))))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-host-metadata-uses-the-frozen-runtime-grammar ()
|
||||||
|
"Semantic metadata should validate before it reaches Runtime or Ebox."
|
||||||
|
(should
|
||||||
|
(equal "Valid"
|
||||||
|
(etaf-test--render-text
|
||||||
|
(etaf-view
|
||||||
|
(text :ref 'status :role 'status :aria-live 'polite
|
||||||
|
:on-press #'ignore "Valid")))))
|
||||||
|
(dolist (view
|
||||||
|
(list (etaf-view (text :ref 1.5 "bad"))
|
||||||
|
(etaf-view (text :role '(button) "bad"))
|
||||||
|
(etaf-view (text :aria-live [polite] "bad"))
|
||||||
|
(etaf-view (text :on-press "bad" "bad"))
|
||||||
|
(etaf-view (text :use 1 "bad"))
|
||||||
|
(etaf-view (text :disabled 1 "bad"))
|
||||||
|
(etaf-view (text :tab-index 1.5 "bad"))
|
||||||
|
(etaf-view (text :face 'bold "bad"))
|
||||||
|
(etaf-view (text :surface-properties '(help-echo "bad")
|
||||||
|
"bad"))
|
||||||
|
(etaf-view (box (text :face 'bold "bad")))
|
||||||
|
(etaf-view
|
||||||
|
(box (text :surface-properties '(help-echo "bad")
|
||||||
|
"bad")))))
|
||||||
|
(should-error (etaf-test--render-text view)
|
||||||
|
:type 'etaf-renderer-error))
|
||||||
|
(should-error (etaf-view (text :content "bad"))
|
||||||
|
:type 'etaf-view-syntax-error))
|
||||||
|
|
||||||
(ert-deftest etaf-view-expr-evaluates-control-flow ()
|
(ert-deftest etaf-view-expr-evaluates-control-flow ()
|
||||||
"Use the sole child computation bridge for ordinary control flow."
|
"Use the sole child computation bridge for ordinary control flow."
|
||||||
(let ((checked t))
|
(let ((checked t))
|
||||||
@ -1226,13 +1254,16 @@
|
|||||||
(when open
|
(when open
|
||||||
(etaf-view (text "Details")))))))))))
|
(etaf-view (text "Details")))))))))))
|
||||||
|
|
||||||
(ert-deftest etaf-view-expr-can-return-a-sequence ()
|
(ert-deftest etaf-text-expr-requires-one-string ()
|
||||||
"Flatten a sequence returned by `expr' into the surrounding Host."
|
"Reject structural and sequence values at the Text interpolation boundary."
|
||||||
(should
|
(dolist (view
|
||||||
(equal "AB"
|
(list
|
||||||
(etaf-test--render-text
|
(etaf-view (text (expr :value (list "A" "B"))))
|
||||||
(etaf-view
|
(etaf-view
|
||||||
(text (expr :value (list "A" "B"))))))))
|
(text
|
||||||
|
(expr :value (etaf-view (text :face 'bold "B")))))))
|
||||||
|
(should-error (etaf-test--render-text view)
|
||||||
|
:type 'etaf-renderer-error)))
|
||||||
|
|
||||||
(ert-deftest etaf-view-normalizes-box-string-child-to-text ()
|
(ert-deftest etaf-view-normalizes-box-string-child-to-text ()
|
||||||
"Normalize a static Box string child to one Text View."
|
"Normalize a static Box string child to one Text View."
|
||||||
@ -1302,6 +1333,20 @@
|
|||||||
(should (equal (substring-no-properties (ebox-render row)) "AB"))
|
(should (equal (substring-no-properties (ebox-render row)) "AB"))
|
||||||
(should (equal (substring-no-properties (ebox-render column)) "A\nB"))))
|
(should (equal (substring-no-properties (ebox-render column)) "A\nB"))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-box-frame-and-axis-properties-share-the-cascade ()
|
||||||
|
"ETAF inline outer and RowConfig facts should outrank stylesheet rules."
|
||||||
|
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
|
||||||
|
(ebox-style-add-rule ".axis" '(:outer block :item-gap 0))
|
||||||
|
(let* ((input
|
||||||
|
(etaf-render
|
||||||
|
(etaf-view
|
||||||
|
(row :class "axis" :outer 'inline :item-gap 2
|
||||||
|
(text "A") (text "B")))))
|
||||||
|
(root (etaf-test--input-root input))
|
||||||
|
(props (ebox-layout-config-props (ebox-box-node-layout root))))
|
||||||
|
(should (equal (ebox--computed-display root) '(inline row)))
|
||||||
|
(should (= (plist-get props :item-gap) 2)))))
|
||||||
|
|
||||||
(ert-deftest etaf-view-box-rejects-invalid-layout ()
|
(ert-deftest etaf-view-box-rejects-invalid-layout ()
|
||||||
"Reject an invalid canonical Box layout before publication."
|
"Reject an invalid canonical Box layout before publication."
|
||||||
(should-error
|
(should-error
|
||||||
@ -1575,17 +1620,17 @@
|
|||||||
(etaf-theme-token
|
(etaf-theme-token
|
||||||
:ui-border
|
:ui-border
|
||||||
(etaf-theme-token :line "#AAAAAA")
|
(etaf-theme-token :line "#AAAAAA")
|
||||||
(lambda (color) (list (list 1) 'solid color)))))
|
(lambda (color) (list 1 'solid color)))))
|
||||||
(should
|
(should
|
||||||
(equal '((1) solid "#202020")
|
(equal '(1 solid "#202020")
|
||||||
(etaf--theme-token-resolve-from-theme
|
(etaf--theme-token-resolve-from-theme
|
||||||
token '(:ui-border "#202020" :line "#101010"))))
|
token '(:ui-border "#202020" :line "#101010"))))
|
||||||
(should
|
(should
|
||||||
(equal '((1) solid "#101010")
|
(equal '(1 solid "#101010")
|
||||||
(etaf--theme-token-resolve-from-theme
|
(etaf--theme-token-resolve-from-theme
|
||||||
token '(:line "#101010"))))
|
token '(:line "#101010"))))
|
||||||
(should
|
(should
|
||||||
(equal '((1) solid "#AAAAAA")
|
(equal '(1 solid "#AAAAAA")
|
||||||
(etaf--theme-token-resolve-from-theme token nil)))))
|
(etaf--theme-token-resolve-from-theme token nil)))))
|
||||||
|
|
||||||
(ert-deftest etaf-theme-token-updates-host-property-without-component-render ()
|
(ert-deftest etaf-theme-token-updates-host-property-without-component-render ()
|
||||||
@ -1615,6 +1660,15 @@
|
|||||||
(when-let* ((buffer (get-buffer buffer-name)))
|
(when-let* ((buffer (get-buffer buffer-name)))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest etaf-theme-border-reuses-the-ebox-value-grammar ()
|
||||||
|
"Theme border slots must not maintain a second border parser."
|
||||||
|
(should (equal (etaf--theme-border-paint-parts "#334155")
|
||||||
|
'(implicit "#334155")))
|
||||||
|
(should (equal (etaf--theme-border-paint-parts '(2 solid "#334155"))
|
||||||
|
'((2 solid) "#334155")))
|
||||||
|
(should-not (etaf--theme-border-paint-parts '(2 dashed "#334155")))
|
||||||
|
(should-not (etaf--theme-border-paint-parts '((2) solid "#334155"))))
|
||||||
|
|
||||||
(ert-deftest etaf-theme-paint-slot-rolls-back-with-generation-failure ()
|
(ert-deftest etaf-theme-paint-slot-rolls-back-with-generation-failure ()
|
||||||
"A failed generation promotion must restore the committed paint plane."
|
"A failed generation promotion must restore the committed paint plane."
|
||||||
(let ((buffer-name " *etaf-theme-paint-rollback-test*")
|
(let ((buffer-name " *etaf-theme-paint-rollback-test*")
|
||||||
@ -1887,7 +1941,7 @@
|
|||||||
(etaf-view
|
(etaf-view
|
||||||
(text
|
(text
|
||||||
:ref 'interactive
|
:ref 'interactive
|
||||||
:use (list (etaf-test-cleanup-behavior))
|
:use 'etaf-test-cleanup-behavior
|
||||||
:on-press (lambda () (etaf-dispatch 'etaf-test-action))
|
:on-press (lambda () (etaf-dispatch 'etaf-test-action))
|
||||||
"Press")))
|
"Press")))
|
||||||
(let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
(let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user