feat(ebox): project layouts into TP surface plans
Assign retained TP identity before layout and emit pure, runtime-free surface plans with exact character and text-property equivalence. Keep live publication unchanged for the staged cutover and add focused surface, package, docs, and CI contracts.\n\nVerified: make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs\nVerified: WERROR byte compilation for all 16 active Lisp files\nVerified: focused ebox-surface checkdoc has zero warnings
This commit is contained in:
parent
bfef45f765
commit
fad54d7fb1
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -12,6 +12,9 @@ jobs:
|
|||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox design
|
# 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
|
## Design principles
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ node tree
|
|||||||
-> measured formatting context
|
-> measured formatting context
|
||||||
-> layout fragments and snapshot
|
-> layout fragments and snapshot
|
||||||
-> render context
|
-> 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
|
## Public boundary
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox 设计
|
# Ebox 设计
|
||||||
|
|
||||||
Ebox 是底层渲染引擎。它负责把声明式节点树转换为经过测量的布局 fragment,再发布到 Emacs buffer。它有意小于应用框架。
|
Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过测量的布局 fragment 与通用 TP surface plan。它有意小于应用框架。
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ Ebox 是底层渲染引擎。它负责把声明式节点树转换为经过测量
|
|||||||
-> 经过测量的 formatting context
|
-> 经过测量的 formatting context
|
||||||
-> 布局 fragment 与 snapshot
|
-> 布局 fragment 与 snapshot
|
||||||
-> render context
|
-> 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 后会删除这套发布执行器。
|
||||||
|
|
||||||
## 公共边界
|
## 公共边界
|
||||||
|
|
||||||
|
|||||||
11
Makefile
11
Makefile
@ -1,12 +1,14 @@
|
|||||||
EMACS ?= emacs
|
EMACS ?= emacs
|
||||||
CARGO ?= cargo
|
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_MANIFEST = native/Cargo.toml
|
||||||
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
|
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
|
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
|
all: check
|
||||||
|
|
||||||
@ -21,7 +23,7 @@ compile:
|
|||||||
rm -f *.elc tests/*.elc scripts/*.elc
|
rm -f *.elc tests/*.elc scripts/*.elc
|
||||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t)' -l ebox.el --eval '(ebox-byte-compile)'
|
$(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:
|
core-tests:
|
||||||
$(EMACS_BATCH) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit
|
$(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:
|
ebox-commit-tests:
|
||||||
$(EMACS_BATCH) -l tests/ebox-commit-tests.el -f ert-run-tests-batch-and-exit
|
$(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:
|
visual-check-tests:
|
||||||
$(EMACS_BATCH) -l tests/ebox-visual-check-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_BATCH) -l tests/ebox-visual-check-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,10 @@ Ebox is a standalone, pixel-precise box and layout engine for Emacs. It provides
|
|||||||
|
|
||||||
## Install and use
|
## 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
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/tp")
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
(add-to-list 'load-path "/path/to/github/ebox")
|
||||||
(require '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.el` | Public facade and package entry point. |
|
||||||
| `ebox-style.el`, `ebox-tree.el`, `ebox-measure.el` | Style, tree, and measurement models. |
|
| `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-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-buffer-backend.el`, `ebox-incremental.el` | Buffer publication and incremental updates. |
|
||||||
| `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and tree/runtime queries. |
|
| `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and tree/runtime queries. |
|
||||||
| `ebox-native-reflow.el`, `native/` | Optional native reflow boundary. |
|
| `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
|
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.
|
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.
|
||||||
|
|||||||
@ -4,9 +4,10 @@ Ebox 是一个独立的 Emacs 像素级 Box 与布局引擎。它是同级 [ETAF
|
|||||||
|
|
||||||
## 安装与使用
|
## 安装与使用
|
||||||
|
|
||||||
将本目录放到 `load-path`:
|
先安装 TP,再通过包管理器安装 Ebox,让声明的依赖自动解析。使用同级源码 checkout 时,把两个目录都加入 `load-path`:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/github/tp")
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
(add-to-list 'load-path "/path/to/github/ebox")
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ ETAF 属于同级独立包。当你需要统一 View 语法、Component、响应
|
|||||||
| `ebox.el` | 公共门面与包入口。 |
|
| `ebox.el` | 公共门面与包入口。 |
|
||||||
| `ebox-style.el`、`ebox-tree.el`、`ebox-measure.el` | 样式、树和测量模型。 |
|
| `ebox-style.el`、`ebox-tree.el`、`ebox-measure.el` | 样式、树和测量模型。 |
|
||||||
| `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Formatting Context 与布局算法。 |
|
| `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-buffer-backend.el`、`ebox-incremental.el` | Buffer 发布与增量更新。 |
|
||||||
| `ebox-dsl.el`、`ebox-selector.el` | 独立 DSL 与树/runtime 查询。 |
|
| `ebox-dsl.el`、`ebox-selector.el` | 独立 DSL 与树/runtime 查询。 |
|
||||||
| `ebox-native-reflow.el`、`native/` | 可选 native reflow 边界。 |
|
| `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 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)。
|
公共构造示例见 [Ebox 用户指南](docs/user/ebox-user-guide.zh.md),职责边界与不变量见 [当前实现参考](docs/maintainer/ebox-current-implementation-reference.zh.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-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-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-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-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-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. |
|
| `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 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
|
## Runtime model
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ Source Tree
|
|||||||
-> Box/Formatting Context
|
-> Box/Formatting Context
|
||||||
-> Measurement + Render Context
|
-> Measurement + Render Context
|
||||||
-> Layout Fragment/Snapshot
|
-> Layout Fragment/Snapshot
|
||||||
|
-> Pure TP Surface Plan
|
||||||
-> Dirty/Patch Plan
|
-> Dirty/Patch Plan
|
||||||
-> Emacs Buffer Backend
|
-> Emacs Buffer Backend
|
||||||
```
|
```
|
||||||
@ -56,6 +58,7 @@ Source Tree
|
|||||||
| Measurement | `ebox-measure.el` | Application state or dirty policy. |
|
| Measurement | `ebox-measure.el` | Application state or dirty policy. |
|
||||||
| Formatting Context | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Buffer edits. |
|
| 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. |
|
| 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. |
|
| Dirty/Patch | `ebox-incremental.el` | Raw measurement or direct buffer edits. |
|
||||||
| Buffer Backend | `ebox-buffer-backend.el` | Style semantics or application state. |
|
| 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.
|
- 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.
|
- 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.
|
- 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
|
## Grid contract
|
||||||
|
|
||||||
@ -88,6 +92,7 @@ make check
|
|||||||
make core-tests
|
make core-tests
|
||||||
make grid-tests
|
make grid-tests
|
||||||
make ebox-commit-tests
|
make ebox-commit-tests
|
||||||
|
make surface-tests
|
||||||
make visual-check-tests
|
make visual-check-tests
|
||||||
make package-tests
|
make package-tests
|
||||||
make selector-tests
|
make selector-tests
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
| `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 |
|
| `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 |
|
||||||
| `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 |
|
| `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 |
|
||||||
| `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 |
|
| `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-buffer-backend.el` | text property、display space/border、marker、extent、替换和 buffer 变更。 |
|
||||||
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、原子发布和报告。 |
|
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、原子发布和报告。 |
|
||||||
| `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 |
|
| `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 |
|
||||||
@ -32,7 +33,7 @@
|
|||||||
|
|
||||||
本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界;Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。
|
本包有意不包含应用 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
|
-> Box/Formatting Context
|
||||||
-> Measurement + Render Context
|
-> Measurement + Render Context
|
||||||
-> Layout Fragment/Snapshot
|
-> Layout Fragment/Snapshot
|
||||||
|
-> 纯 TP Surface Plan
|
||||||
-> Dirty/Patch Plan
|
-> Dirty/Patch Plan
|
||||||
-> Emacs Buffer Backend
|
-> Emacs Buffer Backend
|
||||||
```
|
```
|
||||||
@ -56,6 +58,7 @@ Source Tree
|
|||||||
| Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 |
|
| Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 |
|
||||||
| Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 |
|
| Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 |
|
||||||
| Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 |
|
| Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 |
|
||||||
|
| Surface Projection | `ebox-surface.el` | Buffer 变更或 Ebox 布局决策。 |
|
||||||
| Dirty/Patch | `ebox-incremental.el` | 原始测量或直接 buffer 编辑。 |
|
| Dirty/Patch | `ebox-incremental.el` | 原始测量或直接 buffer 编辑。 |
|
||||||
| Buffer Backend | `ebox-buffer-backend.el` | 样式语义或应用状态。 |
|
| Buffer Backend | `ebox-buffer-backend.el` | 样式语义或应用状态。 |
|
||||||
|
|
||||||
@ -70,6 +73,7 @@ Source Tree
|
|||||||
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
||||||
- Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。
|
- Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。
|
||||||
- 加载 Ebox 不会构建或安装可选 Rust 模块。
|
- 加载 Ebox 不会构建或安装可选 Rust 模块。
|
||||||
|
- Phase 7 projector 在布局前分配 TP candidate identity,输出不含 marker、position、buffer 或 TP runtime handle 的 plan,并与 `ebox-render` 保持逐字节、逐属性等价。公开 live 入口在下一阶段切换前仍使用现有 Ebox backend。
|
||||||
|
|
||||||
## Grid 合同
|
## Grid 合同
|
||||||
|
|
||||||
@ -86,6 +90,7 @@ make check
|
|||||||
make core-tests
|
make core-tests
|
||||||
make grid-tests
|
make grid-tests
|
||||||
make ebox-commit-tests
|
make ebox-commit-tests
|
||||||
|
make surface-tests
|
||||||
make visual-check-tests
|
make visual-check-tests
|
||||||
make package-tests
|
make package-tests
|
||||||
make selector-tests
|
make selector-tests
|
||||||
|
|||||||
@ -1205,10 +1205,8 @@ node, parent, region, Host, or native-layout indexes."
|
|||||||
(plist-put state :selector-index-stale-p nil)))
|
(plist-put state :selector-index-stale-p nil)))
|
||||||
state))
|
state))
|
||||||
|
|
||||||
(defun ebox--set-buffer-render-state (buffer root-node)
|
(defun ebox--new-buffer-render-state (root-node)
|
||||||
"Store ROOT-NODE and current render context for BUFFER."
|
"Return fresh buffer-independent runtime state for ROOT-NODE."
|
||||||
(let ((index (ebox--runtime-index root-node t)))
|
|
||||||
(puthash buffer
|
|
||||||
(list :root-node root-node
|
(list :root-node root-node
|
||||||
:viewport-width ebox-viewport-width
|
:viewport-width ebox-viewport-width
|
||||||
:viewport-height ebox-viewport-height
|
:viewport-height ebox-viewport-height
|
||||||
@ -1216,38 +1214,38 @@ node, parent, region, Host, or native-layout indexes."
|
|||||||
:layout-snapshots (make-hash-table :test 'equal)
|
:layout-snapshots (make-hash-table :test 'equal)
|
||||||
:layout-snapshots-complete-p nil
|
:layout-snapshots-complete-p nil
|
||||||
:layout-snapshot-detail-generation 0
|
:layout-snapshot-detail-generation 0
|
||||||
:runtime-revision 0
|
:runtime-revision 0 :last-update-report nil
|
||||||
:last-update-report nil
|
|
||||||
:render-cache (make-hash-table :test 'equal)
|
:render-cache (make-hash-table :test 'equal)
|
||||||
:detached-identity-history
|
:detached-identity-history
|
||||||
(ebox-incremental--make-detached-history
|
(ebox-incremental--make-detached-history
|
||||||
:table (make-hash-table :test 'equal)
|
:table (make-hash-table :test 'equal)
|
||||||
:order nil
|
:order nil :node-count 0)
|
||||||
:node-count 0)
|
|
||||||
:render-signature-cache (make-hash-table :test 'eq)
|
:render-signature-cache (make-hash-table :test 'eq)
|
||||||
:flex-content-min-widths (make-hash-table :test 'eq)
|
:flex-content-min-widths (make-hash-table :test 'eq)
|
||||||
:viewport-height-dependent-subtree-cache
|
:viewport-height-dependent-subtree-cache
|
||||||
(make-hash-table :test 'eq)
|
(make-hash-table :test 'eq)
|
||||||
:region-role-span-table nil
|
:region-role-span-table nil))
|
||||||
:node-table (plist-get index :node-table)
|
|
||||||
:parent-table (plist-get index :parent-table)
|
(defun ebox--render-state-install-index (state index)
|
||||||
:region-id-set (plist-get index :region-id-set)
|
"Install runtime INDEX tables into candidate STATE and return STATE."
|
||||||
:region-node-table (plist-get index :region-node-table)
|
(dolist (key '(:node-table :parent-table :region-id-set :region-node-table
|
||||||
:region-box-count-table
|
:region-box-count-table :region-box-table :host-ref-table
|
||||||
(plist-get index :region-box-count-table)
|
:selector-id-table :selector-class-table
|
||||||
:region-box-table
|
:selector-type-table :native-node-postorder))
|
||||||
(plist-get index :region-box-table)
|
(setq state (plist-put state key (plist-get index key))))
|
||||||
:host-ref-table (plist-get index :host-ref-table)
|
state)
|
||||||
:selector-id-table
|
|
||||||
(plist-get index :selector-id-table)
|
(defun ebox--make-buffer-render-state (root-node)
|
||||||
:selector-class-table
|
"Return unpublished complete runtime state for ROOT-NODE."
|
||||||
(plist-get index :selector-class-table)
|
(ebox--render-state-install-index
|
||||||
:selector-type-table
|
(ebox--new-buffer-render-state root-node)
|
||||||
(plist-get index :selector-type-table)
|
(ebox--runtime-index root-node t)))
|
||||||
:native-node-postorder
|
|
||||||
(plist-get index :native-node-postorder))
|
(defun ebox--set-buffer-render-state (buffer root-node)
|
||||||
|
"Store ROOT-NODE and current render context for BUFFER."
|
||||||
|
(puthash buffer (ebox--make-buffer-render-state root-node)
|
||||||
ebox--buffer-render-state-table)
|
ebox--buffer-render-state-table)
|
||||||
(ebox--buffer-viewport-dependent-node-id-axes buffer)))
|
(ebox--buffer-viewport-dependent-node-id-axes buffer))
|
||||||
|
|
||||||
(defun ebox--buffer-node-table (buffer)
|
(defun ebox--buffer-node-table (buffer)
|
||||||
"Return BUFFER's runtime node table."
|
"Return BUFFER's runtime node table."
|
||||||
|
|||||||
231
ebox-surface.el
Normal file
231
ebox-surface.el
Normal file
@ -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
|
||||||
@ -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 (eq (plist-get node :ebox-type) 'box)
|
||||||
(when (plist-member node :region-id)
|
(when (plist-member node :region-id)
|
||||||
(plist-put node :region-id nil)))
|
(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))
|
(dolist (child (ebox-tree--children-raw node))
|
||||||
(visit child)))))
|
(visit child)))))
|
||||||
(visit root))
|
(visit root))
|
||||||
root)
|
root)
|
||||||
|
|
||||||
(defconst ebox-tree--runtime-source-keys
|
(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.")
|
"Runtime-owned plist keys excluded from declarative source signatures.")
|
||||||
|
|
||||||
(defconst ebox-tree--child-source-keys
|
(defconst ebox-tree--child-source-keys
|
||||||
|
|||||||
6
ebox.el
6
ebox.el
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
;; Author: ebox contributors
|
;; Author: ebox contributors
|
||||||
;; Version: 1.0.1
|
;; Version: 1.0.1
|
||||||
;; Package-Requires: ((emacs "29.1"))
|
;; Package-Requires: ((emacs "29.1") (tp "1.0.0"))
|
||||||
;; Keywords: ui, graphics, convenience
|
;; Keywords: ui, graphics, convenience
|
||||||
;; URL: https://github.com/ginqi7/ebox
|
;; URL: https://github.com/ginqi7/ebox
|
||||||
|
|
||||||
@ -26,7 +26,8 @@
|
|||||||
'("ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el"
|
'("ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el"
|
||||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.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-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'.")
|
"Active Ebox Lisp sources compiled by `ebox-byte-compile'.")
|
||||||
|
|
||||||
(add-to-list 'load-path ebox--directory)
|
(add-to-list 'load-path ebox--directory)
|
||||||
@ -46,6 +47,7 @@
|
|||||||
(require 'ebox-grid)
|
(require 'ebox-grid)
|
||||||
(require 'ebox-buffer-backend)
|
(require 'ebox-buffer-backend)
|
||||||
(require 'ebox-incremental)
|
(require 'ebox-incremental)
|
||||||
|
(require 'ebox-surface)
|
||||||
(require 'ebox-dsl)
|
(require 'ebox-dsl)
|
||||||
(require 'ebox-selector)
|
(require 'ebox-selector)
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
(let ((source (ebox-ci-test--read "Makefile")))
|
(let ((source (ebox-ci-test--read "Makefile")))
|
||||||
(dolist (target '("check:" "ci:" "load:" "compile:" "test:"
|
(dolist (target '("check:" "ci:" "load:" "compile:" "test:"
|
||||||
"core-tests:" "grid-tests:" "ebox-commit-tests:"
|
"core-tests:" "grid-tests:" "ebox-commit-tests:"
|
||||||
|
"surface-tests:"
|
||||||
"docs-contract-tests:"
|
"docs-contract-tests:"
|
||||||
"ci-contract-tests:" "native-rust-tests:"
|
"ci-contract-tests:" "native-rust-tests:"
|
||||||
"native-build:" "diff-check:"))
|
"native-build:" "diff-check:"))
|
||||||
@ -44,6 +45,7 @@
|
|||||||
"GitHub Actions should call the same local checks as maintainers."
|
"GitHub Actions should call the same local checks as maintainers."
|
||||||
(let ((source (ebox-ci-test--read ".github/workflows/ci.yml")))
|
(let ((source (ebox-ci-test--read ".github/workflows/ci.yml")))
|
||||||
(should (string-match-p "emacs-nox" source))
|
(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 "make ci" source))
|
||||||
(should (string-match-p (regexp-quote "cargo +stable test") source))
|
(should (string-match-p (regexp-quote "cargo +stable test") source))
|
||||||
(should (string-match-p (regexp-quote "cargo +1.82.0 test") source))))
|
(should (string-match-p (regexp-quote "cargo +1.82.0 test") source))))
|
||||||
|
|||||||
@ -20,10 +20,10 @@
|
|||||||
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-tree.el"
|
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-tree.el"
|
||||||
"ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el"
|
"ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el"
|
||||||
"ebox-layout.el" "ebox-flex.el" "ebox-grid.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"
|
"ebox-selector.el" "ebox-native-reflow.el"
|
||||||
"tests/ebox-core-render-tests.el" "tests/ebox-grid-tests.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-dsl-tests.el" "tests/ebox-flex-tests.el"
|
||||||
"tests/ebox-selector-tests.el" "tests/ebox-package-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-visual-check-tests.el" "tests/ebox-docs-contract-tests.el"
|
||||||
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
(defconst ebox-docs-test--targets
|
(defconst ebox-docs-test--targets
|
||||||
'("check" "ci" "load" "compile" "core-tests" "grid-tests"
|
'("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"
|
"selector-tests" "dsl-tests" "flex-tests"
|
||||||
"docs-contract-tests" "ci-contract-tests" "visual-check"
|
"docs-contract-tests" "ci-contract-tests" "visual-check"
|
||||||
"native-rust-tests" "native-build" "package-lint" "diff-check")
|
"native-rust-tests" "native-build" "package-lint" "diff-check")
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
(dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure
|
(dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure
|
||||||
ebox-fragment ebox-layout ebox-flex ebox-grid
|
ebox-fragment ebox-layout ebox-flex ebox-grid
|
||||||
ebox-buffer-backend ebox-incremental
|
ebox-buffer-backend ebox-incremental
|
||||||
ebox-dsl ebox-selector))
|
ebox-surface ebox-dsl ebox-selector))
|
||||||
(should (featurep feature))))
|
(should (featurep feature))))
|
||||||
|
|
||||||
(ert-deftest ebox-facade-exposes-native-workflow-without-loading-it ()
|
(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-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el"
|
||||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.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-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"))))
|
"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 "\\_<tp--" source))))
|
||||||
|
|
||||||
(ert-deftest ebox-byte-compile-rebuilds-the-declared-source-set ()
|
(ert-deftest ebox-byte-compile-rebuilds-the-declared-source-set ()
|
||||||
"The interactive command should recompile every declared source in order."
|
"The interactive command should recompile every declared source in order."
|
||||||
(require 'bytecomp)
|
(require 'bytecomp)
|
||||||
|
|||||||
294
tests/ebox-surface-tests.el
Normal file
294
tests/ebox-surface-tests.el
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
;;; ebox-surface-tests.el --- TP surface projection tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ert)
|
||||||
|
|
||||||
|
(unless load-file-name
|
||||||
|
(error "This test file must be loaded from disk, not eval'ed directly"))
|
||||||
|
|
||||||
|
(setq load-prefer-newer t)
|
||||||
|
|
||||||
|
(load-file (expand-file-name "../ebox.el"
|
||||||
|
(file-name-directory load-file-name)))
|
||||||
|
(require 'tp-surface)
|
||||||
|
|
||||||
|
(defun ebox-surface-test--reset-render-state ()
|
||||||
|
"Reset render identities and side tables used by projection tests."
|
||||||
|
(setq ebox--region-id-counter 0
|
||||||
|
ebox--runtime-node-id-counter 0)
|
||||||
|
(dolist (table (list ebox--region-box-table
|
||||||
|
ebox--scroll-global-state
|
||||||
|
ebox--rendered-root-metadata-table))
|
||||||
|
(clrhash table)))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--interactive-content ()
|
||||||
|
"Return fresh interactive propertized content for projection tests."
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map [mouse-1] #'ignore)
|
||||||
|
(propertize "Open" 'keymap map 'mouse-face 'highlight
|
||||||
|
'help-echo "Open this item")))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--fixtures ()
|
||||||
|
"Return named fresh layout builders covering active Ebox layout kinds."
|
||||||
|
(list
|
||||||
|
(cons 'box
|
||||||
|
(lambda ()
|
||||||
|
(ebox-create :key 'box :content (ebox-surface-test--interactive-content)
|
||||||
|
:width '(120) :padding '(1 (4))
|
||||||
|
:border "#334155" :bgcolor "#E2E8F0"
|
||||||
|
:color "#0F172A")))
|
||||||
|
(cons 'row-column
|
||||||
|
(lambda ()
|
||||||
|
(ebox-column
|
||||||
|
(ebox-row
|
||||||
|
(ebox-create :key 'left :content "Left" :width '(70)
|
||||||
|
:bgcolor "#DBEAFE" :color "#172554")
|
||||||
|
(ebox-create :key 'right :content "Right\nDetail" :width '(90)
|
||||||
|
:bgcolor "#DCFCE7" :color "#14532D"))
|
||||||
|
(ebox-create :key 'footer :content "Footer" :width '(160)
|
||||||
|
:bgcolor "#F1F5F9" :color "#0F172A"))))
|
||||||
|
(cons 'flex
|
||||||
|
(lambda ()
|
||||||
|
(ebox-flex
|
||||||
|
:width '(210) :flex-wrap 'wrap :column-gap '(10)
|
||||||
|
(ebox-flex-item
|
||||||
|
(ebox-create :key 'grow :content "Grow" :width '(80)
|
||||||
|
:bgcolor "#EDE9FE" :color "#2E1065")
|
||||||
|
:flex-grow 1 :flex-basis '(80))
|
||||||
|
(ebox-flex-item
|
||||||
|
(ebox-create :key 'fixed :content "Fixed" :width '(120)
|
||||||
|
:bgcolor "#FFEDD5" :color "#7C2D12")))))
|
||||||
|
(cons 'grid
|
||||||
|
(lambda ()
|
||||||
|
(ebox-grid
|
||||||
|
:width '(220) :grid-template-columns '((70) 1fr)
|
||||||
|
:grid-template-rows '(2) :gap '(1 (8))
|
||||||
|
:border "#475569"
|
||||||
|
(ebox-create :key 'grid-left :content "A\nAA"
|
||||||
|
:bgcolor "#E0F2FE" :color "#0C4A6E")
|
||||||
|
(ebox-create :key 'grid-right :content "B\nBB"
|
||||||
|
:bgcolor "#FCE7F3" :color "#831843"))))
|
||||||
|
(cons 'overflow-scroll
|
||||||
|
(lambda ()
|
||||||
|
(ebox-create :key 'scroll :content "zero\none\ntwo\nthree"
|
||||||
|
:width '(100) :height 2 :overflow 'scroll
|
||||||
|
:bgcolor "#1E293B" :color "#F8FAFC")))))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--render-fresh (builder projector-p)
|
||||||
|
"Render BUILDER after a reset, using the TP projector when PROJECTOR-P."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((ebox-viewport-width 240)
|
||||||
|
(ebox-viewport-height 12)
|
||||||
|
(node (funcall builder)))
|
||||||
|
(if projector-p
|
||||||
|
(tp-surface-materialize-string
|
||||||
|
(ebox-surface--producer node))
|
||||||
|
(ebox-render node))))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--walk-runtime (node function)
|
||||||
|
"Call FUNCTION for every runtime NODE in preorder."
|
||||||
|
(funcall function node)
|
||||||
|
(dolist (child (ebox-tree-node-children node))
|
||||||
|
(ebox-surface-test--walk-runtime child function)))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--plan-runtime-value-p (value)
|
||||||
|
"Return non-nil when VALUE is forbidden runtime state in a pure plan."
|
||||||
|
(cond
|
||||||
|
((or (markerp value) (bufferp value) (tp-object-p value)
|
||||||
|
(tp-binding-p value) (tp-surface-p value))
|
||||||
|
t)
|
||||||
|
((consp value)
|
||||||
|
(or (ebox-surface-test--plan-runtime-value-p (car value))
|
||||||
|
(ebox-surface-test--plan-runtime-value-p (cdr value))))
|
||||||
|
((vectorp value)
|
||||||
|
(cl-some #'ebox-surface-test--plan-runtime-value-p value))
|
||||||
|
(t nil)))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--plan-pure-p (plan)
|
||||||
|
"Return non-nil when PLAN contains only pure projection data."
|
||||||
|
(and
|
||||||
|
(not (cl-some
|
||||||
|
#'ebox-surface-test--plan-runtime-value-p
|
||||||
|
(list (tp-surface-plan-key plan)
|
||||||
|
(tp-surface-plan-kind plan)
|
||||||
|
(tp-surface-plan-text plan)
|
||||||
|
(tp-surface-plan-props plan)
|
||||||
|
(tp-surface-plan-tags plan))))
|
||||||
|
(cl-every #'ebox-surface-test--plan-pure-p
|
||||||
|
(tp-surface-plan-children plan))))
|
||||||
|
|
||||||
|
(defun ebox-surface-test--object-by-key (state key)
|
||||||
|
"Return the candidate surface object for Ebox node KEY in STATE."
|
||||||
|
(let (object)
|
||||||
|
(maphash
|
||||||
|
(lambda (_node-id node)
|
||||||
|
(when (equal (plist-get node :key) key)
|
||||||
|
(setq object (plist-get node :surface-object))))
|
||||||
|
(plist-get state :node-table))
|
||||||
|
object))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-projects-every-layout-with-exact-equivalence ()
|
||||||
|
"TP projection should preserve every character and text property interval."
|
||||||
|
(dolist (fixture (ebox-surface-test--fixtures))
|
||||||
|
(let* ((builder (cdr fixture))
|
||||||
|
(expected (ebox-surface-test--render-fresh builder nil))
|
||||||
|
(actual (ebox-surface-test--render-fresh builder t)))
|
||||||
|
(should (equal-including-properties actual expected))
|
||||||
|
(should (equal (mapcar #'ebox--string-pixel-width
|
||||||
|
(ebox-string-lines actual))
|
||||||
|
(mapcar #'ebox--string-pixel-width
|
||||||
|
(ebox-string-lines expected)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-assigns-object-identity-before-layout ()
|
||||||
|
"Every candidate runtime node should own a TP object before layout starts."
|
||||||
|
(let ((original (symbol-function 'ebox-render))
|
||||||
|
checked captured)
|
||||||
|
(cl-letf (((symbol-function 'ebox-render)
|
||||||
|
(lambda (node)
|
||||||
|
(unless checked
|
||||||
|
(setq checked t)
|
||||||
|
(ebox-surface-test--walk-runtime
|
||||||
|
node
|
||||||
|
(lambda (runtime-node)
|
||||||
|
(let ((object (plist-get runtime-node :surface-object)))
|
||||||
|
(should (tp-object-p object))
|
||||||
|
(push object captured)))))
|
||||||
|
(funcall original node))))
|
||||||
|
(ebox-surface-test--render-fresh
|
||||||
|
(cdr (assq 'grid (ebox-surface-test--fixtures))) t))
|
||||||
|
(should checked)
|
||||||
|
(should captured)
|
||||||
|
(dolist (object captured)
|
||||||
|
(should-not (tp-object-live-p object)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-plan-stays-free-of-runtime-state ()
|
||||||
|
"The generic surface plan should not contain markers or runtime handles."
|
||||||
|
(let ((original (symbol-function 'tp-surface-result-create))
|
||||||
|
captured)
|
||||||
|
(cl-letf (((symbol-function 'tp-surface-result-create)
|
||||||
|
(lambda (plan &optional client-state)
|
||||||
|
(setq captured plan)
|
||||||
|
(funcall original plan client-state))))
|
||||||
|
(ebox-surface-test--render-fresh
|
||||||
|
(cdr (assq 'box (ebox-surface-test--fixtures))) t))
|
||||||
|
(should (tp-surface-plan-p captured))
|
||||||
|
(should (ebox-surface-test--plan-pure-p captured))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-projection-does-not-mutate-a-buffer ()
|
||||||
|
"Pure projection should not call any final buffer mutation primitive."
|
||||||
|
(cl-letf (((symbol-function 'insert)
|
||||||
|
(lambda (&rest _) (error "Unexpected buffer insertion")))
|
||||||
|
((symbol-function 'erase-buffer)
|
||||||
|
(lambda (&rest _) (error "Unexpected buffer erase")))
|
||||||
|
((symbol-function 'delete-region)
|
||||||
|
(lambda (&rest _) (error "Unexpected buffer deletion")))
|
||||||
|
((symbol-function 'replace-region-contents)
|
||||||
|
(lambda (&rest _) (error "Unexpected buffer replacement"))))
|
||||||
|
(should
|
||||||
|
(stringp
|
||||||
|
(ebox-surface-test--render-fresh
|
||||||
|
(cdr (assq 'row-column (ebox-surface-test--fixtures))) t)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-logical-box-owns-disjoint-render-fragments ()
|
||||||
|
"One logical Ebox box should resolve all of its separated painted regions."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-surface-fragments*"))
|
||||||
|
surface)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(setq surface
|
||||||
|
(tp-surface-mount
|
||||||
|
buffer
|
||||||
|
(ebox-surface--producer
|
||||||
|
(funcall (cdr (assq 'box (ebox-surface-test--fixtures)))))
|
||||||
|
'(:capability content)))
|
||||||
|
(let* ((state (tp-surface-client-state surface))
|
||||||
|
(objects (plist-get state :region-surface-object-table))
|
||||||
|
logical)
|
||||||
|
(maphash (lambda (_region-id object)
|
||||||
|
(unless logical (setq logical object)))
|
||||||
|
objects)
|
||||||
|
(should (tp-object-live-p logical))
|
||||||
|
(should (> (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
|
||||||
Loading…
Reference in New Issue
Block a user