Reuse exact native edge facts for local mixed-layout deltas

This commit is contained in:
Kinneyzhang 2026-09-05 09:35:43 +08:00
parent c9131401ea
commit 85849c01e7
3 changed files with 354 additions and 53 deletions

View File

@ -226,6 +226,7 @@ module while loading the package."
layout-fragment-revision layout-fragment-revision
layout-style-index layout-style-index
layout-property-template-index layout-property-template-index
layout-edge-index
readiness-process readiness-process
readiness-preparation readiness-preparation
released-p) released-p)
@ -1419,7 +1420,9 @@ copied separately; no pending frame or registered Rust document is inherited."
(ebox-native-reflow-session-layout-style-index session) (ebox-native-reflow-session-layout-style-index session)
(ebox-native-reflow-session-layout-property-template-index fork) (ebox-native-reflow-session-layout-property-template-index fork)
(ebox-native-reflow-session-layout-property-template-index (ebox-native-reflow-session-layout-property-template-index
session)) session)
(ebox-native-reflow-session-layout-edge-index fork)
(ebox-native-reflow-session-layout-edge-index session))
fork) fork)
(error (error
(ebox-native-reflow-release-session fork) (ebox-native-reflow-release-session fork)
@ -2599,6 +2602,86 @@ per call, and no call recursively visits the captured Ebox tree."
(gethash child flex-content-min-widths))) (gethash child flex-content-min-widths)))
(ebox-native-reflow--retained-layout-children node)))))) (ebox-native-reflow--retained-layout-children node))))))
(defun ebox-native-reflow--retained-edge-rule (node)
"Return NODE's exact child-edge lowering rule, or nil for plain edges."
(let ((node-id (plist-get node :node-id)))
(cond
((and (eq (plist-get node :ebox-type) 'box)
(eq (plist-get node :ebox-kind) 'box))
(let* ((config (plist-get node :ebox-layout-config))
(kind (and config (ebox-layout-config-kind config)))
(props (and config (ebox-layout-config-props config))))
(pcase kind
('flex
(list :parent-id node-id :slot 1 :kind 'flex-item
:axis (ebox--flex-axis props)))
((or 'row 'column)
(if (and (equal (plist-get props :item-gap) 0)
(eq (plist-get props :cross-align) 'stretch))
(list :parent-id node-id :slot 1 :kind 'direct-axis
:axis kind)
(list :parent-id node-id :slot 1 :kind 'typed-axis-item
:axis kind :cross-align
(plist-get props :cross-align)))))))
((eq (plist-get node :ebox-type) 'flex)
(let* ((wrapper (plist-get node :box))
(props (ebox--flex-container-content-props
(plist-get node :props) (plist-get node :raw-props)
wrapper)))
(list :parent-id node-id :slot (if wrapper 1 0) :kind 'flex-item
:axis (ebox--flex-axis props)))))))
(defun ebox-native-reflow--retained-edge-item-local (item)
"Return exact derived edge fields from compiled ITEM."
(let* ((node (plist-get item :node))
(local
(cl-loop for (key value) on item by #'cddr
unless (eq key :node) append (list key value))))
;; Flex compilation may augment the edge's child view with this measured
;; scalar without changing the independently retained child fragment.
(if (plist-member node :content-min-width)
(append local
(list :content-min-width
(plist-get node :content-min-width)))
local)))
(defun ebox-native-reflow--retained-edge-index (postorder cache)
"Build exact child-edge facts from full POSTORDER fragments in CACHE."
(let (index)
(cl-loop for parent across postorder
for parent-id = (plist-get parent :node-id)
for rule = (ebox-native-reflow--retained-edge-rule parent)
when rule do
(setq index
(ebox-native-reflow--persistent-index-put
index (cons 'rule parent-id) rule))
when (memq (plist-get rule :kind)
'(flex-item typed-axis-item)) do
(let* ((entry (gethash parent-id cache))
(fragment (plist-get entry :fragment))
(inner (if (= (plist-get rule :slot) 1)
(plist-get fragment :child)
fragment))
(items (plist-get inner :items))
(children
(ebox-native-reflow--retained-layout-children parent)))
(unless (= (length children) (length items))
(error "Native retained edge facts do not match children"))
(cl-loop for child in children
for item across items
for position from 0 do
(setq index
(ebox-native-reflow--persistent-index-put
index
(cons 'child (plist-get child :node-id))
(append
(list :parent-id parent-id
:slot (plist-get rule :slot)
:item-position position)
(ebox-native-reflow--retained-edge-item-local
item)))))))
index))
(defun ebox-native-reflow--compile-retained-layout-package-full (defun ebox-native-reflow--compile-retained-layout-package-full
(session state node) (session state node)
"Compile a complete retained package for NODE in SESSION from STATE." "Compile a complete retained package for NODE in SESSION from STATE."
@ -2785,61 +2868,98 @@ per call, and no call recursively visits the captured Ebox tree."
(ebox-native-reflow-session-layout-property-template-index (ebox-native-reflow-session-layout-property-template-index
session) session)
(ebox-native-reflow--persistent-index-from-sequence (ebox-native-reflow--persistent-index-from-sequence
(plist-get package :property-templates))) (plist-get package :property-templates))
(ebox-native-reflow-session-layout-edge-index session)
(ebox-native-reflow--retained-edge-index
postorder new-cache))
package))))) package)))))
(defconst ebox-native-reflow--delta-edge-fields (defconst ebox-native-reflow--delta-edge-fields
'(:type :child :children :items :node-id :node-revision) '(:type :child :children :items :node-id :node-revision)
"Fragment fields that a stable local node delta cannot replace.") "Fragment fields that a stable local node delta cannot replace.")
(defconst ebox-native-reflow--delta-flex-edge-properties (defun ebox-native-reflow--current-edge-item-fact
'(:order :flex-grow :flex-shrink :flex-basis :align-self (state owner parent rule retained)
:flex-direction :cross-align) "Return OWNER's current edge fact from STATE under PARENT.
"Changed properties that alter retained Flex item metadata in N1.") RULE identifies the compiler lowering, and RETAINED supplies its stable slot."
(let ((local
(pcase (plist-get rule :kind)
('flex-item
(let* ((widths (plist-get state :flex-content-min-widths))
(missing (make-symbol "missing-native-edge-width"))
(width (if (hash-table-p widths)
(gethash owner widths missing)
missing)))
(ebox-native-reflow--retained-edge-item-local
(ebox-native-reflow--compile-flex-item
owner (plist-get rule :axis) nil t width
(not (eq width missing))))))
('typed-axis-item
(list :order 0 :grow 0 :shrink 0
:basis (list :kind "auto")
:align-self
(ebox-native-reflow--simple-axis-item-align
(plist-get rule :axis) owner
(plist-get rule :cross-align)))))))
(and local
(append
(list :parent-id (plist-get parent :node-id)
:slot (plist-get retained :slot)
:item-position (plist-get retained :item-position))
local))))
(defun ebox-native-reflow--delta-flex-edge-change-p (state) (defun ebox-native-reflow--delta-edge-change-p (session state index)
"Return non-nil when STATE changes unsupported Flex edge metadata." "Return non-nil when STATE changes a retained derived edge in SESSION."
(let ((edge-index (ebox-native-reflow-session-layout-edge-index session))
(missing (make-symbol "missing-native-edge-fact"))
(ids
(append
(mapcar (lambda (dirty) (plist-get dirty :node-id))
(plist-get state :native-local-dirty-entries))
(plist-get state :native-touched-node-ids))))
(cl-some (cl-some
(lambda (dirty) (lambda (dirty-id)
(let* ((keys (plist-get dirty :changed-keys)) (let* ((parents (plist-get state :parent-table))
(parents (plist-get state :parent-table))
(nodes (plist-get state :node-table)) (nodes (plist-get state :node-table))
(dirty-id (plist-get dirty :node-id))
(dirty-node (and nodes (gethash dirty-id nodes)))
(raw-parent-id (and parents (gethash dirty-id parents)))
(raw-parent (and nodes (gethash raw-parent-id nodes)))
(owner-id (owner-id
(if (and (eq (plist-get dirty-node :ebox-kind) 'text) (and nodes parents
(eq (plist-get raw-parent :ebox-kind) 'box) (ebox-native-reflow--delta-owner-id state dirty-id index)))
(when-let* ((layout (owner (and owner-id (gethash owner-id nodes)))
(plist-get raw-parent (parent-id (and owner-id (gethash owner-id parents)))
:ebox-layout-config))) (parent (and parent-id (gethash parent-id nodes)))
(eq (ebox-layout-config-kind layout) 'normal))) (current-parent-rule
raw-parent-id (and parent (ebox-native-reflow--retained-edge-rule parent)))
dirty-id)) (retained-parent-rule
(edge-parent-id (and parents (gethash owner-id parents))) (and parent-id
(edge-parent (and nodes (gethash edge-parent-id nodes))) (ebox-native-reflow--persistent-index-get
(config edge-index (cons 'rule parent-id) missing)))
(and edge-parent (retained-item
(plist-get edge-parent :ebox-layout-config)))) (and owner-id
(or (ebox-native-reflow--persistent-index-get
(cl-some (lambda (key) edge-index (cons 'child owner-id) missing)))
(memq key ebox-native-reflow--delta-flex-edge-properties)) (current-owner-rule
keys) (and owner (ebox-native-reflow--retained-edge-rule owner)))
;; A child-local edit can change content-min-width in a Flex item or (retained-owner-rule
;; derived align-self in typed axis lowering. N1 has no edge patch, (and owner-id
;; so retain correctness through full input until exact edge facts are (ebox-native-reflow--persistent-index-get
;; independently addressable. edge-index (cons 'rule owner-id) missing))))
(or (eq (plist-get edge-parent :ebox-type) 'flex) (or (null owner)
(and config (not (equal current-owner-rule
(let ((kind (ebox-layout-config-kind config)) (unless (eq retained-owner-rule missing)
(props (ebox-layout-config-props config))) retained-owner-rule)))
(or (eq kind 'flex) (not (equal current-parent-rule
(and (memq kind '(row column)) (unless (eq retained-parent-rule missing)
(not (and (equal (plist-get props :item-gap) 0) retained-parent-rule)))
(eq (plist-get props :cross-align) (if (memq (plist-get current-parent-rule :kind)
'stretch))))))))))) '(flex-item typed-axis-item))
(plist-get state :native-local-dirty-entries))) (or (eq retained-item missing)
(not
(equal
retained-item
(ebox-native-reflow--current-edge-item-fact
state owner parent current-parent-rule retained-item))))
(not (eq retained-item missing))))))
(delete-dups (copy-sequence ids)))))
(defun ebox-native-reflow--fragment-local (fragment) (defun ebox-native-reflow--fragment-local (fragment)
"Return FRAGMENT's scalar and local-content fields." "Return FRAGMENT's scalar and local-content fields."
@ -2956,7 +3076,8 @@ per call, and no call recursively visits the captured Ebox tree."
(when (and old-package old-index (hash-table-p node-table) (when (and old-package old-index (hash-table-p node-table)
(plist-get state :native-topology-stable-p) (plist-get state :native-topology-stable-p)
(plist-member state :native-local-dirty-entries) (plist-member state :native-local-dirty-entries)
(not (ebox-native-reflow--delta-flex-edge-change-p state)) (not (ebox-native-reflow--delta-edge-change-p
session state old-index))
;; Descendant inherited-style expansion needs its own bounded ;; Descendant inherited-style expansion needs its own bounded
;; affected-node index. Until then it is an explicit full ;; affected-node index. Until then it is an explicit full
;; input fallback rather than a partial semantic update. ;; input fallback rather than a partial semantic update.

View File

@ -2592,13 +2592,17 @@ remain retained identities."
(require 'ebox-native-reflow) (require 'ebox-native-reflow)
(let* ((cache (make-hash-table :test 'equal)) (let* ((cache (make-hash-table :test 'equal))
(index (ebox-native-reflow--persistent-index-put nil 1 'entry)) (index (ebox-native-reflow--persistent-index-put nil 1 'entry))
(edge-index
(ebox-native-reflow--persistent-index-put
nil '(rule . 1) '(:kind direct-axis)))
(styles (vector 'style)) (styles (vector 'style))
(session (session
(ebox-native-reflow--make-session (ebox-native-reflow--make-session
:handle 'parent :generation 4 :styles styles :handle 'parent :generation 4 :styles styles
:layout-package 'package :layout-fragment-cache cache :layout-package 'package :layout-fragment-cache cache
:layout-fragment-index index :layout-fragment-revision 8 :layout-fragment-index index :layout-fragment-revision 8
:layout-style-index index :layout-property-template-index index))) :layout-style-index index :layout-property-template-index index
:layout-edge-index edge-index)))
(cl-letf (((symbol-function 'ebox-native--module-fork-confirmed) (cl-letf (((symbol-function 'ebox-native--module-fork-confirmed)
(lambda (_handle) 'child))) (lambda (_handle) 'child)))
(let ((fork (ebox-native-reflow-fork-session session))) (let ((fork (ebox-native-reflow-fork-session session)))
@ -2606,6 +2610,8 @@ remain retained identities."
(ebox-native-reflow-session-layout-fragment-cache fork))) (ebox-native-reflow-session-layout-fragment-cache fork)))
(should (eq index (should (eq index
(ebox-native-reflow-session-layout-fragment-index fork))) (ebox-native-reflow-session-layout-fragment-index fork)))
(should (eq edge-index
(ebox-native-reflow-session-layout-edge-index fork)))
(should (eq styles (ebox-native-reflow-session-styles fork))) (should (eq styles (ebox-native-reflow-session-styles fork)))
(should (= 8 (should (= 8
(ebox-native-reflow-session-layout-fragment-revision (ebox-native-reflow-session-layout-fragment-revision
@ -2618,13 +2624,17 @@ remain retained identities."
(base-index (ebox-native-reflow--persistent-index-put nil 1 'old)) (base-index (ebox-native-reflow--persistent-index-put nil 1 'old))
(next-index (next-index
(ebox-native-reflow--persistent-index-put base-index 1 'new)) (ebox-native-reflow--persistent-index-put base-index 1 'new))
(edge-index
(ebox-native-reflow--persistent-index-put
nil '(child . 1) '(:parent-id 9)))
(old (list :document '(:version 2) :document-revision 4 (old (list :document '(:version 2) :document-revision 4
:styles [] :property-templates [])) :styles [] :property-templates []))
(next (copy-sequence old)) (next (copy-sequence old))
(parent (parent
(ebox-native-reflow--make-session (ebox-native-reflow--make-session
:handle 'parent :layout-package old :styles [] :handle 'parent :layout-package old :styles []
:layout-fragment-cache cache :layout-fragment-index base-index))) :layout-fragment-cache cache :layout-fragment-index base-index
:layout-edge-index edge-index)))
(plist-put next :document-revision 5) (plist-put next :document-revision 5)
(plist-put next :document-delta (plist-put next :document-delta
'(:style-base-count 0 :styles-append [] '(:style-base-count 0 :styles-append []
@ -2647,6 +2657,10 @@ remain retained identities."
(ebox-native-reflow-session-layout-fragment-index fork))) (ebox-native-reflow-session-layout-fragment-index fork)))
(should (eq base-index (should (eq base-index
(ebox-native-reflow-session-layout-fragment-index parent))) (ebox-native-reflow-session-layout-fragment-index parent)))
(should (eq edge-index
(ebox-native-reflow-session-layout-edge-index fork)))
(should (eq edge-index
(ebox-native-reflow-session-layout-edge-index parent)))
(should (eq cache (should (eq cache
(ebox-native-reflow-session-layout-fragment-cache fork)))))))) (ebox-native-reflow-session-layout-fragment-cache fork))))))))
@ -2912,7 +2926,8 @@ remain retained identities."
"Default Row/Column edges carry no derived Flex item metadata." "Default Row/Column edges carry no derived Flex item metadata."
(require 'ebox-native-reflow) (require 'ebox-native-reflow)
(let ((nodes (make-hash-table :test 'equal)) (let ((nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))) (parents (make-hash-table :test 'equal))
(index (ebox-native-reflow--persistent-index-put nil 2 'owner)))
(puthash 1 (puthash 1
(list :node-id 1 :ebox-type 'box :ebox-kind 'box (list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-column-layout-create)) :ebox-layout-config (ebox-column-layout-create))
@ -2925,11 +2940,170 @@ remain retained identities."
(puthash 2 1 parents) (puthash 2 1 parents)
(puthash 3 2 parents) (puthash 3 2 parents)
(should-not (should-not
(ebox-native-reflow--delta-flex-edge-change-p (ebox-native-reflow--delta-edge-change-p
(ebox-native-reflow--make-session
:layout-edge-index
(ebox-native-reflow--persistent-index-put
nil '(rule . 1)
'(:parent-id 1 :slot 1 :kind direct-axis :axis column)))
(list :node-table nodes :parent-table parents (list :node-table nodes :parent-table parents
:native-local-dirty-entries :native-local-dirty-entries
'((:node-id 3 :dirty-kind geometry '((:node-id 3 :dirty-kind geometry
:changed-keys (:content)))))))) :changed-keys (:content))))
index))))
(ert-deftest ebox-native-flex-fused-paint-proves-retained-edge-unchanged ()
"A fused paint edit under Flex uses its exact retained item fact."
(require 'ebox-native-reflow)
(let* ((text '(:node-id 3 :ebox-type box :ebox-kind text))
(owner
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-normal-layout-create)
:children (list text)))
(parent
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-flex-layout-create)
:children (list owner)))
(item (ebox-native-reflow--compile-flex-item
owner 'row '(:type "box") t 17 t))
(fragment
(list :type "box"
:child (list :type "flex" :items (vector item))))
(cache (make-hash-table :test 'equal))
(nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(widths (make-hash-table :test 'eq))
(owner-fragment
'(:type "box" :background-style :null :child :null
:node-id 2 :node-revision 1))
(index
(ebox-native-reflow--persistent-index-put
nil 2 (list :fragment owner-fragment :revision 1)))
(_index
(setq index
(ebox-native-reflow--persistent-index-put
index 1 (list :fragment fragment :revision 2)))))
(puthash 1 (list :fragment fragment :revision 1) cache)
(puthash 1 parent nodes)
(puthash 2 owner nodes)
(puthash 3 text nodes)
(puthash 2 1 parents)
(puthash 3 2 parents)
(puthash owner 17 widths)
(let* ((session
(ebox-native-reflow--make-session
:layout-package
'(:document (:version 2) :document-revision 1
:styles [] :property-templates [])
:layout-fragment-index index :layout-fragment-revision 2
:layout-edge-index
(ebox-native-reflow--retained-edge-index
(vector owner parent) cache)))
(state
(list :node-table nodes :parent-table parents
:flex-content-min-widths widths
:native-topology-stable-p t
:native-touched-node-ids '(3 2 1)
:native-local-dirty-entries
'((:node-id 3 :dirty-kind paint
:changed-keys (:background-color))))))
(cl-letf (((symbol-function 'ebox-tree-node-children)
(lambda (&rest _)
(ert-fail "delta edge proof enumerated siblings")))
((symbol-function 'ebox--current-display-signature)
(lambda () 'display))
((symbol-function 'ebox-native-reflow--compile-delta-slots)
(lambda (_node _old)
(vector
'(:type "box" :background-style 0 :child :null)))))
(should-not
(ebox-native-reflow--delta-edge-change-p
session state index))
(let ((package
(ebox-native-reflow--compile-retained-layout-delta
session state parent)))
(should (plist-get package :document-delta))
(should (= 2 (length
(plist-get (plist-get package :document-delta)
:entries)))))))))
(ert-deftest ebox-native-flex-measurement-change-keeps-full-fallback ()
"A changed exact Flex measurement still requires the future edge protocol."
(require 'ebox-native-reflow)
(let* ((owner
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-normal-layout-create)
:wrap-mode 'word))
(parent
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-flex-layout-create)
:children (list owner)))
(item (ebox-native-reflow--compile-flex-item
owner 'row '(:type "box") t 17 t))
(fragment
(list :type "box"
:child (list :type "flex" :items (vector item))))
(cache (make-hash-table :test 'equal))
(nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(widths (make-hash-table :test 'eq))
(index (ebox-native-reflow--persistent-index-put nil 2 'owner)))
(puthash 1 (list :fragment fragment :revision 1) cache)
(puthash 1 parent nodes)
(puthash 2 owner nodes)
(puthash 2 1 parents)
(puthash owner 23 widths)
(should
(ebox-native-reflow--delta-edge-change-p
(ebox-native-reflow--make-session
:layout-edge-index
(ebox-native-reflow--retained-edge-index
(vector owner parent) cache))
(list :node-table nodes :parent-table parents
:flex-content-min-widths widths
:native-local-dirty-entries
'((:node-id 2 :dirty-kind content :changed-keys (:content))))
index))))
(ert-deftest ebox-native-nondefault-axis-fused-edit-uses-exact-edge-fact ()
"A fused edit under nondefault Row reuses an unchanged align-self fact."
(require 'ebox-native-reflow)
(let* ((text '(:node-id 3 :ebox-type box :ebox-kind text))
(owner
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-normal-layout-create)
:children (list text)))
(parent
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config
(ebox-row-layout-create :item-gap 2 :cross-align 'stretch)
:children (list owner)))
(item
(list :node '(:type "box") :order 0 :grow 0 :shrink 0
:basis '(:kind "auto") :align-self "stretch"))
(fragment
(list :type "box"
:child (list :type "flex" :items (vector item))))
(cache (make-hash-table :test 'equal))
(nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(index (ebox-native-reflow--persistent-index-put nil 2 'owner)))
(puthash 1 (list :fragment fragment :revision 1) cache)
(puthash 1 parent nodes)
(puthash 2 owner nodes)
(puthash 3 text nodes)
(puthash 2 1 parents)
(puthash 3 2 parents)
(should-not
(ebox-native-reflow--delta-edge-change-p
(ebox-native-reflow--make-session
:layout-edge-index
(ebox-native-reflow--retained-edge-index
(vector owner parent) cache))
(list :node-table nodes :parent-table parents
:native-local-dirty-entries
'((:node-id 3 :dirty-kind content :changed-keys (:content))))
index))))
(ert-deftest ebox-native-surface-overrides-carry-local-dirty-entries () (ert-deftest ebox-native-surface-overrides-carry-local-dirty-entries ()
"The incremental producer preserves exact local dirtiness to native input." "The incremental producer preserves exact local dirtiness to native input."

View File

@ -436,6 +436,12 @@
:authority ebox :lifetime private-native-session :authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap :rollback private-session-discard :rebuild full-bootstrap
:cleanup native-session-release) :cleanup native-session-release)
(:id native-session/layout-edge-index
:storage (:struct-slot ebox-native-reflow-session layout-edge-index)
:proposed-category generation-bound-mutable :owner ebox-native-reflow
:authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap-edge-facts
:cleanup native-session-release)
(:id native-compile/style-index (:id native-compile/style-index
:storage (:global ebox-native-reflow--compile-style-index) :storage (:global ebox-native-reflow--compile-style-index)
:proposed-category generation-fact :owner ebox-native-reflow :proposed-category generation-fact :owner ebox-native-reflow