Complete retained Ebox publication and scrolling
This commit is contained in:
parent
972126e239
commit
3fd63989e8
@ -22,7 +22,7 @@ node tree
|
|||||||
-> pure TP surface plan
|
-> pure TP surface plan
|
||||||
```
|
```
|
||||||
|
|
||||||
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 retained publication, and `ebox-incremental.el` for dirty-owner planning. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary.
|
The owners are `ebox-child-range.el` for immutable child-sequence and key indexes, `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 retained publication, and `ebox-incremental.el` for dirty-owner planning. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary.
|
||||||
|
|
||||||
## Public boundary
|
## Public boundary
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
|
|||||||
-> 纯 TP surface plan
|
-> 纯 TP surface plan
|
||||||
```
|
```
|
||||||
|
|
||||||
各层 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 投影与 retained 发布,`ebox-incremental.el` 负责 dirty owner 规划。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string;所有公共 live buffer 发布路径都经过 TP surface 边界。
|
各层 owner 是:`ebox-child-range.el` 负责不可变 child sequence 与 key index,`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 投影与 retained 发布,`ebox-incremental.el` 负责 dirty owner 规划。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string;所有公共 live buffer 发布路径都经过 TP surface 边界。
|
||||||
|
|
||||||
## 公共边界
|
## 公共边界
|
||||||
|
|
||||||
|
|||||||
9
Makefile
9
Makefile
@ -1,7 +1,7 @@
|
|||||||
EMACS ?= emacs
|
EMACS ?= emacs
|
||||||
CARGO ?= cargo
|
CARGO ?= cargo
|
||||||
TP_DIR ?= ../tp
|
TP_DIR ?= ../tp
|
||||||
ECSS_DIR ?= ../new-architecture/ecss
|
ECSS_DIR ?= ../ecss
|
||||||
LOAD_EXTRA ?=
|
LOAD_EXTRA ?=
|
||||||
|
|
||||||
EMACS_BATCH = $(EMACS) -Q --batch -L . -L $(ECSS_DIR) -L $(TP_DIR) $(LOAD_EXTRA) --eval '(setq load-prefer-newer t)'
|
EMACS_BATCH = $(EMACS) -Q --batch -L . -L $(ECSS_DIR) -L $(TP_DIR) $(LOAD_EXTRA) --eval '(setq load-prefer-newer t)'
|
||||||
@ -9,7 +9,7 @@ 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 surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests performance-evaluator 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 child-range-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 performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
||||||
|
|
||||||
all: check
|
all: check
|
||||||
|
|
||||||
@ -24,11 +24,14 @@ 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 surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
|
test: core-tests child-range-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
|
||||||
|
|
||||||
|
child-range-tests:
|
||||||
|
$(EMACS_BATCH) -l tests/ebox-child-range-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
grid-tests:
|
grid-tests:
|
||||||
$(EMACS_BATCH) -l tests/ebox-grid-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_BATCH) -l tests/ebox-grid-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,8 @@ ECSS and TP are independent packages and may be installed in either order. Insta
|
|||||||
|
|
||||||
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` materializes propertized text without publishing buffer state (using isolated pure materialization when no cascade/inheritance dependency requires a TP object tree); `ebox-render-to-buffer` mounts a retained TP surface; `ebox-display-buffer` displays that same retained-surface path; and `ebox-commit` atomically updates the mount from a fresh root tree. Ebox copies declarative input before assigning runtime identity, so one source tree may be mounted in multiple buffers without transferring ownership. It exposes no separate public macro that erases a live buffer and evaluates arbitrary body forms.
|
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` materializes propertized text without publishing buffer state (using isolated pure materialization when no cascade/inheritance dependency requires a TP object tree); `ebox-render-to-buffer` mounts a retained TP surface; `ebox-display-buffer` displays that same retained-surface path; and `ebox-commit` atomically updates the mount from a fresh root tree. Ebox copies declarative input before assigning runtime identity, so one source tree may be mounted in multiple buffers without transferring ownership. It exposes no separate public macro that erases a live buffer and evaluates arbitrary body forms.
|
||||||
|
|
||||||
|
Material child lists may contain `ebox-child-range` descriptors, and logical candidates update their base-bound addresses with `ebox-candidate-replace-range-ref`. Candidates may also replace the private mounted root with `ebox-candidate-replace-root`; the last root replacement absorbs descendant operations and never shares an address with `:host-ref`. Reports expose `:range-metrics`, framework participant diagnostics, and scroll finalization diagnostics as read-only snapshots.
|
||||||
|
|
||||||
CSS-like selector strings are compiled to ECSS's structured selector AST. Ebox supplies logical node relations and indexed candidates; ECSS is the only selector matcher and cascade engine for tree queries, rendered-buffer queries, and style rules.
|
CSS-like selector strings are compiled to ECSS's structured selector AST. Ebox supplies logical node relations and indexed candidates; ECSS is the only selector matcher and cascade engine for tree queries, rendered-buffer queries, and style rules.
|
||||||
|
|
||||||
## What belongs here
|
## What belongs here
|
||||||
@ -41,7 +43,7 @@ ETAF belongs in the sibling package. Install it separately when you need the uni
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ebox.el` | Public facade and package entry point. |
|
| `ebox.el` | Public facade and package entry point. |
|
||||||
| `ebox-cache.el`, `ebox-style.el`, `ebox-tree.el`, `ebox-measure.el` | Cache, style, tree, and measurement models. |
|
| `ebox-cache.el`, `ebox-style.el`, `ebox-tree.el`, `ebox-child-range.el`, `ebox-measure.el` | Cache, style, tree, persistent child-sequence, and measurement models. |
|
||||||
| `ebox-fragment.el`, `ebox-render-context.el` | Layout facts and render-local context. |
|
| `ebox-fragment.el`, `ebox-render-context.el` | Layout facts and render-local context. |
|
||||||
| `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` | Ebox candidate projection plus TP surface mount/update and atomic Ebox runtime-state participation. |
|
| `ebox-surface.el` | Ebox candidate projection plus TP surface mount/update and atomic Ebox runtime-state participation. |
|
||||||
|
|||||||
@ -24,6 +24,8 @@ ECSS 与 TP 是互相独立的包,安装顺序任意。两者都安装后再
|
|||||||
|
|
||||||
公共模型是一棵 Ebox 节点树。`ebox-create` 构造叶子或包装节点;`ebox-column`、`ebox-row`、`ebox-flex` 和 `ebox-grid` 组合节点;`ebox-render` 生成带属性文本但不发布 buffer state(没有 cascade/inheritance 依赖时使用隔离的 pure materialization);`ebox-render-to-buffer` 挂载 retained TP surface;`ebox-display-buffer` 展示同一条 retained-surface 路径;`ebox-commit` 使用新的根树原子更新该 mount。Ebox 会在分配 runtime identity 前复制声明式输入,因此同一 source tree 可以挂载到多个 buffer,而不转移所有权。公共 API 不再提供另一套擦除 live buffer 后执行任意 BODY 的宏。
|
公共模型是一棵 Ebox 节点树。`ebox-create` 构造叶子或包装节点;`ebox-column`、`ebox-row`、`ebox-flex` 和 `ebox-grid` 组合节点;`ebox-render` 生成带属性文本但不发布 buffer state(没有 cascade/inheritance 依赖时使用隔离的 pure materialization);`ebox-render-to-buffer` 挂载 retained TP surface;`ebox-display-buffer` 展示同一条 retained-surface 路径;`ebox-commit` 使用新的根树原子更新该 mount。Ebox 会在分配 runtime identity 前复制声明式输入,因此同一 source tree 可以挂载到多个 buffer,而不转移所有权。公共 API 不再提供另一套擦除 live buffer 后执行任意 BODY 的宏。
|
||||||
|
|
||||||
|
Material child list 可包含 `ebox-child-range` descriptor,logical candidate 用 `ebox-candidate-replace-range-ref` 更新其 base-bound 地址。Candidate 也可用 `ebox-candidate-replace-root` 替换私有 mounted root;最后一次 root replacement 会吸收 descendant operation,并且不会与 `:host-ref` 共用地址。报告通过 `:range-metrics`、framework participant diagnostics 和 scroll finalization diagnostics 暴露只读快照。
|
||||||
|
|
||||||
CSS-like selector 字符串会编译为 ECSS 的 structured selector AST。Ebox 提供逻辑节点关系与索引候选;对于 tree query、rendered-buffer query 和样式规则,ECSS 是唯一 selector matcher 与 cascade engine。
|
CSS-like selector 字符串会编译为 ECSS 的 structured selector AST。Ebox 提供逻辑节点关系与索引候选;对于 tree query、rendered-buffer query 和样式规则,ECSS 是唯一 selector matcher 与 cascade engine。
|
||||||
|
|
||||||
## 本包负责什么
|
## 本包负责什么
|
||||||
@ -41,7 +43,7 @@ ETAF 属于同级独立包。当你需要统一 View 语法、Component、响应
|
|||||||
| 路径 | 职责 |
|
| 路径 | 职责 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ebox.el` | 公共门面与包入口。 |
|
| `ebox.el` | 公共门面与包入口。 |
|
||||||
| `ebox-cache.el`、`ebox-style.el`、`ebox-tree.el`、`ebox-measure.el` | 缓存、样式、树和测量模型。 |
|
| `ebox-cache.el`、`ebox-style.el`、`ebox-tree.el`、`ebox-child-range.el`、`ebox-measure.el` | 缓存、样式、树、持久 child sequence 和测量模型。 |
|
||||||
| `ebox-fragment.el`、`ebox-render-context.el` | 布局事实与 render-local context。 |
|
| `ebox-fragment.el`、`ebox-render-context.el` | 布局事实与 render-local context。 |
|
||||||
| `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,并负责 retained mount/update 与原子 runtime-state 参与。 |
|
| `ebox-surface.el` | 将 Ebox runtime/layout 投影为 TP surface plan,并负责 retained mount/update 与原子 runtime-state 参与。 |
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox current implementation reference
|
# Ebox current implementation reference
|
||||||
|
|
||||||
This is the maintainer entry point for the standalone Ebox repository. It describes the package boundary, active files, runtime model, invariants, and verification commands. The historical `emacs-box` checkout is a separate legacy source tree; it is not a dependency of this package. ETAF is the sibling higher-level package.
|
This is the maintainer entry point for the standalone Ebox repository. It describes the package boundary, active files, runtime model, invariants, and verification commands. The historical Ebox checkout is a separate legacy source tree; it is not a dependency of this package. ETAF is the sibling higher-level package.
|
||||||
|
|
||||||
## Reading order
|
## Reading order
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
|
|||||||
| `ebox-cache.el` | Measurement/render cache records, invalidation, and cache reports. |
|
| `ebox-cache.el` | Measurement/render cache records, invalidation, and cache reports. |
|
||||||
| `ebox-style.el` | ECSS property schemas, declarations and cascade, shorthand expansion, computed style, colors, borders, and dirty effects. |
|
| `ebox-style.el` | ECSS property schemas, declarations and cascade, shorthand expansion, computed style, colors, borders, and dirty effects. |
|
||||||
| `ebox-tree.el` | Node traversal, logical child access, ECSS subject adaptation, identity, parent paths, keys, and tree snapshots. |
|
| `ebox-tree.el` | Node traversal, logical child access, ECSS subject adaptation, identity, parent paths, keys, and tree snapshots. |
|
||||||
|
| `ebox-child-range.el` | Immutable weighted segment trie, sparse persistent key trie, Range replacement, and Gate A metrics. |
|
||||||
| `ebox-measure.el` | Display-sensitive character/face/pixel measurement and measurement caches. |
|
| `ebox-measure.el` | Display-sensitive character/face/pixel measurement and measurement caches. |
|
||||||
| `ebox-fragment.el` | Layout fragments, signatures, snapshots, spans, and dirty-kind facts. |
|
| `ebox-fragment.el` | Layout fragments, signatures, snapshots, spans, and dirty-kind facts. |
|
||||||
| `ebox-render-context.el` | Render-local context and publication inputs. |
|
| `ebox-render-context.el` | Render-local context and publication inputs. |
|
||||||
@ -35,7 +36,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-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`, and `scripts/ebox-performance-evaluator.el`.
|
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-child-range-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`, and `scripts/ebox-performance-evaluator.el`.
|
||||||
|
|
||||||
## Runtime model
|
## Runtime model
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ Caller-owned Source Tree
|
|||||||
|
|
||||||
| Model | Owner | Must not own |
|
| Model | Owner | Must not own |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Source/Element Tree | `ebox-tree.el`, `ebox-dsl.el` | Published buffer mutation. |
|
| Source/Element Tree | `ebox-child-range.el`, `ebox-tree.el`, `ebox-dsl.el` | Published buffer mutation. |
|
||||||
| Computed Style | `ebox-style.el` | Layout identity or patch execution. |
|
| Computed Style | `ebox-style.el` | Layout identity or patch execution. |
|
||||||
| 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. |
|
||||||
@ -100,6 +101,7 @@ make load
|
|||||||
make compile
|
make compile
|
||||||
make check
|
make check
|
||||||
make core-tests
|
make core-tests
|
||||||
|
make child-range-tests
|
||||||
make grid-tests
|
make grid-tests
|
||||||
make ebox-commit-tests
|
make ebox-commit-tests
|
||||||
make surface-tests
|
make surface-tests
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox 当前实现参考
|
# Ebox 当前实现参考
|
||||||
|
|
||||||
本文是独立 Ebox 仓库的维护者入口,描述包边界、active 文件、运行时模型、不变量和验证命令。历史 `emacs-box` 是另一个旧架构源码树,不是本包依赖。ETAF 是同级高层包。
|
本文是独立 Ebox 仓库的维护者入口,描述包边界、active 文件、运行时模型、不变量和验证命令。历史 Ebox checkout 是另一个旧架构源码树,不是本包依赖。ETAF 是同级高层包。
|
||||||
|
|
||||||
## 阅读顺序
|
## 阅读顺序
|
||||||
|
|
||||||
@ -20,6 +20,7 @@
|
|||||||
| `ebox-cache.el` | 测量/渲染缓存记录、失效和缓存报告。 |
|
| `ebox-cache.el` | 测量/渲染缓存记录、失效和缓存报告。 |
|
||||||
| `ebox-style.el` | ECSS property schema、declaration 与 cascade、shorthand 展开、computed style、颜色、border 和 dirty effect。 |
|
| `ebox-style.el` | ECSS property schema、declaration 与 cascade、shorthand 展开、computed style、颜色、border 和 dirty effect。 |
|
||||||
| `ebox-tree.el` | 节点遍历、逻辑子节点访问、ECSS subject 适配、identity、父路径、key 和树 snapshot。 |
|
| `ebox-tree.el` | 节点遍历、逻辑子节点访问、ECSS subject 适配、identity、父路径、key 和树 snapshot。 |
|
||||||
|
| `ebox-child-range.el` | 不可变 weighted segment trie、稀疏持久 key trie、Range replacement 与 Gate A metrics。 |
|
||||||
| `ebox-measure.el` | display 敏感的字符、face、像素测量与测量缓存。 |
|
| `ebox-measure.el` | display 敏感的字符、face、像素测量与测量缓存。 |
|
||||||
| `ebox-fragment.el` | 布局 fragment、signature、snapshot、span 和 dirty kind 事实。 |
|
| `ebox-fragment.el` | 布局 fragment、signature、snapshot、span 和 dirty kind 事实。 |
|
||||||
| `ebox-render-context.el` | render-local context 与发布输入。 |
|
| `ebox-render-context.el` | render-local context 与发布输入。 |
|
||||||
@ -35,7 +36,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-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` 和 `scripts/ebox-performance-evaluator.el`。
|
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-child-range-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` 和 `scripts/ebox-performance-evaluator.el`。
|
||||||
|
|
||||||
## 运行时模型
|
## 运行时模型
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
|||||||
|
|
||||||
| 模型 | Owner | 不得拥有 |
|
| 模型 | Owner | 不得拥有 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Source/Element Tree | `ebox-tree.el`、`ebox-dsl.el` | 已发布 buffer 的变更。 |
|
| Source/Element Tree | `ebox-child-range.el`、`ebox-tree.el`、`ebox-dsl.el` | 已发布 buffer 的变更。 |
|
||||||
| Computed Style | `ebox-style.el` | 布局 identity 或 patch 执行。 |
|
| Computed Style | `ebox-style.el` | 布局 identity 或 patch 执行。 |
|
||||||
| 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 编辑。 |
|
||||||
@ -98,6 +99,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
|||||||
```sh
|
```sh
|
||||||
make check
|
make check
|
||||||
make core-tests
|
make core-tests
|
||||||
|
make child-range-tests
|
||||||
make grid-tests
|
make grid-tests
|
||||||
make ebox-commit-tests
|
make ebox-commit-tests
|
||||||
make surface-tests
|
make surface-tests
|
||||||
|
|||||||
@ -43,6 +43,20 @@ When selected, the candidate keeps the published Ebox topology and reuses the re
|
|||||||
|
|
||||||
If any proof condition fails, the update uses the ordinary safe projection path. That fallback may do broader node projection and TP reconciliation, but it preserves the same output, identity, publication, and rollback contract. Tests in `tests/ebox-surface-tests.el` cover the retained width/height/both-axis path, each unsafe fallback trigger, and publication failure after candidate preparation.
|
If any proof condition fails, the update uses the ordinary safe projection path. That fallback may do broader node projection and TP reconciliation, but it preserves the same output, identity, publication, and rollback contract. Tests in `tests/ebox-surface-tests.el` cover the retained width/height/both-axis path, each unsafe fallback trigger, and publication failure after candidate preparation.
|
||||||
|
|
||||||
|
## Retained scroll window
|
||||||
|
|
||||||
|
For a chrome-free root scroll owner whose cached rendered lines cover the
|
||||||
|
current visible window (the lazy prefix may remain incomplete),
|
||||||
|
the scroll patch joins the staged visible window directly instead of entering
|
||||||
|
the full candidate layout. It reuses the published cascade, viewport axes,
|
||||||
|
object maps, and descendant `region-box-table`; it only advances the scroll
|
||||||
|
offset and installs the root `ebox-scroll-window` property. The proof is
|
||||||
|
conservative: a cache miss, chrome, active style dependency, topology change,
|
||||||
|
or incomplete window falls back to the ordinary retained path. Scroll
|
||||||
|
publication still goes through TP and the Ebox participant, so a failure after
|
||||||
|
TP client-state publication restores text, offsets, indexes, objects, mounts,
|
||||||
|
and report together.
|
||||||
|
|
||||||
## Identity and coordinates
|
## Identity and coordinates
|
||||||
|
|
||||||
Keys are local to a sibling collection. Declarative source trees never own live TP objects, markers, or buffer coordinates. A live logical `:id` resolves through `ebox-region-resolve` to an opaque handle tied to one TP surface object; mounting the same source in two buffers therefore yields distinct handles. Region and host-ref positions are generation-bound; after a buffer mutation, callers must obtain fresh positions from the public accessor. Buffer markers, display spans, and text properties are publication facts, not source-tree identity.
|
Keys are local to a sibling collection. Declarative source trees never own live TP objects, markers, or buffer coordinates. A live logical `:id` resolves through `ebox-region-resolve` to an opaque handle tied to one TP surface object; mounting the same source in two buffers therefore yields distinct handles. Region and host-ref positions are generation-bound; after a buffer mutation, callers must obtain fresh positions from the public accessor. Buffer markers, display spans, and text properties are publication facts, not source-tree identity.
|
||||||
|
|||||||
@ -43,6 +43,17 @@
|
|||||||
|
|
||||||
任意 proof 条件失败时,更新必须使用普通安全 projection 路径。该 fallback 可以执行更宽的 node projection 和 TP reconciliation,但必须保持相同的输出、identity、publication 和 rollback 合同。`tests/ebox-surface-tests.el` 覆盖 retained width/height/both-axis 路径、每个不安全 fallback 触发条件,以及 candidate preparation 之后的 publication failure。
|
任意 proof 条件失败时,更新必须使用普通安全 projection 路径。该 fallback 可以执行更宽的 node projection 和 TP reconciliation,但必须保持相同的输出、identity、publication 和 rollback 合同。`tests/ebox-surface-tests.el` 覆盖 retained width/height/both-axis 路径、每个不安全 fallback 触发条件,以及 candidate preparation 之后的 publication failure。
|
||||||
|
|
||||||
|
## 保留式 scroll window
|
||||||
|
|
||||||
|
对于 chrome-free、缓存 rendered lines 已覆盖当前可见窗口的 root scroll owner
|
||||||
|
(lazy prefix 可以仍未完整 materialize),scroll patch 会
|
||||||
|
直接从 staged visible window 拼接输出,不再进入完整 candidate layout。它复用已
|
||||||
|
发布的 cascade、viewport axes、object maps 和 descendant `region-box-table`,只推进
|
||||||
|
scroll offset 并安装 root `ebox-scroll-window` 属性。证明保持保守:cache miss、
|
||||||
|
chrome、active style dependency、topology 变化或窗口不完整时,回退到普通 retained
|
||||||
|
路径。Scroll publication 仍经过 TP 与 Ebox participant;即使 TP client-state 发布
|
||||||
|
之后失败,也会一起恢复文本、offset、index、object、mount 和 report。
|
||||||
|
|
||||||
## Identity 与坐标
|
## Identity 与坐标
|
||||||
|
|
||||||
Key 只在同级兄弟集合内有效。声明式 source tree 不拥有 live TP object、marker 或 buffer 坐标。live 逻辑 `:id` 通过 `ebox-region-resolve` 解析为绑定某个 TP surface object 的不透明 handle;同一 source 挂载到两个 buffer 时会得到不同 handle。Region 与 host-ref 位置绑定于 generation;buffer 变更后调用者必须通过公共 accessor 重新获得位置。Buffer marker、display span 和 text property 是发布事实,不是 source tree identity。
|
Key 只在同级兄弟集合内有效。声明式 source tree 不拥有 live TP object、marker 或 buffer 坐标。live 逻辑 `:id` 通过 `ebox-region-resolve` 解析为绑定某个 TP surface object 的不透明 handle;同一 source 挂载到两个 buffer 时会得到不同 handle。Region 与 host-ref 位置绑定于 generation;buffer 变更后调用者必须通过公共 accessor 重新获得位置。Buffer marker、display span 和 text property 是发布事实,不是 source tree identity。
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox Performance Architecture Diagnosis and Redesign Direction
|
# Ebox Performance Architecture Diagnosis and Redesign Direction
|
||||||
|
|
||||||
> Status: Architecture diagnosis and implementation checkpoint. This document records the performance gap between the current refactored `ebox` and the historical `emacs-box`, the evidence boundary, and the high-level redesign direction. Five narrow slices are implemented; the retained mounted-update architecture is not yet considered generally repaired.
|
> Status: Architecture diagnosis and implementation checkpoint. This document records the performance gap between the current refactored `ebox` and the historical Ebox checkout, the evidence boundary, and the high-level redesign direction. Five narrow slices are implemented; the retained mounted-update architecture is not yet considered generally repaired.
|
||||||
>
|
>
|
||||||
> Date: 2026-08-07
|
> Date: 2026-08-07
|
||||||
>
|
>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|
||||||
Why does the current refactored Ebox fail to match the performance of the pre-refactor `emacs-box` during dynamic content updates, viewport resize, and pure `ebox-render`? The data-flow and ownership causes must be established before optimizing individual functions.
|
Why does the current refactored Ebox fail to match the performance of the pre-refactor historical Ebox checkout during dynamic content updates, viewport resize, and pure `ebox-render`? The data-flow and ownership causes must be established before optimizing individual functions.
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ The key distinction is that current’s “local patch” describes publication
|
|||||||
## Scope and terminology
|
## Scope and terminology
|
||||||
|
|
||||||
- “current” means the standalone refactored `ebox` repository.
|
- “current” means the standalone refactored `ebox` repository.
|
||||||
- “legacy” means the sibling historical `emacs-box` source tree.
|
- “legacy” means the sibling historical Ebox checkout.
|
||||||
- “pure materialization” means producing a propertized render string from a source tree without a live buffer, stable retained identity, or rollback requirement.
|
- “pure materialization” means producing a propertized render string from a source tree without a live buffer, stable retained identity, or rollback requirement.
|
||||||
- “retained publication” means publishing a candidate to a mounted TP surface with stable identity, ownership, scope proof, revision, and rollback semantics.
|
- “retained publication” means publishing a candidate to a mounted TP surface with stable identity, ownership, scope proof, revision, and rollback semantics.
|
||||||
- Absolute timings come from one local measurement environment. The architecture conclusion relies on stage proportions, call counts, and identical outputs rather than on one absolute threshold.
|
- Absolute timings come from one local measurement environment. The architecture conclusion relies on stage proportions, call counts, and identical outputs rather than on one absolute threshold.
|
||||||
@ -208,9 +208,9 @@ The current retained data flow crosses these boundaries:
|
|||||||
|
|
||||||
The corresponding legacy path is closer to “computation scope equals modification scope”:
|
The corresponding legacy path is closer to “computation scope equals modification scope”:
|
||||||
|
|
||||||
- `emacs-box/ebox.el:7195` directly modifies the target region box and enters the local dirty/patch path.
|
- The historical checkout's `ebox.el:7195` directly modifies the target region box and enters the local dirty/patch path.
|
||||||
- `emacs-box/ebox.el:7399` formats only the target content before applying the local patch.
|
- The historical checkout's `ebox.el:7399` formats only the target content before applying the local patch.
|
||||||
- `emacs-box/ebox.el:7578` directly renders, inserts, and refreshes markers/spans.
|
- The historical checkout's `ebox.el:7578` directly renders, inserts, and refreshes markers/spans.
|
||||||
- Legacy `ebox-style.el` mainly expands local properties and classifies dirty work; it has no equivalent current ECSS stylesheet/reactive per-node cascade.
|
- Legacy `ebox-style.el` mainly expands local properties and classifies dirty work; it has no equivalent current ECSS stylesheet/reactive per-node cascade.
|
||||||
|
|
||||||
## Evidence versus inference
|
## Evidence versus inference
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Ebox 性能架构诊断与重构方向
|
# Ebox 性能架构诊断与重构方向
|
||||||
|
|
||||||
> 状态:架构诊断与实现 checkpoint。本文记录 current refactored Ebox 相比历史 `emacs-box` 的性能差异、证据边界和高层重构方向。五个范围很窄的 slice 已经实现;mounted retained-update 架构尚不能认为已经普遍修复。
|
> 状态:架构诊断与实现 checkpoint。本文记录 current refactored Ebox 相比历史 Ebox checkout 的性能差异、证据边界和高层重构方向。五个范围很窄的 slice 已经实现;mounted retained-update 架构尚不能认为已经普遍修复。
|
||||||
>
|
>
|
||||||
> 日期:2026-08-07
|
> 日期:2026-08-07
|
||||||
>
|
>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
|
|
||||||
为什么 current refactored Ebox 在动态内容更新、viewport resize 和纯 `ebox-render` 场景下,性能达不到重构前的 `emacs-box`?需要先确定数据流和 ownership 层面的主因,而不是继续优化某个具体函数。
|
为什么 current refactored Ebox 在动态内容更新、viewport resize 和纯 `ebox-render` 场景下,性能达不到历史 Ebox checkout?需要先确定数据流和 ownership 层面的主因,而不是继续优化某个具体函数。
|
||||||
|
|
||||||
## 结论
|
## 结论
|
||||||
|
|
||||||
@ -21,7 +21,7 @@
|
|||||||
## 比较范围与术语
|
## 比较范围与术语
|
||||||
|
|
||||||
- “current”指独立重构版 `ebox` 仓库。
|
- “current”指独立重构版 `ebox` 仓库。
|
||||||
- “legacy”指同级历史 `emacs-box` 源码树。
|
- “legacy”指同级历史 Ebox checkout。
|
||||||
- “pure materialization”指从 source tree 产生带文本属性的渲染字符串,不需要 live buffer、稳定 retained identity 或 rollback。
|
- “pure materialization”指从 source tree 产生带文本属性的渲染字符串,不需要 live buffer、稳定 retained identity 或 rollback。
|
||||||
- “retained publication”指向已挂载 TP surface 发布候选状态,包含稳定 identity、ownership、scope proof、revision 和 rollback 语义。
|
- “retained publication”指向已挂载 TP surface 发布候选状态,包含稳定 identity、ownership、scope proof、revision 和 rollback 语义。
|
||||||
- 文中的绝对耗时来自同一个本地测量环境;架构判断依赖阶段占比、调用次数和相同输出结果,不依赖某一个绝对时间阈值。
|
- 文中的绝对耗时来自同一个本地测量环境;架构判断依赖阶段占比、调用次数和相同输出结果,不依赖某一个绝对时间阈值。
|
||||||
@ -208,9 +208,9 @@ current 的 retained 数据流由以下边界组成:
|
|||||||
|
|
||||||
legacy 的对应路径更接近“计算范围等于修改范围”:
|
legacy 的对应路径更接近“计算范围等于修改范围”:
|
||||||
|
|
||||||
- `emacs-box/ebox.el:7195` 直接修改目标 region box,并进入局部 dirty/patch 路径。
|
- 历史 checkout 的 `ebox.el:7195` 直接修改目标 region box,并进入局部 dirty/patch 路径。
|
||||||
- `emacs-box/ebox.el:7399` 只格式化目标内容后执行局部 patch。
|
- 历史 checkout 的 `ebox.el:7399` 只格式化目标内容后执行局部 patch。
|
||||||
- `emacs-box/ebox.el:7578` 直接 render、insert 并刷新 marker/span。
|
- 历史 checkout 的 `ebox.el:7578` 直接 render、insert 并刷新 marker/span。
|
||||||
- legacy 的 `ebox-style.el` 主要做本地属性展开和 dirty 分类,没有 current 的 ECSS stylesheet/reactive per-node cascade。
|
- legacy 的 `ebox-style.el` 主要做本地属性展开和 dirty 分类,没有 current 的 ECSS stylesheet/reactive per-node cascade。
|
||||||
|
|
||||||
## Evidence 与 Inference 的边界
|
## Evidence 与 Inference 的边界
|
||||||
|
|||||||
@ -76,8 +76,8 @@ use `ebox-render-to-buffer` when the caller owns window selection.
|
|||||||
(ebox-create &rest properties)
|
(ebox-create &rest properties)
|
||||||
(ebox-concat node-1 node-2) ; two nodes side by side
|
(ebox-concat node-1 node-2) ; two nodes side by side
|
||||||
(ebox-stack node-1 node-2) ; two nodes vertically
|
(ebox-stack node-1 node-2) ; two nodes vertically
|
||||||
(ebox-row &rest nodes) ; many nodes side by side
|
(ebox-row &rest nodes) ; many nodes side by side; a sole Range keeps its row parent
|
||||||
(ebox-column &rest nodes) ; many nodes vertically
|
(ebox-column &rest nodes) ; many nodes vertically; a sole Range keeps its column parent
|
||||||
(ebox-spacer &rest properties) ; blank box
|
(ebox-spacer &rest properties) ; blank box
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -332,12 +332,29 @@ one commit:
|
|||||||
(ebox-commit buffer candidate))
|
(ebox-commit buffer candidate))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Frameworks that retain semantic owners separately from backend anchors can
|
||||||
|
use `ebox-range-ref-present-p` as a read-only check before choosing a wider
|
||||||
|
published owner. It returns nil when the semantic Range is nested inside a
|
||||||
|
material anchor and therefore is not independently addressable in the current
|
||||||
|
Ebox publication.
|
||||||
|
|
||||||
`ebox-candidate-replace-host-ref` uses an application-owned `:host-ref`
|
`ebox-candidate-replace-host-ref` uses an application-owned `:host-ref`
|
||||||
instead of a runtime node id. Both replacement functions accept optional
|
instead of a runtime node id. Both replacement functions accept optional
|
||||||
`old-semantic-key` and `new-semantic-key` pairs for bounded detached identity
|
`old-semantic-key` and `new-semantic-key` pairs for bounded detached identity
|
||||||
reuse. A candidate is sealed by commit and cannot be reused; it also becomes
|
reuse. A candidate is sealed by commit and cannot be reused; it also becomes
|
||||||
stale if the captured buffer runtime or buffer tick changes.
|
stale if the captured buffer runtime or buffer tick changes.
|
||||||
|
|
||||||
|
`ebox-candidate-replace-root` targets a private candidate-bound root address.
|
||||||
|
It accepts exactly one declarative node, clears caller runtime identities, and
|
||||||
|
is last-wins. The final root replacement absorbs descendant node and host-ref
|
||||||
|
operations recorded before or after it, with no wrapper or public reference.
|
||||||
|
|
||||||
|
`ebox-child-range` creates a non-node segment descriptor in a material child
|
||||||
|
list. Its non-nil ref is root-global and its items participate directly in the
|
||||||
|
parent's key, selector, style, and layout scope. Use
|
||||||
|
`ebox-candidate-replace-range-ref` with a proper declarative node list to
|
||||||
|
replace that base payload. Empty payloads remain addressable.
|
||||||
|
|
||||||
### Host-reference positions
|
### Host-reference positions
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
@ -359,6 +376,13 @@ mount has no update report until its first update. Reports include the Ebox
|
|||||||
strategy and publication scope plus TP surface revision and physical
|
strategy and publication scope plus TP surface revision and physical
|
||||||
operation/reconciliation facts.
|
operation/reconciliation facts.
|
||||||
|
|
||||||
|
The complete commit signature is `(ebox-commit BUFFER NEXT-ROOT &optional
|
||||||
|
FRAMEWORK-PUBLISH FRAMEWORK-ROLLBACK)`. Publish receives the report after the
|
||||||
|
buffer, TP surface, and Ebox runtime agree. If a later phase fails, rollback
|
||||||
|
receives that same report at most once; rollback errors and quits are contained.
|
||||||
|
`:framework-participant-state`, `:framework-participant-diagnostics`, and
|
||||||
|
`:scroll-finalization-diagnostics` are read-only outcome fields.
|
||||||
|
|
||||||
Use `ebox-rerender-buffer-with-context` when a root depends on a new viewport:
|
Use `ebox-rerender-buffer-with-context` when a root depends on a new viewport:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
@ -455,9 +479,9 @@ The main customization variables are:
|
|||||||
| Group | Variables and defaults |
|
| Group | Variables and defaults |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Render cache | `ebox-render-cache-max-entries` 2048; `ebox-render-cache-max-bytes` 32 MiB; `ebox-render-root-cache-max-entries` 16; `ebox-render-root-cache-max-bytes` 8 MiB |
|
| Render cache | `ebox-render-cache-max-entries` 2048; `ebox-render-cache-max-bytes` 32 MiB; `ebox-render-root-cache-max-entries` 16; `ebox-render-root-cache-max-bytes` 8 MiB |
|
||||||
| Keyboard/mouse scroll | `ebox-scroll-step` 1; `ebox-wheel-scroll-step` 16; `ebox-wheel-smooth-scroll` t; `ebox-wheel-smooth-scroll-interval` 0.016; `ebox-wheel-smooth-scroll-lines-per-tick` 4; `ebox-wheel-smooth-scroll-target-ticks` 8 |
|
| Keyboard/mouse scroll | `ebox-scroll-step` 1; `ebox-wheel-scroll-step` 1; `ebox-wheel-smooth-scroll` nil; `ebox-wheel-smooth-scroll-interval` 0.016; `ebox-wheel-smooth-scroll-lines-per-tick` 4; `ebox-wheel-smooth-scroll-target-ticks` 8. Keyboard uses point; wheel uses event position; residual lines bubble through nested owners. An interactive complete, chrome-free root document owner may be idle-materialized once and then use Emacs's native window line start; ordinary updates/resizes only warm retained indexes and never start a second native publication; nested/lazy/chrome owners retain transactional Ebox publication. |
|
||||||
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
||||||
| Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32 |
|
| Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32; `ebox-native-buffer-scroll` t (initial/visible-window handoff only; strict root-owner proof) |
|
||||||
| Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
| Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
||||||
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
||||||
| Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. |
|
| Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. |
|
||||||
@ -528,7 +552,7 @@ style-rule functions immediately below are public module-level style APIs.
|
|||||||
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
|
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
|
||||||
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context` | Atomic root commit, report lookup, and viewport-context rerender. |
|
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context` | Atomic root commit, report lookup, and viewport-context rerender. |
|
||||||
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |
|
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |
|
||||||
| `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-host-ref` | One-shot logical candidate transactions. |
|
| `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref` | Persistent material child segments, backend-anchor lookup, and one-shot logical candidate transactions. |
|
||||||
| `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. |
|
| `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. |
|
||||||
| `ebox-selector-parse`, `ebox-selector-match-node-p`, `ebox-selector-query-all`, `ebox-selector-query-buffer`, `ebox-selector-update-buffer` | ECSS selector compilation, matching, querying, and batch update. |
|
| `ebox-selector-parse`, `ebox-selector-match-node-p`, `ebox-selector-query-all`, `ebox-selector-query-buffer`, `ebox-selector-update-buffer` | ECSS selector compilation, matching, querying, and batch update. |
|
||||||
| `ebox-select-all`, `ebox-update-selector` | Compatibility aliases for the two buffer selector functions. |
|
| `ebox-select-all`, `ebox-update-selector` | Compatibility aliases for the two buffer selector functions. |
|
||||||
@ -563,5 +587,5 @@ Focused targets include `make core-tests`, `make grid-tests`,
|
|||||||
`make flex-tests`, and `make visual-check-tests`. The sibling
|
`make flex-tests`, and `make visual-check-tests`. The sibling
|
||||||
`ebox-playground` package owns runnable example fixtures; ETAF owns
|
`ebox-playground` package owns runnable example fixtures; ETAF owns
|
||||||
Components, state, controls, and application behavior. Do not call private
|
Components, state, controls, and application behavior. Do not call private
|
||||||
`ebox--*` functions or edit the historical `emacs-box` checkout as part of
|
`ebox--*` functions or edit the historical Ebox checkout as part of
|
||||||
this package.
|
this package.
|
||||||
|
|||||||
@ -68,8 +68,8 @@ window,再切换到渲染后的 buffer。如果窗口选择由调用方负责
|
|||||||
(ebox-create &rest properties)
|
(ebox-create &rest properties)
|
||||||
(ebox-concat node-1 node-2) ; 两个节点横向排列
|
(ebox-concat node-1 node-2) ; 两个节点横向排列
|
||||||
(ebox-stack node-1 node-2) ; 两个节点纵向排列
|
(ebox-stack node-1 node-2) ; 两个节点纵向排列
|
||||||
(ebox-row &rest nodes) ; 多个节点横向排列
|
(ebox-row &rest nodes) ; 多个节点横向排列;单个 Range 仍保留 row parent
|
||||||
(ebox-column &rest nodes) ; 多个节点纵向排列
|
(ebox-column &rest nodes) ; 多个节点纵向排列;单个 Range 仍保留 column parent
|
||||||
(ebox-spacer &rest properties) ; 空白 box
|
(ebox-spacer &rest properties) ; 空白 box
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -304,11 +304,25 @@ Candidate 是基于当前已发布 runtime 的一次性 transaction。多个稳
|
|||||||
(ebox-commit buffer candidate))
|
(ebox-commit buffer candidate))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果框架的 semantic owner 与 backend anchor 分离,可在选择更宽的发布 owner
|
||||||
|
之前使用只读函数 `ebox-range-ref-present-p`。当 semantic Range 嵌套在 material
|
||||||
|
anchor 内、当前 Ebox publication 没有独立地址时,它返回 nil。
|
||||||
|
|
||||||
`ebox-candidate-replace-host-ref` 使用应用拥有的 `:host-ref`,不需要 runtime
|
`ebox-candidate-replace-host-ref` 使用应用拥有的 `:host-ref`,不需要 runtime
|
||||||
node id。两个 replacement 函数都接受可选的 `old-semantic-key` 与
|
node id。两个 replacement 函数都接受可选的 `old-semantic-key` 与
|
||||||
`new-semantic-key`,用于有限的 detached identity 复用。Candidate 在 commit
|
`new-semantic-key`,用于有限的 detached identity 复用。Candidate 在 commit
|
||||||
时 sealed,不能复用;若捕获的 runtime 或 buffer tick 发生变化,它也会 stale。
|
时 sealed,不能复用;若捕获的 runtime 或 buffer tick 发生变化,它也会 stale。
|
||||||
|
|
||||||
|
`ebox-candidate-replace-root` 使用私有且绑定 candidate 的 root 地址。它只接受
|
||||||
|
一个声明式节点、清除调用方 runtime identity,并采用 last-wins 语义。最终 root
|
||||||
|
replacement 会吸收其前后记录的 descendant node/host-ref operation,不添加
|
||||||
|
wrapper,也不暴露公共 reference。
|
||||||
|
|
||||||
|
`ebox-child-range` 在 material child list 中创建非节点 segment descriptor。
|
||||||
|
其非 nil ref 在 root 内全局唯一,items 直接参与 parent 的 key、selector、style
|
||||||
|
与 layout scope。`ebox-candidate-replace-range-ref` 接受 proper declarative
|
||||||
|
node list,只替换该 base payload;empty payload 仍可寻址。
|
||||||
|
|
||||||
### Host-reference 位置
|
### Host-reference 位置
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
@ -328,6 +342,13 @@ copy;目标缺失或不是 Ebox runtime 时会报错。首次 mount 在第一
|
|||||||
前没有 update report。报告包含 Ebox strategy、publication scope、TP surface
|
前没有 update report。报告包含 Ebox strategy、publication scope、TP surface
|
||||||
revision、物理 operation 和 reconciliation 事实。
|
revision、物理 operation 和 reconciliation 事实。
|
||||||
|
|
||||||
|
完整签名是 `(ebox-commit BUFFER NEXT-ROOT &optional FRAMEWORK-PUBLISH
|
||||||
|
FRAMEWORK-ROLLBACK)`。buffer、TP surface 与 Ebox runtime 一致后,publish 接收
|
||||||
|
report;若后续 phase 失败,rollback 最多一次接收同一个 report,其 error/quit
|
||||||
|
会被隔离。`:framework-participant-state`、
|
||||||
|
`:framework-participant-diagnostics` 与 `:scroll-finalization-diagnostics`
|
||||||
|
是只读 outcome 字段。
|
||||||
|
|
||||||
当 root 依赖新的 viewport 时使用 `ebox-rerender-buffer-with-context`:
|
当 root 依赖新的 viewport 时使用 `ebox-rerender-buffer-with-context`:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
@ -414,9 +435,9 @@ text scale 和固定 `display` space;它也是布局使用的公共测量原
|
|||||||
| 分组 | 变量与默认值 |
|
| 分组 | 变量与默认值 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Render cache | `ebox-render-cache-max-entries` 2048;`ebox-render-cache-max-bytes` 32 MiB;`ebox-render-root-cache-max-entries` 16;`ebox-render-root-cache-max-bytes` 8 MiB |
|
| Render cache | `ebox-render-cache-max-entries` 2048;`ebox-render-cache-max-bytes` 32 MiB;`ebox-render-root-cache-max-entries` 16;`ebox-render-root-cache-max-bytes` 8 MiB |
|
||||||
| 键盘/鼠标滚动 | `ebox-scroll-step` 1;`ebox-wheel-scroll-step` 16;`ebox-wheel-smooth-scroll` t;`ebox-wheel-smooth-scroll-interval` 0.016;`ebox-wheel-smooth-scroll-lines-per-tick` 4;`ebox-wheel-smooth-scroll-target-ticks` 8 |
|
| 键盘/鼠标滚动 | `ebox-scroll-step` 1;`ebox-wheel-scroll-step` 1;`ebox-wheel-smooth-scroll` nil;`ebox-wheel-smooth-scroll-interval` 0.016;`ebox-wheel-smooth-scroll-lines-per-tick` 4;`ebox-wheel-smooth-scroll-target-ticks` 8。键盘以 point 为 anchor,滚轮以 event 位置为 anchor,剩余行沿嵌套 owner 向外冒泡。交互式、内容完整且无 chrome 的根 document owner 可在 idle 时一次 materialize,之后使用 Emacs 原生 window line start;普通更新/resize 只预热 retained index,不启动第二次 native publication;嵌套/lazy/chrome owner 继续走事务化 Ebox publication。 |
|
||||||
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8;`ebox-scroll-lazy-idle-prefetch-lines` 128;`ebox-scroll-lazy-idle-prefetch-slice-lines` 16;`ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8;`ebox-scroll-lazy-idle-prefetch-lines` 128;`ebox-scroll-lazy-idle-prefetch-slice-lines` 16;`ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
||||||
| Runtime prewarm | `ebox-runtime-idle-prewarm` t;`ebox-runtime-idle-prewarm-delay` 0.1;`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0;`ebox-runtime-idle-prewarm-slice-size` 32 |
|
| Runtime prewarm | `ebox-runtime-idle-prewarm` t;`ebox-runtime-idle-prewarm-delay` 0.1;`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0;`ebox-runtime-idle-prewarm-slice-size` 32;`ebox-native-buffer-scroll` t(仅 initial/visible-window handoff;严格 root-owner proof) |
|
||||||
| 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t;`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
| 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t;`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
||||||
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`;`ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2;`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB;`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB;`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB;`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24;`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`;`ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2;`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB;`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB;`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB;`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24;`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
||||||
| Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 |
|
| Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 |
|
||||||
@ -483,7 +504,7 @@ style API。
|
|||||||
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
|
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
|
||||||
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context` | 原子根提交、报告查询和 viewport-context rerender。 |
|
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context` | 原子根提交、报告查询和 viewport-context rerender。 |
|
||||||
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |
|
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |
|
||||||
| `ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-host-ref` | 一次性 logical candidate transaction。 |
|
| `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref` | 持久 material child segment、backend anchor 查询与一次性 logical candidate transaction。 |
|
||||||
| `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 |
|
| `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 |
|
||||||
| `ebox-selector-parse`、`ebox-selector-match-node-p`、`ebox-selector-query-all`、`ebox-selector-query-buffer`、`ebox-selector-update-buffer` | ECSS selector 编译、匹配、查询与批量更新。 |
|
| `ebox-selector-parse`、`ebox-selector-match-node-p`、`ebox-selector-query-all`、`ebox-selector-query-buffer`、`ebox-selector-update-buffer` | ECSS selector 编译、匹配、查询与批量更新。 |
|
||||||
| `ebox-select-all`、`ebox-update-selector` | 两个 buffer selector 函数的兼容别名。 |
|
| `ebox-select-all`、`ebox-update-selector` | 两个 buffer selector 函数的兼容别名。 |
|
||||||
@ -516,4 +537,4 @@ make native-rust-tests
|
|||||||
`make selector-tests`、`make dsl-tests`、`make flex-tests` 和
|
`make selector-tests`、`make dsl-tests`、`make flex-tests` 和
|
||||||
`make visual-check-tests`。可运行的 `.ebox` 示例由同级 `ebox-playground`
|
`make visual-check-tests`。可运行的 `.ebox` 示例由同级 `ebox-playground`
|
||||||
包负责;ETAF 负责 Component、state、control 和应用行为。本包不要调用
|
包负责;ETAF 负责 Component、state、control 和应用行为。本包不要调用
|
||||||
私有 `ebox--*` 函数,也不要在 Ebox 任务中编辑历史 `emacs-box` checkout。
|
私有 `ebox--*` 函数,也不要在 Ebox 任务中编辑历史 Ebox checkout。
|
||||||
|
|||||||
@ -147,7 +147,7 @@ Give a box `:overflow 'scroll` (the default) and a finite `:height` to create a
|
|||||||
:content (mapconcat #'identity lines "\n"))
|
:content (mapconcat #'identity lines "\n"))
|
||||||
```
|
```
|
||||||
|
|
||||||
`ebox-scroll-down`, `ebox-scroll-up`, `ebox-scroll-page-down`, and `ebox-scroll-page-up` operate on the innermost Ebox scroll region at point and fall back to Emacs scrolling when no Ebox region can consume the command. `ebox-wheel-scroll-down` and `ebox-wheel-scroll-up` consume mouse events for Ebox regions and otherwise delegate to `mwheel-scroll`. `ebox-buffer-mode` installs `ebox-scroll-map` locally; `ebox-render-to-buffer` enables it on its returned buffer. `ebox-scroll-state` exposes read-only scroll facts for a numeric region id; use scroll commands or `ebox-region-update` with `:scroll-offset` to change position.
|
`ebox-scroll-down`, `ebox-scroll-up`, `ebox-scroll-page-down`, and `ebox-scroll-page-up` start at the innermost scroll owner under point. Keyboard line intent is allocated one line at a time through semantic scroll ancestors; any residual continues to the parent and then ordinary Emacs buffer scrolling. `ebox-wheel-scroll-down` and `ebox-wheel-scroll-up` anchor at the mouse event position (not point) and use the same inner-to-outer residual rule. When no Ebox region can consume the request they delegate to normal Emacs scrolling. `ebox-buffer-mode` installs `ebox-scroll-map` locally; `ebox-render-to-buffer` enables it on its returned buffer. `ebox-scroll-state` exposes read-only scroll facts for a numeric region id; use scroll commands or `ebox-region-update` with `:scroll-offset` to change position.
|
||||||
|
|
||||||
Viewport-dependent values use `(viewport)` and `(viewport-height)`. Rerender a mounted buffer with an explicit context:
|
Viewport-dependent values use `(viewport)` and `(viewport-height)`. Rerender a mounted buffer with an explicit context:
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ Viewport-dependent values use `(viewport)` and `(viewport-height)`. Rerender a m
|
|||||||
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
||||||
```
|
```
|
||||||
|
|
||||||
The call preserves node and region identity. The [API reference](ebox-api-reference.en.md#8-scrolling-and-viewport-state) lists lazy prefix, idle prefetch, cache, and scroll customization variables.
|
The call preserves node and region identity. In an interactive preview, a complete chrome-free root document owner is idle-materialized once and then follows Emacs's native line-oriented window scrolling; nested or still-lazy owners keep the transactional retained-window path. The [API reference](ebox-api-reference.en.md#8-scrolling-and-viewport-state) lists lazy prefix, idle prefetch, cache, and scroll customization variables.
|
||||||
|
|
||||||
## 9. Standalone `.ebox` files
|
## 9. Standalone `.ebox` files
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@ Selector 查询已渲染的树并返回公共匹配记录,但不拥有应用
|
|||||||
```
|
```
|
||||||
|
|
||||||
`ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down` 和
|
`ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down` 和
|
||||||
`ebox-scroll-page-up` 操作 point 所在的最内层 Ebox scroll region;没有可消费命令的 Ebox region 时回退到 Emacs scrolling。`ebox-wheel-scroll-down` 与 `ebox-wheel-scroll-up` 为 Ebox region 消费 mouse event,否则委托 `mwheel-scroll`。`ebox-buffer-mode` 本地安装 `ebox-scroll-map`;`ebox-render-to-buffer` 会在返回的 buffer 上启用它。`ebox-scroll-state` 为数字 region id 提供只读 scroll facts;使用滚动命令或 `ebox-region-update` 的 `:scroll-offset` 改变位置。
|
`ebox-scroll-page-up`、`ebox-scroll-down` 等键盘命令从 point 所在的最内层 scroll owner 开始,默认按一行意图分配;内层不足的剩余量继续交给语义父盒子,最后才回退到普通 Emacs buffer scrolling。`ebox-wheel-scroll-down` 与 `ebox-wheel-scroll-up` 以 mouse event 位置为 anchor(不使用 point),采用同样的内到外 residual 路由;没有可消费的 Ebox region 时才委托普通 Emacs scrolling。`ebox-buffer-mode` 本地安装 `ebox-scroll-map`;`ebox-render-to-buffer` 会在返回的 buffer 上启用它。`ebox-scroll-state` 为数字 region id 提供只读 scroll facts;使用滚动命令或 `ebox-region-update` 的 `:scroll-offset` 改变位置。
|
||||||
|
|
||||||
Viewport 值使用 `(viewport)` 与 `(viewport-height)`。mounted buffer 要用显式 context 重排:
|
Viewport 值使用 `(viewport)` 与 `(viewport-height)`。mounted buffer 要用显式 context 重排:
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ Viewport 值使用 `(viewport)` 与 `(viewport-height)`。mounted buffer 要用
|
|||||||
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
||||||
```
|
```
|
||||||
|
|
||||||
该调用保留 node 与 region identity。[公共 API 参考](ebox-api-reference.zh.md)列出了 lazy prefix、idle prefetch、cache 和滚动配置变量。
|
该调用保留 node 与 region identity。交互式预览中,内容完整且无 chrome 的根 document owner 会在 idle 时一次 materialize,随后采用 Emacs 原生逐行 window scrolling;嵌套或仍处于 lazy 状态的 owner 继续使用事务化 retained-window 路径。[公共 API 参考](ebox-api-reference.zh.md)列出了 lazy prefix、idle prefetch、cache 和滚动配置变量。
|
||||||
|
|
||||||
## 9. 独立 `.ebox` 文件
|
## 9. 独立 `.ebox` 文件
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
"ebox-render-context" (property value))
|
"ebox-render-context" (property value))
|
||||||
(declare-function ebox--register-render-owned-face-values
|
(declare-function ebox--register-render-owned-face-values
|
||||||
"ebox-render-context" (source rendered))
|
"ebox-render-context" (source rendered))
|
||||||
|
(declare-function ebox--add-render-face!
|
||||||
|
"ebox-render-context" (string start end face &optional append))
|
||||||
(declare-function ebox--string-repeat-lines
|
(declare-function ebox--string-repeat-lines
|
||||||
"ebox" (string count))
|
"ebox" (string count))
|
||||||
(declare-function ebox-lines-join
|
(declare-function ebox-lines-join
|
||||||
@ -240,7 +242,7 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
|||||||
"Apply STYLE's typography to one copy of STRING."
|
"Apply STYLE's typography to one copy of STRING."
|
||||||
(if-let ((face (ebox-buffer--font-face style)))
|
(if-let ((face (ebox-buffer--font-face style)))
|
||||||
(let ((copy (copy-sequence string)))
|
(let ((copy (copy-sequence string)))
|
||||||
(add-face-text-property 0 (length copy) face t copy)
|
(ebox--add-render-face! copy 0 (length copy) face t)
|
||||||
(ebox--register-render-owned-face-values string copy))
|
(ebox--register-render-owned-face-values string copy))
|
||||||
string))
|
string))
|
||||||
|
|
||||||
@ -259,9 +261,8 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
|||||||
(display
|
(display
|
||||||
(ebox--register-render-owned-text-value
|
(ebox--register-render-owned-text-value
|
||||||
'display `(space :width (,pixel-width))))
|
'display `(space :width (,pixel-width))))
|
||||||
(line (propertize " "
|
(line (propertize " " 'display display)))
|
||||||
'face face
|
(ebox--add-render-face! line 0 (length line) face)
|
||||||
'display display)))
|
|
||||||
(ebox--register-render-owned-text-value 'face face)
|
(ebox--register-render-owned-text-value 'face face)
|
||||||
(ebox--string-repeat-lines line height))))
|
(ebox--string-repeat-lines line height))))
|
||||||
|
|
||||||
@ -270,7 +271,7 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
|||||||
(let* ((source string)
|
(let* ((source string)
|
||||||
(string (copy-sequence source))
|
(string (copy-sequence source))
|
||||||
(length (length string)))
|
(length (length string)))
|
||||||
(add-face-text-property 0 length `(:background ,bgcolor) t string)
|
(ebox--add-render-face! string 0 length `(:background ,bgcolor) t)
|
||||||
(ebox--register-render-owned-face-values source string)))
|
(ebox--register-render-owned-face-values source string)))
|
||||||
|
|
||||||
(defun ebox--propertize-color (string color)
|
(defun ebox--propertize-color (string color)
|
||||||
@ -278,12 +279,12 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
|||||||
(let* ((source string)
|
(let* ((source string)
|
||||||
(string (copy-sequence source))
|
(string (copy-sequence source))
|
||||||
(length (length string)))
|
(length (length string)))
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 length
|
string 0 length
|
||||||
(if (eq color 'ebox/default-foreground)
|
(if (eq color 'ebox/default-foreground)
|
||||||
'(:inherit default)
|
'(:inherit default)
|
||||||
`(:foreground ,color))
|
`(:foreground ,color))
|
||||||
t string)
|
t)
|
||||||
(ebox--register-render-owned-face-values source string)))
|
(ebox--register-render-owned-face-values source string)))
|
||||||
|
|
||||||
(defun ebox--propertize-colors (string color bgcolor)
|
(defun ebox--propertize-colors (string color bgcolor)
|
||||||
@ -292,33 +293,33 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
|||||||
(string (copy-sequence source))
|
(string (copy-sequence source))
|
||||||
(length (length string)))
|
(length (length string)))
|
||||||
(when color
|
(when color
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 length
|
string 0 length
|
||||||
(if (eq color 'ebox/default-foreground)
|
(if (eq color 'ebox/default-foreground)
|
||||||
'(:inherit default)
|
'(:inherit default)
|
||||||
`(:foreground ,color))
|
`(:foreground ,color))
|
||||||
t string))
|
t))
|
||||||
(when bgcolor
|
(when bgcolor
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 length `(:background ,bgcolor) t string))
|
string 0 length `(:background ,bgcolor) t))
|
||||||
(ebox--register-render-owned-face-values source string)))
|
(ebox--register-render-owned-face-values source string)))
|
||||||
|
|
||||||
(defun ebox--propertize-overline (string &optional color)
|
(defun ebox--propertize-overline (string &optional color)
|
||||||
"Apply overline to STRING with optional COLOR."
|
"Apply overline to STRING with optional COLOR."
|
||||||
(let* ((source string)
|
(let* ((source string)
|
||||||
(string (copy-sequence source)))
|
(string (copy-sequence source)))
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 (length string) `(:overline ,(or color t)) t string)
|
string 0 (length string) `(:overline ,(or color t)) t)
|
||||||
(ebox--register-render-owned-face-values source string)))
|
(ebox--register-render-owned-face-values source string)))
|
||||||
|
|
||||||
(defun ebox--propertize-underline (string &optional color)
|
(defun ebox--propertize-underline (string &optional color)
|
||||||
"Apply underline to STRING with optional COLOR."
|
"Apply underline to STRING with optional COLOR."
|
||||||
(let* ((source string)
|
(let* ((source string)
|
||||||
(string (copy-sequence source)))
|
(string (copy-sequence source)))
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 (length string)
|
string 0 (length string)
|
||||||
`(:underline (:position t ,@(when color `(:color ,color))))
|
`(:underline (:position t ,@(when color `(:color ,color))))
|
||||||
t string)
|
t)
|
||||||
(ebox--register-render-owned-face-values source string)))
|
(ebox--register-render-owned-face-values source string)))
|
||||||
|
|
||||||
(defun ebox--propertize-region (string property region-id)
|
(defun ebox--propertize-region (string property region-id)
|
||||||
|
|||||||
471
ebox-child-range.el
Normal file
471
ebox-child-range.el
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
;;; ebox-child-range.el --- Persistent child sequence core -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Immutable weighted segment and key tries for future child Range integration.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'subr-x)
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--metrics
|
||||||
|
(:constructor ebox-child-range--make-metrics))
|
||||||
|
(segment-visits 0) (segment-copies 0) (ref-index-visits 0)
|
||||||
|
(ref-index-copies 0) (key-visits 0) (key-copies 0)
|
||||||
|
(collision-visits 0) (collision-copies 0)
|
||||||
|
(old-affected-payload-visits 0) (new-payload-validations 0)
|
||||||
|
(new-payload-copies 0) (new-payload-visits 0)
|
||||||
|
(unaffected-payload-visits 0) (unaffected-payload-validations 0)
|
||||||
|
(unaffected-payload-copies 0))
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--segment
|
||||||
|
(:constructor ebox-child-range--make-segment))
|
||||||
|
ref payload)
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--segment-node
|
||||||
|
(:constructor ebox-child-range--make-segment-node))
|
||||||
|
children value weight segment-count)
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--hash-node
|
||||||
|
(:constructor ebox-child-range--make-hash-node))
|
||||||
|
children bucket)
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--sequence
|
||||||
|
(:constructor ebox-child-range--make-sequence))
|
||||||
|
root height count ref-index key-root hash-function)
|
||||||
|
|
||||||
|
(cl-defstruct (ebox-child-range--descriptor
|
||||||
|
(:constructor ebox-child-range--make-descriptor))
|
||||||
|
ref items)
|
||||||
|
|
||||||
|
(defun ebox-child-range--metric-add (metrics slot &optional amount)
|
||||||
|
"Increment METRICS SLOT by AMOUNT, defaulting to one."
|
||||||
|
(setf (cl-struct-slot-value 'ebox-child-range--metrics slot metrics)
|
||||||
|
(+ (or (cl-struct-slot-value 'ebox-child-range--metrics slot metrics) 0)
|
||||||
|
(or amount 1))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--height (count)
|
||||||
|
"Return the base-32 trie height needed for COUNT segments."
|
||||||
|
(let ((height 0) (capacity 1))
|
||||||
|
(while (> count capacity)
|
||||||
|
(setq height (1+ height) capacity (* capacity 32)))
|
||||||
|
height))
|
||||||
|
|
||||||
|
(defun ebox-child-range--digit (index level)
|
||||||
|
"Return INDEX's five-bit digit at LEVEL."
|
||||||
|
(logand 31 (ash index (* -5 level))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--segment-insert (node height index value)
|
||||||
|
"Persistently insert VALUE at INDEX below NODE of HEIGHT."
|
||||||
|
(if (= height 0)
|
||||||
|
(ebox-child-range--make-segment-node
|
||||||
|
:value value :weight (length (ebox-child-range--segment-payload value))
|
||||||
|
:segment-count 1)
|
||||||
|
(let* ((slot (ebox-child-range--digit index (1- height)))
|
||||||
|
(children (copy-sequence
|
||||||
|
(or (and node (ebox-child-range--segment-node-children node))
|
||||||
|
(make-vector 32 nil))))
|
||||||
|
(old (aref children slot))
|
||||||
|
(new (if (= height 1)
|
||||||
|
(ebox-child-range--make-segment-node
|
||||||
|
:value value :weight (length (ebox-child-range--segment-payload value))
|
||||||
|
:segment-count 1)
|
||||||
|
(ebox-child-range--segment-insert old (1- height) index value))))
|
||||||
|
(aset children slot new)
|
||||||
|
(ebox-child-range--make-segment-node
|
||||||
|
:children children
|
||||||
|
:weight (cl-loop for child across children when child
|
||||||
|
sum (ebox-child-range--segment-node-weight child))
|
||||||
|
:segment-count (cl-loop for child across children when child
|
||||||
|
sum (ebox-child-range--segment-node-segment-count child))))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--segment-at (sequence index)
|
||||||
|
"Return SEQUENCE segment at INDEX."
|
||||||
|
(unless (and (integerp index) (<= 0 index)
|
||||||
|
(< index (ebox-child-range--sequence-count sequence)))
|
||||||
|
(signal 'args-out-of-range (list index)))
|
||||||
|
(let ((node (ebox-child-range--sequence-root sequence))
|
||||||
|
(height (ebox-child-range--sequence-height sequence)))
|
||||||
|
(while (> height 0)
|
||||||
|
(setq node (aref (ebox-child-range--segment-node-children node)
|
||||||
|
(ebox-child-range--digit index (1- height)))
|
||||||
|
height (1- height)))
|
||||||
|
(ebox-child-range--segment-node-value node)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--segment-replace (node height index value metrics)
|
||||||
|
"Return `(NEW-NODE . OLD-SEGMENT)' below NODE of HEIGHT at INDEX.
|
||||||
|
VALUE is the replacement segment and METRICS records all path work."
|
||||||
|
(ebox-child-range--metric-add metrics 'segment-visits)
|
||||||
|
(ebox-child-range--metric-add metrics 'segment-copies)
|
||||||
|
(if (= height 0)
|
||||||
|
(cons (ebox-child-range--make-segment-node
|
||||||
|
:value value
|
||||||
|
:weight (length (ebox-child-range--segment-payload value))
|
||||||
|
:segment-count 1)
|
||||||
|
(ebox-child-range--segment-node-value node))
|
||||||
|
(let* ((slot (ebox-child-range--digit index (1- height)))
|
||||||
|
(children (copy-sequence
|
||||||
|
(ebox-child-range--segment-node-children node)))
|
||||||
|
(old (aref children slot))
|
||||||
|
result new old-segment)
|
||||||
|
(if (= height 1)
|
||||||
|
(progn
|
||||||
|
(ebox-child-range--metric-add metrics 'segment-visits)
|
||||||
|
(ebox-child-range--metric-add metrics 'segment-copies)
|
||||||
|
(setq old-segment (ebox-child-range--segment-node-value old)
|
||||||
|
new (ebox-child-range--make-segment-node
|
||||||
|
:value value
|
||||||
|
:weight (length (ebox-child-range--segment-payload value))
|
||||||
|
:segment-count 1)))
|
||||||
|
(setq result (ebox-child-range--segment-replace
|
||||||
|
old (1- height) index value metrics)
|
||||||
|
new (car result)
|
||||||
|
old-segment (cdr result)))
|
||||||
|
(aset children slot new)
|
||||||
|
(cons
|
||||||
|
(ebox-child-range--make-segment-node
|
||||||
|
:children children
|
||||||
|
:weight (cl-loop for child across children when child
|
||||||
|
sum (ebox-child-range--segment-node-weight child))
|
||||||
|
:segment-count (ebox-child-range--segment-node-segment-count node))
|
||||||
|
old-segment))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--stable-hash (key)
|
||||||
|
"Return KEY's stable unsigned 32-bit hash."
|
||||||
|
(logand #xffffffff (sxhash-equal key)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--bucket-find (bucket key metrics)
|
||||||
|
"Return KEY entry in collision BUCKET and count METRICS comparisons."
|
||||||
|
(let (found)
|
||||||
|
(while bucket
|
||||||
|
(ebox-child-range--metric-add metrics 'collision-visits)
|
||||||
|
(when (equal key (caar bucket)) (setq found (car bucket)))
|
||||||
|
(setq bucket (cdr bucket)))
|
||||||
|
found))
|
||||||
|
|
||||||
|
(defun ebox-child-range--hash-change
|
||||||
|
(node key value hash depth remove-p metrics)
|
||||||
|
"Persistently change KEY in sparse hash NODE using HASH at DEPTH."
|
||||||
|
(if (= depth 7)
|
||||||
|
(let* ((source (and node (ebox-child-range--hash-node-bucket node)))
|
||||||
|
(found (ebox-child-range--bucket-find source key metrics))
|
||||||
|
(bucket (copy-sequence source)))
|
||||||
|
(ebox-child-range--metric-add metrics 'collision-copies
|
||||||
|
(length source))
|
||||||
|
(cond
|
||||||
|
(remove-p
|
||||||
|
(ebox-child-range--make-hash-node
|
||||||
|
:bucket (cl-delete key bucket :key #'car :test #'equal)))
|
||||||
|
(found (error "Ebox child key is not unique: %S" key))
|
||||||
|
(t (ebox-child-range--make-hash-node
|
||||||
|
:bucket (cons (cons key value) bucket)))))
|
||||||
|
(ebox-child-range--metric-add metrics 'key-visits)
|
||||||
|
(ebox-child-range--metric-add metrics 'key-copies)
|
||||||
|
(let* ((slot (logand 31 (ash hash (* -5 depth))))
|
||||||
|
(children (copy-sequence (and node
|
||||||
|
(ebox-child-range--hash-node-children
|
||||||
|
node))))
|
||||||
|
(old (cdr (assq slot children)))
|
||||||
|
(new (ebox-child-range--hash-change
|
||||||
|
old key value hash (1+ depth) remove-p metrics)))
|
||||||
|
(setq children (cons (cons slot new) (assq-delete-all slot children)))
|
||||||
|
(ebox-child-range--make-hash-node :children children))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--hash-lookup (root key hash)
|
||||||
|
"Look up KEY with HASH in ROOT."
|
||||||
|
(let ((node root) (depth 0))
|
||||||
|
(while (< depth 7)
|
||||||
|
(setq node (and node
|
||||||
|
(cdr (assq (logand 31 (ash hash (* -5 depth)))
|
||||||
|
(ebox-child-range--hash-node-children node))))
|
||||||
|
depth (1+ depth)))
|
||||||
|
(cdr (assoc key (and node (ebox-child-range--hash-node-bucket node))
|
||||||
|
#'equal))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--item-copy (item metrics)
|
||||||
|
"Validate and copy declarative ITEM while recording METRICS."
|
||||||
|
(when (ebox-child-range--descriptor-p item)
|
||||||
|
(error "Nested child Range descriptors are reserved"))
|
||||||
|
(unless (and (listp item) (not (stringp item)))
|
||||||
|
(error "Ebox child Range item must be one declarative node"))
|
||||||
|
(ebox-child-range--metric-add metrics 'new-payload-validations)
|
||||||
|
(ebox-child-range--metric-add metrics 'new-payload-visits)
|
||||||
|
(ebox-child-range--metric-add metrics 'new-payload-copies)
|
||||||
|
(copy-sequence item))
|
||||||
|
|
||||||
|
(defun ebox-child-range--descriptor-create (ref items)
|
||||||
|
"Return a reserved Range descriptor for non-nil REF and list ITEMS."
|
||||||
|
(unless ref (error "Ebox child Range ref must be non-nil"))
|
||||||
|
(unless (proper-list-p items)
|
||||||
|
(signal 'wrong-type-argument (list 'proper-list-p items)))
|
||||||
|
(ebox-child-range--make-descriptor :ref ref :items items))
|
||||||
|
|
||||||
|
(defun ebox-child-range--build (segments &optional hash-function)
|
||||||
|
"Build an immutable child sequence from SEGMENTS.
|
||||||
|
Each entry is `(REF . ITEMS)'; nil REF denotes one static child item.
|
||||||
|
HASH-FUNCTION optionally supplies the stable key hash."
|
||||||
|
(unless (proper-list-p segments)
|
||||||
|
(signal 'wrong-type-argument (list 'proper-list-p segments)))
|
||||||
|
(let* ((count (length segments))
|
||||||
|
(height (ebox-child-range--height count))
|
||||||
|
(hash-function (or hash-function #'ebox-child-range--stable-hash))
|
||||||
|
(metrics (ebox-child-range--make-metrics))
|
||||||
|
root key-root (refs (make-hash-table :test #'equal)) (index 0))
|
||||||
|
(when (zerop count) (error "Ebox child sequence requires a segment"))
|
||||||
|
(dolist (entry segments)
|
||||||
|
(let* ((ref (car entry))
|
||||||
|
(items (cdr entry)))
|
||||||
|
(unless (proper-list-p items)
|
||||||
|
(signal 'wrong-type-argument (list 'proper-list-p items)))
|
||||||
|
(when (and (null ref) (/= (length items) 1))
|
||||||
|
(error "Ebox static child segment requires exactly one item"))
|
||||||
|
(when (and ref (gethash ref refs))
|
||||||
|
(error "Ebox child Range ref is not unique: %S" ref))
|
||||||
|
(when ref (puthash ref index refs))
|
||||||
|
(let ((payload (vconcat
|
||||||
|
(mapcar (lambda (item)
|
||||||
|
(ebox-child-range--item-copy item metrics))
|
||||||
|
items))))
|
||||||
|
(setq root (ebox-child-range--segment-insert
|
||||||
|
root height index
|
||||||
|
(ebox-child-range--make-segment :ref ref :payload payload)))
|
||||||
|
(dotimes (offset (length payload))
|
||||||
|
(let* ((item (aref payload offset)) (key (plist-get item :key)))
|
||||||
|
(when key
|
||||||
|
(setq key-root
|
||||||
|
(ebox-child-range--hash-change
|
||||||
|
key-root key (cons index offset)
|
||||||
|
(funcall hash-function key) 0 nil metrics)))))))
|
||||||
|
(setq index (1+ index)))
|
||||||
|
(ebox-child-range--make-sequence
|
||||||
|
:root root :height height :count count :ref-index refs
|
||||||
|
:key-root key-root :hash-function hash-function)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--replace (sequence ref items)
|
||||||
|
"Return `(NEW-SEQUENCE . METRICS)' for SEQUENCE after replacing REF ITEMS."
|
||||||
|
(ebox-child-range--replace-many sequence (list (cons ref items))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--replace-many (sequence replacements)
|
||||||
|
"Replace RANGE REPLACEMENTS in SEQUENCE with one two-phase transaction."
|
||||||
|
(let ((metrics (ebox-child-range--make-metrics))
|
||||||
|
(missing (make-symbol "missing-range-ref"))
|
||||||
|
(seen (make-hash-table :test #'equal)) records)
|
||||||
|
(unless (proper-list-p replacements)
|
||||||
|
(signal 'wrong-type-argument (list 'proper-list-p replacements)))
|
||||||
|
(dolist (entry replacements)
|
||||||
|
(unless (proper-list-p (cdr entry))
|
||||||
|
(signal 'wrong-type-argument (list 'proper-list-p (cdr entry))))
|
||||||
|
(when (gethash (car entry) seen)
|
||||||
|
(error "Ebox child Range replacement ref repeats: %S" (car entry)))
|
||||||
|
(puthash (car entry) t seen)
|
||||||
|
(ebox-child-range--metric-add metrics 'ref-index-visits)
|
||||||
|
(let ((index (gethash (car entry)
|
||||||
|
(ebox-child-range--sequence-ref-index sequence)
|
||||||
|
missing)))
|
||||||
|
(when (eq index missing)
|
||||||
|
(error "Ebox child Range ref does not exist: %S" (car entry)))
|
||||||
|
(push (list (car entry) index
|
||||||
|
(vconcat (mapcar
|
||||||
|
(lambda (item)
|
||||||
|
(ebox-child-range--item-copy item metrics))
|
||||||
|
(cdr entry))))
|
||||||
|
records)))
|
||||||
|
(setq records (nreverse records))
|
||||||
|
(let ((key-root (ebox-child-range--sequence-key-root sequence))
|
||||||
|
(hash-function (ebox-child-range--sequence-hash-function sequence))
|
||||||
|
(root (ebox-child-range--sequence-root sequence)))
|
||||||
|
;; Each affected ref performs exactly one measured segment path copy;
|
||||||
|
;; the returned old leaf supplies phase-one key removal.
|
||||||
|
(setq records
|
||||||
|
(mapcar
|
||||||
|
(lambda (record)
|
||||||
|
(let* ((result
|
||||||
|
(ebox-child-range--segment-replace
|
||||||
|
root (ebox-child-range--sequence-height sequence)
|
||||||
|
(nth 1 record)
|
||||||
|
(ebox-child-range--make-segment
|
||||||
|
:ref (car record) :payload (nth 2 record))
|
||||||
|
metrics)))
|
||||||
|
(setq root (car result))
|
||||||
|
(list (car record) (nth 1 record) (cdr result)
|
||||||
|
(nth 2 record))))
|
||||||
|
records))
|
||||||
|
(dolist (record records)
|
||||||
|
(let ((payload (ebox-child-range--segment-payload (nth 2 record))))
|
||||||
|
(dotimes (offset (length payload))
|
||||||
|
(ebox-child-range--metric-add metrics 'old-affected-payload-visits)
|
||||||
|
(when-let ((key (plist-get (aref payload offset) :key)))
|
||||||
|
(setq key-root
|
||||||
|
(ebox-child-range--hash-change
|
||||||
|
key-root key nil (funcall hash-function key) 0 t metrics))))))
|
||||||
|
(dolist (record records)
|
||||||
|
(let ((index (nth 1 record)) (payload (nth 3 record)))
|
||||||
|
(dotimes (offset (length payload))
|
||||||
|
(when-let ((key (plist-get (aref payload offset) :key)))
|
||||||
|
(setq key-root
|
||||||
|
(ebox-child-range--hash-change
|
||||||
|
key-root key (cons index offset)
|
||||||
|
(funcall hash-function key) 0 nil metrics))))))
|
||||||
|
(cons (ebox-child-range--make-sequence
|
||||||
|
:root root :height (ebox-child-range--sequence-height sequence)
|
||||||
|
:count (ebox-child-range--sequence-count sequence)
|
||||||
|
:ref-index (ebox-child-range--sequence-ref-index sequence)
|
||||||
|
:key-root key-root :hash-function hash-function)
|
||||||
|
metrics))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--replace-item-at
|
||||||
|
(sequence segment-index offset old-item new-item)
|
||||||
|
"Persistently replace OLD-ITEM by NEW-ITEM in SEQUENCE.
|
||||||
|
SEGMENT-INDEX and OFFSET identify its exact material-child location."
|
||||||
|
(let* ((segment (ebox-child-range--segment-at sequence segment-index))
|
||||||
|
(payload (copy-sequence (ebox-child-range--segment-payload segment)))
|
||||||
|
(key-root (ebox-child-range--sequence-key-root sequence))
|
||||||
|
(hash-function (ebox-child-range--sequence-hash-function sequence))
|
||||||
|
(metrics (ebox-child-range--make-metrics))
|
||||||
|
(old-key (plist-get old-item :key))
|
||||||
|
(new-key (plist-get new-item :key)))
|
||||||
|
(unless (eq (aref payload offset) old-item)
|
||||||
|
(error "Ebox child sequence location is stale"))
|
||||||
|
(when old-key
|
||||||
|
(setq key-root (ebox-child-range--hash-change
|
||||||
|
key-root old-key nil (funcall hash-function old-key)
|
||||||
|
0 t metrics)))
|
||||||
|
(when new-key
|
||||||
|
(setq key-root (ebox-child-range--hash-change
|
||||||
|
key-root new-key (cons segment-index offset)
|
||||||
|
(funcall hash-function new-key) 0 nil metrics)))
|
||||||
|
(aset payload offset new-item)
|
||||||
|
(let ((result
|
||||||
|
(ebox-child-range--segment-replace
|
||||||
|
(ebox-child-range--sequence-root sequence)
|
||||||
|
(ebox-child-range--sequence-height sequence) segment-index
|
||||||
|
(ebox-child-range--make-segment
|
||||||
|
:ref (ebox-child-range--segment-ref segment) :payload payload)
|
||||||
|
metrics)))
|
||||||
|
(ebox-child-range--make-sequence
|
||||||
|
:root (car result) :height (ebox-child-range--sequence-height sequence)
|
||||||
|
:count (ebox-child-range--sequence-count sequence)
|
||||||
|
:ref-index (ebox-child-range--sequence-ref-index sequence)
|
||||||
|
:key-root key-root :hash-function hash-function))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--prefix-weight (sequence index)
|
||||||
|
"Return flattened item weight before segment INDEX in SEQUENCE."
|
||||||
|
(let ((node (ebox-child-range--sequence-root sequence))
|
||||||
|
(height (ebox-child-range--sequence-height sequence))
|
||||||
|
(weight 0))
|
||||||
|
(while (> height 0)
|
||||||
|
(let* ((children (ebox-child-range--segment-node-children node))
|
||||||
|
(slot (ebox-child-range--digit index (1- height))))
|
||||||
|
(dotimes (cursor slot)
|
||||||
|
(when-let ((child (aref children cursor)))
|
||||||
|
(setq weight (+ weight
|
||||||
|
(ebox-child-range--segment-node-weight child)))))
|
||||||
|
(setq node (aref children slot)
|
||||||
|
height (1- height))))
|
||||||
|
weight))
|
||||||
|
|
||||||
|
(defun ebox-child-range--rank (sequence index)
|
||||||
|
"Return the flattened child rank of segment INDEX in SEQUENCE."
|
||||||
|
(ebox-child-range--prefix-weight sequence index))
|
||||||
|
|
||||||
|
(defun ebox-child-range--lookup-ref (sequence ref)
|
||||||
|
"Return SEQUENCE's segment addressed by non-nil REF."
|
||||||
|
(let ((missing (make-symbol "missing-range-ref")))
|
||||||
|
(let ((index (gethash ref (ebox-child-range--sequence-ref-index sequence)
|
||||||
|
missing)))
|
||||||
|
(unless (eq index missing)
|
||||||
|
(ebox-child-range--segment-at sequence index)))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--range-records (sequence)
|
||||||
|
"Return stable `(REF INDEX)' records for SEQUENCE ranges."
|
||||||
|
(let (records)
|
||||||
|
(dotimes (index (ebox-child-range--sequence-count sequence))
|
||||||
|
(let ((segment (ebox-child-range--segment-at sequence index)))
|
||||||
|
(when-let ((ref (ebox-child-range--segment-ref segment)))
|
||||||
|
(push (list ref index) records))))
|
||||||
|
(nreverse records)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--segment-memory (sequence)
|
||||||
|
"Return `(NODES . EDGES)' used by SEQUENCE's fixed-vector segment trie."
|
||||||
|
(let ((nodes 0) (edges 0))
|
||||||
|
(cl-labels ((walk (node)
|
||||||
|
(setq nodes (1+ nodes))
|
||||||
|
(when-let ((children
|
||||||
|
(ebox-child-range--segment-node-children node)))
|
||||||
|
(dotimes (slot 32)
|
||||||
|
(when-let ((child (aref children slot)))
|
||||||
|
(setq edges (1+ edges))
|
||||||
|
(walk child))))))
|
||||||
|
(walk (ebox-child-range--sequence-root sequence)))
|
||||||
|
(cons nodes edges)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--segment-vector-memory (sequence)
|
||||||
|
"Return `(VECTORS . SLOTS)' allocated by SEQUENCE's segment trie."
|
||||||
|
(let ((vectors 0))
|
||||||
|
(cl-labels ((walk (node)
|
||||||
|
(when-let ((children
|
||||||
|
(ebox-child-range--segment-node-children node)))
|
||||||
|
(setq vectors (1+ vectors))
|
||||||
|
(dotimes (slot 32)
|
||||||
|
(when-let ((child (aref children slot))) (walk child))))))
|
||||||
|
(walk (ebox-child-range--sequence-root sequence)))
|
||||||
|
(cons vectors (* 32 vectors))))
|
||||||
|
|
||||||
|
(defun ebox-child-range--hash-memory (root)
|
||||||
|
"Return `(NODES . EDGES)' for sparse persistent hash ROOT."
|
||||||
|
(let ((nodes 0) (edges 0))
|
||||||
|
(cl-labels ((walk (node)
|
||||||
|
(when node
|
||||||
|
(setq nodes (1+ nodes))
|
||||||
|
(dolist (entry (ebox-child-range--hash-node-children node))
|
||||||
|
(setq edges (1+ edges))
|
||||||
|
(walk (cdr entry))))))
|
||||||
|
(walk root))
|
||||||
|
(cons nodes edges)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--metrics-plist (metrics)
|
||||||
|
"Return a read-only plist snapshot of METRICS."
|
||||||
|
(list :segment-visits (ebox-child-range--metrics-segment-visits metrics)
|
||||||
|
:segment-copies (ebox-child-range--metrics-segment-copies metrics)
|
||||||
|
:ref-index-visits (ebox-child-range--metrics-ref-index-visits metrics)
|
||||||
|
:ref-index-copies (ebox-child-range--metrics-ref-index-copies metrics)
|
||||||
|
:key-visits (ebox-child-range--metrics-key-visits metrics)
|
||||||
|
:key-copies (ebox-child-range--metrics-key-copies metrics)
|
||||||
|
:collision-visits (ebox-child-range--metrics-collision-visits metrics)
|
||||||
|
:collision-copies (ebox-child-range--metrics-collision-copies metrics)
|
||||||
|
:old-affected-payload-visits
|
||||||
|
(ebox-child-range--metrics-old-affected-payload-visits metrics)
|
||||||
|
:new-payload-validations
|
||||||
|
(ebox-child-range--metrics-new-payload-validations metrics)
|
||||||
|
:new-payload-copies (ebox-child-range--metrics-new-payload-copies metrics)
|
||||||
|
:new-payload-visits (ebox-child-range--metrics-new-payload-visits metrics)
|
||||||
|
:unaffected-payload-visits
|
||||||
|
(ebox-child-range--metrics-unaffected-payload-visits metrics)
|
||||||
|
:unaffected-payload-validations
|
||||||
|
(ebox-child-range--metrics-unaffected-payload-validations metrics)
|
||||||
|
:unaffected-payload-copies
|
||||||
|
(ebox-child-range--metrics-unaffected-payload-copies metrics)))
|
||||||
|
|
||||||
|
(defun ebox-child-range--fold (sequence function initial)
|
||||||
|
"Fold SEQUENCE payloads in stable order with FUNCTION and INITIAL."
|
||||||
|
(let ((value initial)
|
||||||
|
(index 0))
|
||||||
|
(while (< index (ebox-child-range--sequence-count sequence))
|
||||||
|
(let ((payload (ebox-child-range--segment-payload
|
||||||
|
(ebox-child-range--segment-at sequence index))))
|
||||||
|
(dotimes (offset (length payload))
|
||||||
|
(setq value (funcall function value (aref payload offset)))))
|
||||||
|
(setq index (1+ index)))
|
||||||
|
value))
|
||||||
|
|
||||||
|
(defun ebox-child-range--flatten (sequence)
|
||||||
|
"Return SEQUENCE's explicit debug-only flat item list."
|
||||||
|
(nreverse
|
||||||
|
(ebox-child-range--fold
|
||||||
|
sequence (lambda (items item) (cons item items)) nil)))
|
||||||
|
|
||||||
|
(provide 'ebox-child-range)
|
||||||
|
;;; ebox-child-range.el ends here
|
||||||
@ -123,7 +123,13 @@ The first value is string content. The second value is a lazy child node."
|
|||||||
(let* ((split (ebox--build-split-attrs items))
|
(let* ((split (ebox--build-split-attrs items))
|
||||||
(props (car split))
|
(props (car split))
|
||||||
(children (cdr split))
|
(children (cdr split))
|
||||||
(layout (apply constructor (mapcar #'ebox-dsl-build (delq nil children)))))
|
(layout
|
||||||
|
(apply constructor
|
||||||
|
(mapcar (lambda (child)
|
||||||
|
(if (ebox-child-range--descriptor-p child)
|
||||||
|
child
|
||||||
|
(ebox-dsl-build child)))
|
||||||
|
(delq nil children)))))
|
||||||
(ebox--build-wrap-layout tag props layout)))
|
(ebox--build-wrap-layout tag props layout)))
|
||||||
|
|
||||||
(defun ebox--build-flex (items)
|
(defun ebox--build-flex (items)
|
||||||
|
|||||||
12
ebox-flex.el
12
ebox-flex.el
@ -485,7 +485,7 @@ grapheme."
|
|||||||
(dolist (child (ebox--layout-children node))
|
(dolist (child (ebox--layout-children node))
|
||||||
(ebox--flex-recache-source-boxes child)))
|
(ebox--flex-recache-source-boxes child)))
|
||||||
('flex
|
('flex
|
||||||
(dolist (child (plist-get node :children))
|
(dolist (child (ebox-tree-layout-children node))
|
||||||
(ebox--flex-recache-source-boxes child)))
|
(ebox--flex-recache-source-boxes child)))
|
||||||
('flex-item
|
('flex-item
|
||||||
(ebox--flex-recache-source-boxes (plist-get node :node)))))))
|
(ebox--flex-recache-source-boxes (plist-get node :node)))))))
|
||||||
@ -1468,7 +1468,7 @@ its child."
|
|||||||
(plist-get node :raw-props)
|
(plist-get node :raw-props)
|
||||||
(plist-get node :box)))
|
(plist-get node :box)))
|
||||||
(axis (ebox--flex-axis props))
|
(axis (ebox--flex-axis props))
|
||||||
(children (plist-get node :children))
|
(children (ebox-tree-layout-children node))
|
||||||
(main-size (if (eq axis 'row)
|
(main-size (if (eq axis 'row)
|
||||||
(ebox--flex-horizontal-value (plist-get props :width))
|
(ebox--flex-horizontal-value (plist-get props :width))
|
||||||
(ebox--flex-line-value (plist-get props :height))))
|
(ebox--flex-line-value (plist-get props :height))))
|
||||||
@ -1512,7 +1512,7 @@ assembles already-rendered items."
|
|||||||
(plist-get node :raw-props)
|
(plist-get node :raw-props)
|
||||||
(plist-get node :box)))
|
(plist-get node :box)))
|
||||||
(axis (ebox--flex-axis props))
|
(axis (ebox--flex-axis props))
|
||||||
(children (plist-get node :children))
|
(children (ebox-tree-layout-children node))
|
||||||
(main-size (ebox--flex-main-value
|
(main-size (ebox--flex-main-value
|
||||||
axis (plist-get props
|
axis (plist-get props
|
||||||
(if (eq axis 'row)
|
(if (eq axis 'row)
|
||||||
@ -2186,7 +2186,7 @@ Return nil when the flex container needs the normal full renderer."
|
|||||||
(plist-get node :raw-props)
|
(plist-get node :raw-props)
|
||||||
(plist-get node :box)))
|
(plist-get node :box)))
|
||||||
(axis (ebox--flex-axis props))
|
(axis (ebox--flex-axis props))
|
||||||
(children (plist-get node :children))
|
(children (ebox-tree-layout-children node))
|
||||||
(main-size (if (eq axis 'row)
|
(main-size (if (eq axis 'row)
|
||||||
(ebox--flex-horizontal-value
|
(ebox--flex-horizontal-value
|
||||||
(plist-get props :width))
|
(plist-get props :width))
|
||||||
@ -2333,7 +2333,7 @@ known-row render is used only if the new item footprint is not smaller."
|
|||||||
(memq wrap '(wrap nowrap))
|
(memq wrap '(wrap nowrap))
|
||||||
(null (ebox--flex-line-value (plist-get props :height)))
|
(null (ebox--flex-line-value (plist-get props :height)))
|
||||||
(not (ebox--flex-reverse-p props)))
|
(not (ebox--flex-reverse-p props)))
|
||||||
(let* ((children (plist-get node :children))
|
(let* ((children (ebox-tree-layout-children node))
|
||||||
(inline-viewport (ebox--flex-inline-viewport
|
(inline-viewport (ebox--flex-inline-viewport
|
||||||
axis main-size cross-size))
|
axis main-size cross-size))
|
||||||
(row-gap (or (ebox--flex-line-value
|
(row-gap (or (ebox--flex-line-value
|
||||||
@ -2384,7 +2384,7 @@ Return nil when NODE's flex configuration needs a full rerender."
|
|||||||
(memq wrap '(wrap nowrap))
|
(memq wrap '(wrap nowrap))
|
||||||
main-size
|
main-size
|
||||||
(null (ebox--flex-line-value (plist-get props :height))))
|
(null (ebox--flex-line-value (plist-get props :height))))
|
||||||
(let* ((children (plist-get node :children))
|
(let* ((children (ebox-tree-layout-children node))
|
||||||
(cross-size nil)
|
(cross-size nil)
|
||||||
(inline-viewport (ebox--flex-inline-viewport
|
(inline-viewport (ebox--flex-inline-viewport
|
||||||
axis main-size cross-size))
|
axis main-size cross-size))
|
||||||
|
|||||||
@ -446,7 +446,10 @@ Supported properties are `:grid-column', `:grid-row',
|
|||||||
(defun ebox-grid--auto-width-node-p (node)
|
(defun ebox-grid--auto-width-node-p (node)
|
||||||
"Return non-nil when NODE derives its width from the active viewport."
|
"Return non-nil when NODE derives its width from the active viewport."
|
||||||
(let ((width (plist-get node :width)))
|
(let ((width (plist-get node :width)))
|
||||||
(or (null width) (eq width 'auto))))
|
;; `stretch' and `contain' are containing-block widths. Treating them
|
||||||
|
;; as intrinsic leaves fractional Grid tracks empty and makes composite
|
||||||
|
;; cards hug their natural width instead of filling the assigned track.
|
||||||
|
(or (null width) (memq width '(auto stretch contain)))))
|
||||||
|
|
||||||
(defun ebox-grid--entry-source (entry rendered width props)
|
(defun ebox-grid--entry-source (entry rendered width props)
|
||||||
"Return ENTRY's content rendered within WIDTH when it is auto-sized.
|
"Return ENTRY's content rendered within WIDTH when it is auto-sized.
|
||||||
@ -610,7 +613,7 @@ size for start/center/end alignment unless it would overflow its track."
|
|||||||
(defun ebox--render-grid (node)
|
(defun ebox--render-grid (node)
|
||||||
"Render GRID NODE to a propertized string."
|
"Render GRID NODE to a propertized string."
|
||||||
(let* ((props (plist-get node :raw-props))
|
(let* ((props (plist-get node :raw-props))
|
||||||
(children (plist-get node :children))
|
(children (ebox-tree-layout-children node))
|
||||||
(columns (ebox-grid--normalize-tracks
|
(columns (ebox-grid--normalize-tracks
|
||||||
(plist-get props :grid-template-columns) 'columns))
|
(plist-get props :grid-template-columns) 'columns))
|
||||||
(rows (ebox-grid--normalize-tracks
|
(rows (ebox-grid--normalize-tracks
|
||||||
|
|||||||
2288
ebox-incremental.el
2288
ebox-incremental.el
File diff suppressed because it is too large
Load Diff
229
ebox-layout.el
229
ebox-layout.el
@ -40,6 +40,9 @@ must not stretch their own outer width to that containing block.")
|
|||||||
(defvar ebox--flat-preformatted-box-fast-path-disabled nil
|
(defvar ebox--flat-preformatted-box-fast-path-disabled nil
|
||||||
"Non-nil disables one-pass preformatted wrapper rendering for diagnostics.")
|
"Non-nil disables one-pass preformatted wrapper rendering for diagnostics.")
|
||||||
|
|
||||||
|
(declare-function ebox--strip-paint-origins!
|
||||||
|
"ebox-render-context" (string))
|
||||||
|
|
||||||
(defun ebox--record-rendered-uniform-width (rendered pixel-width)
|
(defun ebox--record-rendered-uniform-width (rendered pixel-width)
|
||||||
"Record that every line of RENDERED is exactly PIXEL-WIDTH wide."
|
"Record that every line of RENDERED is exactly PIXEL-WIDTH wide."
|
||||||
(when (and (stringp rendered) (numberp pixel-width))
|
(when (and (stringp rendered) (numberp pixel-width))
|
||||||
@ -203,18 +206,40 @@ and its formatted content exactly fills the used height."
|
|||||||
(ebox--format-content-string box content)))
|
(ebox--format-content-string box content)))
|
||||||
(lines (ebox-string-lines formatted))
|
(lines (ebox-string-lines formatted))
|
||||||
(text-height (length lines))
|
(text-height (length lines))
|
||||||
(content-height (ebox--content-height box text-height)))
|
(content-height (ebox--content-height box text-height))
|
||||||
(when (and (= text-height content-height)
|
(content-pixel (ebox--content-pixel box))
|
||||||
(ebox--preformatted-content-lines-fit-p
|
(lines-fit-p
|
||||||
lines (ebox--content-pixel box)))
|
(ebox--preformatted-content-lines-fit-p lines content-pixel))
|
||||||
|
(underfilled-simple-scroll-p
|
||||||
|
(and lines-fit-p
|
||||||
|
(< text-height content-height)
|
||||||
|
(eq (ebox-get box :overflow) 'scroll)
|
||||||
|
(= (ebox-get box :padding-left-pixel) 0)
|
||||||
|
(= (ebox-get box :padding-right-pixel) 0)
|
||||||
|
(= (floor (ebox-get box :padding-top-height)) 0)
|
||||||
|
(= (floor (ebox-get box :padding-bottom-height)) 0)
|
||||||
|
(= (ebox-get box :border-left-pixel) 0)
|
||||||
|
(= (ebox-get box :border-right-pixel) 0)
|
||||||
|
(not (ebox-get box :border-top-p))
|
||||||
|
(not (ebox-get box :border-bottom-p)))))
|
||||||
|
(when (and lines-fit-p
|
||||||
|
(or (= text-height content-height)
|
||||||
|
underfilled-simple-scroll-p))
|
||||||
(let* ((region-id (or ebox--render-region-id
|
(let* ((region-id (or ebox--render-region-id
|
||||||
(ebox--ensure-region-id box)))
|
(ebox--ensure-region-id box)))
|
||||||
(content-pixel (ebox--content-pixel box))
|
|
||||||
(padding-line-filler (ebox-pixel-space content-pixel))
|
(padding-line-filler (ebox-pixel-space content-pixel))
|
||||||
(rendered
|
(rendered
|
||||||
(ebox-lines-join
|
(if underfilled-simple-scroll-p
|
||||||
(ebox--window-render-flat-wrapper-chunk-lines
|
(ebox-lines-join
|
||||||
box lines 0 region-id padding-line-filler t t))))
|
(ebox--render-underfilled-simple-scroll-lines
|
||||||
|
lines region-id content-height padding-line-filler
|
||||||
|
(ebox-get box :color) (ebox-get box :bgcolor)))
|
||||||
|
(ebox-lines-join
|
||||||
|
(ebox--window-render-flat-wrapper-chunk-lines
|
||||||
|
box lines 0 region-id padding-line-filler t t)))))
|
||||||
|
(when underfilled-simple-scroll-p
|
||||||
|
(ebox--apply-surface-properties
|
||||||
|
rendered (ebox-get box :surface-properties)))
|
||||||
(puthash region-id box ebox--region-box-table)
|
(puthash region-id box ebox--region-box-table)
|
||||||
(unless ebox--intrinsic-layout-measurement
|
(unless ebox--intrinsic-layout-measurement
|
||||||
(ebox--scroll-clear-state region-id))
|
(ebox--scroll-clear-state region-id))
|
||||||
@ -227,6 +252,15 @@ and its formatted content exactly fills the used height."
|
|||||||
(defvar ebox--scroll-window-render-result nil
|
(defvar ebox--scroll-window-render-result nil
|
||||||
"Dynamic metadata for the current box's lazy scroll-window render.")
|
"Dynamic metadata for the current box's lazy scroll-window render.")
|
||||||
|
|
||||||
|
(defvar ebox--scroll-window-cached-state nil
|
||||||
|
"Dynamic staged scroll state used by a bounded cached-window projection.")
|
||||||
|
|
||||||
|
(defvar ebox--scroll-window-cached-rendered-lines-p nil
|
||||||
|
"Non-nil when cached scroll lines already carry wrapper ownership.")
|
||||||
|
|
||||||
|
(defvar ebox--scroll-window-skip-state-rebuild-p nil
|
||||||
|
"Non-nil when a scroll projection already staged its derived line indexes.")
|
||||||
|
|
||||||
(defvar ebox--scroll-rendered-content-lines-private-p nil
|
(defvar ebox--scroll-rendered-content-lines-private-p nil
|
||||||
"Non-nil when scroll content line inputs may receive ownership in place.")
|
"Non-nil when scroll content line inputs may receive ownership in place.")
|
||||||
|
|
||||||
@ -260,6 +294,8 @@ and its formatted content exactly fills the used height."
|
|||||||
(source preserve-identities-p))
|
(source preserve-identities-p))
|
||||||
(declare-function ebox-surface--inline-inheritance-required-p "ebox-surface"
|
(declare-function ebox-surface--inline-inheritance-required-p "ebox-surface"
|
||||||
(root))
|
(root))
|
||||||
|
(declare-function ebox--add-render-face!
|
||||||
|
"ebox-render-context" (string start end face &optional append))
|
||||||
|
|
||||||
(defun ebox--register-window-line-renderer (type function)
|
(defun ebox--register-window-line-renderer (type function)
|
||||||
"Register FUNCTION as TYPE's lazy window-line renderer.
|
"Register FUNCTION as TYPE's lazy window-line renderer.
|
||||||
@ -389,12 +425,12 @@ START-INDEX is the first source line index represented by LINES."
|
|||||||
;; `ebox-string-lines' made these source lines private, and blank
|
;; `ebox-string-lines' made these source lines private, and blank
|
||||||
;; normalization above creates a fresh filler. Apply the same face
|
;; normalization above creates a fresh filler. Apply the same face
|
||||||
;; properties in place instead of copying every viewport-wide line.
|
;; properties in place instead of copying every viewport-wide line.
|
||||||
(when color
|
(when color
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 (length line) `(:foreground ,color) t line))
|
line 0 (length line) `(:foreground ,color) t))
|
||||||
(when bgcolor
|
(when bgcolor
|
||||||
(add-face-text-property
|
(ebox--add-render-face!
|
||||||
0 (length line) `(:background ,bgcolor) t line))
|
line 0 (length line) `(:background ,bgcolor) t))
|
||||||
line))
|
line))
|
||||||
(cl-loop for line in lines
|
(cl-loop for line in lines
|
||||||
for index from 0
|
for index from 0
|
||||||
@ -831,7 +867,9 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
|||||||
(when (and content-viewport
|
(when (and content-viewport
|
||||||
(not (eq (and (listp node) (plist-get node :ebox-type))
|
(not (eq (and (listp node) (plist-get node :ebox-type))
|
||||||
'box))
|
'box))
|
||||||
(not ebox--intrinsic-layout-measurement))
|
(not ebox--intrinsic-layout-measurement)
|
||||||
|
(equal (gethash content ebox--rendered-uniform-width-table)
|
||||||
|
content-viewport))
|
||||||
(plist-put box :ebox-content-width-exact-p t))
|
(plist-put box :ebox-content-width-exact-p t))
|
||||||
content))
|
content))
|
||||||
|
|
||||||
@ -1210,10 +1248,16 @@ Internal implementation of `ebox-render' for box nodes."
|
|||||||
(= border-left-pixel 0)
|
(= border-left-pixel 0)
|
||||||
(= border-right-pixel 0)
|
(= border-right-pixel 0)
|
||||||
(eq (ebox-get box :vertical-align) 'top))
|
(eq (ebox-get box :vertical-align) 'top))
|
||||||
(let ((ebox--scroll-rendered-content-lines-private-p
|
(if (plist-get scroll-window-result
|
||||||
private-formatted-lines-p))
|
:cached-rendered-lines-p)
|
||||||
(ebox--scroll-rendered-content-lines
|
;; The staged producer already applied content/owner
|
||||||
box formatted-lines region-id 0))))
|
;; properties. Re-running the generic ownership pass here
|
||||||
|
;; was the dominant grid-scroll stall.
|
||||||
|
formatted-lines
|
||||||
|
(let ((ebox--scroll-rendered-content-lines-private-p
|
||||||
|
private-formatted-lines-p))
|
||||||
|
(ebox--scroll-rendered-content-lines
|
||||||
|
box formatted-lines region-id 0)))))
|
||||||
|
|
||||||
(simple-scroll-rendered-lines
|
(simple-scroll-rendered-lines
|
||||||
(or underfilled-simple-scroll-lines
|
(or underfilled-simple-scroll-lines
|
||||||
@ -1315,7 +1359,8 @@ Internal implementation of `ebox-render' for box nodes."
|
|||||||
;; Intrinsic measurement must leave published scroll state alone: its
|
;; Intrinsic measurement must leave published scroll state alone: its
|
||||||
;; placeholder content always looks fully visible and would clear the
|
;; placeholder content always looks fully visible and would clear the
|
||||||
;; live lazy producer state.
|
;; live lazy producer state.
|
||||||
(unless ebox--intrinsic-layout-measurement
|
(unless (or ebox--intrinsic-layout-measurement
|
||||||
|
ebox--scroll-window-skip-state-rebuild-p)
|
||||||
(if (and (eq overflow 'scroll)
|
(if (and (eq overflow 'scroll)
|
||||||
(> text-height content-height))
|
(> text-height content-height))
|
||||||
(ebox--scroll-set-state
|
(ebox--scroll-set-state
|
||||||
@ -1520,21 +1565,25 @@ region/node identities and render side tables for this one materialization."
|
|||||||
"Render layout NODE to a multi-line string through a TP materialization.
|
"Render layout NODE to a multi-line string through a TP materialization.
|
||||||
NODE accepts every layout node supported by `ebox--render-layout'. The
|
NODE accepts every layout node supported by `ebox--render-layout'. The
|
||||||
materialized surface is ephemeral and creates no live buffer state."
|
materialized surface is ephemeral and creates no live buffer state."
|
||||||
(if (or ebox--surface-materialization-active
|
(let ((rendered
|
||||||
ebox--render-runtime-revision)
|
(if (or ebox--surface-materialization-active
|
||||||
(ebox--render-layout node)
|
ebox--render-runtime-revision)
|
||||||
(require 'ebox-surface)
|
(ebox--render-layout node)
|
||||||
(if (and (not (stringp node))
|
(require 'ebox-surface)
|
||||||
(not (ebox-style-cascade-active-p))
|
(if (and (not (stringp node))
|
||||||
(not (ebox-surface--inline-inheritance-required-p node)))
|
(not (ebox-style-cascade-active-p))
|
||||||
(let ((ebox--region-id-counter ebox--region-id-counter)
|
(not (ebox-surface--inline-inheritance-required-p node)))
|
||||||
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
(let ((ebox--region-id-counter ebox--region-id-counter)
|
||||||
(ebox--render-ephemeral-static node))
|
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||||
(let ((ebox--surface-materialization-active t)
|
(ebox--render-ephemeral-static node))
|
||||||
(ebox--region-id-counter ebox--region-id-counter)
|
(let ((ebox--surface-materialization-active t)
|
||||||
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
(ebox--region-id-counter ebox--region-id-counter)
|
||||||
(tp-surface-materialize-string
|
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||||
(ebox-surface-producer node nil t))))))
|
(tp-surface-materialize-string
|
||||||
|
(ebox-surface-producer node nil t)))))))
|
||||||
|
(unless ebox--paint-origin-capture-p
|
||||||
|
(ebox--strip-paint-origins! rendered))
|
||||||
|
rendered))
|
||||||
|
|
||||||
(defun ebox--width-pad (string extra-pixels)
|
(defun ebox--width-pad (string extra-pixels)
|
||||||
"Append a display-space of EXTRA-PIXELS to the right of every line in STRING.
|
"Append a display-space of EXTRA-PIXELS to the right of every line in STRING.
|
||||||
@ -1583,11 +1632,14 @@ PLIST accepts the same properties as `ebox-create', such as :width,
|
|||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-row (&rest nodes)
|
(defun ebox-row (&rest nodes)
|
||||||
"Return a lazy horizontal layout node from NODES.
|
"Return a lazy horizontal layout node from NODES.
|
||||||
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'."
|
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'.
|
||||||
|
A sole child Range keeps a material row parent so the Range stays addressable."
|
||||||
(setq nodes (delq nil nodes))
|
(setq nodes (delq nil nodes))
|
||||||
(cond
|
(cond
|
||||||
((null nodes) (ebox-spacer))
|
((null nodes) (ebox-spacer))
|
||||||
((null (cdr nodes)) (car nodes))
|
((and (null (cdr nodes))
|
||||||
|
(not (ebox-child-range--descriptor-p (car nodes))))
|
||||||
|
(car nodes))
|
||||||
(t (list :ebox-type 'concat
|
(t (list :ebox-type 'concat
|
||||||
:display '(block row)
|
:display '(block row)
|
||||||
:children nodes))))
|
:children nodes))))
|
||||||
@ -1598,13 +1650,17 @@ Children are rendered; shorter children are padded with blank lines so all
|
|||||||
reach the same height before horizontal concatenation."
|
reach the same height before horizontal concatenation."
|
||||||
(let* ((rendered (mapcar #'ebox--render-with-cache
|
(let* ((rendered (mapcar #'ebox--render-with-cache
|
||||||
(ebox--layout-children node)))
|
(ebox--layout-children node)))
|
||||||
(max-h (apply #'max (mapcar #'ebox-string-height rendered)))
|
(max-h (if rendered
|
||||||
|
(apply #'max (mapcar #'ebox-string-height rendered))
|
||||||
|
0))
|
||||||
(line-lists
|
(line-lists
|
||||||
(mapcar (lambda (string)
|
(mapcar (lambda (string)
|
||||||
(ebox-string-lines
|
(ebox-string-lines
|
||||||
(ebox--height-pad string max-h)))
|
(ebox--height-pad string max-h)))
|
||||||
rendered)))
|
rendered)))
|
||||||
(ebox-lines-join (apply #'cl-mapcar #'concat line-lists))))
|
(if line-lists
|
||||||
|
(ebox-lines-join (apply #'cl-mapcar #'concat line-lists))
|
||||||
|
"")))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-stack (node1 node2)
|
(defun ebox-stack (node1 node2)
|
||||||
@ -1623,11 +1679,15 @@ is called. To stack more than two boxes, nest calls:
|
|||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-column (&rest nodes)
|
(defun ebox-column (&rest nodes)
|
||||||
"Return a lazy vertical layout node from NODES.
|
"Return a lazy vertical layout node from NODES.
|
||||||
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'."
|
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'.
|
||||||
|
A sole child Range keeps a material column parent so the Range stays
|
||||||
|
addressable."
|
||||||
(setq nodes (delq nil nodes))
|
(setq nodes (delq nil nodes))
|
||||||
(cond
|
(cond
|
||||||
((null nodes) (ebox-spacer))
|
((null nodes) (ebox-spacer))
|
||||||
((null (cdr nodes)) (car nodes))
|
((and (null (cdr nodes))
|
||||||
|
(not (ebox-child-range--descriptor-p (car nodes))))
|
||||||
|
(car nodes))
|
||||||
(t (list :ebox-type 'stack
|
(t (list :ebox-type 'stack
|
||||||
:display '(block column)
|
:display '(block column)
|
||||||
:children nodes))))
|
:children nodes))))
|
||||||
@ -2497,8 +2557,19 @@ no included vertical margin."
|
|||||||
for source-line in content-lines
|
for source-line in content-lines
|
||||||
for idx from start-index
|
for idx from start-index
|
||||||
collect
|
collect
|
||||||
(let* ((blank-replacement-p
|
(let* ((child-ownership-uniform-p
|
||||||
|
(cl-every
|
||||||
|
(lambda (property)
|
||||||
|
(not
|
||||||
|
(text-property-not-all
|
||||||
|
0 (length source-line) property
|
||||||
|
(get-text-property 0 property source-line)
|
||||||
|
source-line)))
|
||||||
|
'(ebox-content ebox-content-idx
|
||||||
|
ebox-content-owner ebox-content-owners)))
|
||||||
|
(blank-replacement-p
|
||||||
(and (string-blank-p source-line)
|
(and (string-blank-p source-line)
|
||||||
|
child-ownership-uniform-p
|
||||||
(not (ebox--line-has-non-content-properties-p
|
(not (ebox--line-has-non-content-properties-p
|
||||||
source-line))))
|
source-line))))
|
||||||
(line
|
(line
|
||||||
@ -2508,16 +2579,36 @@ no included vertical margin."
|
|||||||
0 'ebox-content source-line))
|
0 'ebox-content source-line))
|
||||||
(content-idx
|
(content-idx
|
||||||
(get-text-property
|
(get-text-property
|
||||||
0 'ebox-content-idx source-line)))
|
0 'ebox-content-idx source-line))
|
||||||
(if content-id
|
(content-owner
|
||||||
(propertize padding-line-filler
|
(get-text-property
|
||||||
'ebox-content content-id
|
0 'ebox-content-owner source-line))
|
||||||
'ebox-content-idx content-idx
|
(content-owners
|
||||||
'ebox-content-owner region-id)
|
(get-text-property
|
||||||
(propertize padding-line-filler
|
0 'ebox-content-owners source-line)))
|
||||||
'ebox-content region-id
|
(if (or content-owner content-owners)
|
||||||
'ebox-content-idx idx
|
(ebox--propertize-content-line
|
||||||
'ebox-content-owner region-id)))
|
(propertize
|
||||||
|
padding-line-filler
|
||||||
|
'ebox-content content-id
|
||||||
|
'ebox-content-idx content-idx
|
||||||
|
'ebox-content-owner
|
||||||
|
(or content-owner content-id)
|
||||||
|
'ebox-content-owners
|
||||||
|
(or content-owners
|
||||||
|
(list content-owner)))
|
||||||
|
region-id idx padding-line-filler)
|
||||||
|
(if content-id
|
||||||
|
(propertize
|
||||||
|
padding-line-filler
|
||||||
|
'ebox-content content-id
|
||||||
|
'ebox-content-idx content-idx
|
||||||
|
'ebox-content-owner region-id)
|
||||||
|
(propertize
|
||||||
|
padding-line-filler
|
||||||
|
'ebox-content region-id
|
||||||
|
'ebox-content-idx idx
|
||||||
|
'ebox-content-owner region-id))))
|
||||||
(let* ((colored-p (or color bgcolor))
|
(let* ((colored-p (or color bgcolor))
|
||||||
(owned-source
|
(owned-source
|
||||||
(if colored-p
|
(if colored-p
|
||||||
@ -2833,6 +2924,36 @@ only used for incomplete lazy prefixes."
|
|||||||
|
|
||||||
(defun ebox--format-scroll-window-content (box)
|
(defun ebox--format-scroll-window-content (box)
|
||||||
"Return a lazily rendered scroll content window for BOX, or nil."
|
"Return a lazily rendered scroll content window for BOX, or nil."
|
||||||
|
(catch 'ebox-scroll-window-cached
|
||||||
|
(let* ((cached ebox--scroll-window-cached-state)
|
||||||
|
(cached-box (and cached (plist-get cached :box)))
|
||||||
|
(cached-id (and cached-box (plist-get cached-box :node-id)))
|
||||||
|
(box-id (and (listp box) (plist-get box :node-id)))
|
||||||
|
(cached-rendered-lines
|
||||||
|
(and cached (plist-get cached :rendered-content-lines)))
|
||||||
|
(use-cached-rendered-lines-p
|
||||||
|
(and ebox--scroll-window-skip-state-rebuild-p
|
||||||
|
cached-rendered-lines))
|
||||||
|
(cached-lines (and cached
|
||||||
|
(or (and use-cached-rendered-lines-p
|
||||||
|
cached-rendered-lines)
|
||||||
|
(plist-get cached :content-lines)))))
|
||||||
|
(when (and cached cached-box cached-lines box-id
|
||||||
|
(equal box-id cached-id)
|
||||||
|
(eq (ebox-get box :overflow) 'scroll))
|
||||||
|
(setq ebox--scroll-window-render-result
|
||||||
|
(list :complete (plist-get cached :content-lines-complete-p)
|
||||||
|
:content-height (plist-get cached :content-height)
|
||||||
|
:scroll-window-producer-spec
|
||||||
|
(plist-get cached :scroll-window-producer-spec)
|
||||||
|
:render-content-prefix
|
||||||
|
(plist-get cached :render-content-prefix)
|
||||||
|
:materialize-content-lines
|
||||||
|
(plist-get cached :materialize-content-lines)
|
||||||
|
:cached-rendered-lines-p
|
||||||
|
(and use-cached-rendered-lines-p t)))
|
||||||
|
(throw 'ebox-scroll-window-cached
|
||||||
|
(ebox-lines-join cached-lines))))
|
||||||
(let ((content-height (ebox--scroll-window-content-height box))
|
(let ((content-height (ebox--scroll-window-content-height box))
|
||||||
(scroll-offset (max 0 (or (ebox-get box :scroll-offset) 0)))
|
(scroll-offset (max 0 (or (ebox-get box :scroll-offset) 0)))
|
||||||
(content-viewport (ebox--wrapper-content-viewport-pixel box))
|
(content-viewport (ebox--wrapper-content-viewport-pixel box))
|
||||||
@ -2882,7 +3003,7 @@ only used for incomplete lazy prefixes."
|
|||||||
(ebox-lines-join
|
(ebox-lines-join
|
||||||
(if (plist-get window :complete)
|
(if (plist-get window :complete)
|
||||||
formatted-lines
|
formatted-lines
|
||||||
(seq-take formatted-lines target-lines)))))))))
|
(seq-take formatted-lines target-lines))))))))))
|
||||||
|
|
||||||
(defun ebox--render-stack (node)
|
(defun ebox--render-stack (node)
|
||||||
"Render a stack NODE to a string (internal, called by `ebox-render').
|
"Render a stack NODE to a string (internal, called by `ebox-render').
|
||||||
@ -2898,7 +3019,9 @@ stack itself uses its natural max child width."
|
|||||||
(cons rendered
|
(cons rendered
|
||||||
(ebox--string-max-pixel-width rendered))))
|
(ebox--string-max-pixel-width rendered))))
|
||||||
(ebox--stack-leaves node)))
|
(ebox--stack-leaves node)))
|
||||||
(max-w (apply #'max (mapcar #'cdr rendered-items)))
|
(max-w (if rendered-items
|
||||||
|
(apply #'max (mapcar #'cdr rendered-items))
|
||||||
|
0))
|
||||||
(target-w (if ebox--intrinsic-layout-measurement
|
(target-w (if ebox--intrinsic-layout-measurement
|
||||||
max-w
|
max-w
|
||||||
(or (ebox--viewport-pixel-width nil) max-w))))
|
(or (ebox--viewport-pixel-width nil) max-w))))
|
||||||
|
|||||||
@ -1812,7 +1812,7 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
|||||||
content-min
|
content-min
|
||||||
(not (eq content-min missing))))
|
(not (eq content-min missing))))
|
||||||
(ebox-native-reflow--compile-flex-item item axis)))
|
(ebox-native-reflow--compile-flex-item item axis)))
|
||||||
(plist-get node :children))))))
|
(ebox-tree-layout-children node))))))
|
||||||
(if wrapper
|
(if wrapper
|
||||||
(ebox-native-reflow--compile-box wrapper inner t)
|
(ebox-native-reflow--compile-box wrapper inner t)
|
||||||
inner)))
|
inner)))
|
||||||
@ -1842,6 +1842,7 @@ Grid deliberately stays on the ordinary Ebox renderer until the native
|
|||||||
backend has a matching two-dimensional layout contract."
|
backend has a matching two-dimensional layout contract."
|
||||||
(cond
|
(cond
|
||||||
((or (null node) (stringp node) (not (listp node))) t)
|
((or (null node) (stringp node) (not (listp node))) t)
|
||||||
|
((plist-get node :ebox-child-sequence) nil)
|
||||||
((eq (plist-get node :ebox-type) 'grid) nil)
|
((eq (plist-get node :ebox-type) 'grid) nil)
|
||||||
((eq (plist-get node :ebox-type) 'box)
|
((eq (plist-get node :ebox-type) 'box)
|
||||||
(ebox-native-reflow--native-node-supported-p
|
(ebox-native-reflow--native-node-supported-p
|
||||||
@ -1853,7 +1854,7 @@ backend has a matching two-dimensional layout contract."
|
|||||||
(cl-every #'ebox-native-reflow--native-node-supported-p
|
(cl-every #'ebox-native-reflow--native-node-supported-p
|
||||||
(append (when-let ((box (plist-get node :box)))
|
(append (when-let ((box (plist-get node :box)))
|
||||||
(list box))
|
(list box))
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
((eq (plist-get node :ebox-type) 'flex-item)
|
((eq (plist-get node :ebox-type) 'flex-item)
|
||||||
(ebox-native-reflow--native-node-supported-p (plist-get node :node)))
|
(ebox-native-reflow--native-node-supported-p (plist-get node :node)))
|
||||||
(t nil)))
|
(t nil)))
|
||||||
|
|||||||
@ -22,6 +22,17 @@
|
|||||||
(defvar ebox--render-owned-text-values nil
|
(defvar ebox--render-owned-text-values nil
|
||||||
"Candidate-local text-property values explicitly created by Ebox.")
|
"Candidate-local text-property values explicitly created by Ebox.")
|
||||||
|
|
||||||
|
(cl-defstruct (ebox--paint-origin
|
||||||
|
(:constructor ebox--paint-origin-create))
|
||||||
|
"Transient caller-face baseline captured before Ebox paint composition."
|
||||||
|
baseline)
|
||||||
|
|
||||||
|
(defconst ebox--paint-origin-property 'ebox--paint-origin
|
||||||
|
"Private transient property carrying one render-time paint baseline.")
|
||||||
|
|
||||||
|
(defvar ebox--paint-origin-capture-p nil
|
||||||
|
"Non-nil while production Ebox rendering captures paint origins.")
|
||||||
|
|
||||||
(defvar ebox--render-output-provenance-table
|
(defvar ebox--render-output-provenance-table
|
||||||
(make-hash-table :test #'eq :weakness 'key)
|
(make-hash-table :test #'eq :weakness 'key)
|
||||||
"Weak map from rendered strings to Ebox-owned property values.")
|
"Weak map from rendered strings to Ebox-owned property values.")
|
||||||
@ -50,6 +61,47 @@ REGISTRY defaults to the active render candidate."
|
|||||||
(when-let ((values (ebox--render-owned-text-values-for property)))
|
(when-let ((values (ebox--render-owned-text-values-for property)))
|
||||||
(gethash value values))))
|
(gethash value values))))
|
||||||
|
|
||||||
|
(defun ebox--capture-paint-origins! (string start end)
|
||||||
|
"Capture caller FACE baselines on STRING between START and END.
|
||||||
|
Existing origins are preserved so nested Ebox contributions never replace the
|
||||||
|
original caller baseline. The property is stripped before a TP plan leaves
|
||||||
|
the render boundary."
|
||||||
|
(when (and ebox--paint-origin-capture-p
|
||||||
|
(stringp string)
|
||||||
|
(< start end))
|
||||||
|
(let ((position start))
|
||||||
|
(while (< position end)
|
||||||
|
(let* ((next (min end
|
||||||
|
(or (next-single-property-change
|
||||||
|
position ebox--paint-origin-property string end)
|
||||||
|
end)))
|
||||||
|
(origin (get-text-property
|
||||||
|
position ebox--paint-origin-property string)))
|
||||||
|
(unless origin
|
||||||
|
(put-text-property
|
||||||
|
position next ebox--paint-origin-property
|
||||||
|
(ebox--paint-origin-create
|
||||||
|
:baseline (copy-tree (get-text-property position 'face string)))
|
||||||
|
string))
|
||||||
|
(setq position (max next (1+ position)))))))
|
||||||
|
string)
|
||||||
|
|
||||||
|
(defun ebox--add-render-face! (string start end face &optional append)
|
||||||
|
"Add Ebox FACE to STRING after capturing its caller baseline."
|
||||||
|
(ebox--capture-paint-origins! string start end)
|
||||||
|
(add-face-text-property start end face append string))
|
||||||
|
|
||||||
|
(defun ebox--strip-paint-origins! (string)
|
||||||
|
"Remove transient caller-face carriers from STRING before it escapes.
|
||||||
|
The carrier is retained only while an active surface producer harvests its
|
||||||
|
baseline. Public/ephemeral renders must never expose this private property,
|
||||||
|
including strings returned from a render-cache hit."
|
||||||
|
(when (and (stringp string)
|
||||||
|
(> (length string) 0))
|
||||||
|
(remove-text-properties
|
||||||
|
0 (length string) (list ebox--paint-origin-property nil) string))
|
||||||
|
string)
|
||||||
|
|
||||||
(defun ebox--register-render-owned-face-values (source rendered)
|
(defun ebox--register-render-owned-face-values (source rendered)
|
||||||
"Register Ebox-generated face identities added to RENDERED from SOURCE."
|
"Register Ebox-generated face identities added to RENDERED from SOURCE."
|
||||||
(when (and (stringp source)
|
(when (and (stringp source)
|
||||||
|
|||||||
244
ebox-style.el
244
ebox-style.el
@ -227,6 +227,18 @@
|
|||||||
(defvar ebox-style-schemas (ecss-schema-set-create)
|
(defvar ebox-style-schemas (ecss-schema-set-create)
|
||||||
"ECSS property schemas for the isolated Ebox style domain.")
|
"ECSS property schemas for the isolated Ebox style domain.")
|
||||||
|
|
||||||
|
(defvar ebox-style--registered-schemas nil
|
||||||
|
"Schema-set object whose Ebox properties are already registered.
|
||||||
|
`ecss-schema-set-property' returns a detached schema copy, so probing it on
|
||||||
|
every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||||
|
|
||||||
|
(defconst ebox-style--declaration-cache-max-entries 512
|
||||||
|
"Maximum canonical declaration compilations retained by Ebox.")
|
||||||
|
|
||||||
|
(defvar ebox-style--declaration-cache
|
||||||
|
(make-hash-table :test 'equal)
|
||||||
|
"Bounded memo table for canonical Ebox declaration compilation.")
|
||||||
|
|
||||||
(defun ebox-style--ensure-property-table ()
|
(defun ebox-style--ensure-property-table ()
|
||||||
"Return the canonical style property registry table."
|
"Return the canonical style property registry table."
|
||||||
(or ebox-style--property-table
|
(or ebox-style--property-table
|
||||||
@ -590,12 +602,18 @@
|
|||||||
(dolist (property ebox-style--property-definitions)
|
(dolist (property ebox-style--property-definitions)
|
||||||
(apply #'ecss-schema-set-define
|
(apply #'ecss-schema-set-define
|
||||||
ebox-style-schemas (plist-get property :id)
|
ebox-style-schemas (plist-get property :id)
|
||||||
(ebox-style--schema-options property))))
|
(ebox-style--schema-options property)))
|
||||||
|
(setq ebox-style--registered-schemas ebox-style-schemas))
|
||||||
|
|
||||||
(defun ebox-style--ensure-properties ()
|
(defun ebox-style--ensure-properties ()
|
||||||
"Ensure Ebox property schemas exist in the isolated ECSS schema set."
|
"Ensure Ebox property schemas exist in the isolated ECSS schema set."
|
||||||
(unless (ecss-schema-set-property ebox-style-schemas 'ebox/color)
|
(unless (eq ebox-style--registered-schemas ebox-style-schemas)
|
||||||
(ebox-style-register-properties)))
|
;; Only the first call for a new schema-set needs to inspect/register the
|
||||||
|
;; domain. Avoid the detached `ecss-schema-set-property' accessor on the
|
||||||
|
;; hot `ebox-create' path; it recursively copies schema metadata.
|
||||||
|
(if (ecss-schema-set-property ebox-style-schemas 'ebox/color)
|
||||||
|
(setq ebox-style--registered-schemas ebox-style-schemas)
|
||||||
|
(ebox-style-register-properties))))
|
||||||
|
|
||||||
(defvar ebox-style-stylesheet (ecss-stylesheet-create)
|
(defvar ebox-style-stylesheet (ecss-stylesheet-create)
|
||||||
"Isolated ECSS stylesheet containing Ebox layout and paint rules.")
|
"Isolated ECSS stylesheet containing Ebox layout and paint rules.")
|
||||||
@ -628,6 +646,21 @@ cascade pass during its initial static projection."
|
|||||||
"Return non-nil when PLIST is a proper even-length list."
|
"Return non-nil when PLIST is a proper even-length list."
|
||||||
(and (proper-list-p plist) (zerop (% (length plist) 2))))
|
(and (proper-list-p plist) (zerop (% (length plist) 2))))
|
||||||
|
|
||||||
|
(defun ebox-style--declaration-cache-key (plist strict)
|
||||||
|
"Return a stable style-only cache key for PLIST and STRICT.
|
||||||
|
Non-style node properties such as content, Host references, and callbacks do
|
||||||
|
not affect declaration compilation and are intentionally excluded."
|
||||||
|
(let (canonical)
|
||||||
|
(cl-loop for (property value) on plist by #'cddr
|
||||||
|
for schema = (ebox-style-schema-id property)
|
||||||
|
if schema
|
||||||
|
do (setq canonical
|
||||||
|
(append canonical (list schema value)))
|
||||||
|
else if (ebox-style--custom-property-p property)
|
||||||
|
do (setq canonical
|
||||||
|
(append canonical (list property value))))
|
||||||
|
(list strict canonical)))
|
||||||
|
|
||||||
(defun ebox-style--static-invalid-declaration-p (validator value)
|
(defun ebox-style--static-invalid-declaration-p (validator value)
|
||||||
"Return non-nil when VALIDATOR rejects static VALUE."
|
"Return non-nil when VALIDATOR rejects static VALUE."
|
||||||
(pcase validator
|
(pcase validator
|
||||||
@ -657,17 +690,29 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
|
|||||||
(ebox-style--ensure-properties)
|
(ebox-style--ensure-properties)
|
||||||
(unless (ebox-style--valid-plist-p plist)
|
(unless (ebox-style--valid-plist-p plist)
|
||||||
(user-error "Ebox style declarations must be an even property list"))
|
(user-error "Ebox style declarations must be an even property list"))
|
||||||
(let ((canonical
|
(let* ((cache-key (ebox-style--declaration-cache-key plist strict))
|
||||||
(cl-loop for (property value) on plist by #'cddr
|
(missing (make-symbol "ebox-style-declaration-cache-missing"))
|
||||||
for schema = (ebox-style-schema-id property)
|
(cached (gethash cache-key ebox-style--declaration-cache missing)))
|
||||||
if schema append (list schema value)
|
(if (not (eq cached missing))
|
||||||
else if (ebox-style--custom-property-p property)
|
;; Canonical declaration plists are immutable after compilation;
|
||||||
append (list property value)
|
;; return a fresh top-level plist so callers own their list spine
|
||||||
else if strict
|
;; without re-running ECSS's recursive boundary copier.
|
||||||
do (user-error "Unknown Ebox style property: %S"
|
(copy-sequence cached)
|
||||||
property))))
|
(let ((canonical (cadr cache-key)))
|
||||||
(ebox-style--validate-declaration-values
|
(when strict
|
||||||
(ecss-merge-declarations ebox-style-schemas canonical))))
|
(cl-loop for (property _value) on plist by #'cddr
|
||||||
|
unless (or (ebox-style-schema-id property)
|
||||||
|
(ebox-style--custom-property-p property))
|
||||||
|
do (user-error "Unknown Ebox style property: %S"
|
||||||
|
property)))
|
||||||
|
(let ((compiled
|
||||||
|
(ebox-style--validate-declaration-values
|
||||||
|
(ecss-merge-declarations ebox-style-schemas canonical))))
|
||||||
|
(when (>= (hash-table-count ebox-style--declaration-cache)
|
||||||
|
ebox-style--declaration-cache-max-entries)
|
||||||
|
(clrhash ebox-style--declaration-cache))
|
||||||
|
(puthash cache-key compiled ebox-style--declaration-cache)
|
||||||
|
(copy-sequence compiled))))))
|
||||||
|
|
||||||
(defun ebox-style-merge-declarations (base overrides)
|
(defun ebox-style-merge-declarations (base overrides)
|
||||||
"Merge canonical BASE and OVERRIDES through ECSS property schemas."
|
"Merge canonical BASE and OVERRIDES through ECSS property schemas."
|
||||||
@ -693,6 +738,177 @@ ORIGIN, LAYER, and SCOPE use ECSS cascade semantics."
|
|||||||
:stylesheet ebox-style-stylesheet :parent-style parent-style
|
:stylesheet ebox-style-stylesheet :parent-style parent-style
|
||||||
:value-resolver #'tp-resolve-value))
|
:value-resolver #'tp-resolve-value))
|
||||||
|
|
||||||
|
(defconst ebox-style--theme-delta-properties
|
||||||
|
'(ebox/color ebox/background-color)
|
||||||
|
"Canonical paint properties eligible for a declaration-only Theme delta.")
|
||||||
|
|
||||||
|
(defconst ebox-style--paint-delta-properties
|
||||||
|
'(ebox/color ebox/background-color
|
||||||
|
ebox/border-top-color ebox/border-right-color
|
||||||
|
ebox/border-bottom-color ebox/border-left-color)
|
||||||
|
"Canonical paint properties that may change without changing geometry.")
|
||||||
|
|
||||||
|
(defun ebox-style--without-theme-delta-properties (declarations)
|
||||||
|
"Return DECLARATIONS without the canonical Theme paint properties."
|
||||||
|
(cl-loop for (property value) on declarations by #'cddr
|
||||||
|
unless (memq property ebox-style--theme-delta-properties)
|
||||||
|
append (list property value)))
|
||||||
|
|
||||||
|
(defun ebox-style--without-paint-delta-properties (declarations)
|
||||||
|
"Return DECLARATIONS without canonical paint-only properties."
|
||||||
|
(cl-loop for (property value) on declarations by #'cddr
|
||||||
|
unless (memq property ebox-style--paint-delta-properties)
|
||||||
|
append (list property value)))
|
||||||
|
|
||||||
|
(defun ebox-style--paint-declarations-equivalent-p (old new)
|
||||||
|
"Return non-nil when OLD and NEW differ only in paint declarations.
|
||||||
|
All paint properties retain presence parity; layout, structure, and typography
|
||||||
|
declarations must remain identical."
|
||||||
|
(and (equal (ebox-style--without-paint-delta-properties old)
|
||||||
|
(ebox-style--without-paint-delta-properties new))
|
||||||
|
(cl-every
|
||||||
|
(lambda (property)
|
||||||
|
(= (if (plist-member old property) 1 0)
|
||||||
|
(if (plist-member new property) 1 0)))
|
||||||
|
ebox-style--paint-delta-properties)))
|
||||||
|
|
||||||
|
(defun ebox-style--theme-declarations-equivalent-p (old new)
|
||||||
|
"Return non-nil when OLD and NEW differ only in explicit Theme paint values.
|
||||||
|
Both paint properties must retain presence parity and an explicit foreground
|
||||||
|
winner is required so parent inherited geometry/paint cannot be stale."
|
||||||
|
(and (equal (ebox-style--without-theme-delta-properties old)
|
||||||
|
(ebox-style--without-theme-delta-properties new))
|
||||||
|
(cl-every
|
||||||
|
(lambda (property)
|
||||||
|
(= (if (plist-member old property) 1 0)
|
||||||
|
(if (plist-member new property) 1 0)))
|
||||||
|
ebox-style--theme-delta-properties)
|
||||||
|
(plist-member old 'ebox/color)
|
||||||
|
(plist-member new 'ebox/color)))
|
||||||
|
|
||||||
|
(defconst ebox-style--inherited-properties
|
||||||
|
'(ebox/color ebox/font ebox/font-family ebox/font-height
|
||||||
|
ebox/font-weight ebox/font-slant)
|
||||||
|
"Inherited ECSS properties whose parent values guard Theme deltas.")
|
||||||
|
|
||||||
|
(defun ebox-style--inherited-style-signature (style)
|
||||||
|
"Return the inherited-value fingerprint of computed STYLE, or nil.
|
||||||
|
The fingerprint deliberately contains only values that can flow from a
|
||||||
|
parent into a descendant, plus custom properties which can feed any
|
||||||
|
`var' expression. A Theme delta may reuse a computed style only when this
|
||||||
|
fingerprint is unchanged between the old and candidate parent."
|
||||||
|
(when (ecss-computed-style-p style)
|
||||||
|
(list
|
||||||
|
:inherited
|
||||||
|
(mapcar (lambda (property)
|
||||||
|
(cons property (ecss-computed-style-value style property)))
|
||||||
|
ebox-style--inherited-properties)
|
||||||
|
:custom-properties (ecss-computed-style-custom-properties style))))
|
||||||
|
|
||||||
|
(defun ebox-style--theme-delta-computed
|
||||||
|
(style old-declarations new-declarations
|
||||||
|
&optional old-parent-style new-parent-style)
|
||||||
|
"Return a cheap STYLE copy for a safe Theme paint-only declaration delta.
|
||||||
|
OLD-DECLARATIONS and NEW-DECLARATIONS are the canonical ECSS plists.
|
||||||
|
OLD-PARENT-STYLE and NEW-PARENT-STYLE are the previous and candidate
|
||||||
|
computed parent styles, when the subject has an inherited parent.
|
||||||
|
|
||||||
|
The cascade selector facts and every non-Theme declaration must be unchanged;
|
||||||
|
both Theme properties must keep the same explicit-presence state, and the
|
||||||
|
inherited parent fingerprint must be unchanged. In that case only the
|
||||||
|
computed values and winner metadata for those properties change. Any other
|
||||||
|
shape returns nil so the caller uses the full ECSS computation."
|
||||||
|
(when (and (ecss-computed-style-p style)
|
||||||
|
(ebox-style--theme-declarations-equivalent-p
|
||||||
|
old-declarations new-declarations))
|
||||||
|
(let ((changed-p
|
||||||
|
(cl-some
|
||||||
|
(lambda (property)
|
||||||
|
(and (plist-member old-declarations property)
|
||||||
|
(plist-member new-declarations property)
|
||||||
|
(not (equal (plist-get old-declarations property)
|
||||||
|
(plist-get new-declarations property)))))
|
||||||
|
ebox-style--theme-delta-properties)))
|
||||||
|
(when (and changed-p
|
||||||
|
(equal (ebox-style--inherited-style-signature
|
||||||
|
old-parent-style)
|
||||||
|
(ebox-style--inherited-style-signature
|
||||||
|
new-parent-style)))
|
||||||
|
(let ((values (copy-tree (ecss-computed-style-values style)))
|
||||||
|
(active (copy-sequence
|
||||||
|
(ecss-computed-style-active-properties style)))
|
||||||
|
(specified (copy-sequence
|
||||||
|
(ecss-computed-style-specified-properties style))))
|
||||||
|
(dolist (property ebox-style--theme-delta-properties)
|
||||||
|
(let ((id (ebox-style-schema-id property)))
|
||||||
|
(when (plist-member new-declarations property)
|
||||||
|
(setf values
|
||||||
|
(plist-put values id
|
||||||
|
(copy-tree (plist-get new-declarations property))))
|
||||||
|
(cl-pushnew id active)
|
||||||
|
(cl-pushnew id specified))))
|
||||||
|
(ecss-computed-style-copy-with-values
|
||||||
|
style values :active-properties active :specified-properties specified))))))
|
||||||
|
|
||||||
|
(defun ebox-style--inherited-style-signature-without-theme-color (style)
|
||||||
|
"Return STYLE's inherited fingerprint excluding the Theme color value.
|
||||||
|
The caller uses this to prove that a parent update changes only the inherited
|
||||||
|
foreground value; fonts, custom properties, and all other inherited facts must
|
||||||
|
remain identical before a computed child style may be copied."
|
||||||
|
(when (ecss-computed-style-p style)
|
||||||
|
(list
|
||||||
|
:inherited
|
||||||
|
(mapcar
|
||||||
|
(lambda (property)
|
||||||
|
(cons property (ecss-computed-style-value style property)))
|
||||||
|
(delq 'ebox/color (copy-sequence ebox-style--inherited-properties)))
|
||||||
|
:custom-properties (ecss-computed-style-custom-properties style))))
|
||||||
|
|
||||||
|
(defun ebox-style--theme-parent-delta-computed
|
||||||
|
(style declarations old-parent-style new-parent-style)
|
||||||
|
"Return a copied STYLE when a static parent changes only Theme color.
|
||||||
|
For an explicit child color, STYLE is reusable unchanged. For an inherited
|
||||||
|
child color, the copied value follows NEW-PARENT-STYLE. OLD-PARENT-STYLE and
|
||||||
|
NEW-PARENT-STYLE must have identical non-color inherited fingerprints; any
|
||||||
|
provenance or parent-shape mismatch returns nil for normal ECSS computation."
|
||||||
|
(when (and (ecss-computed-style-p style)
|
||||||
|
(proper-list-p declarations)
|
||||||
|
(ecss-computed-style-p old-parent-style)
|
||||||
|
(ecss-computed-style-p new-parent-style)
|
||||||
|
(equal
|
||||||
|
(ebox-style--inherited-style-signature-without-theme-color
|
||||||
|
old-parent-style)
|
||||||
|
(ebox-style--inherited-style-signature-without-theme-color
|
||||||
|
new-parent-style)))
|
||||||
|
(if (ecss-computed-style-specified-p style 'ebox/color)
|
||||||
|
(ecss-computed-style-copy-with-values
|
||||||
|
style (ecss-computed-style-values style))
|
||||||
|
(when (and (equal (ecss-computed-style-value
|
||||||
|
style 'ebox/color)
|
||||||
|
(ecss-computed-style-value
|
||||||
|
old-parent-style 'ebox/color))
|
||||||
|
(not (equal (ecss-computed-style-value
|
||||||
|
old-parent-style 'ebox/color)
|
||||||
|
(ecss-computed-style-value
|
||||||
|
new-parent-style 'ebox/color))))
|
||||||
|
(let ((values (copy-tree (ecss-computed-style-values style))))
|
||||||
|
(setq values
|
||||||
|
(plist-put values 'ebox/color
|
||||||
|
(copy-tree
|
||||||
|
(ecss-computed-style-value
|
||||||
|
new-parent-style 'ebox/color))))
|
||||||
|
(ecss-computed-style-copy-with-values style values))))))
|
||||||
|
|
||||||
|
(defun ebox-style--theme-inherited-delta-computed
|
||||||
|
(style declarations old-parent-style new-parent-style)
|
||||||
|
"Return a copied STYLE when only inherited Theme color changed.
|
||||||
|
This compatibility entry point requires STYLE to inherit its color; explicit
|
||||||
|
child colors are handled by `ebox-style--theme-parent-delta-computed'."
|
||||||
|
(when (and (ecss-computed-style-p style)
|
||||||
|
(not (ecss-computed-style-specified-p style 'ebox/color)))
|
||||||
|
(ebox-style--theme-parent-delta-computed
|
||||||
|
style declarations old-parent-style new-parent-style)))
|
||||||
|
|
||||||
(defun ebox-style--public-computed-values (style)
|
(defun ebox-style--public-computed-values (style)
|
||||||
"Return STYLE values using canonical public Ebox property names."
|
"Return STYLE values using canonical public Ebox property names."
|
||||||
(let ((values (ecss-computed-style-values style)) result)
|
(let ((values (ecss-computed-style-values style)) result)
|
||||||
|
|||||||
1559
ebox-surface.el
1559
ebox-surface.el
File diff suppressed because it is too large
Load Diff
313
ebox-tree.el
313
ebox-tree.el
@ -12,6 +12,7 @@
|
|||||||
(require 'subr-x)
|
(require 'subr-x)
|
||||||
(require 'ecss-selector)
|
(require 'ecss-selector)
|
||||||
(require 'ebox-style)
|
(require 'ebox-style)
|
||||||
|
(require 'ebox-child-range)
|
||||||
|
|
||||||
(defconst ebox--default-display '(block flow)
|
(defconst ebox--default-display '(block flow)
|
||||||
"Default CSS-like display for a plain Ebox box.")
|
"Default CSS-like display for a plain Ebox box.")
|
||||||
@ -138,11 +139,11 @@ This is an optimization for snapshot capture, not buffer runtime state.")
|
|||||||
('flex
|
('flex
|
||||||
(append (when-let ((box (plist-get node :box)))
|
(append (when-let ((box (plist-get node :box)))
|
||||||
(list box))
|
(list box))
|
||||||
(plist-get node :children)))
|
(ebox-tree-layout-children node)))
|
||||||
('grid
|
('grid
|
||||||
(append (when-let ((box (plist-get node :box)))
|
(append (when-let ((box (plist-get node :box)))
|
||||||
(list box))
|
(list box))
|
||||||
(plist-get node :children)))
|
(ebox-tree-layout-children node)))
|
||||||
('flex-item
|
('flex-item
|
||||||
(delq nil (list (plist-get node :node))))
|
(delq nil (list (plist-get node :node))))
|
||||||
(_ nil)))
|
(_ nil)))
|
||||||
@ -157,15 +158,17 @@ must not modify a published runtime tree through this accessor."
|
|||||||
"Return layout container children for NODE in render order.
|
"Return layout container children for NODE in render order.
|
||||||
New row/column containers store flat `:children'. Legacy `ebox-concat' and
|
New row/column containers store flat `:children'. Legacy `ebox-concat' and
|
||||||
`ebox-stack' nodes may still carry `:left'/`:right' or `:top'/`:bottom'."
|
`ebox-stack' nodes may still carry `:left'/`:right' or `:top'/`:bottom'."
|
||||||
(or (plist-get node :children)
|
(if (plist-member node :ebox-child-sequence)
|
||||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
(ebox-child-range--flatten (plist-get node :ebox-child-sequence))
|
||||||
('concat
|
(or (plist-get node :children)
|
||||||
(delq nil (list (plist-get node :left)
|
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||||
(plist-get node :right))))
|
('concat
|
||||||
('stack
|
(delq nil (list (plist-get node :left)
|
||||||
(delq nil (list (plist-get node :top)
|
(plist-get node :right))))
|
||||||
(plist-get node :bottom))))
|
('stack
|
||||||
(_ nil))))
|
(delq nil (list (plist-get node :top)
|
||||||
|
(plist-get node :bottom))))
|
||||||
|
(_ nil)))))
|
||||||
|
|
||||||
(defun ebox-tree--replace-direct-child (child replacements)
|
(defun ebox-tree--replace-direct-child (child replacements)
|
||||||
"Return CHILD's identity replacement from REPLACEMENTS, when present.
|
"Return CHILD's identity replacement from REPLACEMENTS, when present.
|
||||||
@ -242,6 +245,55 @@ still copied while every nested value remains shared."
|
|||||||
(ebox-tree--replace-direct-child-list children replacements)))
|
(ebox-tree--replace-direct-child-list children replacements)))
|
||||||
(unless (eq replaced children)
|
(unless (eq replaced children)
|
||||||
(setq copy (plist-put copy :children replaced)))))
|
(setq copy (plist-put copy :children replaced)))))
|
||||||
|
(when-let ((sequence (and (memq type '(concat stack flex grid))
|
||||||
|
(plist-get copy :ebox-child-sequence))))
|
||||||
|
(let ((updated sequence) changed fallback)
|
||||||
|
(dolist (replacement replacements)
|
||||||
|
(let* ((old (car replacement)) (new (cdr replacement))
|
||||||
|
(location (and old (plist-get old :ebox-sequence-location)))
|
||||||
|
(key (and old (plist-get old :key)))
|
||||||
|
(key-location
|
||||||
|
(and (null location) key
|
||||||
|
(ebox-child-range--hash-lookup
|
||||||
|
(ebox-child-range--sequence-key-root updated) key
|
||||||
|
(funcall
|
||||||
|
(ebox-child-range--sequence-hash-function updated)
|
||||||
|
key)))))
|
||||||
|
(when key-location
|
||||||
|
(let* ((segment (ebox-child-range--segment-at
|
||||||
|
updated (car key-location)))
|
||||||
|
(candidate (aref
|
||||||
|
(ebox-child-range--segment-payload segment)
|
||||||
|
(cdr key-location))))
|
||||||
|
(when (eq candidate old)
|
||||||
|
(setq location
|
||||||
|
(list :parent-node-id (plist-get node :node-id)
|
||||||
|
:segment-index (car key-location)
|
||||||
|
:offset (cdr key-location))))))
|
||||||
|
(when (and location
|
||||||
|
(equal (plist-get location :parent-node-id)
|
||||||
|
(plist-get node :node-id)))
|
||||||
|
(if new
|
||||||
|
(setq updated
|
||||||
|
(ebox-child-range--replace-item-at
|
||||||
|
updated (plist-get location :segment-index)
|
||||||
|
(plist-get location :offset) old new)
|
||||||
|
changed t)
|
||||||
|
(setq fallback t)))))
|
||||||
|
(when fallback
|
||||||
|
(let (segments)
|
||||||
|
(dotimes (index (ebox-child-range--sequence-count sequence))
|
||||||
|
(let* ((segment (ebox-child-range--segment-at sequence index))
|
||||||
|
(items (append
|
||||||
|
(ebox-child-range--segment-payload segment) nil)))
|
||||||
|
(push (cons (ebox-child-range--segment-ref segment)
|
||||||
|
(ebox-tree--replace-direct-child-list
|
||||||
|
items replacements))
|
||||||
|
segments)))
|
||||||
|
(setq updated (ebox-child-range--build (nreverse segments))
|
||||||
|
changed t)))
|
||||||
|
(when changed
|
||||||
|
(setq copy (plist-put copy :ebox-child-sequence updated)))))
|
||||||
copy))
|
copy))
|
||||||
|
|
||||||
(defun ebox-tree-copy-node-structure (root)
|
(defun ebox-tree-copy-node-structure (root)
|
||||||
@ -250,15 +302,57 @@ The returned tree owns distinct node plists and child lists. Values such as
|
|||||||
keymaps, callbacks, Host references, and surface-property payloads retain their
|
keymaps, callbacks, Host references, and surface-property payloads retain their
|
||||||
original object identity."
|
original object identity."
|
||||||
(cl-labels
|
(cl-labels
|
||||||
((copy-node
|
((without (plist key)
|
||||||
|
(cl-loop for (property value) on plist by #'cddr
|
||||||
|
unless (eq property key) append (list property value)))
|
||||||
|
(copy-node
|
||||||
(node)
|
(node)
|
||||||
(if (or (not (listp node)) (stringp node))
|
(if (or (not (listp node)) (stringp node))
|
||||||
node
|
node
|
||||||
(ebox-tree-copy-with-direct-child-replacements
|
(let* ((type (plist-get node :ebox-type))
|
||||||
node
|
(material-p (memq type '(concat stack flex grid)))
|
||||||
(mapcar (lambda (child)
|
(raw (and material-p (plist-get node :children)))
|
||||||
(cons child (copy-node child)))
|
(sequence (and material-p
|
||||||
(ebox-tree--children-raw node))))))
|
(plist-get node :ebox-child-sequence)))
|
||||||
|
(has-range (cl-some #'ebox-child-range--descriptor-p raw)))
|
||||||
|
(if (or has-range sequence)
|
||||||
|
(let ((copy (copy-sequence node)) segments)
|
||||||
|
(if sequence
|
||||||
|
(dotimes (index
|
||||||
|
(ebox-child-range--sequence-count sequence))
|
||||||
|
(let ((segment
|
||||||
|
(ebox-child-range--segment-at sequence index)))
|
||||||
|
(push
|
||||||
|
(cons (ebox-child-range--segment-ref segment)
|
||||||
|
(mapcar #'copy-node
|
||||||
|
(append
|
||||||
|
(ebox-child-range--segment-payload
|
||||||
|
segment)
|
||||||
|
nil)))
|
||||||
|
segments)))
|
||||||
|
(dolist (entry raw)
|
||||||
|
(if (ebox-child-range--descriptor-p entry)
|
||||||
|
(push (cons (ebox-child-range--descriptor-ref entry)
|
||||||
|
(mapcar
|
||||||
|
#'copy-node
|
||||||
|
(ebox-child-range--descriptor-items entry)))
|
||||||
|
segments)
|
||||||
|
(push (cons nil (list (copy-node entry))) segments))))
|
||||||
|
(setq copy (without copy :children))
|
||||||
|
(pcase type
|
||||||
|
((or 'flex 'grid)
|
||||||
|
(when-let ((box (plist-get node :box)))
|
||||||
|
(setq copy (plist-put copy :box (copy-node box)))))
|
||||||
|
('concat
|
||||||
|
(setq copy (without (without copy :left) :right)))
|
||||||
|
('stack
|
||||||
|
(setq copy (without (without copy :top) :bottom))))
|
||||||
|
(plist-put copy :ebox-child-sequence
|
||||||
|
(ebox-child-range--build (nreverse segments))))
|
||||||
|
(ebox-tree-copy-with-direct-child-replacements
|
||||||
|
node
|
||||||
|
(mapcar (lambda (child) (cons child (copy-node child)))
|
||||||
|
(ebox-tree--children-raw node))))))))
|
||||||
(copy-node root)))
|
(copy-node root)))
|
||||||
|
|
||||||
(defun ebox-tree--runtime-identity-node-shell
|
(defun ebox-tree--runtime-identity-node-shell
|
||||||
@ -312,7 +406,7 @@ Record its identities in NODE-ID-SET and REGION-ID-SET."
|
|||||||
(lambda (child)
|
(lambda (child)
|
||||||
(ebox-tree--runtime-identity-skeleton
|
(ebox-tree--runtime-identity-skeleton
|
||||||
child node-id-set region-id-set))
|
child node-id-set region-id-set))
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
('flex-item
|
('flex-item
|
||||||
(when-let ((child (plist-get node :node)))
|
(when-let ((child (plist-get node :node)))
|
||||||
(plist-put shell :node
|
(plist-put shell :node
|
||||||
@ -368,7 +462,10 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
|||||||
(puthash node t seen)
|
(puthash node t seen)
|
||||||
(puthash node t active)
|
(puthash node t active)
|
||||||
(dolist (child (ebox-tree--children-raw node))
|
(dolist (child (ebox-tree--children-raw node))
|
||||||
(visit child))
|
(if (ebox-child-range--descriptor-p child)
|
||||||
|
(dolist (item (ebox-child-range--descriptor-items child))
|
||||||
|
(visit item))
|
||||||
|
(visit child)))
|
||||||
(remhash node active))))))
|
(remhash node active))))))
|
||||||
(visit root))
|
(visit root))
|
||||||
root))
|
root))
|
||||||
@ -380,9 +477,12 @@ The runtime tree must be a proper tree: a node object cannot appear in two
|
|||||||
locations, cycles are rejected, explicit sibling keys must be unique, and
|
locations, cycles are rejected, explicit sibling keys must be unique, and
|
||||||
non-nil host references must be unique across the root. Keys are sibling-local
|
non-nil host references must be unique across the root. Keys are sibling-local
|
||||||
and all explicit identities are compared with `equal'. Return ROOT on success."
|
and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||||
|
(when (ebox-child-range--descriptor-p root)
|
||||||
|
(error "Ebox child Range descriptor cannot be the root"))
|
||||||
(ebox-tree-validate-host-refs root)
|
(ebox-tree-validate-host-refs root)
|
||||||
(let ((seen (make-hash-table :test 'eq))
|
(let ((seen (make-hash-table :test 'eq))
|
||||||
(active (make-hash-table :test 'eq)))
|
(active (make-hash-table :test 'eq))
|
||||||
|
(range-refs (make-hash-table :test 'equal)))
|
||||||
(cl-labels
|
(cl-labels
|
||||||
((visit (node)
|
((visit (node)
|
||||||
(when (and (listp node) (not (stringp node)))
|
(when (and (listp node) (not (stringp node)))
|
||||||
@ -392,8 +492,36 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
|||||||
(error "Ebox declarative tree reuses one node object"))
|
(error "Ebox declarative tree reuses one node object"))
|
||||||
(puthash node t seen)
|
(puthash node t seen)
|
||||||
(puthash node t active)
|
(puthash node t active)
|
||||||
(let ((keys (make-hash-table :test 'equal)))
|
(let* ((keys (make-hash-table :test 'equal))
|
||||||
(dolist (child (ebox-tree--children-raw node))
|
(type (plist-get node :ebox-type))
|
||||||
|
(raw (and (memq type '(concat stack flex grid))
|
||||||
|
(plist-get node :children)))
|
||||||
|
children)
|
||||||
|
(when (and raw (not (proper-list-p raw)))
|
||||||
|
(error "Ebox material children must be a proper list"))
|
||||||
|
(dolist (entry (ebox-tree--children-raw node))
|
||||||
|
(if (ebox-child-range--descriptor-p entry)
|
||||||
|
(progn
|
||||||
|
(unless (and (proper-list-p raw) (memq entry raw))
|
||||||
|
(error "Ebox child Range descriptor is invalid in a scalar slot"))
|
||||||
|
(let ((ref (ebox-child-range--descriptor-ref entry))
|
||||||
|
(items (ebox-child-range--descriptor-items entry)))
|
||||||
|
(unless (and ref (proper-list-p items))
|
||||||
|
(error "Ebox child Range descriptor is invalid"))
|
||||||
|
(when (gethash ref range-refs)
|
||||||
|
(error "Ebox child Range ref is not unique: %S" ref))
|
||||||
|
(puthash ref t range-refs)
|
||||||
|
(dolist (item items)
|
||||||
|
(when (ebox-child-range--descriptor-p item)
|
||||||
|
(error "Nested child Range descriptors are reserved"))
|
||||||
|
(unless (and (listp item) (not (stringp item)))
|
||||||
|
(error "Ebox child Range item must be a node"))
|
||||||
|
(push item children))))
|
||||||
|
(progn
|
||||||
|
(when (ebox-child-range--descriptor-p entry)
|
||||||
|
(error "Ebox child Range descriptor is invalid here"))
|
||||||
|
(push entry children))))
|
||||||
|
(dolist (child (nreverse children))
|
||||||
(when-let ((key (and (listp child)
|
(when-let ((key (and (listp child)
|
||||||
(not (stringp child))
|
(not (stringp child))
|
||||||
(plist-get child :key))))
|
(plist-get child :key))))
|
||||||
@ -418,17 +546,21 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
|||||||
(plist-put node :region-id nil)))
|
(plist-put node :region-id nil)))
|
||||||
(when (plist-member node :surface-object)
|
(when (plist-member node :surface-object)
|
||||||
(plist-put node :surface-object nil))
|
(plist-put node :surface-object nil))
|
||||||
|
(when (plist-member node :ebox-sequence-location)
|
||||||
|
(plist-put node :ebox-sequence-location 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 :surface-object :render-cache)
|
'(:node-id :region-id :surface-object :render-cache :ebox-sequence-location
|
||||||
|
:ebox-content-width-exact-p)
|
||||||
"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
|
||||||
'(:children :left :right :top :bottom :node :box :ebox-content-node)
|
'(:children :ebox-child-sequence :left :right :top :bottom :node :box
|
||||||
|
:ebox-content-node)
|
||||||
"Child references excluded from node-local declarative signatures.")
|
"Child references excluded from node-local declarative signatures.")
|
||||||
|
|
||||||
(defconst ebox-tree--excluded-source-keys
|
(defconst ebox-tree--excluded-source-keys
|
||||||
@ -449,21 +581,76 @@ projected Ebox longhands carry the actual paint and layout consequences.")
|
|||||||
"Return keys from PLIST in source order."
|
"Return keys from PLIST in source order."
|
||||||
(cl-loop for (key _value) on plist by #'cddr collect key))
|
(cl-loop for (key _value) on plist by #'cddr collect key))
|
||||||
|
|
||||||
|
(defun ebox-tree--layout-props-source-signature (node)
|
||||||
|
"Return canonical layout-only props for a Grid/Flex NODE.
|
||||||
|
Raw layout props also carry derived paint aliases, host references, duplicate
|
||||||
|
width entries, and shorthand gap forms. Those fields are already represented
|
||||||
|
by style declarations or runtime identity and must not make a Theme repaint
|
||||||
|
look like a geometry mutation."
|
||||||
|
(when (memq (plist-get node :ebox-type) '(grid flex))
|
||||||
|
(let* ((raw (or (plist-get node :raw-props)
|
||||||
|
(plist-get node :props)))
|
||||||
|
;; Compare the same engine vocabulary used by Ebox rendering. A
|
||||||
|
;; retained node may arrive once from an already-expanded style
|
||||||
|
;; snapshot and later from fresh shorthand declarations; comparing
|
||||||
|
;; those source spellings directly creates fake geometry dirtiness.
|
||||||
|
(raw (ebox-style-expand-ebox-plist raw))
|
||||||
|
(table (make-hash-table :test #'eq)))
|
||||||
|
(cl-loop for (key value) on raw by #'cddr
|
||||||
|
unless (memq key '(:color :background-color :bgcolor :host-ref
|
||||||
|
:surface-properties))
|
||||||
|
do (puthash key value table))
|
||||||
|
(when (plist-member raw :gap)
|
||||||
|
(let* ((gap (plist-get raw :gap))
|
||||||
|
(row (if (consp gap) (car gap) gap))
|
||||||
|
(column (if (and (consp gap) (cdr gap)) (cadr gap) row))
|
||||||
|
;; Flex and Grid deliberately have separate canonical
|
||||||
|
;; property vocabularies. Using Grid aliases for a Flex node
|
||||||
|
;; makes an unchanged `gap' look like geometry dirtiness on
|
||||||
|
;; the next retained generation.
|
||||||
|
(row-key (if (eq (plist-get node :ebox-type) 'grid)
|
||||||
|
:grid-row-gap
|
||||||
|
:row-gap))
|
||||||
|
(column-key (if (eq (plist-get node :ebox-type) 'grid)
|
||||||
|
:grid-column-gap
|
||||||
|
:column-gap)))
|
||||||
|
(puthash row-key row table)
|
||||||
|
(puthash column-key column table)
|
||||||
|
(remhash :gap table)))
|
||||||
|
(let (keys)
|
||||||
|
(maphash (lambda (key _value) (push key keys)) table)
|
||||||
|
(setq keys
|
||||||
|
(sort keys
|
||||||
|
(lambda (left right)
|
||||||
|
(string< (symbol-name left)
|
||||||
|
(symbol-name right)))))
|
||||||
|
(cl-loop for key in keys
|
||||||
|
append (list key (gethash key table)))))))
|
||||||
|
|
||||||
|
(defun ebox-tree--local-source-signature (node)
|
||||||
|
"Return NODE's filtered local declarative source signature."
|
||||||
|
(let ((plist node)
|
||||||
|
signature)
|
||||||
|
(while plist
|
||||||
|
(let ((key (pop plist))
|
||||||
|
(value (pop plist)))
|
||||||
|
(unless (or (memq key ebox-tree--excluded-source-keys)
|
||||||
|
(and (memq key '(:props :raw-props))
|
||||||
|
(memq (plist-get node :ebox-type) '(grid flex))))
|
||||||
|
(push key signature)
|
||||||
|
(push value signature))))
|
||||||
|
(when-let ((layout (ebox-tree--layout-props-source-signature node)))
|
||||||
|
(push :ebox-layout-props signature)
|
||||||
|
(push layout signature))
|
||||||
|
(nreverse signature)))
|
||||||
|
|
||||||
(defun ebox-tree-node-local-source-signature (node)
|
(defun ebox-tree-node-local-source-signature (node)
|
||||||
"Return NODE's non-recursive declarative source signature.
|
"Return NODE's non-recursive declarative source signature.
|
||||||
|
|
||||||
Runtime identities, caches, and child pointers are excluded. Child order is
|
Runtime identities, caches, and child pointers are excluded. Child order is
|
||||||
compared independently through stable runtime ids, so a leaf change does not
|
compared independently through stable runtime ids, so a leaf change does not
|
||||||
mark every ancestor dirty."
|
mark every ancestor dirty."
|
||||||
(let ((plist node)
|
(ebox-tree--local-source-signature node))
|
||||||
signature)
|
|
||||||
(while plist
|
|
||||||
(let ((key (pop plist))
|
|
||||||
(value (pop plist)))
|
|
||||||
(unless (memq key ebox-tree--excluded-source-keys)
|
|
||||||
(push key signature)
|
|
||||||
(push value signature))))
|
|
||||||
(nreverse signature)))
|
|
||||||
|
|
||||||
(defun ebox-tree--next-local-source-entry (plist)
|
(defun ebox-tree--next-local-source-entry (plist)
|
||||||
"Return PLIST's next node-local source entry, skipping excluded keys."
|
"Return PLIST's next node-local source entry, skipping excluded keys."
|
||||||
@ -489,24 +676,25 @@ mark every ancestor dirty."
|
|||||||
(defun ebox-tree-node-local-changed-keys (old new)
|
(defun ebox-tree-node-local-changed-keys (old new)
|
||||||
"Return ordered declarative keys whose node-local values differ.
|
"Return ordered declarative keys whose node-local values differ.
|
||||||
OLD and NEW are source nodes with the same retained runtime identity."
|
OLD and NEW are source nodes with the same retained runtime identity."
|
||||||
(unless (ebox-tree--local-source-sequence-equal-p old new)
|
(let ((old-signature (ebox-tree--local-source-signature old))
|
||||||
(let (seen changed)
|
(new-signature (ebox-tree--local-source-signature new))
|
||||||
(dolist (source (list old new))
|
seen changed)
|
||||||
(let ((plist source))
|
(dolist (source (list old-signature new-signature))
|
||||||
(while plist
|
(let ((plist source))
|
||||||
(let ((key (pop plist)))
|
(while plist
|
||||||
(pop plist)
|
(let ((key (pop plist)))
|
||||||
(unless (or (memq key ebox-tree--excluded-source-keys)
|
(pop plist)
|
||||||
(memq key seen))
|
(unless (memq key seen)
|
||||||
(push key seen)
|
(push key seen)
|
||||||
(let ((old-entry (plist-member old key))
|
(let ((old-entry (plist-member old-signature key))
|
||||||
(new-entry (plist-member new key)))
|
(new-entry (plist-member new-signature key)))
|
||||||
(unless (and (eq (not (null old-entry))
|
(unless (and (eq (not (null old-entry))
|
||||||
(not (null new-entry)))
|
(not (null new-entry)))
|
||||||
(equal-including-properties
|
(equal-including-properties
|
||||||
(cadr old-entry) (cadr new-entry)))
|
(cadr old-entry) (cadr new-entry)))
|
||||||
(push key changed))))))))
|
(push (if (eq key :ebox-layout-props) :props key)
|
||||||
(nreverse changed))))
|
changed))))))))
|
||||||
|
(nreverse changed)))
|
||||||
|
|
||||||
(defun ebox-tree-transfer-runtime-identity (old new)
|
(defun ebox-tree-transfer-runtime-identity (old new)
|
||||||
"Transfer retained runtime identity from matching OLD to NEW.
|
"Transfer retained runtime identity from matching OLD to NEW.
|
||||||
@ -522,6 +710,9 @@ candidate runtime index agree."
|
|||||||
(plist-get new :ebox-type)))
|
(plist-get new :ebox-type)))
|
||||||
(progn
|
(progn
|
||||||
(plist-put new :node-id (ebox--ensure-node-id old))
|
(plist-put new :node-id (ebox--ensure-node-id old))
|
||||||
|
(when (plist-member old :ebox-sequence-location)
|
||||||
|
(plist-put new :ebox-sequence-location
|
||||||
|
(copy-sequence (plist-get old :ebox-sequence-location))))
|
||||||
(when (eq (plist-get new :ebox-type) 'box)
|
(when (eq (plist-get new :ebox-type) 'box)
|
||||||
(plist-put new :region-id (ebox--ensure-region-id old))
|
(plist-put new :region-id (ebox--ensure-region-id old))
|
||||||
;; Omitted scroll state is uncontrolled host state. `ebox-create'
|
;; Omitted scroll state is uncontrolled host state. `ebox-create'
|
||||||
@ -615,8 +806,8 @@ candidate runtime index agree."
|
|||||||
((or 'concat 'stack)
|
((or 'concat 'stack)
|
||||||
(ebox-tree--semantic-layout-leaves node))
|
(ebox-tree--semantic-layout-leaves node))
|
||||||
('flex
|
('flex
|
||||||
(mapcar #'ebox-tree-flex-item-source-node (plist-get node :children)))
|
(mapcar #'ebox-tree-flex-item-source-node (ebox-tree-layout-children node)))
|
||||||
('grid (plist-get node :children))
|
('grid (ebox-tree-layout-children node))
|
||||||
('flex-item (delq nil (list (plist-get node :node))))
|
('flex-item (delq nil (list (plist-get node :node))))
|
||||||
(_ (ebox-tree-children node))))
|
(_ (ebox-tree-children node))))
|
||||||
|
|
||||||
@ -742,14 +933,14 @@ not only the public update ids."
|
|||||||
(list (ebox--ensure-region-id box)))
|
(list (ebox--ensure-region-id box)))
|
||||||
(apply #'append
|
(apply #'append
|
||||||
(mapcar #'ebox-tree-node-all-region-ids
|
(mapcar #'ebox-tree-node-all-region-ids
|
||||||
(plist-get node :children)))))
|
(ebox-tree-layout-children node)))))
|
||||||
('grid
|
('grid
|
||||||
(append
|
(append
|
||||||
(when-let ((box (plist-get node :box)))
|
(when-let ((box (plist-get node :box)))
|
||||||
(list (ebox--ensure-region-id box)))
|
(list (ebox--ensure-region-id box)))
|
||||||
(apply #'append
|
(apply #'append
|
||||||
(mapcar #'ebox-tree-node-all-region-ids
|
(mapcar #'ebox-tree-node-all-region-ids
|
||||||
(plist-get node :children)))))
|
(ebox-tree-layout-children node)))))
|
||||||
('flex-item
|
('flex-item
|
||||||
(ebox-tree-node-all-region-ids (plist-get node :node)))
|
(ebox-tree-node-all-region-ids (plist-get node :node)))
|
||||||
(_ nil))))
|
(_ nil))))
|
||||||
@ -786,14 +977,14 @@ cannot be inspected and reports no conflict."
|
|||||||
(and (plist-get box paint-key) t)))
|
(and (plist-get box paint-key) t)))
|
||||||
(cl-some (lambda (child)
|
(cl-some (lambda (child)
|
||||||
(ebox-tree-node-paint-conflict-p child paint-key t))
|
(ebox-tree-node-paint-conflict-p child paint-key t))
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
('grid
|
('grid
|
||||||
(or (and inherited
|
(or (and inherited
|
||||||
(when-let ((box (plist-get node :box)))
|
(when-let ((box (plist-get node :box)))
|
||||||
(and (plist-get box paint-key) t)))
|
(and (plist-get box paint-key) t)))
|
||||||
(cl-some (lambda (child)
|
(cl-some (lambda (child)
|
||||||
(ebox-tree-node-paint-conflict-p child paint-key t))
|
(ebox-tree-node-paint-conflict-p child paint-key t))
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
('flex-item
|
('flex-item
|
||||||
(ebox-tree-node-paint-conflict-p
|
(ebox-tree-node-paint-conflict-p
|
||||||
(plist-get node :node) paint-key inherited))
|
(plist-get node :node) paint-key inherited))
|
||||||
@ -834,7 +1025,7 @@ wrapper's visible content is produced by the flex renderer."
|
|||||||
(list node))
|
(list node))
|
||||||
(t
|
(t
|
||||||
(catch 'found
|
(catch 'found
|
||||||
(dolist (child (plist-get node :children))
|
(dolist (child (ebox-tree-layout-children node))
|
||||||
(when-let ((path (ebox-tree-node-path-to-region child region-id)))
|
(when-let ((path (ebox-tree-node-path-to-region child region-id)))
|
||||||
(throw 'found (append path (list node)))))
|
(throw 'found (append path (list node)))))
|
||||||
nil))))
|
nil))))
|
||||||
@ -845,7 +1036,7 @@ wrapper's visible content is produced by the flex renderer."
|
|||||||
(list node))
|
(list node))
|
||||||
(t
|
(t
|
||||||
(catch 'found
|
(catch 'found
|
||||||
(dolist (child (plist-get node :children))
|
(dolist (child (ebox-tree-layout-children node))
|
||||||
(when-let ((path (ebox-tree-node-path-to-region child region-id)))
|
(when-let ((path (ebox-tree-node-path-to-region child region-id)))
|
||||||
(throw 'found (append path (list node)))))
|
(throw 'found (append path (list node)))))
|
||||||
nil))))
|
nil))))
|
||||||
@ -873,12 +1064,12 @@ wrapper's visible content is produced by the flex renderer."
|
|||||||
(or (and (plist-get node :box)
|
(or (and (plist-get node :box)
|
||||||
(ebox--box-visible-overflow-p (plist-get node :box)))
|
(ebox--box-visible-overflow-p (plist-get node :box)))
|
||||||
(cl-some #'ebox-tree-node-visible-overflow-p
|
(cl-some #'ebox-tree-node-visible-overflow-p
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
('grid
|
('grid
|
||||||
(or (and (plist-get node :box)
|
(or (and (plist-get node :box)
|
||||||
(ebox--box-visible-overflow-p (plist-get node :box)))
|
(ebox--box-visible-overflow-p (plist-get node :box)))
|
||||||
(cl-some #'ebox-tree-node-visible-overflow-p
|
(cl-some #'ebox-tree-node-visible-overflow-p
|
||||||
(plist-get node :children))))
|
(ebox-tree-layout-children node))))
|
||||||
('flex-item
|
('flex-item
|
||||||
(ebox-tree-node-visible-overflow-p (plist-get node :node)))
|
(ebox-tree-node-visible-overflow-p (plist-get node :node)))
|
||||||
(_ nil)))))
|
(_ nil)))))
|
||||||
|
|||||||
1007
tests/ebox-child-range-tests.el
Normal file
1007
tests/ebox-child-range-tests.el
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@
|
|||||||
"The Makefile should expose Ebox targets without bundling ETAF."
|
"The Makefile should expose Ebox targets without bundling ETAF."
|
||||||
(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:" "child-range-tests:" "grid-tests:" "ebox-commit-tests:"
|
||||||
"surface-tests:"
|
"surface-tests:"
|
||||||
"docs-contract-tests:"
|
"docs-contract-tests:"
|
||||||
"ci-contract-tests:" "native-rust-tests:"
|
"ci-contract-tests:" "native-rust-tests:"
|
||||||
@ -32,6 +32,7 @@
|
|||||||
(should (string-match-p "byte-compile-error-on-warn" source))
|
(should (string-match-p "byte-compile-error-on-warn" source))
|
||||||
(should (string-match-p "ebox-byte-compile" source))
|
(should (string-match-p "ebox-byte-compile" source))
|
||||||
(should (string-match-p "ebox-native-reflow.el" ebox))
|
(should (string-match-p "ebox-native-reflow.el" ebox))
|
||||||
|
(should (string-match-p "ebox-child-range.el" ebox))
|
||||||
(should-not (string-match-p "ebox-playground.el" ebox))))
|
(should-not (string-match-p "ebox-playground.el" ebox))))
|
||||||
|
|
||||||
(ert-deftest ebox-ci-prefers-newer-source-and-cleans-local-products ()
|
(ert-deftest ebox-ci-prefers-newer-source-and-cleans-local-products ()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4324,6 +4324,43 @@
|
|||||||
(ebox-render box))
|
(ebox-render box))
|
||||||
(should (eq (gethash region-id ebox--scroll-global-state) state))))
|
(should (eq (gethash region-id ebox--scroll-global-state) state))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-flat-preformatted-blank-line-preserves-child-owner-segments ()
|
||||||
|
"Blank composite child segments must retain their distinct ownership."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let* ((left
|
||||||
|
(propertize
|
||||||
|
(ebox-pixel-space 40)
|
||||||
|
'ebox-content 'left 'ebox-content-idx 0
|
||||||
|
'ebox-content-owner 'left 'ebox-content-owners '(left)))
|
||||||
|
(right
|
||||||
|
(propertize
|
||||||
|
(ebox-pixel-space 60)
|
||||||
|
'ebox-content 'right 'ebox-content-idx 0
|
||||||
|
'ebox-content-owner 'right 'ebox-content-owners '(right)))
|
||||||
|
(box (ebox-create :content "" :width 100 :wrap-mode nil
|
||||||
|
:color "#112233" :bgcolor "#DDEEFF"))
|
||||||
|
(rendered
|
||||||
|
(car
|
||||||
|
(ebox--window-render-flat-wrapper-chunk-lines
|
||||||
|
box (list (concat left right)) 0 'outer
|
||||||
|
(ebox-pixel-space 100) t t))))
|
||||||
|
(should (= (length rendered) 2))
|
||||||
|
(should (= (ebox--string-pixel-width rendered) 100))
|
||||||
|
(should (eq (get-text-property 0 'ebox-content rendered) 'left))
|
||||||
|
(should (eq (get-text-property 1 'ebox-content rendered) 'right))
|
||||||
|
(should (equal (get-text-property 0 'ebox-content-owners rendered)
|
||||||
|
'(left outer)))
|
||||||
|
(should (equal (get-text-property 1 'ebox-content-owners rendered)
|
||||||
|
'(right outer)))
|
||||||
|
(should (eq (get-text-property 0 'ebox-content-owner rendered) 'outer))
|
||||||
|
(should (eq (get-text-property 1 'ebox-content-owner rendered) 'outer))
|
||||||
|
(dolist (position '(0 1))
|
||||||
|
(let ((face (get-text-property position 'face rendered)))
|
||||||
|
(should (ebox-test--face-has-key-value-p
|
||||||
|
face :foreground "#112233"))
|
||||||
|
(should (ebox-test--face-has-key-value-p
|
||||||
|
face :background "#DDEEFF"))))))
|
||||||
|
|
||||||
(ert-deftest ebox-transparent-preformatted-fast-path-keeps-intrinsic-scroll-state ()
|
(ert-deftest ebox-transparent-preformatted-fast-path-keeps-intrinsic-scroll-state ()
|
||||||
"Intrinsic transparent rendering must not clear the published scroll state."
|
"Intrinsic transparent rendering must not clear the published scroll state."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
@ -4426,6 +4463,34 @@
|
|||||||
(should (= justify-count 3))
|
(should (= justify-count 3))
|
||||||
(should (= (ebox--string-max-pixel-width rendered) 140))))
|
(should (= (ebox--string-max-pixel-width rendered) 140))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-box-content-node-publishes-only-proven-exact-width ()
|
||||||
|
"A wrapper should mark child output exact only at its proven viewport width."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let* ((overflow-child
|
||||||
|
(ebox-column
|
||||||
|
(ebox-create :content "A" :width 140)
|
||||||
|
(ebox-create :content "B" :width 140)))
|
||||||
|
(overflow-parent
|
||||||
|
(ebox-create :width 120 :wrap-mode nil
|
||||||
|
:ebox-content-node overflow-child))
|
||||||
|
(exact-child
|
||||||
|
(ebox-column
|
||||||
|
(ebox-create :content "A" :width 120)
|
||||||
|
(ebox-create :content "B" :width 120)))
|
||||||
|
(exact-parent
|
||||||
|
(ebox-create :width 120 :wrap-mode nil
|
||||||
|
:ebox-content-node exact-child))
|
||||||
|
(ebox-viewport-width 120)
|
||||||
|
(ebox--box-content-render-cache (make-hash-table :test 'eq))
|
||||||
|
overflow exact)
|
||||||
|
(setq overflow
|
||||||
|
(ebox--render-box-content-node overflow-parent overflow-child))
|
||||||
|
(setq exact (ebox--render-box-content-node exact-parent exact-child))
|
||||||
|
(should (= (ebox--string-max-pixel-width overflow) 140))
|
||||||
|
(should-not (plist-get overflow-parent :ebox-content-width-exact-p))
|
||||||
|
(should (= (ebox--string-max-pixel-width exact) 120))
|
||||||
|
(should (plist-get exact-parent :ebox-content-width-exact-p))))
|
||||||
|
|
||||||
(ert-deftest ebox-render-caches-lazy-box-content-within-render-pass ()
|
(ert-deftest ebox-render-caches-lazy-box-content-within-render-pass ()
|
||||||
"A lazy child layout should be rendered once per box/context in one pass."
|
"A lazy child layout should be rendered once per box/context in one pass."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
@ -7667,6 +7732,7 @@
|
|||||||
(unwind-protect
|
(unwind-protect
|
||||||
(let ((ebox-viewport-width 240)
|
(let ((ebox-viewport-width 240)
|
||||||
(ebox-viewport-height 12)
|
(ebox-viewport-height 12)
|
||||||
|
(ebox-wheel-scroll-step 16)
|
||||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||||
(ebox-scroll-lazy-prefix-lookahead-lines 0))
|
(ebox-scroll-lazy-prefix-lookahead-lines 0))
|
||||||
(setq buffer
|
(setq buffer
|
||||||
@ -8329,8 +8395,7 @@
|
|||||||
`(box :content ,(format "complete %02d" idx)
|
`(box :content ,(format "complete %02d" idx)
|
||||||
:width (100) :height 8)))))
|
:width (100) :height 8)))))
|
||||||
(limit 4)
|
(limit 4)
|
||||||
(full-lines (mapcar #'substring-no-properties
|
(full-lines (ebox-string-lines (ebox-render node)))
|
||||||
(ebox-string-lines (ebox-render node))))
|
|
||||||
(complete-wrapper-count 0)
|
(complete-wrapper-count 0)
|
||||||
(original-render-box-lines
|
(original-render-box-lines
|
||||||
(symbol-function 'ebox--flex-window-render-box-lines)))
|
(symbol-function 'ebox--flex-window-render-box-lines)))
|
||||||
@ -8344,9 +8409,19 @@
|
|||||||
(should window)
|
(should window)
|
||||||
(should-not (plist-get window :complete))
|
(should-not (plist-get window :complete))
|
||||||
(should (= complete-wrapper-count 0))
|
(should (= complete-wrapper-count 0))
|
||||||
(should (equal (seq-take full-lines limit)
|
(should
|
||||||
(mapcar #'substring-no-properties
|
(cl-every
|
||||||
(plist-get window :lines))))))))
|
(lambda (pair)
|
||||||
|
(let ((full (car pair))
|
||||||
|
(prefix (cdr pair)))
|
||||||
|
(or (equal (substring-no-properties full)
|
||||||
|
(substring-no-properties prefix))
|
||||||
|
(and (string-blank-p full)
|
||||||
|
(string-blank-p prefix)
|
||||||
|
(= (ebox--string-pixel-width full)
|
||||||
|
(ebox--string-pixel-width prefix))))))
|
||||||
|
(cl-mapcar #'cons (seq-take full-lines limit)
|
||||||
|
(plist-get window :lines))))))))
|
||||||
|
|
||||||
(ert-deftest ebox-flex-window-complete-extension-appends-bottom-border ()
|
(ert-deftest ebox-flex-window-complete-extension-appends-bottom-border ()
|
||||||
"Completing a cached flex prefix should append bottom border without full render."
|
"Completing a cached flex prefix should append bottom border without full render."
|
||||||
@ -9113,7 +9188,9 @@
|
|||||||
(original-render-box (symbol-function 'ebox--render-box)))
|
(original-render-box (symbol-function 'ebox--render-box)))
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(let ((ebox-viewport-width 240)
|
(let ((ebox-viewport-width 240)
|
||||||
(ebox-viewport-height 2))
|
(ebox-viewport-height 2)
|
||||||
|
(ebox-wheel-scroll-step 16)
|
||||||
|
(ebox-wheel-smooth-scroll t))
|
||||||
(cl-letf (((symbol-function 'ebox--render-box)
|
(cl-letf (((symbol-function 'ebox--render-box)
|
||||||
(lambda (box)
|
(lambda (box)
|
||||||
(when (equal (ebox-tree-node-id box) "row-30")
|
(when (equal (ebox-tree-node-id box) "row-30")
|
||||||
@ -9135,7 +9212,9 @@
|
|||||||
(should (ebox--scroll-region-by root-id 31))
|
(should (ebox--scroll-region-by root-id 31))
|
||||||
(should (= hidden-render-count 0))
|
(should (= hidden-render-count 0))
|
||||||
(should (gethash root-id ebox--scroll-idle-prefetch-timers))
|
(should (gethash root-id ebox--scroll-idle-prefetch-timers))
|
||||||
(ebox--scroll-idle-prefetch root-id)
|
(dotimes (_ 40)
|
||||||
|
(when (= hidden-render-count 0)
|
||||||
|
(ebox--scroll-idle-prefetch root-id)))
|
||||||
(should (> hidden-render-count 0))
|
(should (> hidden-render-count 0))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(let* ((state (ebox--scroll-get-state root-id))
|
||||||
(delta (- 30 (or (plist-get state :scroll-offset)
|
(delta (- 30 (or (plist-get state :scroll-offset)
|
||||||
@ -10627,6 +10706,7 @@
|
|||||||
(unwind-protect
|
(unwind-protect
|
||||||
(let ((ebox-viewport-width 240)
|
(let ((ebox-viewport-width 240)
|
||||||
(ebox-viewport-height 2)
|
(ebox-viewport-height 2)
|
||||||
|
(ebox-wheel-scroll-step 16)
|
||||||
(ebox-wheel-smooth-scroll-lines-per-tick 4)
|
(ebox-wheel-smooth-scroll-lines-per-tick 4)
|
||||||
(ebox-wheel-smooth-scroll-interval 60.0))
|
(ebox-wheel-smooth-scroll-interval 60.0))
|
||||||
(setq buffer
|
(setq buffer
|
||||||
@ -10984,7 +11064,8 @@
|
|||||||
(original-render-box (symbol-function 'ebox--render-box)))
|
(original-render-box (symbol-function 'ebox--render-box)))
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(let ((ebox-viewport-width 240)
|
(let ((ebox-viewport-width 240)
|
||||||
(ebox-viewport-height 2))
|
(ebox-viewport-height 2)
|
||||||
|
(ebox-wheel-scroll-step 16))
|
||||||
(cl-letf (((symbol-function 'ebox--render-box)
|
(cl-letf (((symbol-function 'ebox--render-box)
|
||||||
(lambda (box)
|
(lambda (box)
|
||||||
(when (equal (ebox-tree-node-id box) "row-30")
|
(when (equal (ebox-tree-node-id box) "row-30")
|
||||||
@ -11867,6 +11948,130 @@
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-visible-buffer-rearms-native-scroll-prewarm ()
|
||||||
|
"A buffer rendered off-window should switch to native scrolling when shown."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((buffer nil)
|
||||||
|
(scheduled 0)
|
||||||
|
(fake-window 'ebox-test-window))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox--runtime-prewarm-allow-noninteractive t)
|
||||||
|
(ebox-native-buffer-scroll t)
|
||||||
|
;; The production hook is intentionally GUI-only. Bind the
|
||||||
|
;; flag for this lifecycle test and stub the window primitives.
|
||||||
|
(noninteractive nil))
|
||||||
|
(setq buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(generate-new-buffer-name " *ebox-visible-prewarm*")
|
||||||
|
(ebox-build
|
||||||
|
'(box :id "root" :height 2 :overflow scroll
|
||||||
|
(column
|
||||||
|
(box :content "one" :height 1)
|
||||||
|
(box :content "two" :height 1)
|
||||||
|
(box :content "three" :height 1))))))
|
||||||
|
;; Rendering happened before the buffer had a window, so the
|
||||||
|
;; original job is allowed to finish without native materializing.
|
||||||
|
(ebox--runtime-prewarm-drain buffer)
|
||||||
|
(should-not (plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:native-buffer-scroll-p))
|
||||||
|
(cl-letf (((symbol-function 'window-live-p)
|
||||||
|
(lambda (window) (eq window fake-window)))
|
||||||
|
((symbol-function 'window-buffer)
|
||||||
|
(lambda (_window) buffer))
|
||||||
|
((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (candidate &optional _frame)
|
||||||
|
(and (eq candidate buffer) fake-window)))
|
||||||
|
((symbol-function 'ebox--viewport-dependent-subtree-p)
|
||||||
|
(lambda (&rest _) nil))
|
||||||
|
((symbol-function
|
||||||
|
'ebox--viewport-height-dependent-subtree-p)
|
||||||
|
(lambda (&rest _) nil))
|
||||||
|
((symbol-function 'ebox--schedule-buffer-runtime-prewarm)
|
||||||
|
(lambda (candidate &optional _delay _retained-only-p)
|
||||||
|
(should (eq candidate buffer))
|
||||||
|
(cl-incf scheduled))))
|
||||||
|
(let ((render-state (ebox--buffer-render-state buffer)))
|
||||||
|
;; This fixture stands for a viewport-independent root; the
|
||||||
|
;; production proof consumes the published axes in O(1).
|
||||||
|
(plist-put render-state :viewport-dependent-node-ids-ready t)
|
||||||
|
(plist-put render-state :viewport-dependent-node-id-axes
|
||||||
|
(list nil))
|
||||||
|
(let ((job (ebox--runtime-prewarm-new-job
|
||||||
|
buffer render-state)))
|
||||||
|
(should (eq (plist-get job :phase) 'native-scroll))
|
||||||
|
(should (plist-get job :native-scroll-region-id))))
|
||||||
|
(ebox--window-buffer-change fake-window))
|
||||||
|
(should (= scheduled 1)))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-native-scroll-prewarm-is-isolated-from-updates ()
|
||||||
|
"Ordinary runtime prewarm must not materialize a native scroll surface.
|
||||||
|
Native-window scrolling remains enabled, but only the initial display or an
|
||||||
|
explicit visible-window handoff may admit its full-content publication."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((buffer nil))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((noninteractive nil)
|
||||||
|
(ebox--runtime-prewarm-allow-noninteractive t)
|
||||||
|
(ebox-native-buffer-scroll t))
|
||||||
|
(setq buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(generate-new-buffer-name " *ebox-native-scroll-opt-in*")
|
||||||
|
(ebox-build
|
||||||
|
'(box :id "root" :height 2 :overflow scroll
|
||||||
|
(column
|
||||||
|
(box :content "one" :height 1)
|
||||||
|
(box :content "two" :height 1)
|
||||||
|
(box :content "three" :height 1))))))
|
||||||
|
(let ((state (ebox--buffer-render-state buffer)))
|
||||||
|
(cl-letf (((symbol-function
|
||||||
|
'ebox--native-buffer-scroll-root-region-id)
|
||||||
|
(lambda (&rest _) 1))
|
||||||
|
((symbol-function
|
||||||
|
'ebox--native-buffer-scroll-root-proof-p)
|
||||||
|
(lambda (&rest _) t)))
|
||||||
|
(let ((job (ebox--runtime-prewarm-new-job buffer state t)))
|
||||||
|
(should (eq (plist-get job :phase) 'snapshots))
|
||||||
|
(should-not (plist-get job :native-scroll-region-id)))
|
||||||
|
(let ((job (ebox--runtime-prewarm-new-job buffer state nil)))
|
||||||
|
(should (eq (plist-get job :phase) 'native-scroll))
|
||||||
|
(should (= (plist-get job :native-scroll-region-id) 1))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-native-scroll-rejects-viewport-dependent-root ()
|
||||||
|
"Native full-content materialization must not freeze a resizable root."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((buffer nil)
|
||||||
|
(fake-window 'ebox-test-window))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((noninteractive nil)
|
||||||
|
(ebox-viewport-width 120)
|
||||||
|
(ebox-viewport-height 2)
|
||||||
|
(ebox-native-buffer-scroll t))
|
||||||
|
(setq buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(generate-new-buffer-name " *ebox-native-scroll-viewport*")
|
||||||
|
(ebox-build
|
||||||
|
'(box :id "root" :width (viewport) :height 2
|
||||||
|
:overflow scroll
|
||||||
|
(column
|
||||||
|
(box :content "one" :height 1)
|
||||||
|
(box :content "two" :height 1)
|
||||||
|
(box :content "three" :height 1))))))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(region-id (car (plist-get state :scroll-region-ids))))
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _) fake-window))
|
||||||
|
((symbol-function 'window-live-p)
|
||||||
|
(lambda (window) (eq window fake-window))))
|
||||||
|
(should-not
|
||||||
|
(ebox--native-buffer-scroll-root-proof-p
|
||||||
|
buffer region-id state)))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-runtime-idle-prewarm-does-not-wait-for-prefix-prefetch ()
|
(ert-deftest ebox-runtime-idle-prewarm-does-not-wait-for-prefix-prefetch ()
|
||||||
"Prewarm should index current scroll lines while lazy prefix work is active."
|
"Prewarm should index current scroll lines while lazy prefix work is active."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
@ -12578,11 +12783,241 @@
|
|||||||
(should (ebox--scroll-first-region-by '(stalled active) 1)))
|
(should (ebox--scroll-first-region-by '(stalled active) 1)))
|
||||||
(should (equal (nreverse visited) '(stalled active)))))
|
(should (equal (nreverse visited) '(stalled active)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-distributes-forward-residual-inner-to-buffer ()
|
||||||
|
"A positive line intent should drain inner, parent, then native buffer."
|
||||||
|
(let ((capacity '((inner . 1) (middle . 2) (outer . 0)))
|
||||||
|
native-residual)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (region-id delta &optional _prefix-budget-lines)
|
||||||
|
(let* ((available (alist-get region-id capacity))
|
||||||
|
(consumed (min delta available)))
|
||||||
|
(setf (alist-get region-id capacity) (- available consumed))
|
||||||
|
consumed))))
|
||||||
|
(let ((result
|
||||||
|
(ebox--scroll-consume-intent
|
||||||
|
(ebox--scroll-intent-create 5 '(inner middle outer))
|
||||||
|
(lambda (delta) (setq native-residual delta)))))
|
||||||
|
(should (= (plist-get result :requested) 5))
|
||||||
|
(should (equal (plist-get result :consumption)
|
||||||
|
'((inner . 1) (middle . 2) (outer . 0))))
|
||||||
|
(should (= (plist-get result :ebox-consumed) 3))
|
||||||
|
(should (= (plist-get result :native-residual) 2))
|
||||||
|
(should (= native-residual 2))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-distributes-reverse-residual-inner-to-buffer ()
|
||||||
|
"A negative line intent should preserve direction through the owner chain."
|
||||||
|
(let ((capacity '((inner . 1) (middle . 2) (outer . 0)))
|
||||||
|
native-residual)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (region-id delta &optional _prefix-budget-lines)
|
||||||
|
(let* ((available (alist-get region-id capacity))
|
||||||
|
(magnitude (min (- delta) available))
|
||||||
|
(consumed (- magnitude)))
|
||||||
|
(setf (alist-get region-id capacity) (- available magnitude))
|
||||||
|
consumed))))
|
||||||
|
(let ((result
|
||||||
|
(ebox--scroll-consume-intent
|
||||||
|
(ebox--scroll-intent-create -5 '(inner middle outer))
|
||||||
|
(lambda (delta) (setq native-residual delta)))))
|
||||||
|
(should (equal (plist-get result :consumption)
|
||||||
|
'((inner . -1) (middle . -2) (outer . 0))))
|
||||||
|
(should (= (plist-get result :ebox-consumed) -3))
|
||||||
|
(should (= (plist-get result :native-residual) -2))
|
||||||
|
(should (= native-residual -2))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-distributes-real-nested-owner-capacity ()
|
||||||
|
"Nested boxes should consume their capacity before the native buffer."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((layout
|
||||||
|
(ebox-build
|
||||||
|
'(box :id "outer" :height 4 :overflow scroll
|
||||||
|
(column
|
||||||
|
(box :id "middle" :height 2 :overflow scroll
|
||||||
|
(column
|
||||||
|
(box :id "inner" :content "inner 0\ninner 1"
|
||||||
|
:height 1 :overflow scroll)
|
||||||
|
(box :content "middle 0" :height 1)
|
||||||
|
(box :content "middle 1" :height 1)
|
||||||
|
(box :content "middle 2" :height 1)))
|
||||||
|
(box :content "outer 0\nouter 1" :height 2))))))
|
||||||
|
(ebox-test--with-rendered-buffer layout
|
||||||
|
(let* ((surface (with-current-buffer
|
||||||
|
(current-buffer) ebox-surface--buffer-surface))
|
||||||
|
(revision (tp-surface-revision surface))
|
||||||
|
(inner-id (ebox-test--selector-region-id
|
||||||
|
(current-buffer) "#inner"))
|
||||||
|
(middle-id (ebox-test--selector-region-id
|
||||||
|
(current-buffer) "#middle"))
|
||||||
|
(outer-id (ebox-test--selector-region-id
|
||||||
|
(current-buffer) "#outer"))
|
||||||
|
native-lines)
|
||||||
|
(should (ebox-scroll-state inner-id))
|
||||||
|
(should (ebox-scroll-state middle-id))
|
||||||
|
(should-not (ebox-scroll-state outer-id))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward "inner 0")
|
||||||
|
(backward-char)
|
||||||
|
(cl-letf (((symbol-function 'scroll-up)
|
||||||
|
(lambda (lines) (setq native-lines lines))))
|
||||||
|
(ebox-scroll-down 5))
|
||||||
|
;; Each owner is currently a separately committed retained surface
|
||||||
|
;; update; the later atomic multi-owner scroll transaction will reduce
|
||||||
|
;; this to one revision without changing the consumption contract.
|
||||||
|
(should (= (tp-surface-revision surface) (+ revision 2)))
|
||||||
|
(should (= (plist-get (ebox-scroll-state inner-id) :scroll-offset) 1))
|
||||||
|
(should (= (plist-get (ebox-scroll-state middle-id) :scroll-offset) 2))
|
||||||
|
(should-not (ebox-scroll-state outer-id))
|
||||||
|
(should (= native-lines 2))
|
||||||
|
(setq native-lines nil)
|
||||||
|
(ebox--scroll-consume-intent
|
||||||
|
(ebox--scroll-intent-create -5
|
||||||
|
(list inner-id middle-id outer-id))
|
||||||
|
(lambda (residual) (setq native-lines residual)))
|
||||||
|
(should (= (plist-get (ebox-scroll-state inner-id) :scroll-offset) 0))
|
||||||
|
(should (= (plist-get (ebox-scroll-state middle-id) :scroll-offset) 0))
|
||||||
|
(should-not (ebox-scroll-state outer-id))
|
||||||
|
(should (= native-lines -2))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-refresh-at-boundary-does-not-swallow-input ()
|
||||||
|
"Refreshing unchanged pixels should leave the complete line intent residual."
|
||||||
|
(let (native-residual)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (&rest _) 'refreshed)))
|
||||||
|
(let ((result
|
||||||
|
(ebox--scroll-consume-intent
|
||||||
|
(ebox--scroll-intent-create 1 '(inner))
|
||||||
|
(lambda (delta) (setq native-residual delta)))))
|
||||||
|
(should (equal (plist-get result :consumption) '((inner . 0))))
|
||||||
|
(should (= (plist-get result :ebox-consumed) 0))
|
||||||
|
(should (= (plist-get result :native-residual) 1))
|
||||||
|
(should (= native-residual 1))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-pending-prefix-does-not-swallow-input ()
|
||||||
|
"An unfulfilled lazy-prefix request should leave its line intent residual."
|
||||||
|
(let (native-residual)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (&rest _) 'pending)))
|
||||||
|
(let ((result
|
||||||
|
(ebox--scroll-consume-intent
|
||||||
|
(ebox--scroll-intent-create 3 '(inner parent))
|
||||||
|
(lambda (delta) (setq native-residual delta)))))
|
||||||
|
(should (equal (plist-get result :consumption)
|
||||||
|
'((inner . 0) (parent . 0))))
|
||||||
|
(should (= (plist-get result :ebox-consumed) 0))
|
||||||
|
(should (= (plist-get result :native-residual) 3))
|
||||||
|
(should (= native-residual 3))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-anchor-routes-keyboard-and-wheel-independently ()
|
||||||
|
"Keyboard anchors at point while wheel anchors at the event position."
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-ids-at-point)
|
||||||
|
(lambda () '(point-inner point-outer)))
|
||||||
|
((symbol-function 'ebox--wheel-region-ids)
|
||||||
|
(lambda (_event) '(wheel-inner wheel-outer))))
|
||||||
|
(should (equal (plist-get (ebox--scroll-intent-at-point 1) :chain)
|
||||||
|
'(point-inner point-outer)))
|
||||||
|
(should (equal (plist-get
|
||||||
|
(ebox--scroll-intent-at-event 'fake-event 1) :chain)
|
||||||
|
'(wheel-inner wheel-outer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-intent-native-fallback-preserves-direction-and-count ()
|
||||||
|
"Keyboard residuals should call the matching native command exactly once."
|
||||||
|
(let (native-up native-down)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-region-ids-at-point)
|
||||||
|
(lambda () '(inner)))
|
||||||
|
((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (_region-id delta &optional _budget)
|
||||||
|
(if (> delta 0) 1 -1)))
|
||||||
|
((symbol-function 'scroll-up)
|
||||||
|
(lambda (lines) (setq native-up lines)))
|
||||||
|
((symbol-function 'scroll-down)
|
||||||
|
(lambda (lines) (setq native-down lines))))
|
||||||
|
(ebox-scroll-down 3)
|
||||||
|
(ebox-scroll-up 3))
|
||||||
|
(should (= native-up 2))
|
||||||
|
(should (= native-down 2))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-wheel-partial-residual-uses-exact-native-direction ()
|
||||||
|
"A partial wheel residual should not replay the complete wheel event."
|
||||||
|
(let (native-up native-down mwheel-called)
|
||||||
|
(cl-letf (((symbol-function 'event-start)
|
||||||
|
(lambda (_event) (list (selected-window) (point-min))))
|
||||||
|
((symbol-function 'scroll-up)
|
||||||
|
(lambda (lines) (setq native-up lines)))
|
||||||
|
((symbol-function 'scroll-down)
|
||||||
|
(lambda (lines) (setq native-down lines)))
|
||||||
|
((symbol-function 'mwheel-scroll)
|
||||||
|
(lambda (&rest _) (setq mwheel-called t))))
|
||||||
|
(ebox--wheel-native-residual 'event 2 5 nil)
|
||||||
|
(ebox--wheel-native-residual 'event -3 -5 nil))
|
||||||
|
(should (= native-up 2))
|
||||||
|
(should (= native-down 3))
|
||||||
|
(should-not mwheel-called)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-wheel-smooth-boundary-defers-to-residual-chain ()
|
||||||
|
"Smooth mode should use the chain allocator when an owner cannot finish."
|
||||||
|
(let ((ebox-wheel-smooth-scroll t)
|
||||||
|
(capacity '((inner . 1) (parent . 2)))
|
||||||
|
native-residual
|
||||||
|
smooth-called)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-intent-at-event)
|
||||||
|
(lambda (_event delta)
|
||||||
|
(ebox--scroll-intent-create delta '(inner parent))))
|
||||||
|
((symbol-function 'ebox--first-scrollable-region)
|
||||||
|
(lambda (&rest _) 'inner))
|
||||||
|
((symbol-function 'ebox--scroll-region-can-consume-complete-p)
|
||||||
|
(lambda (&rest _) nil))
|
||||||
|
((symbol-function 'ebox--smooth-scroll-region-by)
|
||||||
|
(lambda (&rest _) (setq smooth-called t)))
|
||||||
|
((symbol-function 'ebox--scroll-region-by)
|
||||||
|
(lambda (region-id delta &optional _budget)
|
||||||
|
(let* ((available (alist-get region-id capacity))
|
||||||
|
(consumed (min delta available)))
|
||||||
|
(setf (alist-get region-id capacity) (- available consumed))
|
||||||
|
consumed)))
|
||||||
|
((symbol-function 'ebox--wheel-native-residual)
|
||||||
|
(lambda (_event residual _requested _arg)
|
||||||
|
(setq native-residual residual))))
|
||||||
|
(ebox--wheel-scroll 'event 5 nil t))
|
||||||
|
(should-not smooth-called)
|
||||||
|
(should (= native-residual 2))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-mount-fallback-orders-equal-spans-deterministically ()
|
||||||
|
"Equal mounted spans should use semantic depth and a stable id tie-break."
|
||||||
|
(let ((ebox--scroll-global-state (make-hash-table :test 'equal)))
|
||||||
|
(puthash 'z-owner t ebox--scroll-global-state)
|
||||||
|
(puthash 'a-owner t ebox--scroll-global-state)
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface-region-bounds)
|
||||||
|
(lambda (&rest _) '(1 . 5)))
|
||||||
|
((symbol-function 'ebox--scroll-region-semantic-depth)
|
||||||
|
(lambda (_region-id) 2)))
|
||||||
|
(should (equal (ebox--scroll-region-ids-in-mounts-at-pos 2)
|
||||||
|
'(a-owner z-owner))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-fast-report-uses-boolean-proof-flag ()
|
||||||
|
"Retained-window proof payload should not leak into the public report."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((layout (ebox-build
|
||||||
|
'(box :id "root" :content "A\nB"
|
||||||
|
:height 1 :overflow scroll))))
|
||||||
|
(ebox-test--with-rendered-buffer layout
|
||||||
|
(let ((region-id (ebox-test--selector-region-id
|
||||||
|
(current-buffer) "#root")))
|
||||||
|
(should (= (ebox--scroll-region-by region-id 1) 1))
|
||||||
|
(let ((flag (plist-get (ebox-test--buffer-update-report)
|
||||||
|
:scroll-patch-fast-p)))
|
||||||
|
(should (memq flag '(nil t))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-defaults-follow-one-line-native-buffer-scrolling ()
|
||||||
|
"Default wheel input should be one immediate document line."
|
||||||
|
(should (= ebox-wheel-scroll-step 1))
|
||||||
|
(should-not ebox-wheel-smooth-scroll))
|
||||||
|
|
||||||
(ert-deftest ebox-scroll-progress-requires-real-delta-or-pending ()
|
(ert-deftest ebox-scroll-progress-requires-real-delta-or-pending ()
|
||||||
"Scroll progress is either a non-zero consumed line count or lazy pending."
|
"Scroll progress is either a non-zero consumed line count or lazy pending."
|
||||||
(should (ebox--scroll-progress-p 1))
|
(should (ebox--scroll-progress-p 1))
|
||||||
(should (ebox--scroll-progress-p -1))
|
(should (ebox--scroll-progress-p -1))
|
||||||
(should (ebox--scroll-progress-p 'pending))
|
(should (ebox--scroll-progress-p 'pending))
|
||||||
|
(should-not (ebox--scroll-progress-p 'refreshed))
|
||||||
(should-not (ebox--scroll-progress-p 0))
|
(should-not (ebox--scroll-progress-p 0))
|
||||||
(should-not (ebox--scroll-progress-p nil)))
|
(should-not (ebox--scroll-progress-p nil)))
|
||||||
|
|
||||||
|
|||||||
@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
(defconst ebox-docs-test--active-files
|
(defconst ebox-docs-test--active-files
|
||||||
'("Makefile" ".github/workflows/ci.yml"
|
'("Makefile" ".github/workflows/ci.yml"
|
||||||
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-tree.el"
|
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-child-range.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-surface.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-child-range-tests.el" "tests/ebox-grid-tests.el"
|
||||||
"tests/ebox-commit-tests.el" "tests/ebox-surface-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"
|
||||||
@ -37,7 +37,7 @@
|
|||||||
"Active Ebox files that the maintainer map must cover.")
|
"Active Ebox files that the maintainer map must cover.")
|
||||||
|
|
||||||
(defconst ebox-docs-test--targets
|
(defconst ebox-docs-test--targets
|
||||||
'("check" "ci" "load" "compile" "core-tests" "grid-tests"
|
'("check" "ci" "load" "compile" "core-tests" "child-range-tests" "grid-tests"
|
||||||
"ebox-commit-tests" "surface-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" "performance-evaluator"
|
"docs-contract-tests" "ci-contract-tests" "performance-evaluator"
|
||||||
|
|||||||
@ -37,6 +37,22 @@
|
|||||||
(should (= (length lines) 1))
|
(should (= (length lines) 1))
|
||||||
(should (= (ebox--string-pixel-width (car lines)) 120))))
|
(should (= (ebox--string-pixel-width (car lines)) 120))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-grid-stretch-children-fill-fractional-tracks ()
|
||||||
|
"Stretch-width children must reflow to their assigned fractional tracks."
|
||||||
|
(let* ((left (ebox-create :content "left" :width 'stretch))
|
||||||
|
(calls 0)
|
||||||
|
(entry-left (list :node left :column 1 :column-span 1))
|
||||||
|
(rendered (make-hash-table :test #'eq)))
|
||||||
|
(puthash left "left" rendered)
|
||||||
|
(cl-letf (((symbol-function 'ebox--render-with-cache)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(cl-incf calls)
|
||||||
|
"track-width")))
|
||||||
|
(should (equal "track-width"
|
||||||
|
(ebox-grid--entry-source
|
||||||
|
entry-left rendered 100 '(:justify-items stretch))))
|
||||||
|
(should (= calls 1)))))
|
||||||
|
|
||||||
(ert-deftest ebox-grid-constrains-auto-width-children-to-track-size ()
|
(ert-deftest ebox-grid-constrains-auto-width-children-to-track-size ()
|
||||||
"Auto-width grid children should render within their assigned track."
|
"Auto-width grid children should render within their assigned track."
|
||||||
(let* ((ebox-viewport-width 120)
|
(let* ((ebox-viewport-width 120)
|
||||||
|
|||||||
@ -65,7 +65,8 @@
|
|||||||
(should (commandp 'ebox-byte-compile))
|
(should (commandp 'ebox-byte-compile))
|
||||||
(should
|
(should
|
||||||
(equal ebox--compile-sources
|
(equal ebox--compile-sources
|
||||||
'("ebox-cache.el" "ebox-style.el" "ebox-tree.el" "ebox-measure.el"
|
'("ebox-cache.el" "ebox-style.el" "ebox-child-range.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-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
"ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||||
@ -163,7 +164,10 @@
|
|||||||
ebox-build
|
ebox-build
|
||||||
ebox-buffer-mode
|
ebox-buffer-mode
|
||||||
ebox-candidate-begin
|
ebox-candidate-begin
|
||||||
|
ebox-child-range
|
||||||
ebox-candidate-replace
|
ebox-candidate-replace
|
||||||
|
ebox-candidate-replace-range-ref
|
||||||
|
ebox-candidate-replace-root
|
||||||
ebox-candidate-replace-host-ref
|
ebox-candidate-replace-host-ref
|
||||||
ebox-clear-cache
|
ebox-clear-cache
|
||||||
ebox-column
|
ebox-column
|
||||||
|
|||||||
@ -27,6 +27,135 @@
|
|||||||
(propertize "Open" 'keymap map 'mouse-face 'highlight
|
(propertize "Open" 'keymap map 'mouse-face 'highlight
|
||||||
'help-echo "Open this item")))
|
'help-echo "Open this item")))
|
||||||
|
|
||||||
|
(ert-deftest ebox-range-ref-present-p-is-a-read-only-boundary-query ()
|
||||||
|
"Expose mounted Range anchor presence without leaking runtime tables."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-range-anchor-query*"))
|
||||||
|
(table (make-hash-table :test #'equal)))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(puthash 'probe 'record table)
|
||||||
|
(cl-letf (((symbol-function 'ebox--buffer-render-state)
|
||||||
|
(lambda (_buffer)
|
||||||
|
(list :range-ref-table table))))
|
||||||
|
(should (equal 'record
|
||||||
|
(ebox-range-ref-present-p buffer 'probe)))
|
||||||
|
(should-not (ebox-range-ref-present-p buffer 'missing))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-context-initializes-from-live-window ()
|
||||||
|
"Use live window dimensions only when no explicit viewport is bound."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-live-viewport-test*"))
|
||||||
|
(noninteractive nil)
|
||||||
|
(ebox-viewport-width nil) (ebox-viewport-height nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _) nil))
|
||||||
|
((symbol-function 'window-body-width)
|
||||||
|
(lambda (_window pixelwise)
|
||||||
|
(should pixelwise)
|
||||||
|
777))
|
||||||
|
((symbol-function 'window-pixel-width)
|
||||||
|
(lambda (_window)
|
||||||
|
(error "Window outer width must not be sampled")))
|
||||||
|
((symbol-function 'window-body-height)
|
||||||
|
(lambda (_window) 31)))
|
||||||
|
(let ((values (ebox-surface--context-values buffer nil nil)))
|
||||||
|
(should (= 776 (plist-get values :viewport-width)))
|
||||||
|
(should (= 31 (plist-get values :viewport-height)))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-context-keeps-headless-viewport-nil ()
|
||||||
|
"Keep viewport dimensions nil when BUFFER has no live window."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-headless-viewport-test*"))
|
||||||
|
(ebox-viewport-width nil) (ebox-viewport-height nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _) nil)))
|
||||||
|
(let ((values (ebox-surface--context-values buffer nil nil)))
|
||||||
|
(should-not (plist-get values :viewport-width))
|
||||||
|
(should-not (plist-get values :viewport-height))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-context-rejects-half-width-pixelwise-report ()
|
||||||
|
"Use the outer pixel width when a GUI body query returns a half-width."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-half-width-viewport-test*"))
|
||||||
|
(noninteractive nil)
|
||||||
|
(ebox-viewport-width nil) (ebox-viewport-height nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _) nil))
|
||||||
|
((symbol-function 'window-body-width)
|
||||||
|
(lambda (_window pixelwise)
|
||||||
|
(should pixelwise)
|
||||||
|
715))
|
||||||
|
((symbol-function 'window-pixel-width)
|
||||||
|
(lambda (_window) 1430))
|
||||||
|
((symbol-function 'window-body-height)
|
||||||
|
(lambda (_window) 62)))
|
||||||
|
(let ((values (ebox-surface--context-values buffer nil nil)))
|
||||||
|
(should (= 1429 (plist-get values :viewport-width)))
|
||||||
|
(should (= 62 (plist-get values :viewport-height)))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-context-prefers-selected-target-window ()
|
||||||
|
"Ignore stale cross-frame lookup when selected window shows BUFFER."
|
||||||
|
(let* ((buffer (generate-new-buffer " *ebox-selected-viewport-test*"))
|
||||||
|
(window (selected-window))
|
||||||
|
(old-buffer (window-buffer window))
|
||||||
|
(ebox-viewport-width nil) (ebox-viewport-height nil))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(set-window-buffer window buffer)
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(error "Stale cross-frame lookup must not run")))
|
||||||
|
((symbol-function 'window-body-width)
|
||||||
|
(lambda (candidate pixelwise)
|
||||||
|
(should (eq candidate window))
|
||||||
|
(should pixelwise)
|
||||||
|
1400))
|
||||||
|
((symbol-function 'window-body-height)
|
||||||
|
(lambda (candidate)
|
||||||
|
(should (eq candidate window))
|
||||||
|
60)))
|
||||||
|
(let ((values (ebox-surface--context-values buffer nil nil)))
|
||||||
|
(should (= 1399 (plist-get values :viewport-width)))
|
||||||
|
(should (= 60 (plist-get values :viewport-height))))))
|
||||||
|
(when (window-live-p window) (set-window-buffer window old-buffer))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-context-prefers-current-frame-over-stale-frame ()
|
||||||
|
"Do not resize a live surface from an older client frame's window."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-current-frame-viewport-test*"))
|
||||||
|
(noninteractive nil)
|
||||||
|
(ebox-viewport-width nil) (ebox-viewport-height nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'selected-window)
|
||||||
|
(lambda () 'selected-window))
|
||||||
|
((symbol-function 'window-live-p)
|
||||||
|
(lambda (_window) t))
|
||||||
|
((symbol-function 'window-buffer)
|
||||||
|
(lambda (_window) (get-buffer-create " *other-window*")))
|
||||||
|
((symbol-function 'selected-frame)
|
||||||
|
(lambda () 'current-frame))
|
||||||
|
((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (_buffer frame)
|
||||||
|
(if (eq frame 'current-frame)
|
||||||
|
'current-frame-window
|
||||||
|
'stale-frame-window)))
|
||||||
|
((symbol-function 'window-body-width)
|
||||||
|
(lambda (window pixelwise)
|
||||||
|
(should pixelwise)
|
||||||
|
(if (eq window 'current-frame-window) 901 333)))
|
||||||
|
((symbol-function 'window-body-height)
|
||||||
|
(lambda (_window) 31)))
|
||||||
|
(let ((values (ebox-surface--context-values buffer nil nil)))
|
||||||
|
(should (= 900 (plist-get values :viewport-width)))
|
||||||
|
(should (= 31 (plist-get values :viewport-height)))))
|
||||||
|
(kill-buffer buffer)
|
||||||
|
(when (get-buffer " *other-window*")
|
||||||
|
(kill-buffer " *other-window*")))))
|
||||||
|
|
||||||
(defun ebox-surface-test--hash-fingerprint (table)
|
(defun ebox-surface-test--hash-fingerprint (table)
|
||||||
"Return a stable content fingerprint for hash TABLE.
|
"Return a stable content fingerprint for hash TABLE.
|
||||||
The fingerprint checks entries rather than only table identity, so a failed
|
The fingerprint checks entries rather than only table identity, so a failed
|
||||||
@ -223,6 +352,52 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(ebox--render-owned-text-value-p
|
(ebox--render-owned-text-value-p
|
||||||
'face (get-text-property 0 'face rendered)))))
|
'face (get-text-property 0 'face rendered)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-paint-origin-captures-before-composition ()
|
||||||
|
"Capture caller face once before Ebox adds a paint contribution."
|
||||||
|
(let* ((caller-face '(:weight bold))
|
||||||
|
(rendered (propertize "x" 'face caller-face))
|
||||||
|
(ebox--paint-origin-capture-p t))
|
||||||
|
(ebox--add-render-face! rendered 0 1 '(:foreground "red") t)
|
||||||
|
(let ((origin (get-text-property 0 ebox--paint-origin-property rendered)))
|
||||||
|
(should (ebox--paint-origin-p origin))
|
||||||
|
(should (equal caller-face
|
||||||
|
(ebox--paint-origin-baseline origin)))
|
||||||
|
(should (equal (list caller-face '(:foreground "red"))
|
||||||
|
(get-text-property 0 'face rendered))))
|
||||||
|
;; A nested contribution must not replace the original caller baseline.
|
||||||
|
(ebox--add-render-face! rendered 0 1 '(:background "blue") t)
|
||||||
|
(should (equal caller-face
|
||||||
|
(ebox--paint-origin-baseline
|
||||||
|
(get-text-property 0 ebox--paint-origin-property
|
||||||
|
rendered))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-surface-paint-address-is-semantic-and-ordered ()
|
||||||
|
"Paint ledger addresses use owner facts, content index, and local order."
|
||||||
|
(let ((rendered (copy-sequence "ab")))
|
||||||
|
(put-text-property 0 1 'ebox-content-owner 7 rendered)
|
||||||
|
(put-text-property 0 1 'ebox-content-idx 3 rendered)
|
||||||
|
(put-text-property 0 1 'ebox-content-owners '(7 2) rendered)
|
||||||
|
(put-text-property 0 1 'face 'bold rendered)
|
||||||
|
(put-text-property 1 2 'ebox-content-owner 7 rendered)
|
||||||
|
(put-text-property 1 2 'ebox-content-idx 3 rendered)
|
||||||
|
(put-text-property 1 2 'ebox-content-owners '(7 2) rendered)
|
||||||
|
(put-text-property 1 2 'face 'italic rendered)
|
||||||
|
(let* ((origin (ebox--paint-origin-create :baseline '(:weight bold)))
|
||||||
|
(_ (put-text-property 0 2 ebox--paint-origin-property
|
||||||
|
origin rendered))
|
||||||
|
(fragments (ebox-surface--rendered-fragments rendered))
|
||||||
|
(first (car fragments))
|
||||||
|
(second (cadr fragments))
|
||||||
|
(address (plist-get first :paint-address)))
|
||||||
|
(should (= (length fragments) 2))
|
||||||
|
(should (equal (plist-get address :content-owner) 7))
|
||||||
|
(should (= (plist-get address :content-index) 3))
|
||||||
|
(should (= (plist-get address :ordinal) 0))
|
||||||
|
(should (= (plist-get (plist-get second :paint-address) :ordinal) 1))
|
||||||
|
(should (equal (plist-get first :face-baseline) '(:weight bold)))
|
||||||
|
(should (plist-get first :face-baseline-known-p))
|
||||||
|
(should-not (get-text-property 0 ebox--paint-origin-property rendered)))))
|
||||||
|
|
||||||
(ert-deftest ebox-surface-candidate-plan-copies-face-property-values ()
|
(ert-deftest ebox-surface-candidate-plan-copies-face-property-values ()
|
||||||
"Candidate plans isolate mutable face values despite provenance hints."
|
"Candidate plans isolate mutable face values despite provenance hints."
|
||||||
(let* ((color (copy-sequence "#192233"))
|
(let* ((color (copy-sequence "#192233"))
|
||||||
@ -1110,15 +1285,19 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-viewport-reflow-falls-back-for-scroll-and-visible-overflow ()
|
(ert-deftest ebox-viewport-reflow-falls-back-for-scroll-and-visible-overflow ()
|
||||||
"Scroll state and visible overflow must not enter retained reflow."
|
"Nested scroll state and visible overflow must not enter retained reflow."
|
||||||
(dolist (fixture
|
(dolist (fixture
|
||||||
(list
|
(list
|
||||||
(cons 'scroll
|
(cons 'nested-scroll
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(ebox-create :content "zero\none\ntwo"
|
(ebox-create :content "outer"
|
||||||
:width '(viewport)
|
:width '(viewport)
|
||||||
:height 2
|
:height 2
|
||||||
:overflow 'scroll)))
|
:overflow 'scroll
|
||||||
|
:ebox-content-node
|
||||||
|
(ebox-create :content "zero\none\ntwo"
|
||||||
|
:height 2
|
||||||
|
:overflow 'scroll))))
|
||||||
(cons 'visible-overflow
|
(cons 'visible-overflow
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(ebox-create :content "one\ntwo"
|
(ebox-create :content "one\ntwo"
|
||||||
@ -1147,6 +1326,47 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer))))))
|
(kill-buffer buffer))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-reflow-retains-viewport-dependent-root-scroll ()
|
||||||
|
"A sole root scroll owner may reflow its own viewport-dependent content."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-viewport-root-scroll*"))
|
||||||
|
(ensured-node-count 0)
|
||||||
|
(original-ensure (symbol-function 'ebox-surface--ensure-node-tree)))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox-viewport-width 160)
|
||||||
|
(ebox-viewport-height 2)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root-scroll
|
||||||
|
:content "zero\none\ntwo\nthree"
|
||||||
|
:width '(viewport)
|
||||||
|
:height '(viewport-height)
|
||||||
|
:overflow 'scroll))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
|
||||||
|
(lambda (&rest args)
|
||||||
|
(cl-incf ensured-node-count)
|
||||||
|
(apply original-ensure args))))
|
||||||
|
(ebox-rerender-buffer-with-context buffer 240 2))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(report (ebox-buffer-update-report buffer))
|
||||||
|
(expected
|
||||||
|
(let ((ebox-viewport-width 240)
|
||||||
|
(ebox-viewport-height 2))
|
||||||
|
(ebox-render (plist-get state :root-node))))
|
||||||
|
(actual
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(buffer-substring (point-min) (point-max)))))
|
||||||
|
(should (= ensured-node-count 0))
|
||||||
|
(should (eq (plist-get report :projection-kind)
|
||||||
|
'viewport-reflow))
|
||||||
|
(should-not (plist-get report :tp-full-root))
|
||||||
|
(should-not (plist-get report :tp-scope-fallback))
|
||||||
|
(should (equal-including-properties actual expected))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-viewport-reflow-supports-height-and-both-axis-resize ()
|
(ert-deftest ebox-viewport-reflow-supports-height-and-both-axis-resize ()
|
||||||
"Retained viewport reflow should cover height-only and two-axis changes."
|
"Retained viewport reflow should cover height-only and two-axis changes."
|
||||||
(dolist (case
|
(dolist (case
|
||||||
@ -1400,6 +1620,200 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-theme-delta-reuses-cascade-facts ()
|
||||||
|
"Copy a paint-only Theme delta without rerunning ECSS cascade computation."
|
||||||
|
(let* ((subject (ecss-subject-create :type "box"))
|
||||||
|
(old-declarations
|
||||||
|
'(ebox/width (100) ebox/color "#252A2E"
|
||||||
|
ebox/background-color "#F8F5EE"))
|
||||||
|
(new-declarations
|
||||||
|
'(ebox/width (100) ebox/color "#F2EEE4"
|
||||||
|
ebox/background-color "#1B1F20"))
|
||||||
|
(old-style
|
||||||
|
(ebox-style-compute-subject subject old-declarations))
|
||||||
|
(delta
|
||||||
|
(ebox-style--theme-delta-computed
|
||||||
|
old-style old-declarations new-declarations)))
|
||||||
|
(should (ecss-computed-style-p delta))
|
||||||
|
(should-not (eq old-style delta))
|
||||||
|
(should (equal "#F2EEE4"
|
||||||
|
(ecss-computed-style-value delta 'ebox/color)))
|
||||||
|
(should (equal "#1B1F20"
|
||||||
|
(ecss-computed-style-value
|
||||||
|
delta 'ebox/background-color)))
|
||||||
|
(should (equal (ecss-computed-style-value old-style 'ebox/width)
|
||||||
|
(ecss-computed-style-value delta 'ebox/width)))
|
||||||
|
(should-not
|
||||||
|
(ebox-style--theme-delta-computed
|
||||||
|
old-style old-declarations
|
||||||
|
(plist-put (copy-sequence new-declarations) 'ebox/width '(120))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-theme-delta-propagates-inherited-color ()
|
||||||
|
"Copy a proven inherited Theme color without rerunning ECSS.
|
||||||
|
The child has no explicit color declaration; only its static parent color
|
||||||
|
changes. Geometry and non-inherited computed values must remain identical."
|
||||||
|
(let* ((parent (ecss-subject-create :type "box"))
|
||||||
|
(child (ecss-subject-create :type "box" :parent parent))
|
||||||
|
(parent-old
|
||||||
|
(ebox-style-compute-subject parent '(ebox/color "#111111")))
|
||||||
|
(parent-new
|
||||||
|
(ebox-style-compute-subject parent '(ebox/color "#222222")))
|
||||||
|
(declarations '(ebox/width (100) ebox/background-color "#eeeeee"))
|
||||||
|
(style
|
||||||
|
(ebox-style-compute-subject child declarations parent-old))
|
||||||
|
(delta
|
||||||
|
(ebox-style--theme-inherited-delta-computed
|
||||||
|
style declarations parent-old parent-new)))
|
||||||
|
(should (ecss-computed-style-p delta))
|
||||||
|
(should (equal "#222222"
|
||||||
|
(ecss-computed-style-value delta 'ebox/color)))
|
||||||
|
(should (equal (ecss-computed-style-value style 'ebox/width)
|
||||||
|
(ecss-computed-style-value delta 'ebox/width)))
|
||||||
|
(should (equal (ecss-computed-style-value style 'ebox/background-color)
|
||||||
|
(ecss-computed-style-value
|
||||||
|
delta 'ebox/background-color)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-theme-parent-delta-reuses-explicit-child-style ()
|
||||||
|
"Reuse an explicit child style when only its static parent Theme changes."
|
||||||
|
(let* ((parent (ecss-subject-create :type "box"))
|
||||||
|
(child (ecss-subject-create :type "box" :parent parent))
|
||||||
|
(parent-old
|
||||||
|
(ebox-style-compute-subject parent '(ebox/color "#111111")))
|
||||||
|
(parent-new
|
||||||
|
(ebox-style-compute-subject parent '(ebox/color "#222222")))
|
||||||
|
(declarations '(ebox/color "#ffffff" ebox/width (100)))
|
||||||
|
(style
|
||||||
|
(ebox-style-compute-subject child declarations parent-old))
|
||||||
|
(delta
|
||||||
|
(ebox-style--theme-parent-delta-computed
|
||||||
|
style declarations parent-old parent-new)))
|
||||||
|
(should (ecss-computed-style-p delta))
|
||||||
|
(should (equal (ecss-computed-style-values style)
|
||||||
|
(ecss-computed-style-values delta)))
|
||||||
|
(let ((parent-font-new
|
||||||
|
(ebox-style-compute-subject
|
||||||
|
parent '(ebox/color "#222222" ebox/font-height 2.0))))
|
||||||
|
(should-not
|
||||||
|
(ebox-style--theme-parent-delta-computed
|
||||||
|
style declarations parent-old parent-font-new)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-tree-source-signature-ignores-derived-width-proof ()
|
||||||
|
"A layout-derived exact-width flag must not dirty declarative content."
|
||||||
|
(let* ((old (ebox-create :content "Stable" :width '(100)))
|
||||||
|
(new (copy-tree old)))
|
||||||
|
(plist-put old :ebox-content-width-exact-p nil)
|
||||||
|
(plist-put new :ebox-content-width-exact-p t)
|
||||||
|
(should (equal (ebox-tree-node-local-source-signature old)
|
||||||
|
(ebox-tree-node-local-source-signature new)))
|
||||||
|
(should-not (memq :ebox-content-width-exact-p
|
||||||
|
(ebox-tree-node-local-changed-keys old new)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-tree-grid-source-signature-canonicalizes-layout-aliases ()
|
||||||
|
"Equivalent Grid gap/paint aliases must not become geometry dirtiness."
|
||||||
|
(let ((old (list :ebox-type 'grid
|
||||||
|
:raw-props '(:width stretch
|
||||||
|
:grid-template-columns (1fr 1fr)
|
||||||
|
:grid-row-gap 1 :grid-column-gap (12)
|
||||||
|
:color "#252A2E" :background-color "#F8F5EE")))
|
||||||
|
(new (list :ebox-type 'grid
|
||||||
|
:raw-props '(:width stretch
|
||||||
|
:grid-template-columns (1fr 1fr)
|
||||||
|
:gap (1 (12))
|
||||||
|
:color "#F2EEE4" :bgcolor "#1B1F20"))))
|
||||||
|
(should-not (memq :props (ebox-tree-node-local-changed-keys old new)))
|
||||||
|
(should-not (memq :raw-props (ebox-tree-node-local-changed-keys old new)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-tree-flex-source-signature-canonicalizes-layout-aliases ()
|
||||||
|
"Equivalent Flex gap aliases must not become geometry dirtiness."
|
||||||
|
(let ((old (list :ebox-type 'flex
|
||||||
|
:raw-props '(:width stretch
|
||||||
|
:row-gap 1 :column-gap (12)
|
||||||
|
:padding-block-start 0
|
||||||
|
:padding-inline-end 2
|
||||||
|
:padding-block-end 0
|
||||||
|
:padding-inline-start 2
|
||||||
|
:border-top-width (1)
|
||||||
|
:border-right-width (1)
|
||||||
|
:border-bottom-width (1)
|
||||||
|
:border-left-width (1)
|
||||||
|
:border-top-style solid
|
||||||
|
:border-right-style solid
|
||||||
|
:border-bottom-style solid
|
||||||
|
:border-left-style solid
|
||||||
|
:border-top-color "#687386"
|
||||||
|
:border-right-color "#687386"
|
||||||
|
:border-bottom-color "#687386"
|
||||||
|
:border-left-color "#687386"
|
||||||
|
:align-items center
|
||||||
|
:color "#252A2E"
|
||||||
|
:background-color "#F8F5EE")))
|
||||||
|
(new (list :ebox-type 'flex
|
||||||
|
:raw-props '(:width stretch
|
||||||
|
:gap (1 (12))
|
||||||
|
:padding (0 2)
|
||||||
|
:border ((1) solid "#687386")
|
||||||
|
:align-items center
|
||||||
|
:color "#F2EEE4"
|
||||||
|
:bgcolor "#1B1F20"))))
|
||||||
|
(should-not (memq :props (ebox-tree-node-local-changed-keys old new)))
|
||||||
|
(should-not (memq :raw-props (ebox-tree-node-local-changed-keys old new)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-theme-delta-rejects-inherited-parent-change ()
|
||||||
|
"Do not reuse a child style when its inherited parent fingerprint changes."
|
||||||
|
(let* ((parent (ecss-subject-create :type "box"))
|
||||||
|
(child (ecss-subject-create :type "box" :parent parent))
|
||||||
|
(parent-old
|
||||||
|
(ebox-style-compute-subject
|
||||||
|
parent '(ebox/font-height 1.0)))
|
||||||
|
(parent-new
|
||||||
|
(ebox-style-compute-subject
|
||||||
|
parent '(ebox/font-height 2.0)))
|
||||||
|
(declarations '(ebox/color "#ffffff"))
|
||||||
|
(style
|
||||||
|
(ebox-style-compute-subject child declarations parent-old)))
|
||||||
|
(should-not
|
||||||
|
(ebox-style--theme-delta-computed
|
||||||
|
style declarations declarations parent-old parent-new))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-theme-delta-rejects-parent-custom-property-change ()
|
||||||
|
"Do not reuse a Theme delta when a parent custom property changes."
|
||||||
|
(let* ((parent (ecss-subject-create :type "box"))
|
||||||
|
(child (ecss-subject-create :type "box" :parent parent))
|
||||||
|
(parent-old
|
||||||
|
(ebox-style-compute-subject parent '(--theme "#ffffff")))
|
||||||
|
(parent-new
|
||||||
|
(ebox-style-compute-subject parent '(--theme "#000000")))
|
||||||
|
(old-declarations
|
||||||
|
'(ebox/color "#ffffff" ebox/background-color "#ffffff"))
|
||||||
|
(new-declarations
|
||||||
|
'(ebox/color "#eeeeee" ebox/background-color "#eeeeee"))
|
||||||
|
(style
|
||||||
|
(ebox-style-compute-subject child old-declarations parent-old)))
|
||||||
|
(should-not
|
||||||
|
(ebox-style--theme-delta-computed
|
||||||
|
style old-declarations new-declarations parent-old parent-new))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-paint-declarations-equivalent-includes-border-colors ()
|
||||||
|
"Pressed/hover paint changes must not invalidate layout style closure."
|
||||||
|
(should
|
||||||
|
(ebox-style--paint-declarations-equivalent-p
|
||||||
|
'(ebox/color "#ffffff"
|
||||||
|
ebox/background-color "#2f6b43"
|
||||||
|
ebox/border-top-color "#2f6b43"
|
||||||
|
ebox/border-right-color "#2f6b43"
|
||||||
|
ebox/border-bottom-color "#2f6b43"
|
||||||
|
ebox/border-left-color "#2f6b43")
|
||||||
|
'(ebox/color "#ffffff"
|
||||||
|
ebox/background-color "#1e5a56"
|
||||||
|
ebox/border-top-color "#174a47"
|
||||||
|
ebox/border-right-color "#174a47"
|
||||||
|
ebox/border-bottom-color "#174a47"
|
||||||
|
ebox/border-left-color "#174a47")))
|
||||||
|
(should-not
|
||||||
|
(ebox-style--paint-declarations-equivalent-p
|
||||||
|
'(ebox/color "#ffffff" ebox/width max-content)
|
||||||
|
'(ebox/color "#ffffff" ebox/width stretch))))
|
||||||
|
|
||||||
(ert-deftest ebox-render-to-buffer-reuses-one-source-across-buffers ()
|
(ert-deftest ebox-render-to-buffer-reuses-one-source-across-buffers ()
|
||||||
"The public mount path should never transfer ownership of its source tree."
|
"The public mount path should never transfer ownership of its source tree."
|
||||||
(ebox-surface-test--reset-render-state)
|
(ebox-surface-test--reset-render-state)
|
||||||
@ -1601,6 +2015,30 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-finalization-contains-each-error-and-quit ()
|
||||||
|
"Post-TP scroll actions report failures without skipping later actions."
|
||||||
|
(let ((ebox--scroll-global-state (make-hash-table :test 'equal))
|
||||||
|
(ebox--smooth-scroll-state-table (make-hash-table :test 'equal))
|
||||||
|
trace diagnostics)
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-cancel-idle-prefetch)
|
||||||
|
(lambda (region-id)
|
||||||
|
(push (list 'cancel region-id) trace)
|
||||||
|
(error "cancel failure")))
|
||||||
|
((symbol-function 'ebox--smooth-scroll-stop)
|
||||||
|
(lambda (region-id)
|
||||||
|
(push (list 'stop region-id) trace)
|
||||||
|
(signal 'quit nil))))
|
||||||
|
(setq diagnostics
|
||||||
|
(ebox-incremental--finalize-declarative-scroll-publication
|
||||||
|
'(one two))))
|
||||||
|
(should (equal (nreverse trace)
|
||||||
|
'((cancel one) (stop one) (cancel two) (stop two))))
|
||||||
|
(should (= (length diagnostics) 4))
|
||||||
|
(should (equal (mapcar (lambda (entry) (plist-get entry :action))
|
||||||
|
diagnostics)
|
||||||
|
'(cancel-prefetch stop-smooth-scroll
|
||||||
|
cancel-prefetch stop-smooth-scroll)))))
|
||||||
|
|
||||||
(ert-deftest ebox-region-handles-are-surface-scoped ()
|
(ert-deftest ebox-region-handles-are-surface-scoped ()
|
||||||
"One logical id should resolve to distinct handles on independent surfaces."
|
"One logical id should resolve to distinct handles on independent surfaces."
|
||||||
(ebox-surface-test--reset-render-state)
|
(ebox-surface-test--reset-render-state)
|
||||||
@ -1656,6 +2094,25 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-region-update-preserves-point-after-incremental-publication ()
|
||||||
|
"An incremental Ebox content update must not leave point at its patch."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-point-preservation*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :id "target" :content "before target after"
|
||||||
|
:width '(200)))
|
||||||
|
(with-current-buffer buffer (goto-char 4))
|
||||||
|
(let ((point-before (with-current-buffer buffer (point))))
|
||||||
|
(ebox-region-update (ebox-region-resolve buffer "target")
|
||||||
|
:content "before changed-target after")
|
||||||
|
(should (= point-before
|
||||||
|
(with-current-buffer buffer (point))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-region-update-rejects-process-global-region-ids ()
|
(ert-deftest ebox-region-update-rejects-process-global-region-ids ()
|
||||||
"Direct updates should require a surface-scoped region handle."
|
"Direct updates should require a surface-scoped region handle."
|
||||||
(ebox-surface-test--reset-render-state)
|
(ebox-surface-test--reset-render-state)
|
||||||
@ -1761,6 +2218,171 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-patch-reuses-visible-lines-and-retains-region-index ()
|
||||||
|
"A chrome-free root scroll patch must avoid layout and keep all regions indexed."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-scroll-visible-window*")))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox-viewport-width 240)
|
||||||
|
(ebox-viewport-height 2)
|
||||||
|
(full-renders 0))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :id "scroll" :content
|
||||||
|
(mapconcat #'number-to-string (number-sequence 0 31) "\n")
|
||||||
|
:width '(120) :height 2 :overflow 'scroll))
|
||||||
|
(let* ((surface (with-current-buffer
|
||||||
|
buffer ebox-surface--buffer-surface))
|
||||||
|
(region-id
|
||||||
|
(plist-get
|
||||||
|
(car (ebox-selector-query-buffer buffer "#scroll"))
|
||||||
|
:region-id))
|
||||||
|
(old-state (tp-surface-client-state surface)))
|
||||||
|
;; Materialize this tiny fixture so the direct visible-window
|
||||||
|
;; proof is exercised rather than the lazy-prefix fallback.
|
||||||
|
(let ((scroll-state
|
||||||
|
(ebox--scroll-state-materialize-lines
|
||||||
|
region-id (ebox--scroll-get-state region-id))))
|
||||||
|
(puthash region-id scroll-state ebox--scroll-global-state))
|
||||||
|
(let ((owner-plan-calls 0)
|
||||||
|
(original-owner-plan
|
||||||
|
(symbol-function
|
||||||
|
'ebox-incremental--layout-owner-plan)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(cl-incf full-renders)
|
||||||
|
(error "full root render used by scroll patch")))
|
||||||
|
((symbol-function
|
||||||
|
'ebox-incremental--layout-owner-plan)
|
||||||
|
(lambda (&rest args)
|
||||||
|
(cl-incf owner-plan-calls)
|
||||||
|
(apply original-owner-plan args))))
|
||||||
|
(should (= (ebox--scroll-region-by region-id 1 1) 1)))
|
||||||
|
(should (= owner-plan-calls 0)))
|
||||||
|
(let* ((report (ebox-buffer-update-report buffer))
|
||||||
|
(state (tp-surface-client-state surface))
|
||||||
|
(region-table (plist-get state :region-box-table))
|
||||||
|
(region-set (plist-get state :region-id-set))
|
||||||
|
(text (with-current-buffer buffer
|
||||||
|
(buffer-substring-no-properties
|
||||||
|
(point-min) (point-max)))))
|
||||||
|
(should (= full-renders 0))
|
||||||
|
(should (eq (plist-get report :projection-kind) 'scroll-patch))
|
||||||
|
(should-not (plist-get report :tp-full-root))
|
||||||
|
(should-not (plist-get report :tp-scope-fallback))
|
||||||
|
(should (= (hash-table-count region-table)
|
||||||
|
(hash-table-count region-set)))
|
||||||
|
(maphash
|
||||||
|
(lambda (id node)
|
||||||
|
(should (eq node
|
||||||
|
(gethash (gethash id
|
||||||
|
(plist-get state
|
||||||
|
:region-node-table))
|
||||||
|
(plist-get state :node-table)))))
|
||||||
|
region-table)
|
||||||
|
(should (string-match-p "1" text))
|
||||||
|
(should-not (string-match-p "^0$" text))
|
||||||
|
(should (equal (plist-get (plist-get old-state :root-node)
|
||||||
|
:node-id)
|
||||||
|
(plist-get (plist-get state :root-node)
|
||||||
|
:node-id)))
|
||||||
|
(should (eq (gethash (plist-get (plist-get state :root-node)
|
||||||
|
:node-id)
|
||||||
|
(plist-get state
|
||||||
|
:surface-node-object-table))
|
||||||
|
(gethash (plist-get (plist-get old-state :root-node)
|
||||||
|
:node-id)
|
||||||
|
(plist-get old-state
|
||||||
|
:surface-node-object-table))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-patch-rolls-back-at-tp-client-state-publication ()
|
||||||
|
"A scroll patch failure after TP client-state must restore the old generation."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-scroll-patch-rollback*")))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox-viewport-width 240)
|
||||||
|
(ebox-viewport-height 2))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :id "scroll" :content "zero\none\ntwo\nthree"
|
||||||
|
:width '(120) :height 2 :overflow 'scroll))
|
||||||
|
(let* ((surface (with-current-buffer
|
||||||
|
buffer ebox-surface--buffer-surface))
|
||||||
|
(region-id
|
||||||
|
(plist-get
|
||||||
|
(car (ebox-selector-query-buffer buffer "#scroll"))
|
||||||
|
:region-id))
|
||||||
|
(state (tp-surface-client-state surface))
|
||||||
|
(revision (tp-surface-revision surface))
|
||||||
|
(contents (with-current-buffer buffer
|
||||||
|
(buffer-substring (point-min) (point-max))))
|
||||||
|
(old-region-table (plist-get state :region-box-table)))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state)
|
||||||
|
(error "reject scroll client-state publication")))))
|
||||||
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (= (tp-surface-revision surface) revision))
|
||||||
|
(should (eq (tp-surface-client-state surface) state))
|
||||||
|
(should (eq (plist-get state :region-box-table)
|
||||||
|
old-region-table))
|
||||||
|
(should (= (plist-get (ebox-scroll-state region-id)
|
||||||
|
:scroll-offset)
|
||||||
|
0))
|
||||||
|
(should (equal-including-properties
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(buffer-substring (point-min) (point-max)))
|
||||||
|
contents))
|
||||||
|
(should (= (ebox--scroll-region-by region-id 1 1) 1))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-patch-reuses-incomplete-flex-visible-window ()
|
||||||
|
"A lazy Flex prefix may use retained output when its visible slice is ready.
|
||||||
|
The prefix need not be fully materialized; a scroll step that remains inside
|
||||||
|
the staged rendered window must not rerun the Flex wrapper layout."
|
||||||
|
(let* ((children
|
||||||
|
(cl-loop for index below 80
|
||||||
|
collect
|
||||||
|
(ebox-create :key (intern (format "flex-cell-%03d" index))
|
||||||
|
:content (format "Cell %03d" index)
|
||||||
|
:width '(80) :height 1)))
|
||||||
|
(flex (apply #'ebox-flex :flex-flow '(row wrap)
|
||||||
|
:width '(180) :column-gap '(8) :row-gap 1
|
||||||
|
children))
|
||||||
|
(root (ebox-create :key 'scroll-root :width '(180) :height 6
|
||||||
|
:overflow 'scroll :ebox-content-node flex))
|
||||||
|
(buffer (ebox-render-to-buffer
|
||||||
|
(generate-new-buffer-name " *ebox-incomplete-flex-scroll*")
|
||||||
|
root))
|
||||||
|
(state (ebox--buffer-render-state buffer))
|
||||||
|
(region-id (car (plist-get state :scroll-region-ids)))
|
||||||
|
(scroll-state (gethash region-id
|
||||||
|
(plist-get state :scroll-state-table))))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(should region-id)
|
||||||
|
(should scroll-state)
|
||||||
|
(should-not (plist-get scroll-state :content-lines-complete-p))
|
||||||
|
(should (ebox--scroll-state-rendered-visible-window scroll-state))
|
||||||
|
(should (ebox--scroll-state-retained-window-ready-p scroll-state))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(ebox--scroll-region-by region-id 1 1))
|
||||||
|
(let ((report (ebox-buffer-update-report buffer))
|
||||||
|
(current (ebox--buffer-render-state buffer)))
|
||||||
|
(should (eq (plist-get report :projection-kind) 'scroll-patch))
|
||||||
|
(should (plist-get report :scroll-patch-fast-p))
|
||||||
|
(should-not (plist-get report :tp-full-root))
|
||||||
|
(should-not (plist-get report :tp-scope-fallback))
|
||||||
|
(should (= (hash-table-count
|
||||||
|
(plist-get current :region-id-set))
|
||||||
|
(hash-table-count
|
||||||
|
(plist-get current :region-box-table)))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer))))))
|
||||||
|
|
||||||
(ert-deftest ebox-scroll-update-rejects-a-runtime-replaced-by-its-hook ()
|
(ert-deftest ebox-scroll-update-rejects-a-runtime-replaced-by-its-hook ()
|
||||||
"A stale scroll candidate must not overwrite a hook publication."
|
"A stale scroll candidate must not overwrite a hook publication."
|
||||||
(ebox-surface-test--reset-render-state)
|
(ebox-surface-test--reset-render-state)
|
||||||
@ -1801,6 +2423,84 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-native-buffer-scroll-is-transactional-and-line-oriented ()
|
||||||
|
"An eligible root scroll uses the window one line at a time.
|
||||||
|
The test stubs the GUI window boundary so batch ERT can exercise the same
|
||||||
|
participant and rollback contract without creating a real frame."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-native-scroll*")))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox-viewport-width 120)
|
||||||
|
(ebox-viewport-height 2)
|
||||||
|
(noninteractive nil)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(window-start 1)
|
||||||
|
(window-point 1))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :id "native-scroll" :content "zero\none\ntwo\nthree"
|
||||||
|
:width '(120) :height 2 :overflow 'scroll))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(region-id (car (plist-get state :scroll-region-ids)))
|
||||||
|
(scroll-state
|
||||||
|
(gethash region-id (plist-get state :scroll-state-table))))
|
||||||
|
(plist-put state :native-buffer-scroll-p t)
|
||||||
|
(plist-put scroll-state :content-lines-complete-p t)
|
||||||
|
(plist-put scroll-state :rendered-content-lines
|
||||||
|
'("zero" "one" "two" "three"))
|
||||||
|
(plist-put scroll-state :content-height 2)
|
||||||
|
(puthash region-id scroll-state ebox--scroll-global-state)
|
||||||
|
(cl-letf (((symbol-function 'get-buffer-window)
|
||||||
|
(lambda (&rest _) 'ebox-test-window))
|
||||||
|
((symbol-function 'window-live-p)
|
||||||
|
(lambda (&rest _) t))
|
||||||
|
((symbol-function 'window-start)
|
||||||
|
(lambda (&rest _) window-start))
|
||||||
|
((symbol-function 'window-point)
|
||||||
|
(lambda (&rest _) window-point))
|
||||||
|
((symbol-function 'set-window-start)
|
||||||
|
(lambda (_window position &rest _)
|
||||||
|
(setq window-start position)))
|
||||||
|
((symbol-function 'set-window-point)
|
||||||
|
(lambda (_window position)
|
||||||
|
(setq window-point position)))
|
||||||
|
((symbol-function
|
||||||
|
'ebox--native-buffer-scroll-root-proof-p)
|
||||||
|
(lambda (&rest _) t)))
|
||||||
|
(should (= (ebox--native-buffer-scroll-by buffer region-id 1)
|
||||||
|
1))
|
||||||
|
(should (= (plist-get scroll-state :scroll-offset) 1))
|
||||||
|
(should (> window-start 1))
|
||||||
|
(should (equal (plist-get state :last-update-report)
|
||||||
|
(ebox-buffer-update-report buffer)))
|
||||||
|
(should (eq (plist-get (ebox-buffer-update-report buffer)
|
||||||
|
:projection-kind)
|
||||||
|
'native-buffer-scroll))
|
||||||
|
;; The native path is a presentation-only transaction, but it
|
||||||
|
;; must still restore both window and Ebox state if a window
|
||||||
|
;; primitive fails halfway through the move.
|
||||||
|
(setq window-start 1
|
||||||
|
window-point 1)
|
||||||
|
(plist-put scroll-state :scroll-offset 0)
|
||||||
|
(ebox-put (plist-get scroll-state :box) :scroll-offset 0)
|
||||||
|
(plist-put state :last-update-report nil)
|
||||||
|
(let ((fail-once t))
|
||||||
|
(cl-letf (((symbol-function 'set-window-point)
|
||||||
|
(lambda (_window position)
|
||||||
|
(if fail-once
|
||||||
|
(progn
|
||||||
|
(setq fail-once nil)
|
||||||
|
(error "native window point failure"))
|
||||||
|
(setq window-point position)))))
|
||||||
|
(should-error
|
||||||
|
(ebox--native-buffer-scroll-by buffer region-id 1))))
|
||||||
|
(should (= window-start 1))
|
||||||
|
(should (= window-point 1))
|
||||||
|
(should (= (plist-get scroll-state :scroll-offset) 0))
|
||||||
|
(should-not (plist-get state :last-update-report))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
(provide 'ebox-surface-tests)
|
(provide 'ebox-surface-tests)
|
||||||
|
|
||||||
;;; ebox-surface-tests.el ends here
|
;;; ebox-surface-tests.el ends here
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user