perf: publish certified scroll viewports without document planning
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-09-11 00:22:59 +08:00
parent efdc35f4af
commit 993e09be6b
8 changed files with 649 additions and 65 deletions

View File

@ -54,6 +54,16 @@ roots may reuse their complete composed content and render only the wrapper.
Pure root offset changes preserve already composed portal and anchor content. Pure root offset changes preserve already composed portal and anchor content.
Style, placement, content and display-context changes must invalidate that proof. Style, placement, content and display-context changes must invalidate that proof.
Proved scroll-only changes build a complete candidate from the published state
and a local immutable index delta, bypassing general declarative preparation.
Surface stages rich replacement text and exact old/new character ranges, then
returns an authenticated TP commit batch with complete target mount specs;
it does not build a surface plan. Equal text lengths never authorize reuse of
old mounts: newly visible rows may have different owners and interaction tags.
Owner resolution can reuse a private cache while its topology and TP object
indexes remain valid. Attribute updates and layout reflow retain their own
preparation paths; only terminal publication and rollback are shared.
A nested owner can replace its viewport within a proven unchanged allocation, A nested owner can replace its viewport within a proven unchanged allocation,
without rendering unrelated siblings. With one unchanged root scroll ancestor, without rendering unrelated siblings. With one unchanged root scroll ancestor,
the update must also patch that ancestor's retained content, ownership membership the update must also patch that ancestor's retained content, ownership membership
@ -68,6 +78,14 @@ freshly rebound producers. A producer must not mutate a published continuation
before TP commits. This isolation can rebuild the target prefix on a miss; before TP commits. This isolation can rebuild the target prefix on a miss;
cached-hit timings do not describe that cost. cached-hit timings do not describe that cost.
The prepared viewport batch is consumed once inside the existing scoped TP
transaction. Subsequent signal-driven evaluation uses the current committed
source and ordinary projection. A proof miss chooses the ordinary path before
publication; a publication error propagates through rollback without retrying
through another renderer. General preparation and surface-plan counters in the
shared evaluator verify the bypass separately from TP bookkeeping and buffer
write time; this path does not establish constant cost in total object count.
Scroll publication still goes through TP and the Ebox participant. Rejection Scroll publication still goes through TP and the Ebox participant. Rejection
preserves the old text, properties, offsets, raw and rendered caches, source preserves the old text, properties, offsets, raw and rendered caches, source
nodes, indexes, objects, mounts and report. The focused regression targets are nodes, indexes, objects, mounts and report. The focused regression targets are

View File

@ -53,6 +53,13 @@ layered 或带 chrome 的 root 可以复用完整合成内容,只渲染 wrappe
变化保留已经合成的 portal 和 anchor 内容style、placement、content 或 display 变化保留已经合成的 portal 和 anchor 内容style、placement、content 或 display
context 变化必须使该证明失效。 context 变化必须使该证明失效。
通过证明的纯滚动变更从已发布状态与局部不可变 index delta 构造完整 candidate
跳过通用声明式准备。Surface 提前生成带属性的替换文本及精确的新旧字符范围,
再通过带完整目标 mount specs 的认证 TP commit batch 发布,不构造 surface plan。
文本等长不能证明旧 mount 可复用:新进入视口的行可能具有不同 owner 与交互 tag。
Topology 与 TP object index 有效时,可复用私有 owner 解析缓存。属性更新与布局
重排仍使用各自的准备路径,只共享最终发布与回滚边界。
Nested owner 在 allocation 不变且通过证明时,可以只替换自己的 viewport避免渲染 Nested owner 在 allocation 不变且通过证明时,可以只替换自己的 viewport避免渲染
无关兄弟节点。若存在一个 offset 不变的 root scroll ancestor还必须更新该祖先的 无关兄弟节点。若存在一个 offset 不变的 root scroll ancestor还必须更新该祖先的
保留内容、ownership membership 和 line index并将 producer 重新绑定到 candidate 保留内容、ownership membership 和 line index并将 producer 重新绑定到 candidate
@ -64,6 +71,12 @@ Nested owner 在 allocation 不变且通过证明时,可以只替换自己的
不得在 TP commit 前修改已发布的 continuation。该隔离可能在 miss 时重建目标 prefix 不得在 TP commit 前修改已发布的 continuation。该隔离可能在 miss 时重建目标 prefix
因此 cached-hit 耗时不能代表 miss 的代价。 因此 cached-hit 耗时不能代表 miss 的代价。
预备的 viewport batch 在现有 scoped TP transaction 中只消费一次。后续 signal
触发的求值读取当前已提交 source使用普通 projection。证明失败在发布前选择普通
路径;发布错误直接进入 rollback不换另一个 renderer 重试。共享 evaluator 的
通用准备与 surface-plan 计数分别验证这两项工作已跳过TP bookkeeping 与实际
buffer 写入另行测量。该路径并不保证耗时与总 object 数无关。
Scroll publication 仍经过 TP 与 Ebox participant。发布失败时旧文本、属性、offset、 Scroll publication 仍经过 TP 与 Ebox participant。发布失败时旧文本、属性、offset、
raw/rendered cache、source node、index、object、mount 和 report 均保持不变。聚焦回归 raw/rendered cache、source node、index、object、mount 和 report 均保持不变。聚焦回归
入口为 `make layer-tests`、`make nested-scroll-tests`、`make scroll-cold-tests` 入口为 `make layer-tests`、`make nested-scroll-tests`、`make scroll-cold-tests`

View File

@ -233,6 +233,17 @@
:rollback discard-unreferenced-candidates :rollback discard-unreferenced-candidates
:rebuild-proof reproject-owner-native-hover-face :rebuild-proof reproject-owner-native-hover-face
:cleanup garbage-collect-unreferenced-faces) :cleanup garbage-collect-unreferenced-faces)
(:id scroll-viewport-owner-cache
:storage (:scroll-viewport-owner-cache)
:current-contract candidate-private-resolved-owner-table
:target-contract discardable-derived-values
:category disposable-cache
:owner ebox-surface
:mutation-api copy-before-candidate-fill
:generation-binding unchanged-parent-region-and-tp-object-indexes
:rollback discard-candidate-cache
:rebuild-proof resolve-rich-line-roles-against-retained-topology
:cleanup ordinary-projection-or-generation-replacement)
(:id derived-caches (:id derived-caches
:storage (ebox--box-content-render-cache ebox--char-width-cache :storage (ebox--box-content-render-cache ebox--char-width-cache
ebox--display-signature-cache ebox--face-height-width-cache ebox--display-signature-cache ebox--face-height-width-cache

View File

@ -184,6 +184,7 @@ the winning client state; otherwise only the candidate is disposable."
(owner key compute value dependencies &rest options)) (owner key compute value dependencies &rest options))
(declare-function ebox--record-render-output-provenance (declare-function ebox--record-render-output-provenance
"ebox-render-context" (rendered)) "ebox-render-context" (rendered))
(declare-function ebox--strip-paint-origins! "ebox-render-context" (string))
(declare-function ebox--render-owned-text-value-p (declare-function ebox--render-owned-text-value-p
"ebox-render-context" (property value &optional registry)) "ebox-render-context" (property value &optional registry))
(declare-function ebox-incremental--hash-snapshot (declare-function ebox-incremental--hash-snapshot
@ -2424,10 +2425,13 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(signals (car signals-result)) (signals (car signals-result))
(signals-created-p (cdr signals-result)) (signals-created-p (cdr signals-result))
(producer (producer
(ebox-surface-producer (if (plist-get state-overrides :scroll-viewport-batch)
source old-state t state-overrides signals projection-kind (ebox-surface--scroll-viewport-producer
source-isolated-p old-state state-overrides signals)
(plist-get state-overrides :source-path-copied-p))) (ebox-surface-producer
source old-state t state-overrides signals projection-kind
source-isolated-p
(plist-get state-overrides :source-path-copied-p))))
success native-settled-p) success native-settled-p)
(when (and (null on-mismatch) (when (and (null on-mismatch)
(or (plist-get state-overrides :owner-scoped-proofs) (or (plist-get state-overrides :owner-scoped-proofs)
@ -5275,6 +5279,173 @@ copies where needed. Detached owners keep their fractional-origin guard."
0 (length output) (list 'ebox-scroll-window region-id) output)) 0 (length output) (list 'ebox-scroll-window region-id) output))
output)))) output))))
(defun ebox-surface--prepare-scroll-viewport (buffer previous-state state)
"Stage STATE's exact rich viewport without document or surface planning.
PREVIOUS-STATE remains the published generation in BUFFER. Existing root
wrapper and nested allocation proofs select the output; a proof miss returns
nil before any TP publication. The resulting ordinary TP batch contains only
the proved replacement coordinates, including an enclosing cache's separate
content-coordinate update when present."
(with-current-buffer buffer
(save-restriction
(widen)
(let* ((nested (plist-get state :nested-scroll-patch-proof))
(output
(if nested
(when (and (not (plist-get previous-state :fractional-pixel-output-p))
(or (not (plist-get nested :scroll-content-region-id))
(plist-get state :scroll-content-proof)))
(unless (plist-get state :owner-scoped-proofs)
(plist-put state :owner-scoped-proofs (list nested)))
(let ((ebox-surface--integral-local-output-required-p t))
(catch 'ebox/fractional-local-output
(ebox-surface--span-patch-output buffer previous-state state))))
(ebox-surface--scroll-patch-output buffer state t))))
(when (and (stringp output)
(not (plist-get state :paint-property-contributions)))
(unless (plist-get state :scroll-patch-lines)
(plist-put state :scroll-viewport-fragments
(or (plist-get state :span-patch-fragment-data)
(ebox-surface--rendered-fragments output))))
(ebox--strip-paint-origins! output)
(let* ((base-extent (- (point-max) (point-min)))
(target-extent (length output))
(coordinates
(or (plist-get state :content-coordinate-patches)
(list (list :old-start 0 :old-end base-extent
:new-start 0 :new-end target-extent))))
(patches
(mapcar
(lambda (patch)
(append patch
(list :replacement
(substring output (plist-get patch :new-start)
(plist-get patch :new-end)))))
coordinates))
(batch
(condition-case nil
(tp-commit-batch-create
:base-revision (plist-get state :runtime-revision)
:target-revision (1+ (plist-get state :runtime-revision))
:base-extent base-extent :target-extent target-extent
:patches patches :coordinate-patches coordinates)
(tp-surface-error nil))))
(when batch
(plist-put state :scroll-viewport-batch batch)
output)))))))
(defun ebox-surface--scroll-viewport-owner-cache (previous-state state)
"Return a private owner-resolution cache for STATE's unchanged topology.
Reuse requires PREVIOUS-STATE's exact parent, region and TP object indexes.
The scroll-only source proof preserves their meanings when rebinding copied
Boxes; ordinary document projection discards this optional derived cache."
(let* ((old (plist-get previous-state :scroll-viewport-owner-cache))
(keys '(:parent-table :region-node-table
:surface-node-object-table :region-surface-object-table))
(valid (and old
(cl-every (lambda (key)
(eq (plist-get old key) (plist-get previous-state key)))
keys)))
(cache (list :owners
(if valid (copy-hash-table (plist-get old :owners))
(make-hash-table :test #'equal)))))
(dolist (key keys)
(plist-put cache key (plist-get state key)))
cache))
(defun ebox-surface--scroll-viewport-result (context previous-state state output batch)
"Publish STATE's proved viewport OUTPUT and BATCH in the active CONTEXT.
Reuse PREVIOUS-STATE's keyed topology and resolve rich roles against that
unchanged topology. Every batch supplies complete target ownership, since
equal character lengths do not imply equal visible owners or interaction."
(let* ((surface-root (tp-object-ensure context nil ebox-surface--root-key 'ebox/surface))
(node-root (tp-object-ensure context surface-root ebox-surface--nodes-key 'ebox/nodes))
(fragment-root (tp-object-ensure context surface-root
ebox-surface--fragments-key 'ebox/fragments))
(text-leaf (tp-object-ensure context fragment-root ebox-surface--text-key 'ebox/text))
(node-objects (plist-get state :surface-node-object-table))
(region-objects (plist-get state :region-surface-object-table))
(lines (plist-get state :scroll-patch-lines))
(fragments
(unless lines
(plist-get state :scroll-viewport-fragments)))
(_cache
(plist-put state :scroll-viewport-owner-cache
(ebox-surface--scroll-viewport-owner-cache previous-state state)))
(owned-ranges
(if lines
(ebox-surface--scroll-owned-ranges
context text-leaf lines state node-objects region-objects nil)
(ebox-surface--owned-ranges
context text-leaf fragments state node-objects region-objects nil)))
(length (length output))
(records
(list (list :object text-leaf :start 0 :end length :props nil :tags nil :leaf t)
(list :object fragment-root :start 0 :end length :props nil :tags nil :leaf nil)
(list :object surface-root :start 0 :end length :props nil :tags nil :leaf nil))))
(tp-object-reuse-subtree context node-root)
(plist-put state :surface-owned-ranges owned-ranges)
(plist-put state :surface-fragments
(if lines (list :scroll-lines (copy-sequence lines))
(ebox-surface--fragment-metadata fragments state)))
(dolist (key '(:scroll-viewport-output :scroll-viewport-batch :scroll-viewport-fragments
:content-coordinate-patches :content-base-extent
:span-patch-fragment-data :span-patch-content-p
:span-patch-retained-owned-ranges :scroll-patch-lines
:nested-scroll-patch-proof :scroll-content-proof
:owner-scoped-proofs :scroll-state-transaction
:scroll-fast-window-p :scroll-patch-region-id :span-patch-owner-id
:preserve-scroll-producer-region-ids :previous-surface-owned-ranges))
(cl-remf state key))
(plist-put state :retained-scroll-content-p nil)
(tp-commit-batch-result-create
context batch :mount-specs (ebox-surface--commit-mount-specs records owned-ranges)
:client-state state)))
(defun ebox-surface--scroll-viewport-producer (previous-state state signals)
"Return a scroll-only producer using the existing scoped transaction.
STATE owns one prepared viewport batch after PREVIOUS-STATE. Subscribe to
SIGNALS just like ordinary projection, but consume that batch only once.
A later reactive evaluation renders the then-current source normally rather
than replaying old coordinates, properties or revision authority."
(let ((output (plist-get state :scroll-viewport-output))
(batch (plist-get state :scroll-viewport-batch))
(offsets (ebox-surface--scroll-offsets (plist-get state :scroll-state-table))))
(lambda (context)
(let* ((buffer (ebox-surface--signals-buffer signals))
(surface (ebox-surface--live-buffer-surface buffer))
(current (tp-surface-client-state surface))
(prepared state)
(base previous-state)
(rendered output)
(prepared-batch batch)
(expected-offsets offsets)
(_consume
;; A producer remains subscribed after this evaluation. Keep
;; only current signals in that closure, on success or fallback;
;; later evaluations must not retain an obsolete document.
(setq state nil previous-state nil output nil batch nil offsets nil))
(axes (plist-get (or prepared current) :viewport-dependent-node-id-axes))
(display (tp-signal-read (ebox-surface--signals-display signals)))
(width (funcall (if (car axes) #'tp-signal-read #'tp-signal-peek)
(ebox-surface--signals-viewport-width signals)))
(height (funcall (if (cdr axes) #'tp-signal-read #'tp-signal-peek)
(ebox-surface--signals-viewport-height signals)))
(scroll (tp-signal-read (ebox-surface--signals-scroll signals))))
(with-current-buffer buffer
(if (and prepared (eq current base)
(equal display (plist-get prepared :display-signature))
(equal width (plist-get prepared :viewport-width))
(equal height (plist-get prepared :viewport-height))
(equal scroll expected-offsets))
(ebox-surface--scroll-viewport-result
context base prepared rendered prepared-batch)
(ebox-surface--project
context (ebox-tree-copy-node-structure (plist-get current :root-node))
current t (list :runtime-revision (tp-surface-revision surface)
:source-base-index (plist-get current :source-index))
signals nil t nil)))))))
(defun ebox-surface--runtime-index-ready-p (state) (defun ebox-surface--runtime-index-ready-p (state)
"Return non-nil when STATE carries a complete prepared runtime index." "Return non-nil when STATE carries a complete prepared runtime index."
(let* ((root (plist-get state :root-node)) (let* ((root (plist-get state :root-node))
@ -5766,7 +5937,9 @@ VISIT-RUNS receives one callback accepting ROLE-IDS, START, END, and optional
layout dependency region ids. layout dependency region ids.
Adjacent ranges merge only when both their owner and opaque tags match." Adjacent ranges merge only when both their owner and opaque tags match."
(let ((active (make-hash-table :test #'eq)) (let ((active (make-hash-table :test #'eq))
(owner-cache (make-hash-table :test #'equal)) (owner-cache
(or (plist-get (plist-get state :scroll-viewport-owner-cache) :owners)
(make-hash-table :test #'equal)))
ranges) ranges)
(funcall (funcall
visit-runs visit-runs

162
ebox.el
View File

@ -3393,29 +3393,35 @@ A miss owns its subtree, indexes, cache lines and producers before staging."
(or (and (ebox--scroll-state-retained-window-ready-p (or (and (ebox--scroll-state-retained-window-ready-p
staged-scroll-state) t) staged-scroll-state) t)
(and (plist-get runtime :nested-scroll-patch-proof) t))) (and (plist-get runtime :nested-scroll-patch-proof) t)))
(report-overrides
(append
(ebox--constraint-change-report-props
(ebox--region-constraint-change
buffer region-id 'geometry '(:scroll-offset)))
(list :region-id region-id
:constraint-source 'scroll
:strategy 'span-patch
:patch-count 1
:patch-ops '(span-patch)
:owner-id owner-id
:owner-ids (list owner-id)
:scroll-state-transaction t
:scroll-patch-fast-p scroll-patch-fast-p
:native-scroll-materialize-p
(plist-get transition :native-materialize-p))))
(viewport-commit
(unless (plist-get transition :native-materialize-p)
(ebox--surface-scroll-viewport-commit
buffer region-id runtime report-overrides)))
(commit-input (commit-input
(or viewport-commit
(let ((ebox--scroll-global-state table)) (let ((ebox--scroll-global-state table))
(ebox-incremental-prepare-scoped-commit (ebox-incremental-prepare-scoped-commit
buffer root buffer root report-overrides t))))
(append
(ebox--constraint-change-report-props
(ebox--region-constraint-change
buffer region-id 'geometry '(:scroll-offset)))
(list :region-id region-id
:constraint-source 'scroll
:strategy 'span-patch
:patch-count 1
:patch-ops '(span-patch)
:owner-id owner-id
:owner-ids (list owner-id)
:scroll-state-transaction t
:scroll-patch-fast-p scroll-patch-fast-p
:native-scroll-materialize-p
(plist-get transition :native-materialize-p)))
t)))
(_staged-cache (_staged-cache
(ebox--surface-restore-staged-scroll-cache (unless viewport-commit
commit-input region-id staged-scroll-state)) (ebox--surface-restore-staged-scroll-cache
commit-input region-id staged-scroll-state)))
(scope-node-ids (scope-node-ids
(or (plist-get commit-input :scope-node-ids) (or (plist-get commit-input :scope-node-ids)
(list owner-id))) (list owner-id)))
@ -3455,6 +3461,124 @@ A miss owns its subtree, indexes, cache lines and producers before staging."
(ignore surface) (ignore surface)
(plist-get transition :result)))) (plist-get transition :result))))
(defun ebox--surface-scroll-viewport-commit
(buffer region-id runtime report)
"Prepare BUFFER's REGION-ID retained viewport from isolated RUNTIME.
Return an ordinary scoped commit input, or nil before publication when the
document, display context or allocation needs general preparation. REPORT
describes the scroll intent; Surface supplies exact text and ownership.
The local index delta preserves immutable source facts and rebinds copied
Boxes without publishing RUNTIME's deliberately sparse staging indexes."
(let* ((base (plist-get runtime :base-state))
(root (plist-get runtime :root))
(old-root (plist-get base :root-node))
(owner (plist-get report :owner-id))
(root-p (equal owner (plist-get root :node-id)))
(nested-proof (plist-get runtime :nested-scroll-patch-proof))
(old-scroll (gethash region-id (plist-get base :scroll-state-table))))
(when (and owner old-scroll
(ebox-surface--runtime-index-ready-p base)
(hash-table-p (plist-get base :surface-node-object-table))
(hash-table-p (plist-get base :region-surface-object-table))
(or root-p nested-proof)
(not (plist-get base :native-sync-session))
(not (plist-get base :native-sync-confirmed-p))
(not (plist-get old-scroll :lazy-scroll-prefix-dirty))
(not (plist-get old-scroll :lazy-scroll-window-refresh-required))
(not (plist-get old-scroll :native-reflow-target-prefix-p))
(not (plist-get old-scroll :native-reflow-materialize-p))
(zerop (ebox-tree-author-style-count old-root))
(zerop (ebox-tree-author-style-count root))
(equal (plist-get base :display-signature)
(ebox--current-display-signature))
(eq (not (null (plist-get base :cascade-active-p)))
(not (null (ebox-style-cascade-active-p))))
(or (not (plist-get base :cascade-active-p))
(equal (plist-get base :stylesheet-signature)
(ebox-surface--stylesheet-signature))))
(let* ((delta (ebox-incremental--candidate-local-index-delta base root))
(dirty (plist-get delta :dirty-set))
(index (plist-get delta :index)))
(when (and (null (plist-get delta :removed-node-ids))
(cl-every
(lambda (entry)
(and (equal owner (plist-get entry :node-id))
(not (plist-get entry :children))
(cl-every
(lambda (key)
(memq key '(:scroll-offset :ebox-scroll-offset-controlled-p)))
(plist-get entry :changed-keys))))
dirty))
(let* ((candidate (ebox--render-state-install-index (copy-sequence base) index))
(table (plist-get runtime :scroll-table))
(prepared (list :root root :dirty-set dirty :scroll-state-table table))
(structural-caches
(ebox-incremental--candidate-structural-caches base index dirty delta)))
(dolist (key '(:owner-scoped-proofs :scroll-content-proof
:content-coordinate-patches :content-base-extent
:span-patch-fragment-data :span-patch-content-p
:span-patch-retained-owned-ranges :scroll-patch-lines
:paint-property-contributions :previous-surface-owned-ranges
:mixed-owner-proof :mixed-owner-fragment-data
:mixed-owner-content-p :formatting-context-reflow-proof
:layer-recompose-p :native-scroll-materialize-p
:native-buffer-scroll-p :native-render-fallback))
(cl-remf candidate key))
(ebox-incremental--isolate-candidate-caches candidate)
(plist-put candidate :root-node root)
(plist-put candidate :scroll-state-table table)
(plist-put candidate :runtime-index-prepared-p t)
(plist-put candidate :runtime-revision
(1+ (or (plist-get base :runtime-revision) 0)))
(plist-put candidate :selector-index-stale-p
(plist-get index :selector-index-stale-p))
(plist-put candidate :layout-snapshots (make-hash-table :test 'equal))
(plist-put candidate :layout-snapshots-complete-p nil)
(plist-put candidate :layout-snapshot-detail-generation
(1+ (or (plist-get base :layout-snapshot-detail-generation) 0)))
(plist-put candidate :retained-allocation-certificates nil)
(plist-put candidate :reflow-prewarm-scratch nil)
;; Quantization registers nested mutable property-value tables.
;; A viewport candidate owns that registry before touching even
;; one fractional carrier, including when TP later rejects it.
(plist-put candidate :render-owned-text-values (make-hash-table :test #'eq))
(plist-put candidate :flex-content-min-widths (make-hash-table :test #'eq))
(dolist (key '(:render-signature-cache :viewport-height-dependent-subtree-cache))
(plist-put candidate key (plist-get structural-caches key)))
(plist-put candidate :projection-kind 'scroll-patch)
(plist-put candidate :scroll-state-transaction t)
(plist-put candidate :scroll-patch-region-id region-id)
(plist-put candidate :span-patch-owner-id owner)
(plist-put candidate :scroll-fast-window-p t)
(plist-put candidate :retained-scroll-content-p root-p)
(plist-put candidate :preserve-scroll-producer-region-ids (list region-id))
(plist-put candidate :nested-scroll-patch-proof nested-proof)
(let ((ebox--render-source-index (plist-get candidate :source-index))
(ebox--render-source-generations
(list (plist-get candidate :source-index) (plist-get base :source-index)))
(ebox--render-source-states (list candidate base)))
(when (plist-get nested-proof :scroll-content-region-id)
(when-let* ((proof (ebox-incremental--scroll-content-span-proof
buffer base prepared candidate owner)))
(plist-put candidate :owner-scoped-proofs (list proof))
(plist-put candidate :scroll-content-proof
(plist-get proof :scroll-content-proof))))
(when-let* (((or (not root-p)
(not (or (plist-get base :layered-p)
(plist-get base :root-portals-p)))
(ebox-incremental--layer-root-scroll-p
base candidate prepared owner)))
(output (ebox-surface--prepare-scroll-viewport buffer base candidate)))
(plist-put candidate :scroll-viewport-output output)
(list :root root :scope-node-ids (list owner)
:report-base
(ebox-incremental--plist-overlay
(ebox-incremental--layout-owner-report
prepared (list (ebox--patch-op 'span-patch owner :dirty dirty)) candidate)
(append report '(:projection-kind scroll-patch :full-rerender nil)))
:projection-kind 'scroll-patch
:state-overrides candidate)))))))))
(defun ebox--surface-restore-staged-scroll-cache (defun ebox--surface-restore-staged-scroll-cache
(commit-input region-id staged-state) (commit-input region-id staged-state)
"Restore REGION-ID STAGED-STATE cache fields in COMMIT-INPUT." "Restore REGION-ID STAGED-STATE cache fields in COMMIT-INPUT."

View File

@ -9,6 +9,27 @@
(require 'ert) (require 'ert)
(require 'cl-lib) (require 'cl-lib)
(require 'ebox) (require 'ebox)
(require 'ebox-layer-publication-tests)
(defmacro ebox-layer-scroll-test--without-general-preparation (&rest body)
"Run cached publication BODY without document preparation or surface scans."
(declare (indent 0) (debug t))
`(let ((stages '(ebox-incremental-prepare-scoped-commit
ebox-incremental--prepare-declarative-runtime
ebox--runtime-index ebox-surface--surface-plan))
counters advices)
(unwind-protect
(progn
(dolist (stage stages)
(let* ((counter (cons stage 0))
(advice (lambda (&rest _) (cl-incf (cdr counter)))))
(push counter counters)
(push (cons stage advice) advices)
(advice-add stage :before advice)))
,@body)
(dolist (entry advices) (advice-remove (car entry) (cdr entry))))
(should (equal (nreverse counters)
(mapcar (lambda (stage) (cons stage 0)) stages)))))
(defmacro ebox-layer-scroll-test--with-buffer (&rest body) (defmacro ebox-layer-scroll-test--with-buffer (&rest body)
"Run BODY in an isolated Elisp scroll buffer without native window scrolling." "Run BODY in an isolated Elisp scroll buffer without native window scrolling."
@ -55,6 +76,24 @@
(root (plist-get runtime :root-node))) (root (plist-get runtime :root-node)))
(gethash (plist-get root :region-id) (plist-get runtime :scroll-state-table)))) (gethash (plist-get root :region-id) (plist-get runtime :scroll-state-table))))
(defun ebox-layer-scroll-test--ordinary-input ()
"Build an ordinary root with fractional carriers in retained physical rows."
(ebox-build
`(column :id "root" :width (vw 100) :height (vh 100) :overflow scroll
,@(cl-loop for index below 80
collect `(box :padding-left (px 1.5)
,(format "ROW%02d" index))))))
(defun ebox-layer-scroll-test--materialize-root ()
"Fully cache the root and bootstrap its retained physical rows."
(let* ((runtime (ebox--buffer-render-state (current-buffer)))
(state (ebox-layer-scroll-test--root-state))
(region (plist-get (plist-get state :box) :region-id)))
(setq state (ebox--scroll-state-materialize-lines region state))
(puthash region state (plist-get runtime :scroll-state-table))
(should (= 1 (ebox--scroll-region-by region 1 1)))
(should (= -1 (ebox--scroll-region-by region -1 1)))))
(defun ebox-layer-scroll-test--portal-chain-input () (defun ebox-layer-scroll-test--portal-chain-input ()
"Build two visible anchored root projections through a clipped logical owner." "Build two visible anchored root projections through a clipped logical owner."
(ebox-build (ebox-build
@ -93,6 +132,7 @@
(unless (eq candidate root) (unless (eq candidate root)
(ebox-put candidate :scroll-offset offset) (ebox-put candidate :scroll-offset offset)
(ebox-put candidate :ebox-scroll-offset-controlled-p t)))) (ebox-put candidate :ebox-scroll-offset-controlled-p t))))
(mounts (ebox-layer-publication-test--mount-signature (current-buffer)))
actual fresh) actual fresh)
(setq actual (buffer-string)) (setq actual (buffer-string))
;; Bind only the first layout invocation, leaving nested scroll offsets ;; Bind only the first layout invocation, leaving nested scroll offsets
@ -102,7 +142,16 @@
(lambda (candidate) (lambda (candidate)
(unless first (setq first t) (funcall set-offset candidate)) (unless first (setq first t) (funcall set-offset candidate))
(funcall render candidate)))) (funcall render candidate))))
(setq fresh (ebox-render input)))) (with-temp-buffer
(unwind-protect
(progn
(ebox-render-to-buffer (current-buffer) input)
(setq fresh (buffer-string))
(should (equal mounts
(ebox-layer-publication-test--mount-signature
(current-buffer)))))
(when (ebox-surface-buffer-mounted-p (current-buffer))
(ebox-unmount-buffer (current-buffer)))))))
(should (equal (substring-no-properties actual) (substring-no-properties fresh))) (should (equal (substring-no-properties actual) (substring-no-properties fresh)))
(should (= (length actual) (length fresh))) (should (= (length actual) (length fresh)))
(dotimes (position (length actual)) (dotimes (position (length actual))
@ -120,22 +169,38 @@
(let* ((state (ebox-layer-scroll-test--root-state)) (let* ((state (ebox-layer-scroll-test--root-state))
(region-id (plist-get (plist-get state :box) :region-id)) (region-id (plist-get (plist-get state :box) :region-id))
(content (plist-get state :content-lines)) (content (plist-get state :content-lines))
(objects (copy-hash-table
(plist-get (ebox--buffer-render-state (current-buffer))
:surface-node-object-table)))
(root-renders 0) (root-renders 0)
(render (symbol-function 'ebox-surface--render-candidate))) (render (symbol-function 'ebox-surface--render-candidate)))
(when (eq chrome 'fractional) (when (eq chrome 'fractional)
(should (plist-get (ebox--buffer-render-state (current-buffer)) (should (plist-get (ebox--buffer-render-state (current-buffer))
:fractional-pixel-output-p))) :fractional-pixel-output-p)))
(dolist (delta '(1 1 -1 -1)) (dolist (delta '(1 1 -1 -1))
(cl-letf (((symbol-function 'ebox-surface--render-candidate) (ebox-layer-scroll-test--without-general-preparation
(lambda (&rest arguments) (let ((revision (ebox-surface-buffer-revision (current-buffer)))
(cl-incf root-renders) (before (buffer-string)))
(apply render arguments)))) (cl-letf (((symbol-function 'ebox-surface--render-candidate)
(should (= delta (ebox--scroll-region-by region-id delta 1))) (lambda (&rest arguments)
(should (eq content (cl-incf root-renders)
(plist-get (ebox-layer-scroll-test--root-state) :content-lines))) (apply render arguments))))
(should (eq (plist-get (ebox-buffer-update-report (current-buffer)) (should (= delta (ebox--scroll-region-by region-id delta 1)))
:projection-kind) (should (eq content
'scroll-patch))) (plist-get (ebox-layer-scroll-test--root-state) :content-lines)))
(should (eq (plist-get (ebox-buffer-update-report (current-buffer))
:projection-kind)
'scroll-patch)))
(should (= (1+ revision)
(ebox-surface-buffer-revision (current-buffer))))
(should-not (equal-including-properties before (buffer-string))))
(maphash
(lambda (id object)
(should (eq object
(gethash id (plist-get
(ebox--buffer-render-state (current-buffer))
:surface-node-object-table)))))
objects))
(ebox-layer-scroll-test--assert-fresh)) (ebox-layer-scroll-test--assert-fresh))
(should (= root-renders 0)))))) (should (= root-renders 0))))))
@ -185,22 +250,53 @@
(ert-deftest ebox-layer-scroll-rejected-publication-keeps-cache-and-input () (ert-deftest ebox-layer-scroll-rejected-publication-keeps-cache-and-input ()
"Both publication failure stages preserve composed cache and current offset." "Both publication failure stages preserve composed cache and current offset."
(dolist (failure-step '(text client-state)) (pcase-dolist (`(,fixture ,failure-step)
'((layer text) (layer client-state)
(ordinary text) (ordinary client-state)))
(ebox-layer-scroll-test--with-buffer (ebox-layer-scroll-test--with-buffer
(ebox-render-to-buffer (ebox-render-to-buffer
(current-buffer) (ebox-layer-scroll-test--input 'fractional)) (current-buffer) (if (eq fixture 'layer)
(ebox-layer-scroll-test--input 'fractional)
(ebox-layer-scroll-test--ordinary-input)))
(when (eq fixture 'ordinary) (ebox-layer-scroll-test--materialize-root))
(let* ((runtime (ebox--buffer-render-state (current-buffer))) (let* ((runtime (ebox--buffer-render-state (current-buffer)))
(state (ebox-layer-scroll-test--root-state)) (state (ebox-layer-scroll-test--root-state))
(region-id (plist-get (plist-get state :box) :region-id)) (region-id (plist-get (plist-get state :box) :region-id))
(content (plist-get state :content-lines)) (content (plist-get state :content-lines))
(snapshot (ebox-surface-buffer-snapshot (current-buffer))) (snapshot (ebox-surface-buffer-snapshot (current-buffer)))
(revision (ebox-surface-buffer-revision (current-buffer))) (revision (ebox-surface-buffer-revision (current-buffer)))
(before (buffer-string))) (mounts (ebox-layer-publication-test--mount-signature (current-buffer)))
(let ((tp--surface-publication-step-function (before (buffer-string))
(lambda (step _surface) (owned (plist-get runtime :render-owned-text-values))
(when (eq step failure-step) (error "Reject cached root scroll"))))) (membership
(should-error (ebox--scroll-region-by region-id 1 1))) (let (properties)
(maphash
(lambda (property values)
(let (entries)
(maphash (lambda (value member) (push (cons value member) entries))
values)
(push (list property values entries) properties)))
owned)
properties))
rejected-step)
(ebox-layer-scroll-test--without-general-preparation
(let ((tp--surface-publication-step-function
(lambda (step _surface)
(when (eq step failure-step)
(setq rejected-step step)
(error "Reject cached root scroll")))))
(should-error (ebox--scroll-region-by region-id 1 1))))
(should (eq rejected-step failure-step))
(should (eq runtime (ebox--buffer-render-state (current-buffer)))) (should (eq runtime (ebox--buffer-render-state (current-buffer))))
(should (eq owned (plist-get runtime :render-owned-text-values)))
(should (= (hash-table-count owned) (length membership)))
(dolist (property membership)
(let ((values (gethash (car property) owned))
(entries (nth 2 property)))
(should (eq values (nth 1 property)))
(should (= (hash-table-count values) (length entries)))
(dolist (entry entries)
(should (eq (gethash (car entry) values 'missing) (cdr entry))))))
(should (eq state (ebox-layer-scroll-test--root-state))) (should (eq state (ebox-layer-scroll-test--root-state)))
(should (eq content (plist-get state :content-lines))) (should (eq content (plist-get state :content-lines)))
(let* ((after (ebox-surface-buffer-snapshot (current-buffer))) (let* ((after (ebox-surface-buffer-snapshot (current-buffer)))
@ -210,8 +306,85 @@
(should same-input) (should same-input)
(should (= (plist-get snapshot :mount-id) (plist-get after :mount-id)))) (should (= (plist-get snapshot :mount-id) (plist-get after :mount-id))))
(should (= revision (ebox-surface-buffer-revision (current-buffer)))) (should (= revision (ebox-surface-buffer-revision (current-buffer))))
(should (equal mounts
(ebox-layer-publication-test--mount-signature (current-buffer))))
(should (equal-including-properties before (buffer-string))) (should (equal-including-properties before (buffer-string)))
(should (= 1 (ebox--scroll-region-by region-id 1 1))) (ebox-layer-scroll-test--without-general-preparation
(should (= 1 (ebox--scroll-region-by region-id 1 1))))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(ebox-layer-scroll-test--assert-fresh)))))
(ert-deftest ebox-layer-scroll-reactive-width-change-renders-current-viewport ()
"A retained cached-scroll producer republishes current width without replay."
(ebox-layer-scroll-test--with-buffer
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--input 'fractional))
(ebox-region-update "root" :width '(vw 100))
(let* ((region (plist-get (plist-get (ebox-layer-scroll-test--root-state) :box)
:region-id))
(width (ebox-surface--signals-viewport-width ebox-surface--context-signals))
(publish (symbol-function 'ebox-surface--scroll-viewport-result))
(prepared-publications 0))
(cl-letf (((symbol-function 'ebox-surface--scroll-viewport-result)
(lambda (&rest arguments)
(cl-incf prepared-publications)
(apply publish arguments))))
(ebox-layer-scroll-test--without-general-preparation
(should (= 1 (ebox--scroll-region-by region 1 1))))
(should (= prepared-publications 1))
(let ((revision (ebox-surface-buffer-revision (current-buffer)))
(before (buffer-string)))
(should (= (tp-signal-subscriber-count width) 1))
(tp-with-transaction (tp-signal-set width 30))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(should (= (plist-get (ebox--buffer-render-state (current-buffer))
:viewport-width) 30))
(should (= (plist-get (ebox-layer-scroll-test--root-state) :scroll-offset) 1))
(should-not (equal-including-properties before (buffer-string)))
(should (= prepared-publications 1))
(ebox-layer-scroll-test--assert-fresh))))))
(ert-deftest ebox-layer-scroll-first-guard-miss-releases-batch-and-uses-current-axes ()
"A declined prepared producer releases its candidate and follows current axes."
(ebox-layer-scroll-test--with-buffer
(ebox-render-to-buffer (current-buffer) (ebox-layer-scroll-test--input 'fractional))
(ebox-region-update "root" :width '(vw 100))
(let* ((region (plist-get (plist-get (ebox-layer-scroll-test--root-state) :box)
:region-id))
(surface ebox-surface--buffer-surface)
(signals ebox-surface--context-signals)
(width (ebox-surface--signals-viewport-width signals))
(make-producer (symbol-function 'ebox-surface--scroll-viewport-producer))
(retired (make-hash-table :test #'eq :weakness 'key))
retained-producer)
(cl-letf (((symbol-function 'ebox-surface--scroll-viewport-producer)
(lambda (previous prepared context-signals)
(let ((declined (copy-sequence prepared)))
;; Invalidate only the captured certificate, before TP's
;; first evaluation, leaving the live display unchanged.
(plist-put declined :display-signature 'stale-display)
(puthash declined t retired)
(puthash (plist-get declined :scroll-viewport-batch) t retired)
(setq retained-producer
(funcall make-producer previous declined context-signals)))))
((symbol-function 'ebox-surface--scroll-viewport-result)
(lambda (&rest _) (ert-fail "Replayed a declined viewport batch"))))
(should (= 1 (ebox--scroll-region-by region 1 1))))
(should retained-producer)
(ebox-layer-scroll-test--assert-fresh)
(garbage-collect)
(should (= (hash-table-count retired) 0))
(ebox-region-update "root" :width '(ch 20))
(should (= (tp-signal-subscriber-count width) 0))
(let ((revision (ebox-surface-buffer-revision (current-buffer))))
(cl-letf (((symbol-function 'ebox-surface--scroll-viewport-result)
(lambda (&rest _) (ert-fail "Replayed a retired viewport batch"))))
(tp-surface-update surface retained-producer))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(should (= (tp-signal-subscriber-count width) 0))
(should (= (tp-signal-subscriber-count
(ebox-surface--signals-display signals)) 1))
(should (= (tp-signal-subscriber-count
(ebox-surface--signals-scroll signals)) 1))
(ebox-layer-scroll-test--assert-fresh))))) (ebox-layer-scroll-test--assert-fresh)))))
(ert-deftest ebox-layer-scroll-visible-root-dependencies-retain-composed-cache () (ert-deftest ebox-layer-scroll-visible-root-dependencies-retain-composed-cache ()
@ -322,11 +495,7 @@
(ebox-layer-scroll-test--with-buffer (ebox-layer-scroll-test--with-buffer
(ebox-render-to-buffer (ebox-render-to-buffer
(current-buffer) (current-buffer)
(ebox-build (ebox-layer-scroll-test--ordinary-input))
`(column :id "root" :width (vw 100) :height (vh 100) :overflow scroll
,@(cl-loop for index below 80
collect `(box :padding-left (px 1.5)
,(format "ROW%02d" index))))))
(let* ((runtime (ebox--buffer-render-state (current-buffer))) (let* ((runtime (ebox--buffer-render-state (current-buffer)))
(state (ebox-layer-scroll-test--root-state)) (state (ebox-layer-scroll-test--root-state))
(region-id (plist-get (plist-get state :box) :region-id))) (region-id (plist-get (plist-get state :box) :region-id)))
@ -339,10 +508,25 @@
(should (= 1 (ebox--scroll-region-by region-id 1 1))) (should (= 1 (ebox--scroll-region-by region-id 1 1)))
(should (= -1 (ebox--scroll-region-by region-id -1 1))) (should (= -1 (ebox--scroll-region-by region-id -1 1)))
(should (plist-get (ebox-layer-scroll-test--root-state) :rendered-content-lines)) (should (plist-get (ebox-layer-scroll-test--root-state) :rendered-content-lines))
(cl-letf (((symbol-function 'ebox-surface--render-candidate-node) (dolist (delta '(1 1 -1 -1))
(lambda (&rest _) (ert-fail "Cached ordinary scroll rendered wrapper")))) (let ((revision (ebox-surface-buffer-revision (current-buffer)))
(should (= 1 (ebox--scroll-region-by region-id 1 1)))) (before (buffer-string))
(ebox-layer-scroll-test--assert-fresh)))) (objects (copy-hash-table
(plist-get (ebox--buffer-render-state (current-buffer))
:surface-node-object-table))))
(ebox-layer-scroll-test--without-general-preparation
(cl-letf (((symbol-function 'ebox-surface--render-candidate-node)
(lambda (&rest _) (ert-fail "Cached ordinary scroll rendered wrapper"))))
(should (= delta (ebox--scroll-region-by region-id delta 1)))))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(should-not (equal-including-properties before (buffer-string)))
(maphash
(lambda (id object)
(should (eq object
(gethash id (plist-get (ebox--buffer-render-state (current-buffer))
:surface-node-object-table)))))
objects)
(ebox-layer-scroll-test--assert-fresh))))))
(provide 'ebox-layer-scroll-tests) (provide 'ebox-layer-scroll-tests)
;;; ebox-layer-scroll-tests.el ends here ;;; ebox-layer-scroll-tests.el ends here

View File

@ -387,6 +387,11 @@
:proposed-category disposable-cache :owner ebox-incremental :authority ebox :proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime buffer-runtime :rollback copy-on-candidate :lifetime buffer-runtime :rollback copy-on-candidate
:rebuild viewport-proof :cleanup clear-cache-or-buffer-kill) :rebuild viewport-proof :cleanup clear-cache-or-buffer-kill)
(:id buffer-runtime/scroll-viewport-owner-cache
:storage (:state-key :scroll-viewport-owner-cache)
:proposed-category disposable-cache :owner ebox-surface :authority ebox
:lifetime unchanged-topology-and-object-indexes :rollback copy-on-candidate
:rebuild rich-line-owner-resolution :cleanup ordinary-projection)
(:id buffer-runtime/render-owned-text-values (:id buffer-runtime/render-owned-text-values
:storage (:state-key :render-owned-text-values) :storage (:state-key :render-owned-text-values)
:proposed-category generation-bound-mutable :owner ebox-surface :authority ebox :proposed-category generation-bound-mutable :owner ebox-surface :authority ebox

View File

@ -10,6 +10,26 @@
(require 'cl-lib) (require 'cl-lib)
(require 'ebox) (require 'ebox)
(defmacro ebox-nested-scroll-test--without-general-preparation (&rest body)
"Run cached publication BODY without document preparation or surface scans."
(declare (indent 0) (debug t))
`(let ((stages '(ebox-incremental-prepare-scoped-commit
ebox-incremental--prepare-declarative-runtime
ebox--runtime-index ebox-surface--surface-plan))
counters advices)
(unwind-protect
(progn
(dolist (stage stages)
(let* ((counter (cons stage 0))
(advice (lambda (&rest _) (cl-incf (cdr counter)))))
(push counter counters)
(push (cons stage advice) advices)
(advice-add stage :before advice)))
,@body)
(dolist (entry advices) (advice-remove (car entry) (cdr entry))))
(should (equal (nreverse counters)
(mapcar (lambda (stage) (cons stage 0)) stages)))))
(defmacro ebox-nested-scroll-test--with-buffer (&rest body) (defmacro ebox-nested-scroll-test--with-buffer (&rest body)
"Run BODY with an isolated mounted surface and the Elisp renderer." "Run BODY with an isolated mounted surface and the Elisp renderer."
(declare (indent 0) (debug t)) (declare (indent 0) (debug t))
@ -87,7 +107,7 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(should (equal (substring-no-properties actual) (should (equal (substring-no-properties actual)
(buffer-substring-no-properties (point-min) (point-max)))) (buffer-substring-no-properties (point-min) (point-max))))
(dotimes (position (length actual)) (dotimes (position (length actual))
(dolist (property '(face display help-echo mouse-face pointer keymap)) (dolist (property '(face font-lock-face display help-echo mouse-face pointer keymap))
(should (equal (get-text-property position property actual) (should (equal (get-text-property position property actual)
(get-text-property (1+ position) property))))) (get-text-property (1+ position) property)))))
(should (equal mounts (ebox-nested-scroll-test--mounts)))) (should (equal mounts (ebox-nested-scroll-test--mounts))))
@ -193,10 +213,28 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(ebox-nested-scroll-test--parity) (ebox-nested-scroll-test--parity)
(let* ((state (ebox--buffer-render-state (current-buffer))) (let* ((state (ebox--buffer-render-state (current-buffer)))
(scroll (gethash right (plist-get state :scroll-state-table))) (scroll (gethash right (plist-get state :scroll-state-table)))
(lines (plist-get scroll :content-lines))) (lines (plist-get scroll :content-lines))
(cl-letf (((symbol-function 'ebox-surface--render-candidate) (revision (ebox-surface-buffer-revision (current-buffer)))
(lambda (&rest _) (ert-fail "Independent scroll rendered root")))) (objects (copy-hash-table (plist-get state :surface-node-object-table))))
(should (= (ebox--surface-scroll-region-by (current-buffer) left 1 nil) 1))) (dolist (delta '(1 1 -1))
(ebox-nested-scroll-test--without-general-preparation
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
(lambda (&rest _) (ert-fail "Independent scroll rendered root"))))
(should (= (ebox--surface-scroll-region-by
(current-buffer) left delta nil) delta))))
(cl-incf revision)
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
(let ((next (gethash right (plist-get (ebox--buffer-render-state
(current-buffer)) :scroll-state-table))))
(should (= (plist-get next :scroll-offset) 2))
(should (eq (plist-get next :content-lines) lines)))
(ebox-nested-scroll-test--parity))
(maphash
(lambda (id object)
(should (eq object
(gethash id (plist-get (ebox--buffer-render-state (current-buffer))
:surface-node-object-table)))))
objects)
(let ((next (gethash right (plist-get (ebox--buffer-render-state (let ((next (gethash right (plist-get (ebox--buffer-render-state
(current-buffer)) :scroll-state-table)))) (current-buffer)) :scroll-state-table))))
(should (= (plist-get next :scroll-offset) 2)) (should (= (plist-get next :scroll-offset) 2))
@ -204,10 +242,14 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(ebox-nested-scroll-test--parity) (ebox-nested-scroll-test--parity)
(ebox-region-update "line-1" :content "UPDATED") (ebox-region-update "line-1" :content "UPDATED")
(should (string-match-p "UPDATED" (buffer-string))) (should (string-match-p "UPDATED" (buffer-string)))
(should (= (plist-get (ebox--scroll-get-state right) :scroll-offset) 2))
(ebox-nested-scroll-test--parity) (ebox-nested-scroll-test--parity)
(ebox-rerender-buffer-with-context (current-buffer) 120 35) (ebox-rerender-buffer-with-context (current-buffer) 120 35)
(should (= (plist-get (ebox--scroll-get-state left) :scroll-offset) 1))
(should (= (plist-get (ebox--scroll-get-state right) :scroll-offset) 2))
(ebox-nested-scroll-test--parity) (ebox-nested-scroll-test--parity)
(should (= (ebox--surface-scroll-region-by (current-buffer) left -1 nil) -1)) (ebox-nested-scroll-test--without-general-preparation
(should (= (ebox--surface-scroll-region-by (current-buffer) left -1 nil) -1)))
(ebox-nested-scroll-test--parity)))) (ebox-nested-scroll-test--parity))))
(ert-deftest ebox-nested-scroll-rejection-restores-local-and-fallback-state () (ert-deftest ebox-nested-scroll-rejection-restores-local-and-fallback-state ()
@ -224,6 +266,7 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(before (buffer-string)) (before (buffer-string))
(mounts (ebox-nested-scroll-test--mounts t)) (mounts (ebox-nested-scroll-test--mounts t))
(revision (ebox-surface-buffer-revision (current-buffer))) (revision (ebox-surface-buffer-revision (current-buffer)))
rejected-step
(root-renders 0) (root-renders 0)
(render (symbol-function 'ebox-surface--render-candidate))) (render (symbol-function 'ebox-surface--render-candidate)))
(cl-letf (((symbol-function 'ebox-surface--render-candidate) (cl-letf (((symbol-function 'ebox-surface--render-candidate)
@ -231,9 +274,16 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(cl-incf root-renders) (apply render arguments)))) (cl-incf root-renders) (apply render arguments))))
(let ((tp--surface-publication-step-function (let ((tp--surface-publication-step-function
(lambda (step _surface) (lambda (step _surface)
(when (eq step 'client-state) (error "Reject nested scroll"))))) (when (eq step 'client-state)
(should-error (ebox--surface-scroll-region-by (setq rejected-step step)
(current-buffer) region 1 nil)))) (error "Reject nested scroll"))))
(attempt (lambda ()
(should-error (ebox--surface-scroll-region-by
(current-buffer) region 1 nil)))))
(if fallback (funcall attempt)
(ebox-nested-scroll-test--without-general-preparation
(funcall attempt)))))
(should (eq rejected-step 'client-state))
(should (eq state (ebox--buffer-render-state (current-buffer)))) (should (eq state (ebox--buffer-render-state (current-buffer))))
(should (= revision (ebox-surface-buffer-revision (current-buffer)))) (should (= revision (ebox-surface-buffer-revision (current-buffer))))
(should (equal-including-properties before (buffer-string))) (should (equal-including-properties before (buffer-string)))
@ -241,7 +291,11 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(should (= (plist-get scroll :scroll-offset) 0)) (should (= (plist-get scroll :scroll-offset) 0))
(should (= (or (ebox-get box :scroll-offset) 0) 0)) (should (= (or (ebox-get box :scroll-offset) 0) 0))
(should (if fallback (> root-renders 0) (= root-renders 0))) (should (if fallback (> root-renders 0) (= root-renders 0)))
(should (= (ebox--surface-scroll-region-by (current-buffer) region 1 nil) 1)) (if fallback
(should (= (ebox--surface-scroll-region-by (current-buffer) region 1 nil) 1))
(ebox-nested-scroll-test--without-general-preparation
(should (= (ebox--surface-scroll-region-by (current-buffer) region 1 nil) 1))))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(ebox-nested-scroll-test--parity))))) (ebox-nested-scroll-test--parity)))))
(ert-deftest ebox-nested-scroll-updates-enclosing-cache-and-role-membership () (ert-deftest ebox-nested-scroll-updates-enclosing-cache-and-role-membership ()
@ -253,12 +307,14 @@ PRESERVE-ORDER-P retains attachment order for exact rollback checks."
(outer (ebox-nested-scroll-test--region "root")) (outer (ebox-nested-scroll-test--region "root"))
(hidden (ebox-nested-scroll-test--region "line-0")) (hidden (ebox-nested-scroll-test--region "line-0"))
(visible (ebox-nested-scroll-test--region "line-5"))) (visible (ebox-nested-scroll-test--region "line-5")))
(cl-letf (((symbol-function 'ebox-surface--render-candidate) (ebox-nested-scroll-test--without-general-preparation
(lambda (&rest _) (ert-fail "Inner scroll rendered enclosing root")))) (cl-letf (((symbol-function 'ebox-surface--render-candidate)
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1))) (lambda (&rest _) (ert-fail "Inner scroll rendered enclosing root"))))
(should (= (ebox--surface-scroll-region-by (current-buffer) inner 1 nil) 1))))
(let* ((state (ebox--buffer-render-state (current-buffer))) (let* ((state (ebox--buffer-render-state (current-buffer)))
(scroll (gethash outer (plist-get state :scroll-state-table))) (scroll (gethash outer (plist-get state :scroll-state-table)))
(members (ebox--scroll-state-ensure-content-region-id-set scroll))) (members (ebox--scroll-state-ensure-content-region-id-set scroll)))
(should (= (plist-get scroll :scroll-offset) 0))
(should (eq (plist-get scroll :box) (plist-get state :root-node))) (should (eq (plist-get scroll :box) (plist-get state :root-node)))
(should-not (gethash hidden members)) (should-not (gethash hidden members))
(should (gethash visible members)) (should (gethash visible members))