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 b824467791
commit c81fa47d31
3 changed files with 354 additions and 53 deletions

View File

@ -226,6 +226,7 @@ module while loading the package."
layout-fragment-revision
layout-style-index
layout-property-template-index
layout-edge-index
readiness-process
readiness-preparation
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-property-template-index fork)
(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)
(error
(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)))
(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
(session state node)
"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
session)
(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)))))
(defconst ebox-native-reflow--delta-edge-fields
'(:type :child :children :items :node-id :node-revision)
"Fragment fields that a stable local node delta cannot replace.")
(defconst ebox-native-reflow--delta-flex-edge-properties
'(:order :flex-grow :flex-shrink :flex-basis :align-self
:flex-direction :cross-align)
"Changed properties that alter retained Flex item metadata in N1.")
(defun ebox-native-reflow--current-edge-item-fact
(state owner parent rule retained)
"Return OWNER's current edge fact from STATE under PARENT.
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)
"Return non-nil when STATE changes unsupported Flex edge metadata."
(defun ebox-native-reflow--delta-edge-change-p (session state index)
"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
(lambda (dirty)
(let* ((keys (plist-get dirty :changed-keys))
(parents (plist-get state :parent-table))
(lambda (dirty-id)
(let* ((parents (plist-get state :parent-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
(if (and (eq (plist-get dirty-node :ebox-kind) 'text)
(eq (plist-get raw-parent :ebox-kind) 'box)
(when-let* ((layout
(plist-get raw-parent
:ebox-layout-config)))
(eq (ebox-layout-config-kind layout) 'normal)))
raw-parent-id
dirty-id))
(edge-parent-id (and parents (gethash owner-id parents)))
(edge-parent (and nodes (gethash edge-parent-id nodes)))
(config
(and edge-parent
(plist-get edge-parent :ebox-layout-config))))
(or
(cl-some (lambda (key)
(memq key ebox-native-reflow--delta-flex-edge-properties))
keys)
;; A child-local edit can change content-min-width in a Flex item or
;; derived align-self in typed axis lowering. N1 has no edge patch,
;; so retain correctness through full input until exact edge facts are
;; independently addressable.
(or (eq (plist-get edge-parent :ebox-type) 'flex)
(and config
(let ((kind (ebox-layout-config-kind config))
(props (ebox-layout-config-props config)))
(or (eq kind 'flex)
(and (memq kind '(row column))
(not (and (equal (plist-get props :item-gap) 0)
(eq (plist-get props :cross-align)
'stretch)))))))))))
(plist-get state :native-local-dirty-entries)))
(and nodes parents
(ebox-native-reflow--delta-owner-id state dirty-id index)))
(owner (and owner-id (gethash owner-id nodes)))
(parent-id (and owner-id (gethash owner-id parents)))
(parent (and parent-id (gethash parent-id nodes)))
(current-parent-rule
(and parent (ebox-native-reflow--retained-edge-rule parent)))
(retained-parent-rule
(and parent-id
(ebox-native-reflow--persistent-index-get
edge-index (cons 'rule parent-id) missing)))
(retained-item
(and owner-id
(ebox-native-reflow--persistent-index-get
edge-index (cons 'child owner-id) missing)))
(current-owner-rule
(and owner (ebox-native-reflow--retained-edge-rule owner)))
(retained-owner-rule
(and owner-id
(ebox-native-reflow--persistent-index-get
edge-index (cons 'rule owner-id) missing))))
(or (null owner)
(not (equal current-owner-rule
(unless (eq retained-owner-rule missing)
retained-owner-rule)))
(not (equal current-parent-rule
(unless (eq retained-parent-rule missing)
retained-parent-rule)))
(if (memq (plist-get current-parent-rule :kind)
'(flex-item typed-axis-item))
(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)
"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)
(plist-get state :native-topology-stable-p)
(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
;; affected-node index. Until then it is an explicit full
;; input fallback rather than a partial semantic update.

View File

@ -2592,13 +2592,17 @@ remain retained identities."
(require 'ebox-native-reflow)
(let* ((cache (make-hash-table :test 'equal))
(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))
(session
(ebox-native-reflow--make-session
:handle 'parent :generation 4 :styles styles
:layout-package 'package :layout-fragment-cache cache
: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)
(lambda (_handle) 'child)))
(let ((fork (ebox-native-reflow-fork-session session)))
@ -2606,6 +2610,8 @@ remain retained identities."
(ebox-native-reflow-session-layout-fragment-cache fork)))
(should (eq index
(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 (= 8
(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))
(next-index
(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
:styles [] :property-templates []))
(next (copy-sequence old))
(parent
(ebox-native-reflow--make-session
(ebox-native-reflow--make-session
: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-delta
'(:style-base-count 0 :styles-append []
@ -2647,6 +2657,10 @@ remain retained identities."
(ebox-native-reflow-session-layout-fragment-index fork)))
(should (eq base-index
(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
(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."
(require 'ebox-native-reflow)
(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
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-column-layout-create))
@ -2925,11 +2940,170 @@ remain retained identities."
(puthash 2 1 parents)
(puthash 3 2 parents)
(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
:native-local-dirty-entries
'((: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 ()
"The incremental producer preserves exact local dirtiness to native input."

View File

@ -436,6 +436,12 @@
:authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap
: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
:storage (:global ebox-native-reflow--compile-style-index)
:proposed-category generation-fact :owner ebox-native-reflow