perf: add native retained frame commits
This commit is contained in:
parent
dd731cfbbf
commit
c591a9fcf4
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
__pycache__
|
||||
scripts/ebox-scroll-profile.el
|
||||
native/target/
|
||||
native/core/target/
|
||||
native/core/Cargo.lock
|
||||
|
||||
# Local oh-my-codex runtime state/logs.
|
||||
.omx/
|
||||
|
||||
@ -10,6 +10,7 @@ All notable changes to Ebox are documented here.
|
||||
- ECSS-backed property schemas, selectors, stylesheets, inheritance, cascade layers, custom properties, computed values, and provenance for Box, Flex, and Grid nodes.
|
||||
- Surface-scoped region handles through `ebox-region-resolve`, retained TP publication through `ebox-render-to-buffer` and `ebox-display-buffer`, and atomic handle/selector/viewport/scroll updates.
|
||||
- Typography longhands and inherited foreground projection that keep text readable when child boxes override background colors.
|
||||
- A public, nestable framework render-burst boundary that covers pre-commit allocation, shares interactive deferred GC ownership with Ebox commits, and restores batch GC settings without collecting at the boundary.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ Ebox 的重要变化记录在这里。
|
||||
- Box、Flex 与 Grid 节点统一使用 ECSS property schema、selector、stylesheet、inheritance、cascade layer、custom property、computed value 与 provenance。
|
||||
- 通过 `ebox-region-resolve` 提供 surface-scoped region handle;`ebox-render-to-buffer` 与 `ebox-display-buffer` 统一使用 retained TP publication;handle、selector、viewport 与 scroll 更新加入同一原子事务。
|
||||
- 增加 typography longhand 与继承 foreground 的投影;子盒子覆盖背景色时仍能保持文字可读。
|
||||
- 增加公开、可嵌套的 framework render-burst 边界,覆盖 commit 前分配,与 Ebox commit 共用交互 deferred GC ownership,并在 batch 边界不主动 GC 的情况下恢复原设置。
|
||||
|
||||
### 变更
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@ -22,7 +22,7 @@ load:
|
||||
|
||||
compile:
|
||||
rm -f *.elc tests/*.elc scripts/*.elc
|
||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t)' -l ebox.el --eval '(ebox-byte-compile)'
|
||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)'
|
||||
|
||||
test: core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
|
||||
|
||||
|
||||
@ -24,6 +24,8 @@ ECSS and TP are independent packages and may be installed in either order. Insta
|
||||
|
||||
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` materializes propertized text without publishing buffer state (using isolated pure materialization when no cascade/inheritance dependency requires a TP object tree); `ebox-render-to-buffer` mounts a retained TP surface; `ebox-display-buffer` displays that same retained-surface path; and `ebox-commit` atomically updates the mount from a fresh root tree. Ebox copies declarative input before assigning runtime identity, so one source tree may be mounted in multiple buffers without transferring ownership. It exposes no separate public macro that erases a live buffer and evaluates arbitrary body forms.
|
||||
|
||||
Framework integrations may wrap an entire input callback with `ebox-call-with-render-burst`, or pair `ebox-render-burst-begin` and `ebox-render-burst-end` under `unwind-protect`, so pre-commit view allocation and Ebox publication share one nested GC boundary.
|
||||
|
||||
Material child lists may contain `ebox-child-range` descriptors, and logical candidates update their base-bound addresses with `ebox-candidate-replace-range-ref`. Candidates may also replace the private mounted root with `ebox-candidate-replace-root`; the last root replacement absorbs descendant operations and never shares an address with `:host-ref`. Reports expose `:range-metrics`, framework participant diagnostics, and scroll finalization diagnostics as read-only snapshots.
|
||||
|
||||
CSS-like selector strings are compiled to ECSS's structured selector AST. Ebox supplies logical node relations and indexed candidates; ECSS is the only selector matcher and cascade engine for tree queries, rendered-buffer queries, and style rules.
|
||||
|
||||
@ -24,6 +24,8 @@ ECSS 与 TP 是互相独立的包,安装顺序任意。两者都安装后再
|
||||
|
||||
公共模型是一棵 Ebox 节点树。`ebox-create` 构造叶子或包装节点;`ebox-column`、`ebox-row`、`ebox-flex` 和 `ebox-grid` 组合节点;`ebox-render` 生成带属性文本但不发布 buffer state(没有 cascade/inheritance 依赖时使用隔离的 pure materialization);`ebox-render-to-buffer` 挂载 retained TP surface;`ebox-display-buffer` 展示同一条 retained-surface 路径;`ebox-commit` 使用新的根树原子更新该 mount。Ebox 会在分配 runtime identity 前复制声明式输入,因此同一 source tree 可以挂载到多个 buffer,而不转移所有权。公共 API 不再提供另一套擦除 live buffer 后执行任意 BODY 的宏。
|
||||
|
||||
框架集成可以用 `ebox-call-with-render-burst` 包住整个输入 callback,或在 `unwind-protect` 中配对 `ebox-render-burst-begin` 与 `ebox-render-burst-end`,让 commit 前的 view 分配和 Ebox 发布共享同一个可嵌套 GC 边界。
|
||||
|
||||
Material child list 可包含 `ebox-child-range` descriptor,logical candidate 用 `ebox-candidate-replace-range-ref` 更新其 base-bound 地址。Candidate 也可用 `ebox-candidate-replace-root` 替换私有 mounted root;最后一次 root replacement 会吸收 descendant operation,并且不会与 `:host-ref` 共用地址。报告通过 `:range-metrics`、framework participant diagnostics 和 scroll finalization diagnostics 暴露只读快照。
|
||||
|
||||
CSS-like selector 字符串会编译为 ECSS 的 structured selector AST。Ebox 提供逻辑节点关系与索引候选;对于 tree query、rendered-buffer query 和样式规则,ECSS 是唯一 selector matcher 与 cascade engine。
|
||||
|
||||
@ -344,6 +344,12 @@ instead of a runtime node id. Both replacement functions accept optional
|
||||
reuse. A candidate is sealed by commit and cannot be reused; it also becomes
|
||||
stale if the captured buffer runtime or buffer tick changes.
|
||||
|
||||
`ebox-candidate-patch-host-paint` compares previous and next declarative Host
|
||||
outputs and records a node-local patch only when every changed field is
|
||||
paint-only. It retains child/runtime identity, updates inherited foreground
|
||||
state in one affected-subtree pass, and returns nil when ordinary subtree
|
||||
replacement is required.
|
||||
|
||||
`ebox-candidate-replace-root` targets a private candidate-bound root address.
|
||||
It accepts exactly one declarative node, clears caller runtime identities, and
|
||||
is last-wins. The final root replacement absorbs descendant node and host-ref
|
||||
@ -370,6 +376,27 @@ selector ids.
|
||||
|
||||
### Reports and viewport reflow
|
||||
|
||||
Frameworks that allocate a new view tree before calling Ebox can include the
|
||||
whole input callback in Ebox's render GC budget:
|
||||
|
||||
```elisp
|
||||
(ebox-call-with-render-burst
|
||||
(lambda ()
|
||||
(let ((next-root (build-framework-view)))
|
||||
(ebox-commit buffer next-root))))
|
||||
```
|
||||
|
||||
`ebox-call-with-render-burst` is the preferred exception-safe boundary. When
|
||||
the operation cannot be expressed as one function call,
|
||||
`ebox-render-burst-begin` returns an opaque token for
|
||||
`ebox-render-burst-end`; callers must use `unwind-protect` and end nested
|
||||
tokens in last-in, first-out order. Interactive bursts share Ebox commit's
|
||||
deferred state and restore the caller's GC settings 0.2 seconds after the
|
||||
outermost end. A new burst cancels and supersedes the pending restore. Batch
|
||||
bursts restore the exact threshold and percentage immediately and do not run
|
||||
GC at the boundary. These functions only define allocation/GC ownership;
|
||||
they do not publish, commit, or expose Ebox runtime state.
|
||||
|
||||
`ebox-buffer-update-report` returns a defensive copy of the last successful
|
||||
report for a mounted buffer; it signals for a missing/non-Ebox buffer. A fresh
|
||||
mount has no update report until its first update. Reports include the Ebox
|
||||
@ -552,7 +579,7 @@ style-rule functions immediately below are public module-level style APIs.
|
||||
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
|
||||
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context` | Atomic root commit, report lookup, and viewport-context rerender. |
|
||||
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |
|
||||
| `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref` | Persistent material child segments, backend-anchor lookup, and one-shot logical candidate transactions. |
|
||||
| `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint` | Persistent material child segments, backend-anchor lookup, subtree replacement, and retained Host paint transactions. |
|
||||
| `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. |
|
||||
| `ebox-selector-parse`, `ebox-selector-match-node-p`, `ebox-selector-query-all`, `ebox-selector-query-buffer`, `ebox-selector-update-buffer` | ECSS selector compilation, matching, querying, and batch update. |
|
||||
| `ebox-select-all`, `ebox-update-selector` | Compatibility aliases for the two buffer selector functions. |
|
||||
|
||||
@ -313,6 +313,10 @@ node id。两个 replacement 函数都接受可选的 `old-semantic-key` 与
|
||||
`new-semantic-key`,用于有限的 detached identity 复用。Candidate 在 commit
|
||||
时 sealed,不能复用;若捕获的 runtime 或 buffer tick 发生变化,它也会 stale。
|
||||
|
||||
`ebox-candidate-patch-host-paint` 比较前后 declarative Host 输出;只有全部变化
|
||||
都属于 node-local paint 时才记录属性 patch。它保留子树与 runtime identity,
|
||||
并在一次受影响子树遍历中更新继承前景;需要普通子树替换时返回 nil。
|
||||
|
||||
`ebox-candidate-replace-root` 使用私有且绑定 candidate 的 root 地址。它只接受
|
||||
一个声明式节点、清除调用方 runtime identity,并采用 last-wins 语义。最终 root
|
||||
replacement 会吸收其前后记录的 descendant node/host-ref operation,不添加
|
||||
@ -337,6 +341,25 @@ Host-reference table 与 logical selector id 是两套不同的机制。
|
||||
|
||||
### Report 与 viewport reflow
|
||||
|
||||
如果上层框架在调用 Ebox 前需要先分配新的 view tree,可以把整个输入 callback
|
||||
纳入 Ebox 的 render GC budget:
|
||||
|
||||
```elisp
|
||||
(ebox-call-with-render-burst
|
||||
(lambda ()
|
||||
(let ((next-root (build-framework-view)))
|
||||
(ebox-commit buffer next-root))))
|
||||
```
|
||||
|
||||
优先使用能保证异常清理的 `ebox-call-with-render-burst`。如果操作无法表达为
|
||||
一次函数调用,`ebox-render-burst-begin` 会返回交给
|
||||
`ebox-render-burst-end` 的不透明 token;调用方必须使用 `unwind-protect`,并按
|
||||
后进先出顺序结束嵌套 token。交互 burst 与 Ebox commit 共用 deferred state,
|
||||
在最外层 end 后 0.2 秒恢复调用方 GC 设置;新 burst 会取消并取代待执行的
|
||||
restore。Batch burst 立即精确恢复 threshold 与 percentage,且不会在边界主动
|
||||
执行 GC。这些函数只定义 allocation/GC ownership,不会 publish、commit,也
|
||||
不会暴露 Ebox runtime state。
|
||||
|
||||
`ebox-buffer-update-report` 返回 mounted buffer 上一次成功报告的 defensive
|
||||
copy;目标缺失或不是 Ebox runtime 时会报错。首次 mount 在第一次 update
|
||||
前没有 update report。报告包含 Ebox strategy、publication scope、TP surface
|
||||
@ -504,7 +527,7 @@ style API。
|
||||
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
|
||||
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context` | 原子根提交、报告查询和 viewport-context rerender。 |
|
||||
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |
|
||||
| `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref` | 持久 material child segment、backend anchor 查询与一次性 logical candidate transaction。 |
|
||||
| `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint` | 持久 material child segment、backend anchor 查询、子树替换与 retained Host paint transaction。 |
|
||||
| `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 |
|
||||
| `ebox-selector-parse`、`ebox-selector-match-node-p`、`ebox-selector-query-all`、`ebox-selector-query-buffer`、`ebox-selector-update-buffer` | ECSS selector 编译、匹配、查询与批量更新。 |
|
||||
| `ebox-select-all`、`ebox-update-selector` | 两个 buffer selector 函数的兼容别名。 |
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
(require 'ebox-measure)
|
||||
(require 'tp-style)
|
||||
|
||||
(defvar ebox--propertize-private-content-line-p nil
|
||||
"Non-nil allows ownership to be added to fresh content lines in place.")
|
||||
@ -116,9 +118,38 @@ Set this to nil to leave `gc-cons-percentage' unchanged.")
|
||||
(defvar ebox--deferred-render-gc-depth 0
|
||||
"Number of overlapping interactive render bursts that still own GC defer.")
|
||||
|
||||
(defvar ebox--deferred-render-gc-generation 0
|
||||
"Generation used to reject callbacks from cancelled GC restore timers.")
|
||||
|
||||
(cl-defstruct (ebox--render-burst-record
|
||||
(:constructor ebox--make-render-burst-record))
|
||||
"Private saved state for one public framework render burst."
|
||||
mode
|
||||
saved-threshold
|
||||
saved-percentage)
|
||||
|
||||
(defvar ebox--render-burst-records (make-hash-table :test #'eq)
|
||||
"Private render-burst records keyed by opaque public tokens.")
|
||||
|
||||
(defvar ebox--render-burst-stack nil
|
||||
"Active public render-burst tokens, innermost first.")
|
||||
|
||||
(defun ebox--render-burst-create-record (mode threshold percentage)
|
||||
"Return private burst state for MODE, THRESHOLD, and PERCENTAGE."
|
||||
(ebox--make-render-burst-record
|
||||
:mode mode
|
||||
:saved-threshold threshold
|
||||
:saved-percentage percentage))
|
||||
|
||||
(defun ebox--make-deferred-render-gc-state (threshold percentage)
|
||||
"Return private deferred GC state for THRESHOLD and PERCENTAGE."
|
||||
(list :gc-cons-threshold threshold
|
||||
:gc-cons-percentage percentage))
|
||||
|
||||
(defun ebox--deferred-render-gc-restore ()
|
||||
"Restore GC settings saved by `ebox--with-deferred-render-gc'."
|
||||
(let ((state ebox--deferred-render-gc-state))
|
||||
(cl-incf ebox--deferred-render-gc-generation)
|
||||
(setq ebox--deferred-render-gc-state nil)
|
||||
(setq ebox--deferred-render-gc-timer nil)
|
||||
(setq ebox--deferred-render-gc-depth 0)
|
||||
@ -131,20 +162,29 @@ Set this to nil to leave `gc-cons-percentage' unchanged.")
|
||||
|
||||
(defun ebox--deferred-render-gc-enter ()
|
||||
"Raise GC settings for an interactive render transaction."
|
||||
(cl-incf ebox--deferred-render-gc-depth)
|
||||
(unless ebox--deferred-render-gc-state
|
||||
(setq ebox--deferred-render-gc-state
|
||||
(list :gc-cons-threshold gc-cons-threshold
|
||||
:gc-cons-percentage gc-cons-percentage)))
|
||||
(when-let ((threshold
|
||||
(ebox--effective-deferred-render-gc-cons-threshold)))
|
||||
(setq gc-cons-threshold threshold))
|
||||
(when (numberp ebox-render-gc-cons-percentage)
|
||||
(setq gc-cons-percentage
|
||||
(max gc-cons-percentage ebox-render-gc-cons-percentage)))
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer)
|
||||
(setq ebox--deferred-render-gc-timer nil)))
|
||||
(let ((saved-threshold gc-cons-threshold)
|
||||
(saved-percentage gc-cons-percentage))
|
||||
;; Cross a safe allocation boundary before automatic tuning or saved-state
|
||||
;; construction can allocate. The original scalar values above require no
|
||||
;; heap record and remain authoritative for the eventual restore.
|
||||
(when (numberp ebox-render-gc-cons-threshold)
|
||||
(setq gc-cons-threshold
|
||||
(max gc-cons-threshold ebox-render-gc-cons-threshold)))
|
||||
(when (numberp ebox-render-gc-cons-percentage)
|
||||
(setq gc-cons-percentage
|
||||
(max gc-cons-percentage ebox-render-gc-cons-percentage)))
|
||||
(cl-incf ebox--deferred-render-gc-generation)
|
||||
(cl-incf ebox--deferred-render-gc-depth)
|
||||
(when-let ((threshold
|
||||
(ebox--effective-deferred-render-gc-cons-threshold)))
|
||||
(setq gc-cons-threshold threshold))
|
||||
(unless ebox--deferred-render-gc-state
|
||||
(setq ebox--deferred-render-gc-state
|
||||
(ebox--make-deferred-render-gc-state
|
||||
saved-threshold saved-percentage)))
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer)
|
||||
(setq ebox--deferred-render-gc-timer nil))))
|
||||
|
||||
(defun ebox--deferred-render-gc-raise-threshold (threshold)
|
||||
"Raise an active interactive burst allocation budget to THRESHOLD."
|
||||
@ -168,8 +208,18 @@ Set this to nil to leave `gc-cons-percentage' unchanged.")
|
||||
ebox--deferred-render-gc-state)
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer))
|
||||
(setq ebox--deferred-render-gc-timer
|
||||
(run-at-time 0.2 nil #'ebox--deferred-render-gc-restore))))
|
||||
(let ((generation (cl-incf ebox--deferred-render-gc-generation)))
|
||||
(setq ebox--deferred-render-gc-timer
|
||||
(run-at-time
|
||||
0.2 nil #'ebox--deferred-render-gc-restore-if-current
|
||||
generation)))))
|
||||
|
||||
(defun ebox--deferred-render-gc-restore-if-current (generation)
|
||||
"Restore deferred GC state when GENERATION still owns the timer boundary."
|
||||
(when (and (= generation ebox--deferred-render-gc-generation)
|
||||
(zerop ebox--deferred-render-gc-depth)
|
||||
ebox--deferred-render-gc-state)
|
||||
(ebox--deferred-render-gc-restore)))
|
||||
|
||||
(defmacro ebox--with-deferred-render-gc (&rest body)
|
||||
"Evaluate BODY with render GC settings restored after idle redisplay.
|
||||
@ -182,6 +232,92 @@ immediately after BODY can trigger GC before the updated buffer is visible."
|
||||
(progn ,@body)
|
||||
(ebox--deferred-render-gc-schedule-restore))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-render-burst-begin ()
|
||||
"Begin a framework render burst and return an opaque ownership token.
|
||||
|
||||
Frameworks may enter this boundary at the start of an input callback so
|
||||
allocation performed before `ebox-commit' shares the same GC budget as Ebox
|
||||
rendering and publication. Interactive callers reuse Ebox's settling burst:
|
||||
the outermost matching end restores settings after 0.2 seconds. Batch callers
|
||||
restore the exact settings captured by this call immediately, without running
|
||||
GC at the boundary.
|
||||
|
||||
Pass the returned token to `ebox-render-burst-end'. Bursts may be nested, but
|
||||
must be ended in last-in, first-out order. Prefer
|
||||
`ebox-call-with-render-burst' when the complete operation is available as a
|
||||
function, because it guarantees cleanup across errors and quits."
|
||||
(let ((saved-threshold gc-cons-threshold)
|
||||
(saved-percentage gc-cons-percentage)
|
||||
token
|
||||
completed)
|
||||
(if noninteractive
|
||||
;; Do not allocate the opaque token, its private record, or a stack
|
||||
;; cons until the old threshold can no longer collect at begin itself.
|
||||
(progn
|
||||
(when (numberp ebox-render-gc-cons-threshold)
|
||||
(setq gc-cons-threshold
|
||||
(max gc-cons-threshold ebox-render-gc-cons-threshold)))
|
||||
(when (numberp ebox-render-gc-cons-percentage)
|
||||
(setq gc-cons-percentage
|
||||
(max gc-cons-percentage ebox-render-gc-cons-percentage))))
|
||||
(ebox--deferred-render-gc-enter))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq token (make-symbol "ebox-render-burst-token"))
|
||||
(puthash
|
||||
token
|
||||
(ebox--render-burst-create-record
|
||||
(if noninteractive 'batch 'interactive)
|
||||
(and noninteractive saved-threshold)
|
||||
(and noninteractive saved-percentage))
|
||||
ebox--render-burst-records)
|
||||
(push token ebox--render-burst-stack)
|
||||
(setq completed t)
|
||||
token)
|
||||
(unless completed
|
||||
(when token
|
||||
(remhash token ebox--render-burst-records))
|
||||
(if noninteractive
|
||||
(setq gc-cons-threshold saved-threshold
|
||||
gc-cons-percentage saved-percentage)
|
||||
(ebox--deferred-render-gc-schedule-restore))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-render-burst-end (token)
|
||||
"End the render burst owned by opaque TOKEN.
|
||||
|
||||
TOKEN must be the most recently returned active token from
|
||||
`ebox-render-burst-begin'. Invalid, repeated, or out-of-order ends signal an
|
||||
error without changing GC ownership. Return nil after a successful end."
|
||||
(let ((record (gethash token ebox--render-burst-records)))
|
||||
(unless record
|
||||
(error "Invalid or inactive Ebox render-burst token"))
|
||||
(unless (eq token (car ebox--render-burst-stack))
|
||||
(error "Ebox render bursts must end in last-in, first-out order"))
|
||||
(pop ebox--render-burst-stack)
|
||||
(remhash token ebox--render-burst-records)
|
||||
(if (eq (ebox--render-burst-record-mode record) 'interactive)
|
||||
(ebox--deferred-render-gc-schedule-restore)
|
||||
(setq gc-cons-threshold
|
||||
(ebox--render-burst-record-saved-threshold record)
|
||||
gc-cons-percentage
|
||||
(ebox--render-burst-record-saved-percentage record))))
|
||||
nil)
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-call-with-render-burst (function &rest arguments)
|
||||
"Call FUNCTION with ARGUMENTS inside one framework render burst.
|
||||
|
||||
The burst is always ended when FUNCTION returns, signals an error, or quits.
|
||||
Return FUNCTION's value."
|
||||
(unless (functionp function)
|
||||
(signal 'wrong-type-argument (list 'functionp function)))
|
||||
(let ((token (ebox-render-burst-begin)))
|
||||
(unwind-protect
|
||||
(apply function arguments)
|
||||
(ebox-render-burst-end token))))
|
||||
|
||||
;;; Paint Mapping
|
||||
|
||||
(defun ebox-buffer--font-attributes (style)
|
||||
@ -243,11 +379,19 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(let ((face (ebox-buffer--font-face style)))
|
||||
(when-let ((foreground (ebox-buffer--paint-color style role)))
|
||||
(setq face
|
||||
(if (eq foreground 'ebox/default-foreground)
|
||||
(append (ebox-buffer--default-foreground-face) face)
|
||||
(plist-put face :foreground foreground))))
|
||||
(cond
|
||||
((tp-paint-slot-p foreground)
|
||||
(let ((slot-face (tp-paint-slot-face foreground)))
|
||||
(if face (list slot-face face) slot-face)))
|
||||
((eq foreground 'ebox/default-foreground)
|
||||
(append (ebox-buffer--default-foreground-face) face))
|
||||
(t (plist-put face :foreground foreground)))))
|
||||
(when-let ((background (ebox-buffer--paint-background-color style role)))
|
||||
(setq face (plist-put face :background background)))
|
||||
(setq face
|
||||
(if (tp-paint-slot-p background)
|
||||
(let ((slot-face (tp-paint-slot-face background)))
|
||||
(if face (list slot-face face) slot-face))
|
||||
(plist-put face :background background))))
|
||||
(when face
|
||||
(list 'face face))))
|
||||
|
||||
@ -263,9 +407,10 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
|
||||
(defun ebox-buffer-side-border-face (color)
|
||||
"Return the canonical Emacs face for a side border with COLOR."
|
||||
(if color
|
||||
`(:background ,color)
|
||||
'(:inverse-video t)))
|
||||
(cond
|
||||
((tp-paint-slot-p color) (tp-paint-slot-face color))
|
||||
(color `(:background ,color))
|
||||
(t '(:inverse-video t))))
|
||||
|
||||
(defun ebox--pixel-border (pixel-width height &optional color)
|
||||
"Generate a border line of PIXEL-WIDTH and HEIGHT using optional COLOR."
|
||||
@ -284,7 +429,12 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(let* ((source string)
|
||||
(string (copy-sequence source))
|
||||
(length (length string)))
|
||||
(ebox--add-render-face! string 0 length `(:background ,bgcolor) t)
|
||||
(ebox--add-render-face!
|
||||
string 0 length
|
||||
(if (tp-paint-slot-p bgcolor)
|
||||
(tp-paint-slot-face bgcolor)
|
||||
`(:background ,bgcolor))
|
||||
t)
|
||||
(ebox--register-render-owned-face-values source string)))
|
||||
|
||||
(defun ebox--propertize-color (string color)
|
||||
@ -294,9 +444,11 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(length (length string)))
|
||||
(ebox--add-render-face!
|
||||
string 0 length
|
||||
(if (eq color 'ebox/default-foreground)
|
||||
(ebox-buffer--default-foreground-face)
|
||||
`(:foreground ,color))
|
||||
(cond
|
||||
((tp-paint-slot-p color) (tp-paint-slot-face color))
|
||||
((eq color 'ebox/default-foreground)
|
||||
(ebox-buffer--default-foreground-face))
|
||||
(t `(:foreground ,color)))
|
||||
t)
|
||||
(ebox--register-render-owned-face-values source string)))
|
||||
|
||||
@ -308,13 +460,19 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(when color
|
||||
(ebox--add-render-face!
|
||||
string 0 length
|
||||
(if (eq color 'ebox/default-foreground)
|
||||
(ebox-buffer--default-foreground-face)
|
||||
`(:foreground ,color))
|
||||
(cond
|
||||
((tp-paint-slot-p color) (tp-paint-slot-face color))
|
||||
((eq color 'ebox/default-foreground)
|
||||
(ebox-buffer--default-foreground-face))
|
||||
(t `(:foreground ,color)))
|
||||
t))
|
||||
(when bgcolor
|
||||
(ebox--add-render-face!
|
||||
string 0 length `(:background ,bgcolor) t))
|
||||
string 0 length
|
||||
(if (tp-paint-slot-p bgcolor)
|
||||
(tp-paint-slot-face bgcolor)
|
||||
`(:background ,bgcolor))
|
||||
t))
|
||||
(ebox--register-render-owned-face-values source string)))
|
||||
|
||||
(defun ebox--propertize-overline (string &optional color)
|
||||
@ -322,7 +480,11 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(let* ((source string)
|
||||
(string (copy-sequence source)))
|
||||
(ebox--add-render-face!
|
||||
string 0 (length string) `(:overline ,(or color t)) t)
|
||||
string 0 (length string)
|
||||
(if (tp-paint-slot-p color)
|
||||
(tp-paint-slot-face color)
|
||||
`(:overline ,(or color t)))
|
||||
t)
|
||||
(ebox--register-render-owned-face-values source string)))
|
||||
|
||||
(defun ebox--propertize-underline (string &optional color)
|
||||
@ -331,7 +493,9 @@ buffer-facing paint properties. Layout-only properties never pass through."
|
||||
(string (copy-sequence source)))
|
||||
(ebox--add-render-face!
|
||||
string 0 (length string)
|
||||
`(:underline (:position t ,@(when color `(:color ,color))))
|
||||
(if (tp-paint-slot-p color)
|
||||
(tp-paint-slot-face color)
|
||||
`(:underline (:position t ,@(when color `(:color ,color)))))
|
||||
t)
|
||||
(ebox--register-render-owned-face-values source string)))
|
||||
|
||||
|
||||
@ -248,8 +248,10 @@ HASH-FUNCTION optionally supplies the stable key hash."
|
||||
"Return `(NEW-SEQUENCE . METRICS)' for SEQUENCE after replacing REF ITEMS."
|
||||
(ebox-child-range--replace-many sequence (list (cons ref items))))
|
||||
|
||||
(defun ebox-child-range--replace-many (sequence replacements)
|
||||
"Replace RANGE REPLACEMENTS in SEQUENCE with one two-phase transaction."
|
||||
(defun ebox-child-range--replace-many (sequence replacements &optional reuse-maps)
|
||||
"Replace RANGE REPLACEMENTS in SEQUENCE with one two-phase transaction.
|
||||
REUSE-MAPS is an alist from Range ref to proven `(NEW . OLD)' item indexes;
|
||||
those NEW items already name canonical published objects and are not copied."
|
||||
(let ((metrics (ebox-child-range--make-metrics))
|
||||
(missing (make-symbol "missing-range-ref"))
|
||||
(seen (make-hash-table :test #'equal)) records)
|
||||
@ -267,12 +269,15 @@ HASH-FUNCTION optionally supplies the stable key hash."
|
||||
missing)))
|
||||
(when (eq index missing)
|
||||
(error "Ebox child Range ref does not exist: %S" (car entry)))
|
||||
(push (list (car entry) index
|
||||
(vconcat (mapcar
|
||||
(lambda (item)
|
||||
(ebox-child-range--item-copy item metrics))
|
||||
(cdr entry))))
|
||||
records)))
|
||||
(let ((reuse-map (cdr (assoc (car entry) reuse-maps #'equal))))
|
||||
(push (list (car entry) index
|
||||
(vconcat
|
||||
(cl-loop for item in (cdr entry) for offset from 0
|
||||
collect
|
||||
(if (assq offset reuse-map)
|
||||
item
|
||||
(ebox-child-range--item-copy item metrics)))))
|
||||
records))))
|
||||
(setq records (nreverse records))
|
||||
(let ((key-root (ebox-child-range--sequence-key-root sequence))
|
||||
(hash-function (ebox-child-range--sequence-hash-function sequence))
|
||||
|
||||
36
ebox-dsl.el
36
ebox-dsl.el
@ -12,6 +12,8 @@
|
||||
(require 'ebox-flex)
|
||||
(require 'ebox-grid)
|
||||
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
|
||||
(defun ebox--dsl-unquote-value (value)
|
||||
"Unquote one static DSL property VALUE when it is quoted data."
|
||||
(if (and (consp value)
|
||||
@ -36,40 +38,6 @@ keyword properties are normalized."
|
||||
(push item result))))
|
||||
(nreverse result)))
|
||||
|
||||
(defun ebox--build-split-attrs (items)
|
||||
"Split DSL ITEMS into (PROPS . CHILDREN)."
|
||||
(let (props children)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(if items
|
||||
(let ((value (pop items)))
|
||||
(push item props)
|
||||
(push value props))
|
||||
(error "ebox-build: missing value for %S" item))
|
||||
(push item children))))
|
||||
(cons (nreverse props) (nreverse children))))
|
||||
|
||||
(defun ebox--plist-keep-keys (plist keys)
|
||||
"Return a copy of PLIST containing only entries whose key is in KEYS."
|
||||
(let (result)
|
||||
(while plist
|
||||
(let ((key (pop plist))
|
||||
(value (pop plist)))
|
||||
(when (memq key keys)
|
||||
(setq result (append result (list key value))))))
|
||||
result))
|
||||
|
||||
(defun ebox--plist-remove-keys (plist keys)
|
||||
"Return a copy of PLIST without entries whose key is in KEYS."
|
||||
(let (result)
|
||||
(while plist
|
||||
(let ((key (pop plist))
|
||||
(value (pop plist)))
|
||||
(unless (memq key keys)
|
||||
(setq result (append result (list key value))))))
|
||||
result))
|
||||
|
||||
(defun ebox--build-children-layout (children)
|
||||
"Build a vertical default layout from CHILDREN."
|
||||
(setq children (delq nil children))
|
||||
|
||||
80
ebox-flex.el
80
ebox-flex.el
@ -22,9 +22,55 @@
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--viewport-height-dependent-subtree-p
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--viewport-dependent-subtree-p
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--viewport-dependent-size-value-p
|
||||
"ebox-incremental" (value &optional nil-dependent))
|
||||
(declare-function ebox--render-cacheable-node-p
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--render-cache-node-body-signature
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--render-cache-node-signature
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--render-cache-lookup
|
||||
"ebox-incremental" (cache-key signature))
|
||||
(declare-function ebox--render-cache-store
|
||||
"ebox-incremental"
|
||||
(cache-key signature rendered &optional side-effects))
|
||||
(declare-function ebox--render-with-cache
|
||||
"ebox-incremental" (node &optional force cache-probe))
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
(declare-function ebox--runtime-node-ids "ebox" (node))
|
||||
(declare-function ebox-region-ids "ebox" (node))
|
||||
(declare-function ebox-string-height "ebox" (string))
|
||||
(declare-function ebox-string-lines "ebox" (string))
|
||||
(declare-function ebox-lines-join "ebox" (lines))
|
||||
(declare-function ebox--maplines "ebox" (function string))
|
||||
(declare-function ebox--line-has-non-content-properties-p
|
||||
"ebox" (line))
|
||||
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
||||
(declare-function ebox--lines-stack-vertical "ebox" (&rest strings))
|
||||
(declare-function ebox--pixel-border
|
||||
"ebox-buffer-backend"
|
||||
(pixel-width height &optional color))
|
||||
(declare-function ebox--propertize-colors
|
||||
"ebox-buffer-backend" (string color bgcolor))
|
||||
(declare-function ebox--propertize-overline
|
||||
"ebox-buffer-backend" (string &optional color))
|
||||
(declare-function ebox--propertize-underline
|
||||
"ebox-buffer-backend" (string &optional color))
|
||||
(declare-function ebox--propertize-region
|
||||
"ebox-buffer-backend" (string property region-id))
|
||||
(declare-function ebox--propertize-content-line
|
||||
"ebox-buffer-backend" (line region-id idx fallback))
|
||||
|
||||
(defvar ebox--propertize-private-content-line-p)
|
||||
(defvar ebox--flex-content-min-width-table)
|
||||
(defvar ebox--render-region-id)
|
||||
(defvar ebox--region-box-table)
|
||||
(defvar ebox-fragment-flex-retention-rerender-count)
|
||||
(defvar ebox--flex-native-size-lines-backend :unknown
|
||||
"Cached optional pure geometry backend for batched Flex line sizing.")
|
||||
@ -43,6 +89,40 @@
|
||||
'(:width :height :box-sizing)
|
||||
"Flex box properties that do not create visible wrapping by themselves.")
|
||||
|
||||
(defun ebox--build-split-attrs (items)
|
||||
"Split constructor ITEMS into (PROPS . CHILDREN)."
|
||||
(let (props children)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(if items
|
||||
(let ((value (pop items)))
|
||||
(push item props)
|
||||
(push value props))
|
||||
(error "ebox-build: missing value for %S" item))
|
||||
(push item children))))
|
||||
(cons (nreverse props) (nreverse children))))
|
||||
|
||||
(defun ebox--plist-keep-keys (plist keys)
|
||||
"Return a copy of PLIST containing only entries whose key is in KEYS."
|
||||
(let (result)
|
||||
(while plist
|
||||
(let ((key (pop plist))
|
||||
(value (pop plist)))
|
||||
(when (memq key keys)
|
||||
(setq result (append result (list key value))))))
|
||||
result))
|
||||
|
||||
(defun ebox--plist-remove-keys (plist keys)
|
||||
"Return a copy of PLIST without entries whose key is in KEYS."
|
||||
(let (result)
|
||||
(while plist
|
||||
(let ((key (pop plist))
|
||||
(value (pop plist)))
|
||||
(unless (memq key keys)
|
||||
(setq result (append result (list key value))))))
|
||||
result))
|
||||
|
||||
(defun ebox--flex-visual-box-props-p (props)
|
||||
"Return non-nil when PROPS contains visible box behavior."
|
||||
(cl-loop for (key _value) on props by #'cddr
|
||||
|
||||
@ -12,6 +12,10 @@
|
||||
(require 'ebox-measure)
|
||||
|
||||
(declare-function ebox--current-display-signature "ebox-measure" ())
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--complete-layout-snapshot
|
||||
"ebox-incremental"
|
||||
(buffer node snapshot &optional copy))
|
||||
|
||||
(defvar ebox--layout-fragments-table nil
|
||||
"Dynamic published-generation layout fragment retention table.")
|
||||
|
||||
1079
ebox-incremental.el
1079
ebox-incremental.el
File diff suppressed because it is too large
Load Diff
@ -13,14 +13,56 @@
|
||||
(require 'ebox-measure)
|
||||
(require 'ebox-fragment)
|
||||
(require 'ebox-render-context)
|
||||
(require 'ebox-buffer-backend)
|
||||
|
||||
(declare-function ebox--render-grid "ebox-grid" (node))
|
||||
(declare-function ebox--render-flex "ebox-flex" (node))
|
||||
(declare-function ebox--render-with-cache
|
||||
"ebox-incremental" (node &optional force cache-probe))
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
(declare-function ebox--scroll-clear-state "ebox" (region-id))
|
||||
(declare-function ebox--scroll-set-state "ebox" (region-id state))
|
||||
(declare-function ebox--apply-hidden-visibility "ebox" (string))
|
||||
(declare-function ebox-string-lines "ebox" (string))
|
||||
(declare-function ebox-string-height "ebox" (string))
|
||||
(declare-function ebox-lines-join "ebox" (lines))
|
||||
(declare-function ebox--maplines "ebox" (function string))
|
||||
(declare-function ebox--line-has-non-content-properties-p "ebox" (line))
|
||||
(declare-function ebox--lines-align-vertical
|
||||
"ebox" (string height align))
|
||||
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
||||
(declare-function ebox--lines-stack-vertical "ebox" (&rest strings))
|
||||
(declare-function ebox--add-content-owner
|
||||
"ebox-buffer-backend" (string region-id))
|
||||
(declare-function ebox--pixel-border
|
||||
"ebox-buffer-backend"
|
||||
(pixel-width height &optional color))
|
||||
(declare-function ebox--propertize-color
|
||||
"ebox-buffer-backend" (string color))
|
||||
(declare-function ebox--propertize-colors
|
||||
"ebox-buffer-backend" (string color bgcolor))
|
||||
(declare-function ebox--propertize-content-line
|
||||
"ebox-buffer-backend" (line region-id idx fallback))
|
||||
(declare-function ebox--propertize-overline
|
||||
"ebox-buffer-backend" (string &optional color))
|
||||
(declare-function ebox--propertize-region
|
||||
"ebox-buffer-backend" (string property region-id))
|
||||
(declare-function ebox--propertize-typography
|
||||
"ebox-buffer-backend" (string style))
|
||||
(declare-function ebox--propertize-underline
|
||||
"ebox-buffer-backend" (string &optional color))
|
||||
|
||||
(defvar ebox--render-runtime-revision nil
|
||||
"Current buffer runtime revision for render-local cache validation.")
|
||||
|
||||
(defvar ebox--region-id-counter)
|
||||
(defvar ebox--runtime-node-id-counter)
|
||||
(defvar ebox--region-box-table)
|
||||
(defvar ebox--render-region-id)
|
||||
(defvar ebox--defer-scroll-content-index)
|
||||
|
||||
(defvar ebox--intrinsic-layout-measurement nil
|
||||
"Non-nil while measuring intrinsic layout contributions.
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
|
||||
(declare-function ebox--register-render-owned-text-value
|
||||
"ebox-render-context" (property value))
|
||||
(declare-function ebox-string-lines "ebox" (string))
|
||||
(declare-function ebox--string-repeat-lines "ebox" (string count))
|
||||
|
||||
;; Measurement Cache Model:
|
||||
;; These caches only accelerate display-signature-sensitive pixel measurement.
|
||||
@ -115,14 +117,18 @@ of pixel measurements it triggers do not each rebuild the display signature.")
|
||||
(fillarray ebox--string-pixel-width-cache-ring nil)
|
||||
(setq ebox--string-pixel-width-cache-ring-index 0))
|
||||
|
||||
(defun ebox--string-pixel-width-visual-key (line factor)
|
||||
"Return a width cache key for LINE at FACTOR when visual properties are constant."
|
||||
(let ((end (length line)))
|
||||
(defun ebox--string-pixel-width-visual-key
|
||||
(line factor &optional start end)
|
||||
"Return a width cache key for LINE slice at FACTOR.
|
||||
START and END default to the complete string; visual properties must be
|
||||
constant across the selected slice."
|
||||
(let ((start (or start 0))
|
||||
(end (or end (length line))))
|
||||
(list factor
|
||||
(substring-no-properties line)
|
||||
(substring-no-properties line start end)
|
||||
(cl-loop for property in '(face display)
|
||||
for value = (and (> end 0)
|
||||
(get-text-property 0 property line))
|
||||
for value = (and (> end start)
|
||||
(get-text-property start property line))
|
||||
when value append (list property value)))))
|
||||
|
||||
(defun ebox--string-pixel-width-cache-key (line factor)
|
||||
@ -140,8 +146,9 @@ of pixel measurements it triggers do not each rebuild the display signature.")
|
||||
(and constant-p
|
||||
(ebox--string-pixel-width-visual-key line factor))))
|
||||
|
||||
(defun ebox--cached-string-pixel-width (line factor key)
|
||||
"Return LINE width at FACTOR through the render and display caches at KEY."
|
||||
(defun ebox--cached-string-pixel-width
|
||||
(line factor key &optional start end)
|
||||
"Return LINE slice width at FACTOR through both caches at KEY."
|
||||
(let ((cached
|
||||
(or (and ebox--render-string-pixel-width-cache
|
||||
(gethash key ebox--render-string-pixel-width-cache))
|
||||
@ -151,7 +158,10 @@ of pixel measurements it triggers do not each rebuild the display signature.")
|
||||
(when ebox--render-string-pixel-width-cache
|
||||
(puthash key cached ebox--render-string-pixel-width-cache))
|
||||
cached)
|
||||
(let* ((width (ebox--scaled-string-pixel-width line factor))
|
||||
(let* ((sample (if (or start end)
|
||||
(substring line (or start 0) (or end (length line)))
|
||||
line))
|
||||
(width (ebox--scaled-string-pixel-width sample factor))
|
||||
(index ebox--string-pixel-width-cache-ring-index)
|
||||
(evicted (aref ebox--string-pixel-width-cache-ring index)))
|
||||
(when ebox--render-string-pixel-width-cache
|
||||
@ -188,21 +198,19 @@ keep their absolute pixel width."
|
||||
minimize (or (next-single-property-change
|
||||
pos property line end)
|
||||
end)))
|
||||
(segment (substring line pos next))
|
||||
;; The segment ends at every visual property change, so its
|
||||
;; width key can skip the second constant-property scan.
|
||||
(segment-key
|
||||
(ebox--string-pixel-width-visual-key segment factor))
|
||||
(space-width (ebox--display-space-width display)))
|
||||
(setq total
|
||||
(+ total
|
||||
(if space-width
|
||||
(* space-width (- next pos))
|
||||
(if segment-key
|
||||
(ebox--cached-string-pixel-width
|
||||
segment factor segment-key)
|
||||
(ebox--scaled-string-pixel-width
|
||||
segment factor)))))
|
||||
;; The segment ends at every visual property change.
|
||||
;; Build only its property-free cache key on hits; a
|
||||
;; propertized substring is materialized solely on miss.
|
||||
(let ((segment-key
|
||||
(ebox--string-pixel-width-visual-key
|
||||
line factor pos next)))
|
||||
(ebox--cached-string-pixel-width
|
||||
line factor segment-key pos next)))))
|
||||
(setq pos next)))
|
||||
total)))
|
||||
|
||||
|
||||
513
ebox-native-commit.el
Normal file
513
ebox-native-commit.el
Normal file
@ -0,0 +1,513 @@
|
||||
;;; ebox-native-commit.el --- Thin native frame commit for Ebox -*- lexical-binding: t; -*-
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Converts one pure native frame into candidate-local Ebox state. Rust owns
|
||||
;; layout, paint-property composition, and the fragment effect tape; this file
|
||||
;; only validates the side-effect boundary and installs already computed facts.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
|
||||
(declare-function ebox-native-reflow-layout-ready-p "ebox-native-reflow" ())
|
||||
(declare-function ebox-native-reflow-execute-sync
|
||||
"ebox-native-reflow" (node frame &optional layout-package))
|
||||
(declare-function ebox-native-reflow-execute-session-sync
|
||||
"ebox-native-reflow"
|
||||
(session node frame &optional layout-package state))
|
||||
(declare-function ebox-native-reflow-create-session
|
||||
"ebox-native-reflow" (&rest arguments))
|
||||
(declare-function ebox-native-reflow-confirm-native-frame
|
||||
"ebox-native-reflow"
|
||||
(session generation key confirmed-revision))
|
||||
(declare-function ebox-native-reflow-session-generation
|
||||
"ebox-native-reflow" (session))
|
||||
(declare-function ebox-native-reflow-frame-fragments
|
||||
"ebox-native-reflow" (frame))
|
||||
(declare-function ebox--literal-root-pixel-width "ebox" (box))
|
||||
(declare-function ebox-get "ebox" (box key))
|
||||
(declare-function ebox-style-cascade-active-p "ebox-style" ())
|
||||
(declare-function ebox-tree-node-key "ebox-tree" (node))
|
||||
(declare-function ebox-tree--children-raw "ebox-tree" (node))
|
||||
(declare-function ebox--node-direct-viewport-width-dependent-p
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--node-direct-viewport-height-dependent-p
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--render-cache-contained-viewport-node-p
|
||||
"ebox-incremental" (node))
|
||||
|
||||
(defun ebox-native-commit-topology-stable-p
|
||||
(old-state candidate-state prepared)
|
||||
"Return non-nil when PREPARED preserves OLD-STATE's object topology."
|
||||
(let ((old-nodes (plist-get old-state :node-table))
|
||||
(new-nodes (plist-get candidate-state :node-table))
|
||||
(old-parents (plist-get old-state :parent-table))
|
||||
(new-parents (plist-get candidate-state :parent-table))
|
||||
(old-regions (plist-get old-state :region-node-table))
|
||||
(new-regions (plist-get candidate-state :region-node-table))
|
||||
(old-objects (plist-get old-state :surface-node-object-table))
|
||||
(stable t)
|
||||
(missing (make-symbol "native-topology-missing")))
|
||||
(setq stable
|
||||
(and (null (plist-get prepared :removed-node-ids))
|
||||
(not (cl-some
|
||||
(lambda (entry)
|
||||
(eq (plist-get entry :dirty-kind) 'structure))
|
||||
(plist-get prepared :dirty-set)))
|
||||
(hash-table-p old-nodes) (hash-table-p new-nodes)
|
||||
(hash-table-p old-parents) (hash-table-p new-parents)
|
||||
(hash-table-p old-regions) (hash-table-p new-regions)
|
||||
(hash-table-p old-objects)
|
||||
(= (hash-table-count old-nodes)
|
||||
(hash-table-count new-nodes))
|
||||
(= (hash-table-count old-regions)
|
||||
(hash-table-count new-regions))))
|
||||
(when stable
|
||||
(maphash
|
||||
(lambda (node-id node)
|
||||
(let ((old-node (gethash node-id old-nodes missing)))
|
||||
(unless (and (not (eq old-node missing))
|
||||
(gethash node-id old-objects)
|
||||
(equal (gethash node-id old-parents missing)
|
||||
(gethash node-id new-parents missing))
|
||||
(eq (plist-get old-node :ebox-type)
|
||||
(plist-get node :ebox-type))
|
||||
(equal (ebox-tree-node-key old-node)
|
||||
(ebox-tree-node-key node)))
|
||||
(setq stable nil))))
|
||||
new-nodes))
|
||||
(when stable
|
||||
(maphash
|
||||
(lambda (region-id node-id)
|
||||
(unless (equal (gethash region-id old-regions missing) node-id)
|
||||
(setq stable nil)))
|
||||
new-regions))
|
||||
stable))
|
||||
|
||||
(defun ebox-native-commit-layout-touched-node-ids (candidate-state prepared)
|
||||
"Return PREPARED's exact dirty-to-root closure in CANDIDATE-STATE."
|
||||
(let ((parents (plist-get candidate-state :parent-table))
|
||||
(seen (make-hash-table :test 'equal))
|
||||
result)
|
||||
(dolist (entry (plist-get prepared :dirty-set))
|
||||
(let ((node-id (plist-get entry :node-id)))
|
||||
(while (and node-id (not (gethash node-id seen)))
|
||||
(puthash node-id t seen)
|
||||
(push node-id result)
|
||||
(setq node-id (and parents (gethash node-id parents))))))
|
||||
(nreverse result)))
|
||||
|
||||
(defun ebox-native-commit--node-position-table (state)
|
||||
"Return NODE-ID to `(PARENT-ID . POSITION)' table for STATE."
|
||||
(let ((nodes (plist-get state :node-table))
|
||||
(table (make-hash-table :test 'equal)))
|
||||
(when (hash-table-p nodes)
|
||||
(maphash
|
||||
(lambda (parent-id node)
|
||||
(cl-loop for child in (ebox-tree--children-raw node)
|
||||
for position from 0
|
||||
do (puthash (plist-get child :node-id)
|
||||
(cons parent-id position) table)))
|
||||
nodes)
|
||||
(when-let ((root (plist-get state :root-node)))
|
||||
(puthash (plist-get root :node-id) (cons nil 0) table)))
|
||||
table))
|
||||
|
||||
(defun ebox-native-commit-object-delta-node-ids
|
||||
(old-state candidate-state prepared)
|
||||
"Return the exact preorder TP object delta, or nil.
|
||||
Compare OLD-STATE with CANDIDATE-STATE under PREPARED's local index proof.
|
||||
Every old node absent from CANDIDATE-STATE must be named by PREPARED's removed
|
||||
set. Every new, replaced, moved, reparented, or explicitly touched node enters
|
||||
the result. Untouched nodes are authorized only when their old TP object,
|
||||
type, key, parent, and sibling position are identical."
|
||||
(let* ((old-root (plist-get old-state :root-node))
|
||||
(new-root (plist-get candidate-state :root-node))
|
||||
(old-nodes (plist-get old-state :node-table))
|
||||
(new-nodes (plist-get candidate-state :node-table))
|
||||
(old-objects (plist-get old-state :surface-node-object-table))
|
||||
(old-positions (ebox-native-commit--node-position-table old-state))
|
||||
(new-positions
|
||||
(ebox-native-commit--node-position-table candidate-state))
|
||||
(requested (make-hash-table :test 'equal))
|
||||
(removed (make-hash-table :test 'equal))
|
||||
(replaced (make-hash-table :test 'equal))
|
||||
(missing (make-symbol "native-object-delta-missing"))
|
||||
(valid
|
||||
(and old-root new-root
|
||||
(equal (plist-get old-root :node-id)
|
||||
(plist-get new-root :node-id))
|
||||
(eq (plist-get old-root :ebox-type)
|
||||
(plist-get new-root :ebox-type))
|
||||
(equal (ebox-tree-node-key old-root)
|
||||
(ebox-tree-node-key new-root))
|
||||
(hash-table-p old-nodes) (hash-table-p new-nodes)
|
||||
(hash-table-p old-objects)))
|
||||
result)
|
||||
(dolist (node-id (plist-get prepared :touched-node-ids))
|
||||
(if (gethash node-id new-nodes)
|
||||
(puthash node-id t requested)
|
||||
(setq valid nil)))
|
||||
(dolist (node-id (plist-get prepared :removed-node-ids))
|
||||
(if (and (gethash node-id old-nodes)
|
||||
(not (gethash node-id new-nodes)))
|
||||
(puthash node-id t removed)
|
||||
(setq valid nil)))
|
||||
(when valid
|
||||
(maphash
|
||||
(lambda (node-id _node)
|
||||
(when (and (not (gethash node-id new-nodes))
|
||||
(not (gethash node-id removed)))
|
||||
(setq valid nil)))
|
||||
old-nodes))
|
||||
(when valid
|
||||
(cl-labels
|
||||
((visit (node)
|
||||
(let* ((node-id (plist-get node :node-id))
|
||||
(old-node (gethash node-id old-nodes missing))
|
||||
(old-object (gethash node-id old-objects))
|
||||
(position (gethash node-id new-positions))
|
||||
(parent-id (car-safe position))
|
||||
(replacement-p
|
||||
(or (eq old-node missing)
|
||||
(null old-object)
|
||||
(not (eq (plist-get old-node :ebox-type)
|
||||
(plist-get node :ebox-type)))
|
||||
(not (equal (ebox-tree-node-key old-node)
|
||||
(ebox-tree-node-key node)))))
|
||||
(touched-p
|
||||
(or replacement-p
|
||||
(gethash node-id requested)
|
||||
(gethash parent-id replaced)
|
||||
(not (equal (gethash node-id old-positions missing)
|
||||
position)))))
|
||||
(unless (and node-id position)
|
||||
(setq valid nil))
|
||||
(when replacement-p (puthash node-id t replaced))
|
||||
(when touched-p (push node-id result))
|
||||
(dolist (child (ebox-tree--children-raw node))
|
||||
(visit child)))))
|
||||
(visit new-root)))
|
||||
(and valid result (nreverse result))))
|
||||
|
||||
(defun ebox-native-commit-context-axes-stable-p
|
||||
(old-state candidate-state prepared)
|
||||
"Return non-nil when PREPARED preserves viewport-axis membership."
|
||||
(let ((old-nodes (plist-get old-state :node-table))
|
||||
(new-nodes (plist-get candidate-state :node-table))
|
||||
stable)
|
||||
(setq stable
|
||||
(and (plist-get old-state :viewport-dependent-node-ids-ready)
|
||||
(plist-member old-state :viewport-dependent-node-id-axes)
|
||||
(hash-table-p old-nodes)
|
||||
(hash-table-p new-nodes)))
|
||||
(dolist (entry (plist-get prepared :dirty-set) stable)
|
||||
(let* ((node-id (plist-get entry :node-id))
|
||||
(old (and stable (gethash node-id old-nodes)))
|
||||
(new (and stable (gethash node-id new-nodes))))
|
||||
(unless (and old new
|
||||
(eq (ebox--node-direct-viewport-width-dependent-p old)
|
||||
(ebox--node-direct-viewport-width-dependent-p new))
|
||||
(eq (ebox--node-direct-viewport-height-dependent-p old)
|
||||
(ebox--node-direct-viewport-height-dependent-p new))
|
||||
(eq (ebox--render-cache-contained-viewport-node-p old)
|
||||
(ebox--render-cache-contained-viewport-node-p new)))
|
||||
(setq stable nil))))))
|
||||
|
||||
(defun ebox-native-commit-projection-eligible-p
|
||||
(previous-state state-overrides stylesheet-active-p)
|
||||
"Return non-nil when a candidate may use changed-only TP projection."
|
||||
(let ((state (append state-overrides nil)))
|
||||
(and previous-state
|
||||
(not stylesheet-active-p)
|
||||
(require 'ebox-native-reflow nil t)
|
||||
(ebox-native-reflow-layout-ready-p)
|
||||
(hash-table-p (plist-get previous-state
|
||||
:surface-node-object-table))
|
||||
(or (plist-get state :native-topology-stable-p)
|
||||
(consp (plist-get state :native-touched-node-ids)))
|
||||
(ebox-native-commit--runtime-types-supported-p state)
|
||||
(ebox-native-commit--region-index-safe-p state))))
|
||||
|
||||
(defun ebox-native-commit-plan-eligible-p
|
||||
(old-state candidate-state prepared)
|
||||
"Return non-nil when PREPARED can bypass superseded local layout proofs."
|
||||
(let* ((stable
|
||||
(ebox-native-commit-topology-stable-p
|
||||
old-state candidate-state prepared))
|
||||
(context-axes-stable
|
||||
(and stable
|
||||
(ebox-native-commit-context-axes-stable-p
|
||||
old-state candidate-state prepared)))
|
||||
(layout-touched
|
||||
(if stable
|
||||
(ebox-native-commit-layout-touched-node-ids
|
||||
candidate-state prepared)
|
||||
(ebox-native-commit-object-delta-node-ids
|
||||
old-state candidate-state prepared)))
|
||||
(state (copy-sequence candidate-state)))
|
||||
(plist-put candidate-state :native-topology-stable-p stable)
|
||||
(plist-put candidate-state :native-touched-node-ids layout-touched)
|
||||
(when context-axes-stable
|
||||
(plist-put candidate-state :viewport-dependent-node-ids-ready t)
|
||||
(plist-put candidate-state :viewport-dependent-node-ids
|
||||
(plist-get old-state :viewport-dependent-node-ids))
|
||||
(plist-put candidate-state :viewport-dependent-node-id-axes
|
||||
(plist-get old-state :viewport-dependent-node-id-axes)))
|
||||
(unless stable
|
||||
(cl-remf candidate-state :native-fragment-owner-cache))
|
||||
(plist-put state :native-touched-node-ids layout-touched)
|
||||
(plist-put state :native-topology-stable-p stable)
|
||||
(and (or stable layout-touched)
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(ebox-native-commit-projection-eligible-p
|
||||
old-state state nil))))
|
||||
|
||||
(defun ebox-native-commit--root-box (node)
|
||||
"Return NODE's outer box, or nil."
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box node)
|
||||
('flex (plist-get node :box))))
|
||||
|
||||
(defun ebox-native-commit--region-box (state region-id)
|
||||
"Resolve REGION-ID's candidate box directly from STATE indexes."
|
||||
(when-let* ((node-id (gethash region-id
|
||||
(plist-get state :region-node-table)))
|
||||
(node (gethash node-id (plist-get state :node-table))))
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box node)
|
||||
('flex (plist-get node :box)))))
|
||||
|
||||
(defun ebox-native-commit--region-index-safe-p (state)
|
||||
"Return non-nil when STATE has an unambiguous, non-scrolling region index."
|
||||
(let ((counts (plist-get state :region-box-count-table))
|
||||
(scroll (plist-get state :scroll-state-table))
|
||||
safe)
|
||||
(setq safe
|
||||
(and (hash-table-p counts)
|
||||
(or (null scroll)
|
||||
(and (hash-table-p scroll)
|
||||
(zerop (hash-table-count scroll))))))
|
||||
(when safe
|
||||
(maphash
|
||||
(lambda (region-id count)
|
||||
(let ((box (ebox-native-commit--region-box state region-id)))
|
||||
(unless (and (= count 1) box
|
||||
(zerop (or (ebox-get box :scroll-offset) 0)))
|
||||
(setq safe nil))))
|
||||
counts))
|
||||
safe))
|
||||
|
||||
(defun ebox-native-commit--runtime-types-supported-p (state)
|
||||
"Return non-nil when STATE contains only executable native node types."
|
||||
(let ((nodes (plist-get state :node-table))
|
||||
(supported t))
|
||||
(when (hash-table-p nodes)
|
||||
(maphash
|
||||
(lambda (_node-id node)
|
||||
(unless (memq (plist-get node :ebox-type)
|
||||
'(box concat stack flex flex-item))
|
||||
(setq supported nil)))
|
||||
nodes))
|
||||
supported))
|
||||
|
||||
(defun ebox-native-commit--install-region-boxes (state)
|
||||
"Install STATE's already indexed region boxes without walking its tree."
|
||||
(let ((table (plist-get state :region-box-table)))
|
||||
(unless (hash-table-p table)
|
||||
(setq table (make-hash-table :test 'equal))
|
||||
(plist-put state :region-box-table table))
|
||||
(when (zerop (hash-table-count table))
|
||||
(maphash
|
||||
(lambda (region-id _count)
|
||||
(puthash region-id
|
||||
(ebox-native-commit--region-box state region-id)
|
||||
table))
|
||||
(plist-get state :region-box-count-table)))
|
||||
table))
|
||||
|
||||
(defun ebox-native-commit--frame-spec (state node)
|
||||
"Return the exact native execution frame for STATE and NODE."
|
||||
(let* ((viewport-width (plist-get state :viewport-width))
|
||||
(viewport-height (plist-get state :viewport-height))
|
||||
(root-box (ebox-native-commit--root-box node))
|
||||
(root-width
|
||||
(or (and root-box (ebox--literal-root-pixel-width root-box))
|
||||
viewport-width)))
|
||||
(when (and (integerp viewport-width) (> viewport-width 0)
|
||||
(integerp viewport-height) (> viewport-height 0)
|
||||
(integerp root-width) (> root-width 0))
|
||||
(append
|
||||
(list :key 1
|
||||
:viewport-width viewport-width
|
||||
:viewport-height viewport-height
|
||||
:root-width root-width
|
||||
:runtime-revision
|
||||
(or (plist-get state :runtime-revision) 0)
|
||||
:context-hash
|
||||
(sxhash-equal
|
||||
(list viewport-width viewport-height root-width
|
||||
(plist-get state :display-signature)))
|
||||
:complete t
|
||||
:root-metadata nil)
|
||||
(when (plist-get state :native-sync-confirmed-p)
|
||||
(list :base-viewport-width viewport-width
|
||||
:base-viewport-height viewport-height
|
||||
:base-root-width root-width))))))
|
||||
|
||||
(defun ebox-native-commit--apply-patches (source patches)
|
||||
"Return SOURCE with ordered native PATCHES applied."
|
||||
(let ((cursor 0) pieces)
|
||||
(dolist (patch patches)
|
||||
(let ((start (plist-get patch :old-start))
|
||||
(end (plist-get patch :old-end))
|
||||
(replacement (plist-get patch :replacement)))
|
||||
(unless (and (integerp start) (integerp end)
|
||||
(<= cursor start end (length source))
|
||||
(stringp replacement))
|
||||
(error "Native retained frame has an invalid patch"))
|
||||
(push (substring source cursor start) pieces)
|
||||
(push replacement pieces)
|
||||
(setq cursor end)))
|
||||
(push (substring source cursor) pieces)
|
||||
(apply #'concat (nreverse pieces))))
|
||||
|
||||
(defun ebox-native-commit--apply-fragment-style-delta (template delta)
|
||||
"Return TEMPLATE with Rust-computed fragment style DELTA applied."
|
||||
(unless (vectorp template)
|
||||
(error "Native fragment style delta has no retained template"))
|
||||
(let ((target (copy-sequence template)))
|
||||
(dolist (entry delta)
|
||||
(let ((index (car entry)))
|
||||
(unless (and (integerp index) (<= 0 index) (< index (length target)))
|
||||
(error "Native fragment style delta index is out of bounds"))
|
||||
(let ((record (copy-sequence (aref target index))))
|
||||
(aset record 7 (copy-sequence (cdr entry)))
|
||||
(aset target index record))))
|
||||
target))
|
||||
|
||||
(defun ebox-native-commit--promote-pending-frame (state session)
|
||||
"Promote STATE's previously published pending frame in SESSION."
|
||||
(when-let ((pending (plist-get state :native-sync-pending)))
|
||||
(ebox-native-reflow-confirm-native-frame
|
||||
session (plist-get pending :generation) (plist-get pending :key)
|
||||
(plist-get pending :confirmed-revision))
|
||||
(plist-put state :native-sync-confirmed-p t)
|
||||
(cl-remf state :native-sync-pending)))
|
||||
|
||||
(defun ebox-native-commit--retained-frame (state node frame-spec)
|
||||
"Return NODE's full or patch-derived retained native frame for STATE."
|
||||
(let ((session
|
||||
(or (plist-get state :native-sync-session)
|
||||
(let ((created
|
||||
(ebox-native-reflow-create-session
|
||||
:workers 1 :max-jobs 4 :max-results 4)))
|
||||
(plist-put state :native-sync-session created)
|
||||
created))))
|
||||
(ebox-native-commit--promote-pending-frame state session)
|
||||
(let* ((result
|
||||
(ebox-native-reflow-execute-session-sync
|
||||
session node frame-spec nil state))
|
||||
(generation (ebox-native-reflow-session-generation session))
|
||||
(frame
|
||||
(if (plist-get result :native-patch)
|
||||
(let* ((source (plist-get state :native-committed-rendered))
|
||||
(metadata (plist-get result :root-render-metadata))
|
||||
(reuse-fragments
|
||||
(plist-get result :reuse-fragment-template))
|
||||
(fragment-style-delta
|
||||
(plist-get result :fragment-style-delta))
|
||||
(rendered nil))
|
||||
(unless (stringp source)
|
||||
(error "Native retained patch has no committed base"))
|
||||
(when (or reuse-fragments fragment-style-delta)
|
||||
(let ((template
|
||||
(plist-get state
|
||||
:native-committed-fragment-template)))
|
||||
(unless (vectorp template)
|
||||
(error "Native retained patch has no fragment base"))
|
||||
(when fragment-style-delta
|
||||
(setq template
|
||||
(ebox-native-commit--apply-fragment-style-delta
|
||||
template fragment-style-delta)))
|
||||
(setq metadata
|
||||
(plist-put metadata :fragment-span-template
|
||||
template))))
|
||||
(when (and (plist-get result :reuse-ownership-template)
|
||||
(plist-get state :native-topology-stable-p))
|
||||
(plist-put state :native-reuse-ownership-p t))
|
||||
(when (and (plist-get result :reuse-mount-projection)
|
||||
(plist-get state :native-topology-stable-p))
|
||||
(plist-put state :native-reuse-mount-projection-p t))
|
||||
(setq rendered
|
||||
(ebox-native-commit--apply-patches
|
||||
source (plist-get result :patches)))
|
||||
(plist-put state :native-coordinate-patches
|
||||
(plist-get result :coordinate-patches))
|
||||
(list :native-frame t :rendered rendered
|
||||
:root-render-metadata metadata
|
||||
:effect-tape metadata
|
||||
:line-widths []
|
||||
:patches (plist-get result :patches)
|
||||
:coordinate-patches
|
||||
(plist-get result :coordinate-patches)
|
||||
:base-character-count
|
||||
(plist-get result :base-character-count)
|
||||
:target-character-count
|
||||
(plist-get result :target-character-count)
|
||||
:styles (plist-get result :styles)
|
||||
:property-templates
|
||||
(plist-get result :property-templates)
|
||||
:native-patch t
|
||||
:reuse-fragment-template reuse-fragments
|
||||
:reuse-ownership-template
|
||||
(plist-get result :reuse-ownership-template)
|
||||
:reuse-mount-projection
|
||||
(plist-get result :reuse-mount-projection)
|
||||
:fragment-style-delta fragment-style-delta))
|
||||
result)))
|
||||
(plist-put state :native-sync-pending
|
||||
(list :generation generation :key 1
|
||||
:confirmed-revision
|
||||
(1+ (or (plist-get state :runtime-revision) 0))))
|
||||
(plist-put state :native-frame-kind
|
||||
(if (plist-get frame :native-patch) 'patch 'full))
|
||||
frame)))
|
||||
|
||||
(defun ebox-native-commit-render (state node)
|
||||
"Return NODE's native rendered string after committing effects into STATE.
|
||||
Return nil without mutating STATE when the native capability proof is absent."
|
||||
(when (and (require 'ebox-native-reflow nil t)
|
||||
(ebox-native-reflow-layout-ready-p)
|
||||
(memq (plist-get state :projection-kind) '(nil native-frame))
|
||||
(ebox-native-commit--runtime-types-supported-p state)
|
||||
(ebox-native-commit--region-index-safe-p state))
|
||||
(when-let ((frame-spec (ebox-native-commit--frame-spec state node)))
|
||||
(condition-case err
|
||||
(let* ((frame (ebox-native-commit--retained-frame
|
||||
state node frame-spec))
|
||||
(effects (plist-get frame :effect-tape))
|
||||
(rendered (plist-get frame :rendered))
|
||||
(fragment-template
|
||||
(and (not (plist-get effects :scroll-window-p))
|
||||
(plist-get effects :fragment-span-template))))
|
||||
(when (and (stringp rendered) (vectorp fragment-template))
|
||||
(ebox-native-commit--install-region-boxes state)
|
||||
(plist-put state :render-owned-text-values
|
||||
(make-hash-table :test #'eq))
|
||||
(plist-put state :native-render-output rendered)
|
||||
(plist-put state :native-render-frame frame)
|
||||
(plist-put state :native-render-fragment-template
|
||||
fragment-template)
|
||||
(plist-put state :native-render-p t)
|
||||
rendered))
|
||||
(error
|
||||
(plist-put state :native-render-fallback
|
||||
(error-message-string err))
|
||||
nil)))))
|
||||
|
||||
(provide 'ebox-native-commit)
|
||||
;;; ebox-native-commit.el ends here
|
||||
File diff suppressed because it is too large
Load Diff
@ -74,6 +74,9 @@ the render boundary."
|
||||
(let* ((next (min end
|
||||
(or (next-single-property-change
|
||||
position ebox--paint-origin-property string end)
|
||||
end)
|
||||
(or (next-single-property-change
|
||||
position 'face string end)
|
||||
end)))
|
||||
(origin (get-text-property
|
||||
position ebox--paint-origin-property string)))
|
||||
|
||||
@ -16,6 +16,24 @@
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox--flex-normalize-container-props
|
||||
"ebox-flex" (props))
|
||||
(declare-function ebox--normalize-horizontal-size-value
|
||||
"ebox-flex" (value allowed-keywords))
|
||||
(declare-function ebox--nonnegative-horizontal-size-pixels
|
||||
"ebox-layout" (value &optional default))
|
||||
(declare-function ecss-schema-set-create "ecss" ())
|
||||
(declare-function ecss-stylesheet-create "ecss" ())
|
||||
|
||||
(defvar ebox--preferred-size-keywords)
|
||||
(defvar ebox--min-size-keywords)
|
||||
(defvar ebox--max-size-keywords)
|
||||
|
||||
(defun ebox-style--create-schema-set ()
|
||||
"Return one private ECSS schema set for Ebox style state."
|
||||
(ecss-schema-set-create))
|
||||
|
||||
(defun ebox-style--create-stylesheet ()
|
||||
"Return one private ECSS stylesheet for Ebox style state."
|
||||
(ecss-stylesheet-create))
|
||||
|
||||
(defconst ebox-style--border-styles
|
||||
'(none hidden dotted dashed solid double groove ridge inset outset)
|
||||
@ -224,7 +242,7 @@
|
||||
(defvar ebox-style--property-table nil
|
||||
"Hash table from canonical property names and aliases to metadata.")
|
||||
|
||||
(defvar ebox-style-schemas (ecss-schema-set-create)
|
||||
(defvar ebox-style-schemas (ebox-style--create-schema-set)
|
||||
"ECSS property schemas for the isolated Ebox style domain.")
|
||||
|
||||
(defvar ebox-style--registered-schemas nil
|
||||
@ -597,12 +615,26 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
(setq options (plist-put options :shorthand shorthand)))
|
||||
options))
|
||||
|
||||
(defvar ebox-style--closed-computed-cache (make-hash-table :test #'equal)
|
||||
"Bounded cache of selector-free, dependency-free computed styles.")
|
||||
|
||||
(defconst ebox-style--closed-computed-cache-limit 512
|
||||
"Maximum selector-free computed styles retained across projections.")
|
||||
|
||||
(declare-function tp-computed-p "tp-style" (value))
|
||||
|
||||
(defun ebox-style--closed-declarations-p (declarations)
|
||||
"Return non-nil when DECLARATIONS contain no computed value source."
|
||||
(cl-loop for (_property value) on declarations by #'cddr
|
||||
never (tp-computed-p value)))
|
||||
|
||||
(defun ebox-style-register-properties ()
|
||||
"Register the complete namespaced Ebox property domain with ECSS."
|
||||
(dolist (property ebox-style--property-definitions)
|
||||
(apply #'ecss-schema-set-define
|
||||
ebox-style-schemas (plist-get property :id)
|
||||
(ebox-style--schema-options property)))
|
||||
(clrhash ebox-style--closed-computed-cache)
|
||||
(setq ebox-style--registered-schemas ebox-style-schemas))
|
||||
|
||||
(defun ebox-style--ensure-properties ()
|
||||
@ -615,7 +647,7 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
(setq ebox-style--registered-schemas ebox-style-schemas)
|
||||
(ebox-style-register-properties))))
|
||||
|
||||
(defvar ebox-style-stylesheet (ecss-stylesheet-create)
|
||||
(defvar ebox-style-stylesheet (ebox-style--create-stylesheet)
|
||||
"Isolated ECSS stylesheet containing Ebox layout and paint rules.")
|
||||
|
||||
(defun ebox-style-cascade-active-p ()
|
||||
@ -733,10 +765,26 @@ ORIGIN, LAYER, and SCOPE use ECSS cascade semantics."
|
||||
(defun ebox-style-compute-subject (subject declarations &optional parent-style)
|
||||
"Compute SUBJECT style from DECLARATIONS and optional PARENT-STYLE via ECSS."
|
||||
(ebox-style--ensure-properties)
|
||||
(ecss-compute-style
|
||||
ebox-style-schemas subject :declarations declarations
|
||||
:stylesheet ebox-style-stylesheet :parent-style parent-style
|
||||
:value-resolver #'tp-resolve-value))
|
||||
(let* ((closed-p
|
||||
(and (not (ebox-style-cascade-active-p))
|
||||
(ebox-style--closed-declarations-p declarations)))
|
||||
(key (and closed-p (list declarations parent-style)))
|
||||
(missing (make-symbol "ebox-closed-style-cache-missing"))
|
||||
(cached (and key (gethash key ebox-style--closed-computed-cache
|
||||
missing))))
|
||||
(if (and key (not (eq cached missing)))
|
||||
cached
|
||||
(let ((style
|
||||
(ecss-compute-style
|
||||
ebox-style-schemas subject :declarations declarations
|
||||
:stylesheet ebox-style-stylesheet :parent-style parent-style
|
||||
:value-resolver #'tp-resolve-value)))
|
||||
(when key
|
||||
(when (>= (hash-table-count ebox-style--closed-computed-cache)
|
||||
ebox-style--closed-computed-cache-limit)
|
||||
(clrhash ebox-style--closed-computed-cache))
|
||||
(puthash key style ebox-style--closed-computed-cache))
|
||||
style))))
|
||||
|
||||
(defconst ebox-style--theme-delta-properties
|
||||
'(ebox/color ebox/background-color)
|
||||
|
||||
1407
ebox-surface.el
1407
ebox-surface.el
File diff suppressed because it is too large
Load Diff
@ -14,12 +14,20 @@
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-child-range)
|
||||
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
(declare-function ebox--next-runtime-node-id "ebox" ())
|
||||
(declare-function ebox--string-region-ids "ebox" (string))
|
||||
(declare-function ebox--box-visible-overflow-p "ebox" (box))
|
||||
|
||||
(defconst ebox--default-display '(block flow)
|
||||
"Default CSS-like display for a plain Ebox box.")
|
||||
|
||||
(defvar ebox--node-region-ids-cache nil
|
||||
"Dynamic snapshot-detail-local node -> region ids cache.
|
||||
This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
(defvar ebox--flex-item-prop-keys)
|
||||
|
||||
(defun ebox-tree-computed-display (node)
|
||||
"Return NODE's canonical CSS-like display pair."
|
||||
|
||||
27
ebox.el
27
ebox.el
@ -26,7 +26,7 @@
|
||||
'("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
|
||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
||||
"ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el"
|
||||
"ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||
"ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||
"ebox-native-reflow.el")
|
||||
"Active Ebox Lisp sources compiled by `ebox-byte-compile'.")
|
||||
|
||||
@ -4520,10 +4520,22 @@ Return CANDIDATE."
|
||||
candidate host-ref next-subtree old-semantic-key new-semantic-key))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-range-ref (candidate range-ref next-items)
|
||||
"Replace base child RANGE-REF in CANDIDATE with proper node NEXT-ITEMS."
|
||||
(defun ebox-candidate-patch-host-paint
|
||||
(candidate host-ref previous-subtree next-subtree)
|
||||
"Patch HOST-REF paint from declarative OLD/NEW subtrees in CANDIDATE.
|
||||
Only node-local paint fields may differ. The Host's children and runtime
|
||||
identities are retained instead of being copied as a replacement subtree.
|
||||
Return CANDIDATE when accepted, or nil when the delta needs ordinary subtree
|
||||
replacement."
|
||||
(ebox-incremental-candidate-patch-host-paint
|
||||
candidate host-ref previous-subtree next-subtree))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-range-ref
|
||||
(candidate range-ref next-items &optional reuse-map)
|
||||
"Replace RANGE-REF, retaining slots named by proven REUSE-MAP."
|
||||
(ebox-incremental-candidate-replace-range-ref
|
||||
candidate range-ref next-items))
|
||||
candidate range-ref next-items reuse-map))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-root (candidate next-root)
|
||||
@ -4648,12 +4660,14 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
ebox-buffer-update-report
|
||||
ebox-build
|
||||
ebox-byte-compile
|
||||
ebox-call-with-render-burst
|
||||
ebox-candidate-begin
|
||||
ebox-child-range
|
||||
ebox-candidate-replace
|
||||
ebox-candidate-replace-range-ref
|
||||
ebox-candidate-replace-root
|
||||
ebox-candidate-replace-host-ref
|
||||
ebox-candidate-patch-host-paint
|
||||
ebox-clear-cache
|
||||
ebox-column
|
||||
ebox-commit
|
||||
@ -4674,6 +4688,8 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
ebox-region-resolve
|
||||
ebox-region-update
|
||||
ebox-render
|
||||
ebox-render-burst-begin
|
||||
ebox-render-burst-end
|
||||
ebox-render-to-buffer
|
||||
ebox-rerender-buffer-with-context
|
||||
ebox-row
|
||||
@ -4707,6 +4723,7 @@ and are documented separately in the public API reference.")
|
||||
direct-region-update
|
||||
dsl
|
||||
failure-atomic-rollback
|
||||
framework-render-burst
|
||||
flex-layout
|
||||
host-reference
|
||||
intrinsic-and-viewport-sizing
|
||||
@ -4763,6 +4780,8 @@ and are documented separately in the public API reference.")
|
||||
;; → replace one runtime node logically
|
||||
;; ebox-candidate-replace-host-ref cand ref root &optional old-key new-key
|
||||
;; → replace one semantic host boundary
|
||||
;; ebox-candidate-patch-host-paint cand ref old-root new-root
|
||||
;; → patch paint without replacing its subtree
|
||||
;; ebox-display-buffer buf box → render through TP and display the buffer
|
||||
;; ebox-host-ref-position buf ref → first live position for :host-ref
|
||||
;; ebox-host-ref-bounds buf ref → live (START . END) bounds for :host-ref
|
||||
|
||||
5
native/Cargo.lock
generated
5
native/Cargo.lock
generated
@ -17,10 +17,15 @@ name = "ebox-native-reflow"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"etaf-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etaf-core"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
||||
@ -10,6 +10,7 @@ name = "ebox_native_reflow"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
etaf-core = { path = "core" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@ -67,6 +67,11 @@ extern bool ebox_native_layout_ready(void);
|
||||
extern bool ebox_native_render_proof(const unsigned char *payload,
|
||||
size_t payload_len,
|
||||
ebox_native_bytes *output);
|
||||
extern bool ebox_native_session_render_sync(void *session,
|
||||
uint64_t generation,
|
||||
const unsigned char *payload,
|
||||
size_t payload_len,
|
||||
ebox_native_bytes *output);
|
||||
extern bool ebox_native_flex_size_lines(const ebox_native_flex_item *items,
|
||||
size_t item_count,
|
||||
const size_t *line_offsets,
|
||||
@ -157,7 +162,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
|
||||
(void) nargs;
|
||||
(void) args;
|
||||
(void) data;
|
||||
static const char version[] = "8:3:9";
|
||||
static const char version[] = "9:7:12";
|
||||
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
|
||||
}
|
||||
|
||||
@ -320,6 +325,333 @@ ebox_module_render_proof(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_read_u16(const unsigned char *bytes, size_t length, size_t offset,
|
||||
uint16_t *value)
|
||||
{
|
||||
if (offset > length || length - offset < 2)
|
||||
return false;
|
||||
*value = (uint16_t) bytes[offset] | ((uint16_t) bytes[offset + 1] << 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_read_u32(const unsigned char *bytes, size_t length, size_t offset,
|
||||
uint32_t *value)
|
||||
{
|
||||
if (offset > length || length - offset < 4)
|
||||
return false;
|
||||
*value = (uint32_t) bytes[offset]
|
||||
| ((uint32_t) bytes[offset + 1] << 8)
|
||||
| ((uint32_t) bytes[offset + 2] << 16)
|
||||
| ((uint32_t) bytes[offset + 3] << 24);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_read_u64(const unsigned char *bytes, size_t length, size_t offset,
|
||||
uint64_t *value)
|
||||
{
|
||||
if (offset > length || length - offset < 8)
|
||||
return false;
|
||||
uint64_t result = 0;
|
||||
for (size_t index = 0; index < 8; index++)
|
||||
result |= (uint64_t) bytes[offset + index] << (index * 8);
|
||||
*value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_checked_add(size_t left, size_t right, size_t *result)
|
||||
{
|
||||
if (right > SIZE_MAX - left)
|
||||
return false;
|
||||
*result = left + right;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_fragment_section(const unsigned char *bytes, size_t length,
|
||||
size_t *start, size_t *fragment_length,
|
||||
size_t *record_count)
|
||||
{
|
||||
static const size_t header_length = 112;
|
||||
uint16_t version = 0;
|
||||
uint16_t flags = 0;
|
||||
uint32_t encoded_header_length = 0;
|
||||
uint64_t encoded_total_length = 0;
|
||||
uint64_t fragment_bytes = 0;
|
||||
uint64_t fragment_records = 0;
|
||||
if (length < header_length
|
||||
|| memcmp(bytes, "EBXT", 4) != 0
|
||||
|| !ebox_read_u16(bytes, length, 4, &version)
|
||||
|| version != 12
|
||||
|| !ebox_read_u16(bytes, length, 6, &flags)
|
||||
|| !ebox_read_u32(bytes, length, 8, &encoded_header_length)
|
||||
|| encoded_header_length != header_length
|
||||
|| !ebox_read_u64(bytes, length, 12, &encoded_total_length)
|
||||
|| encoded_total_length != length)
|
||||
return false;
|
||||
|
||||
size_t offset = header_length;
|
||||
if ((flags & 4) != 0) {
|
||||
uint32_t patch_count = 0;
|
||||
uint32_t coordinate_patch_count = 0;
|
||||
uint64_t payload_length = 0;
|
||||
if (!ebox_read_u32(bytes, length, 120, &patch_count)
|
||||
|| !ebox_read_u64(bytes, length, 136, &payload_length)
|
||||
|| !ebox_read_u64(bytes, length, 144, &fragment_bytes)
|
||||
|| !ebox_read_u64(bytes, length, 152, &fragment_records)
|
||||
|| !ebox_read_u32(bytes, length, 160, &coordinate_patch_count))
|
||||
return false;
|
||||
size_t descriptor_bytes = 0;
|
||||
if ((size_t) patch_count > SIZE_MAX - (size_t) coordinate_patch_count
|
||||
|| (size_t) patch_count + (size_t) coordinate_patch_count
|
||||
> SIZE_MAX / 32)
|
||||
return false;
|
||||
descriptor_bytes = ((size_t) patch_count + (size_t) coordinate_patch_count)
|
||||
* 32;
|
||||
if (!ebox_checked_add(offset, 56, &offset)
|
||||
|| !ebox_checked_add(offset, descriptor_bytes, &offset)
|
||||
|| payload_length > SIZE_MAX
|
||||
|| !ebox_checked_add(offset, (size_t) payload_length, &offset))
|
||||
return false;
|
||||
} else {
|
||||
uint64_t literal_length = 0;
|
||||
uint32_t line_count = 0;
|
||||
uint32_t metadata_length = 0;
|
||||
if (!ebox_read_u64(bytes, length, 112, &literal_length)
|
||||
|| !ebox_read_u32(bytes, length, 120, &line_count)
|
||||
|| !ebox_read_u32(bytes, length, 124, &metadata_length)
|
||||
|| !ebox_read_u64(bytes, length, 136, &fragment_bytes)
|
||||
|| !ebox_read_u64(bytes, length, 144, &fragment_records))
|
||||
return false;
|
||||
size_t width_bytes = 0;
|
||||
if ((size_t) line_count > SIZE_MAX / 8)
|
||||
return false;
|
||||
width_bytes = (size_t) line_count * 8;
|
||||
if (!ebox_checked_add(offset, 40, &offset)
|
||||
|| !ebox_checked_add(offset, width_bytes, &offset)
|
||||
|| literal_length > SIZE_MAX
|
||||
|| !ebox_checked_add(offset, (size_t) literal_length, &offset)
|
||||
|| !ebox_checked_add(offset, (size_t) metadata_length, &offset))
|
||||
return false;
|
||||
}
|
||||
if (fragment_bytes > SIZE_MAX || fragment_records > SIZE_MAX
|
||||
|| offset > length || (size_t) fragment_bytes != length - offset)
|
||||
return false;
|
||||
*start = offset;
|
||||
*fragment_length = (size_t) fragment_bytes;
|
||||
*record_count = (size_t) fragment_records;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_fragment_flat_length(const unsigned char *bytes, size_t length,
|
||||
size_t record_count, size_t *scalar_count)
|
||||
{
|
||||
size_t cursor = 0;
|
||||
size_t scalars = 0;
|
||||
for (size_t record = 0; record < record_count; record++) {
|
||||
uint32_t roles = 0;
|
||||
uint32_t templates = 0;
|
||||
uint32_t styles = 0;
|
||||
uint32_t reserved = 0;
|
||||
if (cursor > length || length - cursor < 56
|
||||
|| !ebox_read_u32(bytes, length, cursor + 40, &roles)
|
||||
|| !ebox_read_u32(bytes, length, cursor + 44, &templates)
|
||||
|| !ebox_read_u32(bytes, length, cursor + 48, &styles)
|
||||
|| !ebox_read_u32(bytes, length, cursor + 52, &reserved)
|
||||
|| reserved != 0)
|
||||
return false;
|
||||
size_t variable = 0;
|
||||
if ((size_t) roles > SIZE_MAX / 9
|
||||
|| (size_t) templates > SIZE_MAX / 4
|
||||
|| (size_t) styles > SIZE_MAX / 4
|
||||
|| !ebox_checked_add((size_t) roles * 9,
|
||||
(size_t) templates * 4, &variable)
|
||||
|| !ebox_checked_add(variable, (size_t) styles * 4, &variable)
|
||||
|| !ebox_checked_add(cursor, 56, &cursor)
|
||||
|| !ebox_checked_add(cursor, variable, &cursor))
|
||||
return false;
|
||||
size_t record_scalars = 8;
|
||||
if ((size_t) roles > (SIZE_MAX - record_scalars) / 2)
|
||||
return false;
|
||||
record_scalars += (size_t) roles * 2;
|
||||
if (!ebox_checked_add(record_scalars, (size_t) templates,
|
||||
&record_scalars)
|
||||
|| !ebox_checked_add(record_scalars, (size_t) styles,
|
||||
&record_scalars)
|
||||
|| !ebox_checked_add(scalars, record_scalars, &scalars))
|
||||
return false;
|
||||
}
|
||||
if (cursor != length || scalars > PTRDIFF_MAX)
|
||||
return false;
|
||||
*scalar_count = scalars;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_flat_set(emacs_env *env, emacs_value vector, size_t *index,
|
||||
int64_t value)
|
||||
{
|
||||
env->vec_set(env, vector, (ptrdiff_t) *index,
|
||||
env->make_integer(env, (intmax_t) value));
|
||||
(*index)++;
|
||||
return env->non_local_exit_check(env) == emacs_funcall_exit_return;
|
||||
}
|
||||
|
||||
static emacs_value
|
||||
ebox_fragment_flat_vector(emacs_env *env, const unsigned char *bytes,
|
||||
size_t length, size_t record_count)
|
||||
{
|
||||
size_t scalar_count = 0;
|
||||
if (!ebox_fragment_flat_length(bytes, length, record_count, &scalar_count)) {
|
||||
static const char message[] = "Native reflow fragment tape is malformed";
|
||||
return ebox_signal_error(env, message, sizeof message - 1);
|
||||
}
|
||||
emacs_value make_args[] = {
|
||||
env->make_integer(env, (intmax_t) scalar_count), ebox_nil(env)
|
||||
};
|
||||
emacs_value vector = env->funcall(env, env->intern(env, "make-vector"),
|
||||
2, make_args);
|
||||
if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
|
||||
return ebox_nil(env);
|
||||
|
||||
size_t cursor = 0;
|
||||
size_t output = 0;
|
||||
for (size_t record = 0; record < record_count; record++) {
|
||||
uint64_t start = 0;
|
||||
uint64_t end = 0;
|
||||
uint64_t line = 0;
|
||||
uint64_t owner = 0;
|
||||
uint64_t content_index = 0;
|
||||
uint32_t roles = 0;
|
||||
uint32_t templates = 0;
|
||||
uint32_t styles = 0;
|
||||
ebox_read_u64(bytes, length, cursor, &start);
|
||||
ebox_read_u64(bytes, length, cursor + 8, &end);
|
||||
ebox_read_u64(bytes, length, cursor + 16, &line);
|
||||
ebox_read_u64(bytes, length, cursor + 24, &owner);
|
||||
ebox_read_u64(bytes, length, cursor + 32, &content_index);
|
||||
ebox_read_u32(bytes, length, cursor + 40, &roles);
|
||||
ebox_read_u32(bytes, length, cursor + 44, &templates);
|
||||
ebox_read_u32(bytes, length, cursor + 48, &styles);
|
||||
if (start > INT64_MAX || end > INT64_MAX || line > INT64_MAX
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) start)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) end)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) line)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) owner)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) content_index)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) roles)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) templates)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) styles))
|
||||
return ebox_nil(env);
|
||||
cursor += 56;
|
||||
for (uint32_t role = 0; role < roles; role++) {
|
||||
uint8_t kind = bytes[cursor];
|
||||
uint64_t region = 0;
|
||||
ebox_read_u64(bytes, length, cursor + 1, ®ion);
|
||||
if (region > INT64_MAX
|
||||
|| !ebox_flat_set(env, vector, &output, kind)
|
||||
|| !ebox_flat_set(env, vector, &output, (int64_t) region))
|
||||
return ebox_nil(env);
|
||||
cursor += 9;
|
||||
}
|
||||
for (uint32_t index = 0; index < templates + styles; index++) {
|
||||
uint32_t value = 0;
|
||||
ebox_read_u32(bytes, length, cursor, &value);
|
||||
if (!ebox_flat_set(env, vector, &output, value))
|
||||
return ebox_nil(env);
|
||||
cursor += 4;
|
||||
}
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
static emacs_value
|
||||
ebox_module_render_frame(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
|
||||
void *data)
|
||||
{
|
||||
(void) nargs;
|
||||
(void) data;
|
||||
unsigned char *payload = NULL;
|
||||
size_t payload_len = 0;
|
||||
if (!ebox_copy_string(env, args[0], &payload, &payload_len))
|
||||
return ebox_nil(env);
|
||||
ebox_native_bytes output = {0};
|
||||
bool ok = ebox_native_render_proof(payload, payload_len, &output);
|
||||
free(payload);
|
||||
if (!ok)
|
||||
return ebox_signal_native_error(env, output,
|
||||
"Native frame render failed");
|
||||
size_t fragment_start = 0;
|
||||
size_t fragment_length = 0;
|
||||
size_t record_count = 0;
|
||||
if (!ebox_fragment_section(output.data, output.len, &fragment_start,
|
||||
&fragment_length, &record_count)) {
|
||||
ebox_native_bytes_free(output);
|
||||
static const char message[] = "Native frame has an invalid effect tape";
|
||||
return ebox_signal_error(env, message, sizeof message - 1);
|
||||
}
|
||||
emacs_value tape = env->make_unibyte_string(
|
||||
env, (const char *) output.data, (ptrdiff_t) output.len);
|
||||
emacs_value flat = ebox_fragment_flat_vector(
|
||||
env, output.data + fragment_start, fragment_length, record_count);
|
||||
ebox_native_bytes_free(output);
|
||||
if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
|
||||
return ebox_nil(env);
|
||||
emacs_value values[] = {
|
||||
tape, flat, env->make_integer(env, (intmax_t) record_count)
|
||||
};
|
||||
return env->funcall(env, env->intern(env, "vector"), 3, values);
|
||||
}
|
||||
|
||||
static emacs_value
|
||||
ebox_module_render_session_frame(emacs_env *env, ptrdiff_t nargs,
|
||||
emacs_value *args, void *data)
|
||||
{
|
||||
(void) nargs;
|
||||
(void) data;
|
||||
void *session = ebox_session_pointer(env, args[0]);
|
||||
uint64_t generation = (uint64_t) env->extract_integer(env, args[1]);
|
||||
if (session == NULL ||
|
||||
env->non_local_exit_check(env) != emacs_funcall_exit_return)
|
||||
return ebox_nil(env);
|
||||
unsigned char *payload = NULL;
|
||||
size_t payload_len = 0;
|
||||
if (!ebox_copy_string(env, args[2], &payload, &payload_len))
|
||||
return ebox_nil(env);
|
||||
ebox_native_bytes output = {0};
|
||||
bool ok = ebox_native_session_render_sync(
|
||||
session, generation, payload, payload_len, &output);
|
||||
free(payload);
|
||||
if (!ok)
|
||||
return ebox_signal_native_error(env, output,
|
||||
"Native retained frame render failed");
|
||||
size_t fragment_start = 0;
|
||||
size_t fragment_length = 0;
|
||||
size_t record_count = 0;
|
||||
if (!ebox_fragment_section(output.data, output.len, &fragment_start,
|
||||
&fragment_length, &record_count)) {
|
||||
ebox_native_bytes_free(output);
|
||||
static const char message[] =
|
||||
"Native retained frame has an invalid effect tape";
|
||||
return ebox_signal_error(env, message, sizeof message - 1);
|
||||
}
|
||||
emacs_value tape = env->make_unibyte_string(
|
||||
env, (const char *) output.data, (ptrdiff_t) output.len);
|
||||
emacs_value flat = ebox_fragment_flat_vector(
|
||||
env, output.data + fragment_start, fragment_length, record_count);
|
||||
ebox_native_bytes_free(output);
|
||||
if (env->non_local_exit_check(env) != emacs_funcall_exit_return)
|
||||
return ebox_nil(env);
|
||||
emacs_value values[] = {
|
||||
tape, flat, env->make_integer(env, (intmax_t) record_count)
|
||||
};
|
||||
return env->funcall(env, env->intern(env, "vector"), 3, values);
|
||||
}
|
||||
|
||||
static bool
|
||||
ebox_extract_flex_integer(emacs_env *env, emacs_value value, int64_t *output)
|
||||
{
|
||||
@ -602,6 +934,12 @@ ebox_module_init_impl(struct emacs_runtime *runtime)
|
||||
ebox_bind_function(env, "ebox-native--module-render-proof", 1, 1,
|
||||
ebox_module_render_proof,
|
||||
"Render one private native proof synchronously.") ||
|
||||
ebox_bind_function(env, "ebox-native--module-render-frame", 1, 1,
|
||||
ebox_module_render_frame,
|
||||
"Render one native frame with decoded effects.") ||
|
||||
ebox_bind_function(env, "ebox-native--module-render-session-frame", 3, 3,
|
||||
ebox_module_render_session_frame,
|
||||
"Render one retained native frame synchronously.") ||
|
||||
ebox_bind_function(env, "ebox-native--module-flex-size-lines", 3, 3,
|
||||
ebox_module_flex_size_lines,
|
||||
"Calculate pure native Flex geometry lines.") ||
|
||||
|
||||
12
native/core/Cargo.toml
Normal file
12
native/core/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "etaf-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.82"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "etaf_core"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[dependencies]
|
||||
432
native/core/src/lib.rs
Normal file
432
native/core/src/lib.rs
Normal file
@ -0,0 +1,432 @@
|
||||
//! Editor-independent retained surface computation.
|
||||
//!
|
||||
//! This crate owns only deterministic data transformations. It deliberately
|
||||
//! has no editor, FFI, serialization, marker, face, or buffer dependency.
|
||||
|
||||
/// Monotonic retained surface revision.
|
||||
pub type Revision = u64;
|
||||
|
||||
/// Half-open coordinate range in one retained surface revision.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TextRange {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl TextRange {
|
||||
pub fn new(start: usize, end: usize) -> Result<Self, CommitError> {
|
||||
if start > end {
|
||||
return Err(CommitError::InvalidRange { start, end });
|
||||
}
|
||||
Ok(Self { start, end })
|
||||
}
|
||||
}
|
||||
|
||||
/// One aligned replacement between base and target revisions.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SpanEdit {
|
||||
pub old_start: usize,
|
||||
pub old_end: usize,
|
||||
pub new_start: usize,
|
||||
pub new_end: usize,
|
||||
}
|
||||
|
||||
/// Pure computation result consumed by an editor-specific commit adapter.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CommitBatch {
|
||||
pub base_revision: Revision,
|
||||
pub target_revision: Revision,
|
||||
pub base_extent: usize,
|
||||
pub target_extent: usize,
|
||||
/// Text or semantic state changed in these aligned spans.
|
||||
pub semantic_edits: Vec<SpanEdit>,
|
||||
/// Only text coordinates changed in these aligned spans.
|
||||
pub coordinate_edits: Vec<SpanEdit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CommitError {
|
||||
NonConsecutiveRevision {
|
||||
base: Revision,
|
||||
target: Revision,
|
||||
},
|
||||
InvalidRange {
|
||||
start: usize,
|
||||
end: usize,
|
||||
},
|
||||
}
|
||||
|
||||
const MYERS_MAX_SEARCH_DISTANCE: usize = 1024;
|
||||
|
||||
fn trace_value(trace: &[isize], distance: usize, diagonal: isize) -> isize {
|
||||
let distance = distance as isize;
|
||||
if diagonal < -distance || diagonal > distance {
|
||||
-1
|
||||
} else {
|
||||
trace[(diagonal + distance) as usize]
|
||||
}
|
||||
}
|
||||
|
||||
fn core_matches<T, F>(
|
||||
old: &[T],
|
||||
new: &[T],
|
||||
equal: &F,
|
||||
max_distance: usize,
|
||||
) -> Option<Vec<(usize, usize, usize)>>
|
||||
where
|
||||
F: Fn(&T, &T) -> bool,
|
||||
{
|
||||
let old_length = old.len();
|
||||
let new_length = new.len();
|
||||
let maximum = old_length.saturating_add(new_length);
|
||||
let bounded = maximum.min(max_distance);
|
||||
let mut traces: Vec<Vec<isize>> = Vec::with_capacity(bounded.saturating_add(1));
|
||||
let mut final_distance = maximum;
|
||||
let mut found = false;
|
||||
|
||||
'search: for distance in 0..=bounded {
|
||||
let previous = distance.checked_sub(1).and_then(|index| traces.get(index));
|
||||
let mut current = vec![-1; 1 + 2 * distance];
|
||||
let mut diagonal = -(distance as isize);
|
||||
while diagonal <= distance as isize {
|
||||
let down = previous.map(|trace| trace_value(trace, distance - 1, diagonal + 1));
|
||||
let right = previous.map(|trace| trace_value(trace, distance - 1, diagonal - 1));
|
||||
let mut old_position = if distance == 0 {
|
||||
0
|
||||
} else if diagonal == -(distance as isize) {
|
||||
down.unwrap_or(-1)
|
||||
} else if diagonal == distance as isize {
|
||||
right.unwrap_or(-1) + 1
|
||||
} else if right.unwrap_or(-1) < down.unwrap_or(-1) {
|
||||
down.unwrap_or(-1)
|
||||
} else {
|
||||
right.unwrap_or(-1) + 1
|
||||
};
|
||||
let mut new_position = old_position - diagonal;
|
||||
while old_position >= 0
|
||||
&& new_position >= 0
|
||||
&& (old_position as usize) < old_length
|
||||
&& (new_position as usize) < new_length
|
||||
&& equal(&old[old_position as usize], &new[new_position as usize])
|
||||
{
|
||||
old_position += 1;
|
||||
new_position += 1;
|
||||
}
|
||||
current[(diagonal + distance as isize) as usize] = old_position;
|
||||
if old_position >= old_length as isize && new_position >= new_length as isize {
|
||||
traces.push(current);
|
||||
final_distance = distance;
|
||||
found = true;
|
||||
break 'search;
|
||||
}
|
||||
diagonal += 2;
|
||||
}
|
||||
traces.push(current);
|
||||
}
|
||||
if !found {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut old_position = old_length as isize;
|
||||
let mut new_position = new_length as isize;
|
||||
let mut matches = Vec::new();
|
||||
for distance in (1..=final_distance).rev() {
|
||||
let previous_distance = distance - 1;
|
||||
let previous = &traces[previous_distance];
|
||||
let diagonal = old_position - new_position;
|
||||
let previous_diagonal = if diagonal == -(distance as isize)
|
||||
|| (diagonal != distance as isize
|
||||
&& trace_value(previous, previous_distance, diagonal - 1)
|
||||
< trace_value(previous, previous_distance, diagonal + 1))
|
||||
{
|
||||
diagonal + 1
|
||||
} else {
|
||||
diagonal - 1
|
||||
};
|
||||
let previous_old = trace_value(previous, previous_distance, previous_diagonal);
|
||||
let previous_new = previous_old - previous_diagonal;
|
||||
if old_position > previous_old && new_position > previous_new {
|
||||
let length = (old_position - previous_old).min(new_position - previous_new) as usize;
|
||||
matches.push((
|
||||
old_position as usize - length,
|
||||
new_position as usize - length,
|
||||
length,
|
||||
));
|
||||
}
|
||||
old_position = previous_old;
|
||||
new_position = previous_new;
|
||||
}
|
||||
if old_position > 0 && new_position > 0 {
|
||||
let length = old_position.min(new_position) as usize;
|
||||
matches.push((
|
||||
old_position as usize - length,
|
||||
new_position as usize - length,
|
||||
length,
|
||||
));
|
||||
}
|
||||
matches.reverse();
|
||||
Some(matches)
|
||||
}
|
||||
|
||||
fn matches<T, F>(old: &[T], new: &[T], equal: &F) -> Vec<(usize, usize, usize)>
|
||||
where
|
||||
F: Fn(&T, &T) -> bool,
|
||||
{
|
||||
let prefix = old
|
||||
.iter()
|
||||
.zip(new)
|
||||
.take_while(|(left, right)| equal(left, right))
|
||||
.count();
|
||||
let old_rest = &old[prefix..];
|
||||
let new_rest = &new[prefix..];
|
||||
let suffix = old_rest
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(new_rest.iter().rev())
|
||||
.take_while(|(left, right)| equal(left, right))
|
||||
.count();
|
||||
let old_core = &old_rest[..old_rest.len() - suffix];
|
||||
let new_core = &new_rest[..new_rest.len() - suffix];
|
||||
let mut result = Vec::new();
|
||||
if prefix > 0 {
|
||||
result.push((0, 0, prefix));
|
||||
}
|
||||
if let Some(core) = core_matches(old_core, new_core, equal, MYERS_MAX_SEARCH_DISTANCE) {
|
||||
result.extend(
|
||||
core.into_iter()
|
||||
.map(|(old_start, new_start, length)| {
|
||||
(old_start + prefix, new_start + prefix, length)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if suffix > 0 {
|
||||
result.push((old.len() - suffix, new.len() - suffix, suffix));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn push_edit(
|
||||
edits: &mut Vec<SpanEdit>,
|
||||
old_start: usize,
|
||||
old_end: usize,
|
||||
new_start: usize,
|
||||
new_end: usize,
|
||||
) {
|
||||
if old_start == old_end && new_start == new_end {
|
||||
return;
|
||||
}
|
||||
if let Some(previous) = edits.last_mut() {
|
||||
if previous.old_end == old_start && previous.new_end == new_start {
|
||||
previous.old_end = old_end;
|
||||
previous.new_end = new_end;
|
||||
return;
|
||||
}
|
||||
}
|
||||
edits.push(SpanEdit {
|
||||
old_start,
|
||||
old_end,
|
||||
new_start,
|
||||
new_end,
|
||||
});
|
||||
}
|
||||
|
||||
fn coordinate_edits<T, F>(old: &[T], new: &[T], text_equal: &F) -> Vec<SpanEdit>
|
||||
where
|
||||
F: Fn(&T, &T) -> bool,
|
||||
{
|
||||
let mut old_position = 0;
|
||||
let mut new_position = 0;
|
||||
let mut edits = Vec::new();
|
||||
for (matched_old, matched_new, length) in matches(old, new, text_equal) {
|
||||
if old_position < matched_old || new_position < matched_new {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
old_position,
|
||||
matched_old,
|
||||
new_position,
|
||||
matched_new,
|
||||
);
|
||||
}
|
||||
old_position = matched_old + length;
|
||||
new_position = matched_new + length;
|
||||
}
|
||||
push_edit(
|
||||
&mut edits,
|
||||
old_position,
|
||||
old.len(),
|
||||
new_position,
|
||||
new.len(),
|
||||
);
|
||||
edits
|
||||
}
|
||||
|
||||
fn semantic_edits<T, FText, FSemantic>(
|
||||
old: &[T],
|
||||
new: &[T],
|
||||
text_equal: &FText,
|
||||
semantic_equal: &FSemantic,
|
||||
) -> Vec<SpanEdit>
|
||||
where
|
||||
FText: Fn(&T, &T) -> bool,
|
||||
FSemantic: Fn(&T, &T) -> bool,
|
||||
{
|
||||
let text_matches = matches(old, new, text_equal);
|
||||
let mut old_position = 0;
|
||||
let mut new_position = 0;
|
||||
let mut edits = Vec::new();
|
||||
for (matched_old, matched_new, length) in text_matches {
|
||||
if old_position < matched_old || new_position < matched_new {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
old_position,
|
||||
matched_old,
|
||||
new_position,
|
||||
matched_new,
|
||||
);
|
||||
}
|
||||
let mut run_start = None;
|
||||
for offset in 0..length {
|
||||
let equal = semantic_equal(&old[matched_old + offset], &new[matched_new + offset]);
|
||||
match (run_start, equal) {
|
||||
(None, false) => run_start = Some(offset),
|
||||
(Some(start), true) => {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
matched_old + start,
|
||||
matched_old + offset,
|
||||
matched_new + start,
|
||||
matched_new + offset,
|
||||
);
|
||||
run_start = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(start) = run_start {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
matched_old + start,
|
||||
matched_old + length,
|
||||
matched_new + start,
|
||||
matched_new + length,
|
||||
);
|
||||
}
|
||||
old_position = matched_old + length;
|
||||
new_position = matched_new + length;
|
||||
}
|
||||
push_edit(
|
||||
&mut edits,
|
||||
old_position,
|
||||
old.len(),
|
||||
new_position,
|
||||
new.len(),
|
||||
);
|
||||
edits
|
||||
}
|
||||
|
||||
/// Diff two retained frames without knowing their editor-specific cell type.
|
||||
pub fn diff_commit_batch<T, FText, FSemantic>(
|
||||
base_revision: Revision,
|
||||
target_revision: Revision,
|
||||
old: &[T],
|
||||
new: &[T],
|
||||
text_equal: FText,
|
||||
semantic_equal: FSemantic,
|
||||
) -> Result<CommitBatch, CommitError>
|
||||
where
|
||||
FText: Fn(&T, &T) -> bool,
|
||||
FSemantic: Fn(&T, &T) -> bool,
|
||||
{
|
||||
if target_revision != base_revision.saturating_add(1) {
|
||||
return Err(CommitError::NonConsecutiveRevision {
|
||||
base: base_revision,
|
||||
target: target_revision,
|
||||
});
|
||||
}
|
||||
Ok(CommitBatch {
|
||||
base_revision,
|
||||
target_revision,
|
||||
base_extent: old.len(),
|
||||
target_extent: new.len(),
|
||||
semantic_edits: semantic_edits(old, new, &text_equal, &semantic_equal),
|
||||
coordinate_edits: coordinate_edits(old, new, &text_equal),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Cell {
|
||||
text: char,
|
||||
style: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_batch_separates_text_and_style_changes() {
|
||||
let old = [
|
||||
Cell { text: 'a', style: 1 },
|
||||
Cell { text: 'b', style: 1 },
|
||||
Cell { text: 'c', style: 1 },
|
||||
];
|
||||
let new = [
|
||||
Cell { text: 'a', style: 1 },
|
||||
Cell { text: 'B', style: 2 },
|
||||
Cell { text: 'c', style: 3 },
|
||||
];
|
||||
let batch = diff_commit_batch(
|
||||
7,
|
||||
8,
|
||||
&old,
|
||||
&new,
|
||||
|left, right| left.text == right.text,
|
||||
|left, right| left.style == right.style,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
batch.coordinate_edits,
|
||||
vec![SpanEdit {
|
||||
old_start: 1,
|
||||
old_end: 2,
|
||||
new_start: 1,
|
||||
new_end: 2,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
batch.semantic_edits,
|
||||
vec![SpanEdit {
|
||||
old_start: 1,
|
||||
old_end: 3,
|
||||
new_start: 1,
|
||||
new_end: 3,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_batch_rejects_revision_gaps() {
|
||||
let result = diff_commit_batch::<u8, _, _>(1, 3, &[], &[], |a, b| a == b, |a, b| a == b);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(CommitError::NonConsecutiveRevision { base: 1, target: 3 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pathological_diff_degrades_to_one_bounded_edit() {
|
||||
let old = vec![b'a'; 30_000];
|
||||
let new = vec![b'b'; 30_000];
|
||||
let batch = diff_commit_batch(0, 1, &old, &new, |a, b| a == b, |a, b| a == b)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
batch.semantic_edits,
|
||||
vec![SpanEdit {
|
||||
old_start: 0,
|
||||
old_end: 30_000,
|
||||
new_start: 0,
|
||||
new_end: 30_000,
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -262,6 +262,11 @@ fn flex_size_line(items: &[NativeFlexItem], main_limit: i64, main_gap: i64) -> V
|
||||
///
|
||||
/// `line_offsets` contains `line_count + 1` offsets into ITEMS. The output
|
||||
/// is a little-endian i64 stream whose values follow the input line order.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Nonempty ITEMS and LINE_OFFSETS must reference their declared readable
|
||||
/// lengths, and OUTPUT must point to writable `NativeBytes` storage.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ebox_native_flex_size_lines(
|
||||
items: *const NativeFlexItem,
|
||||
@ -277,10 +282,8 @@ pub unsafe extern "C" fn ebox_native_flex_size_lines(
|
||||
|| (line_count > 0 && line_offsets.is_null())
|
||||
|| item_count > MAX_FLEX_ITEMS
|
||||
|| line_count > MAX_FLEX_LINES
|
||||
|| main_limit < -MAX_LAYOUT_DIMENSION
|
||||
|| main_limit > MAX_LAYOUT_DIMENSION
|
||||
|| main_gap < 0
|
||||
|| main_gap > MAX_LAYOUT_DIMENSION
|
||||
|| !(-MAX_LAYOUT_DIMENSION..=MAX_LAYOUT_DIMENSION).contains(&main_limit)
|
||||
|| !(0..=MAX_LAYOUT_DIMENSION).contains(&main_gap)
|
||||
{
|
||||
set_error(
|
||||
output,
|
||||
@ -396,12 +399,14 @@ struct BaselineIdentity {
|
||||
struct ConfirmedBaseline {
|
||||
identity: BaselineIdentity,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PendingBaseline {
|
||||
confirmed_identity: BaselineIdentity,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -739,6 +744,69 @@ impl Session {
|
||||
result.map(|entry| entry.bytes)
|
||||
}
|
||||
|
||||
fn render_sync(&self, generation: u64, payload: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if !self.shared.alive.load(Ordering::Acquire) {
|
||||
return Err("Native reflow session is closed".to_owned());
|
||||
}
|
||||
let batch = parse_control_batch(payload)?;
|
||||
let document = batch
|
||||
.document
|
||||
.ok_or_else(|| "Native retained render requires an inline document".to_owned())?;
|
||||
document.validate()?;
|
||||
if batch.frames.len() != 1 {
|
||||
return Err("Native retained render requires exactly one frame".to_owned());
|
||||
}
|
||||
let frame = batch
|
||||
.frames
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("checked retained frame");
|
||||
if !frame.complete || frame.delay_ms != 0 {
|
||||
return Err("Native retained render requires one immediate complete frame".to_owned());
|
||||
}
|
||||
let document = Arc::new(document);
|
||||
let prepared = prepare_layout_job(&document, frame)?;
|
||||
let confirmed = self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.confirmed_baseline
|
||||
.clone();
|
||||
let output = render_layout_payload(
|
||||
prepared.payload,
|
||||
self.shared.id,
|
||||
generation,
|
||||
prepared.key,
|
||||
self.shared.max_result_bytes,
|
||||
confirmed,
|
||||
true,
|
||||
);
|
||||
if output.bytes.len() > self.shared.max_result_bytes {
|
||||
return Err("Native retained render exceeds the result byte limit".to_owned());
|
||||
}
|
||||
let mut state = self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
if output.base_renders > 0 {
|
||||
state.baseline_misses += 1;
|
||||
} else if output.baseline_hit {
|
||||
state.baseline_hits += 1;
|
||||
}
|
||||
state.base_renders += output.base_renders;
|
||||
state.target_renders += output.target_renders;
|
||||
state.pending_baselines.clear();
|
||||
if let Some(pending) = output.pending {
|
||||
state
|
||||
.pending_baselines
|
||||
.insert((generation, prepared.key), pending);
|
||||
}
|
||||
state.completed_jobs += 1;
|
||||
Ok(output.bytes)
|
||||
}
|
||||
|
||||
fn confirm(&self, generation: u64, key: i64, confirmed_revision: u64) -> Result<bool, String> {
|
||||
let mut state = self
|
||||
.shared
|
||||
@ -746,7 +814,10 @@ impl Session {
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
let Some(pending) = state.pending_baselines.remove(&(generation, key)) else {
|
||||
return Ok(false);
|
||||
return Ok(state
|
||||
.confirmed_baseline
|
||||
.as_ref()
|
||||
.is_some_and(|baseline| baseline.identity.runtime_revision == confirmed_revision));
|
||||
};
|
||||
if confirmed_revision <= pending.confirmed_identity.runtime_revision {
|
||||
return Err("Native confirmed frame revision mismatch".to_owned());
|
||||
@ -756,6 +827,7 @@ impl Session {
|
||||
state.confirmed_baseline = Some(ConfirmedBaseline {
|
||||
identity,
|
||||
tape: pending.tape,
|
||||
styles: pending.styles,
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
@ -1050,6 +1122,7 @@ fn render_layout_payload(
|
||||
key: i64,
|
||||
max_result_bytes: usize,
|
||||
confirmed_baseline: Option<ConfirmedBaseline>,
|
||||
require_confirmed_patch_base: bool,
|
||||
) -> RenderedJob {
|
||||
match payload {
|
||||
JobPayload::Echo(bytes) => RenderedJob {
|
||||
@ -1114,9 +1187,21 @@ fn render_layout_payload(
|
||||
runtime_revision,
|
||||
context_hash,
|
||||
complete,
|
||||
)
|
||||
) && baseline.styles == document.styles
|
||||
})
|
||||
.cloned();
|
||||
if require_confirmed_patch_base && base_hit.is_none() {
|
||||
let target = document
|
||||
.layout_tape(context, root_width_override.then_some(root_width))?;
|
||||
let bytes = layout::encode_layout_tape(
|
||||
target.clone(),
|
||||
&document.styles,
|
||||
identity,
|
||||
output.root_metadata,
|
||||
output.max_bytes,
|
||||
)?;
|
||||
return Ok((bytes, target, false, 0, 1));
|
||||
}
|
||||
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
|
||||
(baseline.tape, true, 0)
|
||||
} else {
|
||||
@ -1159,6 +1244,7 @@ fn render_layout_payload(
|
||||
pending: Some(PendingBaseline {
|
||||
confirmed_identity: pending_identity,
|
||||
tape,
|
||||
styles: document.styles.clone(),
|
||||
}),
|
||||
baseline_hit,
|
||||
base_renders,
|
||||
@ -1219,6 +1305,7 @@ fn render_proof(payload: &[u8]) -> Result<Vec<u8>, String> {
|
||||
prepared.key,
|
||||
SYNC_RENDER_MAX_RESULT_BYTES,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
if output.bytes.len() > SYNC_RENDER_MAX_RESULT_BYTES {
|
||||
return Err("Native proof render exceeds the result byte limit".to_owned());
|
||||
@ -1284,6 +1371,7 @@ fn worker_loop(shared: Arc<Shared>) {
|
||||
job.key,
|
||||
shared.max_result_bytes,
|
||||
confirmed_baseline,
|
||||
false,
|
||||
);
|
||||
if output.bytes.len() > shared.max_result_bytes {
|
||||
let mut state = shared
|
||||
@ -1451,6 +1539,50 @@ pub unsafe extern "C" fn ebox_native_session_submit(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// Render one retained frame synchronously through a live native session.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `session` must come from `ebox_native_session_create`. PAYLOAD and OUTPUT
|
||||
/// follow the same validity rules as `ebox_native_render_proof`.
|
||||
pub unsafe extern "C" fn ebox_native_session_render_sync(
|
||||
session: *mut c_void,
|
||||
generation: u64,
|
||||
payload: *const u8,
|
||||
payload_len: usize,
|
||||
output: *mut NativeBytes,
|
||||
) -> bool {
|
||||
if output.is_null() {
|
||||
return false;
|
||||
}
|
||||
let outcome = catch_unwind(AssertUnwindSafe(|| {
|
||||
let session =
|
||||
session_ref(session).ok_or_else(|| "Native reflow session is null".to_owned())?;
|
||||
if payload.is_null() && payload_len != 0 {
|
||||
return Err("Native retained render payload pointer is null".to_owned());
|
||||
}
|
||||
let bytes = if payload_len == 0 {
|
||||
&[][..]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(payload, payload_len) }
|
||||
};
|
||||
session.render_sync(generation, bytes)
|
||||
}));
|
||||
let result = match outcome {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err("Native retained render panicked".to_owned()),
|
||||
};
|
||||
let success = result.is_ok();
|
||||
unsafe {
|
||||
*output = match result {
|
||||
Ok(bytes) => NativeBytes::from_vec(bytes),
|
||||
Err(message) => NativeBytes::from_error(message),
|
||||
};
|
||||
}
|
||||
success
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// Render one complete layout proof synchronously from a versioned payload.
|
||||
///
|
||||
@ -1684,7 +1816,7 @@ mod tests {
|
||||
fn full_tape_line_widths(bytes: &[u8]) -> Vec<u64> {
|
||||
let line_count = u32::from_le_bytes(bytes[120..124].try_into().unwrap()) as usize;
|
||||
(0..line_count)
|
||||
.map(|index| read_le_u64(bytes, 136 + index * 8))
|
||||
.map(|index| read_le_u64(bytes, 152 + index * 8))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,58 @@
|
||||
(mapcar #'ebox-child-range-test--item range-items))))
|
||||
hash))
|
||||
|
||||
(ert-deftest ebox-child-range-reuse-map-rebinds-published-objects ()
|
||||
"Stable slots use published objects; moved slots re-enter normal indexing."
|
||||
(let* ((sequence (ebox-child-range-test--fixture 0 '(one two)))
|
||||
(segment (ebox-child-range--lookup-ref sequence 'range))
|
||||
(payload (ebox-child-range--segment-payload segment))
|
||||
(first (aref payload 0))
|
||||
(second (aref payload 1)))
|
||||
(plist-put first :node-id 1)
|
||||
(plist-put second :node-id 2)
|
||||
(plist-put first :ebox-sequence-location
|
||||
'(:parent-node-id 99 :segment-index 0 :offset 0))
|
||||
(plist-put second :ebox-sequence-location
|
||||
'(:parent-node-id 99 :segment-index 0 :offset 1))
|
||||
(pcase-let
|
||||
((`(,items ,reuse-map)
|
||||
(ebox-incremental--candidate-canonical-range-reuse
|
||||
sequence 'range (list (copy-tree first) (copy-tree second))
|
||||
'((0 . 0) (1 . 1)) 99 0)))
|
||||
(should (eq (nth 0 items) first))
|
||||
(should (eq (nth 1 items) second))
|
||||
(should (equal reuse-map '((0 . 0) (1 . 1)))))
|
||||
(pcase-let
|
||||
((`(,items ,reuse-map)
|
||||
(ebox-incremental--candidate-canonical-range-reuse
|
||||
sequence 'range (list second first)
|
||||
'((0 . 1) (1 . 0)) 99 0)))
|
||||
(should-not (eq (nth 0 items) second))
|
||||
(should-not (eq (nth 1 items) first))
|
||||
(should-not (plist-get (nth 0 items) :node-id))
|
||||
(should-not (plist-get (nth 1 items) :node-id))
|
||||
(should-not reuse-map))
|
||||
(should-error
|
||||
(ebox-incremental--candidate-canonical-range-reuse
|
||||
sequence 'range (list (copy-tree first)) '((1 . 0)) 99 0))))
|
||||
|
||||
(ert-deftest ebox-child-range-render-signature-snapshots-item-identities ()
|
||||
"A cached Range signature must not follow later runtime identity mutation."
|
||||
(let* ((item (ebox-create :content "item" :key 'item))
|
||||
(sequence (ebox-child-range--build (list (cons 'range (list item)))))
|
||||
(published
|
||||
(aref (ebox-child-range--segment-payload
|
||||
(ebox-child-range--lookup-ref sequence 'range))
|
||||
0))
|
||||
(signature (ebox--render-cache-value-signature sequence)))
|
||||
(plist-put published :node-id 1)
|
||||
(plist-put published :region-id 10)
|
||||
(setq signature (ebox--render-cache-value-signature sequence))
|
||||
(plist-put published :region-id 20)
|
||||
(should-not
|
||||
(equal signature (ebox--render-cache-value-signature sequence)))
|
||||
(should (equal 10 (plist-get (cadr signature) :region-id)))))
|
||||
|
||||
(ert-deftest ebox-child-range-gate-a-exact-segment-and-payload-costs ()
|
||||
"Point replacement cost follows trie height, never parent child count."
|
||||
(dolist (case '((0 0 1) (10 1 2) (100 2 3) (500 2 3)))
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
(require 'cl-lib)
|
||||
(require 'ert)
|
||||
(require 'ebox)
|
||||
(require 'ebox-native-commit)
|
||||
|
||||
(defun ebox-commit-test--buffer-string (buffer)
|
||||
"Return BUFFER's complete propertized contents."
|
||||
@ -11,6 +12,90 @@
|
||||
(widen)
|
||||
(buffer-substring (point-min) (point-max)))))
|
||||
|
||||
(ert-deftest ebox-native-fragment-style-delta-copies-only-changed-records ()
|
||||
"A native style delta keeps the retained template immutable."
|
||||
(let* ((first [0 1 0 nil nil nil nil (1)])
|
||||
(second [1 2 0 nil nil nil nil (2)])
|
||||
(template (vector first second))
|
||||
(target
|
||||
(ebox-native-commit--apply-fragment-style-delta
|
||||
template '((1 3 5)))))
|
||||
(should (eq (aref target 0) first))
|
||||
(should-not (eq (aref target 1) second))
|
||||
(should (equal (aref (aref target 1) 7) '(3 5)))
|
||||
(should (equal (aref second 7) '(2)))))
|
||||
|
||||
(ert-deftest ebox-native-object-delta-orders-moved-and-new-nodes ()
|
||||
"A topology delta names parents before moved and introduced children."
|
||||
(let* ((old-root
|
||||
(ebox-column (ebox-create :key 'a :content "A")
|
||||
(ebox-create :key 'b :content "B")))
|
||||
(_old-ids (ebox--runtime-node-ids old-root))
|
||||
(old-index (ebox--runtime-index old-root t))
|
||||
(old-objects (make-hash-table :test 'equal))
|
||||
(new-root
|
||||
(ebox-column (ebox-create :key 'b :content "B")
|
||||
(ebox-create :key 'a :content "A")
|
||||
(ebox-create :key 'c :content "C")))
|
||||
(_reconciled (ebox-tree-reconcile-runtime old-root new-root))
|
||||
(new-index (ebox--runtime-index new-root t))
|
||||
(root-id (plist-get old-root :node-id)))
|
||||
(maphash (lambda (node-id _node)
|
||||
(puthash node-id (list 'object node-id) old-objects))
|
||||
(plist-get old-index :node-table))
|
||||
(let* ((old-state (append (list :root-node old-root
|
||||
:surface-node-object-table old-objects)
|
||||
old-index))
|
||||
(new-state (append (list :root-node new-root) new-index))
|
||||
(delta
|
||||
(ebox-native-commit-object-delta-node-ids
|
||||
old-state new-state
|
||||
(list :touched-node-ids (list root-id)
|
||||
:removed-node-ids nil)))
|
||||
(children (ebox-tree--children-raw new-root)))
|
||||
(should
|
||||
(equal delta
|
||||
(cons root-id (mapcar (lambda (node)
|
||||
(plist-get node :node-id))
|
||||
children)))))))
|
||||
|
||||
(ert-deftest ebox-native-object-delta-requires-complete-removal-proof ()
|
||||
"An unreported disappeared object rejects the native topology delta."
|
||||
(let* ((old-root
|
||||
(ebox-column (ebox-create :key 'a :content "A")
|
||||
(ebox-create :key 'tail :content "T")
|
||||
(ebox-create :key 'b :content "B")))
|
||||
(_old-ids (ebox--runtime-node-ids old-root))
|
||||
(old-index (ebox--runtime-index old-root t))
|
||||
(old-objects (make-hash-table :test 'equal))
|
||||
(removed-node (car (last (ebox-tree--children-raw old-root))))
|
||||
(new-root
|
||||
(ebox-column (ebox-create :key 'a :content "A")
|
||||
(ebox-create :key 'tail :content "T")))
|
||||
(_reconciled (ebox-tree-reconcile-runtime old-root new-root))
|
||||
(new-index (ebox--runtime-index new-root t))
|
||||
(root-id (plist-get old-root :node-id)))
|
||||
(maphash (lambda (node-id _node)
|
||||
(puthash node-id (list 'object node-id) old-objects))
|
||||
(plist-get old-index :node-table))
|
||||
(let ((old-state (append (list :root-node old-root
|
||||
:surface-node-object-table old-objects)
|
||||
old-index))
|
||||
(new-state (append (list :root-node new-root) new-index)))
|
||||
(should-not
|
||||
(ebox-native-commit-object-delta-node-ids
|
||||
old-state new-state
|
||||
(list :touched-node-ids (list root-id)
|
||||
:removed-node-ids nil)))
|
||||
(should
|
||||
(equal
|
||||
(list root-id)
|
||||
(ebox-native-commit-object-delta-node-ids
|
||||
old-state new-state
|
||||
(list :touched-node-ids (list root-id)
|
||||
:removed-node-ids
|
||||
(list (plist-get removed-node :node-id)))))))))
|
||||
|
||||
(ert-deftest ebox-style-schema-registration-is-not-per-node-copy ()
|
||||
"Repeated node construction must not copy the whole ECSS schema domain."
|
||||
(let ((calls 0)
|
||||
@ -330,6 +415,104 @@
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-reuses-local-selector-styles-across-tree-change ()
|
||||
"A subject-local stylesheet should compute only newly inserted node styles."
|
||||
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
(calls 0)
|
||||
(original (symbol-function 'ecss-compute-style))
|
||||
(buffer nil))
|
||||
(ebox-style-add-rule ".card" '(:color "#2255AA") :layer 'components)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-local-selector-reuse*")
|
||||
(ebox-column
|
||||
(ebox-create :key 'first :class "card" :content "First"
|
||||
:width '(40))
|
||||
(ebox-create :key 'second :class "card" :content "Second"
|
||||
:width '(40)))))
|
||||
(cl-letf (((symbol-function 'ecss-compute-style)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf calls)
|
||||
(apply original arguments))))
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'first :class "card" :content "First"
|
||||
:width '(40))
|
||||
(ebox-create :key 'second :class "card" :content "Second"
|
||||
:width '(40))
|
||||
(ebox-create :key 'third :class "card" :content "Third"
|
||||
:width '(40)))))
|
||||
(should (= calls 1))
|
||||
(let* ((text (ebox-commit-test--buffer-string buffer))
|
||||
(position (string-match "Third" text)))
|
||||
(should position)
|
||||
(should (equal (plist-get (get-text-property position 'face text)
|
||||
:foreground)
|
||||
"#2255AA"))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-reuses-successful-allocation-certificate ()
|
||||
"A repeated fixed-slot update must not rescan unchanged ancestor spans."
|
||||
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
(buffer nil)
|
||||
(ancestor-scans 0))
|
||||
(ebox-style-add-rule ".card" '(:color "#111111") :layer 'components)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-certificate*")
|
||||
(ebox-build
|
||||
'(grid :width (100) :height 2
|
||||
:grid-template-columns (1fr 1fr)
|
||||
(column :key card-shell :width stretch :height 2
|
||||
(box :key card :class "card" :host-ref target
|
||||
:content "Before" :width (40)))
|
||||
(box :key stable :content "Stable"
|
||||
:width (40) :height 1)))))
|
||||
(let ((first (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
first 'target
|
||||
(ebox-create :key 'card :class "card" :host-ref 'target
|
||||
:content "Middle" :width '(40)))
|
||||
(should (eq (plist-get (ebox-commit buffer first)
|
||||
:projection-kind)
|
||||
'owner-scoped)))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(certificates
|
||||
(plist-get state :retained-allocation-certificates)))
|
||||
(should (hash-table-p certificates))
|
||||
(should (= 1 (hash-table-count certificates))))
|
||||
(let ((second (ebox-candidate-begin buffer))
|
||||
(original
|
||||
(symbol-function
|
||||
'ebox-incremental--compute-ancestor-slot-signature)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
second 'target
|
||||
(ebox-create :key 'card :class "card" :host-ref 'target
|
||||
:content "After" :width '(40)))
|
||||
(cl-letf
|
||||
(((symbol-function
|
||||
'ebox-incremental--compute-ancestor-slot-signature)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf ancestor-scans)
|
||||
(apply original arguments))))
|
||||
(should (eq (plist-get (ebox-commit buffer second)
|
||||
:projection-kind)
|
||||
'owner-scoped))))
|
||||
(should (= ancestor-scans 0))
|
||||
(should
|
||||
(string-match-p
|
||||
"After"
|
||||
(substring-no-properties
|
||||
(ebox-commit-test--buffer-string buffer)))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-invalidates-selector-tree-token-for-sibling-change ()
|
||||
"A sibling metadata change must invalidate retained selector computations."
|
||||
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
@ -1254,6 +1437,74 @@ remain retained identities."
|
||||
(ebox-create :key 'untouched :host-ref 'untouched
|
||||
:content "untouched")))
|
||||
|
||||
(ert-deftest ebox-candidate-host-paint-patch-preserves-subtree-identity ()
|
||||
"Patch one Host's paint without copying or reconciling its descendants."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-host-paint-patch* ")
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#111111"
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child :host-ref 'child :content "child")))))
|
||||
(child-id (plist-get (ebox--host-ref-node buffer 'child) :node-id)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-patch-host-paint
|
||||
candidate 'target
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#111111"
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child :host-ref 'child :content "child")))
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child :host-ref 'child :content "child"))))
|
||||
(let ((report (ebox-commit buffer candidate)))
|
||||
(should (eq (plist-get report :projection-kind) 'paint))
|
||||
(should (= child-id
|
||||
(plist-get (ebox--host-ref-node buffer 'child)
|
||||
:node-id)))
|
||||
(should (equal "#EEEEEE"
|
||||
(plist-get (ebox--host-ref-node buffer 'target)
|
||||
:bgcolor)))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(should-not
|
||||
(ebox-candidate-patch-host-paint
|
||||
candidate 'target
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
||||
:content "child")
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
||||
:width '(40) :content "child"))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(should
|
||||
(ebox-candidate-patch-host-paint
|
||||
candidate 'target
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child :host-ref 'child :content "child")))
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target :bgcolor "#EEEEEE"
|
||||
:color "#FFFFFF"
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child :host-ref 'child :content "child")))))
|
||||
(ebox-commit buffer candidate)
|
||||
(should
|
||||
(equal "#FFFFFF"
|
||||
(plist-get (ebox--host-ref-node buffer 'target) :color)))
|
||||
(should
|
||||
(equal "#FFFFFF"
|
||||
(plist-get (ebox--host-ref-node buffer 'child) :color)))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(defun ebox-commit-test--fixed-basis-selection-root (row-1 row-2)
|
||||
"Return a stretched fixed-basis panel containing two selectable rows."
|
||||
(let ((panel
|
||||
@ -1287,7 +1538,10 @@ remain retained identities."
|
||||
|
||||
(ert-deftest ebox-commit-fixed-basis-selection-round-trip-stays-local ()
|
||||
"Continuous row-1 -> row-2 -> row-1 publication keeps local TP scope."
|
||||
(let ((buffer
|
||||
(let ((slot-proof-count 0)
|
||||
(original-slot-proof
|
||||
(symbol-function 'ebox--flex-item-slot-footprint-safe-p))
|
||||
(buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-fixed-basis-round-trip* ")
|
||||
(ebox-commit-test--fixed-basis-selection-root
|
||||
@ -1300,13 +1554,17 @@ remain retained identities."
|
||||
(setq panel-id
|
||||
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
|
||||
buffer panel-id))
|
||||
(dolist (contents '(("[ ] row 1" "[x] row 2")
|
||||
("[x] row 1" "[ ] row 2")))
|
||||
(let* ((report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--fixed-basis-selection-candidate
|
||||
buffer (car contents) (cadr contents))))
|
||||
(cl-letf (((symbol-function 'ebox--flex-item-slot-footprint-safe-p)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf slot-proof-count)
|
||||
(apply original-slot-proof arguments))))
|
||||
(dolist (contents '(("[ ] row 1" "[x] row 2")
|
||||
("[x] row 1" "[ ] row 2")))
|
||||
(let* ((report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--fixed-basis-selection-candidate
|
||||
buffer (car contents) (cadr contents))))
|
||||
(surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(tp-report (tp-surface-report surface))
|
||||
@ -1330,13 +1588,14 @@ remain retained identities."
|
||||
;; to recapture geometry from the committed TP mounts.
|
||||
(should panel-snapshot)
|
||||
(should-not (plist-member panel-snapshot :buffer-spans))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal
|
||||
(buffer-substring-no-properties (point-min) (point-max))
|
||||
(substring-no-properties
|
||||
(ebox-render (ebox--buffer-root-node buffer))))))))
|
||||
(should (= (length reports) 2)))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal
|
||||
(buffer-substring-no-properties (point-min) (point-max))
|
||||
(substring-no-properties
|
||||
(ebox-render (ebox--buffer-root-node buffer)))))))))
|
||||
(should (= (length reports) 2))
|
||||
(should (= slot-proof-count 2)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(defun ebox-commit-test--mixed-owner-candidate
|
||||
@ -1474,7 +1733,7 @@ remain retained identities."
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-mixed-owner-reflow-reuses-unchanged-fragments ()
|
||||
"Skip face recomposition for unchanged fragments in a mixed projection."
|
||||
"Delegate affected face composition to TP and retain fragment topology."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-mixed-owner-reuse* ")
|
||||
@ -1484,13 +1743,28 @@ remain retained identities."
|
||||
(old-state (tp-surface-client-state surface))
|
||||
(fragment-count (length (plist-get old-state :surface-fragments)))
|
||||
(repaints 0)
|
||||
(original (symbol-function 'ebox-surface--repaint-fragment)))
|
||||
(metadata-calls 0)
|
||||
(original
|
||||
(symbol-function 'ebox-surface--repaint-output-fragment!))
|
||||
(original-metadata
|
||||
(symbol-function 'ebox-surface--fragment-metadata)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(cl-letf (((symbol-function 'ebox-surface--repaint-fragment)
|
||||
(should
|
||||
(cl-every
|
||||
(lambda (fragment)
|
||||
(or (null (plist-get fragment :paint-role-ids))
|
||||
(plist-get fragment :paint-node-chain)))
|
||||
(plist-get old-state :surface-fragments)))
|
||||
(should (plist-get old-state :surface-owned-ranges))
|
||||
(cl-letf (((symbol-function 'ebox-surface--repaint-output-fragment!)
|
||||
(lambda (&rest args)
|
||||
(cl-incf repaints)
|
||||
(apply original args))))
|
||||
(apply original args)))
|
||||
((symbol-function 'ebox-surface--fragment-metadata)
|
||||
(lambda (&rest args)
|
||||
(cl-incf metadata-calls)
|
||||
(apply original-metadata args))))
|
||||
(should (eq (plist-get
|
||||
(ebox-commit
|
||||
buffer
|
||||
@ -1500,7 +1774,455 @@ remain retained identities."
|
||||
:projection-kind)
|
||||
'mixed-owner-reflow)))
|
||||
(should (> fragment-count 0))
|
||||
(should (< repaints fragment-count)))
|
||||
(should (zerop repaints))
|
||||
(should
|
||||
(plist-get (tp-surface-client-state surface)
|
||||
:paint-property-contributions))
|
||||
(let ((fragments
|
||||
(plist-get (tp-surface-client-state surface)
|
||||
:surface-fragments))
|
||||
(offset 0))
|
||||
(should fragments)
|
||||
(dolist (fragment fragments)
|
||||
(should (= offset (plist-get fragment :start)))
|
||||
(setq offset (plist-get fragment :end)))
|
||||
(should (= offset
|
||||
(with-current-buffer buffer (buffer-size)))))
|
||||
(should (= metadata-calls 0)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-surface-owned-range-index-rebases-exact-boundaries ()
|
||||
"Rebase retained ownership exactly and reject ambiguous inner boundaries."
|
||||
(let* ((patches '((:old-start 5 :old-end 10
|
||||
:new-start 5 :new-end 12)))
|
||||
(ranges '((:object before :start 0 :end 5 :tags (:before t))
|
||||
(:object changed :start 5 :end 10 :tags (:changed t))
|
||||
(:object parent :start 0 :end 20 :tags (:parent t))
|
||||
(:object after :start 10 :end 20 :tags (:after t))))
|
||||
(rebased (cdr (ebox-surface--rebase-owned-ranges
|
||||
ranges patches 22))))
|
||||
(should
|
||||
(equal (mapcar (lambda (range)
|
||||
(list (plist-get range :object)
|
||||
(plist-get range :start)
|
||||
(plist-get range :end)))
|
||||
rebased)
|
||||
'((before 0 5) (changed 5 12) (parent 0 22) (after 12 22))))
|
||||
(should-not
|
||||
(ebox-surface--rebase-owned-ranges
|
||||
'((:object ambiguous :start 6 :end 9)) patches 22))))
|
||||
|
||||
(ert-deftest ebox-commit-structure-skips-inapplicable-paint-span-proofs ()
|
||||
"A structural transaction must not run proofs whose domain excludes it."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-structure-proof-domain* ")
|
||||
(ebox-column
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target
|
||||
:ebox-content-node
|
||||
(ebox-column (ebox-create :key 'first :content "first"))))))
|
||||
(span-calls 0)
|
||||
(mixed-calls 0)
|
||||
(old-span
|
||||
(symbol-function 'ebox-incremental--span-patch-projection-proof))
|
||||
(old-mixed
|
||||
(symbol-function 'ebox-incremental--mixed-owner-proof)))
|
||||
(unwind-protect
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'target
|
||||
(ebox-create
|
||||
:key 'target :host-ref 'target
|
||||
:ebox-content-node
|
||||
(ebox-column (ebox-create :key 'first :content "first")
|
||||
(ebox-create :key 'second :content "second"))))
|
||||
(cl-letf
|
||||
(((symbol-function 'ebox-incremental--span-patch-projection-proof)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf span-calls)
|
||||
(apply old-span arguments)))
|
||||
((symbol-function 'ebox-incremental--mixed-owner-proof)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf mixed-calls)
|
||||
(apply old-mixed arguments))))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (zerop span-calls))
|
||||
(should (zerop mixed-calls))
|
||||
(should (string-match-p "second"
|
||||
(ebox-commit-test--buffer-string buffer))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-candidate-range-structure-stops-at-range-parent ()
|
||||
"A Range child identity change must not mark copied ancestors structural."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-range-dirty-boundary* ")
|
||||
(ebox-column
|
||||
(ebox-child-range
|
||||
'rows (ebox-create :key 'old :content "old")))))
|
||||
(state (ebox--buffer-render-state buffer))
|
||||
(parent-id
|
||||
(plist-get (gethash 'rows (plist-get state :range-ref-table))
|
||||
:parent-node-id))
|
||||
captured)
|
||||
(unwind-protect
|
||||
(let ((candidate (ebox-candidate-begin buffer))
|
||||
(original
|
||||
(symbol-function 'ebox-incremental--surface-commit-input)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'rows (list (ebox-create :key 'new :content "new")))
|
||||
(cl-letf
|
||||
(((symbol-function 'ebox-incremental--surface-commit-input)
|
||||
(lambda (target old-state prepared)
|
||||
(setq captured (copy-tree (plist-get prepared :dirty-set)))
|
||||
(funcall original target old-state prepared))))
|
||||
(ebox-commit buffer candidate))
|
||||
(should
|
||||
(equal
|
||||
(mapcar (lambda (entry)
|
||||
(list (plist-get entry :node-id)
|
||||
(plist-get entry :dirty-kind)
|
||||
(plist-get entry :changed-keys)))
|
||||
captured)
|
||||
(list (list parent-id 'structure '(:children)))))
|
||||
(should (string-match-p "new"
|
||||
(ebox-commit-test--buffer-string buffer))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-mixed-owner-reflow-skips-full-owner-planner ()
|
||||
"A proven mixed union should not also plan the complete dirty set."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-mixed-owner-plan* ")
|
||||
(ebox-commit-test--mixed-owner-root
|
||||
"left-old" "right-old" "#111111" "#222222" "#333333")))
|
||||
(calls 0)
|
||||
(axis-scans 0)
|
||||
(original
|
||||
(symbol-function 'ebox-incremental--layout-owner-plan))
|
||||
(original-context-axes
|
||||
(symbol-function 'ebox-surface--context-axes)))
|
||||
(unwind-protect
|
||||
(cl-letf
|
||||
(((symbol-function 'ebox-incremental--layout-owner-plan)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf calls)
|
||||
(apply original arguments)))
|
||||
((symbol-function 'ebox-surface--context-axes)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf axis-scans)
|
||||
(apply original-context-axes arguments))))
|
||||
(let* ((report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--mixed-owner-candidate
|
||||
buffer "left-new" "right-new"
|
||||
"#AAAAAA" "#BBBBBB" "#CCCCCC")))
|
||||
(owner-ids (plist-get report :owner-ids)))
|
||||
(should (eq (plist-get report :projection-kind)
|
||||
'mixed-owner-reflow))
|
||||
(should (= calls 0))
|
||||
(should (= axis-scans 0))
|
||||
(should (= (plist-get report :tp-scope-count)
|
||||
(length owner-ids)))
|
||||
(should-not (plist-get report :tp-scope-fallback))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal-including-properties
|
||||
(buffer-substring (point-min) (point-max))
|
||||
(ebox-render (ebox--buffer-root-node buffer)))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(defun ebox-commit-test--allocation-closure-root
|
||||
(toast paint-a paint-b &optional width footer-overflow root-overflow)
|
||||
"Return a generic whole-line Flex allocation-closure fixture."
|
||||
(ebox-create
|
||||
:key 'root :width (list (or width 800))
|
||||
:height (and root-overflow 1) :overflow root-overflow
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'status :host-ref 'status
|
||||
:content "Theme: Light" :width '(200))
|
||||
(ebox-create :key 'paint-a :host-ref 'paint-a
|
||||
:content "paint-a" :color paint-a)
|
||||
(ebox-create :key 'paint-b :host-ref 'paint-b
|
||||
:content "paint-b" :bgcolor paint-b)
|
||||
(ebox-create
|
||||
:key 'footer-owner :width (list (or width 800))
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-flex
|
||||
:key 'footer :width (list (or width 800)) :flex-wrap 'wrap
|
||||
:overflow footer-overflow :gap '(1 (4))
|
||||
(ebox-create
|
||||
:key 'toast-slot :width 'stretch :min-width 0
|
||||
:flex-grow 1 :flex-shrink 1 :flex-basis '(0)
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'toast :host-ref 'toast :content toast)))
|
||||
(ebox-create :key 'peer :content "database.sqlite")))))))
|
||||
|
||||
(defun ebox-commit-test--allocation-closure-candidate
|
||||
(buffer toast paint-a paint-b)
|
||||
"Return BUFFER candidate changing one Flex content and two paints."
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'toast
|
||||
(ebox-create :key 'toast :host-ref 'toast :content toast))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'paint-a
|
||||
(ebox-create :key 'paint-a :host-ref 'paint-a
|
||||
:content "paint-a" :color paint-a))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'paint-b
|
||||
(ebox-create :key 'paint-b :host-ref 'paint-b
|
||||
:content "paint-b" :bgcolor paint-b))
|
||||
candidate))
|
||||
|
||||
(defun ebox-commit-test--two-geometry-allocation-candidate
|
||||
(buffer status toast paint-a paint-b)
|
||||
"Return BUFFER candidate with one span and one allocation geometry owner."
|
||||
(let ((candidate
|
||||
(ebox-commit-test--allocation-closure-candidate
|
||||
buffer toast paint-a paint-b)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'status
|
||||
(ebox-create :key 'status :host-ref 'status
|
||||
:content status :width '(200)))
|
||||
candidate))
|
||||
|
||||
(ert-deftest ebox-commit-allocation-closure-round-trips-exactly ()
|
||||
"Stable Flex allocation closes one content damage plus external paint."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-closure* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222")))
|
||||
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(full-renders 0)
|
||||
(original-full-render
|
||||
(symbol-function 'ebox-surface--render-candidate)))
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf full-renders)
|
||||
(apply original-full-render arguments))))
|
||||
(dolist (values '(("A longer dark notification"
|
||||
"#AAAAAA" "#BBBBBB")
|
||||
("Light" "#111111" "#222222")))
|
||||
(let* ((before (tp-surface-revision surface))
|
||||
(candidate
|
||||
(apply #'ebox-commit-test--allocation-closure-candidate
|
||||
buffer values))
|
||||
(report (ebox-commit buffer candidate))
|
||||
(state (tp-surface-client-state surface))
|
||||
(owner-proof
|
||||
(car (plist-get
|
||||
(plist-get (plist-get state :mixed-owner-proof)
|
||||
:geometry-proof)
|
||||
:owner-proofs))))
|
||||
(should (eq (plist-get report :projection-kind)
|
||||
'mixed-owner-reflow))
|
||||
(should (plist-get owner-proof :allocation-closure-p))
|
||||
(should (= (tp-surface-revision surface) (1+ before)))
|
||||
(should-not (plist-get report :tp-full-root))
|
||||
(should-not (plist-get report :tp-scope-fallback))
|
||||
(should (= (plist-get report :created-objects) 0))
|
||||
(should (= (plist-get report :removed-objects) 0))
|
||||
(should (= (or (plist-get report :moved-objects) 0) 0))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal-including-properties
|
||||
(buffer-substring (point-min) (point-max))
|
||||
(ebox-render (ebox--buffer-root-node buffer)))))))
|
||||
(should (= full-renders 0)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-allocation-closure-rolls-back-and-retries ()
|
||||
"Failed allocation-closure publication restores state before retry."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-rollback* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222")))
|
||||
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(state (tp-surface-client-state surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
(text (ebox-commit-test--buffer-string buffer)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(cl-letf (((symbol-function 'accept-change-group)
|
||||
(lambda (_group) (error "allocation final accept"))))
|
||||
(should-error
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--allocation-closure-candidate
|
||||
buffer "A longer dark notification" "#AAAAAA" "#BBBBBB"))))
|
||||
(should (= (tp-surface-revision surface) revision))
|
||||
(should (eq (tp-surface-client-state surface) state))
|
||||
(should (equal-including-properties
|
||||
(ebox-commit-test--buffer-string buffer) text))
|
||||
(should (eq (plist-get
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--allocation-closure-candidate
|
||||
buffer "A longer dark notification"
|
||||
"#AAAAAA" "#BBBBBB"))
|
||||
:projection-kind)
|
||||
'mixed-owner-reflow)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-commit-two-geometry-allocation-closure-round-trips ()
|
||||
"Combine one ordinary span with one allocation closure after global miss."
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-two-geometry-closure* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222")))
|
||||
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(full-renders 0)
|
||||
(original-full-render
|
||||
(symbol-function 'ebox-surface--render-candidate)))
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf full-renders)
|
||||
(apply original-full-render arguments))))
|
||||
(dolist (values '(("Theme: Dark" "A longer dark notification"
|
||||
"#AAAAAA" "#BBBBBB")
|
||||
("Theme: Light" "Light"
|
||||
"#111111" "#222222")))
|
||||
(let* ((before (tp-surface-revision surface))
|
||||
(report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(apply
|
||||
#'ebox-commit-test--two-geometry-allocation-candidate
|
||||
buffer values)))
|
||||
(state (tp-surface-client-state surface))
|
||||
(proofs
|
||||
(plist-get
|
||||
(plist-get (plist-get state :mixed-owner-proof)
|
||||
:geometry-proof)
|
||||
:owner-proofs)))
|
||||
(should (eq (plist-get report :projection-kind)
|
||||
'mixed-owner-reflow))
|
||||
(should (= 2 (length proofs)))
|
||||
(should (cl-some (lambda (proof)
|
||||
(plist-get proof :allocation-closure-p))
|
||||
proofs))
|
||||
(should (= (tp-surface-revision surface) (1+ before)))
|
||||
(should-not (plist-get report :tp-full-root))
|
||||
(should-not (plist-get report :tp-scope-fallback))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal-including-properties
|
||||
(buffer-substring (point-min) (point-max))
|
||||
(ebox-render (ebox--buffer-root-node buffer)))))))
|
||||
(should (= full-renders 0)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-allocation-closure-allows-recomposable-ancestor-paint ()
|
||||
"Allow ancestor paint but reject paint at/below an allocation owner."
|
||||
(let ((parents (make-hash-table :test #'eql)))
|
||||
;; 1(root) -> 2(paint ancestor) -> 3(geometry) -> 4(paint descendant)
|
||||
(puthash 2 1 parents)
|
||||
(puthash 3 2 parents)
|
||||
(puthash 4 3 parents)
|
||||
(let ((state (list :parent-table parents)))
|
||||
(should
|
||||
(ebox-incremental--allocation-closure-paint-disjoint-p
|
||||
state '(3) '(2)))
|
||||
(should-not
|
||||
(ebox-incremental--allocation-closure-paint-disjoint-p
|
||||
state '(3) '(3)))
|
||||
(should-not
|
||||
(ebox-incremental--allocation-closure-paint-disjoint-p
|
||||
state '(3) '(4))))))
|
||||
|
||||
(ert-deftest ebox-commit-allocation-closure-unsafe-shapes-fallback ()
|
||||
"Wrap/width, overflow, and scroll changes cannot use allocation closure."
|
||||
(dolist (case '((120 nil nil) (800 visible nil) (800 nil scroll)))
|
||||
(pcase-let ((`(,width ,footer-overflow ,root-overflow) case))
|
||||
(let ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-fallback* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222"
|
||||
width footer-overflow root-overflow))))
|
||||
(unwind-protect
|
||||
(let ((report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--allocation-closure-candidate
|
||||
buffer (make-string 200 ?X) "#AAAAAA" "#BBBBBB"))))
|
||||
(should-not (eq (plist-get report :projection-kind)
|
||||
'mixed-owner-reflow)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))))
|
||||
|
||||
(ert-deftest ebox-commit-allocation-closure-proof-misses-fallback ()
|
||||
"Topology, selector, cascade, and role misses reject allocation closure."
|
||||
(dolist (kind '(topology selector cascade))
|
||||
(let ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-proof-miss* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222"))))
|
||||
(unwind-protect
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'toast
|
||||
(if (eq kind 'topology)
|
||||
(ebox-create
|
||||
:key 'toast :host-ref 'toast
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'nested-toast
|
||||
:content "A longer notification")))
|
||||
(ebox-create :key 'toast :host-ref 'toast
|
||||
:class (and (eq kind 'selector) "changed")
|
||||
:content "A longer notification")))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'paint-a
|
||||
(ebox-create :key 'paint-a :host-ref 'paint-a
|
||||
:content "paint-a" :color "#AAAAAA"))
|
||||
(let ((report
|
||||
(if (eq kind 'cascade)
|
||||
(cl-letf
|
||||
(((symbol-function 'ebox-style-cascade-active-p)
|
||||
(lambda () t))
|
||||
((symbol-function
|
||||
'ebox-surface--cascade-local-owner-proof-p)
|
||||
(lambda (&rest _) nil)))
|
||||
(ebox-commit buffer candidate))
|
||||
(ebox-commit buffer candidate))))
|
||||
(should-not (eq (plist-get report :projection-kind)
|
||||
'mixed-owner-reflow))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
(let* ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-allocation-role-miss* ")
|
||||
(ebox-commit-test--allocation-closure-root
|
||||
"Light" "#111111" "#222222")))
|
||||
(original-output
|
||||
(symbol-function 'ebox-surface--mixed-owner-output))
|
||||
mixed-output)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox--refresh-buffer-layout-snapshots buffer t)
|
||||
(cl-letf
|
||||
(((symbol-function
|
||||
'ebox-surface--rendered-role-topology-signature)
|
||||
(lambda (&rest _) '(:roles (mismatched))))
|
||||
((symbol-function 'ebox-surface--mixed-owner-output)
|
||||
(lambda (&rest arguments)
|
||||
(setq mixed-output (apply original-output arguments)))))
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--allocation-closure-candidate
|
||||
buffer "A longer notification" "#AAAAAA" "#BBBBBB"))
|
||||
(should-not mixed-output)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(provide 'ebox-commit-tests)
|
||||
|
||||
@ -46,7 +46,10 @@
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer))
|
||||
(setq ebox--deferred-render-gc-timer nil)
|
||||
(setq ebox--deferred-render-gc-state nil))
|
||||
(setq ebox--deferred-render-gc-state nil)
|
||||
(setq ebox--deferred-render-gc-depth 0)
|
||||
(setq ebox--render-burst-stack nil)
|
||||
(clrhash ebox--render-burst-records))
|
||||
|
||||
(defmacro ebox-test--with-rendered-buffer (layout &rest body)
|
||||
"Render LAYOUT into a temporary ebox buffer and run BODY there."
|
||||
@ -1884,6 +1887,17 @@
|
||||
(should props)
|
||||
(should (plist-member props 'face))))
|
||||
|
||||
(ert-deftest ebox-render-keeps-tp-paint-slot-address-stable ()
|
||||
"A TP paint slot should recolor retained output without property rewrites."
|
||||
(let* ((slot (tp-paint-slot-create '(:foreground "red")))
|
||||
(face (tp-paint-slot-face slot))
|
||||
(rendered (ebox-render (ebox-create :content "paint" :color slot)))
|
||||
(property (get-text-property 0 'face rendered)))
|
||||
(should (eq face property))
|
||||
(tp-paint-slot-update slot '(:foreground "blue"))
|
||||
(should (eq property (get-text-property 0 'face rendered)))
|
||||
(should (equal "blue" (face-attribute face :foreground nil nil)))))
|
||||
|
||||
(ert-deftest ebox-buffer-backend-does-not-map-layout-only-properties ()
|
||||
"Layout-only properties should not produce text properties directly."
|
||||
(let* ((style (ebox-style-compute
|
||||
@ -2974,6 +2988,217 @@
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer)))))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-batch-restores-nested-settings-lifo ()
|
||||
"Batch bursts should nest strictly and restore exact caller GC settings."
|
||||
(let ((noninteractive t)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-render-gc-cons-threshold 4000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--render-burst-stack nil)
|
||||
(gc-count 0))
|
||||
(cl-letf (((symbol-function 'garbage-collect)
|
||||
(lambda () (cl-incf gc-count))))
|
||||
(let ((outer (ebox-render-burst-begin)))
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(should (= gc-cons-percentage 0.8))
|
||||
(let ((inner (ebox-render-burst-begin)))
|
||||
(should-error (ebox-render-burst-end outer))
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(ebox-render-burst-end inner)
|
||||
(should (= gc-cons-threshold 4000)))
|
||||
(ebox-render-burst-end outer)
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))
|
||||
(should-error (ebox-render-burst-end outer)))
|
||||
(should (= gc-count 0)))))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-raises-gc-before-private-allocation ()
|
||||
"Begin should cross the GC boundary before creating any ownership record."
|
||||
(let ((noninteractive t)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-render-gc-cons-threshold 4000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--render-burst-stack nil)
|
||||
(ebox--render-burst-records (make-hash-table :test #'eq))
|
||||
(original-make-symbol (symbol-function 'make-symbol))
|
||||
(original-make-record
|
||||
(symbol-function 'ebox--render-burst-create-record))
|
||||
token-threshold
|
||||
record-threshold)
|
||||
(cl-letf (((symbol-function 'make-symbol)
|
||||
(lambda (name)
|
||||
(setq token-threshold gc-cons-threshold)
|
||||
(funcall original-make-symbol name)))
|
||||
((symbol-function 'ebox--render-burst-create-record)
|
||||
(lambda (&rest args)
|
||||
(setq record-threshold gc-cons-threshold)
|
||||
(apply original-make-record args))))
|
||||
(let ((token (ebox-render-burst-begin)))
|
||||
(should (= token-threshold 4000))
|
||||
(should (= record-threshold 4000))
|
||||
(ebox-render-burst-end token)))
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-restores-batch-gc-when-begin-fails ()
|
||||
"A batch begin allocation failure should restore its caller's GC settings."
|
||||
(let ((noninteractive t)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-render-gc-cons-threshold 4000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--render-burst-stack nil)
|
||||
(ebox--render-burst-records (make-hash-table :test #'eq)))
|
||||
(cl-letf (((symbol-function 'make-symbol)
|
||||
(lambda (_name)
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(error "token allocation failed"))))
|
||||
(should-error (ebox-render-burst-begin)))
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))
|
||||
(should-not ebox--render-burst-stack)
|
||||
(should (zerop (hash-table-count ebox--render-burst-records)))))
|
||||
|
||||
(ert-deftest ebox-interactive-render-burst-raises-gc-before-state-allocation ()
|
||||
"Interactive entry should save scalars and raise GC before state allocation."
|
||||
(let ((noninteractive nil)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-render-gc-cons-threshold 4000)
|
||||
(ebox-deferred-render-gc-cons-threshold 8000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--deferred-render-gc-state nil)
|
||||
(ebox--deferred-render-gc-timer nil)
|
||||
(ebox--deferred-render-gc-depth 0)
|
||||
(original-make-state
|
||||
(symbol-function 'ebox--make-deferred-render-gc-state))
|
||||
observed-threshold)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox--make-deferred-render-gc-state)
|
||||
(lambda (threshold percentage)
|
||||
(setq observed-threshold gc-cons-threshold)
|
||||
(funcall original-make-state threshold percentage))))
|
||||
(ebox--deferred-render-gc-enter)
|
||||
(should (= observed-threshold 8000))
|
||||
(should (= (plist-get ebox--deferred-render-gc-state
|
||||
:gc-cons-threshold)
|
||||
1000))
|
||||
(should (= (plist-get ebox--deferred-render-gc-state
|
||||
:gc-cons-percentage)
|
||||
0.1)))
|
||||
(ebox--deferred-render-gc-restore))))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-wrapper-cleans-up-errors-and-quits ()
|
||||
"The call wrapper should restore batch settings across nonlocal exits."
|
||||
(let ((noninteractive t)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-render-gc-cons-threshold 4000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--render-burst-stack nil)
|
||||
quit-seen)
|
||||
(should-error
|
||||
(ebox-call-with-render-burst
|
||||
(lambda ()
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(error "render failed"))))
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))
|
||||
(condition-case nil
|
||||
(ebox-call-with-render-burst (lambda () (signal 'quit nil)))
|
||||
(quit (setq quit-seen t)))
|
||||
(should quit-seen)
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))
|
||||
(should-not ebox--render-burst-stack)))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-interactive-reuses-safe-restore-timer ()
|
||||
"Interactive bursts should defer once and ignore a cancelled timer callback."
|
||||
(let ((noninteractive nil)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-deferred-render-gc-cons-threshold 4000)
|
||||
(ebox-render-gc-cons-percentage 0.8)
|
||||
(ebox--deferred-render-gc-state nil)
|
||||
(ebox--deferred-render-gc-timer nil)
|
||||
(ebox--deferred-render-gc-depth 0)
|
||||
(ebox--deferred-render-gc-generation 0)
|
||||
(ebox--render-burst-stack nil)
|
||||
callbacks
|
||||
cancelled
|
||||
(timer-index 0)
|
||||
(gc-count 0))
|
||||
(cl-letf (((symbol-function 'run-at-time)
|
||||
(lambda (delay repeat function &rest args)
|
||||
(let ((timer (list 'ebox-test-timer (cl-incf timer-index))))
|
||||
(push (list delay repeat function args timer) callbacks)
|
||||
timer)))
|
||||
((symbol-function 'timerp)
|
||||
(lambda (value) (eq (car-safe value) 'ebox-test-timer)))
|
||||
((symbol-function 'cancel-timer)
|
||||
(lambda (timer) (push timer cancelled)))
|
||||
((symbol-function 'garbage-collect)
|
||||
(lambda () (cl-incf gc-count))))
|
||||
(should (eq (ebox-call-with-render-burst #'identity 'done) 'done))
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(should (= (length callbacks) 1))
|
||||
(let ((stale (car callbacks))
|
||||
(token (ebox-render-burst-begin)))
|
||||
(should (= (length cancelled) 1))
|
||||
(apply (nth 2 stale) (nth 3 stale))
|
||||
(should (= gc-cons-threshold 4000))
|
||||
(should (= gc-count 0))
|
||||
(ebox-render-burst-end token))
|
||||
(should (= (length callbacks) 2))
|
||||
(let ((current (car callbacks)))
|
||||
(should (= (nth 0 current) 0.2))
|
||||
(apply (nth 2 current) (nth 3 current)))
|
||||
(should (= gc-cons-threshold 1000))
|
||||
(should (= gc-cons-percentage 0.1))
|
||||
(should (= gc-count 1)))))
|
||||
|
||||
(ert-deftest ebox-public-render-burst-nests-around-interactive-commit ()
|
||||
"A framework burst should own GC before and throughout Ebox commit."
|
||||
(ebox-test--reset-runtime-state)
|
||||
(let ((buffer (ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-test*")
|
||||
(ebox-create :content "before"))))
|
||||
(unwind-protect
|
||||
(let ((noninteractive nil)
|
||||
(gc-cons-threshold 1000)
|
||||
(gc-cons-percentage 0.1)
|
||||
(ebox-deferred-render-gc-cons-threshold 4000)
|
||||
(ebox--deferred-render-gc-state nil)
|
||||
(ebox--deferred-render-gc-timer nil)
|
||||
(ebox--deferred-render-gc-depth 0)
|
||||
(ebox--render-burst-stack nil)
|
||||
scheduled
|
||||
observed-depth)
|
||||
(cl-letf (((symbol-function 'run-at-time)
|
||||
(lambda (delay _repeat function &rest args)
|
||||
(setq scheduled (list delay function args))
|
||||
'ebox-test-timer))
|
||||
((symbol-function 'timerp)
|
||||
(lambda (value) (eq value 'ebox-test-timer)))
|
||||
((symbol-function 'cancel-timer) #'ignore))
|
||||
(let ((token (ebox-render-burst-begin)))
|
||||
(ebox-commit
|
||||
buffer (ebox-create :content "after")
|
||||
(lambda (_report)
|
||||
(setq observed-depth ebox--deferred-render-gc-depth)))
|
||||
(should (= observed-depth 2))
|
||||
(should (= ebox--deferred-render-gc-depth 1))
|
||||
(should-not scheduled)
|
||||
(ebox-render-burst-end token))
|
||||
(should (= ebox--deferred-render-gc-depth 0))
|
||||
(should (= (car scheduled) 0.2))
|
||||
(apply (nth 1 scheduled) (nth 2 scheduled))
|
||||
(should (= gc-cons-threshold 1000))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-deferred-render-gc-keeps-overlapping-burst-owned ()
|
||||
"A nested idle slice must not restore GC during an active animation."
|
||||
(let ((gc-cons-threshold 1000)
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"ebox-layout.el"
|
||||
"ebox-flex.el"
|
||||
"ebox-grid.el"
|
||||
"ebox-buffer-backend.el"
|
||||
"ebox-selector.el"
|
||||
"ebox-native-reflow.el")
|
||||
"Source files that currently own public autoload entry points.")
|
||||
@ -69,7 +70,8 @@
|
||||
"ebox-tree.el" "ebox-measure.el"
|
||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
||||
"ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el"
|
||||
"ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||
"ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el"
|
||||
"ebox-selector.el" "ebox.el"
|
||||
"ebox-native-reflow.el"))))
|
||||
|
||||
(ert-deftest ebox-surface-depends-only-on-public-tp-api ()
|
||||
@ -125,6 +127,9 @@
|
||||
"Stable public entry points should be discoverable by package autoloads."
|
||||
(let ((source (ebox-test--source-text)))
|
||||
(dolist (entry '(("ebox-clear-cache" . "defun")
|
||||
("ebox-render-burst-begin" . "defun")
|
||||
("ebox-render-burst-end" . "defun")
|
||||
("ebox-call-with-render-burst" . "defun")
|
||||
("ebox-render" . "defun")
|
||||
("ebox-region-ids" . "defun")
|
||||
("ebox-concat" . "defun")
|
||||
@ -163,6 +168,7 @@
|
||||
ebox-buffer-update-report
|
||||
ebox-build
|
||||
ebox-buffer-mode
|
||||
ebox-call-with-render-burst
|
||||
ebox-candidate-begin
|
||||
ebox-child-range
|
||||
ebox-candidate-replace
|
||||
@ -189,6 +195,8 @@
|
||||
ebox-region-resolve
|
||||
ebox-region-update
|
||||
ebox-render
|
||||
ebox-render-burst-begin
|
||||
ebox-render-burst-end
|
||||
ebox-render-to-buffer
|
||||
ebox-rerender-buffer-with-context
|
||||
ebox-row
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
(load-file (expand-file-name "../ebox.el"
|
||||
(file-name-directory load-file-name)))
|
||||
(require 'tp-surface)
|
||||
(require 'ebox-native-reflow)
|
||||
|
||||
(defun ebox-surface-test--reset-render-state ()
|
||||
"Reset render identities and side tables used by projection tests."
|
||||
@ -270,6 +271,42 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-surface-producer node))
|
||||
(ebox-render node))))
|
||||
|
||||
(defun ebox-surface-test--project-row-column ()
|
||||
"Project the row/column fixture through the pure TP materializer."
|
||||
(ebox-surface-test--render-fresh
|
||||
(cdr (assq 'row-column (ebox-surface-test--fixtures))) t))
|
||||
|
||||
(defun ebox-surface-test--canonical-region-properties (rendered)
|
||||
"Return RENDERED with opaque region ids renamed by semantic occurrence."
|
||||
(let ((copy (copy-sequence rendered))
|
||||
(ids (make-hash-table :test #'eql))
|
||||
(next-id 0)
|
||||
(properties
|
||||
(delete-dups
|
||||
(append (mapcar #'cdr ebox-region-types)
|
||||
'(ebox-content ebox-content-owner ebox-scroll-window
|
||||
ebox-overflow-foreground-source)))))
|
||||
(cl-labels ((canonical
|
||||
(id)
|
||||
(or (gethash id ids)
|
||||
(let ((canonical (cl-incf next-id)))
|
||||
(puthash id canonical ids)
|
||||
canonical))))
|
||||
(let ((position 0))
|
||||
(while (< position (length copy))
|
||||
(let* ((end (or (next-property-change position copy) (length copy)))
|
||||
(props (text-properties-at position copy)))
|
||||
(dolist (property properties)
|
||||
(when-let* ((id (plist-get props property)))
|
||||
(setq props (plist-put props property (canonical id)))))
|
||||
(when-let* ((owners (plist-get props 'ebox-content-owners)))
|
||||
(setq props
|
||||
(plist-put props 'ebox-content-owners
|
||||
(mapcar #'canonical owners))))
|
||||
(set-text-properties position end props copy)
|
||||
(setq position end)))))
|
||||
copy))
|
||||
|
||||
(defun ebox-surface-test--walk-runtime (node function)
|
||||
"Call FUNCTION for every runtime NODE in preorder."
|
||||
(funcall function node)
|
||||
@ -439,6 +476,119 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should (plist-get first :face-baseline-known-p))
|
||||
(should-not (get-text-property 0 ebox--paint-origin-property rendered)))))
|
||||
|
||||
(ert-deftest ebox-native-layout-ir-preserves-default-foreground-face ()
|
||||
"Serialize and optionally render the Ebox default foreground reset."
|
||||
(let* ((ebox-viewport-width 80)
|
||||
(ebox-viewport-height 20)
|
||||
(node (ebox-create :content "native" :width 20
|
||||
:color 'ebox/default-foreground))
|
||||
(package (ebox-native-reflow--compile-layout-package node))
|
||||
(document (plist-get package :document))
|
||||
(styles (plist-get document :styles))
|
||||
(control
|
||||
(ebox-native-reflow--layout-control-json
|
||||
document
|
||||
'((:key 1 :viewport-width 80 :viewport-height 20
|
||||
:root-width 20 :runtime-revision 0
|
||||
:context-hash 0 :complete t)))))
|
||||
(should (stringp control))
|
||||
(should
|
||||
(cl-find "(:inherit default)" styles
|
||||
:key (lambda (style)
|
||||
(plist-get (plist-get style :face) :lisp))
|
||||
:test #'equal))
|
||||
(when (ebox-native-reflow-layout-ready-p)
|
||||
(let ((frame
|
||||
(ebox-native-reflow-execute-sync
|
||||
node
|
||||
'(:key 1 :viewport-width 80 :viewport-height 20
|
||||
:root-width 20 :runtime-revision 0
|
||||
:context-hash 0 :complete t)
|
||||
package)))
|
||||
(should (plist-get frame :native-frame))
|
||||
(should (vectorp
|
||||
(plist-get (plist-get frame :effect-tape)
|
||||
:fragment-span-template)))
|
||||
(should (equal-including-properties
|
||||
(plist-get frame :rendered)
|
||||
(ebox-render node)))))))
|
||||
|
||||
(ert-deftest ebox-native-layout-effect-fragments-match-render-scan ()
|
||||
"Decode native paint fragments without scanning rendered properties."
|
||||
(skip-unless (ebox-native-reflow-layout-ready-p))
|
||||
(let* ((ebox-viewport-width 80)
|
||||
(ebox-viewport-height 20)
|
||||
(content (propertize "native" 'face 'italic 'help-echo "source"))
|
||||
(node (ebox-create :content content :width 20
|
||||
:color "#f0f0f0" :bgcolor "#101010"))
|
||||
(package (ebox-native-reflow--compile-layout-package node))
|
||||
(normal
|
||||
(let ((ebox--surface-materialization-active t)
|
||||
(ebox--paint-origin-capture-p t))
|
||||
(ebox--render-layout node)))
|
||||
(normal-fragments (ebox-surface--rendered-fragments normal))
|
||||
(frame
|
||||
(ebox-native-reflow-execute-sync
|
||||
node
|
||||
'(:key 1 :viewport-width 80 :viewport-height 20
|
||||
:root-width 20 :runtime-revision 0
|
||||
:context-hash 0 :complete t)
|
||||
package))
|
||||
(native-fragments (ebox-native-reflow-frame-fragments frame)))
|
||||
(should (equal-including-properties normal (plist-get frame :rendered)))
|
||||
(should (= (length normal-fragments) (length native-fragments)))
|
||||
(cl-mapc
|
||||
(lambda (normal-fragment native-fragment)
|
||||
(dolist (key '(:start :end :line :paint-role-ids :role-ids
|
||||
:paint-address :paint-token :face-baseline
|
||||
:face-baseline-known-p :key))
|
||||
(should (equal (plist-get normal-fragment key)
|
||||
(plist-get native-fragment key)))))
|
||||
normal-fragments native-fragments)))
|
||||
|
||||
(ert-deftest ebox-native-layout-effect-fragments-reject-coordinate-gaps ()
|
||||
"Effect templates must cover the rendered frame exactly once."
|
||||
(should-error
|
||||
(ebox-native-reflow--validate-root-fragment-template
|
||||
[[0 1 0 nil nil nil nil nil]
|
||||
[2 3 0 nil nil nil nil nil]]
|
||||
3 0 0)))
|
||||
|
||||
(ert-deftest ebox-surface-fragment-index-rebases-aligned-text-patch ()
|
||||
"Scan only an aligned replacement and retain surrounding paint addresses."
|
||||
(let ((old (copy-sequence "abcXYZdef"))
|
||||
(output (copy-sequence "abcQdef")))
|
||||
(cl-mapc
|
||||
(lambda (range owner text)
|
||||
(put-text-property (car range) (cdr range)
|
||||
'ebox-content-owner owner text))
|
||||
'((0 . 3) (3 . 6) (6 . 9)) '(1 2 3) (make-list 3 old))
|
||||
(cl-mapc
|
||||
(lambda (range owner text)
|
||||
(put-text-property (car range) (cdr range)
|
||||
'ebox-content-owner owner text))
|
||||
'((0 . 3) (3 . 4) (4 . 7)) '(1 2 3) (make-list 3 output))
|
||||
(let* ((old-fragments (ebox-surface--rendered-fragments old))
|
||||
(rebased
|
||||
(ebox-surface--incremental-patched-fragments
|
||||
output old-fragments
|
||||
'((:old-start 3 :old-end 6 :new-start 3 :new-end 4)))))
|
||||
(should rebased)
|
||||
(should (equal (mapcar (lambda (fragment)
|
||||
(cons (plist-get fragment :start)
|
||||
(plist-get fragment :end)))
|
||||
rebased)
|
||||
'((0 . 3) (3 . 4) (4 . 7))))
|
||||
(should (equal (mapcar (lambda (fragment)
|
||||
(plist-get
|
||||
(plist-get fragment :paint-address)
|
||||
:content-owner))
|
||||
rebased)
|
||||
'(1 2 3)))
|
||||
(should (cl-every (lambda (fragment)
|
||||
(eq (plist-get fragment :text) output))
|
||||
rebased)))))
|
||||
|
||||
(ert-deftest ebox-surface-candidate-plan-copies-face-property-values ()
|
||||
"Candidate plans isolate mutable face values despite provenance hints."
|
||||
(let* ((color (copy-sequence "#192233"))
|
||||
@ -469,7 +619,10 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(let* ((builder (cdr fixture))
|
||||
(expected (ebox-surface-test--render-fresh builder nil))
|
||||
(actual (ebox-surface-test--render-fresh builder t)))
|
||||
(should (equal-including-properties actual expected))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(ebox-surface-test--canonical-region-properties actual)
|
||||
(ebox-surface-test--canonical-region-properties expected)))
|
||||
(should (equal (mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines actual))
|
||||
(mapcar #'ebox--string-pixel-width
|
||||
@ -530,6 +683,21 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
t))
|
||||
(should (> calls 0))))
|
||||
|
||||
(ert-deftest ebox-surface-inline-inheritance-crosses-transparent-layout ()
|
||||
"Transparent layout nodes do not consume inherited paint themselves."
|
||||
(let ((closed
|
||||
(ebox-create
|
||||
:color "red"
|
||||
:ebox-content-node
|
||||
(ebox-column (ebox-create :content "child" :color "blue"))))
|
||||
(open
|
||||
(ebox-create
|
||||
:color "red"
|
||||
:ebox-content-node
|
||||
(ebox-column (ebox-create :content "child")))))
|
||||
(should-not (ebox-surface--inline-inheritance-required-p closed))
|
||||
(should (ebox-surface--inline-inheritance-required-p open))))
|
||||
|
||||
(ert-deftest ebox-box-content-cache-reuses-fixed-viewport-subtree ()
|
||||
"A fixed box content viewport should reuse its exact composite layout."
|
||||
(let* ((ebox--render-cache-table (make-hash-table :test 'equal))
|
||||
@ -640,18 +808,33 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
|
||||
(ert-deftest ebox-surface-projection-does-not-mutate-a-buffer ()
|
||||
"Pure projection should not call any final buffer mutation primitive."
|
||||
(cl-letf (((symbol-function 'insert)
|
||||
(lambda (&rest _) (error "Unexpected buffer insertion")))
|
||||
((symbol-function 'erase-buffer)
|
||||
(lambda (&rest _) (error "Unexpected buffer erase")))
|
||||
((symbol-function 'delete-region)
|
||||
(lambda (&rest _) (error "Unexpected buffer deletion")))
|
||||
((symbol-function 'replace-region-contents)
|
||||
(lambda (&rest _) (error "Unexpected buffer replacement"))))
|
||||
(should
|
||||
(stringp
|
||||
(ebox-surface-test--render-fresh
|
||||
(cdr (assq 'row-column (ebox-surface-test--fixtures))) t)))))
|
||||
(let ((probe (generate-new-buffer " *ebox-projection-probe*"))
|
||||
(original-insert (symbol-function 'insert))
|
||||
(original-erase (symbol-function 'erase-buffer))
|
||||
(original-delete (symbol-function 'delete-region))
|
||||
(original-replace (symbol-function 'replace-region-contents)))
|
||||
(unwind-protect
|
||||
(with-current-buffer probe
|
||||
(cl-labels ((guarded
|
||||
(label original arguments)
|
||||
(when (eq (current-buffer) probe)
|
||||
(error "Unexpected probe buffer %s" label))
|
||||
(apply original arguments)))
|
||||
(cl-letf (((symbol-function 'insert)
|
||||
(lambda (&rest arguments)
|
||||
(guarded 'insert original-insert arguments)))
|
||||
((symbol-function 'erase-buffer)
|
||||
(lambda (&rest arguments)
|
||||
(guarded 'erase original-erase arguments)))
|
||||
((symbol-function 'delete-region)
|
||||
(lambda (&rest arguments)
|
||||
(guarded 'delete original-delete arguments)))
|
||||
((symbol-function 'replace-region-contents)
|
||||
(lambda (&rest arguments)
|
||||
(guarded 'replace original-replace arguments))))
|
||||
(should (stringp (ebox-surface-test--project-row-column)))
|
||||
(should (equal (buffer-string) "")))))
|
||||
(when (buffer-live-p probe) (kill-buffer probe)))))
|
||||
|
||||
(ert-deftest ebox-surface-logical-box-owns-disjoint-render-fragments ()
|
||||
"One logical Ebox box should resolve all of its separated painted regions."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user