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_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
|
||||
|
||||
@ -25,7 +25,7 @@ compile:
|
||||
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)'
|
||||
|
||||
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:
|
||||
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
||||
@ -63,6 +63,9 @@ dsl-tests:
|
||||
flex-tests:
|
||||
$(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:
|
||||
$(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
|
||||
(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.
|
||||
This strict projection retains only viewport-independent scroll producers."
|
||||
(and (ebox-incremental--viewport-root-proof-p
|
||||
old-state candidate-state dirty-set owner-plan
|
||||
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))))
|
||||
Scroll producer and spatial reuse require their own independent proofs."
|
||||
(ebox-incremental--viewport-root-proof-p
|
||||
old-state candidate-state dirty-set owner-plan style-context-stable-p))
|
||||
|
||||
(defun ebox-incremental--viewport-root-scroll-reflow-proof-p
|
||||
(old-state candidate-state style-context-stable-p)
|
||||
"Return non-nil for one resizable root scroll owner.
|
||||
The root itself is the only scroll producer, so its viewport-dependent
|
||||
content is exactly the formatting context being reflowed. This proof does
|
||||
not authorize nested scroll owners, visible overflow, cascade changes, or
|
||||
topology changes; those continue to use the conservative full projection."
|
||||
content is exactly the formatting context being reflowed. This spatial reuse
|
||||
proof does not authorize nested scroll owners, visible overflow, cascade
|
||||
changes, or topology changes."
|
||||
(let* ((old-root (plist-get old-state :root-node))
|
||||
(candidate-root (plist-get candidate-state :root-node))
|
||||
(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 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)
|
||||
"Partition STATE scroll owners into stable and viewport-affected sets.
|
||||
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
|
||||
fresh by the mixed projection."
|
||||
fresh regardless of whether any stable producers exist."
|
||||
(let ((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))
|
||||
(stable nil)
|
||||
(affected nil))
|
||||
(when (and (hash-table-p region-node-table)
|
||||
(hash-table-p node-table)
|
||||
(hash-table-p scroll-state-table))
|
||||
(if (not (and (hash-table-p region-node-table)
|
||||
(hash-table-p node-table)
|
||||
(hash-table-p scroll-state-table)))
|
||||
(setq affected (reverse (plist-get state :scroll-region-ids)))
|
||||
(let ((ebox--viewport-dependent-node-ids-cache
|
||||
(make-hash-table :test #'eq))
|
||||
(ebox--viewport-dependent-subtree-cache
|
||||
@ -10778,23 +10750,12 @@ fresh by the mixed projection."
|
||||
(not (ebox-tree-node-visible-overflow-p node)))))
|
||||
(if stable-p
|
||||
(push region-id stable)
|
||||
(push region-id affected)))))
|
||||
(push region-id affected))))))
|
||||
(setq stable (nreverse stable)
|
||||
affected (nreverse affected))
|
||||
(list :stable stable
|
||||
:affected 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))
|
||||
:all (append stable affected))))
|
||||
|
||||
(defun ebox-incremental--viewport-reflow-state-overrides
|
||||
(old-state candidate-state &optional scroll-partition)
|
||||
@ -10882,14 +10843,12 @@ fresh by the mixed projection."
|
||||
:target-viewport-width viewport-width
|
||||
:target-viewport-height target-height)))
|
||||
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-height target-height)
|
||||
(plist-put candidate-state :display-signature display-signature)
|
||||
;; Fragment reuse is a consequence of the complete viewport-reflow
|
||||
;; proof below, not a proxy for "the display did not change". An
|
||||
;; owner-rerender can still change intrinsic measurements, scroll
|
||||
;; dependencies, and generation-sensitive side effects.
|
||||
;; Retaining logical objects does not authorize spatial reuse.
|
||||
;; Affected producers still own fresh geometry and scroll effects.
|
||||
(plist-put candidate-state :layout-fragments-reuse-p nil)
|
||||
(plist-put candidate-state :runtime-revision
|
||||
(1+ (or (plist-get old-state :runtime-revision) 0)))
|
||||
@ -10910,8 +10869,15 @@ fresh by the mixed projection."
|
||||
(ebox-incremental--viewport-reflow-safe-p
|
||||
old-state candidate-state dirty-set owner-plan
|
||||
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
|
||||
(and viewport-reflow-safe-p
|
||||
(and viewport-reflow-safe-p spatial-reuse-safe-p
|
||||
(require 'ebox-native-commit nil t)
|
||||
(ebox-native-commit-prepare-viewport-continuity
|
||||
old-state candidate-state)))
|
||||
@ -10919,15 +10885,10 @@ fresh by the mixed projection."
|
||||
(cond
|
||||
(native-continuity-p 'native-frame)
|
||||
(viewport-reflow-safe-p
|
||||
'viewport-reflow)
|
||||
((and (not display-changed)
|
||||
(setq scroll-partition
|
||||
(ebox-incremental--viewport-scroll-partition
|
||||
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)))
|
||||
(if (and (plist-get scroll-partition :stable)
|
||||
(plist-get scroll-partition :affected))
|
||||
'viewport-reflow-mixed-scroll
|
||||
'viewport-reflow))))
|
||||
(when native-continuity-p
|
||||
(plist-put candidate-state :native-topology-stable-p t)
|
||||
(plist-put candidate-state :native-touched-node-ids nil)
|
||||
@ -10939,8 +10900,13 @@ fresh by the mixed projection."
|
||||
(ebox-incremental--layout-owner-report
|
||||
prepared owner-plan candidate-state)
|
||||
report-overrides)))
|
||||
(when (memq projection-kind
|
||||
'(viewport-reflow viewport-reflow-mixed-scroll))
|
||||
(when (and (memq projection-kind
|
||||
'(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))
|
||||
(when projection-kind
|
||||
(setq report (plist-put report :projection-kind projection-kind))
|
||||
@ -10975,9 +10941,7 @@ fresh by the mixed projection."
|
||||
((memq projection-kind
|
||||
'(viewport-reflow viewport-reflow-mixed-scroll))
|
||||
(ebox-incremental--viewport-reflow-state-overrides
|
||||
old-state candidate-state
|
||||
(and (eq projection-kind 'viewport-reflow-mixed-scroll)
|
||||
scroll-partition)))
|
||||
old-state candidate-state scroll-partition))
|
||||
(t
|
||||
(ebox-incremental--viewport-state-overrides
|
||||
candidate-state))))))))
|
||||
|
||||
@ -2468,8 +2468,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-viewport-reflow-falls-back-for-scroll-and-visible-overflow ()
|
||||
"Nested scroll state and visible overflow must not enter retained reflow."
|
||||
(ert-deftest ebox-viewport-reflow-falls-back-only-for-visible-overflow ()
|
||||
"Nested scroll retains topology; visible overflow still requires fallback."
|
||||
(ebox-surface-test--with-elisp-backend
|
||||
(dolist (fixture
|
||||
(list
|
||||
@ -2504,8 +2504,19 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(apply original-ensure args))))
|
||||
(ebox-rerender-buffer-with-context buffer 240 6))
|
||||
(let ((report (ebox-buffer-update-report buffer)))
|
||||
(should (> ensured-node-count 0))
|
||||
(should-not (plist-get report :projection-kind))
|
||||
(if (eq (car fixture) 'visible-overflow)
|
||||
(progn
|
||||
(should (> ensured-node-count 0))
|
||||
(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))))
|
||||
(when (buffer-live-p 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