diff --git a/ebox-incremental.el b/ebox-incremental.el index 4632f05..4d17191 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -9824,6 +9824,10 @@ role, and outside-complement compatibility before publication." (setq overrides (plist-put overrides :native-removed-node-ids (plist-get prepared :removed-node-ids))) + (setq overrides + (plist-put overrides :native-inherited-dirty-node-ids + (plist-get candidate-state + :native-inherited-dirty-node-ids))) (setq overrides (plist-put overrides :native-topology-stable-p diff --git a/ebox-native-commit.el b/ebox-native-commit.el index 73829db..ffb9824 100644 --- a/ebox-native-commit.el +++ b/ebox-native-commit.el @@ -32,6 +32,7 @@ (declare-function ebox--literal-root-pixel-width "ebox" (box)) (declare-function ebox-get "ebox" (box key)) (declare-function ebox-style-cascade-active-p "ebox-style" ()) +(declare-function ebox-style-property "ebox-style" (name)) (declare-function ebox-tree-node-key "ebox-tree" (source-index node)) (declare-function ebox-tree--children-raw "ebox-tree" (node)) (declare-function ebox--node-direct-viewport-width-dependent-p @@ -105,6 +106,17 @@ (setq node-id (and parents (gethash node-id parents)))))) (nreverse result))) +(defun ebox-native-commit-inherited-dirty-node-ids (prepared) + "Return nodes whose PREPARED changes can alter descendant computed style." + (cl-loop + for entry in (plist-get prepared :dirty-set) + when (cl-some + (lambda (key) + (when-let* ((property (ebox-style-property key))) + (plist-get property :inherits))) + (plist-get entry :changed-keys)) + collect (plist-get entry :node-id))) + (defun ebox-native-commit--node-position-table (state) "Return NODE-ID to `(PARENT-ID . POSITION)' table for STATE." (let ((nodes (plist-get state :node-table)) @@ -325,6 +337,8 @@ the committed text/fragment bases, compiler postorder, and projection support." old-state candidate-state)) (plist-put candidate-state :native-topology-stable-p stable) (plist-put candidate-state :native-touched-node-ids layout-touched) + (plist-put candidate-state :native-inherited-dirty-node-ids + (ebox-native-commit-inherited-dirty-node-ids prepared)) (when context-axes-stable (plist-put candidate-state :viewport-dependent-node-ids-ready t) (plist-put candidate-state :viewport-dependent-node-ids diff --git a/ebox-native-reflow.el b/ebox-native-reflow.el index 4cda61e..39317fe 100644 --- a/ebox-native-reflow.el +++ b/ebox-native-reflow.el @@ -53,6 +53,8 @@ (declare-function ebox-layout-config-props "ebox-layout-config" (config)) (declare-function ebox-tree-node-children "ebox-tree" (node)) (declare-function ebox-buffer--font-face "ebox-buffer-backend" (style)) +(declare-function ebox-buffer--text-decoration-face + "ebox-buffer-backend" (style)) (declare-function ebox--propertize-typography "ebox-buffer-backend" (string style)) (declare-function ebox--ensure-node-id "ebox" (node)) @@ -71,6 +73,7 @@ (declare-function ebox--layout-children "ebox-tree" (node)) (declare-function ebox-tree-layout-children "ebox-tree" (node)) (declare-function ebox-tree-node-local-source-signature "ebox-tree" (node)) +(declare-function ebox-source--index-record "ebox-source" (index handle)) (declare-function ebox-buffer-side-border-face "ebox-buffer-backend" (color)) (declare-function ebox--space-pixel-width "ebox-measure" ()) @@ -2419,7 +2422,7 @@ per call, and no call recursively visits the captured Ebox tree." (vconcat (nreverse nodes)))) (defun ebox-native-reflow--retained-layout-signature - (node child-revisions flex-content-min-widths) + (node child-revisions flex-content-min-widths source-index) "Return exact retained compiler signature for NODE and CHILD-REVISIONS." (let ((wrapper (and (eq (plist-get node :ebox-type) 'flex) (plist-get node :box)))) @@ -2430,6 +2433,12 @@ per call, and no call recursively visits the captured Ebox tree." (and wrapper (plist-get wrapper :region-id))) (ebox-tree-node-local-source-signature node) (and wrapper (ebox-tree-node-local-source-signature wrapper)) + (and source-index + (when-let* ((handle (plist-get node :ebox-source-handle))) + (ebox-source--index-record source-index handle))) + (and source-index wrapper + (when-let* ((handle (plist-get wrapper :ebox-source-handle))) + (ebox-source--index-record source-index handle))) child-revisions (and (eq (plist-get node :ebox-type) 'flex) (mapcar @@ -2455,12 +2464,18 @@ per call, and no call recursively visits the captured Ebox tree." 0)) (flex-content-min-widths (plist-get state :flex-content-min-widths)) + (source-index (plist-get state :source-index)) (touched-ids (plist-get state :native-touched-node-ids)) + (inherited-dirty-ids + (plist-get state :native-inherited-dirty-node-ids)) (touched-set (and (plist-get state :native-topology-stable-p) (plist-member state :native-touched-node-ids) (proper-list-p touched-ids) (make-hash-table :test 'equal))) + (inherited-touched-set + (and touched-set inherited-dirty-ids + (make-hash-table :test 'equal))) (retained-fast-p (and touched-set (= (hash-table-count old-cache) (length postorder)) @@ -2488,6 +2503,24 @@ per call, and no call recursively visits the captured Ebox tree." (make-hash-table :test 'eq))) (dolist (node-id touched-ids) (when touched-set (puthash node-id t touched-set))) + (when (and touched-set inherited-dirty-ids) + (let ((inherited-set (make-hash-table :test 'equal))) + (dolist (node-id inherited-dirty-ids) + (puthash node-id t inherited-set)) + (cl-labels + ((mark + (current inherited-p) + (let* ((node-id (plist-get current :node-id)) + (inherited-p + (or inherited-p (gethash node-id inherited-set)))) + (when inherited-p (puthash node-id t touched-set)) + (when inherited-p + (puthash node-id t inherited-touched-set)) + (dolist (child + (ebox-native-reflow--retained-layout-children + current)) + (mark child inherited-p))))) + (mark node nil)))) (cl-loop for template in ebox-native-reflow--compile-property-templates for index from 0 @@ -2521,9 +2554,12 @@ per call, and no call recursively visits the captured Ebox tree." children)) (signature (ebox-native-reflow--retained-layout-signature - current child-revisions flex-content-min-widths)) + current child-revisions flex-content-min-widths + source-index)) fragment entry) (if (and cached + (not (and inherited-touched-set + (gethash node-id inherited-touched-set))) (equal signature (plist-get cached :signature))) (setq fragment (plist-get cached :fragment) entry cached) diff --git a/ebox-surface.el b/ebox-surface.el index c4dcaea..cab846a 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -1458,6 +1458,14 @@ the projection roots a second time." (ebox-tree-clear-author-style-pending root)) projection)) +(defun ebox-surface--native-full-frame-p (state projection-kind) + "Return non-nil when STATE owns a complete native surface frame." + (and (eq projection-kind 'native-frame) + (plist-get state :native-topology-stable-p) + (let ((frame (plist-get state :native-render-frame))) + (and (plist-get frame :native-frame) + (not (plist-get frame :native-patch)))))) + (defun ebox-surface--projection-result (context projection state output &optional projection-kind) "Complete CONTEXT PROJECTION for STATE from rendered OUTPUT." @@ -1468,13 +1476,14 @@ the projection roots a second time." (owner-id (plist-get state :span-patch-owner-id)) (scroll-fast-p (and (eq projection-kind 'scroll-patch) (plist-get state :scroll-fast-window-p))) - (full-surface-p - (and scroll-fast-p - (plist-get state :retained-scroll-content-p) - root-id owner-id (= root-id owner-id))) (native-retained-p (and (eq projection-kind 'native-frame) (plist-get state :native-topology-stable-p))) + (full-surface-p + (or (and scroll-fast-p + (plist-get state :retained-scroll-content-p) + root-id owner-id (= root-id owner-id)) + (ebox-surface--native-full-frame-p state projection-kind))) (previous-owned-ranges (plist-get state :previous-surface-owned-ranges)) (node-objects diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index dffce27..c807f7f 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -4,6 +4,7 @@ (require 'ert) (require 'ebox) (require 'ebox-native-commit) +(require 'ebox-native-reflow) ;; These tests lock the named Elisp projection proofs. Native commit has its ;; own focused contract tests below; disable runtime module discovery here so @@ -2091,6 +2092,71 @@ remain retained identities." (should-not mixed-output))) (when (buffer-live-p buffer) (kill-buffer buffer))))) +(ert-deftest ebox-native-inherited-dirty-domain-is-schema-owned () + "Native invalidation derives inherited propagation from the style schema." + (should + (equal '(7) + (ebox-native-commit-inherited-dirty-node-ids + '(:dirty-set + ((:node-id 7 :dirty-kind paint :changed-keys (:color))))))) + (should-not + (ebox-native-commit-inherited-dirty-node-ids + '(:dirty-set + ((:node-id 7 :dirty-kind paint + :changed-keys (:background-color))))))) + +(ert-deftest ebox-native-retained-compiler-refreshes-inherited-descendants () + "An inherited parent paint change cannot reuse a stale child fragment." + (require 'ebox-native-reflow) + (let* ((input + (ebox-test-box + :color "#111111" + (ebox-test-text "Paint" :color "#111111"))) + (root (ebox-test-root input)) + (_ids (ebox--runtime-node-ids root)) + (source-index (ebox-test-source-index input)) + (session + (ebox-native-reflow--make-session + :handle 'test :generation 0 :styles nil :layout-package nil + :layout-fragment-cache (make-hash-table :test 'equal) + :layout-fragment-revision 0)) + (initial-state + (list :native-node-postorder + (ebox-native-reflow--retained-layout-postorder root) + :native-topology-stable-p nil + :source-index source-index)) + (initial-package + (ebox-native-reflow--compile-retained-layout-package + session initial-state root)) + (next (copy-tree root)) + (next-child (car (ebox-tree-node-children next)))) + (setf (ebox-native-reflow-session-styles session) + (plist-get initial-package :styles) + (ebox-native-reflow-session-layout-package session) + initial-package) + ;; Model a candidate computed-style projection: the child source record is + ;; unchanged, while its inherited runtime color follows the parent. + (plist-put next :color "#222222") + (plist-put next-child :color "#222222") + (let* ((root-id (plist-get next :node-id)) + (next-state + (list :native-node-postorder + (ebox-native-reflow--retained-layout-postorder next) + :native-topology-stable-p t + :native-touched-node-ids (list root-id) + :native-inherited-dirty-node-ids (list root-id) + :source-index source-index)) + (package + (ebox-native-reflow--compile-retained-layout-package + session next-state next)) + (document-root (plist-get (plist-get package :document) :root)) + (style-id (plist-get document-root :content-foreground-style)) + (styles (plist-get package :styles))) + (should (integerp style-id)) + (should + (equal '(:foreground "#222222") + (plist-get (aref styles style-id) :face)))))) + (provide 'ebox-commit-tests) ;;; ebox-commit-tests.el ends here diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index 94f120f..566e9bc 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -2995,6 +2995,24 @@ participant and rollback contract without creating a real frame." (when (buffer-live-p buffer) (kill-buffer buffer)))) +(ert-deftest ebox-surface-native-full-frame-owns-full-publication-scope () + "A complete native frame is authoritative for the whole TP surface." + (should + (ebox-surface--native-full-frame-p + '(:native-topology-stable-p t + :native-render-frame (:native-frame t :native-patch nil)) + 'native-frame)) + (should-not + (ebox-surface--native-full-frame-p + '(:native-topology-stable-p t + :native-render-frame (:native-frame t :native-patch t)) + 'native-frame)) + (should-not + (ebox-surface--native-full-frame-p + '(:native-topology-stable-p t + :native-render-frame (:native-frame t :native-patch nil)) + 'paint))) + (provide 'ebox-surface-tests) ;;; ebox-surface-tests.el ends here