From 23bf6052ab655a197e2141608f33adc3f4d8a061 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Fri, 11 Sep 2026 02:17:30 +0800 Subject: [PATCH] fix(render): refresh inherited styles for mixed paint updates --- ebox-incremental.el | 50 ++++++++++---- tests/ebox-core-render-tests.el | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 11 deletions(-) diff --git a/ebox-incremental.el b/ebox-incremental.el index 006f17b..f39263a 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -7305,9 +7305,27 @@ certificate." candidate-source-index 'type)))) (candidate-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 (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 (plist-get candidate-index :node-table))))) (candidate-subject-table @@ -7315,11 +7333,6 @@ certificate." (ebox-incremental--prepare-inline-candidate-styles old-state candidate-index style-node-ids))) (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 (unless (ebox-style-cascade-active-p) (cl-loop for entry in source-dirty-set @@ -7374,7 +7387,13 @@ certificate." buffer (plist-get old-root :node-id)))) (let* ((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 (plist-get structural-caches :render-signature-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 (plist-get candidate-state :parent-table))) - (equal (ebox-incremental--layout-slot-style-signature - old-node) - (ebox-incremental--layout-slot-style-signature - new-node)) + ;; Paint consumes effective values. Its newly + ;; computed style may explicitly store nil + ;; defaults that the retained node omitted. + (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) (ebox-get new-node :width)) (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) :retain-external-owner-suffix-p t))) (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 ;; Both the rendered geometry and inherited paint must leave ;; every retained scroll producer outside their dependency scope. diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index cbd1b1f..720ee9c 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -14487,4 +14487,117 @@ face patch must match that exactly (issue014)." (ebox-region-update 'status :content value)))) (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