Implement retained ETAF reactive runtime
This commit is contained in:
parent
30aa141cca
commit
abb947e0bc
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
||||
EMACS ?= emacs
|
||||
LOAD_PATH = -L . -L examples -L ../ebox
|
||||
LOAD_PATH = -L . -L examples -L ../ebox -L ../tp -L ../ecss
|
||||
SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el
|
||||
EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.el
|
||||
TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-examples-tests.el
|
||||
|
||||
@ -109,6 +109,8 @@ Attribute values do not need an `expr` wrapper. `expr` exists only because the c
|
||||
|
||||
It evaluates the expression, then accepts a string, View, sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other Elisp forms remain ordinary Elisp inside `:value`.
|
||||
|
||||
The current core has no file-facing `.etaf` pair loader. `etaf-define-component` is the structure/style/behavior unit: its View defines structure, `:styles` owns presentation rules, and `:setup` owns retained state, events, and lifecycle behavior. A future `.etaf` single-file component format belongs in a compiler layer that lowers into this same public View and Component contract; it is not a second runtime grammar.
|
||||
|
||||
## 4. Components
|
||||
|
||||
The public definition macro has exactly three keywords:
|
||||
@ -127,6 +129,8 @@ The public definition macro has exactly three keywords:
|
||||
|
||||
`:view` and `:setup` are mutually exclusive. `:styles` is optional and may appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection.
|
||||
|
||||
The Component definition is the current structure/style/behavior boundary. Keep dynamic state, Action callbacks, and lifecycle work in `:setup`; keep static presentation in `:styles`. A future `.etaf` SFC compiler may produce these definitions, but the Runtime does not load `.etaf` files directly.
|
||||
|
||||
### 4.1 Stateless and stateful forms
|
||||
|
||||
```elisp
|
||||
@ -342,6 +346,8 @@ Keys are stable ordinary symbols. The nearest ancestor wins; a missing required
|
||||
|
||||
Explicit Host props override Component styles, and Component styles override Theme defaults.
|
||||
|
||||
Style ownership follows the Component that authored each View node. A nested Component is a style boundary: parent rules do not penetrate its internals. Caller-provided slot content is rendered with the caller scope, while a child's own fallback content remains in the child scope.
|
||||
|
||||
### 9.2 Data
|
||||
|
||||
Data is a core ETAF capability, not a second framework package. A Data Source is a small capability plist:
|
||||
@ -417,7 +423,30 @@ etaf-playground → etaf
|
||||
|
||||
Optional storage integrations should use explicit package names such as a concrete SQLite or PostgreSQL source. A generic adapter package would add a name without owning a stable behavior, so it is not part of the public model.
|
||||
|
||||
## 11. Extension rule
|
||||
## 11. Retained publication and fixed-point safety
|
||||
|
||||
Mounted Component, expr, slot, fragment, raw, inline, and Root owners publish
|
||||
through one outer reactive dispatch. A local owner evaluates into a candidate
|
||||
generation and Ebox logical replacement; disjoint owners are coalesced into
|
||||
one TP/Ebox publication. The Root owner is the only complete-root adapter.
|
||||
|
||||
Each Runtime flush records a candidate-aware effect tuple containing the
|
||||
generation id, effect-to-source edges and source versions, plus an immutable
|
||||
semantic-node stamp for candidate input/context/output facts. A repeated tuple
|
||||
reports the ordered effect/edge path of the non-converging graph. The flush
|
||||
bound is derived from candidate nodes, dependency edges, and source entries;
|
||||
it is not a fixed multiplier of an arbitrary threshold. Rollback discards the
|
||||
flush-local stamps, so the same failed state can be retried as a new
|
||||
transaction.
|
||||
|
||||
Behavior installers use target-specific identity: reactive values and
|
||||
functions are compared with `eq`, while scalar attributes use value equality.
|
||||
Each installed Behavior also receives a stable `(mount-epoch resource-id)`
|
||||
address. Generation membership and the Runtime resource registry determine
|
||||
which installer is authoritative; a failed candidate removes only its staged
|
||||
Behavior resource and runs its contained cleanup.
|
||||
|
||||
## 12. Extension rule
|
||||
|
||||
Before adding a new concept, choose the smallest existing owner:
|
||||
|
||||
|
||||
@ -108,6 +108,8 @@ ATTRIBUTE = :KEY VALUE
|
||||
|
||||
它执行表达式,然后接受字符串、View、View 序列或 `nil`。它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、`let`、`mapcar`、`cl-loop` 等 Elisp 形式仍然只是 `:value` 中的普通 Elisp。
|
||||
|
||||
当前 core 没有面向文件的 `.etaf` pair loader。`etaf-define-component` 才是结构/样式/行为单元:View 定义结构,`:styles` 负责 presentation,`:setup` 负责 retained state、事件和生命周期行为。未来的 `.etaf` SFC 属于把结果 lowering 到同一套公共 View/Component 契约的 compiler layer,而不是第二套 Runtime 语法。
|
||||
|
||||
## 4. Component
|
||||
|
||||
公共定义宏只有三个关键字:
|
||||
@ -126,6 +128,8 @@ ATTRIBUTE = :KEY VALUE
|
||||
|
||||
`:view` 和 `:setup` 互斥;`:styles` 可选且最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。
|
||||
|
||||
Component definition 是当前结构/样式/行为边界:动态状态、Action callback 和生命周期工作放进 `:setup`,静态 presentation 放进 `:styles`。未来 `.etaf` SFC compiler 可以生成这些 definition,但 Runtime 不会直接加载 `.etaf` 文件。
|
||||
|
||||
### 4.1 无状态与状态型形式
|
||||
|
||||
```elisp
|
||||
@ -341,6 +345,8 @@ key 使用稳定的普通 symbol。最近的祖先优先,缺失的 required ke
|
||||
|
||||
显式 Host props 覆盖 Component styles,Component styles 覆盖 Theme defaults。
|
||||
|
||||
样式所有权跟随创建每个 View node 的 Component。嵌套 Component 是样式边界:父规则不会穿透其内部。调用者提供的 slot 内容使用 caller scope,子组件自有的 fallback 内容保留在 child scope。
|
||||
|
||||
### 9.2 Data
|
||||
|
||||
Data 是 ETAF core 能力,不是需要用户额外学习的第二套框架。Data Source 是一个小的 capability plist:
|
||||
@ -416,7 +422,27 @@ etaf-playground → etaf
|
||||
|
||||
可选存储集成应使用具体名称,例如 SQLite 或 PostgreSQL source。通用 adapter 包无法拥有稳定的行为,只会增加用户需要记忆的名称,因此不属于公共模型。
|
||||
|
||||
## 11. 拓展规则
|
||||
## 11. 保留式发布与 fixed-point 安全
|
||||
|
||||
mounted Component、expr、slot、fragment、raw、inline 和 Root owner 都经过同一
|
||||
个外层 reactive dispatch。局部 owner 先生成 candidate generation 和 Ebox logical
|
||||
replacement;不相交 owner 会合并为一次 TP/Ebox publication。只有 Root owner
|
||||
可以进入 complete-root adapter。
|
||||
|
||||
每次 Runtime flush 都记录 candidate-aware effect tuple:其中包含 generation id、
|
||||
effect→source 边和 source version,以及 candidate input/context/output facts 的
|
||||
immutable semantic-node stamp。重复 tuple 会报告有序的 effect/edge path;step
|
||||
bound 从 candidate nodes、dependency edges 和 source entries 推导,不再是任意
|
||||
固定倍率阈值。rollback 会丢弃本次 flush 的 stamp,因此同一个失败状态可以作为
|
||||
新的 transaction 重试。
|
||||
|
||||
Behavior installer 使用 target-specific identity:reactive value 和 function 用
|
||||
`eq`,普通 scalar attribute 才使用 value equality。每个已安装 Behavior 都获得
|
||||
稳定的 `(mount-epoch resource-id)` 地址;generation membership 与 Runtime resource
|
||||
registry 决定当前 authority。失败 candidate 只移除 staged Behavior resource,并
|
||||
执行受 containment 保护的 cleanup。
|
||||
|
||||
## 12. 拓展规则
|
||||
|
||||
增加新能力前,优先选择最小的既有 owner:
|
||||
|
||||
|
||||
@ -58,6 +58,8 @@ Mount it into an Emacs buffer:
|
||||
|
||||
Use `etaf-mount` whenever a View contains a stateful Component, reactive data, events, or lifecycle.
|
||||
|
||||
The current core has no direct `.etaf` loader. `etaf-define-component` is the structure/style/behavior unit: use its View for structure, `:styles` for static presentation, and `:setup` for retained state, Actions, and lifecycle. A future `.etaf` SFC belongs to a compiler layer that emits this same Component contract; it is not a second Runtime entry point.
|
||||
|
||||
## 3. Properties and children
|
||||
|
||||
Attribute values are ordinary Elisp expressions. They do not need an extra `expr` wrapper:
|
||||
@ -266,6 +268,14 @@ Precedence is fixed:
|
||||
explicit Host properties > Component :styles > inherited Theme defaults
|
||||
```
|
||||
|
||||
Within one Component style scope, the first matching declaration fills an
|
||||
unspecified Host property; later rules do not overwrite that resolved default.
|
||||
A non-nil Host property remains protected, and a nil Host property is treated
|
||||
as omitted and may receive the Component or Theme default. Use explicit Host
|
||||
props or distinct properties when a variant needs a deliberate override.
|
||||
|
||||
Styles are scoped to the Component that authored a View node. A parent rule does not enter a nested Component's internals; content supplied through a caller slot keeps the caller scope, while a Component's own slot fallback keeps the child scope.
|
||||
|
||||
Theme is a Context convenience, not another runtime object:
|
||||
|
||||
```elisp
|
||||
@ -339,7 +349,7 @@ For a reusable installer, reserve `:install` for the cleanup-producing part of t
|
||||
(message "Behavior removed")))))))
|
||||
```
|
||||
|
||||
The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current.
|
||||
The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current. Behavior equality keeps function and reactive-value identity with `eq`; a newly-created installer closure is therefore a deliberate replacement, not an accidental reuse. The replacement is staged under the mounted resource registry and becomes authoritative only when its generation commits.
|
||||
|
||||
Focus and hit testing are Runtime operations:
|
||||
|
||||
@ -351,6 +361,8 @@ Focus and hit testing are Runtime operations:
|
||||
|
||||
`etaf-host-ref-bounds` and `etaf-host-ref-position` expose the public Ebox hit-test boundary. `etaf-dispatch-event` accepts an optional payload flag when the callback needs one argument.
|
||||
|
||||
Mounted buffers enable `etaf-input-mode` automatically. `TAB` focuses the next Host, `Shift-TAB`/backtab focuses the previous Host, `RET` activates the focused Host, and `mouse-1` activates the Host at the click position. Focus ordering sorts numeric `:tab-index` first and uses live buffer position as the stable tie-breaker; moving focus also moves point to that Host. Unmounting disables the input mode.
|
||||
|
||||
## 9. Context / Provide / Inject
|
||||
|
||||
Use Context for a dependency shared across component depth, not for ordinary props:
|
||||
@ -563,7 +575,7 @@ It depends only on Ebox. Use it when inspecting Ebox boxes and Grid layout; use
|
||||
| Reactive effects | `etaf-watch`, `etaf-watch-effect`, `etaf-effect-scope`, `etaf-scope-run` | Observe state or synchronize external resources |
|
||||
| Lifecycle | `etaf-on-mounted`, `etaf-on-updated`, `etaf-on-unmounted` | Attach Component lifecycle work |
|
||||
| Context | `etaf-provide`, `etaf-inject`, `etaf-theme-provide` | Share ambient dependencies through depth |
|
||||
| Events and focus | `etaf-dispatch-event`, `etaf-activate`, `etaf-focus`, `etaf-focus-next` | Enter interactive Runtime behavior |
|
||||
| Events and focus | `etaf-dispatch-event`, `etaf-activate`, `etaf-focus`, `etaf-focus-next`, `etaf-focus-previous`, `etaf-input-mode` | Enter interactive Runtime behavior |
|
||||
| Actions | `etaf-action-define`, `etaf-dispatch` | Name and reuse business mutations |
|
||||
| Behaviors | `etaf-behavior-create`, `etaf-define-behavior`, `etaf-current-behavior-context`, `etaf-focusable`, `etaf-toggleable` | Reuse non-visual interaction bundles |
|
||||
| Data | `etaf-data-source`, `etaf-data-controller`, `etaf-data-memory-source`, `etaf-data-*` | Query, paginate, mutate, select, and stop data |
|
||||
@ -583,3 +595,9 @@ Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`,
|
||||
- Keep writes out of rendering; use an event, Action, watch callback, or Effect.
|
||||
- Use `etaf-ui` Components for product controls; core Hosts are the structural foundation.
|
||||
- Stop a Data Controller and unmount a Runtime when their owner is no longer needed.
|
||||
|
||||
For retained updates, keep the application pair declarative: `.etaf` contains
|
||||
the static shell and the same-basename `.el` companion owns state, Components,
|
||||
and actions. Reactive writes are batched into one generation publication;
|
||||
failed publication is retryable, and a non-converging effect is reported rather
|
||||
than allowed to keep the UI busy.
|
||||
|
||||
@ -58,6 +58,8 @@ ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`
|
||||
|
||||
View 含有状态型 Component、响应式数据、事件或生命周期时,使用 `etaf-mount`。
|
||||
|
||||
当前 core 不直接加载 `.etaf`。`etaf-define-component` 是结构/样式/行为单元:用 View 定义结构,用 `:styles` 放静态 presentation,用 `:setup` 管理 retained state、Action 和生命周期。未来 `.etaf` SFC 属于把结果编译成同一套 Component 契约的 compiler layer,而不是第二个 Runtime 入口。
|
||||
|
||||
## 3. 属性与子节点
|
||||
|
||||
属性值就是普通 Elisp 表达式,不需要额外的 `expr`:
|
||||
@ -266,6 +268,13 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
||||
显式 Host 属性 > Component :styles > 继承的 Theme 默认值
|
||||
```
|
||||
|
||||
在同一个 Component 样式作用域内,第一条匹配声明会填充尚未指定的 Host
|
||||
属性;后续规则不会覆盖已经解析出的默认值。非 nil 的 Host 属性始终受
|
||||
保护,nil Host 属性按省略处理,可以接收 Component 或 Theme 默认值。若
|
||||
variant 需要明确覆盖,应使用显式 Host 属性或不同的属性键。
|
||||
|
||||
样式作用域归属于创建 View node 的 Component。父组件规则不会进入嵌套 Component 的内部;调用者通过 slot 提供的内容保留 caller scope,Component 自己定义的 slot fallback 保留 child scope。
|
||||
|
||||
Theme 是 Context 的便捷形式,不是另一个 Runtime 对象:
|
||||
|
||||
```elisp
|
||||
@ -339,7 +348,7 @@ Behavior 用来打包可复用的非视觉属性和 cleanup:
|
||||
(message "Behavior removed")))))))
|
||||
```
|
||||
|
||||
Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。
|
||||
Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。Behavior equality 对 function 和 reactive value 使用 `eq`;因此新建的 installer closure 会被视为有意替换,而不是错误复用。替换状态先以 mounted resource registry 的 staged resource 保存,generation commit 后才成为 authority。
|
||||
|
||||
Focus 和 hit testing 是 Runtime 操作:
|
||||
|
||||
@ -351,6 +360,8 @@ Focus 和 hit testing 是 Runtime 操作:
|
||||
|
||||
`etaf-host-ref-bounds` 和 `etaf-host-ref-position` 暴露公共的 Ebox hit-test 边界。`etaf-dispatch-event` 支持可选 payload 标记,用于给 callback 传入一个参数。
|
||||
|
||||
挂载后的 buffer 会自动启用 `etaf-input-mode`:`TAB` 聚焦下一个 Host,`Shift-TAB`/backtab 聚焦上一个 Host,`RET` 激活当前焦点 Host,`mouse-1` 在点击位置激活 Host。焦点顺序先按数字 `:tab-index`,相同 index 再按 live buffer position 稳定排序;移动焦点也会移动 point。卸载时会禁用该输入 mode。
|
||||
|
||||
## 9. Context / Provide / Inject
|
||||
|
||||
Context 适合跨多层共享依赖,不适合普通 label:
|
||||
@ -563,7 +574,7 @@ Loader 错误会保存在 `etaf-resource-error`;cleanup/type 错误保持可
|
||||
| 响应式 effect | `etaf-watch`、`etaf-watch-effect`、`etaf-effect-scope`、`etaf-scope-run` | 观察状态或同步外部资源 |
|
||||
| 生命周期 | `etaf-on-mounted`、`etaf-on-updated`、`etaf-on-unmounted` | 绑定 Component 生命周期工作 |
|
||||
| Context | `etaf-provide`、`etaf-inject`、`etaf-theme-provide` | 跨层级共享依赖 |
|
||||
| 事件与 focus | `etaf-dispatch-event`、`etaf-activate`、`etaf-focus`、`etaf-focus-next` | 进入交互 Runtime |
|
||||
| 事件与 focus | `etaf-dispatch-event`、`etaf-activate`、`etaf-focus`、`etaf-focus-next`、`etaf-focus-previous`、`etaf-input-mode` | 进入交互 Runtime |
|
||||
| Action | `etaf-action-define`、`etaf-dispatch` | 命名和复用业务变更 |
|
||||
| Behavior | `etaf-behavior-create`、`etaf-define-behavior`、`etaf-current-behavior-context`、`etaf-focusable`、`etaf-toggleable` | 复用非视觉交互 bundle |
|
||||
| Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择和停止数据 |
|
||||
@ -583,3 +594,8 @@ Loader 错误会保存在 `etaf-resource-error`;cleanup/type 错误保持可
|
||||
- 不要在 render 中写状态;使用事件、Action、watch callback 或 Effect。
|
||||
- 产品级控件使用 `etaf-ui` Component;core Host 只是结构基础。
|
||||
- owner 不再需要时,停止 Data Controller 并卸载 Runtime。
|
||||
|
||||
对于保留式更新,建议保持 pair 结构:`.etaf` 只放静态 shell,同名 `.el` companion
|
||||
负责 state、Component 和 action。响应式写入会合并为一次 generation publication;
|
||||
发布失败可以在同一旧状态上重试,non-converging effect 会报告错误,不会让界面
|
||||
持续占用事件循环。
|
||||
|
||||
@ -18,6 +18,13 @@
|
||||
"Invalid ETAF Component definition"
|
||||
'etaf-view-error)
|
||||
|
||||
(cl-defstruct (etaf--slot-content (:constructor etaf--slot-content-create))
|
||||
"Internal caller ownership metadata for one slot entry."
|
||||
owner-component-id children)
|
||||
|
||||
(defvar etaf--raw-slot-read-p nil
|
||||
"Set while a Component render uses public raw slot accessors.")
|
||||
|
||||
(defun etaf--component-definition-error (format-string &rest arguments)
|
||||
"Signal a Component definition error from FORMAT-STRING and ARGUMENTS."
|
||||
(signal 'etaf-component-definition-error
|
||||
@ -63,21 +70,23 @@ The function is also useful to code that deliberately avoids that shorthand."
|
||||
|
||||
(defun etaf-current-slots ()
|
||||
"Return the current Component's normalized slot alist."
|
||||
etaf--current-component-slots)
|
||||
(setq etaf--raw-slot-read-p t)
|
||||
(mapcar (lambda (entry)
|
||||
(cons (car entry)
|
||||
(if (etaf--slot-content-p (cdr entry))
|
||||
(etaf--slot-content-children (cdr entry))
|
||||
(cdr entry))))
|
||||
etaf--current-component-slots))
|
||||
|
||||
(defun etaf-current-slot (name &optional fallback)
|
||||
"Return the child list for slot NAME, or FALLBACK when it is absent."
|
||||
(setq etaf--raw-slot-read-p t)
|
||||
(let ((entry (assq name etaf--current-component-slots)))
|
||||
(if entry (cdr entry) fallback)))
|
||||
|
||||
(defun etaf--component-prop-bindings (props)
|
||||
"Return lexical bindings for stateless Component PROPS."
|
||||
(mapcar (lambda (prop)
|
||||
`(,prop
|
||||
(etaf--resolve-property-value
|
||||
(plist-get etaf--component-props
|
||||
,(etaf--component-prop-key prop)))))
|
||||
props))
|
||||
(if entry
|
||||
(if (etaf--slot-content-p (cdr entry))
|
||||
(etaf--slot-content-children (cdr entry))
|
||||
(cdr entry))
|
||||
fallback)))
|
||||
|
||||
(defun etaf--component-prop-symbol-macros (props)
|
||||
"Return symbol macros that read current Component PROPS."
|
||||
@ -183,7 +192,8 @@ forms do not use quote; ordinary Elisp belongs in `expr :value'."
|
||||
(etaf--current-component-slots etaf--component-slots)
|
||||
(etaf--current-component-instance
|
||||
etaf--current-component-instance))
|
||||
(let ,(etaf--component-prop-bindings props)
|
||||
(cl-symbol-macrolet
|
||||
,(etaf--component-prop-symbol-macros props)
|
||||
,(etaf--compile-view-form view-form :projection))))))
|
||||
(setup-lambda
|
||||
(when saw-setup
|
||||
|
||||
@ -18,14 +18,28 @@
|
||||
(:constructor etaf--context-create))
|
||||
"One inherited Context frame owned by a Component instance."
|
||||
parent
|
||||
owner-id
|
||||
(values (make-hash-table :test #'eq)))
|
||||
|
||||
(defvar etaf--current-context nil
|
||||
"Context table of the Component currently being evaluated.")
|
||||
|
||||
(defvar etaf--context-inject-recorder nil
|
||||
"Candidate-local function recording Context provider reads.")
|
||||
|
||||
(defconst etaf--context-missing (make-symbol "etaf-context-missing")
|
||||
"Unique sentinel used to distinguish an absent Context value.")
|
||||
|
||||
(defun etaf-context-copy (context)
|
||||
"Return an immutable candidate copy of CONTEXT's local frame."
|
||||
(when context
|
||||
(let ((values (make-hash-table :test #'eq)))
|
||||
(maphash (lambda (key value) (puthash key value values))
|
||||
(etaf-context-values context))
|
||||
(etaf--context-create :parent (etaf-context-parent context)
|
||||
:owner-id (etaf-context-owner-id context)
|
||||
:values values))))
|
||||
|
||||
(defun etaf--context-key (key)
|
||||
"Validate and normalize Context KEY."
|
||||
(unless (and (symbolp key) (not (keywordp key)) (not (memq key '(nil t))))
|
||||
@ -52,16 +66,21 @@ can be distinguished from a missing dependency."
|
||||
(let ((key (etaf--context-key key))
|
||||
(context etaf--current-context)
|
||||
found
|
||||
provider
|
||||
value)
|
||||
(while (and context (not found))
|
||||
(let ((candidate (gethash key (etaf-context-values context)
|
||||
etaf--context-missing)))
|
||||
(unless (eq candidate etaf--context-missing)
|
||||
(setq value candidate
|
||||
found t)))
|
||||
found t
|
||||
provider context)))
|
||||
(setq context (etaf-context-parent context)))
|
||||
(if found
|
||||
value
|
||||
(progn
|
||||
(when etaf--context-inject-recorder
|
||||
(funcall etaf--context-inject-recorder provider key))
|
||||
value)
|
||||
(if (and required-p-supplied-p required-p)
|
||||
(signal 'etaf-context-error
|
||||
(list (format "Missing ETAF Context dependency: %S" key)))
|
||||
|
||||
238
etaf-events.el
238
etaf-events.el
@ -4,9 +4,9 @@
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Events are Runtime-owned entry points into local `:on-*' callbacks. Ebox
|
||||
;; supplies live Host reference bounds; ETAF decides which callback to invoke
|
||||
;; and keeps focus state separate from the visual tree.
|
||||
;; Events are Runtime-owned entry points into local `:on-*' callbacks. The
|
||||
;; input minor mode is deliberately small: Runtime only toggles it at mount
|
||||
;; boundaries, while this module owns key and mouse command definitions.
|
||||
|
||||
;;; Code:
|
||||
|
||||
@ -20,10 +20,14 @@
|
||||
(declare-function etaf-runtime-p "etaf-runtime" (value))
|
||||
(declare-function etaf-runtime-mounted-p "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-buffer "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-handlers "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-host-props "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-handler-for "etaf-runtime" (runtime host-ref))
|
||||
(declare-function etaf-runtime-handler-entries "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-host-props-for "etaf-runtime" (runtime host-ref))
|
||||
(declare-function etaf-runtime-host-props-entries "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-focus-ref "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-set-focus-ref "etaf-runtime" (runtime host-ref))
|
||||
(declare-function etaf-runtime-event-begin "etaf-runtime" (runtime))
|
||||
(declare-function etaf-runtime-event-end "etaf-runtime" (runtime))
|
||||
|
||||
(define-error 'etaf-event-error "Invalid ETAF Event")
|
||||
|
||||
@ -37,7 +41,7 @@
|
||||
((symbolp kind) (symbol-name kind))
|
||||
((stringp kind) kind)
|
||||
(t (signal 'etaf-event-error
|
||||
(list (format "Invalid event kind: %S" kind)))))))
|
||||
(list (format "Invalid event kind: %S" kind)))))))
|
||||
(setq name (if (string-prefix-p "on-" name)
|
||||
(substring name 3)
|
||||
name))
|
||||
@ -46,7 +50,7 @@
|
||||
(defun etaf--event-handler (runtime host-ref kind)
|
||||
"Return callback for HOST-REF and KIND in RUNTIME, or nil."
|
||||
(cdr (assq (etaf-event-kind kind)
|
||||
(gethash host-ref (etaf-runtime-handlers runtime)))))
|
||||
(etaf-runtime-handler-for runtime host-ref))))
|
||||
|
||||
;;;###autoload
|
||||
(cl-defun etaf-dispatch-event
|
||||
@ -65,9 +69,12 @@ otherwise call the local callback with no arguments."
|
||||
(etaf--current-event
|
||||
(list :runtime runtime :host-ref host-ref
|
||||
:kind (etaf-event-kind kind) :payload payload)))
|
||||
(if payload-p
|
||||
(funcall callback payload)
|
||||
(funcall callback)))))
|
||||
(etaf-runtime-event-begin runtime)
|
||||
(unwind-protect
|
||||
(if payload-p
|
||||
(funcall callback payload)
|
||||
(funcall callback))
|
||||
(etaf-runtime-event-end runtime)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-host-ref-bounds (runtime host-ref)
|
||||
@ -81,71 +88,184 @@ otherwise call the local callback with no arguments."
|
||||
(setq runtime (etaf-runtime-require-mounted runtime))
|
||||
(ebox-host-ref-position (etaf-runtime-buffer runtime) host-ref))
|
||||
|
||||
(defun etaf--position-number (position)
|
||||
"Return integer POSITION, or nil when POSITION is not buffer-local."
|
||||
(cond
|
||||
((markerp position) (marker-position position))
|
||||
((integerp position) position)
|
||||
(t nil)))
|
||||
|
||||
(defun etaf--activation-candidate-before-p (left right)
|
||||
"Return non-nil when activation candidate LEFT precedes RIGHT."
|
||||
(let ((left-length (nth 2 left))
|
||||
(right-length (nth 2 right))
|
||||
(left-start (nth 1 left))
|
||||
(right-start (nth 1 right)))
|
||||
(or (< left-length right-length)
|
||||
(and (= left-length right-length)
|
||||
(or (< left-start right-start)
|
||||
(and (= left-start right-start)
|
||||
(string< (prin1-to-string (car left))
|
||||
(prin1-to-string (car right)))))))))
|
||||
|
||||
(defun etaf--activation-at-position (runtime position &optional quiet)
|
||||
"Activate the smallest enabled Host at POSITION in RUNTIME.
|
||||
|
||||
When QUIET is non-nil, return nil instead of signaling when no callback owns
|
||||
the position."
|
||||
(let (candidates)
|
||||
(dolist (entry (etaf-runtime-handler-entries runtime))
|
||||
(let ((host-ref (car entry)) (handlers (cdr entry)))
|
||||
(let* ((press (assq 'press handlers))
|
||||
(props (etaf-runtime-host-props-for runtime host-ref))
|
||||
(bounds (ebox-host-ref-bounds
|
||||
(etaf-runtime-buffer runtime) host-ref))
|
||||
(start (and bounds (car bounds)))
|
||||
(end (and bounds (cdr bounds))))
|
||||
(when (and press (not (plist-get props :disabled))
|
||||
start end (<= start position) (< position end))
|
||||
(push (list host-ref start (- end start)) candidates)))))
|
||||
(setq candidates (sort candidates #'etaf--activation-candidate-before-p))
|
||||
(if-let ((candidate (car candidates)))
|
||||
(etaf-dispatch-event runtime (car candidate) 'press)
|
||||
(unless quiet
|
||||
(user-error "No interactive ETAF Host at point")))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-activate (&optional runtime)
|
||||
"Dispatch `press' for the smallest active Host containing point.
|
||||
"Dispatch `press' for the smallest enabled Host containing point.
|
||||
|
||||
RUNTIME is the mounted Runtime to activate, or nil for the current Runtime.
|
||||
Return the callback result. This command is intentionally a thin adapter;
|
||||
hit testing remains Ebox's public Host-reference bounds operation."
|
||||
RUNTIME is the mounted Runtime to activate, or nil for the current buffer."
|
||||
(interactive)
|
||||
(setq runtime (etaf-runtime-require-mounted runtime))
|
||||
(let ((position (point)) candidates)
|
||||
(maphash
|
||||
(lambda (host-ref _handlers)
|
||||
(when-let ((bounds (ebox-host-ref-bounds
|
||||
(etaf-runtime-buffer runtime) host-ref)))
|
||||
(when (and (<= (car bounds) position) (< position (cdr bounds)))
|
||||
(push (cons host-ref (- (cdr bounds) (car bounds))) candidates))))
|
||||
(etaf-runtime-handlers runtime))
|
||||
(unless candidates
|
||||
(user-error "No interactive ETAF Host at point"))
|
||||
(let* ((ordered (sort candidates
|
||||
(lambda (left right) (< (cdr left) (cdr right)))))
|
||||
(host-ref (car (car ordered))))
|
||||
(etaf-dispatch-event runtime host-ref 'press))))
|
||||
(let ((position (with-current-buffer (etaf-runtime-buffer runtime)
|
||||
(point))))
|
||||
(etaf--activation-at-position runtime position)))
|
||||
|
||||
(defun etaf--focus-candidate-before-p (left right)
|
||||
"Return non-nil when focus candidate LEFT precedes RIGHT."
|
||||
(or (< (nth 0 left) (nth 0 right))
|
||||
(and (= (nth 0 left) (nth 0 right))
|
||||
(or (< (nth 1 left) (nth 1 right))
|
||||
(and (= (nth 1 left) (nth 1 right))
|
||||
(or (< (nth 2 left) (nth 2 right))
|
||||
(and (= (nth 2 left) (nth 2 right))
|
||||
(string< (prin1-to-string (nth 3 left))
|
||||
(prin1-to-string (nth 3 right))))))))))
|
||||
|
||||
(defun etaf--focus-candidates (runtime)
|
||||
"Return RUNTIME's visible focus candidates in stable buffer order."
|
||||
(let (candidates)
|
||||
(dolist (entry (etaf-runtime-host-props-entries runtime))
|
||||
(let ((host-ref (car entry)) (props (cdr entry)))
|
||||
(let ((tab-index (plist-get props :tab-index))
|
||||
(bounds (ebox-host-ref-bounds
|
||||
(etaf-runtime-buffer runtime) host-ref)))
|
||||
(when (and (numberp tab-index) (>= tab-index 0)
|
||||
(not (plist-get props :disabled)) bounds)
|
||||
(push (list tab-index (car bounds) (cdr bounds) host-ref)
|
||||
candidates)))))
|
||||
(sort candidates #'etaf--focus-candidate-before-p)))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-focus (runtime host-ref)
|
||||
"Move focus to HOST-REF in mounted RUNTIME and return HOST-REF."
|
||||
(defun etaf-focus (&optional runtime host-ref)
|
||||
"Move focus to HOST-REF in mounted RUNTIME and move point to its position.
|
||||
|
||||
When called interactively without arguments, focus the first visible Host."
|
||||
(interactive)
|
||||
(setq runtime (etaf-runtime-require-mounted runtime))
|
||||
(unless (etaf-host-ref-bounds runtime host-ref)
|
||||
(signal 'etaf-event-error
|
||||
(list (format "Cannot focus an invisible Host reference: %S"
|
||||
host-ref))))
|
||||
(etaf-runtime-set-focus-ref runtime host-ref))
|
||||
(unless host-ref
|
||||
(setq host-ref (nth 3 (car (etaf--focus-candidates runtime))))
|
||||
(unless host-ref
|
||||
(user-error "No focusable ETAF Host")))
|
||||
(let ((position (etaf-host-ref-position runtime host-ref)))
|
||||
(unless position
|
||||
(signal 'etaf-event-error
|
||||
(list (format "Cannot focus an invisible Host reference: %S"
|
||||
host-ref))))
|
||||
(etaf-runtime-set-focus-ref runtime host-ref)
|
||||
(with-current-buffer (etaf-runtime-buffer runtime)
|
||||
(goto-char position))
|
||||
host-ref))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-focused-host-ref (&optional runtime)
|
||||
"Return the currently focused Host reference in mounted RUNTIME."
|
||||
(etaf-runtime-focus-ref (etaf-runtime-require-mounted runtime)))
|
||||
|
||||
(defun etaf--focus-cycle (runtime step)
|
||||
"Move focus by STEP positions through RUNTIME's ordered candidates."
|
||||
(setq runtime (etaf-runtime-require-mounted runtime))
|
||||
(let* ((candidates (etaf--focus-candidates runtime))
|
||||
(ordered (mapcar (lambda (entry) (nth 3 entry)) candidates))
|
||||
(current (etaf-runtime-focus-ref runtime))
|
||||
(position (cl-position current ordered :test #'equal)))
|
||||
(unless ordered
|
||||
(user-error "No focusable ETAF Host"))
|
||||
(etaf-focus runtime
|
||||
(if position
|
||||
(nth (mod (+ position step) (length ordered)) ordered)
|
||||
(if (> step 0) (car ordered) (car (last ordered)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-focus-next (&optional runtime)
|
||||
"Focus the next visible Host in mounted RUNTIME.
|
||||
"Focus the next visible Host in mounted RUNTIME."
|
||||
(interactive)
|
||||
(etaf--focus-cycle runtime 1))
|
||||
|
||||
RUNTIME may be nil to use the current Runtime. Hosts need a numeric,
|
||||
non-negative `:tab-index'."
|
||||
(setq runtime (etaf-runtime-require-mounted runtime))
|
||||
(let (candidates)
|
||||
(maphash
|
||||
(lambda (host-ref props)
|
||||
(let ((tab-index (plist-get props :tab-index)))
|
||||
(when (and (numberp tab-index) (>= tab-index 0)
|
||||
(etaf-host-ref-bounds runtime host-ref))
|
||||
(push (cons tab-index host-ref) candidates))))
|
||||
(etaf-runtime-host-props runtime))
|
||||
(setq candidates (sort candidates
|
||||
(lambda (left right)
|
||||
(< (car left) (car right)))))
|
||||
(unless candidates
|
||||
(user-error "No focusable ETAF Host"))
|
||||
(let* ((current (etaf-runtime-focus-ref runtime))
|
||||
(ordered (mapcar #'cdr candidates))
|
||||
(position (cl-position current ordered :test #'equal))
|
||||
(next (if position
|
||||
(or (nth (1+ position) ordered) (car ordered))
|
||||
(car ordered))))
|
||||
(etaf-focus runtime next))))
|
||||
;;;###autoload
|
||||
(defun etaf-focus-previous (&optional runtime)
|
||||
"Focus the previous visible Host in mounted RUNTIME."
|
||||
(interactive)
|
||||
(etaf--focus-cycle runtime -1))
|
||||
|
||||
(defun etaf-activate-mouse (event)
|
||||
"Activate the enabled Host at the real mouse EVENT position."
|
||||
(interactive "e")
|
||||
(let* ((start (event-start event))
|
||||
(window (posn-window start))
|
||||
(position (etaf--position-number (posn-point start))))
|
||||
(when (and (windowp window) position)
|
||||
(let ((buffer (window-buffer window)))
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer)))
|
||||
(goto-char position)
|
||||
(etaf--activation-at-position runtime position t))))))))
|
||||
|
||||
(defvar etaf-input-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "TAB") #'etaf-focus-next)
|
||||
(define-key map [tab] #'etaf-focus-next)
|
||||
(define-key map (kbd "<backtab>") #'etaf-focus-previous)
|
||||
(define-key map [backtab] #'etaf-focus-previous)
|
||||
(define-key map (kbd "S-TAB") #'etaf-focus-previous)
|
||||
(define-key map (kbd "RET") #'etaf-activate)
|
||||
(define-key map (kbd "<return>") #'etaf-activate)
|
||||
(define-key map [mouse-1] #'etaf-activate-mouse)
|
||||
map)
|
||||
"Keymap for mounted ETAF Runtime input.")
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode etaf-input-mode
|
||||
"Enable keyboard and mouse input for a mounted ETAF Runtime."
|
||||
:init-value nil
|
||||
:lighter " ETAF"
|
||||
:keymap etaf-input-mode-map)
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-events-enable-input (buffer)
|
||||
"Enable `etaf-input-mode' in mounted BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(etaf-input-mode 1)))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-events-disable-input (buffer)
|
||||
"Disable `etaf-input-mode' in BUFFER after Runtime unmount."
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(when (bound-and-true-p etaf-input-mode)
|
||||
(etaf-input-mode -1)))))
|
||||
|
||||
(provide 'etaf-events)
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
test
|
||||
(version 0)
|
||||
subscribers
|
||||
source-id
|
||||
name)
|
||||
|
||||
(cl-defstruct (etaf-computed
|
||||
@ -35,6 +36,7 @@
|
||||
test
|
||||
(version 0)
|
||||
subscribers
|
||||
source-id
|
||||
effect
|
||||
(dirty-p t)
|
||||
evaluated-p
|
||||
@ -63,15 +65,31 @@
|
||||
name
|
||||
(active-p t))
|
||||
|
||||
(cl-defstruct (etaf-runtime-route
|
||||
(:constructor etaf-runtime-route-create))
|
||||
"Opaque Runtime route stored by reactive sources."
|
||||
runtime-id mount-epoch scheduler)
|
||||
|
||||
(defvar etaf--active-effect nil
|
||||
"The effect currently collecting dependencies.")
|
||||
|
||||
(defvar etaf--source-id-counter 0)
|
||||
|
||||
(defun etaf-reactive-source-id (source)
|
||||
"Return stable integer id of reactive SOURCE."
|
||||
(cond ((etaf-ref-p source) (etaf-ref-source-id source))
|
||||
((etaf-computed-p source) (etaf-computed-source-id source))
|
||||
(t (signal 'wrong-type-argument (list 'etaf-reactive-source-p source)))))
|
||||
|
||||
(defvar etaf--active-scope nil
|
||||
"The Scope that owns newly created effects and cleanups.")
|
||||
|
||||
(defvar etaf--tracking-enabled-p t
|
||||
"Whether reactive reads may collect dependencies.")
|
||||
|
||||
(defvar etaf--runtime-dependency-collector nil
|
||||
"Runtime-local collector called for each reactive source read.")
|
||||
|
||||
(defvar etaf--render-phase-p nil
|
||||
"Whether the current call is producing a View tree.")
|
||||
|
||||
@ -81,6 +99,53 @@
|
||||
The function receives a zero-argument job and a phase symbol. Outside a
|
||||
Runtime, watchers run synchronously.")
|
||||
|
||||
(defvar etaf--dispatch-depth 0)
|
||||
(defvar etaf--dispatch-source-queue nil)
|
||||
(defvar etaf--dispatch-source-set (make-hash-table :test #'eq))
|
||||
(defvar etaf--dispatch-runtime-queue nil)
|
||||
(defvar etaf--dispatch-runtime-set (make-hash-table :test #'eq))
|
||||
(defvar etaf--dispatch-effect-set (make-hash-table :test #'eq))
|
||||
|
||||
(defun etaf-reactive-enqueue-runtime-flush (runtime function)
|
||||
"Queue FUNCTION once for RUNTIME after the outer reactive dispatch settles."
|
||||
(unless (gethash runtime etaf--dispatch-runtime-set)
|
||||
(puthash runtime function etaf--dispatch-runtime-set)
|
||||
(setq etaf--dispatch-runtime-queue
|
||||
(nconc etaf--dispatch-runtime-queue (list runtime)))))
|
||||
|
||||
(defun etaf--dispatch-source-now (source)
|
||||
"Notify SOURCE subscribers without opening another dispatch boundary."
|
||||
(let ((subscribers (copy-hash-table (etaf--source-subscribers source))))
|
||||
(maphash
|
||||
(lambda (subscriber _)
|
||||
(cond
|
||||
((etaf-runtime-route-p subscriber)
|
||||
(funcall (etaf-runtime-route-scheduler subscriber)
|
||||
subscriber source))
|
||||
((etaf-effect-active-p subscriber)
|
||||
(unless (gethash subscriber etaf--dispatch-effect-set)
|
||||
(puthash subscriber t etaf--dispatch-effect-set)
|
||||
(if-let ((scheduler (etaf-effect-scheduler subscriber)))
|
||||
(funcall scheduler subscriber)
|
||||
(etaf-reactive-effect-run subscriber))))))
|
||||
subscribers)))
|
||||
|
||||
(defun etaf--drain-dispatch ()
|
||||
"Drain reactive sources and Runtime work to a stable outer fixed point."
|
||||
(while (or etaf--dispatch-source-queue etaf--dispatch-runtime-queue)
|
||||
(while etaf--dispatch-source-queue
|
||||
(let ((source (pop etaf--dispatch-source-queue)))
|
||||
(remhash source etaf--dispatch-source-set)
|
||||
(etaf--dispatch-source-now source)))
|
||||
;; Detach this turn. A lifecycle write may enqueue a source and the same
|
||||
;; Runtime again for the following turn without merging it into this one.
|
||||
(let ((turn etaf--dispatch-runtime-queue))
|
||||
(setq etaf--dispatch-runtime-queue nil)
|
||||
(dolist (runtime turn)
|
||||
(let ((function (gethash runtime etaf--dispatch-runtime-set)))
|
||||
(remhash runtime etaf--dispatch-runtime-set)
|
||||
(funcall function))))))
|
||||
|
||||
(defun etaf--reactive-same-p (left right)
|
||||
"Return whether LEFT and RIGHT are equal under ETAF's shallow rule."
|
||||
(if (or (consp left) (vectorp left) (hash-table-p left)
|
||||
@ -104,6 +169,9 @@ Runtime, watchers run synchronously.")
|
||||
|
||||
(defun etaf--track-source (source)
|
||||
"Record SOURCE as a dependency of the active effect."
|
||||
(when (and etaf--tracking-enabled-p etaf--runtime-dependency-collector
|
||||
(null etaf--active-effect))
|
||||
(funcall etaf--runtime-dependency-collector source))
|
||||
(when (and etaf--tracking-enabled-p etaf--active-effect
|
||||
(etaf-effect-active-p etaf--active-effect))
|
||||
(let ((subscribers (etaf--source-subscribers source)))
|
||||
@ -169,6 +237,7 @@ of the run."
|
||||
(etaf--clear-effect-deps effect)
|
||||
(let ((etaf--active-effect effect)
|
||||
(etaf--active-scope (etaf-effect-owner-scope effect))
|
||||
(etaf--runtime-dependency-collector nil)
|
||||
(etaf--tracking-enabled-p t)
|
||||
(etaf--render-phase-p (or rendering etaf--render-phase-p)))
|
||||
(setf (etaf-effect-running-p effect) t)
|
||||
@ -186,14 +255,18 @@ of the run."
|
||||
|
||||
(defun etaf--dispatch-source (source)
|
||||
"Notify every current subscriber of SOURCE once."
|
||||
(let ((subscribers (copy-hash-table (etaf--source-subscribers source))))
|
||||
(maphash
|
||||
(lambda (effect _)
|
||||
(when (etaf-effect-active-p effect)
|
||||
(if-let ((scheduler (etaf-effect-scheduler effect)))
|
||||
(funcall scheduler effect)
|
||||
(etaf-reactive-effect-run effect))))
|
||||
subscribers)))
|
||||
(unless (gethash source etaf--dispatch-source-set)
|
||||
(puthash source t etaf--dispatch-source-set)
|
||||
(setq etaf--dispatch-source-queue
|
||||
(nconc etaf--dispatch-source-queue (list source))))
|
||||
(when (zerop etaf--dispatch-depth)
|
||||
(let ((etaf--dispatch-depth 1))
|
||||
(unwind-protect (etaf--drain-dispatch)
|
||||
(setq etaf--dispatch-source-queue nil
|
||||
etaf--dispatch-runtime-queue nil)
|
||||
(clrhash etaf--dispatch-source-set)
|
||||
(clrhash etaf--dispatch-runtime-set)
|
||||
(clrhash etaf--dispatch-effect-set)))))
|
||||
|
||||
;;;###autoload
|
||||
(cl-defun etaf-ref (initial-value &key test name)
|
||||
@ -205,6 +278,7 @@ TEST optionally compares old and new values. NAME is used in diagnostics."
|
||||
(etaf--ref-create :value initial-value
|
||||
:test test
|
||||
:subscribers (make-hash-table :test #'eq)
|
||||
:source-id (cl-incf etaf--source-id-counter)
|
||||
:name name))
|
||||
|
||||
(defun etaf--computed-value (computed)
|
||||
@ -241,7 +315,8 @@ computed value."
|
||||
(signal 'wrong-type-argument (list 'functionp test)))
|
||||
(let ((computed (etaf--computed-create
|
||||
:getter getter :test test :name name
|
||||
:subscribers (make-hash-table :test #'eq))))
|
||||
:subscribers (make-hash-table :test #'eq)
|
||||
:source-id (cl-incf etaf--source-id-counter))))
|
||||
(setf (etaf-computed-effect computed)
|
||||
(etaf-reactive-effect-create
|
||||
getter
|
||||
|
||||
268
etaf-renderer.el
268
etaf-renderer.el
@ -14,9 +14,23 @@
|
||||
(require 'cl-lib)
|
||||
(require 'ebox)
|
||||
(require 'etaf-view)
|
||||
(require 'etaf-component)
|
||||
(require 'etaf-context)
|
||||
|
||||
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
|
||||
|
||||
(defvar etaf--rendered-range-container-nodes nil
|
||||
"Candidate-local Ebox nodes whose layout directly contains a Range.")
|
||||
(declare-function etaf--runtime-render-child-range "etaf-runtime" (runtime expr path))
|
||||
(declare-function etaf--runtime-render-slot-range
|
||||
"etaf-runtime" (runtime projection path))
|
||||
(declare-function etaf--runtime-register-semantic-host "etaf-runtime" (runtime name props backend-props path))
|
||||
(declare-function etaf--runtime-finish-semantic-host
|
||||
"etaf-runtime" (runtime semantic-id &optional content content-parts))
|
||||
(declare-function etaf--runtime-render-inline-content
|
||||
"etaf-runtime" (runtime host-id values path &optional surface))
|
||||
(declare-function etaf--runtime-call-with-component-env
|
||||
"etaf-runtime" (runtime component-id function))
|
||||
(declare-function etaf--runtime-behavior-node "etaf-runtime" (runtime node path))
|
||||
(declare-function etaf--runtime-register-host "etaf-runtime" (runtime props path))
|
||||
(declare-function etaf-theme-defaults "etaf-context" (&optional default))
|
||||
@ -35,6 +49,15 @@
|
||||
(defvar etaf--render-style-stack nil
|
||||
"Component style scopes active during View lowering.")
|
||||
|
||||
(defvar etaf--render-parent-style-stack nil
|
||||
"Style scopes belonging to the caller of the current Component.")
|
||||
|
||||
(defvar etaf--current-semantic-parent-id 0
|
||||
"Semantic parent receiving Hosts during mounted lowering.")
|
||||
|
||||
(defvar etaf--rendering-range-p nil
|
||||
"Non-nil while eagerly lowering descendants of one Range item Host.")
|
||||
|
||||
(defun etaf--event-property-p (property)
|
||||
"Return non-nil when PROPERTY is an ETAF event callback property."
|
||||
(and (keywordp property)
|
||||
@ -94,7 +117,9 @@ represented as ordinary static data until this final lowering step; they do
|
||||
not create another visual node category."
|
||||
(if (null styles)
|
||||
node
|
||||
(let ((props (copy-sequence (etaf--view-node-props node))))
|
||||
(let ((props (copy-sequence
|
||||
(etaf--resolve-property-plist
|
||||
(etaf--view-node-props node)))))
|
||||
(dolist (rule (cdr styles))
|
||||
(when (and (consp rule) (stringp (car rule))
|
||||
(etaf--style-selector-match-p (car rule) node root-p))
|
||||
@ -109,29 +134,44 @@ not create another visual node category."
|
||||
(setq value (if (and (consp value) (eq (car value) 'quote))
|
||||
(cadr value)
|
||||
value))
|
||||
(unless (plist-member props key)
|
||||
(setq props (append props (list key value)))))))))
|
||||
(unless (plist-get props key)
|
||||
(setq props (etaf--merge-property props key value))))))))
|
||||
(etaf--view-node-create
|
||||
:name (etaf--view-node-name node)
|
||||
:token (etaf--view-node-token node)
|
||||
:props props
|
||||
:children (etaf--view-node-children node)))))
|
||||
|
||||
(defun etaf--apply-theme-defaults (node)
|
||||
"Apply inherited Theme defaults to NODE without overriding props."
|
||||
"Apply known inherited Theme defaults to NODE without overriding props.
|
||||
Unknown Theme keys remain readable through `etaf-theme-value' but are design
|
||||
tokens, not Ebox Host properties, and therefore are not materialized here."
|
||||
(let ((defaults (etaf-theme-defaults)))
|
||||
(if (null defaults)
|
||||
node
|
||||
(let ((props (copy-sequence (etaf--view-node-props node))))
|
||||
(let ((props (copy-sequence
|
||||
(etaf--resolve-property-plist
|
||||
(etaf--view-node-props node)))))
|
||||
(while defaults
|
||||
(let ((key (pop defaults))
|
||||
(value (pop defaults)))
|
||||
(unless (plist-member props key)
|
||||
(setq props (append props (list key value))))))
|
||||
(when (and (etaf--theme-ebox-property-p key)
|
||||
(not (plist-get props key)))
|
||||
(setq props (etaf--merge-property props key value)))))
|
||||
(etaf--view-node-create
|
||||
:name (etaf--view-node-name node)
|
||||
:token (etaf--view-node-token node)
|
||||
:props props
|
||||
:children (etaf--view-node-children node))))))
|
||||
|
||||
(defun etaf--theme-ebox-property-p (key)
|
||||
"Return non-nil when Theme KEY is a known Ebox property.
|
||||
The public `ebox-property-rules' table is the shared property vocabulary;
|
||||
custom Theme tokens remain Context data and are never copied onto Hosts."
|
||||
(and (keywordp key)
|
||||
(boundp 'ebox-property-rules)
|
||||
(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))))
|
||||
@ -145,6 +185,13 @@ not create another visual node category."
|
||||
(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)
|
||||
"Return `(SUCCESS . STRING)' for inline-compatible text VALUE.
|
||||
|
||||
@ -186,6 +233,18 @@ the ordinary layout lowering path instead."
|
||||
(cons valid-p (and valid-p result))))
|
||||
(t (cons nil nil))))
|
||||
|
||||
(defun etaf--inline-text-structural-p (value)
|
||||
"Return non-nil when VALUE can be owned by mounted inline effects."
|
||||
(cond
|
||||
((or (null value) (stringp value) (etaf--expr-p value)) t)
|
||||
((etaf--view-node-p value)
|
||||
(and (eq (etaf--view-node-name value) 'text)
|
||||
(cl-every #'etaf--inline-text-structural-p
|
||||
(etaf--view-node-children value))))
|
||||
((proper-list-p value)
|
||||
(cl-every #'etaf--inline-text-structural-p value))
|
||||
(t nil)))
|
||||
|
||||
(defun etaf--ebox-properties (props path)
|
||||
"Translate ETAF PROPS at PATH into Ebox's public property list."
|
||||
(let* ((source-props (etaf--resolve-property-plist props))
|
||||
@ -237,13 +296,50 @@ the ordinary layout lowering path instead."
|
||||
|
||||
(defun etaf--render-slot-projection (projection path)
|
||||
"Render PROJECTION at PATH using the active Component slot collection."
|
||||
(setq etaf--raw-slot-read-p t)
|
||||
(let* ((name (etaf--slot-projection-name projection))
|
||||
(slots etaf--current-component-slots)
|
||||
(entry (assq name slots))
|
||||
(content (and entry (cdr entry)))
|
||||
(children (if (etaf--slot-content-p content)
|
||||
(etaf--slot-content-children content)
|
||||
content))
|
||||
(owner-id (and (etaf--slot-content-p content)
|
||||
(etaf--slot-content-owner-component-id content)))
|
||||
(fallback (etaf--slot-projection-fallback projection)))
|
||||
(etaf--render-value-list
|
||||
(if entry (cdr entry) fallback)
|
||||
(append path (list :slot name)))))
|
||||
(let ((etaf--render-style-stack
|
||||
(if entry
|
||||
etaf--render-parent-style-stack
|
||||
etaf--render-style-stack)))
|
||||
(let ((render (lambda ()
|
||||
(etaf--render-value-list
|
||||
(if entry children fallback)
|
||||
(append path (list :slot name))))))
|
||||
(if (and entry etaf--render-runtime)
|
||||
(etaf--runtime-call-with-component-env
|
||||
etaf--render-runtime owner-id render)
|
||||
(funcall render))))))
|
||||
|
||||
(defun etaf--slot-projection-range-compatible-p (projection)
|
||||
"Return whether mounted PROJECTION can use the current slot Range slice."
|
||||
(let* ((entry (assq (etaf--slot-projection-name projection)
|
||||
etaf--current-component-slots))
|
||||
(content (and entry (cdr entry)))
|
||||
(values (if entry
|
||||
(if (etaf--slot-content-p content)
|
||||
(etaf--slot-content-children content)
|
||||
content)
|
||||
(etaf--slot-projection-fallback projection))))
|
||||
(cl-labels
|
||||
((valid
|
||||
(value)
|
||||
(cond ((or (null value) (stringp value) (etaf--expr-p value)) t)
|
||||
((etaf--view-node-p value)
|
||||
(and (not (eq (etaf--view-node-name value) 'fragment))
|
||||
(cl-every #'valid (etaf--view-node-children value))))
|
||||
((proper-list-p value) (cl-every #'valid value))
|
||||
(t nil))))
|
||||
(valid values))))
|
||||
|
||||
(defun etaf--render-component-call-pure (call path)
|
||||
"Render stateless CALL at PATH without a retained Runtime."
|
||||
@ -256,11 +352,11 @@ the ordinary layout lowering path instead."
|
||||
(etaf--component-call-props call))
|
||||
(etaf--current-component-slots
|
||||
(etaf--component-call-slots call)))
|
||||
(let ((etaf--render-style-stack
|
||||
(cons (cons (etaf--component-spec-styles spec)
|
||||
(let ((etaf--render-parent-style-stack etaf--render-style-stack)
|
||||
(etaf--render-style-stack
|
||||
(list (cons (etaf--component-spec-styles spec)
|
||||
(append path (list :component
|
||||
(etaf--component-spec-name spec))))
|
||||
etaf--render-style-stack)))
|
||||
(etaf--component-spec-name spec)))))))
|
||||
(etaf--render-value-list
|
||||
(funcall render
|
||||
(etaf--component-call-props call)
|
||||
@ -278,13 +374,24 @@ the ordinary layout lowering path instead."
|
||||
path)
|
||||
append
|
||||
(cond
|
||||
((stringp item) (list (ebox-create :content item)))
|
||||
((stringp item)
|
||||
(when etaf--render-runtime
|
||||
(let ((semantic-id
|
||||
(etaf--runtime-register-semantic-host
|
||||
etaf--render-runtime 'text nil nil item-path)))
|
||||
(etaf--runtime-finish-semantic-host
|
||||
etaf--render-runtime semantic-id item)))
|
||||
(list (ebox-create :content item)))
|
||||
((etaf--view-node-p item)
|
||||
(etaf--render-node item item-path))
|
||||
((etaf--component-call-p item)
|
||||
(if etaf--render-runtime
|
||||
(list (etaf--runtime-render-component
|
||||
etaf--render-runtime item item-path))
|
||||
(let ((result (etaf--runtime-render-component
|
||||
etaf--render-runtime item item-path)))
|
||||
(if (and (consp result)
|
||||
(eq (car result) 'component-output-range))
|
||||
(cdr result)
|
||||
(list result)))
|
||||
(etaf--render-component-call-pure item item-path)))
|
||||
((etaf--slot-projection-p item)
|
||||
(etaf--render-slot-projection item item-path))
|
||||
@ -328,41 +435,115 @@ the ordinary layout lowering path instead."
|
||||
(etaf--runtime-register-host etaf--render-runtime raw-props path))
|
||||
(pcase name
|
||||
('text
|
||||
(let* ((flat (cl-mapcan #'etaf--flatten-view-value children))
|
||||
(let* ((semantic-id
|
||||
(and etaf--render-runtime
|
||||
(etaf--runtime-register-semantic-host
|
||||
etaf--render-runtime name raw-props props path)))
|
||||
(child-path (append path (list :text)))
|
||||
(inline (etaf--inline-text-content flat))
|
||||
(runtime-inline
|
||||
(and semantic-id
|
||||
(etaf--inline-text-structural-p children)
|
||||
(etaf--runtime-render-inline-content
|
||||
etaf--render-runtime semantic-id children child-path)))
|
||||
(flat (unless runtime-inline
|
||||
(cl-mapcan #'etaf--flatten-view-value children)))
|
||||
(inline (if runtime-inline
|
||||
(cons t (car runtime-inline))
|
||||
(etaf--inline-text-content flat)))
|
||||
(rendered (unless (car inline)
|
||||
(etaf--render-value-list flat child-path))))
|
||||
(let ((etaf--current-semantic-parent-id
|
||||
(or semantic-id
|
||||
etaf--current-semantic-parent-id)))
|
||||
(etaf--render-value-list flat child-path)))))
|
||||
(when semantic-id
|
||||
(etaf--runtime-finish-semantic-host
|
||||
etaf--render-runtime semantic-id
|
||||
(and (car inline) (cdr inline))
|
||||
(and runtime-inline (cadr runtime-inline))))
|
||||
(if (car inline)
|
||||
(list (apply #'ebox-create
|
||||
:content (cdr inline)
|
||||
props))
|
||||
(list (ebox-build
|
||||
(append (list 'row) props rendered))))))
|
||||
(list (apply #'ebox-create :content (cdr inline) props))
|
||||
(list (ebox-build (append (list 'row) props rendered))))))
|
||||
('spacer
|
||||
(when children
|
||||
(signal 'etaf-renderer-error
|
||||
(list "spacer cannot have children")))
|
||||
(when etaf--render-runtime
|
||||
(let ((semantic-id (etaf--runtime-register-semantic-host
|
||||
etaf--render-runtime name raw-props props path)))
|
||||
(etaf--runtime-finish-semantic-host
|
||||
etaf--render-runtime semantic-id)))
|
||||
(list (apply #'ebox-spacer props)))
|
||||
('fragment
|
||||
(etaf--render-value-list children (append path (list :fragment))))
|
||||
((or 'row 'column 'container 'stack 'flex 'grid)
|
||||
(let ((nodes nil)
|
||||
(index 0))
|
||||
(index 0)
|
||||
(range-child-p nil)
|
||||
(semantic-id
|
||||
(and etaf--render-runtime
|
||||
(etaf--runtime-register-semantic-host
|
||||
etaf--render-runtime name raw-props props path))))
|
||||
(dolist (child children)
|
||||
(setq nodes
|
||||
(nconc nodes
|
||||
(etaf--render-value-list
|
||||
child (append path (list index)))))
|
||||
(let ((etaf--current-semantic-parent-id
|
||||
(or semantic-id
|
||||
etaf--current-semantic-parent-id)))
|
||||
(if (and semantic-id (etaf--expr-p child)
|
||||
(not etaf--rendering-range-p))
|
||||
(let ((result
|
||||
(etaf--runtime-render-child-range
|
||||
etaf--render-runtime child
|
||||
(append path (list index)))))
|
||||
(when (eq (car result) 'range)
|
||||
(setq range-child-p t))
|
||||
(cdr result))
|
||||
(if (and semantic-id
|
||||
(etaf--view-node-p child)
|
||||
(eq (etaf--view-node-name child) 'fragment)
|
||||
(not etaf--rendering-range-p))
|
||||
(let ((result
|
||||
(etaf--runtime-render-fragment-range
|
||||
etaf--render-runtime child
|
||||
(append path (list index)))))
|
||||
(setq range-child-p t)
|
||||
(cdr result))
|
||||
(if (and semantic-id
|
||||
(etaf--raw-ebox-p child)
|
||||
(not etaf--rendering-range-p))
|
||||
(let ((result
|
||||
(etaf--runtime-render-raw-range
|
||||
etaf--render-runtime child
|
||||
(append path (list index)))))
|
||||
(setq range-child-p t)
|
||||
(cdr result))
|
||||
(if (and semantic-id
|
||||
(etaf--slot-projection-p child)
|
||||
(etaf--slot-projection-range-compatible-p
|
||||
child)
|
||||
(not etaf--rendering-range-p))
|
||||
(let ((result
|
||||
(etaf--runtime-render-slot-range
|
||||
etaf--render-runtime child
|
||||
(append path (list index)))))
|
||||
(setq range-child-p t)
|
||||
(cdr result))
|
||||
(etaf--render-value-list
|
||||
child (append path (list index))))))))))
|
||||
(cl-incf index))
|
||||
(list (etaf--layout-node name props nodes))))
|
||||
(when semantic-id
|
||||
(etaf--runtime-finish-semantic-host
|
||||
etaf--render-runtime semantic-id))
|
||||
(list (etaf--layout-node name props nodes range-child-p))))
|
||||
(_
|
||||
(signal 'etaf-renderer-error
|
||||
(list (format "Unknown Host reached renderer: %S" name)))))))
|
||||
|
||||
(defun etaf--layout-node (name props nodes)
|
||||
"Build layout NAME with Ebox PROPS around child NODES."
|
||||
(cond
|
||||
(defun etaf--layout-node (name props nodes &optional range-child-p)
|
||||
"Build layout NAME with Ebox PROPS around child NODES.
|
||||
RANGE-CHILD-P records that NODES include nonvisual Range segments."
|
||||
(let ((node
|
||||
(cond
|
||||
((eq name 'row)
|
||||
(if (null props)
|
||||
(apply #'ebox-row nodes)
|
||||
@ -376,8 +557,27 @@ the ordinary layout lowering path instead."
|
||||
((eq name 'grid)
|
||||
(apply #'ebox-grid (append props nodes)))
|
||||
(t
|
||||
(signal 'etaf-renderer-error
|
||||
(list (format "Not a layout Host: %S" name))))))
|
||||
(signal 'etaf-renderer-error
|
||||
(list (format "Not a layout Host: %S" name)))))))
|
||||
(when range-child-p
|
||||
(push node etaf--rendered-range-container-nodes))
|
||||
node))
|
||||
|
||||
(defun etaf--lower-resolved-semantic-host
|
||||
(name props content children range-child-p)
|
||||
"Lower resolved NAME, PROPS, CONTENT, and CHILDREN through Renderer.
|
||||
RANGE-CHILD-P preserves the direct material Range parent."
|
||||
(pcase name
|
||||
('text
|
||||
(if children
|
||||
(etaf--layout-node 'row props children range-child-p)
|
||||
(apply #'ebox-create :content content props)))
|
||||
('spacer (apply #'ebox-spacer props))
|
||||
((or 'row 'column 'container 'stack 'flex 'grid)
|
||||
(etaf--layout-node name props children range-child-p))
|
||||
(_ (signal 'etaf-renderer-error
|
||||
(list (format "Semantic Host requires Step4b lowering: %S"
|
||||
name))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-render (view)
|
||||
|
||||
4050
etaf-runtime.el
4050
etaf-runtime.el
File diff suppressed because it is too large
Load Diff
42
etaf-view.el
42
etaf-view.el
@ -23,12 +23,14 @@
|
||||
(:constructor etaf--view-node-create))
|
||||
"Internal normalized description of one Host View."
|
||||
name
|
||||
token
|
||||
props
|
||||
children)
|
||||
|
||||
(cl-defstruct (etaf--expr
|
||||
(:constructor etaf--expr-create))
|
||||
"Internal executable child expression."
|
||||
token
|
||||
thunk)
|
||||
|
||||
(cl-defstruct (etaf--component-spec
|
||||
@ -57,11 +59,13 @@
|
||||
(:constructor etaf--slot-projection-create))
|
||||
"A Component View request for one slot and its fallback children."
|
||||
name
|
||||
token
|
||||
fallback)
|
||||
|
||||
(cl-defstruct (etaf--raw-ebox
|
||||
(:constructor etaf--raw-ebox-create))
|
||||
"Explicit escape carrying an already-built public Ebox node."
|
||||
token
|
||||
thunk
|
||||
key-thunk)
|
||||
|
||||
@ -313,9 +317,11 @@ belong to the anonymous `default' slot."
|
||||
(etaf--compile-child-form child slot-mode))
|
||||
children)))
|
||||
(if (eq slot-mode :projection)
|
||||
`(etaf--slot-projection-create
|
||||
:name ',name
|
||||
:fallback (list ,@compiled))
|
||||
(let ((token (gensym "etaf-slot-site-")))
|
||||
`(etaf--slot-projection-create
|
||||
:name ',name
|
||||
:token ',token
|
||||
:fallback (list ,@compiled)))
|
||||
`(etaf--slot-input-create
|
||||
:name ',name
|
||||
:children (list ,@compiled))))))
|
||||
@ -328,8 +334,10 @@ belong to the anonymous `default' slot."
|
||||
|
||||
(defun etaf--compile-expr-form (items)
|
||||
"Compile an `expr' form with ITEMS into an executable View value."
|
||||
`(etaf--expr-create
|
||||
:thunk (lambda () ,(etaf--parse-expr-form items))))
|
||||
(let ((token (gensym "etaf-expr-site-")))
|
||||
`(etaf--expr-create
|
||||
:token ',token
|
||||
:thunk (lambda () ,(etaf--parse-expr-form items)))))
|
||||
|
||||
(defun etaf--compile-child-form (form &optional slot-mode)
|
||||
"Compile structural child FORM into code returning a View value.
|
||||
@ -380,11 +388,13 @@ SLOT-MODE distinguishes Component-owned projections from call-site inputs."
|
||||
(unless (and (plist-member props :value)
|
||||
(cl-every #'keywordp (cl-loop for (key _value) on props by #'cddr collect key)))
|
||||
(etaf--syntax-error "Raw-ebox requires :value"))
|
||||
(let ((allowed '(:value :key)))
|
||||
(let ((allowed '(:value :key))
|
||||
(token (gensym "etaf-raw-site-")))
|
||||
(dolist (key (cl-loop for (key _value) on props by #'cddr collect key))
|
||||
(unless (memq key allowed)
|
||||
(etaf--syntax-error "Raw-ebox does not accept %S" key)))
|
||||
`(etaf--raw-ebox-create
|
||||
:token ',token
|
||||
:thunk (lambda () ,(plist-get props :value))
|
||||
:key-thunk ,(when (plist-member props :key)
|
||||
`(lambda () ,(plist-get props :key))))))))
|
||||
@ -395,12 +405,15 @@ SLOT-MODE distinguishes Component-owned projections from call-site inputs."
|
||||
(host-view-p (eq (gethash (car form) etaf--view-registry)
|
||||
etaf--host-marker))
|
||||
(child-slot-mode (if host-view-p slot-mode :input)))
|
||||
`(etaf--view-call ',(car form)
|
||||
(list ,@(etaf--compile-property-plist props))
|
||||
(list ,@(mapcar (lambda (child)
|
||||
(etaf--compile-child-form
|
||||
child child-slot-mode))
|
||||
children)))))))
|
||||
(let ((token (and (memq (car form) '(fragment etaf-fragment))
|
||||
(gensym "etaf-fragment-site-"))))
|
||||
`(etaf--view-call ',(car form)
|
||||
(list ,@(etaf--compile-property-plist props))
|
||||
(list ,@(mapcar (lambda (child)
|
||||
(etaf--compile-child-form
|
||||
child child-slot-mode))
|
||||
children))
|
||||
,(and token `',token)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro etaf-view (form)
|
||||
@ -437,8 +450,8 @@ child region and accepts only `:value'."
|
||||
key (etaf--component-spec-name spec)))))
|
||||
props))
|
||||
|
||||
(defun etaf--view-call (name props children)
|
||||
"Construct a Host or Component named NAME from PROPS and CHILDREN."
|
||||
(defun etaf--view-call (name props children &optional token)
|
||||
"Construct NAME from PROPS and CHILDREN, retaining optional site TOKEN."
|
||||
(unless (symbolp name)
|
||||
(etaf--syntax-error "View name must be a symbol: %S" name))
|
||||
(setq props (etaf--validate-property-plist props))
|
||||
@ -450,6 +463,7 @@ child region and accepts only `:value'."
|
||||
(etaf--resolve-property-value (plist-get props :key))))
|
||||
(etaf--view-node-create
|
||||
:name (etaf--canonical-host-name name)
|
||||
:token token
|
||||
:props props
|
||||
:children children))
|
||||
((etaf--component-spec-p entry)
|
||||
|
||||
7
postmortem/2026-08-19-core-boundaries.en.md
Normal file
7
postmortem/2026-08-19-core-boundaries.en.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Core boundaries after the `.etaf` pair experiment
|
||||
|
||||
The safe data-only `.etaf` pair path was removed from current core. It added a second file grammar, companion execution boundary, and registry validation surface before a compiler contract existed. The archived `emacs-box` DSL cannot be reused as a shortcut: it has different ownership and executable semantics. A future `.etaf` SFC belongs in a compiler layer that emits the existing `etaf-view` and `etaf-define-component` contracts.
|
||||
|
||||
The current Component definition remains the single structure/style/behavior unit. Its `:view` owns structure, `:styles` owns presentation, and `:setup` owns retained state, Actions, and lifecycle. Style scopes now stop at nested Component internals; caller-owned slot content keeps the caller scope, while child-authored slot fallbacks keep the child scope.
|
||||
|
||||
Mounted input stays in the small Runtime lifecycle path: the input minor mode is enabled after a successful mount and disabled before unmount. TAB/backtab, RET, and mouse-1 use the public focus and hit-test operations rather than a second event layer.
|
||||
7
postmortem/2026-08-19-core-boundaries.zh.md
Normal file
7
postmortem/2026-08-19-core-boundaries.zh.md
Normal file
@ -0,0 +1,7 @@
|
||||
# 移除 `.etaf` pair 后的 core 边界
|
||||
|
||||
当前 core 移除了只读 data `.etaf`、同名 `.el` companion 的 pair 主路径。它在 compiler 契约尚未存在时增加了第二套文件语法、companion 执行边界和 registry 校验面。不能把 `emacs-box` 的归档 DSL 当捷径复用:它的所有权和可执行语义不同。未来 `.etaf` SFC 应属于 compiler layer,并输出既有的 `etaf-view` 与 `etaf-define-component` 契约。
|
||||
|
||||
当前 Component definition 仍是唯一的结构/样式/行为单元:`:view` 负责结构,`:styles` 负责 presentation,`:setup` 负责 retained state、Action 和生命周期。样式 scope 现在在嵌套 Component 内部边界停止;调用者提供的 slot 内容保留 caller scope,子组件自有的 slot fallback 保留 child scope。
|
||||
|
||||
挂载后的输入仍走窄的 Runtime 生命周期路径:成功 mount 后启用 input minor mode,unmount 前禁用。TAB/backtab、RET 和 mouse-1 复用公共 focus/hit-test 操作,不建立第二套事件层。
|
||||
3572
tests/etaf-tests.el
3572
tests/etaf-tests.el
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user