refactor: separate viewport topology and scroll reuse proofs

This commit is contained in:
Kinneyzhang 2026-09-06 12:57:20 +08:00
parent 36883aefc7
commit 9e581aea2e
4 changed files with 429 additions and 78 deletions

View File

@ -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

View File

@ -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))))))))

View File

@ -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)))))))

View 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