refactor: separate viewport topology and scroll reuse proofs
This commit is contained in:
parent
36883aefc7
commit
9e581aea2e
7
Makefile
7
Makefile
@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml
|
|||||||
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
|
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
|
||||||
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
||||||
|
|
||||||
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
||||||
|
|
||||||
all: check
|
all: check
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ compile:
|
|||||||
rm -f *.elc tests/*.elc scripts/*.elc
|
rm -f *.elc tests/*.elc scripts/*.elc
|
||||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -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: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests
|
test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests
|
||||||
|
|
||||||
source-tests:
|
source-tests:
|
||||||
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
||||||
@ -63,6 +63,9 @@ dsl-tests:
|
|||||||
flex-tests:
|
flex-tests:
|
||||||
$(EMACS_TEST) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_TEST) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
viewport-topology-tests:
|
||||||
|
$(EMACS_TEST) -l tests/ebox-viewport-topology-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
m0a-tests:
|
m0a-tests:
|
||||||
$(EMACS_TEST) -l tests/ebox-m0a-inventory-fixture.el -l tests/ebox-m0a-inventory-tests.el -l tests/ebox-m0a-characterization-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_TEST) -l tests/ebox-m0a-inventory-fixture.el -l tests/ebox-m0a-inventory-tests.el -l tests/ebox-m0a-characterization-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
|||||||
@ -10671,21 +10671,17 @@ When RETAIN-NATIVE-P is non-nil, preserve its committed native frame bases."
|
|||||||
(defun ebox-incremental--viewport-reflow-safe-p
|
(defun ebox-incremental--viewport-reflow-safe-p
|
||||||
(old-state candidate-state dirty-set owner-plan style-context-stable-p)
|
(old-state candidate-state dirty-set owner-plan style-context-stable-p)
|
||||||
"Return non-nil when a viewport reflow can reuse the retained node tree.
|
"Return non-nil when a viewport reflow can reuse the retained node tree.
|
||||||
This strict projection retains only viewport-independent scroll producers."
|
Scroll producer and spatial reuse require their own independent proofs."
|
||||||
(and (ebox-incremental--viewport-root-proof-p
|
(ebox-incremental--viewport-root-proof-p
|
||||||
old-state candidate-state dirty-set owner-plan
|
old-state candidate-state dirty-set owner-plan style-context-stable-p))
|
||||||
style-context-stable-p)
|
|
||||||
(or (ebox-incremental--stable-scroll-state-p old-state)
|
|
||||||
(ebox-incremental--viewport-root-scroll-reflow-proof-p
|
|
||||||
old-state candidate-state style-context-stable-p))))
|
|
||||||
|
|
||||||
(defun ebox-incremental--viewport-root-scroll-reflow-proof-p
|
(defun ebox-incremental--viewport-root-scroll-reflow-proof-p
|
||||||
(old-state candidate-state style-context-stable-p)
|
(old-state candidate-state style-context-stable-p)
|
||||||
"Return non-nil for one resizable root scroll owner.
|
"Return non-nil for one resizable root scroll owner.
|
||||||
The root itself is the only scroll producer, so its viewport-dependent
|
The root itself is the only scroll producer, so its viewport-dependent
|
||||||
content is exactly the formatting context being reflowed. This proof does
|
content is exactly the formatting context being reflowed. This spatial reuse
|
||||||
not authorize nested scroll owners, visible overflow, cascade changes, or
|
proof does not authorize nested scroll owners, visible overflow, cascade
|
||||||
topology changes; those continue to use the conservative full projection."
|
changes, or topology changes."
|
||||||
(let* ((old-root (plist-get old-state :root-node))
|
(let* ((old-root (plist-get old-state :root-node))
|
||||||
(candidate-root (plist-get candidate-state :root-node))
|
(candidate-root (plist-get candidate-state :root-node))
|
||||||
(root-id (and old-root (plist-get old-root :node-id)))
|
(root-id (and old-root (plist-get old-root :node-id)))
|
||||||
@ -10723,44 +10719,20 @@ topology changes; those continue to use the conservative full projection."
|
|||||||
(plist-get old-state :parent-table)
|
(plist-get old-state :parent-table)
|
||||||
(plist-get candidate-state :parent-table)))))
|
(plist-get candidate-state :parent-table)))))
|
||||||
|
|
||||||
(defun ebox-incremental--stable-scroll-state-p (state)
|
|
||||||
"Return non-nil when STATE's scroll producers are safe to retain.
|
|
||||||
The proof is local to each scroll owner: its measured subtree must be
|
|
||||||
independent of both viewport axes, must not spill visible overflow, and must
|
|
||||||
have a published scroll state. A changing ancestor position is harmless;
|
|
||||||
the producer's content geometry is the retained boundary."
|
|
||||||
(let ((scroll-ids (plist-get state :scroll-region-ids))
|
|
||||||
(region-node-table (plist-get state :region-node-table))
|
|
||||||
(node-table (plist-get state :node-table))
|
|
||||||
(scroll-state-table (plist-get state :scroll-state-table)))
|
|
||||||
(or (null scroll-ids)
|
|
||||||
(and (hash-table-p region-node-table)
|
|
||||||
(hash-table-p node-table)
|
|
||||||
(hash-table-p scroll-state-table)
|
|
||||||
(cl-every
|
|
||||||
(lambda (region-id)
|
|
||||||
(let* ((node-id (gethash region-id region-node-table))
|
|
||||||
(node (and node-id (gethash node-id node-table))))
|
|
||||||
(and node
|
|
||||||
(gethash region-id scroll-state-table)
|
|
||||||
(not (ebox--viewport-dependent-subtree-p node))
|
|
||||||
(not (ebox--viewport-height-dependent-subtree-p node))
|
|
||||||
(not (ebox-tree-node-visible-overflow-p node)))))
|
|
||||||
scroll-ids)))))
|
|
||||||
|
|
||||||
(defun ebox-incremental--viewport-scroll-partition (state)
|
(defun ebox-incremental--viewport-scroll-partition (state)
|
||||||
"Partition STATE scroll owners into stable and viewport-affected sets.
|
"Partition STATE scroll owners into stable and viewport-affected sets.
|
||||||
A producer is stable only when its own subtree is independent of both
|
A producer is stable only when its own subtree is independent of both
|
||||||
viewport axes and has no visible overflow; an affected producer is rendered
|
viewport axes and has no visible overflow; an affected producer is rendered
|
||||||
fresh by the mixed projection."
|
fresh regardless of whether any stable producers exist."
|
||||||
(let ((region-node-table (plist-get state :region-node-table))
|
(let ((region-node-table (plist-get state :region-node-table))
|
||||||
(node-table (plist-get state :node-table))
|
(node-table (plist-get state :node-table))
|
||||||
(scroll-state-table (plist-get state :scroll-state-table))
|
(scroll-state-table (plist-get state :scroll-state-table))
|
||||||
(stable nil)
|
(stable nil)
|
||||||
(affected nil))
|
(affected nil))
|
||||||
(when (and (hash-table-p region-node-table)
|
(if (not (and (hash-table-p region-node-table)
|
||||||
(hash-table-p node-table)
|
(hash-table-p node-table)
|
||||||
(hash-table-p scroll-state-table))
|
(hash-table-p scroll-state-table)))
|
||||||
|
(setq affected (reverse (plist-get state :scroll-region-ids)))
|
||||||
(let ((ebox--viewport-dependent-node-ids-cache
|
(let ((ebox--viewport-dependent-node-ids-cache
|
||||||
(make-hash-table :test #'eq))
|
(make-hash-table :test #'eq))
|
||||||
(ebox--viewport-dependent-subtree-cache
|
(ebox--viewport-dependent-subtree-cache
|
||||||
@ -10778,23 +10750,12 @@ fresh by the mixed projection."
|
|||||||
(not (ebox-tree-node-visible-overflow-p node)))))
|
(not (ebox-tree-node-visible-overflow-p node)))))
|
||||||
(if stable-p
|
(if stable-p
|
||||||
(push region-id stable)
|
(push region-id stable)
|
||||||
(push region-id affected)))))
|
(push region-id affected))))))
|
||||||
(setq stable (nreverse stable)
|
(setq stable (nreverse stable)
|
||||||
affected (nreverse affected))
|
affected (nreverse affected))
|
||||||
(list :stable stable
|
(list :stable stable
|
||||||
:affected affected
|
:affected affected
|
||||||
:all (append stable affected)))))
|
:all (append stable affected))))
|
||||||
|
|
||||||
(defun ebox-incremental--viewport-mixed-scroll-proof-p
|
|
||||||
(old-state candidate-state dirty-set owner-plan partition
|
|
||||||
style-context-stable-p)
|
|
||||||
"Return PARTITION when a mixed viewport projection is proven safe."
|
|
||||||
(and (ebox-incremental--viewport-root-proof-p
|
|
||||||
old-state candidate-state dirty-set owner-plan
|
|
||||||
style-context-stable-p)
|
|
||||||
(plist-get partition :stable)
|
|
||||||
(plist-get partition :affected)
|
|
||||||
partition))
|
|
||||||
|
|
||||||
(defun ebox-incremental--viewport-reflow-state-overrides
|
(defun ebox-incremental--viewport-reflow-state-overrides
|
||||||
(old-state candidate-state &optional scroll-partition)
|
(old-state candidate-state &optional scroll-partition)
|
||||||
@ -10882,14 +10843,12 @@ fresh by the mixed projection."
|
|||||||
:target-viewport-width viewport-width
|
:target-viewport-width viewport-width
|
||||||
:target-viewport-height target-height)))
|
:target-viewport-height target-height)))
|
||||||
owner-plan report projection-kind scroll-partition
|
owner-plan report projection-kind scroll-partition
|
||||||
viewport-reflow-safe-p native-continuity-p)
|
viewport-reflow-safe-p spatial-reuse-safe-p native-continuity-p)
|
||||||
(plist-put candidate-state :viewport-width viewport-width)
|
(plist-put candidate-state :viewport-width viewport-width)
|
||||||
(plist-put candidate-state :viewport-height target-height)
|
(plist-put candidate-state :viewport-height target-height)
|
||||||
(plist-put candidate-state :display-signature display-signature)
|
(plist-put candidate-state :display-signature display-signature)
|
||||||
;; Fragment reuse is a consequence of the complete viewport-reflow
|
;; Retaining logical objects does not authorize spatial reuse.
|
||||||
;; proof below, not a proxy for "the display did not change". An
|
;; Affected producers still own fresh geometry and scroll effects.
|
||||||
;; owner-rerender can still change intrinsic measurements, scroll
|
|
||||||
;; dependencies, and generation-sensitive side effects.
|
|
||||||
(plist-put candidate-state :layout-fragments-reuse-p nil)
|
(plist-put candidate-state :layout-fragments-reuse-p nil)
|
||||||
(plist-put candidate-state :runtime-revision
|
(plist-put candidate-state :runtime-revision
|
||||||
(1+ (or (plist-get old-state :runtime-revision) 0)))
|
(1+ (or (plist-get old-state :runtime-revision) 0)))
|
||||||
@ -10910,8 +10869,15 @@ fresh by the mixed projection."
|
|||||||
(ebox-incremental--viewport-reflow-safe-p
|
(ebox-incremental--viewport-reflow-safe-p
|
||||||
old-state candidate-state dirty-set owner-plan
|
old-state candidate-state dirty-set owner-plan
|
||||||
style-context-stable-p)))
|
style-context-stable-p)))
|
||||||
|
(when viewport-reflow-safe-p
|
||||||
|
(setq scroll-partition
|
||||||
|
(ebox-incremental--viewport-scroll-partition candidate-state)
|
||||||
|
spatial-reuse-safe-p
|
||||||
|
(or (null (plist-get scroll-partition :affected))
|
||||||
|
(ebox-incremental--viewport-root-scroll-reflow-proof-p
|
||||||
|
old-state candidate-state style-context-stable-p))))
|
||||||
(setq native-continuity-p
|
(setq native-continuity-p
|
||||||
(and viewport-reflow-safe-p
|
(and viewport-reflow-safe-p spatial-reuse-safe-p
|
||||||
(require 'ebox-native-commit nil t)
|
(require 'ebox-native-commit nil t)
|
||||||
(ebox-native-commit-prepare-viewport-continuity
|
(ebox-native-commit-prepare-viewport-continuity
|
||||||
old-state candidate-state)))
|
old-state candidate-state)))
|
||||||
@ -10919,15 +10885,10 @@ fresh by the mixed projection."
|
|||||||
(cond
|
(cond
|
||||||
(native-continuity-p 'native-frame)
|
(native-continuity-p 'native-frame)
|
||||||
(viewport-reflow-safe-p
|
(viewport-reflow-safe-p
|
||||||
'viewport-reflow)
|
(if (and (plist-get scroll-partition :stable)
|
||||||
((and (not display-changed)
|
(plist-get scroll-partition :affected))
|
||||||
(setq scroll-partition
|
'viewport-reflow-mixed-scroll
|
||||||
(ebox-incremental--viewport-scroll-partition
|
'viewport-reflow))))
|
||||||
candidate-state))
|
|
||||||
(ebox-incremental--viewport-mixed-scroll-proof-p
|
|
||||||
old-state candidate-state dirty-set owner-plan
|
|
||||||
scroll-partition style-context-stable-p))
|
|
||||||
'viewport-reflow-mixed-scroll)))
|
|
||||||
(when native-continuity-p
|
(when native-continuity-p
|
||||||
(plist-put candidate-state :native-topology-stable-p t)
|
(plist-put candidate-state :native-topology-stable-p t)
|
||||||
(plist-put candidate-state :native-touched-node-ids nil)
|
(plist-put candidate-state :native-touched-node-ids nil)
|
||||||
@ -10939,8 +10900,13 @@ fresh by the mixed projection."
|
|||||||
(ebox-incremental--layout-owner-report
|
(ebox-incremental--layout-owner-report
|
||||||
prepared owner-plan candidate-state)
|
prepared owner-plan candidate-state)
|
||||||
report-overrides)))
|
report-overrides)))
|
||||||
(when (memq projection-kind
|
(when (and (memq projection-kind
|
||||||
'(viewport-reflow viewport-reflow-mixed-scroll))
|
'(viewport-reflow viewport-reflow-mixed-scroll))
|
||||||
|
;; Preserve the prior spatial permissions: all-stable,
|
||||||
|
;; sole root scroll, or mixed stable/affected producers.
|
||||||
|
;; Newly admitted all-affected topology gets none of these.
|
||||||
|
(or spatial-reuse-safe-p
|
||||||
|
(plist-get scroll-partition :stable)))
|
||||||
(plist-put candidate-state :layout-fragments-reuse-p t))
|
(plist-put candidate-state :layout-fragments-reuse-p t))
|
||||||
(when projection-kind
|
(when projection-kind
|
||||||
(setq report (plist-put report :projection-kind projection-kind))
|
(setq report (plist-put report :projection-kind projection-kind))
|
||||||
@ -10975,9 +10941,7 @@ fresh by the mixed projection."
|
|||||||
((memq projection-kind
|
((memq projection-kind
|
||||||
'(viewport-reflow viewport-reflow-mixed-scroll))
|
'(viewport-reflow viewport-reflow-mixed-scroll))
|
||||||
(ebox-incremental--viewport-reflow-state-overrides
|
(ebox-incremental--viewport-reflow-state-overrides
|
||||||
old-state candidate-state
|
old-state candidate-state scroll-partition))
|
||||||
(and (eq projection-kind 'viewport-reflow-mixed-scroll)
|
|
||||||
scroll-partition)))
|
|
||||||
(t
|
(t
|
||||||
(ebox-incremental--viewport-state-overrides
|
(ebox-incremental--viewport-state-overrides
|
||||||
candidate-state))))))))
|
candidate-state))))))))
|
||||||
|
|||||||
@ -2468,8 +2468,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-viewport-reflow-falls-back-for-scroll-and-visible-overflow ()
|
(ert-deftest ebox-viewport-reflow-falls-back-only-for-visible-overflow ()
|
||||||
"Nested scroll state and visible overflow must not enter retained reflow."
|
"Nested scroll retains topology; visible overflow still requires fallback."
|
||||||
(ebox-surface-test--with-elisp-backend
|
(ebox-surface-test--with-elisp-backend
|
||||||
(dolist (fixture
|
(dolist (fixture
|
||||||
(list
|
(list
|
||||||
@ -2504,8 +2504,19 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
|||||||
(apply original-ensure args))))
|
(apply original-ensure args))))
|
||||||
(ebox-rerender-buffer-with-context buffer 240 6))
|
(ebox-rerender-buffer-with-context buffer 240 6))
|
||||||
(let ((report (ebox-buffer-update-report buffer)))
|
(let ((report (ebox-buffer-update-report buffer)))
|
||||||
|
(if (eq (car fixture) 'visible-overflow)
|
||||||
|
(progn
|
||||||
(should (> ensured-node-count 0))
|
(should (> ensured-node-count 0))
|
||||||
(should-not (plist-get report :projection-kind))
|
(should-not (plist-get report :projection-kind)))
|
||||||
|
(should (= ensured-node-count 0))
|
||||||
|
(should (eq (plist-get report :projection-kind) 'viewport-reflow))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(ebox-viewport-width 240)
|
||||||
|
(ebox-viewport-height 6))
|
||||||
|
(should-not (plist-get state :layout-fragments-reuse-p))
|
||||||
|
(should (equal-including-properties
|
||||||
|
(with-current-buffer buffer (buffer-string))
|
||||||
|
(ebox-surface-test--render-runtime state)))))
|
||||||
(should (plist-get report :runtime-published))))
|
(should (plist-get report :runtime-published))))
|
||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))))
|
(kill-buffer buffer)))))))
|
||||||
|
|||||||
373
tests/ebox-viewport-topology-tests.el
Normal file
373
tests/ebox-viewport-topology-tests.el
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
;;; ebox-viewport-topology-tests.el --- Viewport topology contracts -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ert)
|
||||||
|
|
||||||
|
(setq load-prefer-newer t)
|
||||||
|
(load-file (expand-file-name "../ebox.el"
|
||||||
|
(file-name-directory load-file-name)))
|
||||||
|
(require 'tp-surface)
|
||||||
|
|
||||||
|
(defun ebox-viewport-topology-test--nested-scroll (map &optional count)
|
||||||
|
"Build one ordinary nested scroll owner with interactive MAP content."
|
||||||
|
(ebox-build
|
||||||
|
`(column :key root :width (viewport)
|
||||||
|
(text "Header")
|
||||||
|
(column :key body :id "body" :width stretch
|
||||||
|
:height (viewport-height) :overflow scroll
|
||||||
|
:padding (0 (2))
|
||||||
|
,@(cl-loop
|
||||||
|
for index below (or count 12)
|
||||||
|
collect
|
||||||
|
`(box :key ,(format "row-%d" index) :width stretch
|
||||||
|
:wrap-mode char
|
||||||
|
(text ,(propertize
|
||||||
|
(format "Open %02d %s" index (make-string 130 ?x))
|
||||||
|
'keymap map 'mouse-face 'highlight
|
||||||
|
'help-echo "Open this row"
|
||||||
|
'face '(:foreground "#123456"))))))
|
||||||
|
(text "Footer"))))
|
||||||
|
|
||||||
|
(defun ebox-viewport-topology-test--fresh-output (input state width height &optional offset)
|
||||||
|
"Freshly mount INPUT and map only its region identities to matching STATE."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-viewport-fresh*"))
|
||||||
|
(ebox-viewport-width width)
|
||||||
|
(ebox-viewport-height height)
|
||||||
|
(regions-by-source (make-hash-table :test 'eq))
|
||||||
|
(region-map (make-hash-table :test 'equal)))
|
||||||
|
(maphash (lambda (_ node)
|
||||||
|
(puthash (ebox-node-source-handle node)
|
||||||
|
(plist-get node :region-id) regions-by-source))
|
||||||
|
(plist-get state :node-table))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer buffer input)
|
||||||
|
(when offset
|
||||||
|
(let ((region-id (car (plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:scroll-region-ids))))
|
||||||
|
(dotimes (_ offset)
|
||||||
|
(ebox--surface-scroll-region-by buffer region-id 1 nil))))
|
||||||
|
(maphash (lambda (_ node)
|
||||||
|
(let ((region (gethash (ebox-node-source-handle node)
|
||||||
|
regions-by-source)))
|
||||||
|
(should region)
|
||||||
|
(puthash (plist-get node :region-id) region region-map)))
|
||||||
|
(plist-get (ebox--buffer-render-state buffer) :node-table))
|
||||||
|
(let ((output (with-current-buffer buffer (buffer-string)))
|
||||||
|
(properties (cons 'ebox-scroll-window (mapcar #'cdr ebox-region-types)))
|
||||||
|
(position 0))
|
||||||
|
(while (< position (length output))
|
||||||
|
(let ((next (or (next-property-change position output) (length output))))
|
||||||
|
(dolist (property properties)
|
||||||
|
(when-let* ((region (get-text-property position property output)))
|
||||||
|
(should (gethash region region-map))
|
||||||
|
(put-text-property position next property
|
||||||
|
(gethash region region-map) output)))
|
||||||
|
(when-let* ((owners (get-text-property position 'ebox-content-owners output)))
|
||||||
|
(put-text-property
|
||||||
|
position next 'ebox-content-owners
|
||||||
|
(mapcar (lambda (region)
|
||||||
|
(should (gethash region region-map))
|
||||||
|
(gethash region region-map)) owners)
|
||||||
|
output))
|
||||||
|
(setq position next)))
|
||||||
|
output))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(defun ebox-viewport-topology-test--resize (width height axes &optional count)
|
||||||
|
"Resize the same nested-scroll topology to WIDTH and HEIGHT on AXES."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-viewport-topology*"))
|
||||||
|
(map (make-sparse-keymap))
|
||||||
|
(ebox-viewport-width 120)
|
||||||
|
(ebox-viewport-height 4)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||||
|
(ensure-count 0)
|
||||||
|
(original-ensure (symbol-function 'ebox-surface--ensure-node-tree)))
|
||||||
|
(define-key map [mouse-1] #'ignore)
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(let* ((input (ebox-viewport-topology-test--nested-scroll map count))
|
||||||
|
(_mounted (ebox-render-to-buffer buffer input))
|
||||||
|
(old-state (ebox--buffer-render-state buffer))
|
||||||
|
(old-objects (copy-hash-table
|
||||||
|
(plist-get old-state :surface-node-object-table)))
|
||||||
|
(old-text (with-current-buffer buffer (buffer-string)))
|
||||||
|
(scroll-ids (plist-get old-state :scroll-region-ids)))
|
||||||
|
;; This is the all-affected case: one nested owner, no stable peer.
|
||||||
|
(should (= (length scroll-ids) 1))
|
||||||
|
(should-not (equal (car scroll-ids)
|
||||||
|
(plist-get (plist-get old-state :root-node) :region-id)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
|
||||||
|
(lambda (&rest args)
|
||||||
|
(cl-incf ensure-count)
|
||||||
|
(apply original-ensure args))))
|
||||||
|
(ebox-rerender-buffer-with-context buffer width height))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(report (ebox-buffer-update-report buffer))
|
||||||
|
(surface (plist-get state :surface))
|
||||||
|
(tp-report (tp-surface-report surface))
|
||||||
|
(objects (plist-get state :surface-node-object-table))
|
||||||
|
(actual (with-current-buffer buffer (buffer-string)))
|
||||||
|
(expected (ebox-viewport-topology-test--fresh-output
|
||||||
|
input state width height))
|
||||||
|
(position (string-match "Open 00" actual)))
|
||||||
|
(should (eq (plist-get report :viewport-axes) axes))
|
||||||
|
(should (ebox-host-ref-position
|
||||||
|
buffer (ebox-canonical-input-root-host-ref input)))
|
||||||
|
(should-not (plist-get state :layout-fragments-reuse-p))
|
||||||
|
(should-not (plist-get report :viewport-scroll-stable-ids))
|
||||||
|
(should (equal (plist-get report :viewport-scroll-affected-ids) scroll-ids))
|
||||||
|
(should-not (equal-including-properties old-text actual))
|
||||||
|
(should (equal-including-properties actual expected))
|
||||||
|
(should position)
|
||||||
|
(should (eq (lookup-key (get-text-property position 'keymap actual)
|
||||||
|
[mouse-1]) #'ignore))
|
||||||
|
(should (equal (get-text-property position 'help-echo actual)
|
||||||
|
"Open this row"))
|
||||||
|
(should (eq (get-text-property position 'mouse-face actual) 'highlight))
|
||||||
|
(should (= (hash-table-count old-objects) (hash-table-count objects)))
|
||||||
|
(maphash (lambda (id object) (should (eq object (gethash id objects))))
|
||||||
|
old-objects)
|
||||||
|
(should (= (plist-get tp-report :created-objects) 0))
|
||||||
|
(should (= (plist-get tp-report :removed-objects) 0))
|
||||||
|
(should (= (plist-get tp-report :moved-objects) 0))
|
||||||
|
;; Producer freshness does not require reconciling stable topology.
|
||||||
|
(ert-info ((format "axes=%S ensure=%S reconciled=%S objects=%S"
|
||||||
|
axes ensure-count
|
||||||
|
(plist-get tp-report :reconciled-objects)
|
||||||
|
(hash-table-count objects)))
|
||||||
|
(should (= ensure-count 0))
|
||||||
|
(should (<= (plist-get tp-report :reconciled-objects) 4))))))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-nested-scroll-width ()
|
||||||
|
(ebox-viewport-topology-test--resize 200 4 'width))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-nested-scroll-height ()
|
||||||
|
(ebox-viewport-topology-test--resize 120 7 'height))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-nested-scroll-both ()
|
||||||
|
(ebox-viewport-topology-test--resize 200 7 'both))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-work-does-not-scale-with-children ()
|
||||||
|
(ebox-viewport-topology-test--resize 200 4 'width 48))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-unproven-producers-remain-affected ()
|
||||||
|
"Missing producer indexes cannot authorize spatial or producer reuse."
|
||||||
|
(let ((partition (ebox-incremental--viewport-scroll-partition
|
||||||
|
'(:scroll-region-ids (11 22)))))
|
||||||
|
(should-not (plist-get partition :stable))
|
||||||
|
(should (equal (plist-get partition :affected) '(11 22)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-mixed-scroll-preserves-only-stable-producer ()
|
||||||
|
"Existing mixed producer preservation remains narrower than topology reuse."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-viewport-mixed-control*"))
|
||||||
|
(ebox-viewport-width 120) (ebox-viewport-height 4)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-build
|
||||||
|
'(column :width (viewport)
|
||||||
|
(box :id "stable" :width (80) :height 2 :overflow scroll
|
||||||
|
(text "stable zero\nstable one\nstable two"))
|
||||||
|
(box :id "affected" :width stretch :height (viewport-height)
|
||||||
|
:overflow scroll :wrap-mode char
|
||||||
|
(text "affected zero\naffected one\naffected two\naffected three\naffected four")))))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(partition (ebox-incremental--viewport-scroll-partition state))
|
||||||
|
(stable-id (car (plist-get partition :stable)))
|
||||||
|
(affected-id (car (plist-get partition :affected)))
|
||||||
|
(table (plist-get state :scroll-state-table))
|
||||||
|
(stable-lines (plist-get (gethash stable-id table) :content-lines))
|
||||||
|
(affected-lines (plist-get (gethash affected-id table) :content-lines)))
|
||||||
|
(should stable-id)
|
||||||
|
(should affected-id)
|
||||||
|
(ebox-rerender-buffer-with-context buffer 200 5)
|
||||||
|
(let* ((new-state (ebox--buffer-render-state buffer))
|
||||||
|
(new-table (plist-get new-state :scroll-state-table))
|
||||||
|
(report (ebox-buffer-update-report buffer)))
|
||||||
|
(should (eq (plist-get report :projection-kind) 'viewport-reflow-mixed-scroll))
|
||||||
|
(should (plist-get new-state :layout-fragments-reuse-p))
|
||||||
|
(should (equal (plist-get report :viewport-scroll-stable-ids) (list stable-id)))
|
||||||
|
(should (eq stable-lines (plist-get (gethash stable-id new-table) :content-lines)))
|
||||||
|
(should-not (eq affected-lines (plist-get (gethash affected-id new-table) :content-lines))))))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-affected-scroll-round-trip ()
|
||||||
|
"Affected producers reach their fresh end after width A-to-B-to-A."
|
||||||
|
(let* ((buffer (generate-new-buffer " *ebox-viewport-scroll-round-trip*"))
|
||||||
|
(input (ebox-viewport-topology-test--nested-scroll (make-sparse-keymap)))
|
||||||
|
(ebox-viewport-width 120) (ebox-viewport-height 4)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-render-to-buffer buffer input)
|
||||||
|
(let* ((initial (with-current-buffer buffer (buffer-string)))
|
||||||
|
(region-id (car (plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:scroll-region-ids))))
|
||||||
|
(dolist (width '(200 120))
|
||||||
|
(ebox-rerender-buffer-with-context buffer width 4)
|
||||||
|
(should (<= (plist-get (ebox-buffer-update-report buffer)
|
||||||
|
:reconciled-objects) 4))
|
||||||
|
;; Small foreground steps synchronously extend a lazy prefix.
|
||||||
|
;; A large absolute jump may intentionally stop at a cold prefix.
|
||||||
|
(cl-loop repeat 32
|
||||||
|
for scroll = (gethash region-id
|
||||||
|
(plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:scroll-state-table))
|
||||||
|
until (and (plist-get scroll :content-lines-complete-p)
|
||||||
|
(= (plist-get scroll :scroll-offset)
|
||||||
|
(- (length (plist-get scroll :content-lines))
|
||||||
|
(plist-get scroll :content-height))))
|
||||||
|
do (ebox--surface-scroll-region-by buffer region-id 1 nil))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(scroll (gethash region-id (plist-get state :scroll-state-table)))
|
||||||
|
(actual (with-current-buffer buffer (buffer-string)))
|
||||||
|
(offset (plist-get scroll :scroll-offset)))
|
||||||
|
(should (plist-get scroll :content-lines-complete-p))
|
||||||
|
(should (= offset (max 0 (- (length (plist-get scroll :content-lines))
|
||||||
|
(plist-get scroll :content-height)))))
|
||||||
|
(should (> offset 0))
|
||||||
|
(should (string-match-p "Open 11" actual))
|
||||||
|
(should (equal-including-properties
|
||||||
|
actual (ebox-viewport-topology-test--fresh-output
|
||||||
|
input state width 4 offset))))
|
||||||
|
(ebox--surface-scroll-to-offset buffer region-id 0))
|
||||||
|
(should (equal-including-properties
|
||||||
|
initial (with-current-buffer buffer (buffer-string))))))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-affected-scroll-failure-rolls-back ()
|
||||||
|
"Failed all-affected reflow keeps committed identities, producer and timer."
|
||||||
|
(let* ((buffer (generate-new-buffer " *ebox-viewport-scroll-failure*"))
|
||||||
|
(input (ebox-viewport-topology-test--nested-scroll (make-sparse-keymap)))
|
||||||
|
(ebox-viewport-width 120) (ebox-viewport-height 4)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||||
|
timer)
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-render-to-buffer buffer input)
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(surface (plist-get state :surface))
|
||||||
|
(revision (tp-surface-revision surface))
|
||||||
|
(contents (with-current-buffer buffer (buffer-string)))
|
||||||
|
(region-id (car (plist-get state :scroll-region-ids)))
|
||||||
|
(scroll (gethash region-id (plist-get state :scroll-state-table)))
|
||||||
|
(lines (plist-get scroll :content-lines))
|
||||||
|
(producer (plist-get scroll :render-content-prefix))
|
||||||
|
(host-ref (ebox-canonical-input-root-host-ref input))
|
||||||
|
(host-position (ebox-host-ref-position buffer host-ref))
|
||||||
|
(host-bounds (ebox-host-ref-bounds buffer host-ref))
|
||||||
|
(indexes (mapcar (lambda (key) (plist-get state key))
|
||||||
|
'(:node-table :parent-table :region-node-table
|
||||||
|
:surface-node-object-table)))
|
||||||
|
(index-contents
|
||||||
|
(mapcar (lambda (table)
|
||||||
|
(let ((copy (copy-hash-table table)))
|
||||||
|
(maphash (lambda (key value)
|
||||||
|
(when (listp value)
|
||||||
|
(puthash key (copy-sequence value) copy)))
|
||||||
|
table)
|
||||||
|
copy))
|
||||||
|
indexes)))
|
||||||
|
(should host-position)
|
||||||
|
(ebox--scroll-cancel-idle-prefetch region-id)
|
||||||
|
(setq timer (run-at-time 3600 nil #'ignore))
|
||||||
|
(puthash region-id timer ebox--scroll-idle-prefetch-timers)
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state)
|
||||||
|
(error "Reject all-affected viewport publication")))))
|
||||||
|
(should-error (ebox-rerender-buffer-with-context buffer 200 7)))
|
||||||
|
(should (eq state (ebox--buffer-render-state buffer)))
|
||||||
|
(should (eq state (tp-surface-client-state surface)))
|
||||||
|
(should (= revision (tp-surface-revision surface)))
|
||||||
|
(should (equal-including-properties
|
||||||
|
contents (with-current-buffer buffer (buffer-string))))
|
||||||
|
(should (eq scroll (gethash region-id (plist-get state :scroll-state-table))))
|
||||||
|
(should (eq lines (plist-get scroll :content-lines)))
|
||||||
|
(should (eq producer (plist-get scroll :render-content-prefix)))
|
||||||
|
(should (eq timer (gethash region-id ebox--scroll-idle-prefetch-timers)))
|
||||||
|
(should (memq timer timer-list))
|
||||||
|
(should (equal host-position (ebox-host-ref-position buffer host-ref)))
|
||||||
|
(should (equal host-bounds (ebox-host-ref-bounds buffer host-ref)))
|
||||||
|
(cl-mapc (lambda (table key) (should (eq table (plist-get state key))))
|
||||||
|
indexes '(:node-table :parent-table :region-node-table
|
||||||
|
:surface-node-object-table))
|
||||||
|
(cl-mapc (lambda (before after)
|
||||||
|
(should (= (hash-table-count before) (hash-table-count after)))
|
||||||
|
(maphash (lambda (key value)
|
||||||
|
(should (equal-including-properties
|
||||||
|
value (gethash key after))))
|
||||||
|
before))
|
||||||
|
index-contents indexes)
|
||||||
|
(ebox-rerender-buffer-with-context buffer 200 7)
|
||||||
|
(should (ebox-host-ref-position buffer host-ref))
|
||||||
|
(should (equal-including-properties
|
||||||
|
(with-current-buffer buffer (buffer-string))
|
||||||
|
(ebox-viewport-topology-test--fresh-output
|
||||||
|
input (ebox--buffer-render-state buffer) 200 7)))))
|
||||||
|
(when (timerp timer) (cancel-timer timer))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-viewport-topology-retains-public-range-and-host-addresses ()
|
||||||
|
"An integration Range and its child Host remain addressable after resize."
|
||||||
|
(let* ((buffer (generate-new-buffer " *ebox-viewport-range*"))
|
||||||
|
(base (ebox-viewport-topology-test--nested-scroll (make-sparse-keymap)))
|
||||||
|
(builder (ebox-source-builder-create))
|
||||||
|
(handle (ebox-source-builder-bind builder :identity 'range-root))
|
||||||
|
(range-ref (make-symbol "viewport-range"))
|
||||||
|
(children (ebox-canonical-input-import-roots
|
||||||
|
base (ebox-canonical-input-roots base) builder))
|
||||||
|
(input (ebox-canonical-input-create
|
||||||
|
(list (ebox-box-create
|
||||||
|
:source-handle handle :source-builder builder
|
||||||
|
:owned-facts (ebox-canonical-facts-from-declarations
|
||||||
|
'box '(ebox/width (viewport)))
|
||||||
|
:layout (ebox-column-layout-create)
|
||||||
|
:children (list (apply #'ebox-child-range range-ref children))))
|
||||||
|
(ebox-source-builder-finish builder)))
|
||||||
|
(host-ref (ebox-canonical-input-root-host-ref base))
|
||||||
|
(ebox-viewport-width 120) (ebox-viewport-height 4)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(ebox-render-to-buffer buffer input)
|
||||||
|
(should (ebox-range-ref-present-p buffer range-ref))
|
||||||
|
(should (ebox-host-ref-position buffer host-ref))
|
||||||
|
(let ((objects (copy-hash-table
|
||||||
|
(plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:surface-node-object-table))))
|
||||||
|
(ebox-rerender-buffer-with-context buffer 200 7)
|
||||||
|
(let ((state (ebox--buffer-render-state buffer)))
|
||||||
|
(maphash (lambda (id object)
|
||||||
|
(should (eq object (gethash id (plist-get state :surface-node-object-table)))))
|
||||||
|
objects)
|
||||||
|
(should (equal-including-properties
|
||||||
|
(with-current-buffer buffer (buffer-string))
|
||||||
|
(ebox-viewport-topology-test--fresh-output input state 200 7)))))
|
||||||
|
(should (ebox-range-ref-present-p buffer range-ref))
|
||||||
|
(should (ebox-host-ref-position buffer host-ref))
|
||||||
|
(should (<= (plist-get (ebox-buffer-update-report buffer) :reconciled-objects) 4))
|
||||||
|
(let ((candidate (ebox-candidate-begin buffer)))
|
||||||
|
(ebox-candidate-replace-range-ref
|
||||||
|
candidate range-ref (ebox-build '(text "Range replaced")))
|
||||||
|
(ebox-commit buffer candidate))
|
||||||
|
(should (ebox-range-ref-present-p buffer range-ref))
|
||||||
|
(should (string-match-p "Range replaced" (with-current-buffer buffer (buffer-string)))))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(provide 'ebox-viewport-topology-tests)
|
||||||
|
;;; ebox-viewport-topology-tests.el ends here
|
||||||
Loading…
Reference in New Issue
Block a user