Keep layer and scroll updates local with isolated cache publication
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Batch region callbacks and preserve native repeated-click behavior. Reuse proven layer slots, root scroll content and nested viewport allocations while retaining conservative fallbacks. Isolate cold prefix producers and caches before publication, retain current-owner continuations, and project separator ownership consistently. Add lifecycle, rollback, layout and performance regression coverage with bilingual contracts.
This commit is contained in:
parent
2a64573d42
commit
efdc35f4af
20
Makefile
20
Makefile
@ -25,7 +25,7 @@ all: check
|
|||||||
|
|
||||||
.PHONY: layer-tests
|
.PHONY: layer-tests
|
||||||
layer-tests:
|
layer-tests:
|
||||||
$(EMACS_TEST) -l tests/ebox-composite-tests.el -l tests/ebox-layer-style-tests.el -l tests/ebox-layer-tests.el -l tests/ebox-layer-publication-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_TEST) -l tests/ebox-composite-tests.el -l tests/ebox-layer-style-tests.el -l tests/ebox-layer-tests.el -l tests/ebox-layer-publication-tests.el -l tests/ebox-layer-scroll-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
test: layer-tests
|
test: layer-tests
|
||||||
|
|
||||||
@ -34,6 +34,24 @@ interaction-tests:
|
|||||||
|
|
||||||
test: interaction-tests
|
test: interaction-tests
|
||||||
|
|
||||||
|
.PHONY: nested-scroll-tests
|
||||||
|
nested-scroll-tests:
|
||||||
|
$(EMACS_TEST) -l tests/ebox-nested-scroll-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
test: nested-scroll-tests
|
||||||
|
|
||||||
|
.PHONY: scroll-slice-tests
|
||||||
|
scroll-slice-tests:
|
||||||
|
$(EMACS_TEST) -l tests/ebox-scroll-slice-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
test: scroll-slice-tests
|
||||||
|
|
||||||
|
.PHONY: scroll-cold-tests
|
||||||
|
scroll-cold-tests:
|
||||||
|
$(EMACS_TEST) -l tests/ebox-scroll-cold-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
test: scroll-cold-tests
|
||||||
|
|
||||||
display-tests:
|
display-tests:
|
||||||
$(EMACS_TEST) -l tests/ebox-display-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_TEST) -l tests/ebox-display-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
|||||||
@ -47,17 +47,33 @@ If any proof condition fails, the update uses the ordinary safe projection path.
|
|||||||
|
|
||||||
## Retained scroll window
|
## Retained scroll window
|
||||||
|
|
||||||
For a chrome-free root scroll owner whose cached rendered lines cover the
|
Cached scroll intents reuse the published cascade, viewport axes and unchanged
|
||||||
current visible window (the lazy prefix may remain incomplete),
|
runtime indexes when the visible window and display context are proven stable.
|
||||||
the scroll patch joins the staged visible window directly instead of entering
|
Chrome-free roots join cached rendered lines directly; layered or chrome-bearing
|
||||||
the full candidate layout. It reuses the published cascade, viewport axes,
|
roots may reuse their complete composed content and render only the wrapper.
|
||||||
object maps, and descendant `region-box-table`; it only advances the scroll
|
Pure root offset changes preserve already composed portal and anchor content.
|
||||||
offset and installs the root `ebox-scroll-window` property. The proof is
|
Style, placement, content and display-context changes must invalidate that proof.
|
||||||
conservative: a cache miss, chrome, active style dependency, topology change,
|
|
||||||
or incomplete window falls back to the ordinary retained path. Scroll
|
A nested owner can replace its viewport within a proven unchanged allocation,
|
||||||
publication still goes through TP and the Ebox participant, so a failure after
|
without rendering unrelated siblings. With one unchanged root scroll ancestor,
|
||||||
TP client-state publication restores text, offsets, indexes, objects, mounts,
|
the update must also patch that ancestor's retained content, ownership membership
|
||||||
and report together.
|
and line indexes, and rebind its producer to the candidate tree. Cached-content
|
||||||
|
coordinates and mounted-buffer coordinates require separate proofs. Unsupported
|
||||||
|
ancestor chains, partial visibility and failed footprint or wrapper proofs retain
|
||||||
|
the ordinary fallback.
|
||||||
|
|
||||||
|
Classify cache hits before staging. A true prefix miss owns a copied target
|
||||||
|
subtree and ancestor path, private indexes and registries, copied cache lines and
|
||||||
|
freshly rebound producers. A producer must not mutate a published continuation
|
||||||
|
before TP commits. This isolation can rebuild the target prefix on a miss;
|
||||||
|
cached-hit timings do not describe that cost.
|
||||||
|
|
||||||
|
Scroll publication still goes through TP and the Ebox participant. Rejection
|
||||||
|
preserves the old text, properties, offsets, raw and rendered caches, source
|
||||||
|
nodes, indexes, objects, mounts and report. The focused regression targets are
|
||||||
|
`make layer-tests`, `make nested-scroll-tests`, `make scroll-cold-tests`
|
||||||
|
and `make scroll-slice-tests`; the playground scroll evaluator checks fresh-render
|
||||||
|
parity and distinguishes cached motion from actual prefix extension.
|
||||||
|
|
||||||
## Identity and coordinates
|
## Identity and coordinates
|
||||||
|
|
||||||
|
|||||||
@ -47,14 +47,28 @@ span、owner-scoped、formatting-context 或 mixed 局部输出被拒绝时,pr
|
|||||||
|
|
||||||
## 保留式 scroll window
|
## 保留式 scroll window
|
||||||
|
|
||||||
对于 chrome-free、缓存 rendered lines 已覆盖当前可见窗口的 root scroll owner
|
缓存滚动在可见窗口和 display context 的稳定性得到证明后,复用已发布的 cascade、
|
||||||
(lazy prefix 可以仍未完整 materialize),scroll patch 会
|
viewport axes 与未变化的 runtime index。Chrome-free root 直接拼接 rendered lines;
|
||||||
直接从 staged visible window 拼接输出,不再进入完整 candidate layout。它复用已
|
layered 或带 chrome 的 root 可以复用完整合成内容,只渲染 wrapper。纯 root offset
|
||||||
发布的 cascade、viewport axes、object maps 和 descendant `region-box-table`,只推进
|
变化保留已经合成的 portal 和 anchor 内容;style、placement、content 或 display
|
||||||
scroll offset 并安装 root `ebox-scroll-window` 属性。证明保持保守:cache miss、
|
context 变化必须使该证明失效。
|
||||||
chrome、active style dependency、topology 变化或窗口不完整时,回退到普通 retained
|
|
||||||
路径。Scroll publication 仍经过 TP 与 Ebox participant;即使 TP client-state 发布
|
Nested owner 在 allocation 不变且通过证明时,可以只替换自己的 viewport,避免渲染
|
||||||
之后失败,也会一起恢复文本、offset、index、object、mount 和 report。
|
无关兄弟节点。若存在一个 offset 不变的 root scroll ancestor,还必须更新该祖先的
|
||||||
|
保留内容、ownership membership 和 line index,并将 producer 重新绑定到 candidate
|
||||||
|
树。缓存内容坐标与 mounted buffer 坐标分别验证。不支持的祖先链、部分可见的 owner,
|
||||||
|
以及 footprint 或 wrapper 证明失败的情况,保留普通 fallback。
|
||||||
|
|
||||||
|
必须在 staging 前区分 cache hit 与真实 prefix miss。Miss 使用复制的目标子树和祖先
|
||||||
|
路径、私有 index 与 registry、复制的缓存行,以及重新绑定的 producer;producer
|
||||||
|
不得在 TP commit 前修改已发布的 continuation。该隔离可能在 miss 时重建目标 prefix,
|
||||||
|
因此 cached-hit 耗时不能代表 miss 的代价。
|
||||||
|
|
||||||
|
Scroll publication 仍经过 TP 与 Ebox participant。发布失败时,旧文本、属性、offset、
|
||||||
|
raw/rendered cache、source node、index、object、mount 和 report 均保持不变。聚焦回归
|
||||||
|
入口为 `make layer-tests`、`make nested-scroll-tests`、`make scroll-cold-tests`
|
||||||
|
和 `make scroll-slice-tests`;playground scroll evaluator 验证 fresh-render parity,
|
||||||
|
并区分缓存滚动与实际 prefix 扩展。
|
||||||
|
|
||||||
## Identity 与坐标
|
## Identity 与坐标
|
||||||
|
|
||||||
|
|||||||
@ -218,10 +218,14 @@ no command binding by itself.
|
|||||||
```
|
```
|
||||||
|
|
||||||
`:activate` accepts a zero-argument function and binds it to `RET`, `[return]`,
|
`:activate` accepts a zero-argument function and binds it to `RET`, `[return]`,
|
||||||
`SPC`, and `[mouse-1]`. `:bindings` accepts an alist from key-description strings
|
`SPC`, and `[mouse-1]`. Double and triple primary-button releases also activate
|
||||||
|
once per click. Primary-button press and drag events, including their double
|
||||||
|
and triple variants, do nothing by default, avoiding native text selection.
|
||||||
|
`:bindings` accepts an alist from key-description strings
|
||||||
or event vectors to zero-argument functions. Both options are optional; `nil`
|
or event vectors to zero-argument functions. Both options are optional; `nil`
|
||||||
omits their bindings. Duplicate keys and overlapping key prefixes are errors,
|
omits their bindings. Duplicate keys and overlapping key prefixes are errors,
|
||||||
including overlaps with the activation keys. The helper returns a native Emacs
|
including overlaps with the four primary activation keys. Explicit bindings
|
||||||
|
can override the repeated-click, press and drag defaults. The helper returns a native Emacs
|
||||||
keymap; callbacks remain literal until their command is invoked. For mouse
|
keymap; callbacks remain literal until their command is invoked. For mouse
|
||||||
events, the helper obtains the event window and runs the callback in its buffer.
|
events, the helper obtains the event window and runs the callback in its buffer.
|
||||||
It does not select that window or move point. Keyboard callbacks run in the
|
It does not select that window or move point. Keyboard callbacks run in the
|
||||||
@ -397,6 +401,7 @@ They do not publish by themselves.
|
|||||||
| `ebox-region-ids` | Return current low-level region ids in document order. |
|
| `ebox-region-ids` | Return current low-level region ids in document order. |
|
||||||
| `ebox-region-resolve` | Resolve one logical `:id` in a mounted buffer to an opaque surface-scoped handle. |
|
| `ebox-region-resolve` | Resolve one logical `:id` in a mounted buffer to an opaque surface-scoped handle. |
|
||||||
| `ebox-region-update` | Apply supported mutable style, scroll, or native node capability properties through one retained transaction. Accept an opaque region handle or a semantic `:id` in the current mounted buffer. |
|
| `ebox-region-update` | Apply supported mutable style, scroll, or native node capability properties through one retained transaction. Accept an opaque region handle or a semantic `:id` in the current mounted buffer. |
|
||||||
|
| `ebox-call-with-update-batch` | `(CALLBACK &optional BUFFER)` runs a zero-argument callback in the target buffer and groups its queued region updates; return the callback's value. |
|
||||||
| `ebox-child-range` | Framework integration only: build a nonvisual, addressable child Range descriptor while assembling a typed canonical input. |
|
| `ebox-child-range` | Framework integration only: build a nonvisual, addressable child Range descriptor while assembling a typed canonical input. |
|
||||||
| `ebox-candidate-begin` | Capture the exact current mounted generation in a one-shot candidate. |
|
| `ebox-candidate-begin` | Capture the exact current mounted generation in a one-shot candidate. |
|
||||||
| `ebox-candidate-replace` | Replace one candidate node address from a single-root `CanonicalEboxInput`. |
|
| `ebox-candidate-replace` | Replace one candidate node address from a single-root `CanonicalEboxInput`. |
|
||||||
@ -426,6 +431,27 @@ blocks an enclosing value. Omitted properties are unchanged. Replace the
|
|||||||
later caller-side mutations do not update a mounted surface. Clearing it removes that
|
later caller-side mutations do not update a mounted surface. Clearing it removes that
|
||||||
node's keymap; ordinary Emacs buffer and global bindings still apply.
|
node's keymap; ordinary Emacs buffer and global bindings still apply.
|
||||||
|
|
||||||
|
### Grouping region updates
|
||||||
|
|
||||||
|
`ebox-call-with-update-batch` defaults `BUFFER` to the current buffer and runs
|
||||||
|
`CALLBACK` there. If it starts the batch, it flushes once after a normal return
|
||||||
|
when changes are pending. An empty or unchanged callback publishes nothing and
|
||||||
|
preserves the last successful update report. The helper returns the callback's
|
||||||
|
value unchanged after a successful flush.
|
||||||
|
|
||||||
|
If a batch is already active in that buffer, the helper joins it without
|
||||||
|
flushing; the outer owner decides when to publish. On an error, quit, or other
|
||||||
|
nonlocal exit, the helper discards its own queued changes and restores the
|
||||||
|
outer batch's pending state as it was on entry. A failed flush of a batch owned
|
||||||
|
by this helper discards its unpublished changes; the existing TP transaction
|
||||||
|
restores the previously committed buffer, runtime, revision, and report.
|
||||||
|
|
||||||
|
The rollback scope is the queued `ebox-region-update` work in `BUFFER`.
|
||||||
|
Assignments to application variables, file writes, and other business side
|
||||||
|
effects in the callback are not reverted. Keep explicit
|
||||||
|
`ebox-incremental-begin-batch` / `ebox-incremental-flush` lifecycle calls outside
|
||||||
|
the callback; those lower-level calls retain their own retry semantics.
|
||||||
|
|
||||||
The author DSL has no Range form. Ordinary authors compose direct Text/Box
|
The author DSL has no Range form. Ordinary authors compose direct Text/Box
|
||||||
children and never call `ebox-child-range`; Range addresses exist only for
|
children and never call `ebox-child-range`; Range addresses exist only for
|
||||||
frameworks that already own semantic child ranges and candidate updates.
|
frameworks that already own semantic child ranges and candidate updates.
|
||||||
@ -493,6 +519,7 @@ names every entry:
|
|||||||
`ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint`,
|
`ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint`,
|
||||||
`ebox-child-range`,
|
`ebox-child-range`,
|
||||||
`ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update`,
|
`ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update`,
|
||||||
|
`ebox-call-with-update-batch`,
|
||||||
`ebox-host-ref-bounds`, `ebox-host-ref-position`.
|
`ebox-host-ref-bounds`, `ebox-host-ref-position`.
|
||||||
- Selectors: `ebox-selector-parse`, `ebox-selector-match-node-p`,
|
- Selectors: `ebox-selector-parse`, `ebox-selector-match-node-p`,
|
||||||
`ebox-selector-query-all`, `ebox-selector-query-buffer`,
|
`ebox-selector-query-all`, `ebox-selector-query-buffer`,
|
||||||
|
|||||||
@ -185,9 +185,12 @@ Emacs 设置及窗口系统;单独设置指针形状不会绑定命令。
|
|||||||
```
|
```
|
||||||
|
|
||||||
`:activate` 接收零参数函数,将其绑定到 `RET`、`[return]`、`SPC` 和 `[mouse-1]`。
|
`:activate` 接收零参数函数,将其绑定到 `RET`、`[return]`、`SPC` 和 `[mouse-1]`。
|
||||||
|
主按钮双击、三击的每次释放也各激活一次。主按钮按下和拖动事件,包括其双击、三击
|
||||||
|
变体,默认不执行操作,避免触发原生文本选择。
|
||||||
`:bindings` 接收 alist,key 是按键描述字符串或事件向量,value 是零参数函数。
|
`:bindings` 接收 alist,key 是按键描述字符串或事件向量,value 是零参数函数。
|
||||||
两项都可省略,`nil` 表示不添加相应绑定。重复按键和重叠前缀会报错,包括与激活键
|
两项都可省略,`nil` 表示不添加相应绑定。重复按键和重叠前缀会报错,包括与上述四个
|
||||||
重叠的绑定。辅助函数返回原生 Emacs keymap;回调保留为函数,直到对应命令触发时才调用。
|
基础激活键重叠的绑定。显式绑定可以覆盖连击、按下和拖动事件的默认行为。
|
||||||
|
辅助函数返回原生 Emacs keymap;回调保留为函数,直到对应命令触发时才调用。
|
||||||
鼠标事件会自动从事件取得窗口,并在其 buffer 中运行回调,不选择窗口或移动 point。
|
鼠标事件会自动从事件取得窗口,并在其 buffer 中运行回调,不选择窗口或移动 point。
|
||||||
键盘回调在当前 buffer 中运行,由 point 所在位置进行原生分派。
|
键盘回调在当前 buffer 中运行,由 point 所在位置进行原生分派。
|
||||||
|
|
||||||
@ -343,6 +346,7 @@ observer 为 nil 时,Ebox 不创建 observation context,不取 timestamp/GC
|
|||||||
| `ebox-region-ids` | 按文档顺序返回当前底层 region id。 |
|
| `ebox-region-ids` | 按文档顺序返回当前底层 region id。 |
|
||||||
| `ebox-region-resolve` | 把 mounted buffer 中一个逻辑 `:id` 解析为不透明、surface-scoped 的 handle。 |
|
| `ebox-region-resolve` | 把 mounted buffer 中一个逻辑 `:id` 解析为不透明、surface-scoped 的 handle。 |
|
||||||
| `ebox-region-update` | 通过一次 retained transaction 应用支持的可变样式、滚动或原生节点能力 property;接受不透明 region handle,或当前已挂载 buffer 中的语义 `:id`。 |
|
| `ebox-region-update` | 通过一次 retained transaction 应用支持的可变样式、滚动或原生节点能力 property;接受不透明 region handle,或当前已挂载 buffer 中的语义 `:id`。 |
|
||||||
|
| `ebox-call-with-update-batch` | `(CALLBACK &optional BUFFER)` 在目标 buffer 中运行零参数回调,合并其排队的 region 更新,并返回回调的返回值。 |
|
||||||
| `ebox-child-range` | 仅供框架集成:在组装 typed canonical input 时构造非视觉、可寻址的 child Range descriptor。 |
|
| `ebox-child-range` | 仅供框架集成:在组装 typed canonical input 时构造非视觉、可寻址的 child Range descriptor。 |
|
||||||
| `ebox-candidate-begin` | 在 one-shot candidate 中捕获当前准确 mounted generation。 |
|
| `ebox-candidate-begin` | 在 one-shot candidate 中捕获当前准确 mounted generation。 |
|
||||||
| `ebox-candidate-replace` | 用 single-root `CanonicalEboxInput` 替换一个 candidate node address。 |
|
| `ebox-candidate-replace` | 用 single-root `CanonicalEboxInput` 替换一个 candidate node address。 |
|
||||||
@ -369,6 +373,24 @@ candidate 被 commit 封存后不能复用。
|
|||||||
之后直接修改原 map 不会更新已挂载的 surface,应显式替换 `:keymap` 来发布新绑定。
|
之后直接修改原 map 不会更新已挂载的 surface,应显式替换 `:keymap` 来发布新绑定。
|
||||||
清除该属性只移除节点 keymap,普通 Emacs buffer 和全局绑定仍然适用。
|
清除该属性只移除节点 keymap,普通 Emacs buffer 和全局绑定仍然适用。
|
||||||
|
|
||||||
|
### 合并 region 更新
|
||||||
|
|
||||||
|
`ebox-call-with-update-batch` 的 `BUFFER` 默认是当前 buffer,`CALLBACK` 在该
|
||||||
|
buffer 中执行。如果 helper 自己创建 batch,回调正常返回且存在待处理变化时只
|
||||||
|
flush 一次。空回调或未产生变化的回调不会发布,也不会替换最近一次成功更新报告。
|
||||||
|
flush 成功后,helper 原样返回回调的返回值。
|
||||||
|
|
||||||
|
目标 buffer 已有活动 batch 时,helper 加入该 batch 而不 flush,由外层拥有者决定
|
||||||
|
何时发布。回调发生 error、quit 或其他非局部退出时,helper 丢弃本次调用排队的
|
||||||
|
变化,并将外层 batch 的待处理状态恢复到进入回调前的检查点。helper 自己拥有的
|
||||||
|
batch 若 flush 失败,其未发布变化会被丢弃;已有 TP transaction 恢复此前已提交
|
||||||
|
的 buffer、runtime、revision 和报告。
|
||||||
|
|
||||||
|
回滚范围仅限于 `BUFFER` 中排队的 `ebox-region-update` 工作。回调对应用变量的
|
||||||
|
赋值、文件写入及其他业务副作用不会被撤销。显式
|
||||||
|
`ebox-incremental-begin-batch` / `ebox-incremental-flush` 生命周期调用应放在回调
|
||||||
|
外部;这些底层调用保留各自的重试语义。
|
||||||
|
|
||||||
author DSL 没有 Range form。普通 author 只组合直接嵌套的 Text/Box child,不调用
|
author DSL 没有 Range form。普通 author 只组合直接嵌套的 Text/Box child,不调用
|
||||||
`ebox-child-range`;Range address 只服务已经拥有 semantic child range 与 candidate
|
`ebox-child-range`;Range address 只服务已经拥有 semantic child range 与 candidate
|
||||||
update 的框架。
|
update 的框架。
|
||||||
@ -429,6 +451,7 @@ native 模块只是加速器,不是正确性依赖。加载包不会构建它
|
|||||||
`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint`、
|
`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint`、
|
||||||
`ebox-child-range`、
|
`ebox-child-range`、
|
||||||
`ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update`、
|
`ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update`、
|
||||||
|
`ebox-call-with-update-batch`、
|
||||||
`ebox-host-ref-bounds`、`ebox-host-ref-position`;
|
`ebox-host-ref-bounds`、`ebox-host-ref-position`;
|
||||||
- Selector:`ebox-selector-parse`、`ebox-selector-match-node-p`、
|
- Selector:`ebox-selector-parse`、`ebox-selector-match-node-p`、
|
||||||
`ebox-selector-query-all`、`ebox-selector-query-buffer`、
|
`ebox-selector-query-all`、`ebox-selector-query-buffer`、
|
||||||
|
|||||||
@ -436,10 +436,14 @@ value; an omitted property leaves the enclosing surface coverage in place.
|
|||||||
|
|
||||||
`:keymap` accepts a native Emacs keymap or `nil`. Prefer `ebox-keymap-create`:
|
`:keymap` accepts a native Emacs keymap or `nil`. Prefer `ebox-keymap-create`:
|
||||||
its `:activate` callback takes no arguments and handles `RET`, `[return]`, `SPC`,
|
its `:activate` callback takes no arguments and handles `RET`, `[return]`, `SPC`,
|
||||||
and `[mouse-1]`. Use `:bindings` for an alist of key-description strings or event
|
and `[mouse-1]`. Rapid double and triple clicks each activate once per release;
|
||||||
|
primary-button press and drag events do nothing, preserving point and selection
|
||||||
|
instead of starting native word or line selection. Use `:bindings` for an alist
|
||||||
|
of key-description strings or event
|
||||||
vectors paired with zero-argument callbacks. The helper uses the event window's
|
vectors paired with zero-argument callbacks. The helper uses the event window's
|
||||||
buffer for mouse callbacks, so a callback can directly update a semantic ID
|
buffer for mouse callbacks, so a callback can directly update a semantic ID
|
||||||
with `(ebox-region-update "action" :help-echo "Activated")`.
|
with `(ebox-region-update "action" :help-echo "Activated")`.
|
||||||
|
Explicit bindings can override the repeated-click, press and drag defaults.
|
||||||
|
|
||||||
Move point into the surface to use keyboard bindings. Optional commands
|
Move point into the surface to use keyboard bindings. Optional commands
|
||||||
`ebox-next-interaction` and `ebox-previous-interaction` move between declaring
|
`ebox-next-interaction` and `ebox-previous-interaction` move between declaring
|
||||||
@ -551,6 +555,26 @@ buffer. `ebox-selector-update-buffer` applies one style update to all editable
|
|||||||
matches. Numeric region ids are diagnostic render metadata, not stable update
|
matches. Numeric region ids are diagnostic render metadata, not stable update
|
||||||
handles.
|
handles.
|
||||||
|
|
||||||
|
For one action that makes several region updates, group them:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update "status" :help-echo "Ready")
|
||||||
|
(ebox-region-update "status" :color "#166534")
|
||||||
|
'ready)
|
||||||
|
(get-buffer "*Ebox Guide*"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback runs in the supplied buffer, or the current buffer when omitted,
|
||||||
|
and the helper returns `ready` here. It flushes its own nonempty batch once;
|
||||||
|
inside an existing batch it leaves the flush to the outer owner. A nonlocal
|
||||||
|
exit restores the pending state from before this call, and a failed owned
|
||||||
|
flush preserves the last committed surface. This does not undo arbitrary
|
||||||
|
business side effects in the callback. See the [batch API
|
||||||
|
contract](ebox-api-reference.en.md#grouping-region-updates) for nesting, no-op,
|
||||||
|
and explicit lifecycle behavior.
|
||||||
|
|
||||||
## 7. Resize and scroll
|
## 7. Resize and scroll
|
||||||
|
|
||||||
When content exceeds a finite height, `:overflow scroll` creates an internal
|
When content exceeds a finite height, `:overflow scroll` creates an internal
|
||||||
|
|||||||
@ -371,9 +371,12 @@ margin 及结构性换行符;悬停遵循上面的规则。嵌套的 Text 或
|
|||||||
|
|
||||||
`:keymap` 接受原生 Emacs keymap 或 `nil`。推荐使用 `ebox-keymap-create`:
|
`:keymap` 接受原生 Emacs keymap 或 `nil`。推荐使用 `ebox-keymap-create`:
|
||||||
`:activate` 的回调不带参数,处理 `RET`、`[return]`、`SPC` 和 `[mouse-1]`。
|
`:activate` 的回调不带参数,处理 `RET`、`[return]`、`SPC` 和 `[mouse-1]`。
|
||||||
|
快速双击、三击的每次释放也各激活一次;主按钮按下和拖动默认不执行操作,保留 point
|
||||||
|
和选区,避免触发原生的选词或整行选择。
|
||||||
`:bindings` 接受 alist,按键描述字符串或事件向量对应零参数回调。
|
`:bindings` 接受 alist,按键描述字符串或事件向量对应零参数回调。
|
||||||
辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新:
|
辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新:
|
||||||
`(ebox-region-update "action" :help-echo "Activated")`。
|
`(ebox-region-update "action" :help-echo "Activated")`。
|
||||||
|
显式绑定可以覆盖连击、按下和拖动事件的默认行为。
|
||||||
|
|
||||||
使用键盘绑定时,先把 point 移到交互范围内。可选命令 `ebox-next-interaction` 与
|
使用键盘绑定时,先把 point 移到交互范围内。可选命令 `ebox-next-interaction` 与
|
||||||
`ebox-previous-interaction` 在当前渲染了可用 keymap 的声明节点之间移动。每个 owner
|
`ebox-previous-interaction` 在当前渲染了可用 keymap 的声明节点之间移动。每个 owner
|
||||||
@ -468,6 +471,23 @@ Ebox 对 keymap 创建快照;发布新绑定时应替换 `:keymap`,而不是
|
|||||||
`ebox-selector-update-buffer` 对所有可编辑匹配应用一次样式更新。数字 region id
|
`ebox-selector-update-buffer` 对所有可编辑匹配应用一次样式更新。数字 region id
|
||||||
只是诊断用渲染 metadata,不是稳定更新 handle。
|
只是诊断用渲染 metadata,不是稳定更新 handle。
|
||||||
|
|
||||||
|
一个操作需要多次更新 region 时,可以合并这些更新:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update "status" :help-echo "Ready")
|
||||||
|
(ebox-region-update "status" :color "#166534")
|
||||||
|
'ready)
|
||||||
|
(get-buffer "*Ebox Guide*"))
|
||||||
|
```
|
||||||
|
|
||||||
|
回调在指定 buffer 中运行,省略 buffer 时使用当前 buffer;本例的 helper 返回
|
||||||
|
`ready`。helper 只 flush 一次自己拥有的非空 batch;已有 batch 时,由外层拥有者
|
||||||
|
负责 flush。非局部退出会恢复到本次调用前的待处理状态,自己拥有的 batch 若
|
||||||
|
flush 失败则保留上次已提交 surface。回调中的其他业务副作用不会被撤销。嵌套、
|
||||||
|
no-op 和显式生命周期行为见[批量更新契约](ebox-api-reference.zh.md#合并-region-更新)。
|
||||||
|
|
||||||
## 7. Resize 与滚动
|
## 7. Resize 与滚动
|
||||||
|
|
||||||
内容超过有限高度时,`:overflow scroll` 会创建内部 scroll window。键盘滚动以
|
内容超过有限高度时,`:overflow scroll` 会创建内部 scroll window。键盘滚动以
|
||||||
|
|||||||
@ -35,7 +35,11 @@
|
|||||||
Retain the source properties and nested display values by identity."
|
Retain the source properties and nested display values by identity."
|
||||||
(let* ((result (substring line start end))
|
(let* ((result (substring line start end))
|
||||||
(display (get-text-property start 'display line))
|
(display (get-text-property start 'display line))
|
||||||
(properties (copy-sequence (cdr display))))
|
(properties (copy-sequence (cdr display)))
|
||||||
|
;; Match `ebox-pixel-space': retained integer allocations and
|
||||||
|
;; freshly resolved float allocations describe the same pixels.
|
||||||
|
(whole-pixels (truncate width))
|
||||||
|
(width (if (= width whole-pixels) whole-pixels width)))
|
||||||
(put-text-property
|
(put-text-property
|
||||||
0 (length result) 'display
|
0 (length result) 'display
|
||||||
(cons 'space (plist-put properties :width (list width))) result)
|
(cons 'space (plist-put properties :width (list width))) result)
|
||||||
|
|||||||
@ -104,6 +104,8 @@
|
|||||||
(declare-function ebox-layer-host-p "ebox-layer" (node))
|
(declare-function ebox-layer-host-p "ebox-layer" (node))
|
||||||
(declare-function ebox-layer-subtree-p "ebox-layer" (node))
|
(declare-function ebox-layer-subtree-p "ebox-layer" (node))
|
||||||
(declare-function ebox-layer-portals-p "ebox-layer" (node))
|
(declare-function ebox-layer-portals-p "ebox-layer" (node))
|
||||||
|
(declare-function ebox-layer-visible-portals-p "ebox-layer" (node))
|
||||||
|
(declare-function ebox-layer-cached-root-scroll-p "ebox-layer" (root state))
|
||||||
|
|
||||||
(cl-defstruct
|
(cl-defstruct
|
||||||
(ebox-incremental-context-port
|
(ebox-incremental-context-port
|
||||||
@ -1070,8 +1072,8 @@ When FORCE is non-nil, cache NODE even when its output is viewport-dependent;
|
|||||||
the complete viewport context remains part of the render signature.
|
the complete viewport context remains part of the render signature.
|
||||||
CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
|
CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
|
||||||
(if (and ebox-incremental--render-portals-p
|
(if (and ebox-incremental--render-portals-p
|
||||||
(fboundp 'ebox-layer-portals-p)
|
(fboundp 'ebox-layer-visible-portals-p)
|
||||||
(ebox-layer-portals-p node))
|
(ebox-layer-visible-portals-p node))
|
||||||
;; Cached strings do not replay the candidate-local portal collector.
|
;; Cached strings do not replay the candidate-local portal collector.
|
||||||
(ebox--render-node node)
|
(ebox--render-node node)
|
||||||
(if-let* ((prepared (and force
|
(if-let* ((prepared (and force
|
||||||
@ -1796,8 +1798,10 @@ separately for every role property."
|
|||||||
(setcdr range (max (cdr range) end))
|
(setcdr range (max (cdr range) end))
|
||||||
(puthash line-number (cons start end) line-map))))
|
(puthash line-number (cons start end) line-map))))
|
||||||
|
|
||||||
(defun ebox--build-region-line-index ()
|
(defun ebox--build-region-line-index (&optional include-owner-stacks-p)
|
||||||
"Return a per-line index of ebox region ownership in the current buffer."
|
"Return a per-line index of ebox region ownership in the current buffer.
|
||||||
|
INCLUDE-OWNER-STACKS-P includes retained wrapper ownership in an unmounted
|
||||||
|
scroll cache, whose nested owners have no TP role mounts yet."
|
||||||
(let ((region-lines (make-hash-table :test 'equal))
|
(let ((region-lines (make-hash-table :test 'equal))
|
||||||
(line-number 0)
|
(line-number 0)
|
||||||
lines)
|
lines)
|
||||||
@ -1810,7 +1814,9 @@ separately for every role property."
|
|||||||
(while (< pos line-end)
|
(while (< pos line-end)
|
||||||
(let* ((role-ids (ebox--pos-region-role-ids pos))
|
(let* ((role-ids (ebox--pos-region-role-ids pos))
|
||||||
(ids (delete-dups
|
(ids (delete-dups
|
||||||
(mapcar #'cdr (copy-sequence role-ids))))
|
(append (mapcar #'cdr (copy-sequence role-ids))
|
||||||
|
(and include-owner-stacks-p
|
||||||
|
(copy-sequence (get-text-property pos 'ebox-content-owners))))))
|
||||||
(next (ebox--next-region-property-change
|
(next (ebox--next-region-property-change
|
||||||
pos line-end)))
|
pos line-end)))
|
||||||
(when ids
|
(when ids
|
||||||
@ -3639,15 +3645,18 @@ running expensive span and snapshot checks."
|
|||||||
(not (equal source-node-id node-id))
|
(not (equal source-node-id node-id))
|
||||||
(eq dirty-kind 'geometry)
|
(eq dirty-kind 'geometry)
|
||||||
(memq :content (plist-get entry :changed-keys))
|
(memq :content (plist-get entry :changed-keys))
|
||||||
|
(or (ebox-incremental--content-independent-allocation-p
|
||||||
|
(ebox--buffer-render-state buffer) node-id)
|
||||||
(cl-some
|
(cl-some
|
||||||
(lambda (signature)
|
(lambda (signature)
|
||||||
(let ((content (plist-get signature :content)))
|
(let ((content (plist-get signature :content)))
|
||||||
(and (stringp content) (string-match-p "\n" content))))
|
(and (stringp content) (string-match-p "\n" content))))
|
||||||
(list (plist-get entry :old-signature)
|
(list (plist-get entry :old-signature)
|
||||||
(plist-get entry :new-signature)))
|
(plist-get entry :new-signature))))
|
||||||
;; Canonical Text owns the content identity; its Box owns the
|
;; Canonical Text owns the content identity; its Box owns the
|
||||||
;; allocated slot. Ask the same slot proof about that owner
|
;; allocated slot. The same proof handles both one-line text
|
||||||
;; before promoting a text-length change to the parent layout.
|
;; and bounded multiline content; source identity alone must
|
||||||
|
;; not promote a local text change to the parent layout.
|
||||||
(ebox-incremental--content-slot-stable-p
|
(ebox-incremental--content-slot-stable-p
|
||||||
buffer (plist-put (copy-sequence entry) :node-id node-id)
|
buffer (plist-put (copy-sequence entry) :node-id node-id)
|
||||||
ebox-incremental--candidate-state-for-slot-proof))
|
ebox-incremental--candidate-state-for-slot-proof))
|
||||||
@ -8637,6 +8646,7 @@ the inner host's output."
|
|||||||
(parents (plist-get candidate-state :parent-table))
|
(parents (plist-get candidate-state :parent-table))
|
||||||
(ebox-incremental--buffer-render-state-override
|
(ebox-incremental--buffer-render-state-override
|
||||||
(cons buffer candidate-state))
|
(cons buffer candidate-state))
|
||||||
|
(ebox-incremental--candidate-state-for-slot-proof candidate-state)
|
||||||
affected operations)
|
affected operations)
|
||||||
(dolist (dirty dirty-set)
|
(dolist (dirty dirty-set)
|
||||||
(unless (eq (plist-get dirty :dirty-kind) 'metadata)
|
(unless (eq (plist-get dirty :dirty-kind) 'metadata)
|
||||||
@ -8662,12 +8672,16 @@ the inner host's output."
|
|||||||
:layer-recompose-p t))
|
:layer-recompose-p t))
|
||||||
(ebox-incremental--declarative-tentative-op buffer dirty))
|
(ebox-incremental--declarative-tentative-op buffer dirty))
|
||||||
operations))))
|
operations))))
|
||||||
;; A root portal can cover or anchor to nodes outside its local host.
|
;; A visible root portal can cover or anchor to nodes outside its host.
|
||||||
;; Their updates therefore belong to the root composition as well.
|
;; Test both snapshots so revealing and hiding keep root composition,
|
||||||
|
;; while hidden portals do not widen unrelated local updates.
|
||||||
(when (or affected
|
(when (or affected
|
||||||
(and operations (plist-get old-state :root-portals-p)))
|
(and operations (plist-get old-state :root-portals-p)))
|
||||||
(if (or (plist-get old-state :root-portals-p)
|
(if (or (and (plist-get old-state :root-portals-p)
|
||||||
(ebox-layer-portals-p (plist-get candidate-state :root-node)))
|
(ebox-layer-visible-portals-p
|
||||||
|
(plist-get old-state :root-node)))
|
||||||
|
(ebox-layer-visible-portals-p
|
||||||
|
(plist-get candidate-state :root-node)))
|
||||||
(list (ebox--patch-op
|
(list (ebox--patch-op
|
||||||
'owner-rerender
|
'owner-rerender
|
||||||
(plist-get (plist-get candidate-state :root-node) :node-id)
|
(plist-get (plist-get candidate-state :root-node) :node-id)
|
||||||
@ -8675,24 +8689,64 @@ the inner host's output."
|
|||||||
:layer-recompose-p t))
|
:layer-recompose-p t))
|
||||||
(ebox-patch-plan-merge-ops parents (nreverse operations)))))))
|
(ebox-patch-plan-merge-ops parents (nreverse operations)))))))
|
||||||
|
|
||||||
|
(defun ebox-incremental--content-independent-allocation-p (state owner-id)
|
||||||
|
"Return non-nil when STATE allocates OWNER-ID independently of its content.
|
||||||
|
Intrinsic widths must render naturally so the final footprint check can detect
|
||||||
|
growth. Automatic widths are retained only through normal flow/Column parents;
|
||||||
|
row, Flex and Grid allocation require their own formatting-context proofs."
|
||||||
|
(let ((nodes (plist-get state :node-table))
|
||||||
|
(parents (plist-get state :parent-table))
|
||||||
|
(id owner-id)
|
||||||
|
(valid (and (plist-get state :node-table)
|
||||||
|
(plist-get state :parent-table))))
|
||||||
|
(while (and valid id)
|
||||||
|
(let* ((node (ebox-runtime-index-get id nodes))
|
||||||
|
(parent-id (ebox-runtime-index-get id parents))
|
||||||
|
(parent (and parent-id (ebox-runtime-index-get parent-id nodes)))
|
||||||
|
(width (ebox-get node :width)))
|
||||||
|
(setq valid
|
||||||
|
(and node
|
||||||
|
(cl-every
|
||||||
|
(lambda (value)
|
||||||
|
(or (memq value '(nil auto stretch none))
|
||||||
|
(numberp value) (ebox-size-value-p value)))
|
||||||
|
(list width (ebox-get node :min-width)
|
||||||
|
(ebox-get node :max-width)))
|
||||||
|
(or (not (memq width '(nil auto stretch)))
|
||||||
|
(and parent
|
||||||
|
(eq (ebox-tree-display-outer node) 'block)
|
||||||
|
(memq (ebox-tree-display-inner parent)
|
||||||
|
'(flow column)))))
|
||||||
|
id parent-id)))
|
||||||
|
valid))
|
||||||
|
|
||||||
(defun ebox-incremental--layer-span-proof
|
(defun ebox-incremental--layer-span-proof
|
||||||
(buffer old-state prepared candidate-state owner-plan path-copied-p)
|
(buffer old-state prepared candidate-state owner-plan path-copied-p)
|
||||||
"Prove the stable outer allocation of one recomposed layer owner.
|
"Prove the stable outer allocation of one recomposed layer owner.
|
||||||
Compare BUFFER's OLD-STATE with CANDIDATE-STATE using PREPARED dirty facts
|
Compare BUFFER's OLD-STATE with CANDIDATE-STATE using PREPARED dirty facts
|
||||||
and OWNER-PLAN. PATH-COPIED-P requires private nodes inside that owner.
|
and OWNER-PLAN. PATH-COPIED-P requires private nodes inside that owner.
|
||||||
Internal visibility and role topology may change. The surface validates the
|
Internal visibility and role topology may change. The surface validates the
|
||||||
complete replacement footprint before publishing any resulting spans."
|
complete replacement footprint before publishing any resulting spans. Freeze
|
||||||
|
the retained margin-box width only when the owner's layout style is unchanged."
|
||||||
(when (and (= (length owner-plan) 1)
|
(when (and (= (length owner-plan) 1)
|
||||||
(plist-get (car owner-plan) :layer-recompose-p))
|
(plist-get (car owner-plan) :layer-recompose-p))
|
||||||
(let* ((owner-id (plist-get (car owner-plan) :owner-id))
|
(let* ((owner-id (plist-get (car owner-plan) :owner-id))
|
||||||
|
(old-node (ebox-runtime-index-get
|
||||||
|
owner-id (plist-get old-state :node-table)))
|
||||||
(node (ebox-runtime-index-get
|
(node (ebox-runtime-index-get
|
||||||
owner-id (plist-get candidate-state :node-table)))
|
owner-id (plist-get candidate-state :node-table)))
|
||||||
(snapshot (ebox-incremental--cached-layout-snapshot-details
|
(snapshot (ebox-incremental--cached-layout-snapshot-details
|
||||||
buffer owner-id))
|
buffer owner-id))
|
||||||
(spans (plist-get snapshot :buffer-spans))
|
(spans (plist-get snapshot :buffer-spans))
|
||||||
|
(allocated-width
|
||||||
|
(plist-get (plist-get snapshot :external-footprint-signature)
|
||||||
|
:max-line-pixel-width))
|
||||||
(ancestors (ebox-incremental--ancestor-slot-signature
|
(ancestors (ebox-incremental--ancestor-slot-signature
|
||||||
buffer old-state candidate-state owner-id)))
|
buffer old-state candidate-state owner-id)))
|
||||||
(when (and node snapshot spans ancestors
|
(when (and old-node node snapshot spans ancestors
|
||||||
|
(numberp allocated-width)
|
||||||
|
(equal (ebox-incremental--layout-slot-style-signature old-node)
|
||||||
|
(ebox-incremental--layout-slot-style-signature node))
|
||||||
(ebox-incremental--same-root-surface-identity-p
|
(ebox-incremental--same-root-surface-identity-p
|
||||||
old-state candidate-state)
|
old-state candidate-state)
|
||||||
(ebox-incremental--span-patch-identity-stable-p
|
(ebox-incremental--span-patch-identity-stable-p
|
||||||
@ -8706,6 +8760,10 @@ complete replacement footprint before publishing any resulting spans."
|
|||||||
(or (ebox-buffer--partial-line-slots-p spans)
|
(or (ebox-buffer--partial-line-slots-p spans)
|
||||||
(ebox--spans-contiguous-lines-p spans))))
|
(ebox--spans-contiguous-lines-p spans))))
|
||||||
(list :owner-id owner-id :snapshot snapshot
|
(list :owner-id owner-id :snapshot snapshot
|
||||||
|
:allocated-width
|
||||||
|
(and (ebox-incremental--content-independent-allocation-p
|
||||||
|
candidate-state owner-id)
|
||||||
|
allocated-width)
|
||||||
:dirty-set (plist-get prepared :dirty-set)
|
:dirty-set (plist-get prepared :dirty-set)
|
||||||
:layer-recompose-p t :ancestor-slot-signature ancestors
|
:layer-recompose-p t :ancestor-slot-signature ancestors
|
||||||
:layout-snapshot-detail-generation
|
:layout-snapshot-detail-generation
|
||||||
@ -9667,6 +9725,11 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
(and (memq :content changed-keys)
|
(and (memq :content changed-keys)
|
||||||
(ebox-incremental--content-slot-stable-p
|
(ebox-incremental--content-slot-stable-p
|
||||||
buffer slot-dirty candidate-state)))
|
buffer slot-dirty candidate-state)))
|
||||||
|
(allocated-content-box-p
|
||||||
|
(and content-slot-evidence
|
||||||
|
(eq (plist-get old-node :ebox-kind) 'box)
|
||||||
|
(ebox-incremental--content-independent-allocation-p
|
||||||
|
candidate-state owner-id)))
|
||||||
(local-typography-p
|
(local-typography-p
|
||||||
(and (not (memq :content changed-keys))
|
(and (not (memq :content changed-keys))
|
||||||
(cl-some
|
(cl-some
|
||||||
@ -9711,7 +9774,8 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
buffer old-state candidate-state owner-id))))
|
buffer old-state candidate-state owner-id))))
|
||||||
(allocated-width
|
(allocated-width
|
||||||
(let* ((owner-slot
|
(let* ((owner-slot
|
||||||
(if content-slot-evidence
|
(if (and content-slot-evidence
|
||||||
|
(eq (plist-get old-node :ebox-kind) 'text))
|
||||||
(or (cadr content-slot-evidence)
|
(or (cadr content-slot-evidence)
|
||||||
(car content-slot-evidence))
|
(car content-slot-evidence))
|
||||||
(car ancestor-slot-signature)))
|
(car ancestor-slot-signature)))
|
||||||
@ -9765,10 +9829,13 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
old-state candidate-state (list dirty))))
|
old-state candidate-state (list dirty))))
|
||||||
(ebox-incremental--owner-disjoint-from-scroll-p
|
(ebox-incremental--owner-disjoint-from-scroll-p
|
||||||
buffer old-state prepared candidate-state owner-id)
|
buffer old-state prepared candidate-state owner-id)
|
||||||
;; Admit the new scroll coexistence case only for an exact
|
;; An allocated Box owns its changing line filler. Other
|
||||||
;; content extent. Length or width changes can alter an
|
;; owners beside retained scroll content still need an exact
|
||||||
;; ancestor's line filler outside the local owner's spans.
|
;; raw extent, because ancestor filler can lie outside their
|
||||||
|
;; local spans. Scroll dependency/binding proof above and
|
||||||
|
;; final slot validation remain mandatory in both cases.
|
||||||
(or (null (plist-get old-state :scroll-region-ids))
|
(or (null (plist-get old-state :scroll-region-ids))
|
||||||
|
allocated-content-box-p
|
||||||
(let ((before (plist-get (plist-get dirty :old-signature)
|
(let ((before (plist-get (plist-get dirty :old-signature)
|
||||||
:content))
|
:content))
|
||||||
(after (plist-get (plist-get dirty :new-signature)
|
(after (plist-get (plist-get dirty :new-signature)
|
||||||
@ -9835,10 +9902,10 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
:ancestor-slot-signature ancestor-slot-signature
|
:ancestor-slot-signature ancestor-slot-signature
|
||||||
:variable-content-p
|
:variable-content-p
|
||||||
(and snapshot
|
(and snapshot
|
||||||
;; With retained scroll siblings, the final rendered
|
;; Only a content-independent Box owns all filler affected
|
||||||
;; footprint must remain exact even when raw content has
|
;; by its changing text beside retained scroll siblings.
|
||||||
;; equal width under the current buffer's default font.
|
(or (null (plist-get old-state :scroll-region-ids))
|
||||||
(null (plist-get old-state :scroll-region-ids))
|
allocated-content-box-p)
|
||||||
(memq :content changed-keys)
|
(memq :content changed-keys)
|
||||||
(stringp
|
(stringp
|
||||||
(plist-get (plist-get dirty :new-signature) :content))
|
(plist-get (plist-get dirty :new-signature) :content))
|
||||||
@ -10493,7 +10560,7 @@ role, and outside-complement compatibility before publication."
|
|||||||
(and (eq projection-kind 'owner-scoped)
|
(and (eq projection-kind 'owner-scoped)
|
||||||
(plist-get span-proof :allocated-width))
|
(plist-get span-proof :allocated-width))
|
||||||
:owner-scoped-proofs
|
:owner-scoped-proofs
|
||||||
(or (and (memq projection-kind '(span-patch owner-scoped))
|
(or (and (memq projection-kind '(span-patch owner-scoped scroll-patch))
|
||||||
(ebox-incremental--owner-proof-list span-proof))
|
(ebox-incremental--owner-proof-list span-proof))
|
||||||
(and (eq projection-kind 'mixed-owner-reflow)
|
(and (eq projection-kind 'mixed-owner-reflow)
|
||||||
(eq (plist-get (plist-get prepared
|
(eq (plist-get (plist-get prepared
|
||||||
@ -10783,20 +10850,23 @@ role, and outside-complement compatibility before publication."
|
|||||||
base)
|
base)
|
||||||
|
|
||||||
(defun ebox-incremental--scroll-content-span-proof
|
(defun ebox-incremental--scroll-content-span-proof
|
||||||
(buffer old-state prepared candidate-state)
|
(buffer old-state prepared candidate-state &optional scroll-owner-id)
|
||||||
"Prove one content owner in a retained root scroll's content coordinates.
|
"Prove one content owner in a retained root scroll's content coordinates.
|
||||||
The visible buffer may contain only part of the scroll content. Its cached
|
The visible buffer may contain only part of the scroll content. Its cached
|
||||||
lines are a separate coordinate space, so run the ordinary owner and footprint
|
lines are a separate coordinate space, so run the ordinary owner and footprint
|
||||||
proofs there rather than treating invisible owners as zero-sized. Nested
|
proofs there rather than treating invisible owners as zero-sized. Nested
|
||||||
scrolls, non-content changes and uncached/partial owners retain their ordinary
|
scrolls, non-content changes and uncached/partial owners retain their ordinary
|
||||||
fallback. The surface must still validate the complete cached replacement and
|
fallback. The surface must still validate the complete cached replacement and
|
||||||
the visible replacement independently before publishing either one."
|
the visible replacement independently before publishing either one.
|
||||||
|
SCROLL-OWNER-ID instead proves one nested scroll viewport in that root cache;
|
||||||
|
other scroll owners must be disjoint from the target."
|
||||||
(let* ((regions (plist-get old-state :scroll-region-ids))
|
(let* ((regions (plist-get old-state :scroll-region-ids))
|
||||||
(region (and (= (length regions) 1) (car regions)))
|
(root (plist-get old-state :root-node))
|
||||||
(scroll (and region (gethash region ebox--scroll-global-state)))
|
(region (if scroll-owner-id (plist-get root :region-id)
|
||||||
|
(and (= (length regions) 1) (car regions))))
|
||||||
|
(scroll (and region (gethash region (plist-get old-state :scroll-state-table))))
|
||||||
(next-scroll (and region (gethash region
|
(next-scroll (and region (gethash region
|
||||||
(plist-get prepared :scroll-state-table))))
|
(plist-get prepared :scroll-state-table))))
|
||||||
(root (plist-get old-state :root-node))
|
|
||||||
(dirty-set (plist-get prepared :dirty-set))
|
(dirty-set (plist-get prepared :dirty-set))
|
||||||
(lines (plist-get scroll :content-lines)))
|
(lines (plist-get scroll :content-lines)))
|
||||||
(when (and region scroll next-scroll lines
|
(when (and region scroll next-scroll lines
|
||||||
@ -10804,9 +10874,24 @@ the visible replacement independently before publishing either one."
|
|||||||
(eq (plist-get scroll :box) root)
|
(eq (plist-get scroll :box) root)
|
||||||
(eq (plist-get next-scroll :box)
|
(eq (plist-get next-scroll :box)
|
||||||
(plist-get candidate-state :root-node))
|
(plist-get candidate-state :root-node))
|
||||||
(= (hash-table-count (plist-get prepared :scroll-state-table)) 1)
|
(if scroll-owner-id
|
||||||
|
(and (not (equal scroll-owner-id (plist-get root :node-id)))
|
||||||
|
(cl-every
|
||||||
|
(lambda (id)
|
||||||
|
(let ((owner (ebox--buffer-region-render-owner-node-id buffer id)))
|
||||||
|
(or (equal id region) (equal owner scroll-owner-id)
|
||||||
|
(and owner
|
||||||
|
(not (ebox--runtime-ancestor-id-p buffer owner scroll-owner-id))
|
||||||
|
(not (ebox--runtime-ancestor-id-p buffer scroll-owner-id owner))))))
|
||||||
|
regions))
|
||||||
|
(= (hash-table-count (plist-get prepared :scroll-state-table)) 1))
|
||||||
(= (length dirty-set) 1)
|
(= (length dirty-set) 1)
|
||||||
(equal (plist-get (car dirty-set) :changed-keys) '(:content))
|
(if scroll-owner-id
|
||||||
|
(and (equal scroll-owner-id (plist-get (car dirty-set) :node-id))
|
||||||
|
(cl-every (lambda (key)
|
||||||
|
(memq key '(:scroll-offset :ebox-scroll-offset-controlled-p)))
|
||||||
|
(plist-get (car dirty-set) :changed-keys)))
|
||||||
|
(equal (plist-get (car dirty-set) :changed-keys) '(:content)))
|
||||||
(not (plist-get (car dirty-set) :children))
|
(not (plist-get (car dirty-set) :children))
|
||||||
(not (plist-get scroll :lazy-scroll-prefix-dirty))
|
(not (plist-get scroll :lazy-scroll-prefix-dirty))
|
||||||
(equal (plist-get scroll :scroll-offset)
|
(equal (plist-get scroll :scroll-offset)
|
||||||
@ -10829,12 +10914,24 @@ the visible replacement independently before publishing either one."
|
|||||||
(ebox-incremental--candidate-base-state
|
(ebox-incremental--candidate-base-state
|
||||||
(cons (current-buffer) base))
|
(cons (current-buffer) base))
|
||||||
(ebox-incremental--candidate-state-for-slot-proof candidate)
|
(ebox-incremental--candidate-state-for-slot-proof candidate)
|
||||||
(ebox--region-line-index (ebox--build-region-line-index))
|
(ebox--region-line-index (ebox--build-region-line-index scroll-owner-id))
|
||||||
(ebox--region-line-index-buffer (current-buffer))
|
(ebox--region-line-index-buffer (current-buffer))
|
||||||
(plan (ebox-incremental--layout-owner-plan
|
(plan (if scroll-owner-id
|
||||||
(current-buffer) base candidate dirty-set))
|
(list (ebox--patch-op 'span-patch scroll-owner-id :dirty dirty-set))
|
||||||
(proof (ebox-incremental--span-patch-projection-proof
|
(ebox-incremental--layout-owner-plan
|
||||||
(current-buffer) base input candidate plan)))
|
(current-buffer) base candidate dirty-set)))
|
||||||
|
(proof
|
||||||
|
(if scroll-owner-id
|
||||||
|
(when-let* ((snapshot (ebox--ensure-layout-snapshot-details
|
||||||
|
(current-buffer) scroll-owner-id))
|
||||||
|
((plist-get snapshot :buffer-spans))
|
||||||
|
(width (plist-get (plist-get snapshot :external-footprint-signature)
|
||||||
|
:max-line-pixel-width)))
|
||||||
|
(list :owner-id scroll-owner-id :snapshot snapshot
|
||||||
|
:allocated-width width :nested-scroll-patch-p t
|
||||||
|
:scroll-content-coordinate-p t))
|
||||||
|
(ebox-incremental--span-patch-projection-proof
|
||||||
|
(current-buffer) base input candidate plan))))
|
||||||
(when proof
|
(when proof
|
||||||
(plist-put proof :scroll-content-proof
|
(plist-put proof :scroll-content-proof
|
||||||
(list :region-id region :lines lines :owner-plan plan
|
(list :region-id region :lines lines :owner-plan plan
|
||||||
@ -10842,6 +10939,32 @@ the visible replacement independently before publishing either one."
|
|||||||
:line-index ebox--region-line-index))
|
:line-index ebox--region-line-index))
|
||||||
proof)))))))
|
proof)))))))
|
||||||
|
|
||||||
|
(defun ebox-incremental--layer-root-scroll-p
|
||||||
|
(old-state candidate-state prepared owner-id)
|
||||||
|
"Prove OWNER-ID only scrolls a retained composed root between both states.
|
||||||
|
OLD-STATE and CANDIDATE-STATE must share the complete cached content; PREPARED
|
||||||
|
must contain no source, geometry, paint, or display change besides its offset."
|
||||||
|
(let* ((old-root (plist-get old-state :root-node))
|
||||||
|
(root (plist-get candidate-state :root-node))
|
||||||
|
(region-id (plist-get root :region-id))
|
||||||
|
(old-scroll (gethash region-id (plist-get old-state :scroll-state-table)))
|
||||||
|
(scroll (gethash region-id (plist-get candidate-state :scroll-state-table))))
|
||||||
|
(and owner-id
|
||||||
|
(equal owner-id (plist-get old-root :node-id))
|
||||||
|
(equal owner-id (plist-get root :node-id))
|
||||||
|
(equal (plist-get old-state :display-signature)
|
||||||
|
(plist-get candidate-state :display-signature))
|
||||||
|
(cl-every (lambda (entry)
|
||||||
|
(and (equal owner-id (plist-get entry :node-id))
|
||||||
|
(cl-every (lambda (key)
|
||||||
|
(memq key '(:scroll-offset
|
||||||
|
:ebox-scroll-offset-controlled-p)))
|
||||||
|
(plist-get entry :changed-keys))))
|
||||||
|
(plist-get prepared :dirty-set))
|
||||||
|
(eq (plist-get old-scroll :content-lines)
|
||||||
|
(plist-get scroll :content-lines))
|
||||||
|
(ebox-layer-cached-root-scroll-p root scroll))))
|
||||||
|
|
||||||
(defun ebox-incremental--prepare-scoped-candidate
|
(defun ebox-incremental--prepare-scoped-candidate
|
||||||
(buffer old-state next-root isolated-candidate-p
|
(buffer old-state next-root isolated-candidate-p
|
||||||
&optional skip-span-proof-p scroll-transaction-p scroll-owner-id)
|
&optional skip-span-proof-p scroll-transaction-p scroll-owner-id)
|
||||||
@ -10909,13 +11032,17 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
(plist-get candidate-state :source-index)))
|
(plist-get candidate-state :source-index)))
|
||||||
(plist-put candidate-state :selector-index-stale-p t)))
|
(plist-put candidate-state :selector-index-stale-p t)))
|
||||||
(layer-owner-plan
|
(layer-owner-plan
|
||||||
|
(unless (and scroll-transaction-p
|
||||||
|
(plist-get old-state :layered-p)
|
||||||
|
(ebox-incremental--layer-root-scroll-p
|
||||||
|
old-state candidate-state prepared scroll-owner-id))
|
||||||
(ebox-incremental--layer-owner-plan
|
(ebox-incremental--layer-owner-plan
|
||||||
buffer old-state candidate-state
|
buffer old-state candidate-state
|
||||||
(or (plist-get prepared :dirty-set)
|
(or (plist-get prepared :dirty-set)
|
||||||
(and scroll-transaction-p scroll-owner-id
|
(and scroll-transaction-p scroll-owner-id
|
||||||
(list (list :node-id scroll-owner-id
|
(list (list :node-id scroll-owner-id
|
||||||
:dirty-kind 'geometry
|
:dirty-kind 'geometry
|
||||||
:changed-keys '(:scroll-offset)))))))
|
:changed-keys '(:scroll-offset))))))))
|
||||||
(formatting-proof
|
(formatting-proof
|
||||||
(and (not layer-owner-plan) (not scroll-transaction-p)
|
(and (not layer-owner-plan) (not scroll-transaction-p)
|
||||||
(not (cl-some
|
(not (cl-some
|
||||||
@ -10952,13 +11079,15 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
(ebox-incremental--paint-projection-p
|
(ebox-incremental--paint-projection-p
|
||||||
buffer old-state prepared candidate-state owner-plan))
|
buffer old-state prepared candidate-state owner-plan))
|
||||||
(span-proof
|
(span-proof
|
||||||
(unless (or paint-p mixed-proof
|
(if (and scroll-transaction-p skip-span-proof-p (not layer-owner-plan))
|
||||||
(and skip-span-proof-p (not layer-owner-plan)))
|
(ebox-incremental--scroll-content-span-proof
|
||||||
|
buffer old-state prepared candidate-state scroll-owner-id)
|
||||||
|
(unless (or paint-p mixed-proof)
|
||||||
(or (ebox-incremental--span-patch-projection-proof
|
(or (ebox-incremental--span-patch-projection-proof
|
||||||
buffer old-state prepared candidate-state owner-plan
|
buffer old-state prepared candidate-state owner-plan
|
||||||
path-copied-p)
|
path-copied-p)
|
||||||
(ebox-incremental--scroll-content-span-proof
|
(ebox-incremental--scroll-content-span-proof
|
||||||
buffer old-state prepared candidate-state))))
|
buffer old-state prepared candidate-state)))))
|
||||||
(projection-kind
|
(projection-kind
|
||||||
(cond
|
(cond
|
||||||
(mixed-proof 'mixed-owner-reflow)
|
(mixed-proof 'mixed-owner-reflow)
|
||||||
|
|||||||
@ -60,9 +60,11 @@
|
|||||||
(defun ebox-keymap-create (&rest options)
|
(defun ebox-keymap-create (&rest options)
|
||||||
"Create an ordinary native keymap from OPTIONS.
|
"Create an ordinary native keymap from OPTIONS.
|
||||||
`:activate' is an optional zero-argument callback for RET, return, SPC and
|
`:activate' is an optional zero-argument callback for RET, return, SPC and
|
||||||
the primary mouse button. Nil omits these activation bindings. `:bindings'
|
the primary mouse button. Repeated clicks each activate once; primary-button
|
||||||
is an alist of string key descriptions or key vectors paired with
|
press and drag events do nothing, so they do not start native text selection.
|
||||||
zero-argument callbacks.
|
Nil omits these activation bindings. `:bindings' is an alist of string key
|
||||||
|
descriptions or key vectors paired with zero-argument callbacks. Explicit
|
||||||
|
bindings override the repeated-click, press and drag defaults.
|
||||||
Callbacks are not invoked until a generated command runs. Keyboard commands
|
Callbacks are not invoked until a generated command runs. Keyboard commands
|
||||||
use the current buffer; mouse commands use the event window's buffer without
|
use the current buffer; mouse commands use the event window's buffer without
|
||||||
selecting a window or moving point. Duplicate or overlapping keys are errors.
|
selecting a window or moving point. Duplicate or overlapping keys are errors.
|
||||||
@ -104,7 +106,20 @@ This function does not supply focus navigation, state or event propagation."
|
|||||||
(error "Ebox keymap binding overlaps an existing key: %s"
|
(error "Ebox keymap binding overlaps an existing key: %s"
|
||||||
(key-description key)))
|
(key-description key)))
|
||||||
(define-key map key (ebox-interaction--command callback))
|
(define-key map key (ebox-interaction--command callback))
|
||||||
(push key keys))))
|
(push key keys)))
|
||||||
|
(when activate
|
||||||
|
;; A release-only map lets the preceding press enter mouse-drag-region;
|
||||||
|
;; double/triple clicks can then select words or whole lines. Claim
|
||||||
|
;; the primary-button sequence locally, preserving explicit handlers.
|
||||||
|
(dolist (event '(double-mouse-1 triple-mouse-1
|
||||||
|
down-mouse-1 double-down-mouse-1
|
||||||
|
triple-down-mouse-1 drag-mouse-1
|
||||||
|
double-drag-mouse-1 triple-drag-mouse-1))
|
||||||
|
(unless (lookup-key map (vector event))
|
||||||
|
(define-key map (vector event)
|
||||||
|
(if (memq event '(double-mouse-1 triple-mouse-1))
|
||||||
|
(lookup-key map [mouse-1])
|
||||||
|
#'ignore))))))
|
||||||
map))
|
map))
|
||||||
|
|
||||||
(defun ebox-interaction--navigable-keymap-p (map)
|
(defun ebox-interaction--navigable-keymap-p (map)
|
||||||
|
|||||||
@ -35,6 +35,10 @@
|
|||||||
(defvar ebox-layer--portals nil
|
(defvar ebox-layer--portals nil
|
||||||
"Candidate-local absolute nodes awaiting root composition.")
|
"Candidate-local absolute nodes awaiting root composition.")
|
||||||
|
|
||||||
|
(defvar ebox-layer--piece-width-cache nil
|
||||||
|
"Original row prefix widths scoped to one host composition.
|
||||||
|
Keys are source strings by identity. Never retained between renders.")
|
||||||
|
|
||||||
(defvar ebox-layer--active-p 'unknown
|
(defvar ebox-layer--active-p 'unknown
|
||||||
"Whether the current candidate contains layered layout.
|
"Whether the current candidate contains layered layout.
|
||||||
Retained publication supplies a known boolean; isolated rendering resolves
|
Retained publication supplies a known boolean; isolated rendering resolves
|
||||||
@ -72,6 +76,18 @@ the fact once for its spatial root.")
|
|||||||
(or (eq (plist-get node :layer) 'root)
|
(or (eq (plist-get node :layer) 'root)
|
||||||
(cl-some #'ebox-layer-portals-p (ebox-tree-node-children node)))))
|
(cl-some #'ebox-layer-portals-p (ebox-tree-node-children node)))))
|
||||||
|
|
||||||
|
(defun ebox-layer-visible-portals-p (node)
|
||||||
|
"Return non-nil when NODE can paint an absolute root projection.
|
||||||
|
Hidden owners suppress their complete logical subtree. Keep this visible
|
||||||
|
dependency separate from structural portal detection: hidden portals remain
|
||||||
|
in the retained input and can become visible in a later candidate."
|
||||||
|
(and (listp node)
|
||||||
|
(not (eq (plist-get node :visibility) 'hidden))
|
||||||
|
(or (and (eq (plist-get node :position) 'absolute)
|
||||||
|
(eq (plist-get node :layer) 'root))
|
||||||
|
(cl-some #'ebox-layer-visible-portals-p
|
||||||
|
(ebox-tree-node-children node)))))
|
||||||
|
|
||||||
(defun ebox-layer--owned-p (line position region)
|
(defun ebox-layer--owned-p (line position region)
|
||||||
"Whether LINE at POSITION visibly belongs to REGION, excluding margins."
|
"Whether LINE at POSITION visibly belongs to REGION, excluding margins."
|
||||||
(and (not (cl-some (lambda (role)
|
(and (not (cl-some (lambda (role)
|
||||||
@ -83,16 +99,46 @@ the fact once for its spatial root.")
|
|||||||
(equal region (get-text-property position (cdr role) line)))
|
(equal region (get-text-property position (cdr role) line)))
|
||||||
ebox-region-types))))
|
ebox-region-types))))
|
||||||
|
|
||||||
|
(defun ebox-layer-cached-root-scroll-p (root state)
|
||||||
|
"Return non-nil when ROOT can slice STATE's complete composed content.
|
||||||
|
This proves only the cached content geometry. Callers must separately prove
|
||||||
|
a pure root scroll transaction with unchanged source and display context.
|
||||||
|
Root projections and anchored provider chains are already composed in this
|
||||||
|
content coordinate system, before the root wrapper applies its scroll offset.
|
||||||
|
Nested scroll changes and source updates cannot use this root-only proof."
|
||||||
|
(and root state
|
||||||
|
(equal (plist-get root :node-id)
|
||||||
|
(plist-get (plist-get state :box) :node-id))
|
||||||
|
(eq (ebox-get root :overflow) 'scroll)
|
||||||
|
(plist-get state :content-lines)
|
||||||
|
(or (plist-get state :content-lines-complete-p)
|
||||||
|
(and (not (plist-get state :render-content-prefix))
|
||||||
|
(not (plist-get state :materialize-content-lines))))
|
||||||
|
(not (plist-get state :lazy-scroll-prefix-dirty))
|
||||||
|
(not (plist-get state :lazy-scroll-window-refresh-required))))
|
||||||
|
|
||||||
|
(defun ebox-layer--prefix-width (line end)
|
||||||
|
"Return LINE's original prefix width at END within this composition."
|
||||||
|
(if (zerop end) 0
|
||||||
|
(let ((widths (or (gethash line ebox-layer--piece-width-cache)
|
||||||
|
(puthash line (make-hash-table :test #'eql)
|
||||||
|
ebox-layer--piece-width-cache))))
|
||||||
|
(or (gethash end widths)
|
||||||
|
(puthash end (ebox--substring-pixel-width line 0 end) widths)))))
|
||||||
|
|
||||||
(defun ebox-layer--pieces (lines node)
|
(defun ebox-layer--pieces (lines node)
|
||||||
"Return NODE's visible source intervals in LINES, before positioning."
|
"Return NODE's visible source intervals in LINES, before positioning."
|
||||||
(let ((region (ebox--ensure-region-id node)) pieces)
|
(let ((region (ebox--ensure-region-id node))
|
||||||
|
(ebox-layer--piece-width-cache
|
||||||
|
(or ebox-layer--piece-width-cache (make-hash-table :test #'eq)))
|
||||||
|
pieces)
|
||||||
(cl-loop for line in lines for row from 0 do
|
(cl-loop for line in lines for row from 0 do
|
||||||
(let ((start 0) (limit (length line)))
|
(let ((start 0) (limit (length line)))
|
||||||
(while (< start limit)
|
(while (< start limit)
|
||||||
(let ((end (next-property-change start line limit)))
|
(let ((end (next-property-change start line limit)))
|
||||||
(when (ebox-layer--owned-p line start region)
|
(when (ebox-layer--owned-p line start region)
|
||||||
(let ((x (ebox--substring-pixel-width line 0 start))
|
(let ((x (ebox-layer--prefix-width line start))
|
||||||
(right (ebox--substring-pixel-width line 0 end)))
|
(right (ebox-layer--prefix-width line end)))
|
||||||
(when (> right x)
|
(when (> right x)
|
||||||
;; Keep the full row as source so shaping and shared
|
;; Keep the full row as source so shaping and shared
|
||||||
;; display objects are measured in their own context.
|
;; display objects are measured in their own context.
|
||||||
@ -310,7 +356,8 @@ Only candidate-local lists of visible paint records are sorted."
|
|||||||
(eq (plist-get child :position) 'absolute)) children))
|
(eq (plist-get child :position) 'absolute)) children))
|
||||||
(base (funcall renderer flow)))
|
(base (funcall renderer flow)))
|
||||||
(if ebox--intrinsic-layout-measurement base
|
(if ebox--intrinsic-layout-measurement base
|
||||||
(let* ((lines (ebox-string-lines base))
|
(let* ((ebox-layer--piece-width-cache (make-hash-table :test #'eq))
|
||||||
|
(lines (ebox-string-lines base))
|
||||||
(width (or (ebox--wrapper-content-viewport-pixel host)
|
(width (or (ebox--wrapper-content-viewport-pixel host)
|
||||||
(if lines (apply #'max (mapcar #'ebox--string-pixel-width lines)) 0)))
|
(if lines (apply #'max (mapcar #'ebox--string-pixel-width lines)) 0)))
|
||||||
(flow-height (length lines))
|
(flow-height (length lines))
|
||||||
@ -335,7 +382,8 @@ Only candidate-local lists of visible paint records are sorted."
|
|||||||
(plist-get piece :end))) records)))))
|
(plist-get piece :end))) records)))))
|
||||||
(unless (eq host ebox-layer--render-root)
|
(unless (eq host ebox-layer--render-root)
|
||||||
(dolist (child absolute)
|
(dolist (child absolute)
|
||||||
(when (eq (plist-get child :layer) 'root)
|
(when (and (eq (plist-get child :layer) 'root)
|
||||||
|
(not (eq (plist-get child :visibility) 'hidden)))
|
||||||
(cl-pushnew child ebox-layer--portals :test #'eq)))
|
(cl-pushnew child ebox-layer--portals :test #'eq)))
|
||||||
(setq absolute (cl-remove-if (lambda (child)
|
(setq absolute (cl-remove-if (lambda (child)
|
||||||
(eq (plist-get child :layer) 'root)) absolute)))
|
(eq (plist-get child :layer) 'root)) absolute)))
|
||||||
|
|||||||
@ -1369,7 +1369,8 @@ Return its empty inline extent through `ebox--box-content-empty-width'."
|
|||||||
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind))))))
|
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind))))))
|
||||||
(if (or (ebox-layer-host-p box)
|
(if (or (ebox-layer-host-p box)
|
||||||
(and (eq box ebox-layer--render-root)
|
(and (eq box ebox-layer--render-root)
|
||||||
(or ebox-layer--portals (ebox-layer-portals-p box))))
|
(or ebox-layer--portals
|
||||||
|
(ebox-layer-visible-portals-p box))))
|
||||||
(ebox-layer-render-children box children render-flow)
|
(ebox-layer-render-children box children render-flow)
|
||||||
(funcall render-flow children)))))))
|
(funcall render-flow children)))))))
|
||||||
(plist-put box :ebox-content-layout-complete-p
|
(plist-put box :ebox-content-layout-complete-p
|
||||||
@ -1731,7 +1732,12 @@ one carrier line preserves their paint without inventing a content line."
|
|||||||
|
|
||||||
(defun ebox--format-content (box)
|
(defun ebox--format-content (box)
|
||||||
"Format BOX content (wrap and justify) returning a string."
|
"Format BOX content (wrap and justify) returning a string."
|
||||||
(or (and (not (ebox-layer-active-p box))
|
(or (and (or (not (ebox-layer-active-p box))
|
||||||
|
;; Only the staged root viewport may replay composed layers.
|
||||||
|
;; Fresh lazy producers and nested owners still compose fully.
|
||||||
|
(and (eq box ebox-layer--render-root)
|
||||||
|
(ebox-layer-cached-root-scroll-p
|
||||||
|
box ebox--scroll-window-cached-state)))
|
||||||
(ebox--format-scroll-window-content box))
|
(ebox--format-scroll-window-content box))
|
||||||
(ebox--format-content-string box (ebox--box-content box))))
|
(ebox--format-content-string box (ebox--box-content box))))
|
||||||
|
|
||||||
@ -2134,7 +2140,12 @@ This function owns spatial layout only; it never creates a live TP surface."
|
|||||||
(cond
|
(cond
|
||||||
((eq (plist-get node :ebox-kind) 'text)
|
((eq (plist-get node :ebox-kind) 'text)
|
||||||
(ebox--render-text-node node))
|
(ebox--render-text-node node))
|
||||||
((eq type 'box) (ebox--render-box node))
|
((eq type 'box)
|
||||||
|
(if (eq (ebox-get node :visibility) 'hidden)
|
||||||
|
;; Keep hidden geometry, but do not let descendant portals
|
||||||
|
;; escape this visibility boundary into an outer composition.
|
||||||
|
(let ((ebox-layer--portals nil)) (ebox--render-box node))
|
||||||
|
(ebox--render-box node)))
|
||||||
((eq type 'concat) (ebox--render-concat node))
|
((eq type 'concat) (ebox--render-concat node))
|
||||||
((eq type 'stack) (ebox--render-stack node))
|
((eq type 'stack) (ebox--render-stack node))
|
||||||
((eq type 'flex) (ebox--render-flex node))
|
((eq type 'flex) (ebox--render-flex node))
|
||||||
|
|||||||
@ -253,7 +253,8 @@
|
|||||||
ebox--render-string-pixel-width-cache
|
ebox--render-string-pixel-width-cache
|
||||||
ebox--rendered-uniform-width-table ebox--rendered-intrinsic-size-table
|
ebox--rendered-uniform-width-table ebox--rendered-intrinsic-size-table
|
||||||
ebox--runtime-ancestor-cache
|
ebox--runtime-ancestor-cache
|
||||||
ebox--scroll-window-cached-state ebox--space-pixel-cache
|
ebox--scroll-window-cached-state :rendered-content-line-cells
|
||||||
|
ebox--space-pixel-cache
|
||||||
ebox--string-pixel-width-cache
|
ebox--string-pixel-width-cache
|
||||||
ebox--string-pixel-width-cache-ring
|
ebox--string-pixel-width-cache-ring
|
||||||
ebox--viewport-dependent-node-ids-cache
|
ebox--viewport-dependent-node-ids-cache
|
||||||
@ -304,6 +305,9 @@ rather than relying on an implicit default.")
|
|||||||
(:symbol ebox-layer--portals
|
(:symbol ebox-layer--portals
|
||||||
:reason dynamically-bound-render-collection
|
:reason dynamically-bound-render-collection
|
||||||
:evidence let-bound-per-spatial-root-and-discarded-with-the-candidate-render)
|
:evidence let-bound-per-spatial-root-and-discarded-with-the-candidate-render)
|
||||||
|
(:symbol ebox-layer--piece-width-cache
|
||||||
|
:reason dynamically-bound-composition-index
|
||||||
|
:evidence let-bound-per-layer-host-and-never-retained-between-renders)
|
||||||
(:symbol ebox--string-pixel-width-cache-ring-index
|
(:symbol ebox--string-pixel-width-cache-ring-index
|
||||||
:reason numeric-eviction-cursor
|
:reason numeric-eviction-cursor
|
||||||
:evidence value-is-an-integer-ring-position)
|
:evidence value-is-an-integer-ring-position)
|
||||||
|
|||||||
429
ebox-surface.el
429
ebox-surface.el
@ -219,6 +219,10 @@ the winning client state; otherwise only the candidate is disposable."
|
|||||||
"ebox" (state))
|
"ebox" (state))
|
||||||
(declare-function ebox--scroll-state-retained-window-ready-p
|
(declare-function ebox--scroll-state-retained-window-ready-p
|
||||||
"ebox" (state))
|
"ebox" (state))
|
||||||
|
(declare-function ebox--scroll-state-set-lines
|
||||||
|
"ebox" (state content-lines &optional rendered-lines))
|
||||||
|
(declare-function ebox--surface-nested-scroll-cached-p
|
||||||
|
"ebox" (scroll &optional delta))
|
||||||
(declare-function ebox--scroll-box-contains-grid-p
|
(declare-function ebox--scroll-box-contains-grid-p
|
||||||
"ebox" (box))
|
"ebox" (box))
|
||||||
(declare-function ebox--scroll-line-region-span
|
(declare-function ebox--scroll-line-region-span
|
||||||
@ -513,34 +517,66 @@ left on the conservative full fragment scanner path."
|
|||||||
(or (gethash line ebox-surface--scroll-line-fragment-cache)
|
(or (gethash line ebox-surface--scroll-line-fragment-cache)
|
||||||
(let ((fragments (ebox-surface--rendered-fragments line)))
|
(let ((fragments (ebox-surface--rendered-fragments line)))
|
||||||
(when (and fragments
|
(when (and fragments
|
||||||
|
(plist-get (car fragments) :paint-role-ids)
|
||||||
|
(plist-get (car (last fragments)) :paint-role-ids)
|
||||||
(cl-every (lambda (fragment)
|
(cl-every (lambda (fragment)
|
||||||
(plist-get fragment :role-ids))
|
(plist-get fragment :role-ids))
|
||||||
fragments))
|
fragments))
|
||||||
(puthash line fragments ebox-surface--scroll-line-fragment-cache)
|
(puthash line fragments ebox-surface--scroll-line-fragment-cache)
|
||||||
fragments))))
|
fragments))))
|
||||||
|
|
||||||
|
(defun ebox-surface--map-scroll-fragments (lines visit)
|
||||||
|
"Call VISIT for LINES' cached fragments and intervening newline ownership.
|
||||||
|
VISIT receives a fragment, its line's absolute offset and its line index.
|
||||||
|
Return nil if a line needs the full scanner, otherwise return non-nil.
|
||||||
|
Unowned separators inherit the previous and next roles just as they do in
|
||||||
|
`ebox-surface--rendered-fragments'. Cached line templates stay immutable."
|
||||||
|
(cl-block ebox-surface--map-scroll-fragments
|
||||||
|
(let ((offset 0) (line-index 0)
|
||||||
|
(template (and lines
|
||||||
|
(ebox-surface--scroll-line-fragment-template
|
||||||
|
(car lines)))))
|
||||||
|
(while lines
|
||||||
|
(unless template
|
||||||
|
(cl-return-from ebox-surface--map-scroll-fragments nil))
|
||||||
|
(let ((length (length (car lines)))
|
||||||
|
previous-roles)
|
||||||
|
(dolist (fragment template)
|
||||||
|
(funcall visit fragment offset line-index)
|
||||||
|
(setq previous-roles (plist-get fragment :role-ids)))
|
||||||
|
(setq lines (cdr lines))
|
||||||
|
(when lines
|
||||||
|
(setq template
|
||||||
|
(ebox-surface--scroll-line-fragment-template (car lines)))
|
||||||
|
(unless template
|
||||||
|
(cl-return-from ebox-surface--map-scroll-fragments nil))
|
||||||
|
(funcall visit
|
||||||
|
(list :text-source-p t :start length :end (1+ length)
|
||||||
|
:role-ids
|
||||||
|
(delete-dups
|
||||||
|
(append (copy-sequence previous-roles)
|
||||||
|
(copy-sequence
|
||||||
|
(plist-get (car template) :role-ids)))))
|
||||||
|
offset line-index))
|
||||||
|
(setq offset (+ offset length 1)
|
||||||
|
line-index (1+ line-index))))
|
||||||
|
t)))
|
||||||
|
|
||||||
(defun ebox-surface--scroll-fragment-data (lines rendered)
|
(defun ebox-surface--scroll-fragment-data (lines rendered)
|
||||||
"Return offset fragments for visible LINES in RENDERED, or nil.
|
"Return offset fragments for visible LINES in RENDERED, or nil.
|
||||||
Each cached line template is copied only at the plist spine and points at the
|
Each cached line template is copied only at the plist spine and points at the
|
||||||
single joined RENDERED string. The text and role values remain candidate
|
single joined RENDERED string. The text and role values remain candidate
|
||||||
owned; no per-fragment substring is allocated."
|
owned; no per-fragment substring is allocated."
|
||||||
(cl-block ebox-surface--scroll-fragment-data
|
(let (result)
|
||||||
(let ((offset 0)
|
(when (ebox-surface--map-scroll-fragments
|
||||||
(line-index 0)
|
lines
|
||||||
result)
|
(lambda (fragment offset line-index)
|
||||||
(dolist (line lines)
|
|
||||||
(let ((template (ebox-surface--scroll-line-fragment-template line)))
|
|
||||||
(unless template
|
|
||||||
(cl-return-from ebox-surface--scroll-fragment-data nil))
|
|
||||||
(dolist (fragment template)
|
|
||||||
(let ((copy (copy-sequence fragment)))
|
(let ((copy (copy-sequence fragment)))
|
||||||
(plist-put copy :text rendered)
|
(plist-put copy :text rendered)
|
||||||
(plist-put copy :start (+ offset (plist-get fragment :start)))
|
(plist-put copy :start (+ offset (plist-get fragment :start)))
|
||||||
(plist-put copy :end (+ offset (plist-get fragment :end)))
|
(plist-put copy :end (+ offset (plist-get fragment :end)))
|
||||||
(plist-put copy :line line-index)
|
(plist-put copy :line line-index)
|
||||||
(push copy result))))
|
(push copy result))))
|
||||||
(setq offset (+ offset (length line) 1))
|
|
||||||
(setq line-index (1+ line-index)))
|
|
||||||
(nreverse result))))
|
(nreverse result))))
|
||||||
|
|
||||||
(defun ebox-surface--scroll-lines-owned-p (lines)
|
(defun ebox-surface--scroll-lines-owned-p (lines)
|
||||||
@ -1896,8 +1932,10 @@ the projection roots a second time."
|
|||||||
(ebox-surface--native-patch-result
|
(ebox-surface--native-patch-result
|
||||||
context projection state output node-objects region-objects))
|
context projection state output node-objects region-objects))
|
||||||
(and (or (null property-contributions) retained-owned-ranges)
|
(and (or (null property-contributions) retained-owned-ranges)
|
||||||
(memq projection-kind
|
(or (memq projection-kind
|
||||||
'(span-patch owner-scoped mixed-owner-reflow))
|
'(span-patch owner-scoped mixed-owner-reflow))
|
||||||
|
(and (eq projection-kind 'scroll-patch)
|
||||||
|
(plist-get state :nested-scroll-patch-proof)))
|
||||||
(ebox-surface--span-coordinate-result
|
(ebox-surface--span-coordinate-result
|
||||||
context projection state output previous-owned-ranges
|
context projection state output previous-owned-ranges
|
||||||
property-contributions retained-owned-ranges))
|
property-contributions retained-owned-ranges))
|
||||||
@ -2267,11 +2305,15 @@ RETAINED-OWNED-RANGES witnesses unchanged ownership when it is that snapshot."
|
|||||||
(or (plist-get state :mixed-owner-fragment-data)
|
(or (plist-get state :mixed-owner-fragment-data)
|
||||||
(plist-get state :span-patch-fragment-data))))
|
(plist-get state :span-patch-fragment-data))))
|
||||||
(rebased
|
(rebased
|
||||||
|
;; Layer composition can replace visible owners without
|
||||||
|
;; moving a single character. Coordinates alone cannot
|
||||||
|
;; retain its mounts; rebuild them from the new role runs.
|
||||||
|
(and (not (plist-get state :layer-recompose-p))
|
||||||
(if retained-origin
|
(if retained-origin
|
||||||
(cons t previous-owned-ranges)
|
(cons t previous-owned-ranges)
|
||||||
(and (null property-contributions) previous-owned-ranges
|
(and (null property-contributions) previous-owned-ranges
|
||||||
(ebox-surface--rebase-owned-ranges
|
(ebox-surface--rebase-owned-ranges
|
||||||
previous-owned-ranges patches (length rendered)))))
|
previous-owned-ranges patches (length rendered))))))
|
||||||
(batch
|
(batch
|
||||||
(ebox-surface--coordinate-batch
|
(ebox-surface--coordinate-batch
|
||||||
state rendered previous-owned-ranges
|
state rendered previous-owned-ranges
|
||||||
@ -3175,6 +3217,7 @@ valid because their declarative layout and viewport did not change."
|
|||||||
(member region-id producer-region-ids))
|
(member region-id producer-region-ids))
|
||||||
(producer-keys
|
(producer-keys
|
||||||
'(:content-lines :rendered-content-lines
|
'(:content-lines :rendered-content-lines
|
||||||
|
:rendered-content-line-cells
|
||||||
:content-lines-complete-p :render-content-prefix
|
:content-lines-complete-p :render-content-prefix
|
||||||
:materialize-content-lines :region-line-bounds-index
|
:materialize-content-lines :region-line-bounds-index
|
||||||
:region-line-span-index :region-line-span-index-deferred
|
:region-line-span-index :region-line-span-index-deferred
|
||||||
@ -3588,8 +3631,9 @@ old allocated width while the owner changes its natural content length."
|
|||||||
lines)))
|
lines)))
|
||||||
|
|
||||||
(defun ebox-surface--retained-external-owner-suffix
|
(defun ebox-surface--retained-external-owner-suffix
|
||||||
(buffer spans region-ids)
|
(buffer spans region-ids &optional allow-empty-p)
|
||||||
"Return one common retained content-owner suffix outside REGION-IDS."
|
"Return one common retained content-owner suffix outside REGION-IDS.
|
||||||
|
ALLOW-EMPTY-P permits t to represent a proved empty suffix in a root cache."
|
||||||
(let ((local (ebox--region-id-set region-ids)) common seen valid)
|
(let ((local (ebox--region-id-set region-ids)) common seen valid)
|
||||||
(setq valid t)
|
(setq valid t)
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
@ -3612,7 +3656,7 @@ old allocated width while the owner changes its natural content length."
|
|||||||
(unless (equal common external) (setq valid nil))
|
(unless (equal common external) (setq valid nil))
|
||||||
(setq common external seen t)))
|
(setq common external seen t)))
|
||||||
(setq position (max (1+ position) next)))))))
|
(setq position (max (1+ position) next)))))))
|
||||||
(and valid seen common)))
|
(and valid seen (or common allow-empty-p))))
|
||||||
|
|
||||||
(defun ebox-surface--owner-needs-ancestor-paint-p (state owner-id)
|
(defun ebox-surface--owner-needs-ancestor-paint-p (state owner-id)
|
||||||
"Return non-nil if STATE's detached OWNER-ID omits enclosing effects.
|
"Return non-nil if STATE's detached OWNER-ID omits enclosing effects.
|
||||||
@ -3719,7 +3763,9 @@ Compare resolved paint and surface properties, preserving opaque identities."
|
|||||||
((numberp width)))
|
((numberp width)))
|
||||||
(ebox--flex-copy-node-for-size node 'row width nil nil)))
|
(ebox--flex-copy-node-for-size node 'row width nil nil)))
|
||||||
((and node allocated-width variable-content-p)
|
((and node allocated-width variable-content-p)
|
||||||
(plist-put (copy-sequence node) :width allocated-width))
|
(if (eq (plist-get node :ebox-kind) 'text)
|
||||||
|
(plist-put (copy-sequence node) :width allocated-width)
|
||||||
|
(ebox--flex-copy-node-for-size node 'row allocated-width nil nil)))
|
||||||
(t node))))
|
(t node))))
|
||||||
|
|
||||||
(defun ebox-surface--retained-slot-owner-text
|
(defun ebox-surface--retained-slot-owner-text
|
||||||
@ -3838,6 +3884,33 @@ Return nil on a proof miss. The caller still validates the final footprint."
|
|||||||
(push line result)))))
|
(push line result)))))
|
||||||
(mapconcat #'identity (nreverse result) "\n"))))
|
(mapconcat #'identity (nreverse result) "\n"))))
|
||||||
|
|
||||||
|
(defun ebox-surface--retain-slot-paint-origins
|
||||||
|
(buffer previous-state spans lines)
|
||||||
|
"Carry known paint baselines from BUFFER's PREVIOUS-STATE into LINES.
|
||||||
|
SPANS and LINES already have identical character coordinates. Preserve only
|
||||||
|
captured baselines; an unknown baseline remains unknown for later proofs."
|
||||||
|
(let ((fragments (ebox-surface--materialized-fragment-ledger previous-state))
|
||||||
|
(origin (with-current-buffer buffer (point-min))))
|
||||||
|
(cl-loop
|
||||||
|
for span in spans for line in lines
|
||||||
|
do
|
||||||
|
(let ((start (- (car span) origin))
|
||||||
|
(end (- (cdr span) origin)))
|
||||||
|
(while (and fragments (<= (plist-get (car fragments) :end) start))
|
||||||
|
(setq fragments (cdr fragments)))
|
||||||
|
(let ((remaining fragments))
|
||||||
|
(while (and remaining (< (plist-get (car remaining) :start) end))
|
||||||
|
(let ((fragment (pop remaining)))
|
||||||
|
(when (plist-get fragment :face-baseline-known-p)
|
||||||
|
(put-text-property
|
||||||
|
(- (max start (plist-get fragment :start)) start)
|
||||||
|
(- (min end (plist-get fragment :end)) start)
|
||||||
|
ebox--paint-origin-property
|
||||||
|
(ebox--paint-origin-create
|
||||||
|
:baseline (copy-tree (plist-get fragment :face-baseline)))
|
||||||
|
line))))))))
|
||||||
|
lines)
|
||||||
|
|
||||||
(defun ebox-surface--retained-owner-text
|
(defun ebox-surface--retained-owner-text
|
||||||
(buffer previous-state state owner-id spans replacement allocated-width
|
(buffer previous-state state owner-id spans replacement allocated-width
|
||||||
variable-content-p variable-content-max-width
|
variable-content-p variable-content-max-width
|
||||||
@ -3879,10 +3952,10 @@ Proof mismatches return nil; unexpected rendering errors propagate."
|
|||||||
(old (ebox-runtime-index-get old-id (plist-get previous-state :node-table)))
|
(old (ebox-runtime-index-get old-id (plist-get previous-state :node-table)))
|
||||||
(new (ebox-runtime-index-get new-id (plist-get state :node-table))))
|
(new (ebox-runtime-index-get new-id (plist-get state :node-table))))
|
||||||
(and (equal old-id new-id)
|
(and (equal old-id new-id)
|
||||||
(if role-owned-lines-p
|
(if (or role-owned-lines-p variable-content-p)
|
||||||
;; The canonical candidate may replace an
|
;; The canonical candidate may replace a
|
||||||
;; anonymous source handle while retaining its
|
;; source handle while retaining its
|
||||||
;; runtime slot. This mode rebuilds fragment
|
;; runtime slot. These modes rebuild fragment
|
||||||
;; metadata; verify that slot's full identity
|
;; metadata; verify that slot's full identity
|
||||||
;; topology instead of retaining the old handle.
|
;; topology instead of retaining the old handle.
|
||||||
(and (eq (plist-get old :ebox-type)
|
(and (eq (plist-get old :ebox-type)
|
||||||
@ -3975,7 +4048,7 @@ Proof mismatches return nil; unexpected rendering errors propagate."
|
|||||||
line)
|
line)
|
||||||
(setq position end)))
|
(setq position end)))
|
||||||
(push line result))))))
|
(push line result))))))
|
||||||
(and valid (mapconcat #'identity (nreverse result) "\n")))))))
|
(and valid (ebox-lines-join (nreverse result))))))))
|
||||||
|
|
||||||
(defun ebox-surface--owner-patch-candidate
|
(defun ebox-surface--owner-patch-candidate
|
||||||
(buffer state owner-id allocated-width
|
(buffer state owner-id allocated-width
|
||||||
@ -4094,31 +4167,113 @@ Proof mismatches return nil; unexpected rendering errors propagate."
|
|||||||
(plist-get (plist-get details :footprint)
|
(plist-get (plist-get details :footprint)
|
||||||
:char-lengths)))))))))
|
:char-lengths)))))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--layer-enclosing-paint
|
||||||
|
(buffer state owner-id spans rendered)
|
||||||
|
"Restore enclosing paint from BUFFER and STATE on RENDERED.
|
||||||
|
OWNER-ID's SPANS delimit the retained slots.
|
||||||
|
Only ancestor roles and their resolved face contributions are retained.
|
||||||
|
Every published property run in a slot must agree on those effects, so a
|
||||||
|
newly exposed child cannot inherit the covered child's paint or interaction.
|
||||||
|
Return nil when the enclosing effect varies inside a retained slot."
|
||||||
|
(catch 'enclosing-paint-miss
|
||||||
|
(let ((parents (plist-get state :parent-table))
|
||||||
|
(nodes (plist-get state :node-table))
|
||||||
|
(parent owner-id)
|
||||||
|
(lines (ebox-string-lines rendered))
|
||||||
|
(ebox--paint-origin-capture-p t)
|
||||||
|
(ebox--render-owned-text-values
|
||||||
|
(plist-get state :render-owned-text-values))
|
||||||
|
ancestors result)
|
||||||
|
(while (setq parent (ebox-runtime-index-get parent parents))
|
||||||
|
(when-let* ((box (ebox-fragment-style-source-node
|
||||||
|
(ebox-runtime-index-get parent nodes))))
|
||||||
|
(push box ancestors)))
|
||||||
|
(setq ancestors (nreverse ancestors))
|
||||||
|
(unless (= (length spans) (length lines))
|
||||||
|
(throw 'enclosing-paint-miss nil))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(cl-loop
|
||||||
|
for span in spans for source in lines
|
||||||
|
do
|
||||||
|
(let* ((published (buffer-substring (car span) (cdr span)))
|
||||||
|
(position 0) (limit (length published))
|
||||||
|
(line (copy-sequence source))
|
||||||
|
seen effects)
|
||||||
|
(while (< position limit)
|
||||||
|
(let ((roles (ebox-surface--role-ids-at published position))
|
||||||
|
faces properties)
|
||||||
|
(dolist (box ancestors)
|
||||||
|
(let* ((region (plist-get box :region-id))
|
||||||
|
(own-roles
|
||||||
|
(cl-loop for (role . id) in roles
|
||||||
|
when (equal id region) collect role)))
|
||||||
|
(setq faces
|
||||||
|
(append faces
|
||||||
|
(ebox-surface--region-face-contributions
|
||||||
|
box own-roles t)))
|
||||||
|
(dolist (role own-roles)
|
||||||
|
(when-let* ((property (alist-get role ebox-region-types)))
|
||||||
|
(setq properties (plist-put properties property region))))))
|
||||||
|
(let ((current (cons faces properties)))
|
||||||
|
(when (and seen (not (equal effects current)))
|
||||||
|
(throw 'enclosing-paint-miss nil))
|
||||||
|
(setq effects current seen t)))
|
||||||
|
(setq position (next-property-change position published limit)))
|
||||||
|
(when (> (length line) 0)
|
||||||
|
(add-text-properties 0 (length line) (cdr effects) line)
|
||||||
|
(dolist (face (car effects))
|
||||||
|
(ebox--add-render-face! line 0 (length line) face t)))
|
||||||
|
(push line result))))
|
||||||
|
(ebox-lines-join (nreverse result)))))
|
||||||
|
|
||||||
(defun ebox-surface--range-patch-candidate (buffer state proof)
|
(defun ebox-surface--range-patch-candidate (buffer state proof)
|
||||||
"Return PROOF's exact equal-line Range replacement candidate, or nil."
|
"Return PROOF's exact equal-line Range or layer replacement candidate, or nil.
|
||||||
|
Render STATE's candidate into BUFFER's retained owner allocation."
|
||||||
(let* ((owner-id (plist-get proof :owner-id))
|
(let* ((owner-id (plist-get proof :owner-id))
|
||||||
(old-snapshot (plist-get proof :snapshot))
|
(old-snapshot (plist-get proof :snapshot))
|
||||||
(spans (plist-get old-snapshot :buffer-spans))
|
(spans (plist-get old-snapshot :buffer-spans))
|
||||||
(node (ebox-runtime-index-get owner-id (plist-get state :node-table)))
|
(node (ebox-runtime-index-get owner-id (plist-get state :node-table)))
|
||||||
(render-node
|
(render-node
|
||||||
(if (and node (numberp (plist-get proof :allocated-width)))
|
(cond
|
||||||
|
((or (plist-get proof :layer-recompose-p)
|
||||||
|
(plist-get proof :nested-scroll-patch-p))
|
||||||
|
(ebox-surface--owner-slot-render-node
|
||||||
|
buffer state owner-id old-snapshot
|
||||||
|
(plist-get proof :allocated-width) nil nil nil))
|
||||||
|
((and node (numberp (plist-get proof :allocated-width)))
|
||||||
(plist-put (copy-sequence node)
|
(plist-put (copy-sequence node)
|
||||||
:width (plist-get proof :allocated-width))
|
:width (plist-get proof :allocated-width)))
|
||||||
node))
|
(t node)))
|
||||||
(rendered
|
(rendered
|
||||||
(and render-node
|
(and render-node
|
||||||
(ebox-surface--render-candidate-node state render-node)))
|
(ebox-surface--render-candidate-node state render-node)))
|
||||||
|
(rendered
|
||||||
|
(if (and rendered (plist-get proof :nested-scroll-patch-p))
|
||||||
|
(ebox-surface--nested-scroll-owned-lines
|
||||||
|
buffer spans rendered (plist-get node :region-id))
|
||||||
|
rendered))
|
||||||
(external-owners
|
(external-owners
|
||||||
(and rendered
|
(and rendered
|
||||||
(ebox-surface--retained-external-owner-suffix
|
(ebox-surface--retained-external-owner-suffix
|
||||||
buffer spans (plist-get old-snapshot :region-ids))))
|
buffer spans (plist-get old-snapshot :region-ids)
|
||||||
|
(plist-get proof :scroll-content-coordinate-p))))
|
||||||
(rendered
|
(rendered
|
||||||
(and external-owners
|
(and external-owners
|
||||||
(ebox--add-content-owners rendered external-owners)))
|
(ebox--add-content-owners rendered
|
||||||
|
(unless (eq external-owners t) external-owners))))
|
||||||
|
(rendered
|
||||||
|
(if (and rendered (or (plist-get proof :layer-recompose-p)
|
||||||
|
(plist-get proof :nested-scroll-patch-p)))
|
||||||
|
(ebox-surface--layer-enclosing-paint
|
||||||
|
buffer state owner-id spans rendered)
|
||||||
|
rendered))
|
||||||
(replacement
|
(replacement
|
||||||
(and rendered
|
(and rendered
|
||||||
(ebox-buffer--rendered-in-existing-slots
|
(or (ebox-buffer--rendered-in-existing-slots
|
||||||
spans rendered nil t)))
|
spans rendered nil t)
|
||||||
|
(and (plist-get proof :scroll-content-coordinate-p)
|
||||||
|
(cl-every #'ebox--span-whole-line-p spans)
|
||||||
|
rendered))))
|
||||||
(lines (and replacement (ebox-string-lines replacement)))
|
(lines (and replacement (ebox-string-lines replacement)))
|
||||||
(new-spans
|
(new-spans
|
||||||
(and lines (ebox-surface--projected-span-positions spans lines)))
|
(and lines (ebox-surface--projected-span-positions spans lines)))
|
||||||
@ -4134,7 +4289,10 @@ Proof mismatches return nil; unexpected rendering errors propagate."
|
|||||||
(ebox--project-parent-slot-signature
|
(ebox--project-parent-slot-signature
|
||||||
(plist-get old-snapshot :parent-slot-signature)
|
(plist-get old-snapshot :parent-slot-signature)
|
||||||
new-footprint)))
|
new-footprint)))
|
||||||
(new-region-ids (and node (ebox--node-all-region-ids node)))
|
(new-region-ids
|
||||||
|
(and node (if (plist-get proof :nested-scroll-patch-p)
|
||||||
|
(plist-get old-snapshot :region-ids)
|
||||||
|
(ebox--node-all-region-ids node))))
|
||||||
(new-roles
|
(new-roles
|
||||||
(and replacement
|
(and replacement
|
||||||
(ebox-surface--rendered-role-topology-signature
|
(ebox-surface--rendered-role-topology-signature
|
||||||
@ -4238,6 +4396,7 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(lines (plist-get proof :lines))
|
(lines (plist-get proof :lines))
|
||||||
(owner-proof (car (plist-get state :owner-scoped-proofs)))
|
(owner-proof (car (plist-get state :owner-scoped-proofs)))
|
||||||
(owner-id (plist-get owner-proof :owner-id))
|
(owner-id (plist-get owner-proof :owner-id))
|
||||||
|
(nested-scroll-p (plist-get owner-proof :nested-scroll-patch-p))
|
||||||
cached-candidate cached-lines changes output)
|
cached-candidate cached-lines changes output)
|
||||||
(when (and old-scroll next-scroll
|
(when (and old-scroll next-scroll
|
||||||
(eq lines (plist-get old-scroll :content-lines))
|
(eq lines (plist-get old-scroll :content-lines))
|
||||||
@ -4279,6 +4438,11 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(ebox-incremental--candidate-base-state
|
(ebox-incremental--candidate-base-state
|
||||||
(cons (current-buffer) base)))
|
(cons (current-buffer) base)))
|
||||||
(setq cached-candidate
|
(setq cached-candidate
|
||||||
|
(if nested-scroll-p
|
||||||
|
(ebox-surface--nested-scroll-patch-candidate
|
||||||
|
(current-buffer) base candidate
|
||||||
|
(plist-put (copy-sequence owner-proof) :snapshot
|
||||||
|
(gethash owner-id (plist-get base :layout-snapshots))))
|
||||||
(ebox-surface--owner-patch-candidate
|
(ebox-surface--owner-patch-candidate
|
||||||
(current-buffer) candidate owner-id
|
(current-buffer) candidate owner-id
|
||||||
(plist-get owner-proof :allocated-width)
|
(plist-get owner-proof :allocated-width)
|
||||||
@ -4288,7 +4452,7 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(plist-get owner-proof :changed-keys)
|
(plist-get owner-proof :changed-keys)
|
||||||
(plist-get owner-proof :allocation-closure-p)
|
(plist-get owner-proof :allocation-closure-p)
|
||||||
(plist-get owner-proof :retain-external-owner-suffix-p)
|
(plist-get owner-proof :retain-external-owner-suffix-p)
|
||||||
base))
|
base)))
|
||||||
(when cached-candidate
|
(when cached-candidate
|
||||||
(setq cached-lines (copy-sequence lines))
|
(setq cached-lines (copy-sequence lines))
|
||||||
(cl-mapc
|
(cl-mapc
|
||||||
@ -4309,6 +4473,14 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(plist-get previous-state :node-table)))
|
(plist-get previous-state :node-table)))
|
||||||
(visible (ebox--region-ids-visible-in-buffer-p
|
(visible (ebox--region-ids-visible-in-buffer-p
|
||||||
buffer (ebox--node-all-region-ids node))))
|
buffer (ebox--node-all-region-ids node))))
|
||||||
|
(when (and nested-scroll-p visible)
|
||||||
|
;; The cache and the mounted window are distinct coordinate spaces.
|
||||||
|
;; A clipped owner must independently prove its mounted allocation.
|
||||||
|
(when-let* ((snapshot (ebox--ensure-layout-snapshot-details buffer owner-id)))
|
||||||
|
(plist-put state :owner-scoped-proofs
|
||||||
|
(list (let ((mounted (plist-put (copy-sequence owner-proof) :snapshot snapshot)))
|
||||||
|
(cl-remf mounted :scroll-content-coordinate-p)
|
||||||
|
mounted)))))
|
||||||
(setq output
|
(setq output
|
||||||
(if visible
|
(if visible
|
||||||
(ebox-surface--buffer-span-patch-output buffer previous-state state)
|
(ebox-surface--buffer-span-patch-output buffer previous-state state)
|
||||||
@ -4337,6 +4509,10 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
bounds)
|
bounds)
|
||||||
(plist-put private :region-line-bounds-index copy)))
|
(plist-put private :region-line-bounds-index copy)))
|
||||||
(when rendered
|
(when rendered
|
||||||
|
;; Auto-width scroll chrome can measure its candidate children
|
||||||
|
;; while shaping a cached line. Those children belong to this
|
||||||
|
;; input generation, even though BUFFER still holds the old one.
|
||||||
|
(let ((ebox--render-source-index (plist-get state :source-index)))
|
||||||
(dolist (change changes)
|
(dolist (change changes)
|
||||||
(let* ((number (car change))
|
(let* ((number (car change))
|
||||||
(old (nth number rendered))
|
(old (nth number rendered))
|
||||||
@ -4344,7 +4520,7 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(plist-get private :box) (list (nth 2 change))
|
(plist-get private :box) (list (nth 2 change))
|
||||||
region number))))
|
region number))))
|
||||||
(setcar (nthcdr number rendered) new)
|
(setcar (nthcdr number rendered) new)
|
||||||
(push (list number old new) rendered-changes))))
|
(push (list number old new) rendered-changes)))))
|
||||||
(plist-put private :content-lines cached-lines)
|
(plist-put private :content-lines cached-lines)
|
||||||
(plist-put private :region-line-span-index
|
(plist-put private :region-line-span-index
|
||||||
(ebox-surface--scroll-replace-index-lines
|
(ebox-surface--scroll-replace-index-lines
|
||||||
@ -4357,9 +4533,19 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(ebox-surface--scroll-replace-index-lines
|
(ebox-surface--scroll-replace-index-lines
|
||||||
(plist-get private :rendered-region-line-span-index)
|
(plist-get private :rendered-region-line-span-index)
|
||||||
rendered-changes))
|
rendered-changes))
|
||||||
;; The validated owner preserves line and property topology;
|
(if nested-scroll-p
|
||||||
;; line bounds/membership stay valid, character hints do not.
|
;; A viewport reveals different child roles. Rebuild the
|
||||||
(cl-remf private :region-line-span-hints)
|
;; content membership/bounds and retire the old slice index.
|
||||||
|
(progn
|
||||||
|
(plist-put private :content-lines lines)
|
||||||
|
(setq private (ebox--scroll-state-set-lines private cached-lines rendered))
|
||||||
|
(setq private (ebox--scroll-window-rebind-state-producers
|
||||||
|
private (plist-get private :box)))
|
||||||
|
(cl-remf private :lazy-scroll-prefix-dirty)
|
||||||
|
(cl-remf private :lazy-scroll-window-refresh-required))
|
||||||
|
;; Content-only patches preserve role topology, but their
|
||||||
|
;; character hints must be derived from the replacement text.
|
||||||
|
(cl-remf private :region-line-span-hints))
|
||||||
(puthash region private (plist-get state :scroll-state-table)))
|
(puthash region private (plist-get state :scroll-state-table)))
|
||||||
(cl-remf state :scroll-content-proof))))
|
(cl-remf state :scroll-content-proof))))
|
||||||
output)))
|
output)))
|
||||||
@ -4378,6 +4564,9 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(mapcar
|
(mapcar
|
||||||
(lambda (proof)
|
(lambda (proof)
|
||||||
(cond
|
(cond
|
||||||
|
((plist-get proof :nested-scroll-patch-p)
|
||||||
|
(ebox-surface--nested-scroll-patch-candidate
|
||||||
|
buffer previous-state state proof))
|
||||||
((plist-get proof :layer-recompose-p)
|
((plist-get proof :layer-recompose-p)
|
||||||
(ebox-surface--layer-patch-candidate
|
(ebox-surface--layer-patch-candidate
|
||||||
buffer previous-state state proof))
|
buffer previous-state state proof))
|
||||||
@ -4394,7 +4583,10 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(plist-get proof :changed-keys)
|
(plist-get proof :changed-keys)
|
||||||
(plist-get proof :allocation-closure-p)
|
(plist-get proof :allocation-closure-p)
|
||||||
(plist-get proof :retain-external-owner-suffix-p)
|
(plist-get proof :retain-external-owner-suffix-p)
|
||||||
(and (= (length proofs) 1) previous-state)))))
|
;; Every disjoint owner is proved against the same
|
||||||
|
;; committed coordinates. Its enclosing properties
|
||||||
|
;; need that baseline even in a multi-owner patch.
|
||||||
|
previous-state))))
|
||||||
proofs)
|
proofs)
|
||||||
(list
|
(list
|
||||||
(ebox-surface--owner-patch-candidate
|
(ebox-surface--owner-patch-candidate
|
||||||
@ -4411,6 +4603,15 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(ebox-surface--materialized-fragment-ledger previous-state))))
|
(ebox-surface--materialized-fragment-ledger previous-state))))
|
||||||
(when (and candidates (cl-every #'identity candidates)
|
(when (and candidates (cl-every #'identity candidates)
|
||||||
(or (not retained-properties-p) retained-fragments))
|
(or (not retained-properties-p) retained-fragments))
|
||||||
|
;; Rebuilding any owner reparses the combined replacements. Exact
|
||||||
|
;; property copies need their ledger baseline on that path; batches
|
||||||
|
;; that retain the ledger do not put private carriers in output text.
|
||||||
|
(unless retained-properties-p
|
||||||
|
(dolist (candidate candidates)
|
||||||
|
(when (plist-get candidate :retained-properties-p)
|
||||||
|
(ebox-surface--retain-slot-paint-origins
|
||||||
|
buffer previous-state (plist-get candidate :old-spans)
|
||||||
|
(plist-get (plist-get candidate :details) :lines)))))
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(save-restriction
|
(save-restriction
|
||||||
(widen)
|
(widen)
|
||||||
@ -4889,14 +5090,86 @@ causes the caller to use the ordinary full projection."
|
|||||||
(mapcar (lambda (fragment) (plist-get fragment :text))
|
(mapcar (lambda (fragment) (plist-get fragment :text))
|
||||||
fragments)))))))))
|
fragments)))))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--nested-scroll-owned-lines (buffer spans rendered region)
|
||||||
|
"Select RENDERED's exact chrome-aligned rows owned by BUFFER SPANS and REGION.
|
||||||
|
Row composition may retain an upper border in its owner spans while Column
|
||||||
|
composition leaves both border rows outside. Match explicit vertical roles
|
||||||
|
instead of choosing which edge to trim from a numeric line-count difference."
|
||||||
|
(let ((lines (ebox-string-lines rendered)) signatures matches)
|
||||||
|
(cl-labels
|
||||||
|
((signature (text)
|
||||||
|
(cl-loop for role in '(mt bt pt pb bb mb)
|
||||||
|
for property = (alist-get role ebox-region-types)
|
||||||
|
when (text-property-any 0 (length text) property region text)
|
||||||
|
collect role)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(setq signatures
|
||||||
|
(mapcar (lambda (span)
|
||||||
|
(signature (buffer-substring (car span) (cdr span))))
|
||||||
|
spans)))
|
||||||
|
(let ((rendered-signatures (mapcar #'signature lines))
|
||||||
|
(count (length spans)))
|
||||||
|
(cl-loop for start from 0 to (- (length lines) count)
|
||||||
|
when (equal signatures (cl-subseq rendered-signatures start (+ start count)))
|
||||||
|
do (push start matches))
|
||||||
|
(when (= (length matches) 1)
|
||||||
|
(ebox-lines-join (cl-subseq lines (car matches) (+ (car matches) count))))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--nested-scroll-patch-candidate (buffer previous-state state proof)
|
||||||
|
"Return PROOF's cached nested scroll replacement without rendering siblings.
|
||||||
|
BUFFER and PREVIOUS-STATE supply the committed physical slots. STATE owns an
|
||||||
|
isolated scroll Box. Cached content is reshaped only through its wrapper;
|
||||||
|
changing child roles must satisfy the same exact external footprint proof as
|
||||||
|
a local composed host."
|
||||||
|
(let* ((region (plist-get state :scroll-patch-region-id))
|
||||||
|
(table (plist-get state :scroll-state-table))
|
||||||
|
(scroll (gethash region table))
|
||||||
|
(box (plist-get scroll :box))
|
||||||
|
(owner (plist-get proof :owner-id))
|
||||||
|
(parent (ebox-runtime-index-get owner (plist-get state :parent-table)))
|
||||||
|
(valid t))
|
||||||
|
(while (and valid parent)
|
||||||
|
(when (ebox-get (ebox-runtime-index-get parent (plist-get state :node-table))
|
||||||
|
:surface-properties)
|
||||||
|
(setq valid nil))
|
||||||
|
(setq parent (ebox-runtime-index-get parent (plist-get state :parent-table))))
|
||||||
|
(when (and valid box
|
||||||
|
(ebox--surface-nested-scroll-cached-p scroll 0)
|
||||||
|
(ebox-surface--unchanged-enclosing-effects-p previous-state state owner))
|
||||||
|
(let ((ebox--scroll-window-cached-state scroll)
|
||||||
|
(ebox--scroll-window-skip-state-rebuild-p
|
||||||
|
(not (plist-get proof :scroll-content-coordinate-p))))
|
||||||
|
(prog1 (ebox-surface--range-patch-candidate buffer state proof)
|
||||||
|
;; Slot sizing renders a temporary copy. Future prefix producers
|
||||||
|
;; and imperative updates must keep the canonical candidate Box.
|
||||||
|
(puthash region box (plist-get state :region-box-table))
|
||||||
|
(puthash region scroll table))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--nested-scroll-full-overrides (buffer root state-overrides)
|
||||||
|
"Return private ROOT and overrides after a nested scroll slot proof misses.
|
||||||
|
BUFFER's candidate scroll caches rebind to the copied tree before any full
|
||||||
|
renderer may mutate geometry. Published siblings stay untouched on rollback."
|
||||||
|
(let* ((copy (ebox-tree-copy-node-structure root))
|
||||||
|
(overrides (copy-sequence state-overrides))
|
||||||
|
(index (ebox--runtime-index copy t (plist-get overrides :source-index)))
|
||||||
|
(ebox--scroll-global-state (plist-get overrides :scroll-state-table))
|
||||||
|
(table (ebox-incremental--candidate-scroll-state-table
|
||||||
|
buffer overrides index (plist-get index :region-box-table))))
|
||||||
|
(setq overrides (ebox--render-state-install-index overrides index))
|
||||||
|
(plist-put overrides :scroll-state-table table)
|
||||||
|
(plist-put overrides :runtime-index-prepared-p t)
|
||||||
|
(plist-put overrides :source-path-copied-p nil)
|
||||||
|
(cl-remf overrides :nested-scroll-patch-proof)
|
||||||
|
(list copy overrides)))
|
||||||
|
|
||||||
(defun ebox-surface--scroll-patch-output (_buffer state &optional complete-lines-only-p)
|
(defun ebox-surface--scroll-patch-output (_buffer state &optional complete-lines-only-p)
|
||||||
"Return a staged visible scroll window without rerunning layout when safe.
|
"Return a staged visible scroll window without rerunning layout when safe.
|
||||||
Chrome-free top-aligned root scroll boxes already store fully propertized
|
Chrome-free top-aligned root scroll boxes already store fully propertized
|
||||||
rendered lines. Join only the visible slice and add the scroll marker; any
|
rendered lines. Join only the visible slice and add the scroll marker; any
|
||||||
box with wrapper chrome falls back to the retained wrapper renderer.
|
box with wrapper chrome falls back to the retained wrapper renderer.
|
||||||
When COMPLETE-LINES-ONLY-P is non-nil, return only proven complete cached
|
When COMPLETE-LINES-ONLY-P is non-nil, return only proven complete cached
|
||||||
physical lines, with private pixel-normalized copies where needed. Decline
|
physical lines or a complete cached root wrapper, with private pixel-normalized
|
||||||
wrapper rendering so a caller may keep its ordinary fractional-origin guard."
|
copies where needed. Detached owners keep their fractional-origin guard."
|
||||||
(let* ((region-id (plist-get state :scroll-patch-region-id))
|
(let* ((region-id (plist-get state :scroll-patch-region-id))
|
||||||
(scroll-table (plist-get state :scroll-state-table))
|
(scroll-table (plist-get state :scroll-state-table))
|
||||||
(scroll-state (and region-id scroll-table
|
(scroll-state (and region-id scroll-table
|
||||||
@ -4961,11 +5234,31 @@ wrapper rendering so a caller may keep its ordinary fractional-origin guard."
|
|||||||
(list 'ebox-scroll-window region-id)
|
(list 'ebox-scroll-window region-id)
|
||||||
output))
|
output))
|
||||||
output)
|
output)
|
||||||
(unless complete-lines-only-p
|
(let ((root-wrapper-p
|
||||||
(let ((ebox--scroll-window-cached-state scroll-state)
|
(and (or (plist-get state :layered-p) (not chrome-free-p))
|
||||||
|
(ebox-layer-cached-root-scroll-p root scroll-state))))
|
||||||
|
;; Ordinary chrome-free roots first rebuild physical row metadata
|
||||||
|
;; after content-only materialization. Retaining that raw cache
|
||||||
|
;; here would prevent later scrolls from using direct row slices.
|
||||||
|
(when (or root-wrapper-p (not complete-lines-only-p))
|
||||||
|
(let* ((ebox--scroll-window-cached-state scroll-state)
|
||||||
(ebox--scroll-window-skip-state-rebuild-p
|
(ebox--scroll-window-skip-state-rebuild-p
|
||||||
(plist-get scroll-state :content-lines-complete-p)))
|
(or (plist-get scroll-state :content-lines-complete-p)
|
||||||
(ebox-surface--render-candidate-node state root))))))))
|
root-wrapper-p))
|
||||||
|
;; This wrapper owns the entire physical line origin.
|
||||||
|
(ebox-surface--integral-local-output-required-p
|
||||||
|
(and (not root-wrapper-p)
|
||||||
|
ebox-surface--integral-local-output-required-p))
|
||||||
|
(output (ebox-surface--render-candidate-node state root)))
|
||||||
|
(when root-wrapper-p
|
||||||
|
(plist-put state :fractional-pixel-output-p
|
||||||
|
(ebox--fractional-pixel-spaces-p output))
|
||||||
|
(when (plist-get state :fractional-pixel-output-p)
|
||||||
|
(let ((ebox--render-owned-text-values
|
||||||
|
(plist-get state :render-owned-text-values)))
|
||||||
|
(setq output (ebox--quantize-pixel-spaces output))
|
||||||
|
(ebox--record-render-output-provenance output))))
|
||||||
|
output))))))))
|
||||||
|
|
||||||
(defun ebox-surface--native-scroll-full-output (state)
|
(defun ebox-surface--native-scroll-full-output (state)
|
||||||
"Return the complete propertized root scroll output for native scrolling."
|
"Return the complete propertized root scroll output for native scrolling."
|
||||||
@ -5552,18 +5845,15 @@ Adjacent ranges merge only when both their owner and opaque tags match."
|
|||||||
(ebox-surface--owned-ranges-from-runs
|
(ebox-surface--owned-ranges-from-runs
|
||||||
context leaf state node-objects region-objects attach-p
|
context leaf state node-objects region-objects attach-p
|
||||||
(lambda (visit)
|
(lambda (visit)
|
||||||
(let ((offset 0))
|
(unless (ebox-surface--map-scroll-fragments
|
||||||
(dolist (line lines)
|
lines
|
||||||
(let ((template
|
(lambda (fragment offset _line-index)
|
||||||
(or (ebox-surface--scroll-line-fragment-template line)
|
|
||||||
(error "Ebox retained scroll line lost fragment ownership"))))
|
|
||||||
(dolist (fragment template)
|
|
||||||
(funcall visit
|
(funcall visit
|
||||||
(plist-get fragment :role-ids)
|
(plist-get fragment :role-ids)
|
||||||
(+ offset (plist-get fragment :start))
|
(+ offset (plist-get fragment :start))
|
||||||
(+ offset (plist-get fragment :end))
|
(+ offset (plist-get fragment :end))
|
||||||
(plist-get fragment :layout-region-ids)))
|
(plist-get fragment :layout-region-ids))))
|
||||||
(setq offset (+ offset (length line) 1))))))))
|
(error "Ebox retained scroll line lost fragment ownership")))))
|
||||||
|
|
||||||
(defun ebox-surface--native-owned-ranges
|
(defun ebox-surface--native-owned-ranges
|
||||||
(context leaf template state node-objects region-objects attach-p)
|
(context leaf template state node-objects region-objects attach-p)
|
||||||
@ -5733,6 +6023,7 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
|||||||
'(span-patch owner-scoped
|
'(span-patch owner-scoped
|
||||||
mixed-owner-reflow))
|
mixed-owner-reflow))
|
||||||
(not formatting-reflow-p)
|
(not formatting-reflow-p)
|
||||||
|
(not (plist-get state :layer-recompose-p))
|
||||||
(plist-get state :previous-surface-owned-ranges)
|
(plist-get state :previous-surface-owned-ranges)
|
||||||
(plist-get state :content-coordinate-patches)
|
(plist-get state :content-coordinate-patches)
|
||||||
(ebox-surface--rebase-owned-ranges
|
(ebox-surface--rebase-owned-ranges
|
||||||
@ -6334,6 +6625,14 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
:key ebox-surface--root-key :kind 'ebox/string
|
:key ebox-surface--root-key :kind 'ebox/string
|
||||||
:text source :capability 'content)))
|
:text source :capability 'content)))
|
||||||
(let* ((scroll-patch-p (eq projection-kind 'scroll-patch))
|
(let* ((scroll-patch-p (eq projection-kind 'scroll-patch))
|
||||||
|
(scroll-input-stable-p
|
||||||
|
(and scroll-patch-p previous-state preserve-identities-p
|
||||||
|
(plist-get state-overrides :scroll-state-transaction)
|
||||||
|
(plist-get state-overrides :runtime-index-prepared-p)
|
||||||
|
(equal (plist-get source :node-id)
|
||||||
|
(plist-get (plist-get previous-state :root-node) :node-id))
|
||||||
|
(equal (plist-get state-overrides :display-signature)
|
||||||
|
(plist-get previous-state :display-signature))))
|
||||||
(initial-native-p
|
(initial-native-p
|
||||||
(and (null previous-state)
|
(and (null previous-state)
|
||||||
(null projection-kind)
|
(null projection-kind)
|
||||||
@ -6355,7 +6654,7 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
(setq fast nil))
|
(setq fast nil))
|
||||||
fast)))
|
fast)))
|
||||||
(stylesheet-active-p
|
(stylesheet-active-p
|
||||||
(if scroll-fast-p
|
(if scroll-input-stable-p
|
||||||
(plist-get previous-state :cascade-active-p)
|
(plist-get previous-state :cascade-active-p)
|
||||||
(ebox-style-cascade-active-p)))
|
(ebox-style-cascade-active-p)))
|
||||||
(retained-runtime-source-p
|
(retained-runtime-source-p
|
||||||
@ -6438,20 +6737,20 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
(equal (plist-get entry :changed-keys) '(:content)))
|
(equal (plist-get entry :changed-keys) '(:content)))
|
||||||
dirty-set)))))
|
dirty-set)))))
|
||||||
(inline-inheritance-required-p
|
(inline-inheritance-required-p
|
||||||
(unless (or scroll-fast-p retained-runtime-source-p
|
(unless (or scroll-input-stable-p retained-runtime-source-p
|
||||||
retained-inline-styles-p)
|
retained-inline-styles-p)
|
||||||
(ebox-surface--inline-inheritance-required-p
|
(ebox-surface--inline-inheritance-required-p
|
||||||
root source-index)))
|
root source-index)))
|
||||||
(author-style-pending-p
|
(author-style-pending-p
|
||||||
(> (ebox-tree-author-style-count root) 0))
|
(> (ebox-tree-author-style-count root) 0))
|
||||||
(cascade-required-p
|
(cascade-required-p
|
||||||
(if (or scroll-fast-p retained-runtime-source-p
|
(if (or scroll-input-stable-p retained-runtime-source-p
|
||||||
retained-inline-styles-p)
|
retained-inline-styles-p)
|
||||||
(plist-get previous-state :cascade-required-p)
|
(plist-get previous-state :cascade-required-p)
|
||||||
(or stylesheet-active-p
|
(or stylesheet-active-p
|
||||||
inline-inheritance-required-p)))
|
inline-inheritance-required-p)))
|
||||||
(style-required-p
|
(style-required-p
|
||||||
(if scroll-fast-p
|
(if scroll-input-stable-p
|
||||||
cascade-required-p
|
cascade-required-p
|
||||||
(or cascade-required-p
|
(or cascade-required-p
|
||||||
author-style-pending-p
|
author-style-pending-p
|
||||||
@ -6493,7 +6792,7 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
(and
|
(and
|
||||||
signals
|
signals
|
||||||
(cond
|
(cond
|
||||||
((and scroll-fast-p
|
((and scroll-input-stable-p
|
||||||
(plist-get previous-state
|
(plist-get previous-state
|
||||||
:viewport-dependent-node-ids-ready)
|
:viewport-dependent-node-ids-ready)
|
||||||
(plist-member
|
(plist-member
|
||||||
@ -6641,13 +6940,22 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
(ebox-surface--native-scroll-full-output state))
|
(ebox-surface--native-scroll-full-output state))
|
||||||
(ebox-surface--scroll-patch-output
|
(ebox-surface--scroll-patch-output
|
||||||
(ebox-surface--signals-buffer signals) state)
|
(ebox-surface--signals-buffer signals) state)
|
||||||
|
(when-let* ((proof (plist-get state :nested-scroll-patch-proof))
|
||||||
|
((or (not (plist-get proof :scroll-content-region-id))
|
||||||
|
(plist-get state :scroll-content-proof))))
|
||||||
|
(unless (plist-get state :owner-scoped-proofs)
|
||||||
|
(plist-put state :owner-scoped-proofs (list proof)))
|
||||||
|
(ebox-surface--span-patch-output
|
||||||
|
(ebox-surface--signals-buffer signals)
|
||||||
|
previous-state state))
|
||||||
;; A nested scroll already owns a
|
;; A nested scroll already owns a
|
||||||
;; complete candidate projection.
|
;; complete candidate projection.
|
||||||
;; Preserve its ordinary renderer
|
;; Preserve its ordinary renderer
|
||||||
;; fallback without recascading the
|
;; fallback without recascading the
|
||||||
;; retained source. The raw-output
|
;; retained source. The raw-output
|
||||||
;; guard still rejects fractions.
|
;; guard still rejects fractions.
|
||||||
(unless (plist-get state :native-scroll-materialize-p)
|
(unless (or (plist-get state :native-scroll-materialize-p)
|
||||||
|
(plist-get state :nested-scroll-patch-proof))
|
||||||
(ebox-surface--render-candidate state))))
|
(ebox-surface--render-candidate state))))
|
||||||
('formatting-context-reflow
|
('formatting-context-reflow
|
||||||
(ebox-surface--formatting-context-reflow-output
|
(ebox-surface--formatting-context-reflow-output
|
||||||
@ -6664,6 +6972,12 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
;; A declined output may already have staged coordinates,
|
;; A declined output may already have staged coordinates,
|
||||||
;; fragments or paint layers. Rebuild from the original
|
;; fragments or paint layers. Rebuild from the original
|
||||||
;; candidate inputs before producing ordinary full output.
|
;; candidate inputs before producing ordinary full output.
|
||||||
|
(when (plist-get state-overrides :nested-scroll-patch-proof)
|
||||||
|
(pcase-let ((`(,private-root ,private-overrides)
|
||||||
|
(ebox-surface--nested-scroll-full-overrides
|
||||||
|
(ebox-surface--signals-buffer signals)
|
||||||
|
root state-overrides)))
|
||||||
|
(setq root private-root state-overrides private-overrides)))
|
||||||
;; Range deltas already projected their complete object
|
;; Range deltas already projected their complete object
|
||||||
;; map, including newly registered keys in this context.
|
;; map, including newly registered keys in this context.
|
||||||
(unless (and (eq projection-kind 'mixed-owner-reflow)
|
(unless (and (eq projection-kind 'mixed-owner-reflow)
|
||||||
@ -6736,6 +7050,7 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
(cl-remf state :native-removed-node-ids)
|
(cl-remf state :native-removed-node-ids)
|
||||||
(cl-remf state :computed-participation-validated-p)
|
(cl-remf state :computed-participation-validated-p)
|
||||||
(cl-remf state :candidate-subject-table)
|
(cl-remf state :candidate-subject-table)
|
||||||
|
(cl-remf state :nested-scroll-patch-proof)
|
||||||
result)))))))
|
result)))))))
|
||||||
|
|
||||||
(defun ebox-surface-producer
|
(defun ebox-surface-producer
|
||||||
|
|||||||
240
ebox.el
240
ebox.el
@ -91,6 +91,44 @@
|
|||||||
buffer 'batch
|
buffer 'batch
|
||||||
(lambda () (ebox-incremental--flush buffer))))
|
(lambda () (ebox-incremental--flush buffer))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun ebox-call-with-update-batch (callback &optional buffer)
|
||||||
|
"Call CALLBACK in BUFFER, coalescing its region updates into one publication.
|
||||||
|
BUFFER defaults to the current buffer. CALLBACK receives no arguments and its
|
||||||
|
return value is returned unchanged. Flush after CALLBACK returns normally;
|
||||||
|
an empty or unchanged batch does not publish or replace the last update report.
|
||||||
|
|
||||||
|
When BUFFER already has an active batch, join it without flushing. Errors,
|
||||||
|
quits and other nonlocal exits discard only this call's queued changes and
|
||||||
|
preserve the outer batch. A failed owned flush discards its unpublished batch;
|
||||||
|
the TP publication boundary preserves the previously committed generation.
|
||||||
|
|
||||||
|
This scope covers queued `ebox-region-update' calls for BUFFER, not arbitrary
|
||||||
|
side effects in CALLBACK. Leave explicit begin/flush lifecycle calls outside
|
||||||
|
CALLBACK; the existing explicit API retains its separate retry semantics."
|
||||||
|
(let* ((buffer (or buffer (current-buffer)))
|
||||||
|
(previous (ebox-incremental--batch-state buffer))
|
||||||
|
(outer-batch (ebox-incremental-batching-p buffer))
|
||||||
|
completed)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(if outer-batch
|
||||||
|
;; Queuing replaces candidate roots/source indexes and appends
|
||||||
|
;; pending entries, so copying the plist isolates this scope.
|
||||||
|
(puthash buffer (copy-sequence previous)
|
||||||
|
ebox-incremental--batch-table)
|
||||||
|
(ebox-incremental-begin-batch buffer))
|
||||||
|
(prog1 (funcall callback)
|
||||||
|
(when (and (not outer-batch)
|
||||||
|
(> (ebox-incremental-pending-count buffer) 0))
|
||||||
|
(ebox-incremental-flush buffer))
|
||||||
|
(setq completed t)))
|
||||||
|
(unless (and outer-batch completed)
|
||||||
|
(if previous
|
||||||
|
(puthash buffer previous ebox-incremental--batch-table)
|
||||||
|
(remhash buffer ebox-incremental--batch-table)))))))
|
||||||
|
|
||||||
(declare-function ebox--buffer-update-report "ebox-incremental" (buffer))
|
(declare-function ebox--buffer-update-report "ebox-incremental" (buffer))
|
||||||
(declare-function ebox--set-buffer-update-report
|
(declare-function ebox--set-buffer-update-report
|
||||||
"ebox-incremental" (buffer report))
|
"ebox-incremental" (buffer report))
|
||||||
@ -1089,6 +1127,7 @@ content back to their own source model."
|
|||||||
:region-line-span-index-deferred
|
:region-line-span-index-deferred
|
||||||
:rendered-region-line-span-index
|
:rendered-region-line-span-index
|
||||||
:rendered-region-line-span-index-deferred
|
:rendered-region-line-span-index-deferred
|
||||||
|
:rendered-content-line-cells
|
||||||
:region-line-span-hints
|
:region-line-span-hints
|
||||||
:lazy-scroll-prefix-dirty
|
:lazy-scroll-prefix-dirty
|
||||||
:lazy-scroll-window-refresh-required
|
:lazy-scroll-window-refresh-required
|
||||||
@ -1268,6 +1307,33 @@ remain transaction-local."
|
|||||||
(when had-state
|
(when had-state
|
||||||
(ebox--record-render-cache-scroll-region region-id))))
|
(ebox--record-render-cache-scroll-region region-id))))
|
||||||
|
|
||||||
|
(defun ebox--scroll-rendered-line-cells-current-p (cells lines)
|
||||||
|
"Return non-nil when positional CELLS still describe rendered LINES.
|
||||||
|
Rendered list spines are append-only between installations. Equal-length
|
||||||
|
prefix rewrites replace cars, so retained cons handles see current row values;
|
||||||
|
an appended tail or replacement head invalidates the positional index."
|
||||||
|
(and (vectorp cells) (> (length cells) 0)
|
||||||
|
(eq (aref cells 0) lines)
|
||||||
|
(null (cdr (aref cells (1- (length cells)))))))
|
||||||
|
|
||||||
|
(defun ebox--scroll-rendered-line-cells (state lines)
|
||||||
|
"Return STATE's immutable positional cons-cell index for complete LINES.
|
||||||
|
Only a complete cache is indexed, avoiding full-prefix rebuilds during lazy
|
||||||
|
extension. Indexes may be shared by shallow candidate state copies; replacement
|
||||||
|
or extension creates a new vector without changing earlier state or row data."
|
||||||
|
(when (or (plist-get state :content-lines-complete-p)
|
||||||
|
(and (not (plist-get state :render-content-prefix))
|
||||||
|
(not (plist-get state :materialize-content-lines))))
|
||||||
|
(let ((cells (plist-get state :rendered-content-line-cells)))
|
||||||
|
(unless (ebox--scroll-rendered-line-cells-current-p cells lines)
|
||||||
|
(setq cells (make-vector (length lines) nil))
|
||||||
|
(let ((tail lines))
|
||||||
|
(dotimes (index (length cells))
|
||||||
|
(aset cells index tail)
|
||||||
|
(setq tail (cdr tail))))
|
||||||
|
(plist-put state :rendered-content-line-cells cells))
|
||||||
|
cells)))
|
||||||
|
|
||||||
(defun ebox--scroll-state-rendered-visible-window (state)
|
(defun ebox--scroll-state-rendered-visible-window (state)
|
||||||
"Return STATE's current rendered visible slice, including text properties."
|
"Return STATE's current rendered visible slice, including text properties."
|
||||||
(let* ((content-height (plist-get state :content-height))
|
(let* ((content-height (plist-get state :content-height))
|
||||||
@ -1282,10 +1348,16 @@ remain transaction-local."
|
|||||||
(list offset native-lines))
|
(list offset native-lines))
|
||||||
((and (integerp content-height) (>= content-height 0))
|
((and (integerp content-height) (>= content-height 0))
|
||||||
(when-let* ((lines (plist-get state :rendered-content-lines)))
|
(when-let* ((lines (plist-get state :rendered-content-lines)))
|
||||||
(let* ((max-offset (max 0 (- (length lines) content-height)))
|
(let* ((cells (ebox--scroll-rendered-line-cells state lines))
|
||||||
|
(count (if cells (length cells) (length lines)))
|
||||||
|
(max-offset (max 0 (- count content-height)))
|
||||||
(offset (max 0 (min max-offset offset)))
|
(offset (max 0 (min max-offset offset)))
|
||||||
(end (min (+ offset content-height) (length lines))))
|
(end (min (+ offset content-height) count)))
|
||||||
(list offset (seq-subseq lines offset end))))))))
|
(list offset
|
||||||
|
(if cells
|
||||||
|
(when (< offset end)
|
||||||
|
(seq-subseq (aref cells offset) 0 (- end offset)))
|
||||||
|
(seq-subseq lines offset end)))))))))
|
||||||
|
|
||||||
(defun ebox--scroll-state-retained-window-ready-p (state)
|
(defun ebox--scroll-state-retained-window-ready-p (state)
|
||||||
"Return non-nil when STATE can publish its cached visible window directly.
|
"Return non-nil when STATE can publish its cached visible window directly.
|
||||||
@ -2047,6 +2119,9 @@ fall back to the complete role scanner."
|
|||||||
(defun ebox--scroll-state-set-lines
|
(defun ebox--scroll-state-set-lines
|
||||||
(state content-lines &optional rendered-lines)
|
(state content-lines &optional rendered-lines)
|
||||||
"Return STATE with CONTENT-LINES and derived span indexes installed."
|
"Return STATE with CONTENT-LINES and derived span indexes installed."
|
||||||
|
(unless (ebox--scroll-rendered-line-cells-current-p
|
||||||
|
(plist-get state :rendered-content-line-cells) rendered-lines)
|
||||||
|
(setq state (plist-put state :rendered-content-line-cells nil)))
|
||||||
(let ((old-lines (plist-get state :content-lines)))
|
(let ((old-lines (plist-get state :content-lines)))
|
||||||
(setq state
|
(setq state
|
||||||
(plist-put
|
(plist-put
|
||||||
@ -3061,23 +3136,117 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(ebox--scroll-state-ensure-prefix-lines
|
(ebox--scroll-state-ensure-prefix-lines
|
||||||
region-id state required-lines t))))
|
region-id state required-lines t))))
|
||||||
|
|
||||||
(defun ebox--surface-scroll-runtime (buffer region-id)
|
(defun ebox--surface-scroll-cached-intent-p (scroll delta)
|
||||||
"Return isolated scroll candidate runtime for BUFFER and REGION-ID."
|
"Return non-nil when SCROLL can consume DELTA without any producer work."
|
||||||
|
(and (not (plist-get scroll :lazy-scroll-prefix-dirty))
|
||||||
|
(not (plist-get scroll :lazy-scroll-window-refresh-required))
|
||||||
|
(not (plist-get scroll :native-reflow-target-prefix-p))
|
||||||
|
(not (and (plist-get scroll :native-reflow-materialize-p)
|
||||||
|
(plist-get scroll :materialize-content-lines)))
|
||||||
|
(or (plist-get scroll :content-lines-complete-p)
|
||||||
|
(and (not (plist-get scroll :render-content-prefix))
|
||||||
|
(not (plist-get scroll :materialize-content-lines)))
|
||||||
|
(and (integerp delta)
|
||||||
|
(<= (+ (max 0 (+ (or (plist-get scroll :scroll-offset) 0) delta))
|
||||||
|
(or (plist-get scroll :content-height) 0))
|
||||||
|
(length (plist-get scroll :content-lines)))))))
|
||||||
|
|
||||||
|
(defun ebox--surface-nested-scroll-cached-p (scroll &optional delta)
|
||||||
|
"Return non-nil when SCROLL can move DELTA without running a producer.
|
||||||
|
Complete caches support the ordinary wrapper proof. An incomplete prefix
|
||||||
|
must cover the exact requested chrome-free window before nodes can be shared."
|
||||||
|
(or (plist-get scroll :content-lines-complete-p)
|
||||||
|
(and (not (plist-get scroll :render-content-prefix))
|
||||||
|
(not (plist-get scroll :materialize-content-lines)))
|
||||||
|
(and (integerp delta)
|
||||||
|
(not (plist-get scroll :native-reflow-materialize-p))
|
||||||
|
(not (plist-get scroll :native-reflow-target-prefix-p))
|
||||||
|
(let* ((offset (max 0 (+ (or (plist-get scroll :scroll-offset) 0) delta)))
|
||||||
|
(height (plist-get scroll :content-height))
|
||||||
|
(candidate (plist-put (copy-sequence scroll) :scroll-offset offset))
|
||||||
|
(visible (ebox--scroll-state-retained-window-ready-p candidate)))
|
||||||
|
(and (integerp height) visible (= (car visible) offset)
|
||||||
|
(= (length (cadr visible)) height)
|
||||||
|
(<= (+ offset height) (length (plist-get scroll :content-lines))))))))
|
||||||
|
|
||||||
|
(defun ebox--surface-nested-scroll-proof
|
||||||
|
(buffer state region-id owner-id &optional delta isolated-producer-p)
|
||||||
|
"Return a retained allocation proof for BUFFER's nested scroll REGION-ID.
|
||||||
|
OWNER-ID names its Box in STATE. Complete cached content keeps intrinsic size
|
||||||
|
unchanged; the publisher still validates every replacement slot and enclosing
|
||||||
|
effect. DELTA additionally permits a covered incomplete prefix.
|
||||||
|
ISOLATED-PRODUCER-P permits a clean prefix miss after the caller isolates the
|
||||||
|
owner subtree and its producer. One enclosing root cache has a separate
|
||||||
|
coordinate proof; deeper scroll chains, layers and fractions keep fallback."
|
||||||
|
(let* ((root (plist-get state :root-node))
|
||||||
|
(table (plist-get state :scroll-state-table))
|
||||||
|
(scroll (and table (gethash region-id table)))
|
||||||
|
(box (and scroll (plist-get scroll :box)))
|
||||||
|
enclosing-region)
|
||||||
|
(when (and owner-id root box
|
||||||
|
(not (equal owner-id (plist-get root :node-id)))
|
||||||
|
(cl-every
|
||||||
|
(lambda (other-region)
|
||||||
|
(or (equal other-region region-id)
|
||||||
|
(when-let* ((other-owner
|
||||||
|
(ebox--buffer-region-render-owner-node-id
|
||||||
|
buffer other-region)))
|
||||||
|
(cond
|
||||||
|
((equal other-owner (plist-get root :node-id))
|
||||||
|
(setq enclosing-region other-region))
|
||||||
|
(t
|
||||||
|
(not (or (ebox--runtime-ancestor-id-p buffer other-owner owner-id)
|
||||||
|
(ebox--runtime-ancestor-id-p buffer owner-id other-owner))))))))
|
||||||
|
(plist-get state :scroll-region-ids))
|
||||||
|
(or (ebox--surface-nested-scroll-cached-p scroll delta)
|
||||||
|
(and isolated-producer-p (integerp delta) (> delta 0)
|
||||||
|
(plist-get scroll :render-content-prefix)
|
||||||
|
(not (plist-get scroll :native-reflow-materialize-p))
|
||||||
|
(not (plist-get scroll :native-reflow-target-prefix-p))))
|
||||||
|
(plist-get scroll :content-lines)
|
||||||
|
(not (plist-get scroll :lazy-scroll-prefix-dirty))
|
||||||
|
(not (plist-get scroll :lazy-scroll-window-refresh-required))
|
||||||
|
(not (plist-get state :fractional-pixel-output-p))
|
||||||
|
(not (if (plist-member state :layered-p)
|
||||||
|
(plist-get state :layered-p)
|
||||||
|
(ebox-layer-active-p root)))
|
||||||
|
(equal (plist-get state :display-signature)
|
||||||
|
(ebox--current-display-signature)))
|
||||||
|
(if enclosing-region
|
||||||
|
;; The incremental preparation validates the owner in its enclosing
|
||||||
|
;; root cache, including owners currently outside the mounted window.
|
||||||
|
(list :owner-id owner-id :nested-scroll-patch-p t
|
||||||
|
:scroll-content-region-id enclosing-region)
|
||||||
|
(when-let* ((snapshot (ebox--ensure-layout-snapshot-details buffer owner-id))
|
||||||
|
((plist-get snapshot :buffer-spans))
|
||||||
|
(width (plist-get
|
||||||
|
(plist-get snapshot :external-footprint-signature)
|
||||||
|
:max-line-pixel-width)))
|
||||||
|
(list :owner-id owner-id :snapshot snapshot :allocated-width width
|
||||||
|
:nested-scroll-patch-p t))))))
|
||||||
|
|
||||||
|
(defun ebox--surface-scroll-runtime (buffer region-id &optional delta)
|
||||||
|
"Return isolated scroll candidate runtime for BUFFER and REGION-ID.
|
||||||
|
DELTA proves cached hits before sharing children or continuation closures.
|
||||||
|
A miss owns its subtree, indexes, cache lines and producers before staging."
|
||||||
(let* ((old-state (ebox--buffer-render-state buffer))
|
(let* ((old-state (ebox--buffer-render-state buffer))
|
||||||
(old-root (plist-get old-state :root-node))
|
(old-root (plist-get old-state :root-node))
|
||||||
|
(old-scroll-state (gethash region-id (plist-get old-state :scroll-state-table)))
|
||||||
|
(cached-intent-p (ebox--surface-scroll-cached-intent-p old-scroll-state delta))
|
||||||
(owner-id (ebox--buffer-region-render-owner-node-id
|
(owner-id (ebox--buffer-region-render-owner-node-id
|
||||||
buffer region-id))
|
buffer region-id))
|
||||||
|
(nested-proof (ebox--surface-nested-scroll-proof
|
||||||
|
buffer old-state region-id owner-id delta (not cached-intent-p)))
|
||||||
;; Scrolling changes one box property and never changes the logical
|
;; Scrolling changes one box property and never changes the logical
|
||||||
;; tree. Copy only that owner path when it is the published root;
|
;; tree. A nested owner additionally keeps a retained allocation
|
||||||
;; nested scroll owners retain the old conservative isolated-copy
|
;; certificate; its cached wrapper never renders shared descendants. The
|
||||||
;; path until their ancestor publication contract is proven. The
|
|
||||||
;; previous implementation copied and indexed the complete gallery
|
;; previous implementation copied and indexed the complete gallery
|
||||||
;; for every wheel event, which made a 512-node grid visibly stall.
|
;; for every wheel event, which made a 512-node grid visibly stall.
|
||||||
(path-root
|
(path-root
|
||||||
(and owner-id old-root
|
(and owner-id old-root
|
||||||
(equal owner-id (plist-get old-root :node-id))
|
(or (equal owner-id (plist-get old-root :node-id)) nested-proof)
|
||||||
(ebox-incremental-surface-region-candidate-root
|
(ebox-incremental-surface-region-candidate-root
|
||||||
buffer region-id)))
|
buffer region-id (not cached-intent-p))))
|
||||||
(root (or path-root
|
(root (or path-root
|
||||||
(ebox-tree-copy-node-structure old-root)))
|
(ebox-tree-copy-node-structure old-root)))
|
||||||
;; All runtime index keys remain stable for a scroll-only path copy.
|
;; All runtime index keys remain stable for a scroll-only path copy.
|
||||||
@ -3087,29 +3256,49 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(copy-hash-table (plist-get old-state :region-box-table)))
|
(copy-hash-table (plist-get old-state :region-box-table)))
|
||||||
(candidate-box (ebox--root-region-box root region-id))
|
(candidate-box (ebox--root-region-box root region-id))
|
||||||
(_ (when candidate-box
|
(_ (when candidate-box
|
||||||
(puthash region-id candidate-box region-box-table)))
|
(puthash region-id candidate-box region-box-table)
|
||||||
|
(when-let* ((enclosing (plist-get nested-proof :scroll-content-region-id)))
|
||||||
|
(puthash enclosing root region-box-table))))
|
||||||
(index
|
(index
|
||||||
|
(if cached-intent-p
|
||||||
(list :region-id-set (plist-get old-state :region-id-set)
|
(list :region-id-set (plist-get old-state :region-id-set)
|
||||||
:region-box-table region-box-table))
|
:region-box-table region-box-table)
|
||||||
|
(plist-get (ebox-incremental--candidate-local-index-delta old-state root) :index)))
|
||||||
|
(_private-registry
|
||||||
|
(setq region-box-table (plist-get index :region-box-table)))
|
||||||
(scroll-table
|
(scroll-table
|
||||||
|
(let ((ebox--render-string-pixel-width-cache
|
||||||
|
(unless cached-intent-p (make-hash-table :test 'equal)))
|
||||||
|
(ebox--render-string-max-pixel-width-cache
|
||||||
|
(unless cached-intent-p (make-hash-table :test 'eq)))
|
||||||
|
(ebox--render-recached-source-node-cache
|
||||||
|
(unless cached-intent-p (make-hash-table :test 'eq))))
|
||||||
(ebox-incremental--candidate-scroll-state-table
|
(ebox-incremental--candidate-scroll-state-table
|
||||||
buffer old-state index region-box-table))
|
buffer old-state index region-box-table)))
|
||||||
(old-scroll-state (ebox--scroll-get-state region-id))
|
|
||||||
(candidate-scroll-state (gethash region-id scroll-table))
|
(candidate-scroll-state (gethash region-id scroll-table))
|
||||||
(state (copy-sequence old-state)))
|
(state (copy-sequence old-state)))
|
||||||
(dolist (key '(:render-content-prefix :materialize-content-lines))
|
;; Even a cache hit publishes a new Box. Keep its rebound continuations:
|
||||||
(when (plist-member old-scroll-state key)
|
;; a later idle prefix or materializer must never return the retired Box.
|
||||||
|
(unless cached-intent-p
|
||||||
|
;; Prefix continuations may extend or rewrite both lists and their
|
||||||
|
;; indexes in place. A new closure must never receive published spines.
|
||||||
|
(let ((ebox--defer-scroll-content-index t))
|
||||||
|
(plist-put candidate-scroll-state :region-line-bounds-index nil)
|
||||||
(setq candidate-scroll-state
|
(setq candidate-scroll-state
|
||||||
(plist-put candidate-scroll-state key
|
(ebox--scroll-state-set-lines
|
||||||
(plist-get old-scroll-state key)))))
|
candidate-scroll-state
|
||||||
|
(mapcar #'copy-sequence (plist-get old-scroll-state :content-lines))
|
||||||
|
(mapcar #'copy-sequence (plist-get old-scroll-state :rendered-content-lines))))))
|
||||||
(puthash region-id candidate-scroll-state scroll-table)
|
(puthash region-id candidate-scroll-state scroll-table)
|
||||||
(setq state (ebox--render-state-install-index state index))
|
(setq state (ebox--render-state-install-index state index))
|
||||||
(plist-put state :root-node root)
|
(plist-put state :root-node root)
|
||||||
(plist-put state :scroll-state-table scroll-table)
|
(plist-put state :scroll-state-table scroll-table)
|
||||||
|
(unless cached-intent-p
|
||||||
|
(ebox-incremental--isolate-candidate-caches state))
|
||||||
(unless (gethash region-id scroll-table)
|
(unless (gethash region-id scroll-table)
|
||||||
(error "Ebox scroll region has no candidate state: %S" region-id))
|
(error "Ebox scroll region has no candidate state: %S" region-id))
|
||||||
(list :root root :state state :scroll-table scroll-table
|
(list :root root :state state :scroll-table scroll-table
|
||||||
:base-state old-state)))
|
:base-state old-state :nested-scroll-patch-proof nested-proof)))
|
||||||
|
|
||||||
(defun ebox--surface-scroll-target (region-id state delta prefix-budget-lines)
|
(defun ebox--surface-scroll-target (region-id state delta prefix-budget-lines)
|
||||||
"Return REGION-ID STATE staged by DELTA within PREFIX-BUDGET-LINES."
|
"Return REGION-ID STATE staged by DELTA within PREFIX-BUDGET-LINES."
|
||||||
@ -3201,9 +3390,9 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(error "Ebox runtime changed during scroll update notification"))
|
(error "Ebox runtime changed during scroll update notification"))
|
||||||
(let* ((staged-scroll-state (copy-sequence state))
|
(let* ((staged-scroll-state (copy-sequence state))
|
||||||
(scroll-patch-fast-p
|
(scroll-patch-fast-p
|
||||||
(and (ebox--scroll-state-retained-window-ready-p
|
(or (and (ebox--scroll-state-retained-window-ready-p
|
||||||
staged-scroll-state)
|
staged-scroll-state) t)
|
||||||
t))
|
(and (plist-get runtime :nested-scroll-patch-proof) t)))
|
||||||
(commit-input
|
(commit-input
|
||||||
(let ((ebox--scroll-global-state table))
|
(let ((ebox--scroll-global-state table))
|
||||||
(ebox-incremental-prepare-scoped-commit
|
(ebox-incremental-prepare-scoped-commit
|
||||||
@ -3232,6 +3421,9 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(list owner-id)))
|
(list owner-id)))
|
||||||
(state-overrides
|
(state-overrides
|
||||||
(let ((value (plist-get commit-input :state-overrides)))
|
(let ((value (plist-get commit-input :state-overrides)))
|
||||||
|
(when (plist-get runtime :nested-scroll-patch-proof)
|
||||||
|
(setq value (plist-put value :nested-scroll-patch-proof
|
||||||
|
(plist-get runtime :nested-scroll-patch-proof))))
|
||||||
(setq value
|
(setq value
|
||||||
(plist-put
|
(plist-put
|
||||||
value :preserve-scroll-producer-region-ids
|
value :preserve-scroll-producer-region-ids
|
||||||
@ -3270,6 +3462,7 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(table (plist-get overrides :scroll-state-table))
|
(table (plist-get overrides :scroll-state-table))
|
||||||
(state (gethash region-id table)))
|
(state (gethash region-id table)))
|
||||||
(dolist (key '(:content-lines :rendered-content-lines
|
(dolist (key '(:content-lines :rendered-content-lines
|
||||||
|
:rendered-content-line-cells
|
||||||
:content-lines-complete-p :render-content-prefix
|
:content-lines-complete-p :render-content-prefix
|
||||||
:materialize-content-lines :content-region-id-set
|
:materialize-content-lines :content-region-id-set
|
||||||
:region-line-bounds-index :region-line-span-index
|
:region-line-bounds-index :region-line-span-index
|
||||||
@ -3290,11 +3483,12 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(defun ebox--surface-scroll-region-by
|
(defun ebox--surface-scroll-region-by
|
||||||
(buffer region-id delta prefix-budget-lines)
|
(buffer region-id delta prefix-budget-lines)
|
||||||
"Scroll mounted BUFFER REGION-ID by DELTA within PREFIX-BUDGET-LINES."
|
"Scroll mounted BUFFER REGION-ID by DELTA within PREFIX-BUDGET-LINES."
|
||||||
(let* ((runtime (ebox--surface-scroll-runtime buffer region-id))
|
(let* ((runtime (ebox--surface-scroll-runtime buffer region-id delta))
|
||||||
(table (plist-get runtime :scroll-table))
|
(table (plist-get runtime :scroll-table))
|
||||||
(candidate-state (plist-get runtime :state))
|
(candidate-state (plist-get runtime :state))
|
||||||
(transition
|
(transition
|
||||||
(let ((ebox--scroll-global-state table)
|
(let ((ebox--scroll-global-state table)
|
||||||
|
(ebox--region-box-table (plist-get candidate-state :region-box-table))
|
||||||
(ebox--scroll-idle-prefetch-timers
|
(ebox--scroll-idle-prefetch-timers
|
||||||
(make-hash-table :test 'equal))
|
(make-hash-table :test 'equal))
|
||||||
(ebox--smooth-scroll-state-table
|
(ebox--smooth-scroll-state-table
|
||||||
|
|||||||
@ -455,7 +455,7 @@
|
|||||||
(should (zerop root-renders)))))))
|
(should (zerop root-renders)))))))
|
||||||
|
|
||||||
(ert-deftest ebox-commit-inherited-text-exclusions-render-current-properties ()
|
(ert-deftest ebox-commit-inherited-text-exclusions-render-current-properties ()
|
||||||
"Property, extent, display and identity changes retain the exact fallback."
|
"Unsafe property changes fall back; proven source rebinding keeps current facts."
|
||||||
(dolist (change '(property char-length display identity opaque-callback))
|
(dolist (change '(property char-length display identity opaque-callback))
|
||||||
(ert-info ((format "inherited text exclusion: %S" change))
|
(ert-info ((format "inherited text exclusion: %S" change))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
@ -492,7 +492,12 @@
|
|||||||
(when (eq change 'identity)
|
(when (eq change 'identity)
|
||||||
(should-not (ebox--host-ref-node (current-buffer) 'counter))
|
(should-not (ebox--host-ref-node (current-buffer) 'counter))
|
||||||
(should (ebox--host-ref-node (current-buffer) 'new-counter)))
|
(should (ebox--host-ref-node (current-buffer) 'new-counter)))
|
||||||
(should (> (cdr result) 0)))))))))
|
;; A source handle can change while the runtime slot, author key
|
||||||
|
;; and full rendered properties remain proven. The assertions
|
||||||
|
;; above also require replacing its public host lookup identity.
|
||||||
|
(if (eq change 'identity)
|
||||||
|
(should (zerop (cdr result)))
|
||||||
|
(should (> (cdr result) 0))))))))))
|
||||||
|
|
||||||
(ert-deftest ebox-commit-inherited-text-rolls-back-properties-and-retries ()
|
(ert-deftest ebox-commit-inherited-text-rolls-back-properties-and-retries ()
|
||||||
"Late publication rejection retains text, metadata and one old generation."
|
"Late publication rejection retains text, metadata and one old generation."
|
||||||
@ -1833,7 +1838,7 @@
|
|||||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-commit-expands-scope-for-length-changing-column-content ()
|
(ert-deftest ebox-commit-expands-scope-for-length-changing-column-content ()
|
||||||
"A column content growth must publish shifted later styled siblings."
|
"An intrinsically sized column grows with content and later styled siblings."
|
||||||
(let* ((buffer (generate-new-buffer-name " *ebox-commit-column-scope*"))
|
(let* ((buffer (generate-new-buffer-name " *ebox-commit-column-scope*"))
|
||||||
(old-root
|
(old-root
|
||||||
(ebox-test-column
|
(ebox-test-column
|
||||||
@ -1866,7 +1871,8 @@
|
|||||||
(with-current-buffer buffer (buffer-string))))
|
(with-current-buffer buffer (buffer-string))))
|
||||||
(should (eq (plist-get report :strategy) 'owner-rerender))
|
(should (eq (plist-get report :strategy) 'owner-rerender))
|
||||||
(should (equal (plist-get report :patch-ops) '(owner-rerender)))
|
(should (equal (plist-get report :patch-ops) '(owner-rerender)))
|
||||||
(should-not (plist-get report :tp-scope-fallback)))
|
(should-not (plist-get report :tp-scope-fallback))
|
||||||
|
(ebox-commit-test--assert-allocated-output (get-buffer buffer) new-root))
|
||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
@ -2314,6 +2320,8 @@ remain retained identities."
|
|||||||
;; publishes one scoped transaction, with one exact text operation
|
;; publishes one scoped transaction, with one exact text operation
|
||||||
;; per changed slot instead of replacing the unchanged gap.
|
;; per changed slot instead of replacing the unchanged gap.
|
||||||
(should (= 2 (plist-get report :tp-text-operations)))
|
(should (= 2 (plist-get report :tp-text-operations)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(ebox-commit-test--assert-full-render-equivalent))
|
||||||
(should (string-match-p "L"
|
(should (string-match-p "L"
|
||||||
(ebox-commit-test--buffer-string buffer)))
|
(ebox-commit-test--buffer-string buffer)))
|
||||||
(setq fresh
|
(setq fresh
|
||||||
@ -2383,10 +2391,61 @@ remain retained identities."
|
|||||||
(should (memq (plist-get report :projection-kind)
|
(should (memq (plist-get report :projection-kind)
|
||||||
'(span-patch owner-scoped)))
|
'(span-patch owner-scoped)))
|
||||||
(should-not (plist-get report :tp-full-root))
|
(should-not (plist-get report :tp-full-root))
|
||||||
(should-not (plist-get report :tp-scope-fallback)))))
|
(should-not (plist-get report :tp-scope-fallback))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(ebox-commit-test--assert-full-render-equivalent)))))
|
||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))))
|
(kill-buffer buffer)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-variable-grid-retains-ancestor-and-caller-properties ()
|
||||||
|
"Anonymous Text replacements keep proven properties and new opaque values."
|
||||||
|
(let* ((callback (lambda () 'original))
|
||||||
|
(replacement-callback (lambda () 'replacement))
|
||||||
|
(payload (make-symbol "caller-payload"))
|
||||||
|
(properties (list 'help-echo callback 'caller-payload payload)))
|
||||||
|
(cl-labels
|
||||||
|
((child (key value action)
|
||||||
|
(ebox-test-box
|
||||||
|
:key key :source-identity key
|
||||||
|
(ebox-test-text (propertize value 'face '(:weight bold)
|
||||||
|
'action action)))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-test-grid
|
||||||
|
:key 'grid :width '(80) :grid-template-columns '((36) (36))
|
||||||
|
:column-gap '(8) :bgcolor "#102030" :surface-properties properties
|
||||||
|
(child 'left "left-old" callback)
|
||||||
|
(child 'right "right-old" callback)))
|
||||||
|
(let ((candidate (ebox-candidate-begin (current-buffer))))
|
||||||
|
(ebox-candidate-replace-host-ref candidate 'left (child 'left "L" callback))
|
||||||
|
(ebox-candidate-replace-host-ref candidate 'right (child 'right "R" callback))
|
||||||
|
(let* ((observed
|
||||||
|
(ebox-commit-test--count-root-renders
|
||||||
|
(lambda () (ebox-commit (current-buffer) candidate))))
|
||||||
|
(report (car observed)))
|
||||||
|
(should (zerop (cdr observed)))
|
||||||
|
(should (memq (plist-get report :projection-kind)
|
||||||
|
'(span-patch owner-scoped)))))
|
||||||
|
(ebox-commit-test--assert-full-render-equivalent)
|
||||||
|
(dolist (value '("L" "R"))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward value)
|
||||||
|
(should (eq callback (get-text-property (1- (point)) 'action)))
|
||||||
|
(should (eq callback (get-text-property (1- (point)) 'help-echo)))
|
||||||
|
(should (eq payload (get-text-property (1- (point)) 'caller-payload))))
|
||||||
|
;; A new opaque value must survive any declined local property proof.
|
||||||
|
(let ((candidate (ebox-candidate-begin (current-buffer))))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'left (child 'left "NEW" replacement-callback))
|
||||||
|
(ebox-commit (current-buffer) candidate))
|
||||||
|
(ebox-commit-test--assert-full-render-equivalent)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward "NEW")
|
||||||
|
(should (eq replacement-callback
|
||||||
|
(get-text-property (1- (point)) 'action)))
|
||||||
|
(should (eq callback (get-text-property (1- (point)) 'help-echo)))))))
|
||||||
|
|
||||||
(ert-deftest ebox-commit-updates-selector-type-counts ()
|
(ert-deftest ebox-commit-updates-selector-type-counts ()
|
||||||
"A structural candidate should publish exact author selector-type counts."
|
"A structural candidate should publish exact author selector-type counts."
|
||||||
(let* ((buffer
|
(let* ((buffer
|
||||||
@ -2896,7 +2955,7 @@ Normalize only buffer-local region identifiers; retain every text property."
|
|||||||
buffer (ebox-commit-test--allocated-single-root content color nil sibling-content)))
|
buffer (ebox-commit-test--allocated-single-root content color nil sibling-content)))
|
||||||
|
|
||||||
(defun ebox-commit-test--allocated-single-round-trip (paint-p)
|
(defun ebox-commit-test--allocated-single-round-trip (paint-p)
|
||||||
"One longer/shorter row promotes its allocated owner, with disjoint paint."
|
"One longer/shorter row retains its smallest proven allocation and scroll."
|
||||||
(ert-info ((format "allocated single with disjoint paint: %S" paint-p))
|
(ert-info ((format "allocated single with disjoint paint: %S" paint-p))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
|
|||||||
@ -144,6 +144,26 @@
|
|||||||
(should (equal-including-properties before source))
|
(should (equal-including-properties before source))
|
||||||
(should (equal (plist-get (cdr display) :width) '(15.5))))))
|
(should (equal (plist-get (cdr display) :width) '(15.5))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-canonicalizes-whole-pixel-cuts ()
|
||||||
|
"Equal cuts share display properties regardless of numeric allocation type."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((height (list 20))
|
||||||
|
(display (list 'space :width '(15.5) :height height))
|
||||||
|
(source (propertize "abc" 'display display))
|
||||||
|
(integer-cut (ebox-composite-slice source 2 13))
|
||||||
|
(float-cut (ebox-composite-slice source 2.0 13.0))
|
||||||
|
(fractional-cut (ebox-composite-slice source 2.0 13.5)))
|
||||||
|
(should (equal-including-properties integer-cut float-cut))
|
||||||
|
(should (equal (plist-get (cdr (get-text-property 0 'display float-cut))
|
||||||
|
:width)
|
||||||
|
'(11)))
|
||||||
|
(should (eq (plist-get (cdr (get-text-property 0 'display float-cut))
|
||||||
|
:height)
|
||||||
|
height))
|
||||||
|
(ebox-composite-test--assert-width fractional-cut 11.5)
|
||||||
|
(should (eq (get-text-property 0 'display source) display))
|
||||||
|
(should (equal (plist-get (cdr display) :width) '(15.5))))))
|
||||||
|
|
||||||
(ert-deftest ebox-composite-slice-never-splits-replacement-runs ()
|
(ert-deftest ebox-composite-slice-never-splits-replacement-runs ()
|
||||||
"Shared string and image replacements are kept whole, even across faces."
|
"Shared string and image replacements are kept whole, even across faces."
|
||||||
(ebox-composite-test--with-metrics
|
(ebox-composite-test--with-metrics
|
||||||
|
|||||||
@ -367,6 +367,24 @@
|
|||||||
"Return first content face in STRING."
|
"Return first content face in STRING."
|
||||||
(ebox-test--face-at-property string 'ebox-content))
|
(ebox-test--face-at-property string 'ebox-content))
|
||||||
|
|
||||||
|
(cl-defmacro ebox-test--with-scroll-producer-counts
|
||||||
|
((prefix-count &optional materialize-count requested-lines) &body body)
|
||||||
|
"Run BODY observing source calls across candidate producer rebinding.
|
||||||
|
Increment PREFIX-COUNT for bounded rendering, optional MATERIALIZE-COUNT for
|
||||||
|
full rendering, and store the latest bounded limit in REQUESTED-LINES."
|
||||||
|
(declare (indent 1))
|
||||||
|
(let ((original (make-symbol "original-scroll-source")))
|
||||||
|
`(let ((,original (symbol-function 'ebox--render-scroll-window-source)))
|
||||||
|
(cl-letf (((symbol-function 'ebox--render-scroll-window-source)
|
||||||
|
(lambda (source limit &optional cache)
|
||||||
|
(if limit
|
||||||
|
(progn
|
||||||
|
(cl-incf ,prefix-count)
|
||||||
|
,@(when requested-lines `((setq ,requested-lines limit))))
|
||||||
|
,@(when materialize-count `((cl-incf ,materialize-count))))
|
||||||
|
(funcall ,original source limit cache))))
|
||||||
|
,@body))))
|
||||||
|
|
||||||
(defmacro ebox-test--count-full-surface-updates (&rest body)
|
(defmacro ebox-test--count-full-surface-updates (&rest body)
|
||||||
"Run BODY and return the number of full TP surface updates it triggered."
|
"Run BODY and return the number of full TP surface updates it triggered."
|
||||||
(declare (indent 0))
|
(declare (indent 0))
|
||||||
@ -4999,6 +5017,201 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(should (eq (plist-get flush-report :strategy)
|
(should (eq (plist-get flush-report :strategy)
|
||||||
'paint-patch)))))))
|
'paint-patch)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-coalesces-ordinary-regions-in-target-buffer ()
|
||||||
|
"A callback should queue every region update and publish once in BUFFER."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(column :width (px 180)
|
||||||
|
(box :id first "First")
|
||||||
|
(box :id second "Second")))
|
||||||
|
(let ((target (current-buffer))
|
||||||
|
(before (buffer-string))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(publisher (symbol-function 'ebox--publish-surface-region-batch))
|
||||||
|
(result (list 'callback-result))
|
||||||
|
(publications 0))
|
||||||
|
(cl-letf (((symbol-function 'ebox--publish-surface-region-batch)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf publications)
|
||||||
|
(apply publisher arguments))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(let ((caller (current-buffer)))
|
||||||
|
(should
|
||||||
|
(eq result
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(should (eq (current-buffer) target))
|
||||||
|
(ebox-region-update 'first :content "First changed")
|
||||||
|
(ebox-region-update 'second :color "#112233")
|
||||||
|
(ebox-region-update 'first :background-color "#445566")
|
||||||
|
(ebox-region-update 'second :content "Second changed")
|
||||||
|
(should (= (ebox-incremental-pending-count target) 4))
|
||||||
|
(should (= publications 0))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision target)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
result)
|
||||||
|
target)))
|
||||||
|
(should (eq caller (current-buffer))))))
|
||||||
|
(should (= publications 1))
|
||||||
|
(should (= (1+ revision) (ebox-surface-buffer-revision target)))
|
||||||
|
(should (= (plist-get (ebox-buffer-update-report target) :batched-count) 4))
|
||||||
|
(should-not (gethash target ebox-incremental--batch-table))
|
||||||
|
(should (string-match-p "First changed" (buffer-string)))
|
||||||
|
(should (string-match-p "Second changed" (buffer-string)))
|
||||||
|
(should (ebox-test--buffer-visually-matches-runtime-render-p)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-empty-callback-preserves-prior-state ()
|
||||||
|
"An empty or unchanged callback should not flush or replace prior reports."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(box :id target :color "#112233" "Stable"))
|
||||||
|
(let ((buffer (current-buffer)))
|
||||||
|
(ebox-incremental-begin-batch buffer)
|
||||||
|
(ebox-incremental-flush buffer)
|
||||||
|
(let ((state (ebox-incremental--batch-state buffer))
|
||||||
|
(report (ebox-buffer-update-report buffer))
|
||||||
|
(revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-incremental-flush)
|
||||||
|
(lambda (_) (ert-fail "Empty callback must not flush"))))
|
||||||
|
(should (eq 'empty (ebox-call-with-update-batch (lambda () 'empty))))
|
||||||
|
(should-not
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda () (ebox-region-update 'target :color "#112233")))))
|
||||||
|
(should (eq state (ebox-incremental--batch-state buffer)))
|
||||||
|
(should (equal report (ebox-buffer-update-report buffer)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision buffer)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-nested-success-flushes-only-outer-callback ()
|
||||||
|
"Nested callbacks should share one accumulated candidate and publication."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(column :width (px 180)
|
||||||
|
(box :id first "First")
|
||||||
|
(box :id second "Second")))
|
||||||
|
(let ((buffer (current-buffer))
|
||||||
|
(publisher (symbol-function 'ebox--publish-surface-region-batch))
|
||||||
|
(publications 0))
|
||||||
|
(cl-letf (((symbol-function 'ebox--publish-surface-region-batch)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf publications)
|
||||||
|
(apply publisher arguments))))
|
||||||
|
(should
|
||||||
|
(eq 'outer
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update 'first :content "Outer")
|
||||||
|
(should
|
||||||
|
(eq 'inner
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update 'second :content "Inner")
|
||||||
|
'inner))))
|
||||||
|
(should (ebox-incremental-batching-p buffer))
|
||||||
|
(should (= (ebox-incremental-pending-count buffer) 2))
|
||||||
|
(should (= publications 0))
|
||||||
|
(ebox-region-update 'first :color "#112233")
|
||||||
|
'outer)))))
|
||||||
|
(should (= publications 1))
|
||||||
|
(should-not (ebox-incremental-batching-p buffer))
|
||||||
|
(should (string-match-p "Outer" (buffer-string)))
|
||||||
|
(should (string-match-p "Inner" (buffer-string)))
|
||||||
|
(should (ebox-test--buffer-visually-matches-runtime-render-p)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-joins-explicit-batch-without-flushing ()
|
||||||
|
"A callback inside legacy begin/flush should leave publication to its caller."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(box :id target "Original"))
|
||||||
|
(let ((buffer (current-buffer))
|
||||||
|
(before (buffer-string)))
|
||||||
|
(ebox-incremental-begin-batch buffer)
|
||||||
|
(ebox-region-update 'target :color "#112233")
|
||||||
|
(cl-letf (((symbol-function 'ebox-incremental-flush)
|
||||||
|
(lambda (_) (ert-fail "Joined callback must not flush"))))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda () (ebox-region-update 'target :content "Joined"))))
|
||||||
|
(should (ebox-incremental-batching-p buffer))
|
||||||
|
(should (= (ebox-incremental-pending-count buffer) 2))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(ebox-incremental-flush buffer)
|
||||||
|
(should (string-match-p "Joined" (buffer-string)))
|
||||||
|
(should (ebox-test--buffer-visually-matches-runtime-render-p)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-nonlocal-exits-restore-owned-or-outer-state ()
|
||||||
|
"Errors, quits and throws should discard only the callback's queued changes."
|
||||||
|
(dolist (outer '(nil t))
|
||||||
|
(dolist (exit-kind '(error quit throw))
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(column :width (px 180)
|
||||||
|
(box :id first "First")
|
||||||
|
(box :id second "Second")))
|
||||||
|
(let* ((buffer (current-buffer))
|
||||||
|
(before (buffer-string))
|
||||||
|
(runtime (ebox--buffer-render-state buffer))
|
||||||
|
(revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(when outer
|
||||||
|
(ebox-incremental-begin-batch buffer)
|
||||||
|
(ebox-region-update 'first :content "Outer retained"))
|
||||||
|
(let ((checkpoint (ebox-incremental--batch-state buffer)))
|
||||||
|
(should
|
||||||
|
(eq 'aborted
|
||||||
|
(catch 'batch-abort
|
||||||
|
(condition-case nil
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update 'first :content "Rejected first")
|
||||||
|
(ebox-region-update 'second :content "Rejected second")
|
||||||
|
(pcase exit-kind
|
||||||
|
('error (error "Abort callback"))
|
||||||
|
('quit (signal 'quit nil))
|
||||||
|
('throw (throw 'batch-abort 'aborted)))))
|
||||||
|
((error quit) 'aborted)))))
|
||||||
|
(should (eq checkpoint (ebox-incremental--batch-state buffer))))
|
||||||
|
(should (eq runtime (ebox--buffer-render-state buffer)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (= (ebox-incremental-pending-count buffer) (if outer 1 0)))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda () (ebox-region-update 'second :content "Next callback")))
|
||||||
|
(when outer (ebox-incremental-flush buffer))
|
||||||
|
(should (string-match-p (if outer "Outer retained" "First")
|
||||||
|
(buffer-string)))
|
||||||
|
(should (string-match-p "Next callback" (buffer-string)))
|
||||||
|
(should-not (string-match-p "Rejected" (buffer-string)))
|
||||||
|
(should (ebox-test--buffer-visually-matches-runtime-render-p)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-update-batch-flush-failure-restores-committed-generation ()
|
||||||
|
"A failed TP publication should abort the owned batch and allow the next call."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build '(column :width (px 180)
|
||||||
|
(box :id first "First")
|
||||||
|
(box :id second "Second")))
|
||||||
|
(let* ((buffer (current-buffer))
|
||||||
|
(before (buffer-string))
|
||||||
|
(runtime (ebox--buffer-render-state buffer))
|
||||||
|
(root (ebox-test--runtime-root buffer))
|
||||||
|
(revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(cl-letf (((symbol-function 'accept-change-group)
|
||||||
|
(lambda (_) (error "Reject batch publication"))))
|
||||||
|
(should-error
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update 'first :content "Rejected first")
|
||||||
|
(ebox-region-update 'second :color "#112233")))))
|
||||||
|
(should-not (gethash buffer ebox-incremental--batch-table))
|
||||||
|
(should (eq runtime (ebox--buffer-render-state buffer)))
|
||||||
|
(should (eq root (ebox-test--runtime-root buffer)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda () (ebox-region-update 'second :content "Next callback")))
|
||||||
|
(should (string-match-p "First" (buffer-string)))
|
||||||
|
(should (string-match-p "Next callback" (buffer-string)))
|
||||||
|
(should-not (string-match-p "Rejected" (buffer-string)))
|
||||||
|
(should (ebox-test--buffer-visually-matches-runtime-render-p)))))
|
||||||
|
|
||||||
(ert-deftest ebox-batch-flush-stores-report-on-flushed-buffer-only ()
|
(ert-deftest ebox-batch-flush-stores-report-on-flushed-buffer-only ()
|
||||||
"Empty and non-empty flushes should update only the target buffer report."
|
"Empty and non-empty flushes should update only the target buffer report."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
@ -7797,17 +8010,13 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
|
(ebox-test--with-scroll-producer-counts (prefix-count)
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(let* ((state (ebox--scroll-get-state root-id))
|
||||||
(render-prefix (plist-get state :render-content-prefix))
|
|
||||||
(initial-lines (length (plist-get state :content-lines)))
|
(initial-lines (length (plist-get state :content-lines)))
|
||||||
(content-height (plist-get state :content-height))
|
(content-height (plist-get state :content-height))
|
||||||
(boundary-offset (- initial-lines content-height)))
|
(boundary-offset (- initial-lines content-height)))
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state)
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (= boundary-offset
|
(should (= boundary-offset
|
||||||
(ebox--scroll-region-by root-id
|
(ebox--scroll-region-by root-id
|
||||||
@ -7827,7 +8036,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(plist-get (ebox--buffer-render-state buffer)
|
(plist-get (ebox--buffer-render-state buffer)
|
||||||
:surface-fragments)
|
:surface-fragments)
|
||||||
:scroll-lines))))))
|
:scroll-lines))))))
|
||||||
(should (= prefix-count 1))))
|
(should (= prefix-count 1)))))
|
||||||
(when root-id
|
(when root-id
|
||||||
(ebox--smooth-scroll-stop root-id))
|
(ebox--smooth-scroll-stop root-id))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -7861,23 +8070,19 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
|
(ebox-test--with-scroll-producer-counts (prefix-count)
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(let* ((state (ebox--scroll-get-state root-id))
|
||||||
(render-prefix (plist-get state :render-content-prefix))
|
|
||||||
(initial-lines (length (plist-get state :content-lines)))
|
(initial-lines (length (plist-get state :content-lines)))
|
||||||
(content-height (plist-get state :content-height))
|
(content-height (plist-get state :content-height))
|
||||||
(boundary-offset (- initial-lines content-height)))
|
(boundary-offset (- initial-lines content-height)))
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state)
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (= boundary-offset
|
(should (= boundary-offset
|
||||||
(ebox--scroll-region-by root-id
|
(ebox--scroll-region-by root-id
|
||||||
boundary-offset)))
|
boundary-offset)))
|
||||||
(should (= 4 (ebox--scroll-region-by root-id 4 4)))))
|
(should (= 4 (ebox--scroll-region-by root-id 4 4)))))
|
||||||
(should (= prefix-count 1))))
|
(should (= prefix-count 1)))))
|
||||||
(when root-id
|
(when root-id
|
||||||
(ebox--smooth-scroll-stop root-id))
|
(ebox--smooth-scroll-stop root-id))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -9238,21 +9443,9 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(ebox-test--with-scroll-producer-counts (prefix-count materialize-count)
|
||||||
(render-prefix (plist-get state :render-content-prefix))
|
(let ((state (ebox--scroll-get-state root-id)))
|
||||||
(materialize (plist-get state :materialize-content-lines)))
|
(should (= (length (plist-get state :content-lines)) 3)))
|
||||||
(should (= (length (plist-get state :content-lines)) 3))
|
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(plist-put
|
|
||||||
state :materialize-content-lines
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf materialize-count)
|
|
||||||
(apply materialize args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (ebox--smooth-scroll-region-by root-id 16)))
|
(should (ebox--smooth-scroll-region-by root-id 16)))
|
||||||
(should (= prefix-count 1))
|
(should (= prefix-count 1))
|
||||||
@ -9263,7 +9456,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
||||||
(should entry)
|
(should entry)
|
||||||
(should (> (or (plist-get entry :pending) 0) 0))
|
(should (> (or (plist-get entry :pending) 0) 0))
|
||||||
(should-not (plist-get entry :waiting-prefetch))))
|
(should-not (plist-get entry :waiting-prefetch)))))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-cancel-idle-prefetch root-id)
|
(ebox--scroll-cancel-idle-prefetch root-id)
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -9301,21 +9494,9 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(ebox-test--with-scroll-producer-counts (prefix-count materialize-count)
|
||||||
(render-prefix (plist-get state :render-content-prefix))
|
(let ((state (ebox--scroll-get-state root-id)))
|
||||||
(materialize (plist-get state :materialize-content-lines)))
|
(should (= (length (plist-get state :content-lines)) 3)))
|
||||||
(should (= (length (plist-get state :content-lines)) 3))
|
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(plist-put
|
|
||||||
state :materialize-content-lines
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf materialize-count)
|
|
||||||
(apply materialize args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (ebox--smooth-scroll-region-by root-id 16))
|
(should (ebox--smooth-scroll-region-by root-id 16))
|
||||||
(ebox--smooth-scroll-tick root-id))
|
(ebox--smooth-scroll-tick root-id))
|
||||||
@ -9326,7 +9507,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
1))
|
1))
|
||||||
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
||||||
(should entry)
|
(should entry)
|
||||||
(should-not (plist-get entry :waiting-prefetch))))
|
(should-not (plist-get entry :waiting-prefetch)))))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-cancel-idle-prefetch root-id)
|
(ebox--scroll-cancel-idle-prefetch root-id)
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -9365,16 +9546,9 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(ebox-test--with-scroll-producer-counts (prefix-count nil requested-lines)
|
||||||
(render-prefix (plist-get state :render-content-prefix)))
|
(let ((state (ebox--scroll-get-state root-id)))
|
||||||
(should (= (length (plist-get state :content-lines)) 3))
|
(should (= (length (plist-get state :content-lines)) 3)))
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (state region-id required-lines)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(setq requested-lines required-lines)
|
|
||||||
(funcall render-prefix state region-id required-lines)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (ebox--smooth-scroll-region-by root-id 1000)))
|
(should (ebox--smooth-scroll-region-by root-id 1000)))
|
||||||
(should (= prefix-count 1))
|
(should (= prefix-count 1))
|
||||||
@ -9384,7 +9558,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
ebox-wheel-smooth-scroll-lines-per-tick))
|
ebox-wheel-smooth-scroll-lines-per-tick))
|
||||||
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
||||||
(should entry)
|
(should entry)
|
||||||
(should-not (plist-get entry :waiting-prefetch))))
|
(should-not (plist-get entry :waiting-prefetch)))))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-cancel-idle-prefetch root-id)
|
(ebox--scroll-cancel-idle-prefetch root-id)
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -9421,14 +9595,8 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(ebox-test--with-scroll-producer-counts (prefix-count)
|
||||||
(render-prefix (plist-get state :render-content-prefix)))
|
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (ebox--smooth-scroll-region-by root-id 16)))
|
(should (ebox--smooth-scroll-region-by root-id 16)))
|
||||||
(should (gethash root-id ebox--smooth-scroll-state-table))
|
(should (gethash root-id ebox--smooth-scroll-state-table))
|
||||||
@ -9439,7 +9607,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(should (gethash root-id ebox--scroll-idle-prefetch-timers))
|
(should (gethash root-id ebox--scroll-idle-prefetch-timers))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-idle-prefetch root-id)
|
(ebox--scroll-idle-prefetch root-id)
|
||||||
(should (> prefix-count prefix-after-start))))
|
(should (> prefix-count prefix-after-start)))))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-cancel-idle-prefetch root-id)
|
(ebox--scroll-cancel-idle-prefetch root-id)
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -9476,14 +9644,8 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(let* ((state (ebox--scroll-get-state root-id))
|
(ebox-test--with-scroll-producer-counts (prefix-count)
|
||||||
(render-prefix (plist-get state :render-content-prefix)))
|
|
||||||
(plist-put
|
|
||||||
state :render-content-prefix
|
|
||||||
(lambda (&rest args)
|
|
||||||
(cl-incf prefix-count)
|
|
||||||
(apply render-prefix args)))
|
|
||||||
(puthash root-id state ebox--scroll-global-state))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (ebox--smooth-scroll-region-by root-id 16)))
|
(should (ebox--smooth-scroll-region-by root-id 16)))
|
||||||
(should (> prefix-count 0))
|
(should (> prefix-count 0))
|
||||||
@ -9496,7 +9658,7 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
(let ((entry (gethash root-id ebox--smooth-scroll-state-table)))
|
||||||
(should entry)
|
(should entry)
|
||||||
(should-not (plist-get entry :waiting-prefetch))
|
(should-not (plist-get entry :waiting-prefetch))
|
||||||
(should (timerp (plist-get entry :timer)))))
|
(should (timerp (plist-get entry :timer))))))
|
||||||
(ebox--smooth-scroll-stop root-id)
|
(ebox--smooth-scroll-stop root-id)
|
||||||
(ebox--scroll-cancel-idle-prefetch root-id)
|
(ebox--scroll-cancel-idle-prefetch root-id)
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -14224,4 +14386,105 @@ face patch must match that exactly (issue014)."
|
|||||||
(should-not (gethash 999 actual))
|
(should-not (gethash 999 actual))
|
||||||
(should (= calls 36))))
|
(should (= calls 36))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-content-box-owner-keeps-one-line-updates-local ()
|
||||||
|
"Canonical Text changes use their allocated Box, alone or beside a layer."
|
||||||
|
(dolist (with-layer '(nil t))
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((ebox-viewport-width 24) (ebox-viewport-height 8))
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build
|
||||||
|
'(column :width (vw 100) :padding-inline (ch 2)
|
||||||
|
(box :id host :width (ch 8) :height (lh 2)
|
||||||
|
(box :id card :position absolute :width (ch 4)
|
||||||
|
:height (lh 1) "CARD"))
|
||||||
|
(box :id status :padding-inline (ch 1)
|
||||||
|
:background-color "#112233" "Idle")
|
||||||
|
(box "FOLLOWING")))
|
||||||
|
(dolist (value '("Expanded" "ok"))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "One-line content update rendered the root"))))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(when with-layer
|
||||||
|
(ebox-region-update 'card :left
|
||||||
|
(if (equal value "Expanded") '(ch 1) '(ch 0))))
|
||||||
|
(ebox-region-update 'status :content value))))
|
||||||
|
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))
|
||||||
|
;; Growth that wraps must still take a wider proof/fallback.
|
||||||
|
(ebox-region-update 'status :content "This content needs several rows")
|
||||||
|
(should (ebox-test--buffer-propertized-matches-runtime-render-p))
|
||||||
|
(should (string-match-p "FOLLOWING" (buffer-string))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-content-box-owner-keeps-disjoint-scroll-cache ()
|
||||||
|
"An allocated Box keeps unrelated scroll lines during growth and shrink."
|
||||||
|
(dolist (with-layer '(nil t))
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((ebox-viewport-width 24) (ebox-viewport-height 10))
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build
|
||||||
|
'(column :width (vw 100) :padding-inline (ch 2)
|
||||||
|
:background-color "#101117" :color "#DCE0EC"
|
||||||
|
(box :id host :width (ch 8) :height (lh 2)
|
||||||
|
(box :id card :position absolute :width (ch 4)
|
||||||
|
:height (lh 1) "CARD"))
|
||||||
|
(box :id status :padding-inline (ch 1)
|
||||||
|
:margin-inline (ch 1) :box-sizing content-box
|
||||||
|
:background-color "#112233" "Idle")
|
||||||
|
(column :id scroller :height (lh 2) :overflow scroll
|
||||||
|
(box "A") (box "B") (box "C") (box "D"))
|
||||||
|
(box "FOLLOWING")))
|
||||||
|
(let* ((scroll-id (ebox-test--selector-region-id
|
||||||
|
(current-buffer) "#scroller"))
|
||||||
|
(lines (plist-get (gethash scroll-id ebox--scroll-global-state)
|
||||||
|
:content-lines)))
|
||||||
|
(should lines)
|
||||||
|
(dolist (value '("Expanded" "ok"))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Disjoint scroll forced a root render"))))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(when with-layer
|
||||||
|
(ebox-region-update
|
||||||
|
'card :left (if (equal value "Expanded") '(ch 1) '(ch 0))))
|
||||||
|
(ebox-region-update 'status :content value))))
|
||||||
|
(should (eq lines
|
||||||
|
(plist-get (gethash scroll-id ebox--scroll-global-state)
|
||||||
|
:content-lines)))
|
||||||
|
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-content-box-owner-retains-painted-ancestors-in-batch ()
|
||||||
|
"Disjoint Box and layer updates keep their shared ancestor's properties."
|
||||||
|
(ebox-test--reset-runtime-state)
|
||||||
|
(let ((ebox-viewport-width 24) (ebox-viewport-height 8))
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-test--with-rendered-buffer
|
||||||
|
(ebox-build
|
||||||
|
'(column :width (vw 100) :padding-inline (ch 2)
|
||||||
|
:background-color "#101117" :color "#DCE0EC"
|
||||||
|
(box :id host :width (ch 8) :height (lh 2)
|
||||||
|
(box :id card :position absolute :width (ch 4)
|
||||||
|
:height (lh 1) "CARD"))
|
||||||
|
(box :id status :padding-inline (ch 1)
|
||||||
|
:background-color "#112233" "Idle")
|
||||||
|
(box "FOLLOWING")))
|
||||||
|
;; Equal-length transport must keep paint provenance for a later
|
||||||
|
;; variable-length update after another owner rebuilds the ledger.
|
||||||
|
(dolist (value '("Seed" "Expanded" "ok"))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Painted multi-owner update rendered the root"))))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(ebox-region-update 'card :left
|
||||||
|
(if (equal value "ok") '(ch 0) '(ch 1)))
|
||||||
|
(ebox-region-update 'status :content value))))
|
||||||
|
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))))
|
||||||
|
|
||||||
;;; ebox-core-render-tests.el ends here
|
;;; ebox-core-render-tests.el ends here
|
||||||
|
|||||||
@ -273,8 +273,8 @@ MIXED starts with three integral rows before the fractional cached rows."
|
|||||||
(cl-mapc (lambda (actual source) (should (eq actual source)))
|
(cl-mapc (lambda (actual source) (should (eq actual source)))
|
||||||
lines (seq-take raw 3))))))))
|
lines (seq-take raw 3))))))))
|
||||||
|
|
||||||
(ert-deftest ebox-css-layout-fractional-scroll-keeps-wrapper-fallbacks ()
|
(ert-deftest ebox-css-layout-fractional-scroll-respects-wrapper-boundaries ()
|
||||||
"Nested or chrome-bearing fractional scrolls retain full-line projection."
|
"Root chrome normalizes complete rows; nested fractional origins fall back."
|
||||||
(ebox-css-layout-test--with-pixel-display
|
(ebox-css-layout-test--with-pixel-display
|
||||||
(let ((ebox-viewport-width 101)
|
(let ((ebox-viewport-width 101)
|
||||||
(ebox-viewport-height 3))
|
(ebox-viewport-height 3))
|
||||||
@ -292,7 +292,7 @@ MIXED starts with three integral rows before the fractional cached rows."
|
|||||||
(cl-incf calls)
|
(cl-incf calls)
|
||||||
(apply original args))))
|
(apply original args))))
|
||||||
(should (= (ebox--scroll-region-by region 1 1) 1)))
|
(should (= (ebox--scroll-region-by region 1 1) 1)))
|
||||||
(should (= calls 1))
|
(should (= calls (if (eq boundary 'nested) 1 0)))
|
||||||
(should (equal (ebox-css-layout-test--display (buffer-string))
|
(should (equal (ebox-css-layout-test--display (buffer-string))
|
||||||
(ebox-css-layout-test--display
|
(ebox-css-layout-test--display
|
||||||
(ebox-render
|
(ebox-render
|
||||||
|
|||||||
@ -330,6 +330,138 @@
|
|||||||
(kill-buffer target)
|
(kill-buffer target)
|
||||||
(kill-buffer other))))
|
(kill-buffer other))))
|
||||||
|
|
||||||
|
(defun ebox-interaction-test--mouse-event (type window position)
|
||||||
|
"Return an event of TYPE at WINDOW's POSITION with its native click count."
|
||||||
|
(append (list type (list window position '(1 . 1) 0))
|
||||||
|
(cond
|
||||||
|
((memq 'drag (event-modifiers type))
|
||||||
|
(list (list window (1+ position) '(2 . 1) 1)))
|
||||||
|
((memq 'triple (event-modifiers type)) '(3))
|
||||||
|
((memq 'double (event-modifiers type)) '(2)))))
|
||||||
|
|
||||||
|
(defun ebox-interaction-test--dispatch-mouse (type window position)
|
||||||
|
"Dispatch a native event of TYPE at WINDOW's POSITION without moving point."
|
||||||
|
(let* ((event (ebox-interaction-test--mouse-event type window position))
|
||||||
|
(keys (vector event))
|
||||||
|
(command (with-current-buffer (window-buffer window)
|
||||||
|
(key-binding keys nil nil position)))
|
||||||
|
(map (with-current-buffer (window-buffer window)
|
||||||
|
(get-text-property position 'keymap))))
|
||||||
|
;; Resolve the event's actual window/position, not the caller's point.
|
||||||
|
;; Fail before invoking a global drag command that would read more input.
|
||||||
|
(should (commandp command))
|
||||||
|
(should (eq command (lookup-key map (vector type))))
|
||||||
|
(let ((last-input-event event))
|
||||||
|
(call-interactively command nil keys))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-keymap-create-repeated-clicks-preserve-point-and-selection ()
|
||||||
|
"Press/release/drag sequences activate once per click without text selection."
|
||||||
|
(let ((target (generate-new-buffer " *ebox-click-target*"))
|
||||||
|
(other (generate-new-buffer " *ebox-click-other*"))
|
||||||
|
(calls 0) seen)
|
||||||
|
(unwind-protect
|
||||||
|
(save-window-excursion
|
||||||
|
(delete-other-windows)
|
||||||
|
(set-window-buffer (selected-window) other)
|
||||||
|
(let* ((selected (selected-window))
|
||||||
|
(window (split-window-right))
|
||||||
|
(map (ebox-keymap-create
|
||||||
|
:activate (lambda ()
|
||||||
|
(cl-incf calls)
|
||||||
|
(push (current-buffer) seen)
|
||||||
|
(ebox-region-update "action" :help-echo
|
||||||
|
(number-to-string calls))))))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
target (ebox-build (list 'column
|
||||||
|
(list 'text :id "action" :keymap map "action")
|
||||||
|
'(text "ordinary text"))))
|
||||||
|
(set-window-buffer window target)
|
||||||
|
(with-current-buffer other
|
||||||
|
(insert "other") (goto-char 2) (set-mark 4)
|
||||||
|
(setq mark-active nil)
|
||||||
|
(dolist (active '(nil t))
|
||||||
|
(with-current-buffer target
|
||||||
|
(setq buffer-read-only t)
|
||||||
|
(goto-char (- (point-max) 2)) (set-mark 3)
|
||||||
|
(setq mark-active active))
|
||||||
|
(let ((point-before (with-current-buffer target (point)))
|
||||||
|
(window-point-before (window-point window)))
|
||||||
|
(dolist (events '((down-mouse-1 mouse-1)
|
||||||
|
(double-down-mouse-1 double-mouse-1)
|
||||||
|
(triple-down-mouse-1 triple-mouse-1)))
|
||||||
|
(let ((before calls))
|
||||||
|
(ebox-interaction-test--dispatch-mouse (car events) window 1)
|
||||||
|
(should (= calls before))
|
||||||
|
(ebox-interaction-test--dispatch-mouse (cadr events) window 1)
|
||||||
|
(should (= calls (1+ before)))))
|
||||||
|
(let ((before calls))
|
||||||
|
(dolist (type '(drag-mouse-1 double-drag-mouse-1
|
||||||
|
triple-drag-mouse-1))
|
||||||
|
(ebox-interaction-test--dispatch-mouse type window 1))
|
||||||
|
(should (= calls before)))
|
||||||
|
(with-current-buffer target
|
||||||
|
(should (= (point) point-before))
|
||||||
|
(should (= (mark) 3))
|
||||||
|
(should (eq mark-active active)))
|
||||||
|
(should (= (window-point window) window-point-before))
|
||||||
|
(should (eq (current-buffer) other))
|
||||||
|
(should (= (point) 2))
|
||||||
|
(should (= (mark) 4))
|
||||||
|
(should-not mark-active)
|
||||||
|
(should (eq (selected-window) selected)))))
|
||||||
|
(should (= calls 6))
|
||||||
|
(should (cl-every (lambda (buffer) (eq buffer target)) seen))))
|
||||||
|
(kill-buffer target)
|
||||||
|
(kill-buffer other))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-keymap-create-click-defaults-follow-replacement-and-removal ()
|
||||||
|
"Native click defaults follow published maps while explicit bindings win."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-click-lifecycle*"))
|
||||||
|
(activations 0) (overrides 0))
|
||||||
|
(unwind-protect
|
||||||
|
(save-window-excursion
|
||||||
|
(let ((window (selected-window))
|
||||||
|
(map (ebox-keymap-create :activate (lambda ()
|
||||||
|
(cl-incf activations)))))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer (ebox-build (list 'text :id "action" :keymap map "action")))
|
||||||
|
(set-window-buffer window buffer)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(ebox-region-update "action" :color "red")
|
||||||
|
(ebox-rerender-buffer-with-context buffer 80 30)
|
||||||
|
(ebox-interaction-test--dispatch-mouse 'double-mouse-1 window 1)
|
||||||
|
(should (= activations 1))
|
||||||
|
(ebox-region-update
|
||||||
|
"action" :keymap
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate (lambda () (cl-incf activations))
|
||||||
|
:bindings (mapcar (lambda (key)
|
||||||
|
(cons (vector key)
|
||||||
|
(lambda () (cl-incf overrides))))
|
||||||
|
'(down-mouse-1 double-mouse-1 triple-mouse-1
|
||||||
|
drag-mouse-1))))
|
||||||
|
(dolist (type '(down-mouse-1 double-mouse-1 triple-mouse-1
|
||||||
|
drag-mouse-1))
|
||||||
|
(ebox-interaction-test--dispatch-mouse type window 1))
|
||||||
|
(should (= overrides 4))
|
||||||
|
(should (= activations 1))
|
||||||
|
(ebox-interaction-test--dispatch-mouse 'mouse-1 window 1)
|
||||||
|
(should (= activations 2))
|
||||||
|
;; A raw map remains literal; replacing/removing the helper
|
||||||
|
;; restores native selection instead of leaving stale guards.
|
||||||
|
(ebox-region-update "action" :keymap '(keymap (mouse-1 . ignore)))
|
||||||
|
(let ((event (list 'down-mouse-1 (list window 1 '(1 . 1) 0))))
|
||||||
|
(should (eq (key-binding (vector event))
|
||||||
|
(global-key-binding [down-mouse-1]))))
|
||||||
|
(should-not (lookup-key (get-text-property 1 'keymap)
|
||||||
|
[double-mouse-1]))
|
||||||
|
(ebox-region-update "action" :keymap nil)
|
||||||
|
(dolist (type '(down-mouse-1 mouse-1 drag-mouse-1))
|
||||||
|
(let ((event (list type (list window 1 '(1 . 1) 0))))
|
||||||
|
(should (eq (key-binding (vector event))
|
||||||
|
(global-key-binding (vector type)))))))))
|
||||||
|
(kill-buffer buffer))))
|
||||||
|
|
||||||
(ert-deftest ebox-region-update-semantic-id-keeps-handle-lifecycle ()
|
(ert-deftest ebox-region-update-semantic-id-keeps-handle-lifecycle ()
|
||||||
"IDs resolve locally and opaque handles retain their owning surface checks."
|
"IDs resolve locally and opaque handles retain their owning surface checks."
|
||||||
(let ((first (generate-new-buffer " *ebox-id-first*"))
|
(let ((first (generate-new-buffer " *ebox-id-first*"))
|
||||||
|
|||||||
@ -42,6 +42,54 @@
|
|||||||
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
||||||
:input))))))
|
:input))))))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--mount-signature (buffer)
|
||||||
|
"Return BUFFER's complete mounted ranges normalized to runtime tree paths."
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(objects (plist-get state :surface-node-object-table))
|
||||||
|
result)
|
||||||
|
(cl-labels
|
||||||
|
((walk (node path)
|
||||||
|
(let ((object (gethash (plist-get node :node-id) objects)))
|
||||||
|
(should object)
|
||||||
|
(push
|
||||||
|
(cons path
|
||||||
|
(sort
|
||||||
|
(mapcar (lambda (mount)
|
||||||
|
(cons (plist-get mount :start)
|
||||||
|
(plist-get mount :end)))
|
||||||
|
(tp-object-mounts object))
|
||||||
|
(lambda (left right)
|
||||||
|
(if (= (car left) (car right))
|
||||||
|
(< (cdr left) (cdr right))
|
||||||
|
(< (car left) (car right))))))
|
||||||
|
result))
|
||||||
|
(cl-loop for child in (ebox-tree-node-children node)
|
||||||
|
for index from 0
|
||||||
|
do (walk child (append path (list index))))))
|
||||||
|
(walk (plist-get state :root-node) '(root)))
|
||||||
|
(nreverse result)))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--assert-fresh-mounts ()
|
||||||
|
"Compare current mounts and paint with an independent committed-input render."
|
||||||
|
(let ((buffer (current-buffer))
|
||||||
|
(input (plist-get (ebox-surface-buffer-snapshot (current-buffer)) :input)))
|
||||||
|
(with-temp-buffer
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer (current-buffer) input)
|
||||||
|
(should (equal
|
||||||
|
(ebox-layer-publication-test--mount-signature buffer)
|
||||||
|
(ebox-layer-publication-test--mount-signature (current-buffer))))
|
||||||
|
(let ((actual (with-current-buffer buffer (buffer-string)))
|
||||||
|
(expected (buffer-string)))
|
||||||
|
(should (equal (substring-no-properties actual)
|
||||||
|
(substring-no-properties expected)))
|
||||||
|
(dotimes (position (length actual))
|
||||||
|
(should (equal (get-text-property position 'face actual)
|
||||||
|
(get-text-property position 'face expected))))))
|
||||||
|
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||||
|
(ebox-unmount-buffer (current-buffer)))))))
|
||||||
|
|
||||||
(ert-deftest ebox-layer-publication-updates-every-placement-property ()
|
(ert-deftest ebox-layer-publication-updates-every-placement-property ()
|
||||||
"Each layer declaration reaches the mounted runtime through a region update."
|
"Each layer declaration reaches the mounted runtime through a region update."
|
||||||
(dolist (properties '((:position relative) (:left (ch 2)) (:top (lh 1))
|
(dolist (properties '((:position relative) (:left (ch 2)) (:top (lh 1))
|
||||||
@ -181,6 +229,60 @@
|
|||||||
(should (equal (get-text-property position 'help-echo) "lower-new")))
|
(should (equal (get-text-property position 'help-echo) "lower-new")))
|
||||||
(ebox-layer-publication-test--assert-fresh-render)))
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-visible-mounts-follow-occlusion-and-rollback ()
|
||||||
|
"Hide/reveal remounts visible owners and rolls back both TP failure stages."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 12) :height (lh 2)
|
||||||
|
:padding-inline (ch 2) :background-color "#101117" :color "#DCE0EC"
|
||||||
|
:border-top-width (px 1) :border-top-style solid
|
||||||
|
:border-top-color "#ABCDEF"
|
||||||
|
:border-bottom-width (px 1) :border-bottom-style solid
|
||||||
|
:border-bottom-color "#FEDCBA"
|
||||||
|
(box :id "host" :width (ch 8) :height (lh 2)
|
||||||
|
(box :id "lower" :width (ch 8) :height (lh 2)
|
||||||
|
:background-color "#234567" :help-echo "lower"
|
||||||
|
"LOWER001\nLOWER002")
|
||||||
|
(box :id "upper" :position absolute :width (ch 8) :height (lh 1)
|
||||||
|
:background-color "#AABBCC" :help-echo "upper" "UPPER001")))))
|
||||||
|
(ebox-region-update "lower" :keymap '(keymap (13 . backward-char)))
|
||||||
|
(ebox-region-update "upper" :keymap '(keymap (13 . forward-char)))
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(let ((before (buffer-string))
|
||||||
|
(state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(mounts (ebox-layer-publication-test--mount-signature (current-buffer))))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step) (error "Reject layer mounts")))))
|
||||||
|
(should-error (ebox-region-update "upper" :visibility 'hidden)))
|
||||||
|
(should (eq state (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (equal mounts
|
||||||
|
(ebox-layer-publication-test--mount-signature (current-buffer)))))
|
||||||
|
(dolist (visibility '(hidden visible))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Layer remount rendered the root"))))
|
||||||
|
(ebox-region-update "upper" :visibility visibility))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(upper (car (ebox-selector-query-buffer (current-buffer) "#upper")))
|
||||||
|
(object (gethash (plist-get upper :node-id)
|
||||||
|
(plist-get state :surface-node-object-table))))
|
||||||
|
(should (eq (null (tp-object-mounts object)) (eq visibility 'hidden)))
|
||||||
|
(should (eq (null (ebox-surface-region-bounds
|
||||||
|
(current-buffer) (plist-get upper :region-id)))
|
||||||
|
(eq visibility 'hidden)))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward (if (eq visibility 'hidden) "LOWER001" "UPPER001"))
|
||||||
|
(should (equal (get-text-property (1- (point)) 'help-echo)
|
||||||
|
(if (eq visibility 'hidden) "lower" "upper")))
|
||||||
|
(should (eq (lookup-key (get-text-property (1- (point)) 'keymap) (kbd "RET"))
|
||||||
|
(if (eq visibility 'hidden) #'backward-char #'forward-char))))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-mounts)))))
|
||||||
|
|
||||||
(ert-deftest ebox-layer-publication-ancestor-paint-survives-recomposition ()
|
(ert-deftest ebox-layer-publication-ancestor-paint-survives-recomposition ()
|
||||||
"A detached host retains enclosing paint and interaction contributions."
|
"A detached host retains enclosing paint and interaction contributions."
|
||||||
(ebox-layer-publication-test--with-buffer
|
(ebox-layer-publication-test--with-buffer
|
||||||
@ -218,6 +320,45 @@
|
|||||||
(should (string-match-p "LOWER002" (buffer-string)))
|
(should (string-match-p "LOWER002" (buffer-string)))
|
||||||
(ebox-layer-publication-test--assert-fresh-render)))
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-hidden-portals-keep-unrelated-host-local ()
|
||||||
|
"Hidden portals cannot widen unrelated local updates, even after a reveal."
|
||||||
|
(dolist (hidden-owner '(nil t))
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "host" :width (ch 8) :height (lh 2)
|
||||||
|
(box :id "lower" :width (ch 8) :height (lh 1) "LOWER001")
|
||||||
|
(box :id "upper" :position absolute :width (ch 8)
|
||||||
|
:height (lh 1) :z-index 1 "UPPER001"))
|
||||||
|
(box :id "portal-owner" :height (lh 1)
|
||||||
|
:visibility ,(if hidden-owner 'hidden 'visible)
|
||||||
|
(box :id "portal" :position absolute :layer root
|
||||||
|
:width (ch 8) :height (lh 1) :z-index 2
|
||||||
|
:visibility ,(if hidden-owner 'visible 'hidden) "PORTAL01")))))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Hidden portal caused an unrelated root render"))))
|
||||||
|
(ebox-region-update "lower" :content "LOWER002")
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden))
|
||||||
|
(should (string-match-p "LOWER002" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)
|
||||||
|
(ebox-region-update (if hidden-owner "portal-owner" "portal")
|
||||||
|
:visibility 'visible)
|
||||||
|
(should (string-match-p "PORTAL01" (buffer-string)))
|
||||||
|
(ebox-region-update "lower" :content "LOWER003")
|
||||||
|
(should-not (string-match-p "LOWER003" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)
|
||||||
|
(ebox-region-update (if hidden-owner "portal-owner" "portal")
|
||||||
|
:visibility 'hidden)
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Closed portal retained a stale root dependency"))))
|
||||||
|
(ebox-region-update "lower" :content "LOWER004"))
|
||||||
|
(should (string-match-p "LOWER004" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render))))
|
||||||
|
|
||||||
(ert-deftest ebox-layer-publication-root-portal-occludes-unrelated-owner ()
|
(ert-deftest ebox-layer-publication-root-portal-occludes-unrelated-owner ()
|
||||||
"Updates outside a root portal's local host cannot paint over the portal."
|
"Updates outside a root portal's local host cannot paint over the portal."
|
||||||
(ebox-layer-publication-test--with-buffer
|
(ebox-layer-publication-test--with-buffer
|
||||||
@ -319,5 +460,186 @@
|
|||||||
(should (string-match-p "Before" (buffer-string)))
|
(should (string-match-p "Before" (buffer-string)))
|
||||||
(should (string-match-p "After" (buffer-string)))))))
|
(should (string-match-p "After" (buffer-string)))))))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--allocated-input (&rest host-style)
|
||||||
|
"Build a host with HOST-STYLE inside a padded parent allocation."
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (ch 24) :height (lh 7)
|
||||||
|
:padding ((lh 1) (ch 2))
|
||||||
|
(box :id "before" "Before")
|
||||||
|
(box :id "host" :height (lh 2) :background-color "#004488"
|
||||||
|
,@host-style
|
||||||
|
(box :id "lower" :width (ch 8) :height (lh 1) "LOWER001")
|
||||||
|
(box :id "upper" :position absolute :left (px 0) :top (lh 0)
|
||||||
|
:width (ch 4) :height (lh 1) :background-color "#884400"
|
||||||
|
:help-echo "Upper layer" :pointer hand
|
||||||
|
:keymap (keymap (13 . ignore)) "UP01"))
|
||||||
|
(box :id "after" "After"))))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--assert-fresh-properties ()
|
||||||
|
"Compare visible text, geometry, paint and interactions with a fresh render."
|
||||||
|
(let ((actual (buffer-string))
|
||||||
|
(fresh (ebox-render
|
||||||
|
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
||||||
|
:input))))
|
||||||
|
(should (equal (substring-no-properties actual)
|
||||||
|
(substring-no-properties fresh)))
|
||||||
|
(dotimes (position (length actual))
|
||||||
|
(dolist (property '(face font-lock-face display help-echo keymap
|
||||||
|
mouse-face pointer))
|
||||||
|
(ert-info ((format "Property %S at character %d" property position))
|
||||||
|
(should (equal (get-text-property position property actual)
|
||||||
|
(get-text-property position property fresh))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-auto-host-keeps-padded-parent-allocation ()
|
||||||
|
"An auto-width host keeps its parent's content width during local updates."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--allocated-input))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Auto-width layer host rendered the root"))))
|
||||||
|
(dolist (left '((ch 1) (ch 0)))
|
||||||
|
(ebox-region-update "upper" :left left)
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-allocation-respects-host-box-model ()
|
||||||
|
"A local allocation includes margins and content-box padding exactly once."
|
||||||
|
(dolist (style '((:width (ch 8) :margin-inline (ch 1))
|
||||||
|
(:width (ch 8) :box-sizing content-box
|
||||||
|
:padding-inline (ch 1))))
|
||||||
|
(ert-info ((format "Host style %S" style))
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(apply #'ebox-layer-publication-test--allocated-input style))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Stable host box model rendered the root"))))
|
||||||
|
(dolist (left '((ch 1) (ch 0)))
|
||||||
|
(ebox-region-update "upper" :left left)
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-intrinsic-host-width-can-grow ()
|
||||||
|
"Content-dependent hosts must grow naturally and move their following sibling."
|
||||||
|
(dolist (width '(max-content min-content fit-content auto))
|
||||||
|
(ert-info ((format "Intrinsic host width %S" width))
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
`(row :width (ch 20) :height (lh 2)
|
||||||
|
(box :id "host" :width ,width :height (lh 1)
|
||||||
|
(box :id "lower" "AAAA")
|
||||||
|
(box :position absolute :width (ch 2)
|
||||||
|
:height (lh 1) "XX"))
|
||||||
|
(box "TAIL"))))
|
||||||
|
(dolist (content '("AAAAAA" "AAA"))
|
||||||
|
(ebox-region-update "lower" :content content)
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-inline-auto-host-width-can-grow ()
|
||||||
|
"Automatic inline widths remain content-dependent even under normal flow."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (ch 20) :height (lh 2)
|
||||||
|
(box :id "host" :outer inline :height (lh 1)
|
||||||
|
(box :id "lower" :outer inline "AAAA")
|
||||||
|
(box :position absolute :width (ch 2)
|
||||||
|
:height (lh 1) "XX"))
|
||||||
|
(box :outer inline "TAIL"))))
|
||||||
|
(dolist (content '("AAA" "AAAAAA"))
|
||||||
|
(ebox-region-update "lower" :content content)
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-owner-geometry-does-not-reuse-allocation ()
|
||||||
|
"Owner width, sizing model and margin changes retain the geometry fallback."
|
||||||
|
(dolist (case '(((:width (ch 8)) (:width (ch 10)))
|
||||||
|
((:width (ch 8) :padding-inline (ch 1))
|
||||||
|
(:box-sizing content-box))
|
||||||
|
((:width (ch 8) :margin-inline (ch 1))
|
||||||
|
(:margin-inline (ch 2)))))
|
||||||
|
(ert-info ((format "Host geometry change %S" case))
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(apply #'ebox-layer-publication-test--allocated-input (car case)))
|
||||||
|
(let ((render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(full-renders 0))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf full-renders)
|
||||||
|
(apply render arguments))))
|
||||||
|
(apply #'ebox-region-update "host" (cadr case)))
|
||||||
|
(should (> full-renders 0)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-allocated-host-failure-rolls-back ()
|
||||||
|
"Failed local text and client-state publication preserve the old allocation."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--allocated-input))
|
||||||
|
(let ((before (buffer-string))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Allocated layer rollback rendered the root"))))
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(let* ((failed-step nil)
|
||||||
|
(tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step)
|
||||||
|
(setq failed-step step)
|
||||||
|
(error "Reject allocated layer %S" step)))))
|
||||||
|
(should-error (ebox-region-update "upper" :left '(ch 1)))
|
||||||
|
(should (eq failed-step failure-step)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties))
|
||||||
|
(ebox-region-update "upper" :left '(ch 1))
|
||||||
|
(should (= (1+ revision)
|
||||||
|
(ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should-not (equal-including-properties before (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-hidden-ancestor-portals-stay-hidden-in-active-root ()
|
||||||
|
"Root composition must not collect portals under a hidden flow ancestor."
|
||||||
|
(dolist (other-root-paint '(absolute portal))
|
||||||
|
(ert-info ((format "Other root paint %S" other-root-paint))
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
`(box :width (ch 12) :height (lh 3)
|
||||||
|
(box "BASE00000000\nABCDEFGHIJKL\n0123456789ab")
|
||||||
|
(box :id "hidden-owner" :visibility hidden :height (lh 1)
|
||||||
|
(box :height (lh 1)
|
||||||
|
(box :id "hidden-portal" :position absolute :layer root
|
||||||
|
:left (ch 0) :top (lh 0) :width (ch 8)
|
||||||
|
:height (lh 1) :z-index 9 "HIDDEN01")))
|
||||||
|
,(if (eq other-root-paint 'absolute)
|
||||||
|
'(box :position absolute :left (ch 10) :top (lh 2)
|
||||||
|
:width (ch 2) :height (lh 1) "XY")
|
||||||
|
'(box :height (lh 1)
|
||||||
|
(box :position absolute :layer root
|
||||||
|
:left (ch 10) :top (lh 2)
|
||||||
|
:width (ch 2) :height (lh 1) "XY"))))))
|
||||||
|
(should-not (string-match-p "HIDDEN01" (buffer-string)))
|
||||||
|
(should (string-match-p "XY" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)
|
||||||
|
(ebox-region-update "hidden-portal" :content "HIDDEN02")
|
||||||
|
(should-not (string-match-p "HIDDEN02" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)
|
||||||
|
(dotimes (_ 2)
|
||||||
|
(ebox-region-update "hidden-owner" :visibility 'visible)
|
||||||
|
(should (string-match-p "HIDDEN02" (buffer-string)))
|
||||||
|
(should (string-match-p "XY" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties)
|
||||||
|
(ebox-region-update "hidden-owner" :visibility 'hidden)
|
||||||
|
(should-not (string-match-p "HIDDEN02" (buffer-string)))
|
||||||
|
(should (string-match-p "XY" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-properties))))))
|
||||||
|
|
||||||
(provide 'ebox-layer-publication-tests)
|
(provide 'ebox-layer-publication-tests)
|
||||||
;;; ebox-layer-publication-tests.el ends here
|
;;; ebox-layer-publication-tests.el ends here
|
||||||
|
|||||||
348
tests/ebox-layer-scroll-tests.el
Normal file
348
tests/ebox-layer-scroll-tests.el
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
;;; ebox-layer-scroll-tests.el --- Retained layered root scroll -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Cached root scroll projects composed content without repositioning its layers.
|
||||||
|
;; Nested owners and active root projections keep their composition boundary.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defmacro ebox-layer-scroll-test--with-buffer (&rest body)
|
||||||
|
"Run BODY in an isolated Elisp scroll buffer without native window scrolling."
|
||||||
|
(declare (indent 0) (debug t))
|
||||||
|
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(let ((ebox-viewport-width 24) (ebox-viewport-height 8)
|
||||||
|
(ebox-native-buffer-scroll nil)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||||
|
(ebox-scroll-lazy-idle-prefetch-lines 0))
|
||||||
|
(with-temp-buffer ,@body))))
|
||||||
|
|
||||||
|
(defun ebox-layer-scroll-test--input (&optional chrome portal nested)
|
||||||
|
"Build a layered scrolling root with CHROME, hidden PORTAL and NESTED owner."
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (ch 20) :height (lh 5) :overflow scroll
|
||||||
|
:padding ,(pcase chrome
|
||||||
|
('fractional '((lh 1) (px 1.5)))
|
||||||
|
('nil '((lh 0) (ch 0)))
|
||||||
|
(_ '((lh 1) (ch 1))))
|
||||||
|
:color "#102030" :background-color "#DDEEFF"
|
||||||
|
(box :id "before" "BEFORE")
|
||||||
|
(box :id "host" :height (lh 4)
|
||||||
|
(box :id "lower" "BASE0000\nBASE1111\nBASE2222\nBASE3333")
|
||||||
|
(box :id "upper" :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 4) :height (lh 2) :background-color "#334455"
|
||||||
|
:color "#FFFFFF" :pointer hand :help-echo "layer action"
|
||||||
|
:keymap (keymap (13 . ignore)) "TOP1\nTOP2"))
|
||||||
|
(box :id "after" "AFTER000\nAFTER111\nAFTER222\nAFTER333")
|
||||||
|
,@(when nested
|
||||||
|
'((box :id "nested" :width (ch 10) :height (lh 2)
|
||||||
|
:overflow scroll :padding ((lh 1) (ch 1))
|
||||||
|
"NESTED0\nNESTED1\nNESTED2\nNESTED3")))
|
||||||
|
,@(when portal
|
||||||
|
'((box :id "portal-owner" :height (lh 1)
|
||||||
|
(box :id "portal" :position absolute :layer root
|
||||||
|
:anchor "before" :visibility hidden
|
||||||
|
:width (ch 6) :height (lh 1) "PORTAL")))))))
|
||||||
|
|
||||||
|
(defun ebox-layer-scroll-test--root-state ()
|
||||||
|
"Return the mounted root's current scroll state."
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(root (plist-get runtime :root-node)))
|
||||||
|
(gethash (plist-get root :region-id) (plist-get runtime :scroll-state-table))))
|
||||||
|
|
||||||
|
(defun ebox-layer-scroll-test--portal-chain-input ()
|
||||||
|
"Build two visible anchored root projections through a clipped logical owner."
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 20) :height (lh 6) :overflow scroll
|
||||||
|
:padding ((lh 1) (px 1.5)) :margin-left (px 0.5)
|
||||||
|
:background-color "#EEF4F8" :color "#173047"
|
||||||
|
(box "PRE0\nPRE1\nPRE2")
|
||||||
|
(box :id "clipped-owner" :height (lh 1) :overflow hidden
|
||||||
|
(box :id "trigger" :position relative :left (ch 1)
|
||||||
|
:width (ch 6) :height (lh 1) "TRIGGER")
|
||||||
|
;; Source order is the reverse of the anchor dependency order.
|
||||||
|
(box :id "dependent" :position absolute :layer root
|
||||||
|
:anchor "provider-anchor" :placement bottom-end :z-index 9
|
||||||
|
:width (ch 5) :height (lh 1) :background-color "#704080"
|
||||||
|
:color "#FFFFFF" :pointer hand :help-echo "Dependent action"
|
||||||
|
:keymap (keymap (13 . ignore)) "CHAIN")
|
||||||
|
(box :id "provider" :position absolute :layer root :anchor "trigger"
|
||||||
|
:placement bottom-start :width (ch 7) :height (lh 2)
|
||||||
|
:background-color "#206050" :color "#FFFFFF"
|
||||||
|
(box :id "provider-anchor" :position relative :left (ch 1)
|
||||||
|
:width (ch 4) :height (lh 1) :help-echo "Provider action"
|
||||||
|
:keymap (keymap (13 . ignore)) "LINK")
|
||||||
|
(box "PANEL")))
|
||||||
|
(box "POST0\nPOST1\nPOST2\nPOST3\nPOST4\nPOST5\nPOST6\nPOST7"))))
|
||||||
|
|
||||||
|
(defun ebox-layer-scroll-test--assert-fresh ()
|
||||||
|
"Compare the committed window with an independent render at the same offset."
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(root (plist-get runtime :root-node))
|
||||||
|
(offset (ebox-get root :scroll-offset))
|
||||||
|
(input (plist-get (ebox-surface-buffer-snapshot (current-buffer)) :input))
|
||||||
|
(ebox-viewport-width (plist-get runtime :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get runtime :viewport-height))
|
||||||
|
(set-offset
|
||||||
|
(lambda (candidate)
|
||||||
|
(unless (eq candidate root)
|
||||||
|
(ebox-put candidate :scroll-offset offset)
|
||||||
|
(ebox-put candidate :ebox-scroll-offset-controlled-p t))))
|
||||||
|
actual fresh)
|
||||||
|
(setq actual (buffer-string))
|
||||||
|
;; Bind only the first layout invocation, leaving nested scroll offsets
|
||||||
|
;; to the committed source used by this root-only oracle.
|
||||||
|
(let ((render (symbol-function 'ebox--render-layout)) first)
|
||||||
|
(cl-letf (((symbol-function 'ebox--render-layout)
|
||||||
|
(lambda (candidate)
|
||||||
|
(unless first (setq first t) (funcall set-offset candidate))
|
||||||
|
(funcall render candidate))))
|
||||||
|
(setq fresh (ebox-render input))))
|
||||||
|
(should (equal (substring-no-properties actual) (substring-no-properties fresh)))
|
||||||
|
(should (= (length actual) (length fresh)))
|
||||||
|
(dotimes (position (length actual))
|
||||||
|
(dolist (property '(face font-lock-face display help-echo keymap mouse-face pointer))
|
||||||
|
(ert-info ((format "Property %S at %d" property position))
|
||||||
|
(should (equal (get-text-property position property actual)
|
||||||
|
(get-text-property position property fresh))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-root-reuses-composed-content ()
|
||||||
|
"Pure root scroll retains layered content and skips the complete tree render."
|
||||||
|
(dolist (chrome '(nil t fractional))
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input chrome t))
|
||||||
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get state :box) :region-id))
|
||||||
|
(content (plist-get state :content-lines))
|
||||||
|
(root-renders 0)
|
||||||
|
(render (symbol-function 'ebox-surface--render-candidate)))
|
||||||
|
(when (eq chrome 'fractional)
|
||||||
|
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
||||||
|
:fractional-pixel-output-p)))
|
||||||
|
(dolist (delta '(1 1 -1 -1))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf root-renders)
|
||||||
|
(apply render arguments))))
|
||||||
|
(should (= delta (ebox--scroll-region-by region-id delta 1)))
|
||||||
|
(should (eq content
|
||||||
|
(plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
||||||
|
(should (eq (plist-get (ebox-buffer-update-report (current-buffer))
|
||||||
|
:projection-kind)
|
||||||
|
'scroll-patch)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh))
|
||||||
|
(should (= root-renders 0))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-retains-input-facts-with-unready-nested-window ()
|
||||||
|
"A nested window's chrome cannot invalidate the root's style or axis facts."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input t nil t))
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(root-state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get root-state :box) :region-id))
|
||||||
|
(axes (plist-get runtime :viewport-dependent-node-id-axes))
|
||||||
|
(nested (car (ebox-selector-query-buffer (current-buffer) "#nested")))
|
||||||
|
(nested-state (gethash (plist-get nested :region-id)
|
||||||
|
(plist-get runtime :scroll-state-table))))
|
||||||
|
(should nested-state)
|
||||||
|
(should-not (ebox--scroll-state-retained-window-ready-p nested-state))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--context-axes)
|
||||||
|
(lambda (&rest _) (ert-fail "Scroll rescanned context axes")))
|
||||||
|
((symbol-function 'ebox-surface--inline-inheritance-required-p)
|
||||||
|
(lambda (&rest _) (ert-fail "Scroll rescanned inheritance")))
|
||||||
|
((symbol-function 'ebox-surface--apply-node-style)
|
||||||
|
(lambda (&rest _) (ert-fail "Scroll reapplied unchanged styles"))))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
||||||
|
(should (eq axes (plist-get (ebox--buffer-render-state (current-buffer))
|
||||||
|
:viewport-dependent-node-id-axes)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-content-and-visibility-refresh-composed-cache ()
|
||||||
|
"Content and visibility updates replace the cache consumed by later scrolling."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input t))
|
||||||
|
(let* ((root-state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get root-state :box) :region-id)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(dolist (change '((:content "NEW1\nNEW2")
|
||||||
|
(:visibility hidden) (:visibility visible)))
|
||||||
|
(let ((content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
||||||
|
(apply #'ebox-region-update "upper" change)
|
||||||
|
(should-not (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
||||||
|
:content-lines))))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)
|
||||||
|
(should (= -1 (ebox--scroll-region-by region-id -1 1)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-rejected-publication-keeps-cache-and-input ()
|
||||||
|
"Both publication failure stages preserve composed cache and current offset."
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input 'fractional))
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get state :box) :region-id))
|
||||||
|
(content (plist-get state :content-lines))
|
||||||
|
(snapshot (ebox-surface-buffer-snapshot (current-buffer)))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(before (buffer-string)))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step) (error "Reject cached root scroll")))))
|
||||||
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (eq runtime (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (eq state (ebox-layer-scroll-test--root-state)))
|
||||||
|
(should (eq content (plist-get state :content-lines)))
|
||||||
|
(let* ((after (ebox-surface-buffer-snapshot (current-buffer)))
|
||||||
|
(same-input
|
||||||
|
(equal (ebox-canonical-input-roots (plist-get snapshot :input))
|
||||||
|
(ebox-canonical-input-roots (plist-get after :input)))))
|
||||||
|
(should same-input)
|
||||||
|
(should (= (plist-get snapshot :mount-id) (plist-get after :mount-id))))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-visible-root-dependencies-retain-composed-cache ()
|
||||||
|
"Pure root scroll slices already composed visible projections and anchors."
|
||||||
|
(dolist (target '(portal anchor))
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input t t))
|
||||||
|
(if (eq target 'portal)
|
||||||
|
(ebox-region-update "portal" :visibility 'visible)
|
||||||
|
(ebox-region-update "upper" :anchor "before"))
|
||||||
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
||||||
|
(root (plist-get state :box))
|
||||||
|
(region-id (plist-get root :region-id))
|
||||||
|
(render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(root-renders 0))
|
||||||
|
(should (ebox-layer-cached-root-scroll-p root state))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf root-renders)
|
||||||
|
(apply render arguments))))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
||||||
|
(should (= root-renders 0))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-anchor-provider-chain-crosses-viewport ()
|
||||||
|
"Cached provider chains keep text and interactions when entering and leaving."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--portal-chain-input))
|
||||||
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get state :box) :region-id))
|
||||||
|
(content (plist-get state :content-lines))
|
||||||
|
(visible 0) (outside 0))
|
||||||
|
(should (string-match-p "CHAIN" (ebox-lines-join content)))
|
||||||
|
(dolist (delta '(1 1 1 1 1 1 -1 -1 -1 -1 -1 -1))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Root scroll recomposed portals"))))
|
||||||
|
(should (= delta (ebox--scroll-region-by region-id delta 1))))
|
||||||
|
(should (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
||||||
|
:content-lines)))
|
||||||
|
(if (string-match-p "CHAIN" (buffer-string))
|
||||||
|
(cl-incf visible)
|
||||||
|
(cl-incf outside))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh))
|
||||||
|
(should (> visible 0))
|
||||||
|
(should (> outside 0)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-anchor-provider-invalidation-and-rollback ()
|
||||||
|
"Provider changes replace composed caches; rejected scrolls preserve them."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--portal-chain-input))
|
||||||
|
(let* ((state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get state :box) :region-id)))
|
||||||
|
(should (= 3 (ebox--scroll-region-by region-id 3 3)))
|
||||||
|
(dolist (change '(("provider-anchor" :content "EDIT")
|
||||||
|
("provider" :placement top-end)
|
||||||
|
("trigger" :left (ch 3))
|
||||||
|
("clipped-owner" :visibility hidden)
|
||||||
|
("clipped-owner" :visibility visible)))
|
||||||
|
(let ((content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
||||||
|
(apply #'ebox-region-update change)
|
||||||
|
(should-not (eq content (plist-get (ebox-layer-scroll-test--root-state)
|
||||||
|
:content-lines))))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh)
|
||||||
|
(should (= -1 (ebox--scroll-region-by region-id -1 1))))
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(let ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(content (plist-get (ebox-layer-scroll-test--root-state) :content-lines))
|
||||||
|
(before (buffer-string))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step) (error "Reject anchored root scroll")))))
|
||||||
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (eq runtime (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (eq content (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-declines-incomplete-or-changed-cache ()
|
||||||
|
"Dirty or lazy content and simultaneous source changes cannot reuse layers."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-scroll-test--input t))
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(state (ebox-layer-scroll-test--root-state))
|
||||||
|
(root (plist-get state :box))
|
||||||
|
(owner (plist-get root :node-id)))
|
||||||
|
(dolist (key '(:render-content-prefix :materialize-content-lines
|
||||||
|
:lazy-scroll-prefix-dirty :lazy-scroll-window-refresh-required))
|
||||||
|
(let ((candidate (copy-sequence state)))
|
||||||
|
(plist-put candidate :content-lines-complete-p nil)
|
||||||
|
(setq candidate (plist-put candidate key t))
|
||||||
|
(should-not (ebox-layer-cached-root-scroll-p root candidate))))
|
||||||
|
(dolist (keys '((:scroll-offset :content) (:visibility) (:width)))
|
||||||
|
(should-not
|
||||||
|
(ebox-incremental--layer-root-scroll-p
|
||||||
|
runtime runtime
|
||||||
|
(list :dirty-set (list (list :node-id owner :changed-keys keys)))
|
||||||
|
owner))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-scroll-ordinary-materialized-content-builds-rendered-rows ()
|
||||||
|
"Ordinary materialization still bootstraps its reusable physical row cache."
|
||||||
|
(ebox-layer-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (vw 100) :height (vh 100) :overflow scroll
|
||||||
|
,@(cl-loop for index below 80
|
||||||
|
collect `(box :padding-left (px 1.5)
|
||||||
|
,(format "ROW%02d" index))))))
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(state (ebox-layer-scroll-test--root-state))
|
||||||
|
(region-id (plist-get (plist-get state :box) :region-id)))
|
||||||
|
(should-not (plist-get runtime :layered-p))
|
||||||
|
(should (plist-get runtime :fractional-pixel-output-p))
|
||||||
|
(setq state (ebox--scroll-state-materialize-lines region-id state))
|
||||||
|
(puthash region-id state (plist-get runtime :scroll-state-table))
|
||||||
|
(should (plist-get state :content-lines-complete-p))
|
||||||
|
(should-not (plist-get state :rendered-content-lines))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (= -1 (ebox--scroll-region-by region-id -1 1)))
|
||||||
|
(should (plist-get (ebox-layer-scroll-test--root-state) :rendered-content-lines))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate-node)
|
||||||
|
(lambda (&rest _) (ert-fail "Cached ordinary scroll rendered wrapper"))))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
|
||||||
|
(ebox-layer-scroll-test--assert-fresh))))
|
||||||
|
|
||||||
|
(provide 'ebox-layer-scroll-tests)
|
||||||
|
;;; ebox-layer-scroll-tests.el ends here
|
||||||
@ -160,5 +160,105 @@
|
|||||||
(should (string-match-p "LOWER002" (buffer-string)))
|
(should (string-match-p "LOWER002" (buffer-string)))
|
||||||
(should-not (string-match-p "UPPER001" (buffer-string))))))
|
(should-not (string-match-p "UPPER001" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-pieces-preserve-owner-margins-and-property-boundaries ()
|
||||||
|
"Nested ownership and each owner's margins produce separate exact pieces."
|
||||||
|
(let* ((parent (list :node-id 1 :region-id 0))
|
||||||
|
(child (list :node-id 2 :region-id "child"))
|
||||||
|
(line (propertize "ABCDEFGH" 'ebox-content-owners '(0 "child")))
|
||||||
|
(second (propertize "XY" 'ebox-content "child"))
|
||||||
|
(lines (list line second)))
|
||||||
|
(put-text-property 0 1 'ebox-ml "child" line)
|
||||||
|
(put-text-property 3 4 'ebox-mt "child" line)
|
||||||
|
(put-text-property 6 7 'ebox-mb "child" line)
|
||||||
|
(put-text-property 7 8 'ebox-mr "child" line)
|
||||||
|
;; Duplicate roles and unrelated face boundaries do not split ownership.
|
||||||
|
(put-text-property 1 2 'ebox-content-owner "child" line)
|
||||||
|
(put-text-property 2 3 'face 'bold line)
|
||||||
|
(put-text-property 4 5 'ebox-pl "child" line)
|
||||||
|
(put-text-property 5 6 'ebox-br "child" line)
|
||||||
|
(cl-letf (((symbol-function 'ebox--string-pixel-width) #'length))
|
||||||
|
(let ((pieces (ebox-layer--pieces lines parent)))
|
||||||
|
(should (equal pieces (list (list :line line :start 0 :end 8 :y 0)))))
|
||||||
|
(let ((pieces (ebox-layer--pieces lines child)))
|
||||||
|
(should (equal pieces
|
||||||
|
(list (list :line line :start 1 :end 3 :y 0)
|
||||||
|
(list :line line :start 4 :end 6 :y 0)
|
||||||
|
(list :line second :start 0 :end 2 :y 1))))
|
||||||
|
(should (eq (plist-get (car pieces) :line) line))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-pieces-keep-shaping-graphemes-and-shared-display-source ()
|
||||||
|
"An ownership scan preserves original shaping and complete display runs."
|
||||||
|
(let* ((node (list :node-id 1 :region-id "owner"))
|
||||||
|
(display (list 'space :width '(15)))
|
||||||
|
(keymap (make-sparse-keymap))
|
||||||
|
(line (propertize "fi éabcQ" 'ebox-content-owner "owner"
|
||||||
|
'keymap keymap))
|
||||||
|
;; Original prefixes: fi shapes, the combining mark has no advance,
|
||||||
|
;; and abc is one EQ replacement run despite property boundaries.
|
||||||
|
(widths [0 7 11 18 25 25 40 40 40 47]))
|
||||||
|
(put-text-property 5 8 'display display line)
|
||||||
|
(dotimes (index (length line))
|
||||||
|
(put-text-property index (1+ index) 'source-position index line))
|
||||||
|
(cl-letf (((symbol-function 'ebox--string-pixel-width)
|
||||||
|
(lambda (source)
|
||||||
|
(should (equal-including-properties
|
||||||
|
source (substring line 0 (length source))))
|
||||||
|
(aref widths (length source)))))
|
||||||
|
(let* ((pieces (ebox-layer--pieces (list line) node))
|
||||||
|
(source (plist-get (car pieces) :line)))
|
||||||
|
(should (equal pieces (list (list :line line :start 0 :end 47 :y 0))))
|
||||||
|
(should (eq source line))
|
||||||
|
(should (eq (get-text-property 5 'display source) display))
|
||||||
|
(should (eq (get-text-property 7 'display source) display))
|
||||||
|
(should (eq (get-text-property 0 'keymap source) keymap))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-pieces-share-prefix-measurements-within-one-composition ()
|
||||||
|
"Sibling and anchor lookups share metrics without sharing mutable pieces."
|
||||||
|
(let* ((line (propertize "ABCD" 'ebox-content-owners '(0 "child" "child")))
|
||||||
|
(lines (list line))
|
||||||
|
(parent (list :node-id 1 :region-id 0))
|
||||||
|
(child (list :node-id 2 :region-id (copy-sequence "child")))
|
||||||
|
(measurements 0)
|
||||||
|
(ebox-layer--piece-width-cache (make-hash-table :test #'eq)))
|
||||||
|
(put-text-property 0 1 'ebox-ml "child" line)
|
||||||
|
(put-text-property 3 4 'ebox-mr "child" line)
|
||||||
|
(cl-letf (((symbol-function 'ebox--string-pixel-width)
|
||||||
|
(lambda (source)
|
||||||
|
(cl-incf measurements)
|
||||||
|
(length source))))
|
||||||
|
(let ((parent-pieces (ebox-layer--pieces lines parent))
|
||||||
|
(child-pieces (ebox-layer--pieces lines child)))
|
||||||
|
(should (= measurements 3))
|
||||||
|
(dotimes (_ 10)
|
||||||
|
(should (equal parent-pieces (ebox-layer--pieces lines parent)))
|
||||||
|
(should (equal child-pieces (ebox-layer--pieces lines child))))
|
||||||
|
(should (= measurements 3))
|
||||||
|
(should (equal parent-pieces
|
||||||
|
(list (list :line line :start 0 :end 4 :y 0))))
|
||||||
|
(should (equal child-pieces
|
||||||
|
(list (list :line line :start 1 :end 3 :y 0))))
|
||||||
|
(should-not (eq (car parent-pieces) (car child-pieces))))
|
||||||
|
;; An independently composed equal source keeps its own source identity.
|
||||||
|
(let* ((copy (copy-sequence line))
|
||||||
|
(pieces (ebox-layer--pieces (list copy) parent)))
|
||||||
|
(should (eq (plist-get (car pieces) :line) copy))
|
||||||
|
(should (= measurements 6))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-pieces-standalone-does-not-retain-prefix-widths ()
|
||||||
|
"An isolated lookup rebuilds metrics after measurement context changes."
|
||||||
|
(let* ((node (list :node-id 1 :region-id "owner"))
|
||||||
|
(line (propertize "AB" 'ebox-content-owner "owner"))
|
||||||
|
(lines (list line))
|
||||||
|
(scale 1)
|
||||||
|
(ebox-layer--piece-width-cache nil))
|
||||||
|
(cl-letf (((symbol-function 'ebox--string-pixel-width)
|
||||||
|
(lambda (source) (* scale (length source)))))
|
||||||
|
(should (= (plist-get (car (ebox-layer--pieces lines node)) :end) 2))
|
||||||
|
(setq scale 2)
|
||||||
|
(should (= (plist-get (car (ebox-layer--pieces lines node)) :end) 4))
|
||||||
|
(remove-text-properties 0 (length line) '(ebox-content-owner nil) line)
|
||||||
|
(should-not (ebox-layer--pieces lines node))
|
||||||
|
(should-not ebox-layer--piece-width-cache))))
|
||||||
|
|
||||||
(provide 'ebox-layer-tests)
|
(provide 'ebox-layer-tests)
|
||||||
;;; ebox-layer-tests.el ends here
|
;;; ebox-layer-tests.el ends here
|
||||||
|
|||||||
@ -94,6 +94,7 @@
|
|||||||
((string-match-p "canonical" name) 'ebox-canonical)
|
((string-match-p "canonical" name) 'ebox-canonical)
|
||||||
((string-match-p "render-burst" name) 'ebox-buffer-backend)
|
((string-match-p "render-burst" name) 'ebox-buffer-backend)
|
||||||
((string-match-p "render-owned-text-values" name) 'ebox-render-context)
|
((string-match-p "render-owned-text-values" name) 'ebox-render-context)
|
||||||
|
((string-prefix-p "ebox-layer--" name) 'ebox-layer)
|
||||||
((string-match-p "font\\|char-width\\|pixel-width\\|face-height\\|space-pixel\\|display-signature" name)
|
((string-match-p "font\\|char-width\\|pixel-width\\|face-height\\|space-pixel\\|display-signature" name)
|
||||||
'ebox-measure)
|
'ebox-measure)
|
||||||
((string-match-p "style" name) 'ebox-style)
|
((string-match-p "style" name) 'ebox-style)
|
||||||
@ -153,6 +154,7 @@
|
|||||||
ebox-incremental--allocated-slot-proof-cache
|
ebox-incremental--allocated-slot-proof-cache
|
||||||
ebox-incremental--candidate-path-copy-origin-table
|
ebox-incremental--candidate-path-copy-origin-table
|
||||||
ebox-incremental--candidate-proof-node-table
|
ebox-incremental--candidate-proof-node-table
|
||||||
|
ebox-layer--piece-width-cache
|
||||||
ebox-style--box-engine-longhand-cache ebox-style--closed-computed-cache
|
ebox-style--box-engine-longhand-cache ebox-style--closed-computed-cache
|
||||||
ebox-style--closed-inheritance-cache ebox-style--computed-snapshot-cache
|
ebox-style--closed-inheritance-cache ebox-style--computed-snapshot-cache
|
||||||
ebox-style--declaration-cache ebox-style--text-engine-longhand-cache
|
ebox-style--declaration-cache ebox-style--text-engine-longhand-cache
|
||||||
|
|||||||
372
tests/ebox-nested-scroll-tests.el
Normal file
372
tests/ebox-nested-scroll-tests.el
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
;;; ebox-nested-scroll-tests.el --- Cached nested scroll publication -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Fixed scroll allocations publish their retained windows locally while
|
||||||
|
;; preserving ancestor effects, native interaction and TP mount ownership.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defmacro ebox-nested-scroll-test--with-buffer (&rest body)
|
||||||
|
"Run BODY with an isolated mounted surface and the Elisp renderer."
|
||||||
|
(declare (indent 0) (debug t))
|
||||||
|
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(let ((ebox-viewport-width 104) (ebox-viewport-height 30)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(with-temp-buffer
|
||||||
|
(unwind-protect (progn ,@body)
|
||||||
|
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||||
|
(ebox-unmount-buffer (current-buffer))))))))
|
||||||
|
|
||||||
|
(defun ebox-nested-scroll-test--input (&optional layout chrome peers)
|
||||||
|
"Build a scroll owner under LAYOUT with CHROME and PEERS other boxes."
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (px 104) :color "#204060"
|
||||||
|
(box :id "before" "before")
|
||||||
|
(,(or layout 'column)
|
||||||
|
,@(pcase layout
|
||||||
|
('row '(:width (px 104)))
|
||||||
|
('flex '(:width (px 104) :gap ((lh 0) (px 2))))
|
||||||
|
('grid '(:width (px 104) :grid-template-columns ((px 80) (px 20)))))
|
||||||
|
:background-color "#E0E8F0" :padding-left (px 2)
|
||||||
|
(box :id "scroll" :width (px 80) :height (lh 5) :overflow scroll
|
||||||
|
,@(when chrome '(:padding ((lh 1) (px 1)) :border-width (px 1)
|
||||||
|
:border-style solid :border-color "#456789"))
|
||||||
|
,@(cl-loop for number below 30
|
||||||
|
collect `(box :id ,(format "line-%d" number)
|
||||||
|
:help-echo ,(format "line %d" number)
|
||||||
|
:pointer hand
|
||||||
|
:hover-style (:background-color "#FFFFA0")
|
||||||
|
:keymap (keymap (13 . ignore))
|
||||||
|
,(format "line %02d" number))))
|
||||||
|
(box :id "sibling" :width (px 20) "sibling"))
|
||||||
|
,@(cl-loop for index below (or peers 0)
|
||||||
|
collect `(box :id ,(format "peer-%d" index) "peer"))
|
||||||
|
(box :id "after" "after"))))
|
||||||
|
|
||||||
|
(defun ebox-nested-scroll-test--region (id)
|
||||||
|
"Return ID's runtime region in the current mounted buffer."
|
||||||
|
(cdr (ebox-selector--region-target (ebox-region-resolve (current-buffer) id))))
|
||||||
|
|
||||||
|
(defun ebox-nested-scroll-test--mounts (&optional preserve-order-p)
|
||||||
|
"Return semantic IDs and their live mount ranges in the current buffer.
|
||||||
|
PRESERVE-ORDER-P retains attachment order for exact rollback checks."
|
||||||
|
(mapcar
|
||||||
|
(lambda (id)
|
||||||
|
(let* ((region (ebox-nested-scroll-test--region id))
|
||||||
|
(state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(object (gethash region (plist-get state :region-surface-object-table))))
|
||||||
|
(let ((ranges (mapcar (lambda (mount)
|
||||||
|
(cons (plist-get mount :start) (plist-get mount :end)))
|
||||||
|
(tp-object-mounts object))))
|
||||||
|
(cons id (if preserve-order-p ranges
|
||||||
|
(sort ranges
|
||||||
|
(lambda (left right)
|
||||||
|
(if (= (car left) (car right)) (< (cdr left) (cdr right))
|
||||||
|
(< (car left) (car right))))))))))
|
||||||
|
(append '("before" "scroll" "sibling" "after")
|
||||||
|
(cl-loop for index below 30 collect (format "line-%d" index)))))
|
||||||
|
|
||||||
|
(defun ebox-nested-scroll-test--parity ()
|
||||||
|
"Compare current paint, interactions and mounts with fresh committed input."
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(ebox-viewport-width (plist-get state :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get state :viewport-height))
|
||||||
|
(input (plist-get (ebox-surface-buffer-snapshot (current-buffer)) :input))
|
||||||
|
(actual (buffer-string))
|
||||||
|
(mounts (ebox-nested-scroll-test--mounts)))
|
||||||
|
(with-temp-buffer
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer (current-buffer) input)
|
||||||
|
(should (equal (substring-no-properties actual)
|
||||||
|
(buffer-substring-no-properties (point-min) (point-max))))
|
||||||
|
(dotimes (position (length actual))
|
||||||
|
(dolist (property '(face display help-echo mouse-face pointer keymap))
|
||||||
|
(should (equal (get-text-property position property actual)
|
||||||
|
(get-text-property (1+ position) property)))))
|
||||||
|
(should (equal mounts (ebox-nested-scroll-test--mounts))))
|
||||||
|
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||||
|
(ebox-unmount-buffer (current-buffer)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-retained-fragments-own-newline-separators ()
|
||||||
|
"Retained paint ledgers cover separators with both neighboring owners."
|
||||||
|
(let* ((lines (list (propertize "before" 'ebox-content-owner 1)
|
||||||
|
(propertize "inside" 'ebox-content-owner 2)
|
||||||
|
(propertize "after" 'ebox-content-owner 3)))
|
||||||
|
(output (ebox-lines-join lines))
|
||||||
|
(retained (ebox-surface--scroll-fragment-data lines output))
|
||||||
|
(fresh (ebox-surface--rendered-fragments (copy-sequence output))))
|
||||||
|
(cl-labels ((roles-at (fragments position)
|
||||||
|
(plist-get
|
||||||
|
(cl-find-if (lambda (fragment)
|
||||||
|
(and (<= (plist-get fragment :start) position)
|
||||||
|
(< position (plist-get fragment :end))))
|
||||||
|
fragments)
|
||||||
|
:role-ids)))
|
||||||
|
(dotimes (position (length output))
|
||||||
|
(should (equal (roles-at retained position) (roles-at fresh position))))
|
||||||
|
(should (equal (roles-at retained 6) '((content-owner . 1) (content-owner . 2))))
|
||||||
|
(should (equal (roles-at retained 13) '((content-owner . 2) (content-owner . 3)))))
|
||||||
|
;; Leading/trailing unowned runs also depend on the adjacent line, so
|
||||||
|
;; these retain the complete scanner until that wider context is proved.
|
||||||
|
(dolist (boundary '(" unowned" "unowned "))
|
||||||
|
(let ((line (copy-sequence boundary)))
|
||||||
|
(put-text-property 1 (1- (length line)) 'ebox-content-owner 1 line)
|
||||||
|
(should-not (ebox-surface--scroll-line-fragment-template line))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-cached-window-does-not-render-siblings ()
|
||||||
|
"One cached nested window avoids root renders and unrelated node work."
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-nested-scroll-test--input nil nil 200))
|
||||||
|
(let* ((region (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(owner (ebox--buffer-region-render-owner-node-id (current-buffer) region))
|
||||||
|
(child-region (ebox-nested-scroll-test--region "line-0"))
|
||||||
|
(child (ebox--buffer-region-render-owner-node (current-buffer) child-region))
|
||||||
|
(root-renders 0) (index-builds 0)
|
||||||
|
(render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(layout (symbol-function 'ebox--render-layout))
|
||||||
|
(index (symbol-function 'ebox--runtime-index)))
|
||||||
|
(dolist (delta '(1 1 -1 -1))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf root-renders) (apply render arguments)))
|
||||||
|
((symbol-function 'ebox--runtime-index)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf index-builds) (apply index arguments)))
|
||||||
|
((symbol-function 'ebox--render-layout)
|
||||||
|
(lambda (node)
|
||||||
|
(should (equal (plist-get node :node-id) owner))
|
||||||
|
(funcall layout node))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by
|
||||||
|
(current-buffer) region delta nil) delta)))
|
||||||
|
(should (eq child (ebox--buffer-region-render-owner-node
|
||||||
|
(current-buffer) child-region)))
|
||||||
|
(ebox-nested-scroll-test--parity))
|
||||||
|
(should (= root-renders 0))
|
||||||
|
(should (= index-builds 0))
|
||||||
|
(ebox-nested-scroll-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-preserves-allocated-layout-and-chrome ()
|
||||||
|
"Column, Row, Flex and Grid owners retain paint, slots and scroll identities."
|
||||||
|
(dolist (layout '(column row flex grid))
|
||||||
|
(dolist (chrome '(nil t))
|
||||||
|
(ert-info ((format "layout=%S chrome=%S" layout chrome))
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer)
|
||||||
|
(ebox-nested-scroll-test--input layout chrome))
|
||||||
|
(let* ((region (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(old-state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(old-box (gethash region (plist-get old-state :region-box-table)))
|
||||||
|
(old-scroll (gethash region (plist-get old-state :scroll-state-table))))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Nested allocation rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by
|
||||||
|
(current-buffer) region 1 nil) 1)))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(box (gethash region (plist-get state :region-box-table)))
|
||||||
|
(scroll (gethash region (plist-get state :scroll-state-table))))
|
||||||
|
(should-not (eq old-box box))
|
||||||
|
(should (eq box (plist-get scroll :box)))
|
||||||
|
(should (= (or (ebox-get old-box :scroll-offset) 0) 0))
|
||||||
|
(should (= (plist-get old-scroll :scroll-offset) 0))
|
||||||
|
(should (= (ebox-get box :scroll-offset) 1))
|
||||||
|
(should (= (plist-get scroll :scroll-offset) 1)))
|
||||||
|
(ebox-nested-scroll-test--parity)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-independent-owners-retain-their-caches ()
|
||||||
|
"Side-by-side scrolls keep independent offsets across content and resize."
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-nested-scroll-test--input 'row))
|
||||||
|
(ebox-region-update "sibling" :height '(lh 5) :overflow 'scroll
|
||||||
|
:content "sibling 0\nsibling 1\nsibling 2\nsibling 3\nsibling 4\nsibling 5\nsibling 6\nsibling 7")
|
||||||
|
(let ((left (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(right (ebox-nested-scroll-test--region "sibling")))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Independent scroll rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) right 2 nil) 2)))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll (gethash right (plist-get state :scroll-state-table)))
|
||||||
|
(lines (plist-get scroll :content-lines)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Independent scroll rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) left 1 nil) 1)))
|
||||||
|
(let ((next (gethash right (plist-get (ebox--buffer-render-state
|
||||||
|
(current-buffer)) :scroll-state-table))))
|
||||||
|
(should (= (plist-get next :scroll-offset) 2))
|
||||||
|
(should (eq (plist-get next :content-lines) lines))))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(ebox-region-update "line-1" :content "UPDATED")
|
||||||
|
(should (string-match-p "UPDATED" (buffer-string)))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(ebox-rerender-buffer-with-context (current-buffer) 120 35)
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) left -1 nil) -1))
|
||||||
|
(ebox-nested-scroll-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-rejection-restores-local-and-fallback-state ()
|
||||||
|
"TP rejection restores text, mounts and runtime on local and declined slots."
|
||||||
|
(dolist (fallback '(nil t))
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-nested-scroll-test--input))
|
||||||
|
;; Arbitrary inherited interactions deliberately require full projection.
|
||||||
|
(when fallback (ebox-region-update "root" :help-echo "enclosing action"))
|
||||||
|
(let* ((region (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll (gethash region (plist-get state :scroll-state-table)))
|
||||||
|
(box (plist-get scroll :box))
|
||||||
|
(before (buffer-string))
|
||||||
|
(mounts (ebox-nested-scroll-test--mounts t))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(root-renders 0)
|
||||||
|
(render (symbol-function 'ebox-surface--render-candidate)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf root-renders) (apply render arguments))))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state) (error "Reject nested scroll")))))
|
||||||
|
(should-error (ebox--surface-scroll-region-by
|
||||||
|
(current-buffer) region 1 nil))))
|
||||||
|
(should (eq state (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (equal mounts (ebox-nested-scroll-test--mounts t)))
|
||||||
|
(should (= (plist-get scroll :scroll-offset) 0))
|
||||||
|
(should (= (or (ebox-get box :scroll-offset) 0) 0))
|
||||||
|
(should (if fallback (> root-renders 0) (= root-renders 0)))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) region 1 nil) 1))
|
||||||
|
(ebox-nested-scroll-test--parity)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-updates-enclosing-cache-and-role-membership ()
|
||||||
|
"Inner motion repairs enclosing cache coordinates before outer scrolling."
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-nested-scroll-test--input nil nil 200))
|
||||||
|
(ebox-region-update "root" :height '(lh 15) :overflow 'scroll)
|
||||||
|
(let ((inner (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(outer (ebox-nested-scroll-test--region "root"))
|
||||||
|
(hidden (ebox-nested-scroll-test--region "line-0"))
|
||||||
|
(visible (ebox-nested-scroll-test--region "line-5")))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Inner scroll rendered enclosing root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1)))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll (gethash outer (plist-get state :scroll-state-table)))
|
||||||
|
(members (ebox--scroll-state-ensure-content-region-id-set scroll)))
|
||||||
|
(should (eq (plist-get scroll :box) (plist-get state :root-node)))
|
||||||
|
(should-not (gethash hidden members))
|
||||||
|
(should (gethash visible members))
|
||||||
|
(let ((bounds (plist-get scroll :region-line-bounds-index))
|
||||||
|
(fresh (ebox--scroll-build-region-line-bounds-index
|
||||||
|
(plist-get scroll :content-lines))))
|
||||||
|
(should (= (hash-table-count bounds) (hash-table-count fresh)))
|
||||||
|
(maphash (lambda (id range) (should (equal range (gethash id bounds)))) fresh)))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(ebox-region-update "line-5" :content "NEW VISIBLE")
|
||||||
|
(should (string-match-p "NEW VISIBLE" (buffer-string)))
|
||||||
|
(ebox-region-update "line-0" :content "NEW HIDDEN")
|
||||||
|
(should-not (string-match-p "NEW HIDDEN" (buffer-string)))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer 2 nil) 2))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
;; The inner allocation is partially clipped at this outer offset.
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer 10 nil) 10))
|
||||||
|
(let ((before (buffer-string)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Hidden inner scroll rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1)))
|
||||||
|
(should (equal (substring-no-properties before) (buffer-string)))
|
||||||
|
(dotimes (position (length before))
|
||||||
|
(dolist (property '(face font-lock-face display help-echo keymap mouse-face pointer))
|
||||||
|
(should (equal (get-text-property position property before)
|
||||||
|
(get-text-property (1+ position) property)))))
|
||||||
|
(ebox-nested-scroll-test--parity))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer -12 nil) -12))
|
||||||
|
(ebox-nested-scroll-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-enclosing-cache-rejection-is-atomic ()
|
||||||
|
"Rejected inner publication preserves both scroll caches and source boxes."
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-nested-scroll-test--input))
|
||||||
|
(ebox-region-update "root" :height '(lh 15) :overflow 'scroll)
|
||||||
|
(let* ((inner (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(outer (ebox-nested-scroll-test--region "root"))
|
||||||
|
(state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(table (plist-get state :scroll-state-table))
|
||||||
|
(before (buffer-string))
|
||||||
|
(mounts (ebox-nested-scroll-test--mounts t))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(outer-state (gethash outer table))
|
||||||
|
(outer-lines (plist-get outer-state :content-lines)))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state) (error "Reject enclosing cache")))))
|
||||||
|
(should-error (ebox--surface-scroll-region-by (current-buffer) inner 1 nil)))
|
||||||
|
(should (eq state (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (eq outer-lines (plist-get (gethash outer table) :content-lines)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (equal mounts (ebox-nested-scroll-test--mounts t)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (= (plist-get (gethash inner table) :scroll-offset) 0))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1))
|
||||||
|
(ebox-nested-scroll-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-nested-scroll-enclosing-prefix-and-independent-owner-stay-current ()
|
||||||
|
"Ancestor prefix growth, materialization and resize retain inner state."
|
||||||
|
(ebox-nested-scroll-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (px 104) :height (vh 50) :overflow scroll
|
||||||
|
(box :id "before" "before")
|
||||||
|
(column :id "scroll" :width (px 80) :height (vh 20) :overflow scroll
|
||||||
|
,@(cl-loop for n below 100 collect
|
||||||
|
`(box :id ,(format "line-%d" n) :help-echo ,(format "Action %d" n)
|
||||||
|
,(format "line %02d" n))))
|
||||||
|
(box :id "sibling" :width (px 20) :height (lh 6) :overflow scroll
|
||||||
|
"sibling 0\nsibling 1\nsibling 2\nsibling 3\nsibling 4\nsibling 5\nsibling 6\nsibling 7")
|
||||||
|
,@(cl-loop repeat 200 collect '(box "peer"))
|
||||||
|
(box :id "after" "after"))))
|
||||||
|
(let* ((inner (ebox-nested-scroll-test--region "scroll"))
|
||||||
|
(outer (ebox-nested-scroll-test--region "root"))
|
||||||
|
(other (ebox-nested-scroll-test--region "sibling"))
|
||||||
|
(old-outer (ebox--scroll-get-state outer))
|
||||||
|
(old-producer (plist-get old-outer :render-content-prefix))
|
||||||
|
(count (length (plist-get old-outer :content-lines))))
|
||||||
|
(should old-producer)
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Cached inner motion rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 1) 1)))
|
||||||
|
(should-not (eq old-producer (plist-get (ebox--scroll-get-state outer) :render-content-prefix)))
|
||||||
|
(let ((lines (plist-get (ebox--scroll-get-state inner) :content-lines)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _) (ert-fail "Independent inner motion rendered root"))))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) other 1 1) 1)))
|
||||||
|
(should (eq lines (plist-get (ebox--scroll-get-state inner) :content-lines))))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(dotimes (_ 40)
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer 1 1) 1)))
|
||||||
|
(should (> (length (plist-get (ebox--scroll-get-state outer) :content-lines)) count))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer -40 1) -40))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(ebox--scroll-state-materialize-lines outer (ebox--scroll-get-state outer))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer 1 1) 1))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) outer -1 1) -1))
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(ebox-rerender-buffer-with-context (current-buffer) 120 40)
|
||||||
|
(ebox-nested-scroll-test--parity)
|
||||||
|
(should (= (plist-get (ebox--scroll-get-state inner) :scroll-offset) 1))
|
||||||
|
(should (= (plist-get (ebox--scroll-get-state other) :scroll-offset) 1))
|
||||||
|
(should (= (ebox--surface-scroll-region-by (current-buffer) inner -1 1) -1))
|
||||||
|
(ebox-nested-scroll-test--parity))))
|
||||||
|
|
||||||
|
(provide 'ebox-nested-scroll-tests)
|
||||||
|
;;; ebox-nested-scroll-tests.el ends here
|
||||||
330
tests/ebox-scroll-cold-tests.el
Normal file
330
tests/ebox-scroll-cold-tests.el
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
;;; ebox-scroll-cold-tests.el --- Lazy scroll continuation boundaries -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; A real lazy-prefix miss must stage both continuation data and source-node
|
||||||
|
;; geometry before TP publishes text or client state. Rejected candidates
|
||||||
|
;; leave every published scroll owner ready to retry the same miss.
|
||||||
|
;; A published cache hit must also keep later prefix and materializer work
|
||||||
|
;; attached to the current owner instead of reviving its retired predecessor.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defmacro ebox-scroll-cold-test--with-buffer (&rest body)
|
||||||
|
"Run BODY in an isolated mounted Elisp surface with no idle prefix work."
|
||||||
|
(declare (indent 0) (debug t))
|
||||||
|
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(let ((ebox-viewport-width 80) (ebox-viewport-height 8)
|
||||||
|
(ebox-native-buffer-scroll nil)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||||
|
(ebox-scroll-lazy-prefix-lookahead-lines 0)
|
||||||
|
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||||
|
(ebox-scroll-lazy-idle-prefetch-delay 999)
|
||||||
|
(ebox--scroll-window-initial-lookahead-lines-override 0))
|
||||||
|
(with-temp-buffer
|
||||||
|
(unwind-protect (progn ,@body)
|
||||||
|
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||||
|
(ebox-unmount-buffer (current-buffer))))))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--input (boundary)
|
||||||
|
"Build a lazy owner at root, nested, or enclosing-scroll BOUNDARY."
|
||||||
|
(let ((target
|
||||||
|
`(column :id "target" :width (px 80) :height (vh 25) :overflow scroll
|
||||||
|
,@(cl-loop for index below 40
|
||||||
|
collect `(box :id ,(format "row-%d" index)
|
||||||
|
:height (lh 1)
|
||||||
|
:color "#204060"
|
||||||
|
:help-echo ,(format "Action %d" index)
|
||||||
|
:pointer hand
|
||||||
|
:keymap (keymap (13 . ignore))
|
||||||
|
,(format "row %02d" index))))))
|
||||||
|
(ebox-build
|
||||||
|
(if (eq boundary 'root) target
|
||||||
|
`(column :id "container" :width (px 80)
|
||||||
|
,@(when (eq boundary 'enclosing-scroll)
|
||||||
|
'(:height (vh 50) :overflow scroll))
|
||||||
|
(box :id "head" "head")
|
||||||
|
,target
|
||||||
|
(box :id "tail" :height (lh 8) "tail"))))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--assert-eq (label expected actual)
|
||||||
|
"Require identity of EXPECTED and ACTUAL, reporting only bounded LABEL."
|
||||||
|
(ert-info ((format "%s retains identity" label))
|
||||||
|
(let ((unchanged (eq expected actual))) (should unchanged))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--assert-equal (label expected actual)
|
||||||
|
"Compare EXPECTED and ACTUAL without printing runtime objects on failure."
|
||||||
|
(ert-info (label)
|
||||||
|
(let ((unchanged (equal expected actual))) (should unchanged))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--index-facts (state)
|
||||||
|
"Copy STATE's bounded region-index values and cache validity flags."
|
||||||
|
(cl-loop for key in '(:region-line-bounds-index :region-line-span-index
|
||||||
|
:rendered-region-line-span-index :content-region-id-set
|
||||||
|
:region-line-span-hints :region-line-span-index-deferred
|
||||||
|
:rendered-region-line-span-index-deferred
|
||||||
|
:content-lines-complete-p :lazy-scroll-prefix-dirty
|
||||||
|
:lazy-scroll-window-refresh-required :scroll-offset)
|
||||||
|
for value = (plist-get state key)
|
||||||
|
collect
|
||||||
|
(cons key
|
||||||
|
(if (hash-table-p value)
|
||||||
|
(let (entries)
|
||||||
|
(maphash (lambda (region spans)
|
||||||
|
(push (cons region (copy-tree spans)) entries))
|
||||||
|
value)
|
||||||
|
(sort entries
|
||||||
|
(lambda (left right)
|
||||||
|
(string< (prin1-to-string (car left))
|
||||||
|
(prin1-to-string (car right))))))
|
||||||
|
(copy-tree value)))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--node-facts (node)
|
||||||
|
"Copy NODE's source geometry and lazy-layout completion facts."
|
||||||
|
(cl-loop for key in '(:width :height :min-width :max-width :min-height :max-height
|
||||||
|
:padding-left-pixel :padding-right-pixel
|
||||||
|
:padding-top-height :padding-bottom-height
|
||||||
|
:margin-left-pixel :margin-right-pixel
|
||||||
|
:margin-top-height :margin-bottom-height
|
||||||
|
:border-left-pixel :border-right-pixel
|
||||||
|
:scroll-offset :ebox-scroll-offset-controlled-p
|
||||||
|
:ebox-content-layout-complete-p :ebox-content-width-exact-p)
|
||||||
|
collect (list key (and (plist-member node key) t)
|
||||||
|
(copy-tree (plist-get node key)))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--snapshot ()
|
||||||
|
"Snapshot published identities plus bounded line, index, and node facts."
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(table (plist-get runtime :scroll-state-table))
|
||||||
|
scrolls nodes)
|
||||||
|
(maphash
|
||||||
|
(lambda (region state)
|
||||||
|
(push (list :region region :state state :box (plist-get state :box)
|
||||||
|
:producer (plist-get state :render-content-prefix)
|
||||||
|
:materializer (plist-get state :materialize-content-lines)
|
||||||
|
:content-head (plist-get state :content-lines)
|
||||||
|
:rendered-head (plist-get state :rendered-content-lines)
|
||||||
|
:content (mapcar #'tp-text-snapshot (plist-get state :content-lines))
|
||||||
|
:rendered (mapcar #'tp-text-snapshot (plist-get state :rendered-content-lines))
|
||||||
|
:index-facts (ebox-scroll-cold-test--index-facts state))
|
||||||
|
scrolls))
|
||||||
|
table)
|
||||||
|
(ebox-runtime-index-map
|
||||||
|
(lambda (node-id node)
|
||||||
|
(push (list node-id node (ebox-scroll-cold-test--node-facts node)) nodes))
|
||||||
|
(plist-get runtime :node-table))
|
||||||
|
(list :runtime runtime :root (plist-get runtime :root-node)
|
||||||
|
:source-index (plist-get runtime :source-index)
|
||||||
|
:region-box-registry ebox--region-box-table
|
||||||
|
:region-box-entries (copy-hash-table ebox--region-box-table)
|
||||||
|
:scrolls scrolls :nodes nodes :text (tp-text-snapshot (buffer-string))
|
||||||
|
:revision (ebox-surface-buffer-revision (current-buffer)))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--assert-lines (label expected actual)
|
||||||
|
"Require unchanged cached line counts and full properties for LABEL."
|
||||||
|
(ert-info ((format "%s line count" label))
|
||||||
|
(should (= (length expected) (length actual))))
|
||||||
|
(cl-loop for before in expected for after in actual for row from 0
|
||||||
|
do (ert-info ((format "%s row %d properties" label row))
|
||||||
|
(let ((unchanged (equal-including-properties before after)))
|
||||||
|
(should unchanged)))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--assert-snapshot (snapshot)
|
||||||
|
"Verify SNAPSHOT's complete published boundary after a rejected scroll."
|
||||||
|
(let ((runtime (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(ebox-scroll-cold-test--assert-eq "Runtime" (plist-get snapshot :runtime) runtime)
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
"Root" (plist-get snapshot :root) (plist-get runtime :root-node))
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
"Source index" (plist-get snapshot :source-index) (plist-get runtime :source-index))
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
"Global region registry" (plist-get snapshot :region-box-registry) ebox--region-box-table)
|
||||||
|
(let ((entries (plist-get snapshot :region-box-entries)))
|
||||||
|
(should (= (hash-table-count entries) (hash-table-count ebox--region-box-table)))
|
||||||
|
(maphash (lambda (region box)
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
(format "Global region %s" region) box (gethash region ebox--region-box-table)))
|
||||||
|
entries))
|
||||||
|
(should (= (plist-get snapshot :revision)
|
||||||
|
(ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(let ((unchanged (equal-including-properties (plist-get snapshot :text) (buffer-string))))
|
||||||
|
(should unchanged))
|
||||||
|
(dolist (entry (plist-get snapshot :scrolls))
|
||||||
|
(let* ((region (plist-get entry :region))
|
||||||
|
(state (gethash region (plist-get runtime :scroll-state-table))))
|
||||||
|
(ebox-scroll-cold-test--assert-eq "Scroll owner" (plist-get entry :state) state)
|
||||||
|
(dolist (pair '((:box . :box) (:producer . :render-content-prefix)
|
||||||
|
(:materializer . :materialize-content-lines)
|
||||||
|
(:content-head . :content-lines) (:rendered-head . :rendered-content-lines)))
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
(format "Region %s %s" region (car pair))
|
||||||
|
(plist-get entry (car pair)) (plist-get state (cdr pair))))
|
||||||
|
(ebox-scroll-cold-test--assert-lines
|
||||||
|
(format "Region %s content" region)
|
||||||
|
(plist-get entry :content) (plist-get state :content-lines))
|
||||||
|
(ebox-scroll-cold-test--assert-lines
|
||||||
|
(format "Region %s rendered" region)
|
||||||
|
(plist-get entry :rendered) (plist-get state :rendered-content-lines))
|
||||||
|
(ebox-scroll-cold-test--assert-equal
|
||||||
|
(format "Region %s indexes and offset" region)
|
||||||
|
(plist-get entry :index-facts) (ebox-scroll-cold-test--index-facts state))))
|
||||||
|
(dolist (entry (plist-get snapshot :nodes))
|
||||||
|
(let ((node (ebox-runtime-index-get (car entry) (plist-get runtime :node-table))))
|
||||||
|
(ebox-scroll-cold-test--assert-eq "Published source child" (cadr entry) node)
|
||||||
|
(ebox-scroll-cold-test--assert-equal
|
||||||
|
(format "Source node %s geometry and completion" (car entry))
|
||||||
|
(nth 2 entry) (ebox-scroll-cold-test--node-facts node))))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--assert-fresh ()
|
||||||
|
"Compare committed text and interactions with independent source rendering."
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(input (plist-get (ebox-surface-buffer-snapshot (current-buffer)) :input))
|
||||||
|
(actual (buffer-string))
|
||||||
|
(ebox-viewport-width (plist-get runtime :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get runtime :viewport-height))
|
||||||
|
(render (symbol-function 'ebox--render-layout))
|
||||||
|
offsets first fresh)
|
||||||
|
(maphash
|
||||||
|
(lambda (_region state)
|
||||||
|
(push (cons (ebox-tree-node-id (plist-get runtime :source-index) (plist-get state :box))
|
||||||
|
(plist-get state :scroll-offset)) offsets))
|
||||||
|
(plist-get runtime :scroll-state-table))
|
||||||
|
(cl-letf (((symbol-function 'ebox--render-layout)
|
||||||
|
(lambda (root)
|
||||||
|
(unless first
|
||||||
|
(setq first t)
|
||||||
|
(cl-labels ((visit (node)
|
||||||
|
(when-let* ((entry (assoc (ebox-tree-node-id ebox--render-source-index node)
|
||||||
|
offsets)))
|
||||||
|
(ebox-put node :scroll-offset (cdr entry))
|
||||||
|
(ebox-put node :ebox-scroll-offset-controlled-p t))
|
||||||
|
(mapc #'visit (ebox-tree-node-children node))))
|
||||||
|
(visit root)))
|
||||||
|
(funcall render root))))
|
||||||
|
(setq fresh (ebox-render input)))
|
||||||
|
(should (equal (substring-no-properties actual) (substring-no-properties fresh)))
|
||||||
|
(should (= (length actual) (length fresh)))
|
||||||
|
(dotimes (position (length actual))
|
||||||
|
(dolist (property '(face font-lock-face display help-echo keymap mouse-face pointer))
|
||||||
|
(ebox-scroll-cold-test--assert-equal
|
||||||
|
(format "Fresh property %S at %d" property position)
|
||||||
|
(get-text-property position property actual)
|
||||||
|
(get-text-property position property fresh))))))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--rollback (boundary failure-step)
|
||||||
|
"Reject a true lazy-prefix miss at BOUNDARY during FAILURE-STEP, then retry."
|
||||||
|
(ebox-scroll-cold-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-scroll-cold-test--input boundary))
|
||||||
|
(let* ((region (cdr (ebox-selector--region-target (ebox-region-resolve (current-buffer) "target"))))
|
||||||
|
(state (ebox--scroll-get-state region))
|
||||||
|
(count (length (plist-get state :content-lines)))
|
||||||
|
(height (plist-get state :content-height))
|
||||||
|
(delta (1+ (max 0 (- count height))))
|
||||||
|
(snapshot (ebox-scroll-cold-test--snapshot))
|
||||||
|
(ensure (symbol-function 'ebox--scroll-state-ensure-prefix-lines))
|
||||||
|
prefix-extended rejected)
|
||||||
|
(should (functionp (plist-get state :render-content-prefix)))
|
||||||
|
(should-not (plist-get state :content-lines-complete-p))
|
||||||
|
(should (= count 4))
|
||||||
|
(should (= (length (plist-get state :rendered-content-lines)) count))
|
||||||
|
(should (> (+ delta height) count))
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-state-ensure-prefix-lines)
|
||||||
|
(lambda (owner candidate required &rest options)
|
||||||
|
(let ((result (apply ensure owner candidate required options)))
|
||||||
|
(when (and (equal owner region)
|
||||||
|
(> (length (plist-get result :content-lines)) count))
|
||||||
|
(setq prefix-extended t))
|
||||||
|
result))))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step)
|
||||||
|
(setq rejected t)
|
||||||
|
(error "Reject cold scroll publication")))))
|
||||||
|
(should-error (ebox--surface-scroll-region-by (current-buffer) region delta 1))))
|
||||||
|
(should rejected)
|
||||||
|
(should prefix-extended)
|
||||||
|
(ebox-scroll-cold-test--assert-snapshot snapshot)
|
||||||
|
;; The mounted state still has its original producer; retrying the same
|
||||||
|
;; miss must produce the requested rows instead of skipping leaked work.
|
||||||
|
(should (= delta (ebox--surface-scroll-region-by (current-buffer) region delta 1)))
|
||||||
|
(should (= delta (plist-get (ebox--scroll-get-state region) :scroll-offset)))
|
||||||
|
(should (>= (length (plist-get (ebox--scroll-get-state region) :content-lines))
|
||||||
|
(+ delta height)))
|
||||||
|
(ebox-scroll-cold-test--assert-fresh)
|
||||||
|
(should (= (- delta) (ebox--surface-scroll-region-by (current-buffer) region (- delta) 1)))
|
||||||
|
(ebox-scroll-cold-test--assert-fresh))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-cold-root-text-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'root 'text))
|
||||||
|
(ert-deftest ebox-scroll-cold-root-client-state-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'root 'client-state))
|
||||||
|
(ert-deftest ebox-scroll-cold-nested-text-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'nested 'text))
|
||||||
|
(ert-deftest ebox-scroll-cold-nested-client-state-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'nested 'client-state))
|
||||||
|
(ert-deftest ebox-scroll-cold-enclosing-scroll-text-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'enclosing-scroll 'text))
|
||||||
|
(ert-deftest ebox-scroll-cold-enclosing-scroll-client-state-rejection ()
|
||||||
|
(ebox-scroll-cold-test--rollback 'enclosing-scroll 'client-state))
|
||||||
|
|
||||||
|
(defun ebox-scroll-cold-test--continue-after-hit (caller)
|
||||||
|
"Run CALLER after a cache hit and verify the continuation's published owner."
|
||||||
|
(ebox-scroll-cold-test--with-buffer
|
||||||
|
(ebox-render-to-buffer (current-buffer) (ebox-scroll-cold-test--input 'root))
|
||||||
|
(let* ((region (cdr (ebox-selector--region-target
|
||||||
|
(ebox-region-resolve (current-buffer) "target"))))
|
||||||
|
(initial (ebox--scroll-get-state region))
|
||||||
|
(count (length (plist-get initial :content-lines))))
|
||||||
|
(should (= count 4))
|
||||||
|
(should-not (plist-get initial :content-lines-complete-p))
|
||||||
|
(should (ebox--surface-scroll-cached-intent-p initial 1))
|
||||||
|
(should (= 1 (ebox--surface-scroll-region-by (current-buffer) region 1 1)))
|
||||||
|
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(owner-id (ebox--buffer-region-render-owner-node-id (current-buffer) region))
|
||||||
|
(owner (ebox-runtime-index-get owner-id (plist-get runtime :node-table)))
|
||||||
|
(state (ebox--scroll-get-state region)))
|
||||||
|
(should (= count (length (plist-get state :content-lines))))
|
||||||
|
(ebox-scroll-cold-test--assert-eq "Hit owner" owner (plist-get state :box))
|
||||||
|
(should (functionp (plist-get state :render-content-prefix)))
|
||||||
|
(should (functionp (plist-get state :materialize-content-lines)))
|
||||||
|
(pcase caller
|
||||||
|
('prefix
|
||||||
|
(setq state (ebox--scroll-state-ensure-prefix-lines region state (1+ count) t)))
|
||||||
|
('idle
|
||||||
|
(let ((ebox-scroll-lazy-idle-prefetch-lines 2)
|
||||||
|
(ebox-scroll-lazy-idle-prefetch-slice-lines 2))
|
||||||
|
;; Invoke the actual idle callback once without scheduling another
|
||||||
|
;; turn; its own bounded-prefix logic remains exercised.
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch)
|
||||||
|
(lambda (&rest _) nil)))
|
||||||
|
(ebox--scroll-idle-prefetch region)))
|
||||||
|
(setq state (ebox--scroll-get-state region)))
|
||||||
|
('materializer
|
||||||
|
(setq state (ebox--scroll-state-materialize-lines region state))
|
||||||
|
(should (plist-get state :content-lines-complete-p))))
|
||||||
|
(should (> (length (plist-get state :content-lines)) count))
|
||||||
|
(should (= 1 (plist-get state :scroll-offset)))
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
"Continued owner" owner (plist-get state :box))
|
||||||
|
(ebox-scroll-cold-test--assert-eq
|
||||||
|
"Current owner after continuation" owner
|
||||||
|
(ebox-runtime-index-get owner-id
|
||||||
|
(plist-get (ebox--buffer-render-state (current-buffer)) :node-table)))
|
||||||
|
(ebox-scroll-cold-test--assert-fresh)
|
||||||
|
(should (= -1 (ebox--surface-scroll-region-by (current-buffer) region -1 1)))
|
||||||
|
(ebox-scroll-cold-test--assert-fresh)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-cold-root-hit-prefix-keeps-current-owner ()
|
||||||
|
(ebox-scroll-cold-test--continue-after-hit 'prefix))
|
||||||
|
(ert-deftest ebox-scroll-cold-root-hit-idle-keeps-current-owner ()
|
||||||
|
(ebox-scroll-cold-test--continue-after-hit 'idle))
|
||||||
|
(ert-deftest ebox-scroll-cold-root-hit-materializer-keeps-current-owner ()
|
||||||
|
(ebox-scroll-cold-test--continue-after-hit 'materializer))
|
||||||
|
|
||||||
|
(provide 'ebox-scroll-cold-tests)
|
||||||
|
;;; ebox-scroll-cold-tests.el ends here
|
||||||
201
tests/ebox-scroll-slice-tests.el
Normal file
201
tests/ebox-scroll-slice-tests.el
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
;;; ebox-scroll-slice-tests.el --- Retained visible scroll slices -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Complete line caches index stable cons handles, preserving current row
|
||||||
|
;; strings through equal-length rewrites and isolated publication rollback.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defun ebox-scroll-slice-test--lines (count)
|
||||||
|
"Return COUNT independently allocated deterministic source lines."
|
||||||
|
(cl-loop for index below count collect (format "Row %05d" index)))
|
||||||
|
|
||||||
|
(defun ebox-scroll-slice-test--state (lines &optional incomplete)
|
||||||
|
"Return a scroll state containing LINES, or a lazy prefix if INCOMPLETE."
|
||||||
|
(list :content-height 3 :scroll-offset 0
|
||||||
|
:content-lines lines :rendered-content-lines lines
|
||||||
|
:content-lines-complete-p (not incomplete)
|
||||||
|
:render-content-prefix (and incomplete #'ignore)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-complete-window-avoids-prefix-walks ()
|
||||||
|
"Cached windows index only visible rows even at a distant document offset."
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines 10000))
|
||||||
|
(state (ebox-scroll-slice-test--state lines))
|
||||||
|
(expected (seq-subseq lines 9000 9003))
|
||||||
|
(native-comp-enable-subr-trampolines nil)
|
||||||
|
(length-function (symbol-function 'length))
|
||||||
|
(subseq-function (symbol-function 'seq-subseq))
|
||||||
|
(full-lengths 0) (list-slices 0))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(should (vectorp (plist-get state :rendered-content-line-cells)))
|
||||||
|
(plist-put state :scroll-offset 9000)
|
||||||
|
(cl-letf (((symbol-function 'length)
|
||||||
|
(lambda (value)
|
||||||
|
(when (eq value lines) (cl-incf full-lengths))
|
||||||
|
(funcall length-function value)))
|
||||||
|
((symbol-function 'seq-subseq)
|
||||||
|
(lambda (value start &optional end)
|
||||||
|
(when (eq value lines) (cl-incf list-slices))
|
||||||
|
(funcall subseq-function value start end))))
|
||||||
|
(let ((visible (ebox--scroll-state-rendered-visible-window state)))
|
||||||
|
(should (= (car visible) 9000))
|
||||||
|
(should (cl-every #'eq (cadr visible) expected))))
|
||||||
|
(should (= full-lengths 0))
|
||||||
|
(should (= list-slices 0))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-index-observes-current-cons-cars ()
|
||||||
|
"Same-spine row rewrites retain exact strings, opaque callbacks and hover groups."
|
||||||
|
(let* ((first (lambda () 'first))
|
||||||
|
(second (lambda () 'second))
|
||||||
|
(hover (list :background "blue"))
|
||||||
|
(line (propertize "before" 'help-echo first 'mouse-face hover))
|
||||||
|
(replacement (propertize "after" 'help-echo second 'mouse-face hover))
|
||||||
|
(lines (list line (propertize "tail" 'mouse-face (copy-sequence hover))))
|
||||||
|
(state (ebox-scroll-slice-test--state lines)))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(let ((index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(ebox--line-list-rewrite! lines 0 (list replacement))
|
||||||
|
(let ((visible (cadr (ebox--scroll-state-rendered-visible-window state))))
|
||||||
|
(should (eq index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(should (eq replacement (car visible)))
|
||||||
|
(should (eq second (get-text-property 0 'help-echo (car visible))))
|
||||||
|
(should (eq hover (get-text-property 0 'mouse-face (car visible))))
|
||||||
|
(should-not (eq hover (get-text-property 0 'mouse-face (cadr visible))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-extension-and-replacement-refresh-index ()
|
||||||
|
"Tail extension and replacement heads cannot reuse stale positional indexes."
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines 4))
|
||||||
|
(state (ebox-scroll-slice-test--state lines)))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(let ((first-index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(ebox--line-list-append! lines (last lines) (list "new fourth" "new fifth"))
|
||||||
|
(plist-put state :scroll-offset 3)
|
||||||
|
(should (equal (cadr (ebox--scroll-state-rendered-visible-window state))
|
||||||
|
'("Row 00003" "new fourth" "new fifth")))
|
||||||
|
(should-not (eq first-index (plist-get state :rendered-content-line-cells))))
|
||||||
|
(let* ((old-index (plist-get state :rendered-content-line-cells))
|
||||||
|
(replacement (copy-sequence lines))
|
||||||
|
(ebox--defer-scroll-content-index t))
|
||||||
|
(setcar (nthcdr 3 replacement) "replacement")
|
||||||
|
(setq state (ebox--scroll-state-set-lines state replacement replacement))
|
||||||
|
(should-not (eq old-index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(should (equal (car (cadr (ebox--scroll-state-rendered-visible-window state)))
|
||||||
|
"replacement"))
|
||||||
|
(let ((index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(setq state (ebox--scroll-state-set-lines state replacement replacement))
|
||||||
|
(should (eq index (plist-get state :rendered-content-line-cells)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-lazy-prefix-remains-unindexed ()
|
||||||
|
"Incrementally extended prefixes avoid rebuilding a complete vector each slice."
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines 4))
|
||||||
|
(state (ebox-scroll-slice-test--state lines t))
|
||||||
|
(ebox--defer-scroll-content-index t))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(should-not (plist-get state :rendered-content-line-cells))
|
||||||
|
(ebox--line-list-rewrite! lines 3 '("rewritten tail"))
|
||||||
|
(ebox--line-list-append! lines (last lines) '("appended"))
|
||||||
|
(setq state (ebox--scroll-state-set-lines state lines lines))
|
||||||
|
(plist-put state :scroll-offset 2)
|
||||||
|
(should (equal (cadr (ebox--scroll-state-rendered-visible-window state))
|
||||||
|
'("Row 00002" "rewritten tail" "appended")))
|
||||||
|
(should-not (plist-get state :rendered-content-line-cells))
|
||||||
|
(plist-put state :content-lines-complete-p t)
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(should (vectorp (plist-get state :rendered-content-line-cells)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-native-empty-and-short-windows ()
|
||||||
|
"Native windows take precedence; empty, short and clamped windows stay exact."
|
||||||
|
(dolist (count '(0 1 2 3 4))
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines count))
|
||||||
|
(state (ebox-scroll-slice-test--state lines)))
|
||||||
|
(dolist (offset '(-4 0 1 20))
|
||||||
|
(plist-put state :scroll-offset offset)
|
||||||
|
(if lines
|
||||||
|
(let* ((start (max 0 (min (max 0 (- count 3)) offset)))
|
||||||
|
(expected (list start (seq-subseq lines start (min count (+ start 3))))))
|
||||||
|
(should (equal (ebox--scroll-state-rendered-visible-window state)
|
||||||
|
expected)))
|
||||||
|
(should-not (ebox--scroll-state-rendered-visible-window state))))))
|
||||||
|
(let* ((state (ebox-scroll-slice-test--state '("ordinary")))
|
||||||
|
(native (list "native one" "native two" "native three")))
|
||||||
|
(plist-put state :scroll-offset 100)
|
||||||
|
(plist-put state :native-reflow-visible-offset 100)
|
||||||
|
(plist-put state :native-reflow-visible-lines native)
|
||||||
|
(let ((visible (ebox--scroll-state-rendered-visible-window state)))
|
||||||
|
(should (= (car visible) 100))
|
||||||
|
(should (eq (cadr visible) native))
|
||||||
|
(should-not (plist-get state :rendered-content-line-cells)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-template-discards-derived-index ()
|
||||||
|
"Render-cache templates retain source lines without retaining the derived vector."
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines 5))
|
||||||
|
(state (ebox-scroll-slice-test--state lines)))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(let ((template (ebox--scroll-cache-state-template state)))
|
||||||
|
(should (eq lines (plist-get template :rendered-content-lines)))
|
||||||
|
(should-not (plist-member template :rendered-content-line-cells)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-candidate-replacement-keeps-prior-index ()
|
||||||
|
"A copied candidate state can replace its lines without mutating the old index."
|
||||||
|
(let* ((lines (ebox-scroll-slice-test--lines 8))
|
||||||
|
(state (ebox-scroll-slice-test--state lines))
|
||||||
|
(ebox--defer-scroll-content-index t))
|
||||||
|
(ebox--scroll-state-rendered-visible-window state)
|
||||||
|
(let* ((old-index (plist-get state :rendered-content-line-cells))
|
||||||
|
(candidate (copy-sequence state))
|
||||||
|
(candidate-lines (copy-sequence lines)))
|
||||||
|
(setcar candidate-lines "candidate only")
|
||||||
|
(setq candidate (ebox--scroll-state-set-lines candidate candidate-lines candidate-lines))
|
||||||
|
(should (equal (car (cadr (ebox--scroll-state-rendered-visible-window candidate)))
|
||||||
|
"candidate only"))
|
||||||
|
(should (eq old-index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(should (eq lines (aref old-index 0)))
|
||||||
|
(should (eq (car lines) (car (cadr (ebox--scroll-state-rendered-visible-window state))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-scroll-slice-publication-rollback-retains-index-generation ()
|
||||||
|
"Failed publication leaves old row handles usable and the next scroll succeeds."
|
||||||
|
(let ((ebox-viewport-width 104) (ebox-viewport-height 5)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p) (lambda () nil)))
|
||||||
|
(with-temp-buffer
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build `(box :id "scroll" :width (vw 100) :height (vh 100)
|
||||||
|
:overflow scroll
|
||||||
|
,(string-join (ebox-scroll-slice-test--lines 100) "\n"))))
|
||||||
|
(let* ((buffer (current-buffer))
|
||||||
|
(region (cdr (ebox-selector--region-target
|
||||||
|
(ebox-region-resolve buffer "scroll")))))
|
||||||
|
(ebox--scroll-state-materialize-lines region (ebox-scroll-state region))
|
||||||
|
(ebox--surface-scroll-to-offset buffer region 1)
|
||||||
|
(ebox--surface-scroll-to-offset buffer region 0)
|
||||||
|
(let* ((state (ebox-scroll-state region))
|
||||||
|
(index (plist-get state :rendered-content-line-cells))
|
||||||
|
(before (buffer-string))
|
||||||
|
(revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(should (vectorp index))
|
||||||
|
(cl-letf (((symbol-function 'accept-change-group)
|
||||||
|
(lambda (_) (error "Reject indexed scroll"))))
|
||||||
|
(should-error (ebox--surface-scroll-to-offset buffer region 1)))
|
||||||
|
(should (eq state (ebox-scroll-state region)))
|
||||||
|
(should (eq index (plist-get state :rendered-content-line-cells)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision buffer)))
|
||||||
|
(ebox--surface-scroll-to-offset buffer region 1)
|
||||||
|
(should (eq index (plist-get (ebox-scroll-state region)
|
||||||
|
:rendered-content-line-cells)))
|
||||||
|
(should (= 1 (plist-get (ebox-scroll-state region) :scroll-offset)))
|
||||||
|
(should (string-prefix-p "Row 00001" (buffer-string))))))
|
||||||
|
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||||
|
(ebox-unmount-buffer (current-buffer))))))))
|
||||||
|
|
||||||
|
(provide 'ebox-scroll-slice-tests)
|
||||||
|
|
||||||
|
;;; ebox-scroll-slice-tests.el ends here
|
||||||
Loading…
Reference in New Issue
Block a user