diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7ec750..ab92734 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,9 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Check out TP runtime dependency + run: git clone --depth 1 https://github.com/Kinneyzhang/tp.git ../tp + - name: Install system dependencies run: | sudo apt-get update diff --git a/DESIGN.md b/DESIGN.md index e0ed31b..e723453 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,6 +1,6 @@ # Ebox design -Ebox is the low-level rendering engine. Its job is to turn a declarative node tree into measured layout fragments and then publish those fragments into an Emacs buffer. It is deliberately smaller than an application framework. +Ebox is the low-level spatial rendering engine. Its job is to turn a declarative node tree into measured layout fragments and a generic TP surface plan. It is deliberately smaller than an application framework. ## Design principles @@ -19,10 +19,10 @@ node tree -> measured formatting context -> layout fragments and snapshot -> render context - -> buffer patch/publication + -> pure TP surface plan ``` -The owners are `ebox-tree.el` for identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-buffer-backend.el` for text properties and buffer edits, and `ebox-incremental.el` for dirty planning and publication. +The owners are `ebox-tree.el` for Ebox identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-surface.el` for pure TP plan projection, and `ebox-incremental.el` for dirty-owner planning. During the staged cutover, the existing Ebox buffer backend still publishes live public entry points; it is not used by the pure projector and will be removed when those entry points move to TP surfaces. ## Public boundary diff --git a/DESIGN.zh.md b/DESIGN.zh.md index 1e9cfe8..8de10b4 100644 --- a/DESIGN.zh.md +++ b/DESIGN.zh.md @@ -1,6 +1,6 @@ # Ebox 设计 -Ebox 是底层渲染引擎。它负责把声明式节点树转换为经过测量的布局 fragment,再发布到 Emacs buffer。它有意小于应用框架。 +Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过测量的布局 fragment 与通用 TP surface plan。它有意小于应用框架。 ## 设计原则 @@ -19,10 +19,10 @@ Ebox 是底层渲染引擎。它负责把声明式节点树转换为经过测量 -> 经过测量的 formatting context -> 布局 fragment 与 snapshot -> render context - -> buffer patch/发布 + -> 纯 TP surface plan ``` -各层 owner 是:`ebox-tree.el` 负责 identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-buffer-backend.el` 负责 text property 与 buffer 编辑,`ebox-incremental.el` 负责 dirty 规划与发布。 +各层 owner 是:`ebox-tree.el` 负责 Ebox identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-surface.el` 负责纯 TP plan 投影,`ebox-incremental.el` 负责 dirty owner 规划。分阶段切换期间,现有 Ebox buffer backend 仍为公开 live 入口执行发布;纯 projector 不调用它,公开入口切到 TP surface 后会删除这套发布执行器。 ## 公共边界 diff --git a/Makefile b/Makefile index 87c74d2..1e7536b 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,14 @@ EMACS ?= emacs CARGO ?= cargo +TP_DIR ?= ../tp +LOAD_EXTRA ?= -EMACS_BATCH = $(EMACS) -Q --batch -L . --eval '(setq load-prefer-newer t)' +EMACS_BATCH = $(EMACS) -Q --batch -L . -L $(TP_DIR) $(LOAD_EXTRA) --eval '(setq load-prefer-newer t)' NATIVE_MANIFEST = native/Cargo.toml NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))') NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release -.PHONY: all check ci load compile test checkdoc core-tests grid-tests ebox-commit-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install +.PHONY: all check ci load compile test checkdoc core-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install all: check @@ -21,7 +23,7 @@ compile: rm -f *.elc tests/*.elc scripts/*.elc $(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t)' -l ebox.el --eval '(ebox-byte-compile)' -test: core-tests grid-tests ebox-commit-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests +test: core-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests core-tests: $(EMACS_BATCH) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit @@ -32,6 +34,9 @@ grid-tests: ebox-commit-tests: $(EMACS_BATCH) -l tests/ebox-commit-tests.el -f ert-run-tests-batch-and-exit +surface-tests: + $(EMACS_BATCH) -l tests/ebox-surface-tests.el -f ert-run-tests-batch-and-exit + visual-check-tests: $(EMACS_BATCH) -l tests/ebox-visual-check-tests.el -f ert-run-tests-batch-and-exit diff --git a/README.md b/README.md index 412ed9c..75c7b26 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ Ebox is a standalone, pixel-precise box and layout engine for Emacs. It provides ## Install and use -Clone or place this directory on `load-path`: +Install TP first, then install Ebox through a package manager so its declared dependency is resolved. For sibling source checkouts, place both directories on `load-path`: ```elisp +(add-to-list 'load-path "/path/to/github/tp") (add-to-list 'load-path "/path/to/github/ebox") (require 'ebox) @@ -39,6 +40,7 @@ ETAF belongs in the sibling package. Install it separately when you need the uni | `ebox.el` | Public facade and package entry point. | | `ebox-style.el`, `ebox-tree.el`, `ebox-measure.el` | Style, tree, and measurement models. | | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Formatting contexts and layout algorithms. | +| `ebox-surface.el` | Pure Ebox runtime/layout projection into generic TP surface plans. | | `ebox-buffer-backend.el`, `ebox-incremental.el` | Buffer publication and incremental updates. | | `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and tree/runtime queries. | | `ebox-native-reflow.el`, `native/` | Optional native reflow boundary. | @@ -54,6 +56,6 @@ make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs make native-rust-tests ``` -Run the smallest focused target first (`make grid-tests`, `make dsl-tests`, or `make visual-check-tests`), then run `make check` for shared rendering or public API changes. +Run the smallest focused target first (`make grid-tests`, `make surface-tests`, `make dsl-tests`, or `make visual-check-tests`), then run `make check` for shared rendering or public API changes. A sibling TP checkout is found at `../tp` by default; override `TP_DIR` when it lives elsewhere. Read the [Ebox user guide](docs/user/ebox-user-guide.en.md) for public construction examples and the [current implementation reference](docs/maintainer/ebox-current-implementation-reference.en.md) for ownership boundaries and invariants. diff --git a/README.zh-CN.md b/README.zh-CN.md index 18e0155..8f0af95 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,9 +4,10 @@ Ebox 是一个独立的 Emacs 像素级 Box 与布局引擎。它是同级 [ETAF ## 安装与使用 -将本目录放到 `load-path`: +先安装 TP,再通过包管理器安装 Ebox,让声明的依赖自动解析。使用同级源码 checkout 时,把两个目录都加入 `load-path`: ```elisp +(add-to-list 'load-path "/path/to/github/tp") (add-to-list 'load-path "/path/to/github/ebox") (require 'ebox) @@ -39,6 +40,7 @@ ETAF 属于同级独立包。当你需要统一 View 语法、Component、响应 | `ebox.el` | 公共门面与包入口。 | | `ebox-style.el`、`ebox-tree.el`、`ebox-measure.el` | 样式、树和测量模型。 | | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Formatting Context 与布局算法。 | +| `ebox-surface.el` | 将 Ebox runtime/layout 纯投影为通用 TP surface plan。 | | `ebox-buffer-backend.el`、`ebox-incremental.el` | Buffer 发布与增量更新。 | | `ebox-dsl.el`、`ebox-selector.el` | 独立 DSL 与树/runtime 查询。 | | `ebox-native-reflow.el`、`native/` | 可选 native reflow 边界。 | @@ -54,6 +56,6 @@ make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs make native-rust-tests ``` -小范围修改先运行对应目标(例如 `make grid-tests`、`make dsl-tests` 或 `make visual-check-tests`);共享渲染或公共 API 修改后运行完整的 `make check`。 +小范围修改先运行对应目标(例如 `make grid-tests`、`make surface-tests`、`make dsl-tests` 或 `make visual-check-tests`);共享渲染或公共 API 修改后运行完整的 `make check`。默认从 `../tp` 查找同级 TP checkout;若路径不同可覆盖 `TP_DIR`。 公共构造示例见 [Ebox 用户指南](docs/user/ebox-user-guide.zh.md),职责边界与不变量见 [当前实现参考](docs/maintainer/ebox-current-implementation-reference.zh.md)。 diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index 1b2f0a8..754852f 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -24,6 +24,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri | `ebox-layout.el` | Box, row, column, stack, concatenation, spacer, wrapping, and common formatting context. | | `ebox-flex.el` | Flex normalization, lines, free-space distribution, and flex rendering. | | `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. | +| `ebox-surface.el` | Pure candidate identity and projection from Ebox layout output to generic TP surface plans. | | `ebox-buffer-backend.el` | Text properties, display spaces/borders, markers, extents, replacement, and buffer mutation. | | `ebox-incremental.el` | Runtime state, snapshots, dirty planning, owner escalation, atomic publication, and reports. | | `ebox-dsl.el` | Data-oriented `.ebox` forms and lowering to public nodes. | @@ -32,7 +33,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri The package intentionally does not include application Components, UI controls, reactive data, or a playground implementation. Those are sibling-package responsibilities. -The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, and `scripts/ebox-visual-check.el`. +The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, and `scripts/ebox-visual-check.el`. ## Runtime model @@ -45,6 +46,7 @@ Source Tree -> Box/Formatting Context -> Measurement + Render Context -> Layout Fragment/Snapshot + -> Pure TP Surface Plan -> Dirty/Patch Plan -> Emacs Buffer Backend ``` @@ -56,6 +58,7 @@ Source Tree | Measurement | `ebox-measure.el` | Application state or dirty policy. | | Formatting Context | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Buffer edits. | | Fragment/Snapshot | `ebox-fragment.el`, `ebox-incremental.el` | Source parsing or identity allocation. | +| Surface Projection | `ebox-surface.el` | Buffer mutation or Ebox layout decisions. | | Dirty/Patch | `ebox-incremental.el` | Raw measurement or direct buffer edits. | | Buffer Backend | `ebox-buffer-backend.el` | Style semantics or application state. | @@ -70,6 +73,7 @@ Source Tree - Buffer coordinates belong to the generation that produced them and must be refreshed after mutation. - Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness. - Loading Ebox never builds or installs the optional Rust module. +- The Phase 7 projector assigns TP candidate identity before layout, emits plans containing no markers, positions, buffers, or TP runtime handles, and is byte-for-byte and property-for-property equivalent to `ebox-render`. Public live entry points still use the existing Ebox backend until the next cutover phase. ## Grid contract @@ -88,6 +92,7 @@ make check make core-tests make grid-tests make ebox-commit-tests +make surface-tests make visual-check-tests make package-tests make selector-tests diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index 6280014..a0d5a86 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -24,6 +24,7 @@ | `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 | | `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 | | `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 | +| `ebox-surface.el` | Ebox 候选 identity,以及从布局输出到通用 TP surface plan 的纯投影。 | | `ebox-buffer-backend.el` | text property、display space/border、marker、extent、替换和 buffer 变更。 | | `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、原子发布和报告。 | | `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 | @@ -32,7 +33,7 @@ 本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界;Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。 -active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el` 和 `scripts/ebox-visual-check.el`。 +active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el` 和 `scripts/ebox-visual-check.el`。 ## 运行时模型 @@ -45,6 +46,7 @@ Source Tree -> Box/Formatting Context -> Measurement + Render Context -> Layout Fragment/Snapshot + -> 纯 TP Surface Plan -> Dirty/Patch Plan -> Emacs Buffer Backend ``` @@ -56,6 +58,7 @@ Source Tree | Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 | | Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 | | Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 | +| Surface Projection | `ebox-surface.el` | Buffer 变更或 Ebox 布局决策。 | | Dirty/Patch | `ebox-incremental.el` | 原始测量或直接 buffer 编辑。 | | Buffer Backend | `ebox-buffer-backend.el` | 样式语义或应用状态。 | @@ -70,6 +73,7 @@ Source Tree - Buffer 坐标属于生成它的 generation,变更后必须重新获取。 - Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。 - 加载 Ebox 不会构建或安装可选 Rust 模块。 +- Phase 7 projector 在布局前分配 TP candidate identity,输出不含 marker、position、buffer 或 TP runtime handle 的 plan,并与 `ebox-render` 保持逐字节、逐属性等价。公开 live 入口在下一阶段切换前仍使用现有 Ebox backend。 ## Grid 合同 @@ -86,6 +90,7 @@ make check make core-tests make grid-tests make ebox-commit-tests +make surface-tests make visual-check-tests make package-tests make selector-tests diff --git a/ebox-incremental.el b/ebox-incremental.el index a9b3900..a4f7392 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -1205,49 +1205,47 @@ node, parent, region, Host, or native-layout indexes." (plist-put state :selector-index-stale-p nil))) state)) +(defun ebox--new-buffer-render-state (root-node) + "Return fresh buffer-independent runtime state for ROOT-NODE." + (list :root-node root-node + :viewport-width ebox-viewport-width + :viewport-height ebox-viewport-height + :display-signature (ebox--current-display-signature) + :layout-snapshots (make-hash-table :test 'equal) + :layout-snapshots-complete-p nil + :layout-snapshot-detail-generation 0 + :runtime-revision 0 :last-update-report nil + :render-cache (make-hash-table :test 'equal) + :detached-identity-history + (ebox-incremental--make-detached-history + :table (make-hash-table :test 'equal) + :order nil :node-count 0) + :render-signature-cache (make-hash-table :test 'eq) + :flex-content-min-widths (make-hash-table :test 'eq) + :viewport-height-dependent-subtree-cache + (make-hash-table :test 'eq) + :region-role-span-table nil)) + +(defun ebox--render-state-install-index (state index) + "Install runtime INDEX tables into candidate STATE and return STATE." + (dolist (key '(:node-table :parent-table :region-id-set :region-node-table + :region-box-count-table :region-box-table :host-ref-table + :selector-id-table :selector-class-table + :selector-type-table :native-node-postorder)) + (setq state (plist-put state key (plist-get index key)))) + state) + +(defun ebox--make-buffer-render-state (root-node) + "Return unpublished complete runtime state for ROOT-NODE." + (ebox--render-state-install-index + (ebox--new-buffer-render-state root-node) + (ebox--runtime-index root-node t))) + (defun ebox--set-buffer-render-state (buffer root-node) "Store ROOT-NODE and current render context for BUFFER." - (let ((index (ebox--runtime-index root-node t))) - (puthash buffer - (list :root-node root-node - :viewport-width ebox-viewport-width - :viewport-height ebox-viewport-height - :display-signature (ebox--current-display-signature) - :layout-snapshots (make-hash-table :test 'equal) - :layout-snapshots-complete-p nil - :layout-snapshot-detail-generation 0 - :runtime-revision 0 - :last-update-report nil - :render-cache (make-hash-table :test 'equal) - :detached-identity-history - (ebox-incremental--make-detached-history - :table (make-hash-table :test 'equal) - :order nil - :node-count 0) - :render-signature-cache (make-hash-table :test 'eq) - :flex-content-min-widths (make-hash-table :test 'eq) - :viewport-height-dependent-subtree-cache - (make-hash-table :test 'eq) - :region-role-span-table nil - :node-table (plist-get index :node-table) - :parent-table (plist-get index :parent-table) - :region-id-set (plist-get index :region-id-set) - :region-node-table (plist-get index :region-node-table) - :region-box-count-table - (plist-get index :region-box-count-table) - :region-box-table - (plist-get index :region-box-table) - :host-ref-table (plist-get index :host-ref-table) - :selector-id-table - (plist-get index :selector-id-table) - :selector-class-table - (plist-get index :selector-class-table) - :selector-type-table - (plist-get index :selector-type-table) - :native-node-postorder - (plist-get index :native-node-postorder)) - ebox--buffer-render-state-table) - (ebox--buffer-viewport-dependent-node-id-axes buffer))) + (puthash buffer (ebox--make-buffer-render-state root-node) + ebox--buffer-render-state-table) + (ebox--buffer-viewport-dependent-node-id-axes buffer)) (defun ebox--buffer-node-table (buffer) "Return BUFFER's runtime node table." diff --git a/ebox-surface.el b/ebox-surface.el new file mode 100644 index 0000000..4bbea24 --- /dev/null +++ b/ebox-surface.el @@ -0,0 +1,231 @@ +;;; ebox-surface.el --- Pure retained surface projection -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; Owns the pure boundary from an Ebox candidate runtime and rendered layout to +;; a generic TP surface plan. It never publishes or scans a live buffer. + +;;; Code: + +(require 'cl-lib) +(require 'ebox-tree) +(require 'ebox-layout) +(require 'ebox-incremental) +(require 'tp-surface) + +(defvar ebox-region-types) +(defvar ebox--region-box-table) +(defvar ebox--scroll-global-state) +(defvar ebox--scroll-idle-prefetch-timers) +(defvar ebox--smooth-scroll-state-table) + +(declare-function ebox-render "ebox-layout" (node)) +(declare-function ebox--scroll-schedule-idle-prefetch + "ebox" (region-id &optional delay)) + +(defconst ebox-surface--root-key 'ebox/surface + "Stable TP key for an Ebox surface projection root.") + +(defconst ebox-surface--nodes-key 'ebox/nodes + "Stable TP key for the retained logical Ebox node tree.") + +(defconst ebox-surface--fragments-key 'ebox/fragments + "Stable TP key for linear rendered fragments.") + +(defun ebox-surface--candidate-root (source previous-state) + "Return an isolated runtime copy of SOURCE reconciled with PREVIOUS-STATE." + (unless (and (listp source) (not (stringp source))) + (error "Ebox surface source must be an Ebox node")) + (ebox-tree-validate-declarative-root source) + (let ((candidate + (ebox-tree-clear-runtime-identities + (ebox-tree-copy-node-structure source)))) + (when-let ((previous-root (plist-get previous-state :root-node))) + (ebox-tree-reconcile-runtime previous-root candidate)) + candidate)) + +(defun ebox-surface--node-key (node) + "Return NODE's namespaced sibling key, or nil for positional identity." + (when-let ((key (plist-get node :key))) + (list 'ebox/key key))) + +(defun ebox-surface--node-kind (node) + "Return the TP kind discriminator for Ebox NODE." + (list 'ebox/node (plist-get node :ebox-type))) + +(defun ebox-surface--ensure-node-tree (context parent node table) + "Ensure NODE and descendants below PARENT in CONTEXT and fill TABLE." + (let ((object + (tp-object-ensure context parent + (ebox-surface--node-key node) + (ebox-surface--node-kind node)))) + (tp-object-retain context object) + (plist-put node :surface-object object) + (puthash node object table) + (dolist (child (ebox-tree--children-raw node)) + (ebox-surface--ensure-node-tree context object child table)) + object)) + +(defun ebox-surface--node-object-table (objects-by-node) + "Return a node-id keyed table from OBJECTS-BY-NODE." + (let ((table (make-hash-table :test 'equal))) + (maphash + (lambda (node object) + (let ((node-id (plist-get node :node-id))) + (unless node-id + (error "Ebox rendered node has no runtime identity")) + (puthash node-id object table))) + objects-by-node) + table)) + +(defun ebox-surface--region-object-table (state node-objects) + "Return region-to-object table from candidate STATE and NODE-OBJECTS." + (let ((table (make-hash-table :test 'equal))) + (maphash + (lambda (region-id node-id) + (let ((object (gethash node-id node-objects))) + (unless object + (error "Ebox region %S has no candidate surface object" region-id)) + (puthash region-id object table))) + (plist-get state :region-node-table)) + table)) + +(defun ebox-surface--hash-keys (table) + "Return TABLE keys in unspecified order." + (let (keys) + (maphash (lambda (key _value) (push key keys)) table) + keys)) + +(defun ebox-surface--render-candidate (state) + "Render candidate STATE in isolated Ebox side tables." + (let ((scroll-table (make-hash-table :test 'equal)) + (timer-table (make-hash-table :test 'equal)) + (smooth-table (make-hash-table :test 'equal))) + (let ((ebox--region-box-table (plist-get state :region-box-table)) + (ebox--scroll-global-state scroll-table) + (ebox--scroll-idle-prefetch-timers timer-table) + (ebox--smooth-scroll-state-table smooth-table)) + (cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch) + (lambda (&rest _) nil))) + (prog1 (ebox-render (plist-get state :root-node)) + (plist-put state :scroll-state-table scroll-table) + (plist-put state :scroll-region-ids + (ebox-surface--hash-keys scroll-table))))))) + +(defun ebox-surface--finish-runtime-state (state) + "Install post-layout runtime indexes into candidate STATE." + (ebox--render-state-install-index + state (ebox--runtime-index (plist-get state :root-node) t))) + +(defun ebox-surface--role-ids-at (rendered position) + "Return namespaced Ebox role/id pairs at POSITION in RENDERED." + (let (roles) + (dolist (region-id + (get-text-property position 'ebox-content-owners rendered)) + (cl-pushnew (cons 'content-owner region-id) roles :test #'equal)) + (dolist (entry ebox-region-types) + (when-let ((region-id + (get-text-property position (cdr entry) rendered))) + (cl-pushnew (cons (car entry) region-id) roles :test #'equal))) + (nreverse roles))) + +(defun ebox-surface--region-role-tags (region-id role-ids) + "Return projection tags for REGION-ID from ROLE-IDS." + (list :ebox/region-id region-id + :ebox/roles + (cl-loop for (role . owner) in role-ids + when (equal owner region-id) collect role))) + +(defun ebox-surface--attach-fragment + (context fragment role-ids region-objects) + "Attach FRAGMENT to ROLE-IDS owners through REGION-OBJECTS in CONTEXT." + (dolist (region-id (delete-dups (mapcar #'cdr role-ids))) + (let ((object (gethash region-id region-objects))) + (unless object + (error "Ebox output references unknown region %S" region-id)) + (tp-object-attach-fragment + context object fragment + (ebox-surface--region-role-tags region-id role-ids))))) + +(defun ebox-surface--fragment-plan + (context parent rendered start end index region-objects) + "Return one plan below PARENT for RENDERED START..END at INDEX in CONTEXT." + (let* ((key (cons 'ebox/fragment index)) + (object (tp-object-ensure context parent key 'ebox/fragment)) + (role-ids (ebox-surface--role-ids-at rendered start)) + (tags (list :ebox/fragment index :ebox/role-ids role-ids))) + (ebox-surface--attach-fragment + context object role-ids region-objects) + (tp-surface-plan-create + :key key :kind 'ebox/fragment + :text (substring rendered start end) :tags tags + :capability 'content))) + +(defun ebox-surface--fragment-plans + (context parent rendered region-objects) + "Return property-interval plans in CONTEXT for RENDERED below PARENT." + (let ((position 0) + (limit (length rendered)) + (index 0) + plans) + (while (< position limit) + (let ((next (or (next-property-change position rendered limit) limit))) + (push (ebox-surface--fragment-plan + context parent rendered position next index region-objects) + plans) + (setq position (max next (1+ position)) + index (1+ index)))) + (nreverse plans))) + +(defun ebox-surface--surface-plan + (context surface-root rendered region-objects) + "Return CONTEXT's plan below SURFACE-ROOT for RENDERED and REGION-OBJECTS." + (let* ((fragment-root + (tp-object-ensure context surface-root + ebox-surface--fragments-key 'ebox/fragments)) + (fragments + (ebox-surface--fragment-plans + context fragment-root rendered region-objects))) + (tp-surface-plan-create + :key ebox-surface--root-key :kind 'ebox/surface + :children + (list (tp-surface-plan-create + :key ebox-surface--fragments-key :kind 'ebox/fragments + :children fragments :capability 'content)) + :capability 'content))) + +(defun ebox-surface--project (context source previous-state) + "Project SOURCE in CONTEXT using optional PREVIOUS-STATE." + (let* ((root (ebox-surface--candidate-root source previous-state)) + (state (ebox--new-buffer-render-state root)) + (surface-root + (tp-object-ensure context nil ebox-surface--root-key 'ebox/surface)) + (node-root + (tp-object-ensure context surface-root + ebox-surface--nodes-key 'ebox/nodes)) + (objects-by-node (make-hash-table :test 'eq))) + (tp-object-retain context node-root) + (ebox-surface--ensure-node-tree context node-root root objects-by-node) + (plist-put state :region-box-table (make-hash-table :test 'equal)) + (let ((rendered (ebox-surface--render-candidate state))) + (setq state (ebox-surface--finish-runtime-state state)) + (let* ((node-objects (ebox-surface--node-object-table objects-by-node)) + (region-objects + (ebox-surface--region-object-table state node-objects))) + (plist-put state :surface-node-object-table node-objects) + (plist-put state :region-surface-object-table region-objects) + (tp-surface-result-create + (ebox-surface--surface-plan + context surface-root rendered region-objects) + state))))) + +(defun ebox-surface--producer (source &optional previous-state) + "Return a TP producer for Ebox SOURCE and optional PREVIOUS-STATE." + (lambda (context) + (ebox-surface--project context source previous-state))) + +(provide 'ebox-surface) + +;;; ebox-surface.el ends here diff --git a/ebox-tree.el b/ebox-tree.el index b8a6394..7b8975c 100644 --- a/ebox-tree.el +++ b/ebox-tree.el @@ -372,13 +372,15 @@ and all explicit identities are compared with `equal'. Return ROOT on success." (when (eq (plist-get node :ebox-type) 'box) (when (plist-member node :region-id) (plist-put node :region-id nil))) + (when (plist-member node :surface-object) + (plist-put node :surface-object nil)) (dolist (child (ebox-tree--children-raw node)) (visit child))))) (visit root)) root) (defconst ebox-tree--runtime-source-keys - '(:node-id :region-id :render-cache) + '(:node-id :region-id :surface-object :render-cache) "Runtime-owned plist keys excluded from declarative source signatures.") (defconst ebox-tree--child-source-keys diff --git a/ebox.el b/ebox.el index 84c5571..d89f5c3 100644 --- a/ebox.el +++ b/ebox.el @@ -2,7 +2,7 @@ ;; Author: ebox contributors ;; Version: 1.0.1 -;; Package-Requires: ((emacs "29.1")) +;; Package-Requires: ((emacs "29.1") (tp "1.0.0")) ;; Keywords: ui, graphics, convenience ;; URL: https://github.com/ginqi7/ebox @@ -26,7 +26,8 @@ '("ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" - "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el") + "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" + "ebox-native-reflow.el") "Active Ebox Lisp sources compiled by `ebox-byte-compile'.") (add-to-list 'load-path ebox--directory) @@ -46,6 +47,7 @@ (require 'ebox-grid) (require 'ebox-buffer-backend) (require 'ebox-incremental) +(require 'ebox-surface) (require 'ebox-dsl) (require 'ebox-selector) diff --git a/tests/ebox-ci-contract-tests.el b/tests/ebox-ci-contract-tests.el index 3b128e7..a493be4 100644 --- a/tests/ebox-ci-contract-tests.el +++ b/tests/ebox-ci-contract-tests.el @@ -17,6 +17,7 @@ (let ((source (ebox-ci-test--read "Makefile"))) (dolist (target '("check:" "ci:" "load:" "compile:" "test:" "core-tests:" "grid-tests:" "ebox-commit-tests:" + "surface-tests:" "docs-contract-tests:" "ci-contract-tests:" "native-rust-tests:" "native-build:" "diff-check:")) @@ -44,6 +45,7 @@ "GitHub Actions should call the same local checks as maintainers." (let ((source (ebox-ci-test--read ".github/workflows/ci.yml"))) (should (string-match-p "emacs-nox" source)) + (should (string-match-p "Kinneyzhang/tp" source)) (should (string-match-p "make ci" source)) (should (string-match-p (regexp-quote "cargo +stable test") source)) (should (string-match-p (regexp-quote "cargo +1.82.0 test") source)))) diff --git a/tests/ebox-docs-contract-tests.el b/tests/ebox-docs-contract-tests.el index 7866cdf..1013e23 100644 --- a/tests/ebox-docs-contract-tests.el +++ b/tests/ebox-docs-contract-tests.el @@ -20,10 +20,10 @@ "ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" - "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-dsl.el" + "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox-native-reflow.el" "tests/ebox-core-render-tests.el" "tests/ebox-grid-tests.el" - "tests/ebox-commit-tests.el" + "tests/ebox-commit-tests.el" "tests/ebox-surface-tests.el" "tests/ebox-dsl-tests.el" "tests/ebox-flex-tests.el" "tests/ebox-selector-tests.el" "tests/ebox-package-tests.el" "tests/ebox-visual-check-tests.el" "tests/ebox-docs-contract-tests.el" @@ -36,7 +36,7 @@ (defconst ebox-docs-test--targets '("check" "ci" "load" "compile" "core-tests" "grid-tests" - "ebox-commit-tests" "visual-check-tests" "package-tests" + "ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests" "selector-tests" "dsl-tests" "flex-tests" "docs-contract-tests" "ci-contract-tests" "visual-check" "native-rust-tests" "native-build" "package-lint" "diff-check") diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 346d711..697e058 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -49,7 +49,7 @@ (dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure ebox-fragment ebox-layout ebox-flex ebox-grid ebox-buffer-backend ebox-incremental - ebox-dsl ebox-selector)) + ebox-surface ebox-dsl ebox-selector)) (should (featurep feature)))) (ert-deftest ebox-facade-exposes-native-workflow-without-loading-it () @@ -67,9 +67,18 @@ '("ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" - "ebox-dsl.el" "ebox-selector.el" "ebox.el" + "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el")))) +(ert-deftest ebox-surface-depends-only-on-public-tp-api () + "The Ebox projection boundary should never call private TP symbols." + (let ((source + (with-temp-buffer + (insert-file-contents + (expand-file-name "ebox-surface.el" ebox-test--root)) + (buffer-string)))) + (should-not (string-match-p "\\_ (length (tp-object-mounts logical)) 1)))) + (when (and surface (tp-surface-live-p surface)) + (tp-surface-unmount surface)) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + +(ert-deftest ebox-surface-reuses-one-source-with-isolated-runtime-state () + "One source description should mount into two independent TP surfaces." + (ebox-surface-test--reset-render-state) + (let* ((source + (ebox-create :key 'shared :content "Shared" :width '(100) + :bgcolor "#E2E8F0" :color "#0F172A")) + (first-buffer (generate-new-buffer " *ebox-surface-first*")) + (second-buffer (generate-new-buffer " *ebox-surface-second*")) + first second) + (unwind-protect + (progn + (setq first + (tp-surface-mount + first-buffer (ebox-surface--producer source) + '(:capability content))) + (setq second + (tp-surface-mount + second-buffer (ebox-surface--producer source) + '(:capability content))) + (let* ((first-state (tp-surface-client-state first)) + (second-state (tp-surface-client-state second)) + (first-root (plist-get first-state :root-node)) + (second-root (plist-get second-state :root-node))) + (should-not (eq first-root second-root)) + (should-not (eq (plist-get first-root :surface-object) + (plist-get second-root :surface-object))) + (should-not (plist-member source :node-id)) + (should-not (plist-member source :region-id)) + (should-not (plist-member source :surface-object)))) + (dolist (surface (list first second)) + (when (and surface (tp-surface-live-p surface)) + (tp-surface-unmount surface))) + (dolist (buffer (list first-buffer second-buffer)) + (when (buffer-live-p buffer) + (kill-buffer buffer)))))) + +(ert-deftest ebox-surface-keyed-reorder-retains-logical-objects () + "A keyed child reorder should retain TP objects through a new Ebox runtime." + (ebox-surface-test--reset-render-state) + (let ((buffer (generate-new-buffer " *ebox-surface-reorder*")) + surface) + (unwind-protect + (let* ((first-source + (ebox-row + (ebox-create :key 'left :content "Left" :width '(60)) + (ebox-create :key 'right :content "Right" :width '(60)))) + (_mount + (setq surface + (tp-surface-mount + buffer (ebox-surface--producer first-source) + '(:capability content)))) + (first-state (tp-surface-client-state surface)) + (left (ebox-surface-test--object-by-key first-state 'left)) + (right (ebox-surface-test--object-by-key first-state 'right)) + (next-source + (ebox-row + (ebox-create :key 'right :content "Right!" :width '(60)) + (ebox-create :key 'left :content "Left!" :width '(60))))) + (tp-surface-update + surface (ebox-surface--producer next-source first-state)) + (let ((next-state (tp-surface-client-state surface))) + (should (eq left + (ebox-surface-test--object-by-key next-state 'left))) + (should (eq right + (ebox-surface-test--object-by-key next-state 'right))) + (should (string-match-p + "Right!.*Left!" + (substring-no-properties + (with-current-buffer buffer (buffer-string))))))) + (when (and surface (tp-surface-live-p surface)) + (tp-surface-unmount surface)) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + +(provide 'ebox-surface-tests) + +;;; ebox-surface-tests.el ends here