diff --git a/Makefile b/Makefile index a60f46d..275adaa 100644 --- a/Makefile +++ b/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 diff --git a/ebox-incremental.el b/ebox-incremental.el index 7910bb9..17ba131 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -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)))))))) diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index a17c6f4..c3a1975 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -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))))))) diff --git a/tests/ebox-viewport-topology-tests.el b/tests/ebox-viewport-topology-tests.el new file mode 100644 index 0000000..f980aeb --- /dev/null +++ b/tests/ebox-viewport-topology-tests.el @@ -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