fix(render): refresh inherited styles for mixed paint updates
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-09-11 02:17:30 +08:00
parent 8da2fe68cc
commit 23bf6052ab
2 changed files with 152 additions and 11 deletions

View File

@ -7305,9 +7305,27 @@ certificate."
candidate-source-index 'type)))) candidate-source-index 'type))))
(candidate-region-box-table (candidate-region-box-table
(plist-get candidate-index :region-box-table)) (plist-get candidate-index :region-box-table))
(source-dirty-set
(if local-preparation
(plist-get local-preparation :dirty-set)
(ebox-incremental--declarative-dirty-set
old-state candidate-root candidate-index)))
(style-node-ids (style-node-ids
(or (plist-get local-preparation :touched-node-ids) (or (plist-get local-preparation :touched-node-ids)
(and prepare-all-styles-p (and (or prepare-all-styles-p
;; A fully copied region batch has no touched-node
;; index. Mixed projection retains the style tree,
;; so prepare inherited consumers before proving or
;; rendering either its paint or geometry owners.
(and (cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'paint))
source-dirty-set)
(cl-some
(lambda (entry)
(memq (plist-get entry :dirty-kind)
'(geometry structure)))
source-dirty-set)))
(ebox-incremental--hash-keys (ebox-incremental--hash-keys
(plist-get candidate-index :node-table))))) (plist-get candidate-index :node-table)))))
(candidate-subject-table (candidate-subject-table
@ -7315,11 +7333,6 @@ certificate."
(ebox-incremental--prepare-inline-candidate-styles (ebox-incremental--prepare-inline-candidate-styles
old-state candidate-index style-node-ids))) old-state candidate-index style-node-ids)))
(styles-prepared-p (hash-table-p candidate-subject-table)) (styles-prepared-p (hash-table-p candidate-subject-table))
(source-dirty-set
(if local-preparation
(plist-get local-preparation :dirty-set)
(ebox-incremental--declarative-dirty-set
old-state candidate-root candidate-index)))
(axis-node-ids (axis-node-ids
(unless (ebox-style-cascade-active-p) (unless (ebox-style-cascade-active-p)
(cl-loop for entry in source-dirty-set (cl-loop for entry in source-dirty-set
@ -7374,7 +7387,13 @@ certificate."
buffer (plist-get old-root :node-id)))) buffer (plist-get old-root :node-id))))
(let* ((structural-caches (let* ((structural-caches
(ebox-incremental--candidate-structural-caches (ebox-incremental--candidate-structural-caches
old-state candidate-index dirty-set local-preparation)) old-state candidate-index dirty-set
(or local-preparation
;; Newly prepared inheritance changes descendants whose
;; source declarations are unchanged. Their retained
;; render signatures must not survive the style refresh.
(and styles-prepared-p
(list :touched-node-ids style-node-ids)))))
(render-signature-cache (render-signature-cache
(plist-get structural-caches :render-signature-cache)) (plist-get structural-caches :render-signature-cache))
(viewport-height-dependent-subtree-cache (viewport-height-dependent-subtree-cache
@ -8081,10 +8100,15 @@ suppresses the ordinary fallback when any fact is missing."
(ebox-runtime-index-get node-id (ebox-runtime-index-get node-id
(plist-get candidate-state (plist-get candidate-state
:parent-table))) :parent-table)))
(equal (ebox-incremental--layout-slot-style-signature ;; Paint consumes effective values. Its newly
old-node) ;; computed style may explicitly store nil
(ebox-incremental--layout-slot-style-signature ;; defaults that the retained node omitted.
new-node)) (null
(ebox-fragment-plist-changed-keys
(ebox-incremental--layout-slot-style-signature
old-node)
(ebox-incremental--layout-slot-style-signature
new-node)))
(equal (ebox-get old-node :width) (equal (ebox-get old-node :width)
(ebox-get new-node :width)) (ebox-get new-node :width))
(equal (ebox-get old-node :height) (equal (ebox-get old-node :height)
@ -8121,6 +8145,10 @@ suppresses the ordinary fallback when any fact is missing."
(plist-put (copy-sequence geometry-span-proof) (plist-put (copy-sequence geometry-span-proof)
:retain-external-owner-suffix-p t))) :retain-external-owner-suffix-p t)))
(when (and geometry-proof paint-valid-p disjoint-p (when (and geometry-proof paint-valid-p disjoint-p
;; Reusing the style tree is safe only after the candidate's
;; inherited paint closure was projected. Tree-dependent
;; selectors keep the ordinary cascade/render fallback.
(plist-get prepared :styles-prepared-p)
allocation-disjoint-p cascade-safe-p allocation-disjoint-p cascade-safe-p
;; Both the rendered geometry and inherited paint must leave ;; Both the rendered geometry and inherited paint must leave
;; every retained scroll producer outside their dependency scope. ;; every retained scroll producer outside their dependency scope.

View File

@ -14487,4 +14487,117 @@ face patch must match that exactly (issue014)."
(ebox-region-update 'status :content value)))) (ebox-region-update 'status :content value))))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))))) (should (ebox-test--buffer-propertized-matches-runtime-render-p)))))))
(ert-deftest ebox-mixed-content-paint-refreshes-inherited-foreground ()
"A changed sibling label must not retain another Box's inherited color."
(ebox-test--reset-runtime-state)
(let ((ebox-viewport-width 80) (ebox-viewport-height 24))
(ebox-test--with-rendered-buffer
(ebox-build
'(column :width (ch 20)
(box :id paint :color "#BBA9FF" "Alpha")
(box :id label "Beta")))
(dolist (step '(("B" "#674795")
("Beta longer" "#BBA9FF")
("Beta" "#674795")))
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
(lambda (&rest _)
(ert-fail "Mixed inherited paint rendered the root"))))
(ebox-call-with-update-batch
(lambda ()
(ebox-region-update 'paint :color (cadr step))
(ebox-region-update 'label :content (car step)))))
(should
(equal (ebox-test--visible-foreground
(get-text-property (point-min) 'face))
(cadr step)))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))))
(ert-deftest ebox-mixed-content-paint-refreshes-ancestor-and-keeps-override ()
"Ancestor paint reaches retained and replaced Text while keeping overrides."
(ebox-test--reset-runtime-state)
(let ((ebox-viewport-width 80) (ebox-viewport-height 24))
(ebox-test--with-rendered-buffer
(ebox-build
'(column :id root :width (ch 20) :color "#BBA9FF"
(column (box "Alpha"))
(box :id label "Beta")
(box :color "#123456" "Override")))
(dolist (step '(("B" "#674795") ("Beta longer" "#BBA9FF")))
(ebox-call-with-update-batch
(lambda ()
(ebox-region-update 'root :color (cadr step))
(ebox-region-update 'label :content (car step))))
(dolist (text (list "Alpha" (car step) "Override"))
(goto-char (point-min))
(search-forward text)
(should
(equal (ebox-test--visible-foreground
(get-text-property (match-beginning 0) 'face))
(if (equal text "Override") "#123456" (cadr step)))))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))))
(ert-deftest ebox-mixed-content-paint-inheritance-rolls-back-and-retries ()
"Failed mixed publication preserves the old style closure and accepts retry."
(ebox-test--reset-runtime-state)
(let ((ebox-viewport-width 80) (ebox-viewport-height 24))
(ebox-test--with-rendered-buffer
(ebox-build
'(column :width (ch 20)
(box :id paint :color "#BBA9FF" "Alpha")
(box :id label "Beta")))
(let* ((state (ebox--buffer-render-state (current-buffer)))
(before (buffer-string))
(revision (ebox-surface-buffer-revision (current-buffer)))
(paint (plist-get
(car (ebox-selector-query-buffer (current-buffer) "#paint"))
:node))
(text (car (ebox-tree-node-children paint)))
(color (plist-get text :color))
(update (lambda ()
(ebox-region-update 'paint :color "#674795")
(ebox-region-update 'label :content "B")))
rejected)
(should text)
(should (equal color "#BBA9FF"))
(let ((tp--surface-publication-step-function
(lambda (step _surface)
(when (eq step 'client-state)
(setq rejected t)
(error "Reject mixed inherited paint")))))
(should-error (ebox-call-with-update-batch update)))
(should rejected)
(should (eq state (ebox--buffer-render-state (current-buffer))))
(should (equal (plist-get text :color) color))
(should (equal-including-properties before (buffer-string)))
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
(ebox-call-with-update-batch update)
(should (equal (ebox-test--visible-foreground
(get-text-property (point-min) 'face))
"#674795"))
(should (= (1+ revision) (ebox-surface-buffer-revision (current-buffer))))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))))
(ert-deftest ebox-mixed-content-paint-inheritance-keeps-cascade-fallback ()
"Tree-dependent rules retain full style projection for mixed inheritance."
(ebox-test--reset-runtime-state)
(let ((ebox-viewport-width 80) (ebox-viewport-height 24)
(ebox-style-stylesheet (ecss-stylesheet-create)))
(ebox-style-add-rule "#paint text" '(:font-weight bold))
(ebox-test--with-rendered-buffer
(ebox-build
'(column :width (ch 20)
(box :id paint :color "#BBA9FF" "Alpha")
(box :id label "Beta")))
(cl-letf (((symbol-function 'ebox-surface--mixed-owner-output)
(lambda (&rest _)
(ert-fail "Unprepared cascade entered mixed projection"))))
(ebox-call-with-update-batch
(lambda ()
(ebox-region-update 'paint :color "#674795")
(ebox-region-update 'label :content "B"))))
(should (equal (ebox-test--visible-foreground
(get-text-property (point-min) 'face))
"#674795"))
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))
;;; ebox-core-render-tests.el ends here ;;; ebox-core-render-tests.el ends here